From f91dd22a7c000f4bd89498fef9007900257f87f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:47:33 +0000 Subject: [PATCH 1/6] Initial plan From 0b72556ae3bc137b84f5d91913b863ab791fed8a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:58:26 +0000 Subject: [PATCH 2/6] fix(mcp-server): bind HTTP server to 127.0.0.1 and add validation comment Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/mcp_argument_validation.go | 5 +++++ pkg/cli/mcp_server_http.go | 11 ++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/cli/mcp_argument_validation.go b/pkg/cli/mcp_argument_validation.go index b415a004b2e..a5a4425c250 100644 --- a/pkg/cli/mcp_argument_validation.go +++ b/pkg/cli/mcp_argument_validation.go @@ -77,6 +77,11 @@ func argumentValidationMiddleware(toolParams map[string]toolParamEntry) mcp.Midd toolName := extractMCPToolName(req) validParams, ok := toolParams[toolName] if !ok { + // The tool name is not in the hardcoded registry (mcpToolParams). + // This should not happen in practice because the registry is + // exhaustive, but escalating to a protocol-level error here + // (CodeMethodNotFound) is intentional: returning a tool-level + // result for an unknown tool would silently hide the gap. return nil, newMCPError(jsonrpc.CodeMethodNotFound, fmt.Sprintf("unknown MCP tool: %q", toolName), nil) } diff --git a/pkg/cli/mcp_server_http.go b/pkg/cli/mcp_server_http.go index 9f3e2b4f74d..9bc616861e2 100644 --- a/pkg/cli/mcp_server_http.go +++ b/pkg/cli/mcp_server_http.go @@ -75,15 +75,20 @@ func runHTTPServer(server *mcp.Server, port int) error { handlerWithLogging := loggingHandler(handler) - // Create HTTP server - addr := fmt.Sprintf(":%d", port) + // Bind to loopback only. Since v1.6.0, the SDK no longer enables + // cross-origin protection by default, so binding to 127.0.0.1 (instead + // of all interfaces) is the safest default for a local MCP server — it + // prevents the server from being reachable on non-localhost interfaces. + // The SDK's built-in localhost protection (DisableLocalhostProtection=false) + // additionally rejects requests with non-localhost Host headers. + addr := fmt.Sprintf("127.0.0.1:%d", port) httpServer := &http.Server{ Addr: addr, Handler: handlerWithLogging, ReadHeaderTimeout: MCPServerHTTPTimeout, } - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Starting MCP server on http://localhost"+addr)) + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Starting MCP server on http://localhost:%d", port))) mcpLog.Printf("HTTP server listening on %s", addr) // Run the HTTP server From 3b51710214e6a71ff147bbe7c02d5bf1f19d6867 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:41:32 +0000 Subject: [PATCH 3/6] fix(mcp-server): address review feedback Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/mcp_argument_validation.go | 8 ++++---- pkg/cli/mcp_argument_validation_test.go | 8 ++++---- pkg/cli/mcp_server_http.go | 20 ++++++++++++++------ pkg/cli/mcp_server_http_test.go | 12 ++++++++++++ 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/pkg/cli/mcp_argument_validation.go b/pkg/cli/mcp_argument_validation.go index a5a4425c250..c05a0ce296f 100644 --- a/pkg/cli/mcp_argument_validation.go +++ b/pkg/cli/mcp_argument_validation.go @@ -79,10 +79,10 @@ func argumentValidationMiddleware(toolParams map[string]toolParamEntry) mcp.Midd if !ok { // The tool name is not in the hardcoded registry (mcpToolParams). // This should not happen in practice because the registry is - // exhaustive, but escalating to a protocol-level error here - // (CodeMethodNotFound) is intentional: returning a tool-level - // result for an unknown tool would silently hide the gap. - return nil, newMCPError(jsonrpc.CodeMethodNotFound, fmt.Sprintf("unknown MCP tool: %q", toolName), nil) + // exhaustive. Escalating to a protocol-level internal error + // makes any registry gap fail loudly as a server bug instead of + // surfacing as an ordinary tool-level validation failure. + return nil, newMCPError(jsonrpc.CodeInternalError, fmt.Sprintf("unknown MCP tool: %q", toolName), nil) } mcpArgValidationLog.Printf("Intercepted unknown param error: tool=%s, unknown_params=%v", toolName, unknownParams) diff --git a/pkg/cli/mcp_argument_validation_test.go b/pkg/cli/mcp_argument_validation_test.go index dd10da4c6aa..a783639a239 100644 --- a/pkg/cli/mcp_argument_validation_test.go +++ b/pkg/cli/mcp_argument_validation_test.go @@ -296,9 +296,9 @@ func TestArgumentValidationMiddleware_PassesThroughSuccessResults(t *testing.T) assert.False(t, toolResult.IsError) } -// TestArgumentValidationMiddleware_UnknownToolReturnsMethodNotFound verifies -// that an unregistered tool name is reported as method-not-found. -func TestArgumentValidationMiddleware_UnknownToolReturnsMethodNotFound(t *testing.T) { +// TestArgumentValidationMiddleware_UnknownToolReturnsInternalError verifies +// that an unregistered tool name is reported as an internal server error. +func TestArgumentValidationMiddleware_UnknownToolReturnsInternalError(t *testing.T) { toolParams := map[string]toolParamEntry{ "compile": {"workflows"}, } @@ -318,7 +318,7 @@ func TestArgumentValidationMiddleware_UnknownToolReturnsMethodNotFound(t *testin var rpcErr *jsonrpc.Error require.ErrorAs(t, err, &rpcErr, "error should be a JSON-RPC error") - assert.Equal(t, int64(jsonrpc.CodeMethodNotFound), rpcErr.Code, "unknown tool should use method-not-found code") + assert.Equal(t, int64(jsonrpc.CodeInternalError), rpcErr.Code, "unknown tool should use internal-error code") } // TestArgumentValidationMiddleware_PassesThroughNonToolCallMethods verifies diff --git a/pkg/cli/mcp_server_http.go b/pkg/cli/mcp_server_http.go index 9bc616861e2..f4ea265f917 100644 --- a/pkg/cli/mcp_server_http.go +++ b/pkg/cli/mcp_server_http.go @@ -29,6 +29,14 @@ type responseWriter struct { statusCode int } +func mcpHTTPServerAddr(port int) string { + return fmt.Sprintf("127.0.0.1:%d", port) +} + +func mcpHTTPServerDisplayURL(port int) string { + return fmt.Sprintf("http://127.0.0.1:%d", port) +} + func loggingHandler(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() @@ -69,8 +77,9 @@ func runHTTPServer(server *mcp.Server, port int) error { handler := mcp.NewStreamableHTTPHandler(func(req *http.Request) *mcp.Server { return server }, &mcp.StreamableHTTPOptions{ - SessionTimeout: 2 * time.Hour, // Close idle sessions after 2 hours - Logger: logger.NewSlogLoggerWithHandler(mcpLog), + SessionTimeout: 2 * time.Hour, // Close idle sessions after 2 hours + DisableLocalhostProtection: false, // Keep the SDK's localhost Host-header checks enabled. + Logger: logger.NewSlogLoggerWithHandler(mcpLog), }) handlerWithLogging := loggingHandler(handler) @@ -79,16 +88,15 @@ func runHTTPServer(server *mcp.Server, port int) error { // cross-origin protection by default, so binding to 127.0.0.1 (instead // of all interfaces) is the safest default for a local MCP server — it // prevents the server from being reachable on non-localhost interfaces. - // The SDK's built-in localhost protection (DisableLocalhostProtection=false) - // additionally rejects requests with non-localhost Host headers. - addr := fmt.Sprintf("127.0.0.1:%d", port) + // The SDK's localhost Host-header checks stay enabled as a second layer. + addr := mcpHTTPServerAddr(port) httpServer := &http.Server{ Addr: addr, Handler: handlerWithLogging, ReadHeaderTimeout: MCPServerHTTPTimeout, } - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Starting MCP server on http://localhost:%d", port))) + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Starting MCP server on "+mcpHTTPServerDisplayURL(port))) mcpLog.Printf("HTTP server listening on %s", addr) // Run the HTTP server diff --git a/pkg/cli/mcp_server_http_test.go b/pkg/cli/mcp_server_http_test.go index 8438f8794af..cd4aac221aa 100644 --- a/pkg/cli/mcp_server_http_test.go +++ b/pkg/cli/mcp_server_http_test.go @@ -8,6 +8,18 @@ import ( "testing" ) +func TestMCPHTTPServerAddr_BindsToLoopback(t *testing.T) { + if got := mcpHTTPServerAddr(12345); got != "127.0.0.1:12345" { + t.Fatalf("mcpHTTPServerAddr(12345) = %q, want %q", got, "127.0.0.1:12345") + } +} + +func TestMCPHTTPServerDisplayURL_UsesLoopbackAddress(t *testing.T) { + if got := mcpHTTPServerDisplayURL(12345); got != "http://127.0.0.1:12345" { + t.Fatalf("mcpHTTPServerDisplayURL(12345) = %q, want %q", got, "http://127.0.0.1:12345") + } +} + func TestSanitizeForLog_NoSpecialChars(t *testing.T) { input := "/api/v1/workflows" got := sanitizeForLog(input) From 82f25146fb44f8d50d04fcf3a100775d7cf9b59f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:46:12 +0000 Subject: [PATCH 4/6] test(mcp-server): cover loopback helpers edge ports Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/mcp_server_http_test.go | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/pkg/cli/mcp_server_http_test.go b/pkg/cli/mcp_server_http_test.go index cd4aac221aa..18d34f71b98 100644 --- a/pkg/cli/mcp_server_http_test.go +++ b/pkg/cli/mcp_server_http_test.go @@ -9,14 +9,36 @@ import ( ) func TestMCPHTTPServerAddr_BindsToLoopback(t *testing.T) { - if got := mcpHTTPServerAddr(12345); got != "127.0.0.1:12345" { - t.Fatalf("mcpHTTPServerAddr(12345) = %q, want %q", got, "127.0.0.1:12345") + testCases := []struct { + port int + want string + }{ + {port: 0, want: "127.0.0.1:0"}, + {port: 12345, want: "127.0.0.1:12345"}, + {port: 65535, want: "127.0.0.1:65535"}, + } + + for _, tc := range testCases { + if got := mcpHTTPServerAddr(tc.port); got != tc.want { + t.Fatalf("mcpHTTPServerAddr(%d) = %q, want %q", tc.port, got, tc.want) + } } } func TestMCPHTTPServerDisplayURL_UsesLoopbackAddress(t *testing.T) { - if got := mcpHTTPServerDisplayURL(12345); got != "http://127.0.0.1:12345" { - t.Fatalf("mcpHTTPServerDisplayURL(12345) = %q, want %q", got, "http://127.0.0.1:12345") + testCases := []struct { + port int + want string + }{ + {port: 0, want: "http://127.0.0.1:0"}, + {port: 12345, want: "http://127.0.0.1:12345"}, + {port: 65535, want: "http://127.0.0.1:65535"}, + } + + for _, tc := range testCases { + if got := mcpHTTPServerDisplayURL(tc.port); got != tc.want { + t.Fatalf("mcpHTTPServerDisplayURL(%d) = %q, want %q", tc.port, got, tc.want) + } } } From 23661b965a27ad04e023405de1f35ad0c3b49828 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:50:57 +0000 Subject: [PATCH 5/6] test(mcp-server): keep loopback helper cases running Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/mcp_server_http_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cli/mcp_server_http_test.go b/pkg/cli/mcp_server_http_test.go index 18d34f71b98..539e1a65ed4 100644 --- a/pkg/cli/mcp_server_http_test.go +++ b/pkg/cli/mcp_server_http_test.go @@ -20,7 +20,7 @@ func TestMCPHTTPServerAddr_BindsToLoopback(t *testing.T) { for _, tc := range testCases { if got := mcpHTTPServerAddr(tc.port); got != tc.want { - t.Fatalf("mcpHTTPServerAddr(%d) = %q, want %q", tc.port, got, tc.want) + t.Errorf("mcpHTTPServerAddr(%d) = %q, want %q", tc.port, got, tc.want) } } } @@ -37,7 +37,7 @@ func TestMCPHTTPServerDisplayURL_UsesLoopbackAddress(t *testing.T) { for _, tc := range testCases { if got := mcpHTTPServerDisplayURL(tc.port); got != tc.want { - t.Fatalf("mcpHTTPServerDisplayURL(%d) = %q, want %q", tc.port, got, tc.want) + t.Errorf("mcpHTTPServerDisplayURL(%d) = %q, want %q", tc.port, got, tc.want) } } } From a9c2e2635ef90122671908f7b8351364c4cea0a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 03:24:32 +0000 Subject: [PATCH 6/6] fix(mcp-server): capture explicit HTTP status codes Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/mcp_server_http.go | 5 +++++ pkg/cli/mcp_server_http_test.go | 3 +++ 2 files changed, 8 insertions(+) diff --git a/pkg/cli/mcp_server_http.go b/pkg/cli/mcp_server_http.go index f4ea265f917..faa691657d9 100644 --- a/pkg/cli/mcp_server_http.go +++ b/pkg/cli/mcp_server_http.go @@ -29,6 +29,11 @@ type responseWriter struct { statusCode int } +func (w *responseWriter) WriteHeader(statusCode int) { + w.statusCode = statusCode + w.ResponseWriter.WriteHeader(statusCode) +} + func mcpHTTPServerAddr(port int) string { return fmt.Sprintf("127.0.0.1:%d", port) } diff --git a/pkg/cli/mcp_server_http_test.go b/pkg/cli/mcp_server_http_test.go index 539e1a65ed4..931c089b610 100644 --- a/pkg/cli/mcp_server_http_test.go +++ b/pkg/cli/mcp_server_http_test.go @@ -105,6 +105,9 @@ func TestResponseWriter_EmbeddedResponseWriter(t *testing.T) { if rec.Code != http.StatusCreated { t.Errorf("embedded ResponseWriter code = %d, want %d", rec.Code, http.StatusCreated) } + if rw.statusCode != http.StatusCreated { + t.Errorf("responseWriter statusCode = %d, want %d", rw.statusCode, http.StatusCreated) + } } func TestLoggingHandler_PassesRequestToHandler(t *testing.T) {