From cc0cb109a871ea4a983656cfbc7a6c2bc1ad0706 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Mon, 3 Aug 2026 12:02:21 +0800 Subject: [PATCH] fix(mcp): address SDK migration review Co-Authored-By: OpenAI Codex (GPT-5) --- operation/operation.go | 13 +++++++-- operation/operation_test.go | 16 +++++++++++ operation/sdk_integration_test.go | 47 +++++++++++++++++++++++++------ pkg/tool/tool.go | 1 + 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/operation/operation.go b/operation/operation.go index 5ef1a37..561cd58 100644 --- a/operation/operation.go +++ b/operation/operation.go @@ -41,6 +41,9 @@ const maxRequestBodyBytes = 32 << 20 // session, and initialize takes no token. Clients re-initialize on the 404. const sessionTimeout = 30 * time.Minute +// httpReadHeaderTimeout bounds slow header reads without limiting SSE writes. +const httpReadHeaderTimeout = 10 * time.Second + var ( mcpServer *mcp.Server @@ -111,11 +114,15 @@ func newHTTPServer(addr string, s *mcp.Server) *http.Server { &mcp.StreamableHTTPOptions{ Logger: log.Slog(), MaxRequestBodyBytes: maxRequestBodyBytes, - Stateless: false, // PR 2 switches this on + Stateless: false, // SessionTimeout requires stateful sessions. SessionTimeout: sessionTimeout, }, )) - return &http.Server{Addr: addr, Handler: mux} + return &http.Server{ + Addr: addr, + Handler: mux, + ReadHeaderTimeout: httpReadHeaderTimeout, + } } func Run() error { @@ -157,6 +164,8 @@ func Run() error { } func newMCPServer(version string) *mcp.Server { + // SDK keepalives send MCP ping requests and disconnect clients without a + // server-to-client channel, so KeepAlive stays disabled. s := mcp.NewServer( &mcp.Implementation{ Name: "Gitea MCP Server", diff --git a/operation/operation_test.go b/operation/operation_test.go index 6c97c77..c0fceb4 100644 --- a/operation/operation_test.go +++ b/operation/operation_test.go @@ -2,6 +2,22 @@ package operation import "testing" +func TestNewHTTPServerConfig(t *testing.T) { + server := newHTTPServer(":12345", newMCPServer("test")) + if server.Addr != ":12345" { + t.Errorf("Addr = %q, want %q", server.Addr, ":12345") + } + if server.Handler == nil { + t.Error("Handler is nil") + } + if server.ReadHeaderTimeout != httpReadHeaderTimeout { + t.Errorf("ReadHeaderTimeout = %v, want %v", server.ReadHeaderTimeout, httpReadHeaderTimeout) + } + if server.WriteTimeout != 0 { + t.Errorf("WriteTimeout = %v, want zero for SSE", server.WriteTimeout) + } +} + func TestParseAuthToken(t *testing.T) { tests := []struct { name string diff --git a/operation/sdk_integration_test.go b/operation/sdk_integration_test.go index 20cebfb..f1f52e2 100644 --- a/operation/sdk_integration_test.go +++ b/operation/sdk_integration_test.go @@ -21,7 +21,12 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) -const testServerVersion = "test-version" +// Pin negotiated versions so SDK upgrades require compatibility review. +const ( + testServerVersion = "test-version" + expectedProtocolVersion = "2026-07-28" + expectedStatefulHTTPProtocolVersion = "2025-11-25" +) func exposeAllTools(t *testing.T) { t.Helper() @@ -51,6 +56,21 @@ func registeredToolCount() int { return count } +// stdioCommandEnvironment removes variables that override subprocess flags. +func stdioCommandEnvironment() []string { + environment := os.Environ() + filtered := make([]string, 0, len(environment)) + for _, entry := range environment { + name, _, _ := strings.Cut(entry, "=") + switch name { + case "GITEA_READONLY", "GITEA_SCOPES", "GITEA_TOOLS", "MCP_MODE": + continue + } + filtered = append(filtered, entry) + } + return filtered +} + func textContent(t *testing.T, result *mcp.CallToolResult) string { t.Helper() if len(result.Content) != 1 { @@ -104,8 +124,8 @@ func TestOfficialSDKInMemory(t *testing.T) { if err != nil { t.Fatalf("Connect() error = %v", err) } - if got := session.InitializeResult().ProtocolVersion; got != "2026-07-28" { - t.Errorf("protocol version = %q, want %q", got, "2026-07-28") + if got := session.InitializeResult().ProtocolVersion; got != expectedProtocolVersion { + t.Errorf("protocol version = %q, want %q", got, expectedProtocolVersion) } listAndCallVersion(ctx, t, session, testServerVersion) if err := session.Close(); err != nil { @@ -142,8 +162,9 @@ func TestStreamableHTTPStateful(t *testing.T) { t.Fatalf("Connect() error = %v", err) } defer session.Close() - if got := session.InitializeResult().ProtocolVersion; got != "2025-11-25" { - t.Errorf("protocol version = %q, want %q", got, "2025-11-25") + // Stateful Streamable HTTP cannot negotiate the sessionless 2026 protocol. + if got := session.InitializeResult().ProtocolVersion; got != expectedStatefulHTTPProtocolVersion { + t.Errorf("protocol version = %q, want %q", got, expectedStatefulHTTPProtocolVersion) } listAndCallVersion(ctx, t, session, testServerVersion) @@ -347,19 +368,27 @@ func TestStdioCommandTransport(t *testing.T) { if testing.Short() { t.Skip("skipping subprocess build in short mode") } - exposeAllTools(t) // the subprocess runs with default flags, so match them here + for name, value := range map[string]string{ + "GITEA_READONLY": "true", + "GITEA_SCOPES": "user", + "GITEA_TOOLS": "get_me", + "MCP_MODE": "http", + } { + t.Setenv(name, value) + } + exposeAllTools(t) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() binary := filepath.Join(t.TempDir(), "gitea-mcp") build := exec.CommandContext(ctx, "go", "build", "-o", binary, "..") - build.Env = os.Environ() if output, err := build.CombinedOutput(); err != nil { t.Fatalf("build stdio test binary: %v\n%s", err, output) } client := mcp.NewClient(&mcp.Implementation{Name: "gitea-mcp-stdio-test", Version: "1"}, nil) command := exec.CommandContext(ctx, binary, "--transport", "stdio") + command.Env = stdioCommandEnvironment() session, err := client.Connect(ctx, &mcp.CommandTransport{ Command: command, TerminateDuration: 2 * time.Second, @@ -368,8 +397,8 @@ func TestStdioCommandTransport(t *testing.T) { t.Fatalf("Connect() error = %v", err) } defer session.Close() - if got := session.InitializeResult().ProtocolVersion; got != "2026-07-28" { - t.Errorf("protocol version = %q, want %q", got, "2026-07-28") + if got := session.InitializeResult().ProtocolVersion; got != expectedProtocolVersion { + t.Errorf("protocol version = %q, want %q", got, expectedProtocolVersion) } listAndCallVersion(ctx, t, session, "Gitea MCP Server version:") } diff --git a/pkg/tool/tool.go b/pkg/tool/tool.go index bcd7284..fa52d1c 100644 --- a/pkg/tool/tool.go +++ b/pkg/tool/tool.go @@ -107,6 +107,7 @@ func (s ServerTool) MCPHandler() mcp.ToolHandler { if errors.As(err, &protocolErr) { return nil, err } + // Preserve mcp-go behavior; tool-result errors are a separate change. return nil, internalError(err) } return result, nil