From 3ba0c9170c46d33db02e6741e34dd8f7830c9c7f Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 1 May 2025 09:51:56 -0400 Subject: [PATCH 01/29] feat(sse): Add `SessionWithTools` support to SSEServer (#232) Implement SessionWithTools interface for sseSession to support session-specific tools: - Add tools field to sseSession struct - Implement GetSessionTools and SetSessionTools methods --- server/sse.go | 30 +++++++++++- server/sse_test.go | 113 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/server/sse.go b/server/sse.go index e380d20a..7994c606 100644 --- a/server/sse.go +++ b/server/sse.go @@ -28,6 +28,7 @@ type sseSession struct { requestID atomic.Int64 notificationChannel chan mcp.JSONRPCNotification initialized atomic.Bool + tools sync.Map // stores session-specific tools } // SSEContextFunc is a function that takes an existing context and the current @@ -58,7 +59,34 @@ func (s *sseSession) Initialized() bool { return s.initialized.Load() } -var _ ClientSession = (*sseSession)(nil) +func (s *sseSession) GetSessionTools() map[string]ServerTool { + tools := make(map[string]ServerTool) + s.tools.Range(func(key, value interface{}) bool { + if tool, ok := value.(ServerTool); ok { + tools[key.(string)] = tool + } + return true + }) + return tools +} + +func (s *sseSession) SetSessionTools(tools map[string]ServerTool) { + // Clear existing tools + s.tools.Range(func(key, _ interface{}) bool { + s.tools.Delete(key) + return true + }) + + // Set new tools + for name, tool := range tools { + s.tools.Store(name, tool) + } +} + +var ( + _ ClientSession = (*sseSession)(nil) + _ SessionWithTools = (*sseSession)(nil) +) // SSEServer implements a Server-Sent Events (SSE) based MCP server. // It provides real-time communication capabilities over HTTP using the SSE protocol. diff --git a/server/sse_test.go b/server/sse_test.go index a121581a..75da1eac 100644 --- a/server/sse_test.go +++ b/server/sse_test.go @@ -666,7 +666,8 @@ func TestSSEServer(t *testing.T) { t.Fatalf("Failed to marshal tool request: %v", err) } - req, err := http.NewRequest(http.MethodPost, messageURL, bytes.NewBuffer(requestBody)) + var req *http.Request + req, err = http.NewRequest(http.MethodPost, messageURL, bytes.NewBuffer(requestBody)) if err != nil { t.Fatalf("Failed to create tool request: %v", err) } @@ -1129,6 +1130,116 @@ func TestSSEServer(t *testing.T) { }) } }) + + t.Run("SessionWithTools implementation", func(t *testing.T) { + // Create hooks to track sessions + hooks := &Hooks{} + var registeredSession *sseSession + hooks.AddOnRegisterSession(func(ctx context.Context, session ClientSession) { + if s, ok := session.(*sseSession); ok { + registeredSession = s + } + }) + + mcpServer := NewMCPServer("test", "1.0.0", WithHooks(hooks)) + testServer := NewTestServer(mcpServer) + defer testServer.Close() + + // Connect to SSE endpoint + sseResp, err := http.Get(fmt.Sprintf("%s/sse", testServer.URL)) + if err != nil { + t.Fatalf("Failed to connect to SSE endpoint: %v", err) + } + defer sseResp.Body.Close() + + // Read the endpoint event to ensure session is established + _, err = readSeeEvent(sseResp) + if err != nil { + t.Fatalf("Failed to read SSE response: %v", err) + } + + // Verify we got a session + if registeredSession == nil { + t.Fatal("Session was not registered via hook") + } + + // Test setting and getting tools + tools := map[string]ServerTool{ + "test_tool": { + Tool: mcp.Tool{ + Name: "test_tool", + Description: "A test tool", + Annotations: mcp.ToolAnnotation{ + Title: "Test Tool", + }, + }, + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return mcp.NewToolResultText("test"), nil + }, + }, + } + + // Test SetSessionTools + registeredSession.SetSessionTools(tools) + + // Test GetSessionTools + retrievedTools := registeredSession.GetSessionTools() + if len(retrievedTools) != 1 { + t.Errorf("Expected 1 tool, got %d", len(retrievedTools)) + } + if tool, exists := retrievedTools["test_tool"]; !exists { + t.Error("Expected test_tool to exist") + } else if tool.Tool.Name != "test_tool" { + t.Errorf("Expected tool name test_tool, got %s", tool.Tool.Name) + } + + // Test concurrent access + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(2) + go func(i int) { + defer wg.Done() + tools := map[string]ServerTool{ + fmt.Sprintf("tool_%d", i): { + Tool: mcp.Tool{ + Name: fmt.Sprintf("tool_%d", i), + Description: fmt.Sprintf("Tool %d", i), + Annotations: mcp.ToolAnnotation{ + Title: fmt.Sprintf("Tool %d", i), + }, + }, + }, + } + registeredSession.SetSessionTools(tools) + }(i) + go func() { + defer wg.Done() + _ = registeredSession.GetSessionTools() + }() + } + wg.Wait() + + // Verify we can still get and set tools after concurrent access + finalTools := map[string]ServerTool{ + "final_tool": { + Tool: mcp.Tool{ + Name: "final_tool", + Description: "Final Tool", + Annotations: mcp.ToolAnnotation{ + Title: "Final Tool", + }, + }, + }, + } + registeredSession.SetSessionTools(finalTools) + retrievedTools = registeredSession.GetSessionTools() + if len(retrievedTools) != 1 { + t.Errorf("Expected 1 tool, got %d", len(retrievedTools)) + } + if _, exists := retrievedTools["final_tool"]; !exists { + t.Error("Expected final_tool to exist") + } + }) } func readSeeEvent(sseResp *http.Response) (string, error) { From f3fef81032fde6519525abf64a9f67afdb0b3e38 Mon Sep 17 00:00:00 2001 From: Roman Gelembjuk Date: Fri, 2 May 2025 08:55:46 +0100 Subject: [PATCH 02/29] Fix bug with MarshalJSON for NotificationParams (#233) Co-authored-by: Roman Gelembjuk --- mcp/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp/types.go b/mcp/types.go index c79baae1..516f90b4 100644 --- a/mcp/types.go +++ b/mcp/types.go @@ -132,7 +132,7 @@ type NotificationParams struct { } // MarshalJSON implements custom JSON marshaling -func (p *NotificationParams) MarshalJSON() ([]byte, error) { +func (p NotificationParams) MarshalJSON() ([]byte, error) { // Create a map to hold all fields m := make(map[string]interface{}) From 90bd8779a6895dd5ef1eb6ac9084653d783287ca Mon Sep 17 00:00:00 2001 From: QihengZhou Date: Fri, 2 May 2025 18:34:37 +0800 Subject: [PATCH 03/29] fix: write back error message if the response marshal failed (#235) --- server/sse.go | 2 - server/sse_test.go | 102 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/server/sse.go b/server/sse.go index 7994c606..94dee192 100644 --- a/server/sse.go +++ b/server/sse.go @@ -457,7 +457,6 @@ func (s *SSEServer) handleMessage(w http.ResponseWriter, r *http.Request) { go func() { // Process message through MCPServer response := s.server.HandleMessage(ctx, rawMessage) - // Only send response if there is one (not for notifications) if response != nil { var message string @@ -465,7 +464,6 @@ func (s *SSEServer) handleMessage(w http.ResponseWriter, r *http.Request) { // If there is an error marshalling the response, send a generic error response log.Printf("failed to marshal response: %v", err) message = fmt.Sprintf("event: message\ndata: {\"error\": \"internal error\",\"jsonrpc\": \"2.0\", \"id\": null}\n\n") - return } else { message = fmt.Sprintf("event: message\ndata: %s\n\n", eventData) } diff --git a/server/sse_test.go b/server/sse_test.go index 75da1eac..937dc274 100644 --- a/server/sse_test.go +++ b/server/sse_test.go @@ -62,7 +62,7 @@ func TestSSEServer(t *testing.T) { defer sseResp.Body.Close() // Read the endpoint event - endpointEvent, err := readSeeEvent(sseResp) + endpointEvent, err := readSSEEvent(sseResp) if err != nil { t.Fatalf("Failed to read SSE response: %v", err) } @@ -195,7 +195,7 @@ func TestSSEServer(t *testing.T) { } defer resp.Body.Close() - endpointEvent, err = readSeeEvent(sseResp) + endpointEvent, err = readSSEEvent(sseResp) if err != nil { t.Fatalf("Failed to read SSE response: %v", err) } @@ -590,7 +590,7 @@ func TestSSEServer(t *testing.T) { defer sseResp.Body.Close() // Read the endpoint event - endpointEvent, err := readSeeEvent(sseResp) + endpointEvent, err := readSSEEvent(sseResp) if err != nil { t.Fatalf("Failed to read SSE response: %v", err) } @@ -632,16 +632,16 @@ func TestSSEServer(t *testing.T) { } // Verify response - endpointEvent, err = readSeeEvent(sseResp) + endpointEvent, err = readSSEEvent(sseResp) if err != nil { t.Fatalf("Failed to read SSE response: %v", err) } - respFromSee := strings.TrimSpace( + respFromSSE := strings.TrimSpace( strings.Split(strings.Split(endpointEvent, "data: ")[1], "\n")[0], ) var response map[string]interface{} - if err := json.NewDecoder(strings.NewReader(respFromSee)).Decode(&response); err != nil { + if err := json.NewDecoder(strings.NewReader(respFromSSE)).Decode(&response); err != nil { t.Fatalf("Failed to decode response: %v", err) } @@ -680,17 +680,17 @@ func TestSSEServer(t *testing.T) { } defer resp.Body.Close() - endpointEvent, err = readSeeEvent(sseResp) + endpointEvent, err = readSSEEvent(sseResp) if err != nil { t.Fatalf("Failed to read SSE response: %v", err) } - respFromSee = strings.TrimSpace( + respFromSSE = strings.TrimSpace( strings.Split(strings.Split(endpointEvent, "data: ")[1], "\n")[0], ) response = make(map[string]interface{}) - if err := json.NewDecoder(strings.NewReader(respFromSee)).Decode(&response); err != nil { + if err := json.NewDecoder(strings.NewReader(respFromSSE)).Decode(&response); err != nil { t.Fatalf("Failed to decode response: %v", err) } @@ -1140,7 +1140,7 @@ func TestSSEServer(t *testing.T) { registeredSession = s } }) - + mcpServer := NewMCPServer("test", "1.0.0", WithHooks(hooks)) testServer := NewTestServer(mcpServer) defer testServer.Close() @@ -1153,7 +1153,7 @@ func TestSSEServer(t *testing.T) { defer sseResp.Body.Close() // Read the endpoint event to ensure session is established - _, err = readSeeEvent(sseResp) + _, err = readSSEEvent(sseResp) if err != nil { t.Fatalf("Failed to read SSE response: %v", err) } @@ -1240,9 +1240,87 @@ func TestSSEServer(t *testing.T) { t.Error("Expected final_tool to exist") } }) + + t.Run("TestServerResponseMarshalError", func(t *testing.T) { + mcpServer := NewMCPServer("test", "1.0.0", + WithResourceCapabilities(true, true), + WithHooks(&Hooks{ + OnAfterInitialize: []OnAfterInitializeFunc{ + func(ctx context.Context, id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) { + result.Result.Meta = map[string]interface{}{"invalid": func() {}} // marshal will fail + }, + }, + }), + ) + testServer := NewTestServer(mcpServer) + defer testServer.Close() + + // Connect to SSE endpoint + sseResp, err := http.Get(fmt.Sprintf("%s/sse", testServer.URL)) + if err != nil { + t.Fatalf("Failed to connect to SSE endpoint: %v", err) + } + defer sseResp.Body.Close() + + // Read the endpoint event + endpointEvent, err := readSSEEvent(sseResp) + if err != nil { + t.Fatalf("Failed to read SSE response: %v", err) + } + if !strings.Contains(endpointEvent, "event: endpoint") { + t.Fatalf("Expected endpoint event, got: %s", endpointEvent) + } + + // Extract message endpoint URL + messageURL := strings.TrimSpace( + strings.Split(strings.Split(endpointEvent, "data: ")[1], "\n")[0], + ) + + // Send initialize request + initRequest := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": map[string]interface{}{ + "protocolVersion": "2024-11-05", + "clientInfo": map[string]interface{}{ + "name": "test-client", + "version": "1.0.0", + }, + }, + } + + requestBody, err := json.Marshal(initRequest) + if err != nil { + t.Fatalf("Failed to marshal request: %v", err) + } + + resp, err := http.Post( + messageURL, + "application/json", + bytes.NewBuffer(requestBody), + ) + if err != nil { + t.Fatalf("Failed to send message: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusAccepted { + t.Errorf("Expected status 202, got %d", resp.StatusCode) + } + + endpointEvent, err = readSSEEvent(sseResp) + if err != nil { + t.Fatalf("Failed to read SSE response: %v", err) + } + + if !strings.Contains(endpointEvent, "\"id\": null") { + t.Errorf("Expected id to be null") + } + }) } -func readSeeEvent(sseResp *http.Response) (string, error) { +func readSSEEvent(sseResp *http.Response) (string, error) { buf := make([]byte, 1024) n, err := sseResp.Body.Read(buf) if err != nil { From 6d55e4eb867e1911cda3326c7c863e178df4d645 Mon Sep 17 00:00:00 2001 From: cryo Date: Sun, 4 May 2025 00:59:01 +0800 Subject: [PATCH 04/29] fix(server/sse): potential goroutine leak in Heartbeat sender (#236) --- server/sse.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/sse.go b/server/sse.go index 94dee192..90fde667 100644 --- a/server/sse.go +++ b/server/sse.go @@ -367,7 +367,12 @@ func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) { } messageBytes, _ := json.Marshal(message) pingMsg := fmt.Sprintf("event: message\ndata:%s\n\n", messageBytes) - session.eventQueue <- pingMsg + select { + case session.eventQueue <- pingMsg: + // Message sent successfully + case <-session.done: + return + } case <-session.done: return case <-r.Context().Done(): From 524448985ec3ddcbcfe6680365f81c532cc1fd07 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Sun, 4 May 2025 13:59:15 +0300 Subject: [PATCH 05/29] Fix stdio test compilation issues in CI (#240) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes the test failures in CI by: 1. Using -buildmode=pie flag when compiling test binaries 2. Using os.CreateTemp() for more reliable temporary file creation 3. Verifying binary existence after compilation 4. Fixing variable shadowing issues 🤖 Generated with opencode Co-Authored-By: opencode --- client/stdio_test.go | 23 ++++++++--- client/transport/stdio_test.go | 75 +++++++++++++++++++++++----------- 2 files changed, 70 insertions(+), 28 deletions(-) diff --git a/client/stdio_test.go b/client/stdio_test.go index 7bffa3b2..8c9ff299 100644 --- a/client/stdio_test.go +++ b/client/stdio_test.go @@ -7,7 +7,7 @@ import ( "log/slog" "os" "os/exec" - "path/filepath" + "runtime" "sync" "testing" "time" @@ -19,6 +19,7 @@ func compileTestServer(outputPath string) error { cmd := exec.Command( "go", "build", + "-buildmode=pie", "-o", outputPath, "../testdata/mockstdio_server.go", @@ -33,10 +34,22 @@ func compileTestServer(outputPath string) error { } func TestStdioMCPClient(t *testing.T) { - // Compile mock server - mockServerPath := filepath.Join(os.TempDir(), "mockstdio_server") - if err := compileTestServer(mockServerPath); err != nil { - t.Fatalf("Failed to compile mock server: %v", err) + // Create a temporary file for the mock server + tempFile, err := os.CreateTemp("", "mockstdio_server") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + tempFile.Close() + mockServerPath := tempFile.Name() + + // Add .exe suffix on Windows + if runtime.GOOS == "windows" { + os.Remove(mockServerPath) // Remove the empty file first + mockServerPath += ".exe" + } + + if compileErr := compileTestServer(mockServerPath); compileErr != nil { + t.Fatalf("Failed to compile mock server: %v", compileErr) } defer os.Remove(mockServerPath) diff --git a/client/transport/stdio_test.go b/client/transport/stdio_test.go index aa728ec6..53db7a0f 100644 --- a/client/transport/stdio_test.go +++ b/client/transport/stdio_test.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "os/exec" - "path/filepath" "runtime" "sync" "testing" @@ -19,6 +18,7 @@ func compileTestServer(outputPath string) error { cmd := exec.Command( "go", "build", + "-buildmode=pie", "-o", outputPath, "../../testdata/mockstdio_server.go", @@ -26,18 +26,30 @@ func compileTestServer(outputPath string) error { if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("compilation failed: %v\nOutput: %s", err, output) } + // Verify the binary was actually created + if _, err := os.Stat(outputPath); os.IsNotExist(err) { + return fmt.Errorf("mock server binary not found at %s after compilation", outputPath) + } return nil } func TestStdio(t *testing.T) { - // Compile mock server - mockServerPath := filepath.Join(os.TempDir(), "mockstdio_server") + // Create a temporary file for the mock server + tempFile, err := os.CreateTemp("", "mockstdio_server") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + tempFile.Close() + mockServerPath := tempFile.Name() + // Add .exe suffix on Windows if runtime.GOOS == "windows" { + os.Remove(mockServerPath) // Remove the empty file first mockServerPath += ".exe" } - if err := compileTestServer(mockServerPath); err != nil { - t.Fatalf("Failed to compile mock server: %v", err) + + if compileErr := compileTestServer(mockServerPath); compileErr != nil { + t.Fatalf("Failed to compile mock server: %v", compileErr) } defer os.Remove(mockServerPath) @@ -48,9 +60,9 @@ func TestStdio(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - err := stdio.Start(ctx) - if err != nil { - t.Fatalf("Failed to start Stdio transport: %v", err) + startErr := stdio.Start(ctx) + if startErr != nil { + t.Fatalf("Failed to start Stdio transport: %v", startErr) } defer stdio.Close() @@ -307,13 +319,22 @@ func TestStdioErrors(t *testing.T) { }) t.Run("RequestBeforeStart", func(t *testing.T) { - mockServerPath := filepath.Join(os.TempDir(), "mockstdio_server") + // Create a temporary file for the mock server + tempFile, err := os.CreateTemp("", "mockstdio_server") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + tempFile.Close() + mockServerPath := tempFile.Name() + // Add .exe suffix on Windows if runtime.GOOS == "windows" { + os.Remove(mockServerPath) // Remove the empty file first mockServerPath += ".exe" } - if err := compileTestServer(mockServerPath); err != nil { - t.Fatalf("Failed to compile mock server: %v", err) + + if compileErr := compileTestServer(mockServerPath); compileErr != nil { + t.Fatalf("Failed to compile mock server: %v", compileErr) } defer os.Remove(mockServerPath) @@ -328,23 +349,31 @@ func TestStdioErrors(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel() - _, err := uninitiatedStdio.SendRequest(ctx, request) - if err == nil { + _, reqErr := uninitiatedStdio.SendRequest(ctx, request) + if reqErr == nil { t.Errorf("Expected SendRequest to panic before Start(), but it didn't") - } else if err.Error() != "stdio client not started" { - t.Errorf("Expected error 'stdio client not started', got: %v", err) + } else if reqErr.Error() != "stdio client not started" { + t.Errorf("Expected error 'stdio client not started', got: %v", reqErr) } }) t.Run("RequestAfterClose", func(t *testing.T) { - // Compile mock server - mockServerPath := filepath.Join(os.TempDir(), "mockstdio_server") + // Create a temporary file for the mock server + tempFile, err := os.CreateTemp("", "mockstdio_server") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + tempFile.Close() + mockServerPath := tempFile.Name() + // Add .exe suffix on Windows if runtime.GOOS == "windows" { + os.Remove(mockServerPath) // Remove the empty file first mockServerPath += ".exe" } - if err := compileTestServer(mockServerPath); err != nil { - t.Fatalf("Failed to compile mock server: %v", err) + + if compileErr := compileTestServer(mockServerPath); compileErr != nil { + t.Fatalf("Failed to compile mock server: %v", compileErr) } defer os.Remove(mockServerPath) @@ -353,8 +382,8 @@ func TestStdioErrors(t *testing.T) { // Start the transport ctx := context.Background() - if err := stdio.Start(ctx); err != nil { - t.Fatalf("Failed to start Stdio transport: %v", err) + if startErr := stdio.Start(ctx); startErr != nil { + t.Fatalf("Failed to start Stdio transport: %v", startErr) } // Close the transport - ignore errors like "broken pipe" since the process might exit already @@ -370,8 +399,8 @@ func TestStdioErrors(t *testing.T) { Method: "ping", } - _, err := stdio.SendRequest(ctx, request) - if err == nil { + _, sendErr := stdio.SendRequest(ctx, request) + if sendErr == nil { t.Errorf("Expected error when sending request after close, got nil") } }) From 2f24f3f146cd006eee8e75112ce631168a3efb9c Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 4 May 2025 11:37:10 -0400 Subject: [PATCH 06/29] refactor(server/sse): rename WithBasePath to WithStaticBasePath for clarity (#238) The new name makes its relationship to `WithDynamicBasePath` clearer. The implementation preserves the original functionality with a build time warning (in go 1.21+). --- server/sse.go | 13 +++++++++++-- server/sse_test.go | 6 +++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/server/sse.go b/server/sse.go index 90fde667..8467b02f 100644 --- a/server/sse.go +++ b/server/sse.go @@ -135,13 +135,22 @@ func WithBaseURL(baseURL string) SSEOption { } } -// WithBasePath adds a new option for setting a static base path -func WithBasePath(basePath string) SSEOption { +// WithStaticBasePath adds a new option for setting a static base path +func WithStaticBasePath(basePath string) SSEOption { return func(s *SSEServer) { s.basePath = normalizeURLPath(basePath) } } +// WithBasePath adds a new option for setting a static base path. +// +// Deprecated: Use WithStaticBasePath instead. This will be removed in a future version. +// +//go:deprecated +func WithBasePath(basePath string) SSEOption { + return WithStaticBasePath(basePath) +} + // WithDynamicBasePath accepts a function for generating the base path. This is // useful for cases where the base path is not known at the time of SSE server // creation, such as when using a reverse proxy or when the server is mounted diff --git a/server/sse_test.go b/server/sse_test.go index 937dc274..9196c8fe 100644 --- a/server/sse_test.go +++ b/server/sse_test.go @@ -24,7 +24,7 @@ func TestSSEServer(t *testing.T) { mcpServer := NewMCPServer("test", "1.0.0") sseServer := NewSSEServer(mcpServer, WithBaseURL("http://localhost:8080"), - WithBasePath("/mcp"), + WithStaticBasePath("/mcp"), ) if sseServer == nil { @@ -499,7 +499,7 @@ func TestSSEServer(t *testing.T) { t.Run("works as http.Handler with custom basePath", func(t *testing.T) { mcpServer := NewMCPServer("test", "1.0.0") - sseServer := NewSSEServer(mcpServer, WithBasePath("/mcp")) + sseServer := NewSSEServer(mcpServer, WithStaticBasePath("/mcp")) ts := httptest.NewServer(sseServer) defer ts.Close() @@ -717,7 +717,7 @@ func TestSSEServer(t *testing.T) { useFullURLForMessageEndpoint := false srv := &http.Server{} rands := []SSEOption{ - WithBasePath(basePath), + WithStaticBasePath(basePath), WithBaseURL(baseURL), WithMessageEndpoint(messageEndpoint), WithUseFullURLForMessageEndpoint(useFullURLForMessageEndpoint), From e40e7a79aebe51278fac8ebe9cd5faeb051d2e0a Mon Sep 17 00:00:00 2001 From: cryo Date: Sun, 4 May 2025 23:38:00 +0800 Subject: [PATCH 07/29] fix(MCPServer): Session tool handler not used due to variable shadowing (#242) --- server/server.go | 2 +- server/session_test.go | 59 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/server/server.go b/server/server.go index 95831ebd..8aac05ca 100644 --- a/server/server.go +++ b/server/server.go @@ -856,7 +856,7 @@ func (s *MCPServer) handleToolCall( session := ClientSessionFromContext(ctx) if session != nil { - if sessionWithTools, ok := session.(SessionWithTools); ok { + if sessionWithTools, typeAssertOk := session.(SessionWithTools); typeAssertOk { if sessionTools := sessionWithTools.GetSessionTools(); sessionTools != nil { var sessionOk bool tool, sessionOk = sessionTools[request.Params.Name] diff --git a/server/session_test.go b/server/session_test.go index d1d0bc79..42def221 100644 --- a/server/session_test.go +++ b/server/session_test.go @@ -2,6 +2,7 @@ package server import ( "context" + "encoding/json" "errors" "sync" "testing" @@ -295,6 +296,64 @@ func TestMCPServer_AddSessionTool(t *testing.T) { assert.Contains(t, session.GetSessionTools(), "session-tool-helper") } +func TestMCPServer_CallSessionTool(t *testing.T) { + server := NewMCPServer("test-server", "1.0.0", WithToolCapabilities(true)) + + // Add global tool + server.AddTool(mcp.NewTool("test_tool"), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return mcp.NewToolResultText("global result"), nil + }) + + // Create a session + sessionChan := make(chan mcp.JSONRPCNotification, 10) + session := &sessionTestClientWithTools{ + sessionID: "session-1", + notificationChannel: sessionChan, + initialized: true, + } + + // Register the session + err := server.RegisterSession(context.Background(), session) + require.NoError(t, err) + + // Add session-specific tool with the same name to override the global tool + err = server.AddSessionTool( + session.SessionID(), + mcp.NewTool("test_tool"), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return mcp.NewToolResultText("session result"), nil + }, + ) + require.NoError(t, err) + + // Call the tool using session context + sessionCtx := server.WithContext(context.Background(), session) + toolRequest := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": map[string]interface{}{ + "name": "test_tool", + }, + } + requestBytes, err := json.Marshal(toolRequest) + if err != nil { + t.Fatalf("Failed to marshal tool request: %v", err) + } + + response := server.HandleMessage(sessionCtx, requestBytes) + resp, ok := response.(mcp.JSONRPCResponse) + assert.True(t, ok) + + callToolResult, ok := resp.Result.(mcp.CallToolResult) + assert.True(t, ok) + + // Since we specify a tool with the same name for current session, the expected text should be "session result" + if text := callToolResult.Content[0].(mcp.TextContent).Text; text != "session result" { + t.Errorf("Expected result 'session result', got %q", text) + } +} + func TestMCPServer_DeleteSessionTools(t *testing.T) { server := NewMCPServer("test-server", "1.0.0", WithToolCapabilities(true)) ctx := context.Background() From a999079f3650486677e7698963fb2505b7bdda87 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 4 May 2025 11:38:40 -0400 Subject: [PATCH 08/29] test: build mockstdio_server with isolated cache to prevent flaky CI (#241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI occasionally failed with the linker error: /link: cannot open file DO NOT USE - main build pseudo-cache built This is most likely because several parallel `go build` invocations shared the same `$GOCACHE`, letting one job evict the object file another job had promised the linker. The placeholder path then leaked through and the build aborted. This gives each compile its own cache by setting `GOCACHE=$(mktemp -d)` for the helper’s `go build` call. After these changes `go test ./... -race` passed 100/100 consecutive runs locally. --- client/stdio_test.go | 4 ++++ client/transport/stdio_test.go | 3 +++ 2 files changed, 7 insertions(+) diff --git a/client/stdio_test.go b/client/stdio_test.go index 8c9ff299..fe4e3b5a 100644 --- a/client/stdio_test.go +++ b/client/stdio_test.go @@ -24,9 +24,13 @@ func compileTestServer(outputPath string) error { outputPath, "../testdata/mockstdio_server.go", ) + tmpCache, _ := os.MkdirTemp("", "gocache") + cmd.Env = append(os.Environ(), "GOCACHE="+tmpCache) + if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("compilation failed: %v\nOutput: %s", err, output) } + // Verify the binary was actually created if _, err := os.Stat(outputPath); os.IsNotExist(err) { return fmt.Errorf("mock server binary not found at %s after compilation", outputPath) } diff --git a/client/transport/stdio_test.go b/client/transport/stdio_test.go index 53db7a0f..6d87cdbd 100644 --- a/client/transport/stdio_test.go +++ b/client/transport/stdio_test.go @@ -23,6 +23,9 @@ func compileTestServer(outputPath string) error { outputPath, "../../testdata/mockstdio_server.go", ) + tmpCache, _ := os.MkdirTemp("", "gocache") + cmd.Env = append(os.Environ(), "GOCACHE="+tmpCache) + if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("compilation failed: %v\nOutput: %s", err, output) } From f47e2bce1b69c409bf262f18f6ecba8039ee7fb3 Mon Sep 17 00:00:00 2001 From: Yashwanth <53632453+yash025@users.noreply.github.com> Date: Mon, 5 May 2025 14:42:48 +0530 Subject: [PATCH 09/29] fix: Use detached context for SSE message handling (#244) * fix: Use detached context for SSE message handling Prevents premature cancellation of message processing when HTTP request ends. * test for message processing when we return early to the client * rename variable --------- Co-authored-by: Yashwanth H L --- server/sse.go | 13 +++++-- server/sse_test.go | 84 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/server/sse.go b/server/sse.go index 8467b02f..018657e6 100644 --- a/server/sse.go +++ b/server/sse.go @@ -465,10 +465,19 @@ func (s *SSEServer) handleMessage(w http.ResponseWriter, r *http.Request) { return } + // Create a context that preserves all values from parent ctx but won't be canceled when the parent is canceled. + // this is required because the http ctx will be canceled when the client disconnects + detachedCtx := context.WithoutCancel(ctx) + // quick return request, send 202 Accepted with no body, then deal the message and sent response via SSE w.WriteHeader(http.StatusAccepted) - go func() { + // Create a new context for handling the message that will be canceled when the message handling is done + messageCtx, cancel := context.WithCancel(detachedCtx) + + go func(ctx context.Context) { + defer cancel() + // Use the context that will be canceled when session is done // Process message through MCPServer response := s.server.HandleMessage(ctx, rawMessage) // Only send response if there is one (not for notifications) @@ -493,7 +502,7 @@ func (s *SSEServer) handleMessage(w http.ResponseWriter, r *http.Request) { log.Printf("Event queue full for session %s", sessionID) } } - }() + }(messageCtx) } // writeJSONRPCError writes a JSON-RPC error response with the given error details. diff --git a/server/sse_test.go b/server/sse_test.go index 9196c8fe..393a70cf 100644 --- a/server/sse_test.go +++ b/server/sse_test.go @@ -203,7 +203,6 @@ func TestSSEServer(t *testing.T) { strings.Split(strings.Split(endpointEvent, "data: ")[1], "\n")[0], ) - fmt.Printf("========> %v", respFromSee) var response map[string]interface{} if err := json.NewDecoder(strings.NewReader(respFromSee)).Decode(&response); err != nil { t.Errorf( @@ -1318,6 +1317,89 @@ func TestSSEServer(t *testing.T) { t.Errorf("Expected id to be null") } }) + + t.Run("Message processing continues after we return back result to client", func(t *testing.T) { + mcpServer := NewMCPServer("test", "1.0.0") + + processingCompleted := make(chan struct{}) + processingStarted := make(chan struct{}) + + mcpServer.AddTool(mcp.NewTool("slowMethod"), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + close(processingStarted) // signal for processing started + + select { + case <-ctx.Done(): // If this happens, the test will fail because processingCompleted won't be closed + return nil, fmt.Errorf("context was canceled") + case <-time.After(1 * time.Second): // Simulate processing time + // Successfully completed processing, now close the completed channel to signal completion + close(processingCompleted) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: "success", + }, + }, + }, nil + } + }) + + testServer := NewTestServer(mcpServer) + defer testServer.Close() + + sseResp, err := http.Get(fmt.Sprintf("%s/sse", testServer.URL)) + require.NoError(t, err, "Failed to connect to SSE endpoint") + defer sseResp.Body.Close() + + endpointEvent, err := readSSEEvent(sseResp) + require.NoError(t, err, "Failed to read SSE response") + require.Contains(t, endpointEvent, "event: endpoint", "Expected endpoint event") + + messageURL := strings.TrimSpace( + strings.Split(strings.Split(endpointEvent, "data: ")[1], "\n")[0], + ) + + messageRequest := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": map[string]interface{}{ + "name": "slowMethod", + "parameters": map[string]interface{}{}, + }, + } + + requestBody, err := json.Marshal(messageRequest) + require.NoError(t, err, "Failed to marshal request") + + ctx, cancel := context.WithCancel(context.Background()) + req, err := http.NewRequestWithContext(ctx, "POST", messageURL, bytes.NewBuffer(requestBody)) + require.NoError(t, err, "Failed to create request") + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err, "Failed to send message") + defer resp.Body.Close() + + require.Equal(t, http.StatusAccepted, resp.StatusCode, "Expected status 202 Accepted") + + // Wait for processing to start + select { + case <-processingStarted: // Processing has started, now cancel the client context to simulate disconnection + case <-time.After(2 * time.Second): + t.Fatal("Timed out waiting for processing to start") + } + + cancel() // cancel the client context to simulate disconnection + + // wait for processing to complete, if the test passes, it means the processing continued despite client disconnection + select { + case <-processingCompleted: + case <-time.After(2 * time.Second): + t.Fatal("Processing did not complete after client disconnection") + } + }) } func readSSEEvent(sseResp *http.Response) (string, error) { From 9d6b793133b9b56a25152083a0d5fcfd92d59882 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 6 May 2025 19:36:54 +0300 Subject: [PATCH 10/29] Format --- client/stdio_test.go | 4 ++-- client/transport/stdio_test.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/stdio_test.go b/client/stdio_test.go index fe4e3b5a..48514d91 100644 --- a/client/stdio_test.go +++ b/client/stdio_test.go @@ -45,13 +45,13 @@ func TestStdioMCPClient(t *testing.T) { } tempFile.Close() mockServerPath := tempFile.Name() - + // Add .exe suffix on Windows if runtime.GOOS == "windows" { os.Remove(mockServerPath) // Remove the empty file first mockServerPath += ".exe" } - + if compileErr := compileTestServer(mockServerPath); compileErr != nil { t.Fatalf("Failed to compile mock server: %v", compileErr) } diff --git a/client/transport/stdio_test.go b/client/transport/stdio_test.go index 6d87cdbd..cb25bf79 100644 --- a/client/transport/stdio_test.go +++ b/client/transport/stdio_test.go @@ -44,13 +44,13 @@ func TestStdio(t *testing.T) { } tempFile.Close() mockServerPath := tempFile.Name() - + // Add .exe suffix on Windows if runtime.GOOS == "windows" { os.Remove(mockServerPath) // Remove the empty file first mockServerPath += ".exe" } - + if compileErr := compileTestServer(mockServerPath); compileErr != nil { t.Fatalf("Failed to compile mock server: %v", compileErr) } @@ -329,13 +329,13 @@ func TestStdioErrors(t *testing.T) { } tempFile.Close() mockServerPath := tempFile.Name() - + // Add .exe suffix on Windows if runtime.GOOS == "windows" { os.Remove(mockServerPath) // Remove the empty file first mockServerPath += ".exe" } - + if compileErr := compileTestServer(mockServerPath); compileErr != nil { t.Fatalf("Failed to compile mock server: %v", compileErr) } @@ -368,13 +368,13 @@ func TestStdioErrors(t *testing.T) { } tempFile.Close() mockServerPath := tempFile.Name() - + // Add .exe suffix on Windows if runtime.GOOS == "windows" { os.Remove(mockServerPath) // Remove the empty file first mockServerPath += ".exe" } - + if compileErr := compileTestServer(mockServerPath); compileErr != nil { t.Fatalf("Failed to compile mock server: %v", compileErr) } From cb632f2a894a54673c73d9ab9842ff6ca6dd35ea Mon Sep 17 00:00:00 2001 From: dugenkui Date: Thu, 8 May 2025 19:33:44 +0800 Subject: [PATCH 11/29] support audio content type (#250) --- client/inprocess_test.go | 24 ++++++++++++++++++++---- mcp/prompts.go | 2 +- mcp/tools.go | 2 +- mcp/types.go | 15 ++++++++++++++- mcp/utils.go | 40 +++++++++++++++++++++++++++++++++++++++- 5 files changed, 75 insertions(+), 8 deletions(-) diff --git a/client/inprocess_test.go b/client/inprocess_test.go index de447602..71f86a48 100644 --- a/client/inprocess_test.go +++ b/client/inprocess_test.go @@ -36,6 +36,11 @@ func TestInProcessMCPClient(t *testing.T) { Type: "text", Text: "Input parameter: " + request.Params.Arguments["parameter-1"].(string), }, + mcp.AudioContent{ + Type: "audio", + Data: "base64-encoded-audio-data", + MIMEType: "audio/wav", + }, }, }, nil }) @@ -77,6 +82,14 @@ func TestInProcessMCPClient(t *testing.T) { Text: "Test prompt with arg1: " + request.Params.Arguments["arg1"], }, }, + { + Role: mcp.RoleUser, + Content: mcp.AudioContent{ + Type: "audio", + Data: "base64-encoded-audio-data", + MIMEType: "audio/wav", + }, + }, }, }, nil }, @@ -192,8 +205,8 @@ func TestInProcessMCPClient(t *testing.T) { t.Fatalf("CallTool failed: %v", err) } - if len(result.Content) != 1 { - t.Errorf("Expected 1 content item, got %d", len(result.Content)) + if len(result.Content) != 2 { + t.Errorf("Expected 2 content item, got %d", len(result.Content)) } }) @@ -359,14 +372,17 @@ func TestInProcessMCPClient(t *testing.T) { request := mcp.GetPromptRequest{} request.Params.Name = "test-prompt" + request.Params.Arguments = map[string]string{ + "arg1": "arg1 value", + } result, err := client.GetPrompt(context.Background(), request) if err != nil { t.Errorf("GetPrompt failed: %v", err) } - if len(result.Messages) != 1 { - t.Errorf("Expected 1 message, got %d", len(result.Messages)) + if len(result.Messages) != 2 { + t.Errorf("Expected 2 message, got %d", len(result.Messages)) } }) diff --git a/mcp/prompts.go b/mcp/prompts.go index bc12a729..1309cc5c 100644 --- a/mcp/prompts.go +++ b/mcp/prompts.go @@ -78,7 +78,7 @@ const ( // resources from the MCP server. type PromptMessage struct { Role Role `json:"role"` - Content Content `json:"content"` // Can be TextContent, ImageContent, or EmbeddedResource + Content Content `json:"content"` // Can be TextContent, ImageContent, AudioContent or EmbeddedResource } // PromptListChangedNotification is an optional notification from the server diff --git a/mcp/tools.go b/mcp/tools.go index d4fde482..f92c3388 100644 --- a/mcp/tools.go +++ b/mcp/tools.go @@ -33,7 +33,7 @@ type ListToolsResult struct { // should be reported as an MCP error response. type CallToolResult struct { Result - Content []Content `json:"content"` // Can be TextContent, ImageContent, or EmbeddedResource + Content []Content `json:"content"` // Can be TextContent, ImageContent, AudioContent, or EmbeddedResource // Whether the tool call ended in an error. // // If not set, this is assumed to be false (the call was successful). diff --git a/mcp/types.go b/mcp/types.go index 516f90b4..53cc3283 100644 --- a/mcp/types.go +++ b/mcp/types.go @@ -656,7 +656,7 @@ type CreateMessageResult struct { // SamplingMessage describes a message issued to or received from an LLM API. type SamplingMessage struct { Role Role `json:"role"` - Content interface{} `json:"content"` // Can be TextContent or ImageContent + Content interface{} `json:"content"` // Can be TextContent, ImageContent or AudioContent } type Annotations struct { @@ -709,6 +709,19 @@ type ImageContent struct { func (ImageContent) isContent() {} +// AudioContent represents the contents of audio, embedded into a prompt or tool call result. +// It must have Type set to "audio". +type AudioContent struct { + Annotated + Type string `json:"type"` // Must be "audio" + // The base64-encoded audio data. + Data string `json:"data"` + // The MIME type of the audio. Different providers may support different audio types. + MIMEType string `json:"mimeType"` +} + +func (AudioContent) isContent() {} + // EmbeddedResource represents the contents of a resource, embedded into a prompt or tool call result. // // It is up to the client how best to render embedded resources for the diff --git a/mcp/utils.go b/mcp/utils.go index 250357fc..02f12812 100644 --- a/mcp/utils.go +++ b/mcp/utils.go @@ -78,6 +78,11 @@ func AsImageContent(content interface{}) (*ImageContent, bool) { return asType[ImageContent](content) } +// AsAudioContent attempts to cast the given interface to AudioContent +func AsAudioContent(content interface{}) (*AudioContent, bool) { + return asType[AudioContent](content) +} + // AsEmbeddedResource attempts to cast the given interface to EmbeddedResource func AsEmbeddedResource(content interface{}) (*EmbeddedResource, bool) { return asType[EmbeddedResource](content) @@ -208,7 +213,15 @@ func NewImageContent(data, mimeType string) ImageContent { } } -// NewEmbeddedResource +// Helper function to create a new AudioContent +func NewAudioContent(data, mimeType string) AudioContent { + return AudioContent{ + Type: "audio", + Data: data, + MIMEType: mimeType, + } +} + // Helper function to create a new EmbeddedResource func NewEmbeddedResource(resource ResourceContents) EmbeddedResource { return EmbeddedResource{ @@ -246,6 +259,23 @@ func NewToolResultImage(text, imageData, mimeType string) *CallToolResult { } } +// NewToolResultAudio creates a new CallToolResult with both text and audio content +func NewToolResultAudio(text, imageData, mimeType string) *CallToolResult { + return &CallToolResult{ + Content: []Content{ + TextContent{ + Type: "text", + Text: text, + }, + AudioContent{ + Type: "audio", + Data: imageData, + MIMEType: mimeType, + }, + }, + } +} + // NewToolResultResource creates a new CallToolResult with an embedded resource func NewToolResultResource( text string, @@ -423,6 +453,14 @@ func ParseContent(contentMap map[string]any) (Content, error) { } return NewImageContent(data, mimeType), nil + case "audio": + data := ExtractString(contentMap, "data") + mimeType := ExtractString(contentMap, "mimeType") + if data == "" || mimeType == "" { + return nil, fmt.Errorf("audio data or mimeType is missing") + } + return NewAudioContent(data, mimeType), nil + case "resource": resourceMap := ExtractMap(contentMap, "resource") if resourceMap == nil { From dd1e1e8604d855556219042748c014553a2d1ac2 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 8 May 2025 07:42:01 -0400 Subject: [PATCH 12/29] refactor(server): extract shared HTTP transport configuration options (#253) Create a common interface and pattern for HTTP transport configuration to enable code sharing between SSEServer and the upcoming StreamableHTTPServer. - Add new httpTransportConfigurable interface for shared configuration - Refactor SSEServer to implement the shared interface - Convert With* option functions to work with both server types - Add stub for StreamableHTTPServer to demonstrate implementation pattern - Deprecate WithSSEContextFunc in favor of WithHTTPContextFunc This change preserves backward compatibility while allowing the reuse of configuration code across different HTTP server implementations. --- server/http_transport_options.go | 189 +++++++++++++++++++++++++++++++ server/sse.go | 131 ++++++++------------- 2 files changed, 237 insertions(+), 83 deletions(-) create mode 100644 server/http_transport_options.go diff --git a/server/http_transport_options.go b/server/http_transport_options.go new file mode 100644 index 00000000..91dd875d --- /dev/null +++ b/server/http_transport_options.go @@ -0,0 +1,189 @@ +package server + +import ( + "context" + "net/http" + "net/url" + "strings" + "time" +) + +// HTTPContextFunc is a function that takes an existing context and the current +// request and returns a potentially modified context based on the request +// content. This can be used to inject context values from headers, for example. +type HTTPContextFunc func(ctx context.Context, r *http.Request) context.Context + +// httpTransportConfigurable is an internal interface for shared HTTP transport configuration. +type httpTransportConfigurable interface { + setBasePath(string) + setDynamicBasePath(DynamicBasePathFunc) + setKeepAliveInterval(time.Duration) + setKeepAlive(bool) + setContextFunc(HTTPContextFunc) + setHTTPServer(*http.Server) + setBaseURL(string) +} + +// HTTPTransportOption is a function that configures an httpTransportConfigurable. +type HTTPTransportOption func(httpTransportConfigurable) + +// Option interfaces and wrappers for server configuration +// Base option interface +type HTTPServerOption interface { + isHTTPServerOption() +} + +// SSE-specific option interface +type SSEOption interface { + HTTPServerOption + applyToSSE(*SSEServer) +} + +// StreamableHTTP-specific option interface +type StreamableHTTPOption interface { + HTTPServerOption + applyToStreamableHTTP(*StreamableHTTPServer) +} + +// Common options that work with both server types +type CommonHTTPServerOption interface { + SSEOption + StreamableHTTPOption +} + +// Wrapper for SSE-specific functional options +type sseOption func(*SSEServer) + +func (o sseOption) isHTTPServerOption() {} +func (o sseOption) applyToSSE(s *SSEServer) { o(s) } + +// Wrapper for StreamableHTTP-specific functional options +type streamableHTTPOption func(*StreamableHTTPServer) + +func (o streamableHTTPOption) isHTTPServerOption() {} +func (o streamableHTTPOption) applyToStreamableHTTP(s *StreamableHTTPServer) { o(s) } + +// Refactor commonOption to use a single apply func(httpTransportConfigurable) +type commonOption struct { + apply func(httpTransportConfigurable) +} + +func (o commonOption) isHTTPServerOption() {} +func (o commonOption) applyToSSE(s *SSEServer) { o.apply(s) } +func (o commonOption) applyToStreamableHTTP(s *StreamableHTTPServer) { o.apply(s) } + +// TODO: This is a stub implementation of StreamableHTTPServer just to show how +// to use it with the new options interfaces. +type StreamableHTTPServer struct{} + +// Add stub methods to satisfy httpTransportConfigurable + +func (s *StreamableHTTPServer) setBasePath(string) {} +func (s *StreamableHTTPServer) setDynamicBasePath(DynamicBasePathFunc) {} +func (s *StreamableHTTPServer) setKeepAliveInterval(time.Duration) {} +func (s *StreamableHTTPServer) setKeepAlive(bool) {} +func (s *StreamableHTTPServer) setContextFunc(HTTPContextFunc) {} +func (s *StreamableHTTPServer) setHTTPServer(srv *http.Server) {} +func (s *StreamableHTTPServer) setBaseURL(baseURL string) {} + +// Ensure the option types implement the correct interfaces +var ( + _ httpTransportConfigurable = (*StreamableHTTPServer)(nil) + _ SSEOption = sseOption(nil) + _ StreamableHTTPOption = streamableHTTPOption(nil) + _ CommonHTTPServerOption = commonOption{} +) + +// WithStaticBasePath adds a new option for setting a static base path. +// This is useful for mounting the server at a known, fixed path. +func WithStaticBasePath(basePath string) CommonHTTPServerOption { + return commonOption{ + apply: func(c httpTransportConfigurable) { + c.setBasePath(basePath) + }, + } +} + +// DynamicBasePathFunc allows the user to provide a function to generate the +// base path for a given request and sessionID. This is useful for cases where +// the base path is not known at the time of SSE server creation, such as when +// using a reverse proxy or when the base path is dynamically generated. The +// function should return the base path (e.g., "/mcp/tenant123"). +type DynamicBasePathFunc func(r *http.Request, sessionID string) string + +// WithDynamicBasePath accepts a function for generating the base path. +// This is useful for cases where the base path is not known at the time of server creation, +// such as when using a reverse proxy or when the server is mounted at a dynamic path. +func WithDynamicBasePath(fn DynamicBasePathFunc) CommonHTTPServerOption { + return commonOption{ + apply: func(c httpTransportConfigurable) { + c.setDynamicBasePath(fn) + }, + } +} + +// WithKeepAliveInterval sets the keep-alive interval for the transport. +// When enabled, the server will periodically send ping events to keep the connection alive. +func WithKeepAliveInterval(interval time.Duration) CommonHTTPServerOption { + return commonOption{ + apply: func(c httpTransportConfigurable) { + c.setKeepAliveInterval(interval) + }, + } +} + +// WithKeepAlive enables or disables keep-alive for the transport. +// When enabled, the server will send periodic keep-alive events to clients. +func WithKeepAlive(keepAlive bool) CommonHTTPServerOption { + return commonOption{ + apply: func(c httpTransportConfigurable) { + c.setKeepAlive(keepAlive) + }, + } +} + +// WithHTTPContextFunc sets a function that will be called to customize the context +// for the server using the incoming request. This is useful for injecting +// context values from headers or other request properties. +func WithHTTPContextFunc(fn HTTPContextFunc) CommonHTTPServerOption { + return commonOption{ + apply: func(c httpTransportConfigurable) { + c.setContextFunc(fn) + }, + } +} + +// WithBaseURL sets the base URL for the HTTP transport server. +// This is useful for configuring the externally visible base URL for clients. +func WithBaseURL(baseURL string) CommonHTTPServerOption { + return commonOption{ + apply: func(c httpTransportConfigurable) { + if baseURL != "" { + u, err := url.Parse(baseURL) + if err != nil { + return + } + if u.Scheme != "http" && u.Scheme != "https" { + return + } + if u.Host == "" || strings.HasPrefix(u.Host, ":") { + return + } + if len(u.Query()) > 0 { + return + } + } + c.setBaseURL(strings.TrimSuffix(baseURL, "/")) + }, + } +} + +// WithHTTPServer sets the HTTP server instance for the transport. +// This is useful for advanced scenarios where you want to provide your own http.Server. +func WithHTTPServer(srv *http.Server) CommonHTTPServerOption { + return commonOption{ + apply: func(c httpTransportConfigurable) { + c.setHTTPServer(srv) + }, + } +} diff --git a/server/sse.go b/server/sse.go index 018657e6..81e48d0d 100644 --- a/server/sse.go +++ b/server/sse.go @@ -36,13 +36,6 @@ type sseSession struct { // content. This can be used to inject context values from headers, for example. type SSEContextFunc func(ctx context.Context, r *http.Request) context.Context -// DynamicBasePathFunc allows the user to provide a function to generate the -// base path for a given request and sessionID. This is useful for cases where -// the base path is not known at the time of SSE server creation, such as when -// using a reverse proxy or when the base path is dynamically generated. The -// function should return the base path (e.g., "/mcp/tenant123"). -type DynamicBasePathFunc func(r *http.Request, sessionID string) string - func (s *sseSession) SessionID() string { return s.sessionID } @@ -100,7 +93,7 @@ type SSEServer struct { sseEndpoint string sessions sync.Map srv *http.Server - contextFunc SSEContextFunc + contextFunc HTTPContextFunc dynamicBasePathFunc DynamicBasePathFunc keepAlive bool @@ -109,37 +102,41 @@ type SSEServer struct { mu sync.RWMutex } -// SSEOption defines a function type for configuring SSEServer -type SSEOption func(*SSEServer) +// Ensure SSEServer implements httpTransportConfigurable +var _ httpTransportConfigurable = (*SSEServer)(nil) -// WithBaseURL sets the base URL for the SSE server -func WithBaseURL(baseURL string) SSEOption { - return func(s *SSEServer) { - if baseURL != "" { - u, err := url.Parse(baseURL) - if err != nil { - return - } - if u.Scheme != "http" && u.Scheme != "https" { - return - } - // Check if the host is empty or only contains a port - if u.Host == "" || strings.HasPrefix(u.Host, ":") { - return - } - if len(u.Query()) > 0 { - return - } +func (s *SSEServer) setBasePath(basePath string) { + s.basePath = normalizeURLPath(basePath) +} + +func (s *SSEServer) setDynamicBasePath(fn DynamicBasePathFunc) { + if fn != nil { + s.dynamicBasePathFunc = func(r *http.Request, sid string) string { + bp := fn(r, sid) + return normalizeURLPath(bp) } - s.baseURL = strings.TrimSuffix(baseURL, "/") } } -// WithStaticBasePath adds a new option for setting a static base path -func WithStaticBasePath(basePath string) SSEOption { - return func(s *SSEServer) { - s.basePath = normalizeURLPath(basePath) - } +func (s *SSEServer) setKeepAliveInterval(interval time.Duration) { + s.keepAlive = true + s.keepAliveInterval = interval +} + +func (s *SSEServer) setKeepAlive(keepAlive bool) { + s.keepAlive = keepAlive +} + +func (s *SSEServer) setContextFunc(fn HTTPContextFunc) { + s.contextFunc = fn +} + +func (s *SSEServer) setHTTPServer(srv *http.Server) { + s.srv = srv +} + +func (s *SSEServer) setBaseURL(baseURL string) { + s.baseURL = baseURL } // WithBasePath adds a new option for setting a static base path. @@ -151,26 +148,11 @@ func WithBasePath(basePath string) SSEOption { return WithStaticBasePath(basePath) } -// WithDynamicBasePath accepts a function for generating the base path. This is -// useful for cases where the base path is not known at the time of SSE server -// creation, such as when using a reverse proxy or when the server is mounted -// at a dynamic path. -func WithDynamicBasePath(fn DynamicBasePathFunc) SSEOption { - return func(s *SSEServer) { - if fn != nil { - s.dynamicBasePathFunc = func(r *http.Request, sid string) string { - bp := fn(r, sid) - return normalizeURLPath(bp) - } - } - } -} - // WithMessageEndpoint sets the message endpoint path func WithMessageEndpoint(endpoint string) SSEOption { - return func(s *SSEServer) { + return sseOption(func(s *SSEServer) { s.messageEndpoint = endpoint - } + }) } // WithAppendQueryToMessageEndpoint configures the SSE server to append the original request's @@ -179,53 +161,37 @@ func WithMessageEndpoint(endpoint string) SSEOption { // SSE connection request and carry them over to subsequent message requests, maintaining // context or authentication details across the communication channel. func WithAppendQueryToMessageEndpoint() SSEOption { - return func(s *SSEServer) { + return sseOption(func(s *SSEServer) { s.appendQueryToMessageEndpoint = true - } + }) } // WithUseFullURLForMessageEndpoint controls whether the SSE server returns a complete URL (including baseURL) // or just the path portion for the message endpoint. Set to false when clients will concatenate // the baseURL themselves to avoid malformed URLs like "http://localhost/mcphttp://localhost/mcp/message". func WithUseFullURLForMessageEndpoint(useFullURLForMessageEndpoint bool) SSEOption { - return func(s *SSEServer) { + return sseOption(func(s *SSEServer) { s.useFullURLForMessageEndpoint = useFullURLForMessageEndpoint - } + }) } // WithSSEEndpoint sets the SSE endpoint path func WithSSEEndpoint(endpoint string) SSEOption { - return func(s *SSEServer) { + return sseOption(func(s *SSEServer) { s.sseEndpoint = endpoint - } -} - -// WithHTTPServer sets the HTTP server instance -func WithHTTPServer(srv *http.Server) SSEOption { - return func(s *SSEServer) { - s.srv = srv - } -} - -func WithKeepAliveInterval(keepAliveInterval time.Duration) SSEOption { - return func(s *SSEServer) { - s.keepAlive = true - s.keepAliveInterval = keepAliveInterval - } -} - -func WithKeepAlive(keepAlive bool) SSEOption { - return func(s *SSEServer) { - s.keepAlive = keepAlive - } + }) } // WithSSEContextFunc sets a function that will be called to customise the context // to the server using the incoming request. +// +// Deprecated: Use WithContextFunc instead. This will be removed in a future version. +// +//go:deprecated func WithSSEContextFunc(fn SSEContextFunc) SSEOption { - return func(s *SSEServer) { - s.contextFunc = fn - } + return sseOption(func(s *SSEServer) { + WithHTTPContextFunc(HTTPContextFunc(fn)).applyToSSE(s) + }) } // NewSSEServer creates a new SSE server instance with the given MCP server and options. @@ -241,16 +207,15 @@ func NewSSEServer(server *MCPServer, opts ...SSEOption) *SSEServer { // Apply all options for _, opt := range opts { - opt(s) + opt.applyToSSE(s) } return s } -// NewTestServer creates a test server for testing purposes +// NewTestServer creates a test server for testing purposes. func NewTestServer(server *MCPServer, opts ...SSEOption) *httptest.Server { sseServer := NewSSEServer(server, opts...) - testServer := httptest.NewServer(sseServer) sseServer.baseURL = testServer.URL return testServer From 4558b68574dbe1e8ed8e7a9698b3853ab5065753 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 8 May 2025 14:16:04 -0400 Subject: [PATCH 13/29] ci: add check to verify generated code is up-to-date (#258) This commit adds a new CI job that runs code generation and verifies no uncommitted changes exist. This prevents accidental manual edits to autogenerated files by: 1. Running `go generate ./...` during CI 2. Failing the build when generated code differs from committed code 3. Providing clear error messages with instructions for developers --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60b74f2b..7baf10c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,8 @@ on: branches: - main pull_request: + workflow_dispatch: + jobs: test: runs-on: ubuntu-latest @@ -13,3 +15,21 @@ jobs: with: go-version-file: 'go.mod' - run: go test ./... -race + + verify-codegen: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + - name: Run code generation + run: go generate ./... + - name: Check for uncommitted changes + run: | + if [[ -n $(git status --porcelain) ]]; then + echo "Error: Generated code is not up to date. Please run 'go generate ./...' and commit the changes." + git status + git diff + exit 1 + fi From 718522427c2853953ab3efbcce07cb79ce9b0946 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 9 May 2025 11:32:31 +0300 Subject: [PATCH 14/29] add logo --- README.md | 4 +++- logo.png | Bin 0 -> 43341 bytes 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 logo.png diff --git a/README.md b/README.md index 594d49ca..86c1378a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -# MCP Go 🚀 +
+MCP Go Logo +
[![Build](https://github.com/mark3labs/mcp-go/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mark3labs/mcp-go/actions/workflows/ci.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/mark3labs/mcp-go?cache)](https://goreportcard.com/report/github.com/mark3labs/mcp-go) [![GoDoc](https://pkg.go.dev/badge/github.com/mark3labs/mcp-go.svg)](https://pkg.go.dev/github.com/mark3labs/mcp-go) diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1d71c43d93598c90f4015de7afd2b37c81198129 GIT binary patch literal 43341 zcmb@tbyQrmP|&ixKxW9n=O zcDJ#%b>epCqy7h%`@Q_Pnu+?I$-z@JGAGL+EvpqKxlbf3xqZ=EeoufGu z3l|p`6EiClD=Wi02ZNJ`t+Sy!gRK+!I|2gu9}H1bCu7Gi_Re4IY{7pq4UOzvocXA! z5y1b37IJoW{9^PUbXzAz!+&aA{&rz}7n;e?#EFT8k@@emz+m2g@pB8AIQ=F6HHfmQ z!#~8oDC;l(jAU=<=wzzmVQF`hE|KwM(b1}B~D+)jBf7t%3?jK%W zroY;?{_-!S{6qX(EC1>7FO~g^@xPhn|GTM7jQ?XMdlyIRe_X)Cn90=I)W+1-`LB^! z{%s@^V{Yd!&eo>?Wg;PK=l^MEbt5sg_k)^e9#lyt!c_h@9UdX+9$&pC{&;i>TBPV=LX0N;NZruIKxp`WFd3i^Ns0nG4JII+G}zzB9~}k-kisk~=?_8zCb*$kZ4iP`6SplB`A+mAC0of%V%d+@t=u6{%Ud$9k z0G)90%Sgdn^E#A-e^k9w#kA-jm3BUYCfp!iCqy8iA<+WBd44Q!U?3m?9O#=V7#JR| zaZr6&o%q6yS5%mF$Rap$eKw{5Pd21U!J@!u1N>(sB?m}{^djI;$JcEdC40M0p7pTx%gjB znB52S(SJ`(u77eV^wc`;>ysPLQrExEv^rf&lp4&vntCGJwYOZ zp1jFjPfd5xBVr3%Tf5J%eq;EVyXs3~TXEth=K~P*h+GH*n9yJT7rOiEoxJ&5{p<^}p4_#jUWY9lvgwLZ&~vZpUCa8)e;Yo}%toSp7D~ zIXXXkz`Gsy^r};Rrqp*i(-8@n`*+cgCtM`+{w7I1MC6&p4v%}l|W#)N4DiUBY zIbbbqy~?w7pv~%l^R1{}fEfCcd*P(5c3{GgBh*s42X|h>sJr6 znLo3CTje%2_vkbyuzxxxewH)0kW;>{-@>-2s_#ASF5}aIwr%m?aZUS)+P$DN+1o&G zFelTt#a!OWV&rSD_S-*MY17^v=yMBDEPbB^{5D`5cAFnG?|<2fwfXfJBwg!A^il6GG~b_LRBID(H(Pq1Vx%LJF|Q z$KAj;^0ouI8vqx*<3-;(gf@e)HsL|9KYN-QAiW8Kl>w_NhDgXndY82Umd-PuIh5IEJrdsOJ&Cwa=ROFXnjuNo$`*y@!14K@X?&5o$RY ziP7qyat!ElV|U?kaZFVfH<92^oIcY!9|2izlv^`cU>b}giFl1XZsu>X1%kwJ^1+=`BPqK+^ieb@w@KxPl*VBTCB4?LCQ;sY$J$#xT%o0CEntV!w3H+beZT%J&0-j(Y1R?1fl5r2{WqIdP&iyVfr^@hcKBDTc zvHcAQDF5`kNlshQ!J|4cCok`h=h5q9$S!orN01_dc_DsIwndZ>i7a^nr zJmYIm+~sioofQE35z#ow?lnu_j;PfB?MvE<+0yX2OGV3%=u7b8aPRAk;W&40f!cc1 zj?y4HhyyH2L@JIJDM7<@=0CI+3x4?d>0_e^KOllS43`rbLL{jG;T=~_L{QEJvw>Y7 ztufK9+^iNKUDtpLSVqd!5H-HKkk?aPri{4aV404({3M`(gaVmuie4>Hu0d$4GrDa| zvSsFkXSGityMFgEs^R6>lb+zlyt$LB?fQ51%aX>ua;F|j0pSVH-fZUG>DitfXdimO zY&Wso{QmT@G4o!QqVuqv;Gc9TJU9yaLn|5v) zF+OoAYK)x3lUg-K0!PJ(1-LGObbq~=IqG5ZQ%osqbt+o1nQpqo3Xrcg7rfm`e2UaH z#+mOqnpyCnA8)8P{kvQn_Pr}$6RcP&xVq-u?pfDC(INac_b~CRih^$38WT zMR(`-6=SMT2_OKsyFd2_BP2>yxdUr#$GPvW9KH!iEMgjk{rfAS8ylok(boXYT`r$7MDo#$OkRNsmx(_0A!U3CLZ zyD^malZjw;IDk5;XvM$#=%QoS=*q`ZL5I;GMDzM-1qvkvPg!2JVnSFX@!~xQEQ_02}%`1>7-ub{K3;l|N zt}&KOd|A4p6-8&!R$(X6t0<32Q;;7BumFIHQTm=Js)J2mN#u-$Tl^}VjyUzn?vx$H zAFhChn|K<+CGJR9B)@@XQK_#hGI6 zXDE;3D5ZN_qru=#viRy#86DU( zk<7MY&5dc8ah+sxd&|ZJdSX)c=Z!3mrP^DyWV98K{P4WW%vqs=g%J7$ z?N9)Uw?Eb;MHA(RW&=^O)R19=u^|!yjYx(13A*rB0n_#>(I;ufdv5mP%6%A(nMn6W&8 zROs$gBa4-GOA7_Og`fPc8b_W7!5y9edfV6Yp6v4$X6{Ot5<)?c;kFpVleH?PIuy;0A?xU6Bx+Vpb3NDqoM$KDWaS5yz+}J z9m3b|DKe0@qJG2Mm&XtYviW$U(d#{K{?h~(_kN#yIdhHaS(pKr3nB%Ry86O}g`C4v z%bEtk%7`?_r6p0vEt)sy*P)HSQ#Kx)&Q@M1Dl*Rx$x*@yoDeMSiNpnNw@gG)kWY29 zx`ifxo*$nlV)$%q!d9#Q%F-H{q`kWfvQ{VyGZM6rqSOGfB|yUOE7TgSEqVaJ_Z zxz3Eyqy<FEj{L;f*R8?Uh4CEfd8fb`$qU}49x}5dqj~CU1Xx^Y|KKXvV@k5X ziyZ|r%!{XVG|Q5~DA8-AYOJ!d6^@$8)}g`P!xSArlp z+os5&M=FTE`b@nUOvw|(_dAUeL&&?QCZ=aZSV)38Iz(xhJiOTerMOl~mf2h(op|!I z`pXA+`W>l+%WXl?B0=;D$?)*BGLM86snJ4~>uq39E)2vPsx5y8eMNZ~D&ud*)7!Q$ z&Lh!t%3-tn`B&Px;PmoP%T%r9BV28AmHGg|a>{GrVMj&?N+m+{G%V*z8~5}f6-M2d zxjOiHY;+bxZ0L9c8Zl^la?E-j%x*@Qnkbkx`L6Z?mb-G3yRx-x)OMY(R{O>?Y?Cnga(!fNF!fUp6>n@I#+tguU5NoLcSg;}PJ-pAm)K;o>+2B0L=lM>VNz z7FL*~W{ObdUVq%*cU09)+&atXtrx#&FIyc^n!brd%l{$XbL&v2GLpG^6iB_b+fmpgSI3O|5wzJ}yy7|2K(5Xto5M@tee_Vi#Jz5jAah>R zEI!H;2M?q$A~kdR??wL4CrWgqqk)hRAJU8;%kny(bjm7U1_ol{3Y9#J#AD7k^Gdzj z?29)aEV;{0VWc zG1Jg~rvP_UbtRAmph-Ku?r38sa$J_DPg8y+(4?O4$z_(!l*PqM5i{J3h%18p$&u0;Eq$GY3Hm3B#bXdeZ@~ z1fneO{W$ak#3R@vN`d2@^J8r1*_`SX2i61yDv0L zR=puos=T^>M(_)rovu&XGpWixvBKLZcjD}LV-iC`k=%6TL~dBx@eF4G2N&fPlt(X=T+ z{^8cZR3k{AEFc+-p*MmLQ>_Y|(V_^zB>*bhp^P9d&H0Y*_?)>U#Ymc72n3BF2?JS~ z7k`3IJ(RuWwd;O)Jf`ccH;qfLvfga5>|#kNTd^3=h^7&5V5l{Lsd8|YWUtp+o&-RX z1kqkQP?2Ga0#_GwQPWvWLb1@oPn-~L_o2~(!3e+_a8<;(iGsUPTjn|%=WgeB%MG5{ zJB~V7*cJ6 zv_ZEjKfu;NBA5Fw_C^`Zd7u7nSyz59Lg5$X-5xMyl^s`9YF&%{5u;!?(P1;5FlK(fLXujGLR{t=Mqq7@_3 zBzFA@tOJSJ4ch+(2??I%#gstfk_2T=f_)kKSe z4Go(@KpV&4Q`N&W&$H7=kg+T^e1{ zSPKvo#&tCSjt%?~5TPo9%b@}m!z4A-e*I9veHmS0V@?hm5slUWZIZ69QB%+#^y6OX zgL7w%8ZN`m4qHg!9IlVf)B|GFp20m>m+Vx_+dSrvj$ZwC0-O#V&MAR7x}t zR6*B}MPRyg%c+fCZJ;6Dg1RX4iS1sA>SN>>8A=jJoIVf~|GA-}@gBja*qp9~m38B;#2E524^nZtB!%*nZz zXWX4-!A5jDCb@ex{!2DAiPe5>{2(IdX&Z~QY77-fUs0uYpA29#$ekhsgJeo03u%~3&g3k=5A(3k%n`8W{wNF( zj0_N!KYk4g5}f`JF`v_K?nIRP`*X$aqI=~y2AP7>GTfT>cDcSxiNBn`EPa=?TBq!z zPJmC%$y@!$9B6odcd|4l>Z{x;vERJ1&Qk+1a8>HbeI_fTV!qz_rzEEagIP8L>evL? zGH>z{=-r$HRV}NwZr0dU7S%L0Mz!Q}@`@5xZtgJox1}cL`+>iHLLiS8S-IfWICQ5x z)ICy%4l7@3j(xvv7B8;})r2wWqh9K^IzHp>_0T=+f95xof1x1A`AF1j&Hsz)!CWP0 zJRP=6yom)G4Wm5R1f2+v`f_9V&&96eL&WNU@`l{|vZ~OU0|mfUH$3*a-#OmqVXE%G zMWvK-Lc!1;wi3&$L|W7iq};emcc87^JBmSY`JPP&v(q#GaUxlfb>b(Skc)(GFgs) z@WwtrW1*ixj(W`)DJqb-`+ZAuM&9Q?+rIVDwW6C?&49JyqtdqeBqJ2wlsTB`7Z9-= z;=u99PF+Ubo{`R~8}g=R_?r44x!aXH9?Q zhkX}kw{V=06SW)OT~~j&tr2b48=d_!!rggYcHePkh2!@euFYTLv}|HojjDm#g>Uns z>~T*nzW`OgZMsfM6&h$jIt!g$ia*EZQ54(VZ5p<2wv0iJ=2i_-FmmsE5mJucEj`yS_0K4D`Z{F!T0M(94^pOGtPk@zT<%C{!n zQ-wT+?6Y#(hWQkT%=@4^`>sKT+QvldNM!Tt!bl|W@2CA7AeFJuqoz>bl894)PB&RCGcs+nI``8V)*iqP+ytx zLE-nGjgN_c=%i1XH0@@`ta%=h7UP`=N*~Lj+9q1{Pd7>*I0G!=m@B;u_CJPqlOx9k-R>H2;0TUEZh&gj`QhCy) z7!pcb%o2N}bN&gDJwVVbH1%heF|~GSwn99H1T02X3`YvDGz9wh1kkqYz=Fn5gy}kx z2EPzdB*fx+${E$gz0|;eDj7*Aq7>$No(WO3fHE1#ou};iVK`&f<@2LU5!*xk(+3{GpC$&RPMs zm;O2(KwZC!0xdb)lVyFu^Ies~r2Uu9DR)ep5WW2F~RG0hQhY-{c=HAbr2B>3=y>~P=k)wd+GJ=i2XD4xQcWV;- zRn`t)etyXFUIeXe4gTP`XY=-(+42({p|FPjQQH}ASEcQ~43$lkTTWdcxoFl<23mu z{B*0w;gV1mJZSXD?_ykYMI$tHl@Rqb%Q(WOh$SV{MUqXlnNp{D>|jf8Vt17IFUhV- zt_kpNmGvDBcGDU5E6nu`eGT+W(ir4;y~0cA>ZN8ZZ^C!eBQH!44MY%gggly*m7I`t z(6YY%Orgq+y&1tO&r1sM^gxEKLlp7`;jg>_$e-X3$Csw7mk#u~&9Yl&?T&r161RV{ z(zJhATB80n1HKVTCUtV?if92x7TyYGG!{HF<821#=W1-ZjxAn75;SKB7h&)gZyM=- z8G9-Ia89IVk1SVFC#N|;_GOkSBFwFl>6(|$t(wPXT>+9T$D+25({@;&`|y7p74*x{?inGy0w~jnf~7wZcwF+C^ih(Geug!5Gs? z5!qEnZ+C~lof|h}wi#p0A^MUpGL0^9^WRq@Of{!hvVNc0xSFu0b@!=qK;!dQj!Jzf605yLV|OU{*>$;ZhJcTKZ8UjNehx&{btl(DuswRyR^wwUPyI zScnqc5)o10*+g13uE}pNzuy}agj$aCyPIt{1%2D+EgO;g;+yEtmNe>J_@<9P2hvwR zLEu4N-8R&uguZ2Mx{~*}WIqNP0I|9A=Oa1H?VSs3mOB9O)KpTmcmu4y3^2dvS%1xu zGJiBY3Sjp){ci0}&hgtEn?}_yN#*@KZp}d?dA(d4*lo@S%HBnNW-25(oPi6xM|r?jM}3Q`^e~ibs04l zj>Fj}nX64n{KkeKtV@)dJZpOVau_bFcDsI)1z`!%9n>CFrYoW!Aoy=&l-Q4aiJr(E z(!l;k(6nNJgqhM)((BRO=)nKLd-nMGxS8rYX43Gkx>~!*u%5Os@^w=BT`Y#
    _foJJ94vqms$0svqHGYyVU!d) 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 literal 0 HcmV?d00001 From ff3ee05d1c6fd73dfc4f34351e98ac71fc14c447 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 9 May 2025 11:34:41 +0300 Subject: [PATCH 15/29] fix --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 86c1378a..5ae88d02 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@

    MCP Go Logo
    + [![Build](https://github.com/mark3labs/mcp-go/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mark3labs/mcp-go/actions/workflows/ci.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/mark3labs/mcp-go?cache)](https://goreportcard.com/report/github.com/mark3labs/mcp-go) [![GoDoc](https://pkg.go.dev/badge/github.com/mark3labs/mcp-go.svg)](https://pkg.go.dev/github.com/mark3labs/mcp-go) From cecf3e0898b5f4ac6027c48d2a8c5e6798a1f0a6 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 9 May 2025 11:35:32 +0300 Subject: [PATCH 16/29] tweak --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 5ae88d02..7b838efd 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,11 @@
    MCP Go Logo -
    [![Build](https://github.com/mark3labs/mcp-go/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mark3labs/mcp-go/actions/workflows/ci.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/mark3labs/mcp-go?cache)](https://goreportcard.com/report/github.com/mark3labs/mcp-go) [![GoDoc](https://pkg.go.dev/badge/github.com/mark3labs/mcp-go.svg)](https://pkg.go.dev/github.com/mark3labs/mcp-go) -
    - A Go implementation of the Model Context Protocol (MCP), enabling seamless integration between LLM applications and external data sources and tools.
    From 5c197107b50d1b3f18dce094df77d12e35edd854 Mon Sep 17 00:00:00 2001 From: cryo Date: Fri, 9 May 2025 16:36:53 +0800 Subject: [PATCH 17/29] fix(MCPServer): correct notification method in RemoveResource() (#262) --- server/resource_test.go | 4 ++-- server/server.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/resource_test.go b/server/resource_test.go index 94b35a3d..acd5dc5d 100644 --- a/server/resource_test.go +++ b/server/resource_test.go @@ -84,7 +84,7 @@ func TestMCPServer_RemoveResource(t *testing.T) { expectedNotifications: 1, validate: func(t *testing.T, notifications []mcp.JSONRPCNotification, resourcesList mcp.JSONRPCMessage) { // Check that we received a list_changed notification - assert.Equal(t, "resources/list_changed", notifications[0].Method) + assert.Equal(t, mcp.MethodNotificationResourcesListChanged, notifications[0].Method) // Verify we now have only one resource resp, ok := resourcesList.(mcp.JSONRPCResponse) @@ -133,7 +133,7 @@ func TestMCPServer_RemoveResource(t *testing.T) { expectedNotifications: 1, // Still sends a notification validate: func(t *testing.T, notifications []mcp.JSONRPCNotification, resourcesList mcp.JSONRPCMessage) { // Check that we received a list_changed notification - assert.Equal(t, "resources/list_changed", notifications[0].Method) + assert.Equal(t, mcp.MethodNotificationResourcesListChanged, notifications[0].Method) // The original resource should still be there resp, ok := resourcesList.(mcp.JSONRPCResponse) diff --git a/server/server.go b/server/server.go index 8aac05ca..9076c9b5 100644 --- a/server/server.go +++ b/server/server.go @@ -341,7 +341,7 @@ func (s *MCPServer) RemoveResource(uri string) { // Send notification to all initialized sessions if listChanged capability is enabled if s.capabilities.resources != nil && s.capabilities.resources.listChanged { - s.SendNotificationToAllClients("resources/list_changed", nil) + s.SendNotificationToAllClients(mcp.MethodNotificationResourcesListChanged, nil) } } From b4686dbd55b04bdad64e320f2287f7bd424979fb Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Sat, 10 May 2025 13:00:27 +0300 Subject: [PATCH 18/29] Create sample client (#265) --- examples/simple_client/main.go | 193 +++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 examples/simple_client/main.go diff --git a/examples/simple_client/main.go b/examples/simple_client/main.go new file mode 100644 index 00000000..5532e05f --- /dev/null +++ b/examples/simple_client/main.go @@ -0,0 +1,193 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + "log" + "os" + "time" + + "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/client/transport" + "github.com/mark3labs/mcp-go/mcp" +) + +func main() { + // Define command line flags + stdioCmd := flag.String("stdio", "", "Command to execute for stdio transport (e.g. 'python server.py')") + sseURL := flag.String("sse", "", "URL for SSE transport (e.g. 'http://localhost:8080/sse')") + flag.Parse() + + // Validate flags + if (*stdioCmd == "" && *sseURL == "") || (*stdioCmd != "" && *sseURL != "") { + fmt.Println("Error: You must specify exactly one of --stdio or --sse") + flag.Usage() + os.Exit(1) + } + + // Create a context with timeout + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Create client based on transport type + var c *client.Client + var err error + + if *stdioCmd != "" { + fmt.Println("Initializing stdio client...") + // Parse command and arguments + args := parseCommand(*stdioCmd) + if len(args) == 0 { + fmt.Println("Error: Invalid stdio command") + os.Exit(1) + } + + // Create command and stdio transport + command := args[0] + cmdArgs := args[1:] + + // Create stdio transport with verbose logging + stdioTransport := transport.NewStdio(command, nil, cmdArgs...) + + // Start the transport + if err := stdioTransport.Start(ctx); err != nil { + log.Fatalf("Failed to start stdio transport: %v", err) + } + + // Create client with the transport + c = client.NewClient(stdioTransport) + + // Set up logging for stderr if available + if stderr, ok := client.GetStderr(c); ok { + go func() { + buf := make([]byte, 4096) + for { + n, err := stderr.Read(buf) + if err != nil { + if err != io.EOF { + log.Printf("Error reading stderr: %v", err) + } + return + } + if n > 0 { + fmt.Fprintf(os.Stderr, "[Server] %s", buf[:n]) + } + } + }() + } + } else { + fmt.Println("Initializing SSE client...") + // Create SSE transport + sseTransport, err := transport.NewSSE(*sseURL) + if err != nil { + log.Fatalf("Failed to create SSE transport: %v", err) + } + + // Start the transport + if err := sseTransport.Start(ctx); err != nil { + log.Fatalf("Failed to start SSE transport: %v", err) + } + + // Create client with the transport + c = client.NewClient(sseTransport) + } + + // Set up notification handler + c.OnNotification(func(notification mcp.JSONRPCNotification) { + fmt.Printf("Received notification: %s\n", notification.Method) + }) + + // Initialize the client + fmt.Println("Initializing client...") + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: "MCP-Go Simple Client Example", + Version: "1.0.0", + } + initRequest.Params.Capabilities = mcp.ClientCapabilities{} + + serverInfo, err := c.Initialize(ctx, initRequest) + if err != nil { + log.Fatalf("Failed to initialize: %v", err) + } + + // Display server information + fmt.Printf("Connected to server: %s (version %s)\n", + serverInfo.ServerInfo.Name, + serverInfo.ServerInfo.Version) + fmt.Printf("Server capabilities: %+v\n", serverInfo.Capabilities) + + // List available tools if the server supports them + if serverInfo.Capabilities.Tools != nil { + fmt.Println("Fetching available tools...") + toolsRequest := mcp.ListToolsRequest{} + toolsResult, err := c.ListTools(ctx, toolsRequest) + if err != nil { + log.Printf("Failed to list tools: %v", err) + } else { + fmt.Printf("Server has %d tools available\n", len(toolsResult.Tools)) + for i, tool := range toolsResult.Tools { + fmt.Printf(" %d. %s - %s\n", i+1, tool.Name, tool.Description) + } + } + } + + // List available resources if the server supports them + if serverInfo.Capabilities.Resources != nil { + fmt.Println("Fetching available resources...") + resourcesRequest := mcp.ListResourcesRequest{} + resourcesResult, err := c.ListResources(ctx, resourcesRequest) + if err != nil { + log.Printf("Failed to list resources: %v", err) + } else { + fmt.Printf("Server has %d resources available\n", len(resourcesResult.Resources)) + for i, resource := range resourcesResult.Resources { + fmt.Printf(" %d. %s - %s\n", i+1, resource.URI, resource.Name) + } + } + } + + fmt.Println("Client initialized successfully. Shutting down...") + c.Close() +} + +// parseCommand splits a command string into command and arguments +func parseCommand(cmd string) []string { + // This is a simple implementation that doesn't handle quotes or escapes + // For a more robust solution, consider using a shell parser library + var result []string + var current string + var inQuote bool + var quoteChar rune + + for _, r := range cmd { + switch { + case r == ' ' && !inQuote: + if current != "" { + result = append(result, current) + current = "" + } + case (r == '"' || r == '\''): + if inQuote && r == quoteChar { + inQuote = false + quoteChar = 0 + } else if !inQuote { + inQuote = true + quoteChar = r + } else { + current += string(r) + } + default: + current += string(r) + } + } + + if current != "" { + result = append(result, current) + } + + return result +} \ No newline at end of file From 64290191e845da685e67eed51e8964499a69a72a Mon Sep 17 00:00:00 2001 From: bing <32173777@qq.com> Date: Sat, 10 May 2025 23:08:28 +0800 Subject: [PATCH 19/29] Fix the issue where the 'Shutdown' method fails to properly exit. (#255) * Fix the issue where the 'Shutdown' method fails to properly exit. * Fix the issue where the 'Shutdown' method fails to properly exit. --------- Co-authored-by: CHENBING1 --- server/sse.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/sse.go b/server/sse.go index 81e48d0d..65001ba0 100644 --- a/server/sse.go +++ b/server/sse.go @@ -225,8 +225,6 @@ func NewTestServer(server *MCPServer, opts ...SSEOption) *httptest.Server { // It sets up HTTP handlers for SSE and message endpoints. func (s *SSEServer) Start(addr string) error { s.mu.Lock() - defer s.mu.Unlock() - if s.srv == nil { s.srv = &http.Server{ Addr: addr, @@ -239,8 +237,10 @@ func (s *SSEServer) Start(addr string) error { return fmt.Errorf("conflicting listen address: WithHTTPServer(%q) vs Start(%q)", s.srv.Addr, addr) } } + srv := s.srv + s.mu.Unlock() - return s.srv.ListenAndServe() + return srv.ListenAndServe() } // Shutdown gracefully stops the SSE server, closing all active sessions From 1c99eaf3bfa39f832e73ec26402b4c5fa62d0d16 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sat, 10 May 2025 13:18:45 -0400 Subject: [PATCH 20/29] test(server): reliably detect Start/Shutdown deadlock in SSEServer (#264) This test demonstrates the deadlock described in https://github.com/mark3labs/mcp-go/issues/254 and https://github.com/mark3labs/mcp-go/issues/263 by running `SSEServer.Start` then `SSEServer.Shutdown`. If it deadlocks, the test fails quickly and reliably, rather than hanging indefinitely. References: - https://github.com/mark3labs/mcp-go/issues/254 - https://github.com/mark3labs/mcp-go/issues/255 - https://github.com/mark3labs/mcp-go/issues/263 --- server/sse_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/server/sse_test.go b/server/sse_test.go index 393a70cf..c3c8bb28 100644 --- a/server/sse_test.go +++ b/server/sse_test.go @@ -1400,6 +1400,38 @@ func TestSSEServer(t *testing.T) { t.Fatal("Processing did not complete after client disconnection") } }) + + t.Run("Start() then Shutdown() should not deadlock", func(t *testing.T) { + mcpServer := NewMCPServer("test", "1.0.0") + sseServer := NewSSEServer(mcpServer, WithBaseURL("http://localhost:0")) + + done := make(chan struct{}) + + go func() { + _ = sseServer.Start("127.0.0.1:0") + close(done) + }() + + // Wait a bit to ensure the server is running + time.Sleep(50 * time.Millisecond) + + shutdownDone := make(chan error, 1) + ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) + defer cancel() + go func() { + err := sseServer.Shutdown(ctx) + shutdownDone <- err + }() + + select { + case err := <-shutdownDone: + if ctx.Err() == context.DeadlineExceeded { + t.Fatalf("Shutdown deadlocked (timed out): %v", err) + } + case <-time.After(1 * time.Second): + t.Fatal("Shutdown did not return in time (likely deadlocked)") + } + }) } func readSSEEvent(sseResp *http.Response) (string, error) { From 61b9784ea84d637e29a1bb2b226b953c4bdce4fe Mon Sep 17 00:00:00 2001 From: Navendu Pottekkat Date: Sat, 10 May 2025 22:51:37 +0530 Subject: [PATCH 21/29] docs: make code examples in the README correct as per spec (#268) --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7b838efd..d7dd9d09 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Discuss the SDK on [Discord](https://discord.gg/RqSS2NQVsY)
    + ```go package main @@ -31,10 +32,11 @@ import ( ) func main() { - // Create MCP server + // Create a new MCP server s := server.NewMCPServer( "Demo 🚀", "1.0.0", + server.WithToolCapabilities(false), ) // Add tool @@ -116,7 +118,6 @@ package main import ( "context" - "errors" "fmt" "github.com/mark3labs/mcp-go/mcp" @@ -128,8 +129,7 @@ func main() { s := server.NewMCPServer( "Calculator Demo", "1.0.0", - server.WithResourceCapabilities(true, true), - server.WithLogging(), + server.WithToolCapabilities(false), server.WithRecovery(), ) @@ -181,6 +181,7 @@ func main() { } } ``` + ## What is MCP? The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can: From 3442d321ad10a9edce5f2f76580e014a67de2229 Mon Sep 17 00:00:00 2001 From: cryo Date: Sun, 11 May 2025 01:25:02 +0800 Subject: [PATCH 22/29] feat(MCPServer): avoid unnecessary notifications when Resource/Tool not exists (#266) --- server/resource_test.go | 9 +++++---- server/server.go | 17 ++++++++++++----- server/server_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/server/resource_test.go b/server/resource_test.go index acd5dc5d..11f1275e 100644 --- a/server/resource_test.go +++ b/server/resource_test.go @@ -98,7 +98,7 @@ func TestMCPServer_RemoveResource(t *testing.T) { }, }, { - name: "RemoveResource with non-existent resource does nothing", + name: "RemoveResource with non-existent resource does nothing and not receives notifications from MCPServer", action: func(t *testing.T, server *MCPServer, notificationChannel chan mcp.JSONRPCNotification) { // Add a test resource server.AddResource( @@ -130,10 +130,11 @@ func TestMCPServer_RemoveResource(t *testing.T) { // Remove a non-existent resource server.RemoveResource("test://nonexistent") }, - expectedNotifications: 1, // Still sends a notification + expectedNotifications: 0, // No notifications expected validate: func(t *testing.T, notifications []mcp.JSONRPCNotification, resourcesList mcp.JSONRPCMessage) { - // Check that we received a list_changed notification - assert.Equal(t, mcp.MethodNotificationResourcesListChanged, notifications[0].Method) + // verify that no notifications were sent + assert.Empty(t, notifications) + // The original resource should still be there resp, ok := resourcesList.(mcp.JSONRPCResponse) diff --git a/server/server.go b/server/server.go index 9076c9b5..dc4e16a9 100644 --- a/server/server.go +++ b/server/server.go @@ -336,11 +336,14 @@ func (s *MCPServer) AddResource( // RemoveResource removes a resource from the server func (s *MCPServer) RemoveResource(uri string) { s.resourcesMu.Lock() - delete(s.resources, uri) + _, exists := s.resources[uri] + if exists { + delete(s.resources, uri) + } s.resourcesMu.Unlock() - // Send notification to all initialized sessions if listChanged capability is enabled - if s.capabilities.resources != nil && s.capabilities.resources.listChanged { + // Send notification to all initialized sessions if listChanged capability is enabled and we actually remove a resource + if exists && s.capabilities.resources != nil && s.capabilities.resources.listChanged { s.SendNotificationToAllClients(mcp.MethodNotificationResourcesListChanged, nil) } } @@ -448,13 +451,17 @@ func (s *MCPServer) SetTools(tools ...ServerTool) { // DeleteTools removes a tool from the server func (s *MCPServer) DeleteTools(names ...string) { s.toolsMu.Lock() + var exists bool for _, name := range names { - delete(s.tools, name) + if _, ok := s.tools[name]; ok { + delete(s.tools, name) + exists = true + } } s.toolsMu.Unlock() // When the list of available tools changes, servers that declared the listChanged capability SHOULD send a notification. - if s.capabilities.tools.listChanged { + if exists && s.capabilities.tools != nil && s.capabilities.tools.listChanged { // Send notification to all initialized sessions s.SendNotificationToAllClients(mcp.MethodNotificationToolsListChanged, nil) } diff --git a/server/server_test.go b/server/server_test.go index c5e99c0a..c3edd45f 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -308,6 +308,34 @@ func TestMCPServer_Tools(t *testing.T) { assert.Empty(t, result.Tools, "Expected empty tools list") }, }, + { + name: "DeleteTools with non-existent tools does nothing and not receives notifications from MCPServer", + action: func(t *testing.T, server *MCPServer, notificationChannel chan mcp.JSONRPCNotification) { + err := server.RegisterSession(context.TODO(), &fakeSession{ + sessionID: "test", + notificationChannel: notificationChannel, + initialized: true, + }) + require.NoError(t, err) + server.SetTools( + ServerTool{Tool: mcp.NewTool("test-tool-1")}, + ServerTool{Tool: mcp.NewTool("test-tool-2")}) + + // Remove non-existing tools + server.DeleteTools("test-tool-3", "test-tool-4") + }, + expectedNotifications: 1, + validate: func(t *testing.T, notifications []mcp.JSONRPCNotification, toolsList mcp.JSONRPCMessage) { + // Only one notification expected for SetTools + assert.Equal(t, mcp.MethodNotificationToolsListChanged, notifications[0].Method) + + // Confirm the tool list does not change + tools := toolsList.(mcp.JSONRPCResponse).Result.(mcp.ListToolsResult).Tools + assert.Len(t, tools, 2) + assert.Equal(t, "test-tool-1", tools[0].Name) + assert.Equal(t, "test-tool-2", tools[1].Name) + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From f8badd69d08f609cbbd7a218c3b2b8de05987277 Mon Sep 17 00:00:00 2001 From: Navendu Pottekkat Date: Sat, 10 May 2025 23:23:00 +0530 Subject: [PATCH 23/29] chore: replace `interface{}` with `any` (#261) --- client/client.go | 2 +- client/inprocess_test.go | 2 +- client/sse_test.go | 2 +- client/stdio_test.go | 2 +- client/transport/sse_test.go | 32 ++++----- client/transport/stdio_test.go | 26 ++++---- client/transport/streamable_http_test.go | 32 ++++----- examples/custom_context/main.go | 4 +- examples/everything/main.go | 18 ++--- examples/filesystem_stdio_client/main.go | 10 +-- mcp/tools.go | 84 ++++++++++++------------ mcp/tools_test.go | 52 +++++++-------- mcp/types.go | 66 +++++++++---------- mcp/utils.go | 26 ++++---- server/server.go | 30 ++++----- server/server_race_test.go | 2 +- server/server_test.go | 4 +- server/session.go | 10 +-- server/session_test.go | 8 +-- server/sse.go | 10 +-- server/sse_test.go | 64 +++++++++--------- server/stdio_test.go | 24 +++---- testdata/mockstdio_server.go | 50 +++++++------- 23 files changed, 280 insertions(+), 280 deletions(-) diff --git a/client/client.go b/client/client.go index 7854ccbc..7689633c 100644 --- a/client/client.go +++ b/client/client.go @@ -94,7 +94,7 @@ func (c *Client) OnNotification( func (c *Client) sendRequest( ctx context.Context, method string, - params interface{}, + params any, ) (*json.RawMessage, error) { if !c.initialized && method != "initialize" { return nil, fmt.Errorf("client not initialized") diff --git a/client/inprocess_test.go b/client/inprocess_test.go index 71f86a48..c9b63b47 100644 --- a/client/inprocess_test.go +++ b/client/inprocess_test.go @@ -196,7 +196,7 @@ func TestInProcessMCPClient(t *testing.T) { request := mcp.CallToolRequest{} request.Params.Name = "test-tool" - request.Params.Arguments = map[string]interface{}{ + request.Params.Arguments = map[string]any{ "parameter-1": "value1", } diff --git a/client/sse_test.go b/client/sse_test.go index 8e3607f6..7ff2a16a 100644 --- a/client/sse_test.go +++ b/client/sse_test.go @@ -238,7 +238,7 @@ func TestSSEMCPClient(t *testing.T) { request := mcp.CallToolRequest{} request.Params.Name = "test-tool" - request.Params.Arguments = map[string]interface{}{ + request.Params.Arguments = map[string]any{ "parameter-1": "value1", } diff --git a/client/stdio_test.go b/client/stdio_test.go index 48514d91..b6faf9bf 100644 --- a/client/stdio_test.go +++ b/client/stdio_test.go @@ -232,7 +232,7 @@ func TestStdioMCPClient(t *testing.T) { request := mcp.CallToolRequest{} request.Params.Name = "test-tool" - request.Params.Arguments = map[string]interface{}{ + request.Params.Arguments = map[string]any{ "param1": "value1", } diff --git a/client/transport/sse_test.go b/client/transport/sse_test.go index b8b59d06..230157d2 100644 --- a/client/transport/sse_test.go +++ b/client/transport/sse_test.go @@ -64,7 +64,7 @@ func startMockSSEEchoServer() (string, func()) { } // Parse incoming JSON-RPC request - var request map[string]interface{} + var request map[string]any decoder := json.NewDecoder(r.Body) if err := decoder.Decode(&request); err != nil { http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) @@ -72,7 +72,7 @@ func startMockSSEEchoServer() (string, func()) { } // Echo back the request as the response result - response := map[string]interface{}{ + response := map[string]any{ "jsonrpc": "2.0", "id": request["id"], "result": request, @@ -96,7 +96,7 @@ func startMockSSEEchoServer() (string, func()) { mu.Unlock() case "debug/echo_error_string": data, _ := json.Marshal(request) - response["error"] = map[string]interface{}{ + response["error"] = map[string]any{ "code": -1, "message": string(data), } @@ -153,9 +153,9 @@ func TestSSE(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - params := map[string]interface{}{ + params := map[string]any{ "string": "hello world", - "array": []interface{}{1, 2, 3}, + "array": []any{1, 2, 3}, } request := JSONRPCRequest{ @@ -173,10 +173,10 @@ func TestSSE(t *testing.T) { // Parse the result to verify echo var result struct { - JSONRPC string `json:"jsonrpc"` - ID int64 `json:"id"` - Method string `json:"method"` - Params map[string]interface{} `json:"params"` + JSONRPC string `json:"jsonrpc"` + ID int64 `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` } if err := json.Unmarshal(response.Result, &result); err != nil { @@ -198,7 +198,7 @@ func TestSSE(t *testing.T) { t.Errorf("Expected string 'hello world', got %v", result.Params["string"]) } - if arr, ok := result.Params["array"].([]interface{}); !ok || len(arr) != 3 { + if arr, ok := result.Params["array"].([]any); !ok || len(arr) != 3 { t.Errorf("Expected array with 3 items, got %v", result.Params["array"]) } }) @@ -244,7 +244,7 @@ func TestSSE(t *testing.T) { Notification: mcp.Notification{ Method: "debug/echo_notification", Params: mcp.NotificationParams{ - AdditionalFields: map[string]interface{}{"test": "value"}, + AdditionalFields: map[string]any{"test": "value"}, }, }, } @@ -294,7 +294,7 @@ func TestSSE(t *testing.T) { JSONRPC: "2.0", ID: int64(100 + idx), Method: "debug/echo", - Params: map[string]interface{}{ + Params: map[string]any{ "requestIndex": idx, "timestamp": time.Now().UnixNano(), }, @@ -324,10 +324,10 @@ func TestSSE(t *testing.T) { // Parse the result to verify echo var result struct { - JSONRPC string `json:"jsonrpc"` - ID int64 `json:"id"` - Method string `json:"method"` - Params map[string]interface{} `json:"params"` + JSONRPC string `json:"jsonrpc"` + ID int64 `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` } if err := json.Unmarshal(responses[i].Result, &result); err != nil { diff --git a/client/transport/stdio_test.go b/client/transport/stdio_test.go index cb25bf79..155859e1 100644 --- a/client/transport/stdio_test.go +++ b/client/transport/stdio_test.go @@ -73,9 +73,9 @@ func TestStdio(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5000000000*time.Second) defer cancel() - params := map[string]interface{}{ + params := map[string]any{ "string": "hello world", - "array": []interface{}{1, 2, 3}, + "array": []any{1, 2, 3}, } request := JSONRPCRequest{ @@ -93,10 +93,10 @@ func TestStdio(t *testing.T) { // Parse the result to verify echo var result struct { - JSONRPC string `json:"jsonrpc"` - ID int64 `json:"id"` - Method string `json:"method"` - Params map[string]interface{} `json:"params"` + JSONRPC string `json:"jsonrpc"` + ID int64 `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` } if err := json.Unmarshal(response.Result, &result); err != nil { @@ -118,7 +118,7 @@ func TestStdio(t *testing.T) { t.Errorf("Expected string 'hello world', got %v", result.Params["string"]) } - if arr, ok := result.Params["array"].([]interface{}); !ok || len(arr) != 3 { + if arr, ok := result.Params["array"].([]any); !ok || len(arr) != 3 { t.Errorf("Expected array with 3 items, got %v", result.Params["array"]) } }) @@ -164,7 +164,7 @@ func TestStdio(t *testing.T) { Notification: mcp.Notification{ Method: "debug/echo_notification", Params: mcp.NotificationParams{ - AdditionalFields: map[string]interface{}{"test": "value"}, + AdditionalFields: map[string]any{"test": "value"}, }, }, } @@ -213,7 +213,7 @@ func TestStdio(t *testing.T) { JSONRPC: "2.0", ID: int64(100 + idx), Method: "debug/echo", - Params: map[string]interface{}{ + Params: map[string]any{ "requestIndex": idx, "timestamp": time.Now().UnixNano(), }, @@ -243,10 +243,10 @@ func TestStdio(t *testing.T) { // Parse the result to verify echo var result struct { - JSONRPC string `json:"jsonrpc"` - ID int64 `json:"id"` - Method string `json:"method"` - Params map[string]interface{} `json:"params"` + JSONRPC string `json:"jsonrpc"` + ID int64 `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` } if err := json.Unmarshal(responses[i].Result, &result); err != nil { diff --git a/client/transport/streamable_http_test.go b/client/transport/streamable_http_test.go index b7b76b96..deff2963 100644 --- a/client/transport/streamable_http_test.go +++ b/client/transport/streamable_http_test.go @@ -46,7 +46,7 @@ func startMockStreamableHTTPServer() (string, func()) { w.Header().Set("Mcp-Session-Id", sessionID) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusAccepted) - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "jsonrpc": "2.0", "id": request["id"], "result": "initialized", @@ -62,7 +62,7 @@ func startMockStreamableHTTPServer() (string, func()) { // Echo back the request as the response result w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "jsonrpc": "2.0", "id": request["id"], "result": request, @@ -104,10 +104,10 @@ func startMockStreamableHTTPServer() (string, func()) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) data, _ := json.Marshal(request) - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "jsonrpc": "2.0", "id": request["id"], - "error": map[string]interface{}{ + "error": map[string]any{ "code": -1, "message": string(data), }, @@ -152,9 +152,9 @@ func TestStreamableHTTP(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - params := map[string]interface{}{ + params := map[string]any{ "string": "hello world", - "array": []interface{}{1, 2, 3}, + "array": []any{1, 2, 3}, } request := JSONRPCRequest{ @@ -172,10 +172,10 @@ func TestStreamableHTTP(t *testing.T) { // Parse the result to verify echo var result struct { - JSONRPC string `json:"jsonrpc"` - ID int64 `json:"id"` - Method string `json:"method"` - Params map[string]interface{} `json:"params"` + JSONRPC string `json:"jsonrpc"` + ID int64 `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` } if err := json.Unmarshal(response.Result, &result); err != nil { @@ -197,7 +197,7 @@ func TestStreamableHTTP(t *testing.T) { t.Errorf("Expected string 'hello world', got %v", result.Params["string"]) } - if arr, ok := result.Params["array"].([]interface{}); !ok || len(arr) != 3 { + if arr, ok := result.Params["array"].([]any); !ok || len(arr) != 3 { t.Errorf("Expected array with 3 items, got %v", result.Params["array"]) } }) @@ -295,7 +295,7 @@ func TestStreamableHTTP(t *testing.T) { JSONRPC: "2.0", ID: int64(100 + idx), Method: "debug/echo", - Params: map[string]interface{}{ + Params: map[string]any{ "requestIndex": idx, "timestamp": time.Now().UnixNano(), }, @@ -325,10 +325,10 @@ func TestStreamableHTTP(t *testing.T) { // Parse the result to verify echo var result struct { - JSONRPC string `json:"jsonrpc"` - ID int64 `json:"id"` - Method string `json:"method"` - Params map[string]interface{} `json:"params"` + JSONRPC string `json:"jsonrpc"` + ID int64 `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` } if err := json.Unmarshal(responses[i].Result, &result); err != nil { diff --git a/examples/custom_context/main.go b/examples/custom_context/main.go index 4d028876..03bb56e3 100644 --- a/examples/custom_context/main.go +++ b/examples/custom_context/main.go @@ -44,8 +44,8 @@ func tokenFromContext(ctx context.Context) (string, error) { } type response struct { - Args map[string]interface{} `json:"args"` - Headers map[string]string `json:"headers"` + Args map[string]any `json:"args"` + Headers map[string]string `json:"headers"` } // makeRequest makes a request to httpbin.org including the auth token in the request diff --git a/examples/everything/main.go b/examples/everything/main.go index fa1c3043..8f119703 100644 --- a/examples/everything/main.go +++ b/examples/everything/main.go @@ -137,12 +137,12 @@ func NewMCPServer() *server.MCPServer { // Description: "Samples from an LLM using MCP's sampling feature", // InputSchema: mcp.ToolInputSchema{ // Type: "object", - // Properties: map[string]interface{}{ - // "prompt": map[string]interface{}{ + // Properties: map[string]any{ + // "prompt": map[string]any{ // "type": "string", // "description": "The prompt to send to the LLM", // }, - // "maxTokens": map[string]interface{}{ + // "maxTokens": map[string]any{ // "type": "number", // "description": "Maximum number of tokens to generate", // "default": 100, @@ -190,9 +190,9 @@ func runUpdateInterval() { // Notification: mcp.Notification{ // Method: "resources/updated", // Params: struct { - // Meta map[string]interface{} `json:"_meta,omitempty"` + // Meta map[string]any `json:"_meta,omitempty"` // }{ - // Meta: map[string]interface{}{"uri": uri}, + // Meta: map[string]any{"uri": uri}, // }, // }, // }, @@ -333,7 +333,7 @@ func handleSendNotification( err := server.SendNotificationToClient( ctx, "notifications/progress", - map[string]interface{}{ + map[string]any{ "progress": 10, "total": 10, "progressToken": 0, @@ -370,7 +370,7 @@ func handleLongRunningOperationTool( server.SendNotificationToClient( ctx, "notifications/progress", - map[string]interface{}{ + map[string]any{ "progress": i, "total": int(steps), "progressToken": progressToken, @@ -394,7 +394,7 @@ func handleLongRunningOperationTool( }, nil } -// func (s *MCPServer) handleSampleLLMTool(arguments map[string]interface{}) (*mcp.CallToolResult, error) { +// func (s *MCPServer) handleSampleLLMTool(arguments map[string]any) (*mcp.CallToolResult, error) { // prompt, _ := arguments["prompt"].(string) // maxTokens, _ := arguments["maxTokens"].(float64) @@ -406,7 +406,7 @@ func handleLongRunningOperationTool( // ) // return &mcp.CallToolResult{ -// Content: []interface{}{ +// Content: []any{ // mcp.TextContent{ // Type: "text", // Text: fmt.Sprintf("LLM sampling result: %s", result), diff --git a/examples/filesystem_stdio_client/main.go b/examples/filesystem_stdio_client/main.go index 5a2d9af1..3dcd89fa 100644 --- a/examples/filesystem_stdio_client/main.go +++ b/examples/filesystem_stdio_client/main.go @@ -79,7 +79,7 @@ func main() { fmt.Println("Listing /tmp directory...") listTmpRequest := mcp.CallToolRequest{} listTmpRequest.Params.Name = "list_directory" - listTmpRequest.Params.Arguments = map[string]interface{}{ + listTmpRequest.Params.Arguments = map[string]any{ "path": "/tmp", } @@ -94,7 +94,7 @@ func main() { fmt.Println("Creating /tmp/mcp directory...") createDirRequest := mcp.CallToolRequest{} createDirRequest.Params.Name = "create_directory" - createDirRequest.Params.Arguments = map[string]interface{}{ + createDirRequest.Params.Arguments = map[string]any{ "path": "/tmp/mcp", } @@ -109,7 +109,7 @@ func main() { fmt.Println("Creating /tmp/mcp/hello.txt...") writeFileRequest := mcp.CallToolRequest{} writeFileRequest.Params.Name = "write_file" - writeFileRequest.Params.Arguments = map[string]interface{}{ + writeFileRequest.Params.Arguments = map[string]any{ "path": "/tmp/mcp/hello.txt", "content": "Hello World", } @@ -125,7 +125,7 @@ func main() { fmt.Println("Reading /tmp/mcp/hello.txt...") readFileRequest := mcp.CallToolRequest{} readFileRequest.Params.Name = "read_file" - readFileRequest.Params.Arguments = map[string]interface{}{ + readFileRequest.Params.Arguments = map[string]any{ "path": "/tmp/mcp/hello.txt", } @@ -139,7 +139,7 @@ func main() { fmt.Println("Getting info for /tmp/mcp/hello.txt...") fileInfoRequest := mcp.CallToolRequest{} fileInfoRequest.Params.Name = "get_file_info" - fileInfoRequest.Params.Arguments = map[string]interface{}{ + fileInfoRequest.Params.Arguments = map[string]any{ "path": "/tmp/mcp/hello.txt", } diff --git a/mcp/tools.go b/mcp/tools.go index f92c3388..4f69d874 100644 --- a/mcp/tools.go +++ b/mcp/tools.go @@ -44,8 +44,8 @@ type CallToolResult struct { type CallToolRequest struct { Request Params struct { - Name string `json:"name"` - Arguments map[string]interface{} `json:"arguments,omitempty"` + Name string `json:"name"` + Arguments map[string]any `json:"arguments,omitempty"` Meta *struct { // If specified, the caller is requesting out-of-band progress // notifications for this request (as represented by @@ -83,7 +83,7 @@ type Tool struct { // It handles marshaling either InputSchema or RawInputSchema based on which is set. func (t Tool) MarshalJSON() ([]byte, error) { // Create a map to build the JSON structure - m := make(map[string]interface{}, 3) + m := make(map[string]any, 3) // Add the name and description m["name"] = t.Name @@ -108,14 +108,14 @@ func (t Tool) MarshalJSON() ([]byte, error) { } type ToolInputSchema struct { - Type string `json:"type"` - Properties map[string]interface{} `json:"properties,omitempty"` - Required []string `json:"required,omitempty"` + Type string `json:"type"` + Properties map[string]any `json:"properties,omitempty"` + Required []string `json:"required,omitempty"` } // MarshalJSON implements the json.Marshaler interface for ToolInputSchema. func (tis ToolInputSchema) MarshalJSON() ([]byte, error) { - m := make(map[string]interface{}) + m := make(map[string]any) m["type"] = tis.Type // Marshal Properties to '{}' rather than `nil` when its length equals zero @@ -149,7 +149,7 @@ type ToolOption func(*Tool) // PropertyOption is a function that configures a property in a Tool's input schema. // It allows for flexible configuration of JSON Schema properties using the functional options pattern. -type PropertyOption func(map[string]interface{}) +type PropertyOption func(map[string]any) // // Core Tool Functions @@ -163,7 +163,7 @@ func NewTool(name string, opts ...ToolOption) Tool { Name: name, InputSchema: ToolInputSchema{ Type: "object", - Properties: make(map[string]interface{}), + Properties: make(map[string]any), Required: nil, // Will be omitted from JSON if empty }, Annotations: ToolAnnotation{ @@ -220,7 +220,7 @@ func WithToolAnnotation(annotation ToolAnnotation) ToolOption { // Description adds a description to a property in the JSON Schema. // The description should explain the purpose and expected values of the property. func Description(desc string) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["description"] = desc } } @@ -228,7 +228,7 @@ func Description(desc string) PropertyOption { // Required marks a property as required in the tool's input schema. // Required properties must be provided when using the tool. func Required() PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["required"] = true } } @@ -236,7 +236,7 @@ func Required() PropertyOption { // Title adds a display-friendly title to a property in the JSON Schema. // This title can be used by UI components to show a more readable property name. func Title(title string) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["title"] = title } } @@ -248,7 +248,7 @@ func Title(title string) PropertyOption { // DefaultString sets the default value for a string property. // This value will be used if the property is not explicitly provided. func DefaultString(value string) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["default"] = value } } @@ -256,7 +256,7 @@ func DefaultString(value string) PropertyOption { // Enum specifies a list of allowed values for a string property. // The property value must be one of the specified enum values. func Enum(values ...string) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["enum"] = values } } @@ -264,7 +264,7 @@ func Enum(values ...string) PropertyOption { // MaxLength sets the maximum length for a string property. // The string value must not exceed this length. func MaxLength(max int) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["maxLength"] = max } } @@ -272,7 +272,7 @@ func MaxLength(max int) PropertyOption { // MinLength sets the minimum length for a string property. // The string value must be at least this length. func MinLength(min int) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["minLength"] = min } } @@ -280,7 +280,7 @@ func MinLength(min int) PropertyOption { // Pattern sets a regex pattern that a string property must match. // The string value must conform to the specified regular expression. func Pattern(pattern string) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["pattern"] = pattern } } @@ -292,7 +292,7 @@ func Pattern(pattern string) PropertyOption { // DefaultNumber sets the default value for a number property. // This value will be used if the property is not explicitly provided. func DefaultNumber(value float64) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["default"] = value } } @@ -300,7 +300,7 @@ func DefaultNumber(value float64) PropertyOption { // Max sets the maximum value for a number property. // The number value must not exceed this maximum. func Max(max float64) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["maximum"] = max } } @@ -308,7 +308,7 @@ func Max(max float64) PropertyOption { // Min sets the minimum value for a number property. // The number value must not be less than this minimum. func Min(min float64) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["minimum"] = min } } @@ -316,7 +316,7 @@ func Min(min float64) PropertyOption { // MultipleOf specifies that a number must be a multiple of the given value. // The number value must be divisible by this value. func MultipleOf(value float64) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["multipleOf"] = value } } @@ -328,7 +328,7 @@ func MultipleOf(value float64) PropertyOption { // DefaultBool sets the default value for a boolean property. // This value will be used if the property is not explicitly provided. func DefaultBool(value bool) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["default"] = value } } @@ -340,7 +340,7 @@ func DefaultBool(value bool) PropertyOption { // DefaultArray sets the default value for an array property. // This value will be used if the property is not explicitly provided. func DefaultArray[T any](value []T) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["default"] = value } } @@ -353,7 +353,7 @@ func DefaultArray[T any](value []T) PropertyOption { // It accepts property options to configure the boolean property's behavior and constraints. func WithBoolean(name string, opts ...PropertyOption) ToolOption { return func(t *Tool) { - schema := map[string]interface{}{ + schema := map[string]any{ "type": "boolean", } @@ -375,7 +375,7 @@ func WithBoolean(name string, opts ...PropertyOption) ToolOption { // It accepts property options to configure the number property's behavior and constraints. func WithNumber(name string, opts ...PropertyOption) ToolOption { return func(t *Tool) { - schema := map[string]interface{}{ + schema := map[string]any{ "type": "number", } @@ -397,7 +397,7 @@ func WithNumber(name string, opts ...PropertyOption) ToolOption { // It accepts property options to configure the string property's behavior and constraints. func WithString(name string, opts ...PropertyOption) ToolOption { return func(t *Tool) { - schema := map[string]interface{}{ + schema := map[string]any{ "type": "string", } @@ -419,9 +419,9 @@ func WithString(name string, opts ...PropertyOption) ToolOption { // It accepts property options to configure the object property's behavior and constraints. func WithObject(name string, opts ...PropertyOption) ToolOption { return func(t *Tool) { - schema := map[string]interface{}{ + schema := map[string]any{ "type": "object", - "properties": map[string]interface{}{}, + "properties": map[string]any{}, } for _, opt := range opts { @@ -442,7 +442,7 @@ func WithObject(name string, opts ...PropertyOption) ToolOption { // It accepts property options to configure the array property's behavior and constraints. func WithArray(name string, opts ...PropertyOption) ToolOption { return func(t *Tool) { - schema := map[string]interface{}{ + schema := map[string]any{ "type": "array", } @@ -461,65 +461,65 @@ func WithArray(name string, opts ...PropertyOption) ToolOption { } // Properties defines the properties for an object schema -func Properties(props map[string]interface{}) PropertyOption { - return func(schema map[string]interface{}) { +func Properties(props map[string]any) PropertyOption { + return func(schema map[string]any) { schema["properties"] = props } } // AdditionalProperties specifies whether additional properties are allowed in the object // or defines a schema for additional properties -func AdditionalProperties(schema interface{}) PropertyOption { - return func(schemaMap map[string]interface{}) { +func AdditionalProperties(schema any) PropertyOption { + return func(schemaMap map[string]any) { schemaMap["additionalProperties"] = schema } } // MinProperties sets the minimum number of properties for an object func MinProperties(min int) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["minProperties"] = min } } // MaxProperties sets the maximum number of properties for an object func MaxProperties(max int) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["maxProperties"] = max } } // PropertyNames defines a schema for property names in an object -func PropertyNames(schema map[string]interface{}) PropertyOption { - return func(schemaMap map[string]interface{}) { +func PropertyNames(schema map[string]any) PropertyOption { + return func(schemaMap map[string]any) { schemaMap["propertyNames"] = schema } } // Items defines the schema for array items -func Items(schema interface{}) PropertyOption { - return func(schemaMap map[string]interface{}) { +func Items(schema any) PropertyOption { + return func(schemaMap map[string]any) { schemaMap["items"] = schema } } // MinItems sets the minimum number of items for an array func MinItems(min int) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["minItems"] = min } } // MaxItems sets the maximum number of items for an array func MaxItems(max int) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["maxItems"] = max } } // UniqueItems specifies whether array items must be unique func UniqueItems(unique bool) PropertyOption { - return func(schema map[string]interface{}) { + return func(schema map[string]any) { schema["uniqueItems"] = unique } } diff --git a/mcp/tools_test.go b/mcp/tools_test.go index 872749e1..e2be72fb 100644 --- a/mcp/tools_test.go +++ b/mcp/tools_test.go @@ -50,7 +50,7 @@ func TestToolWithRawSchema(t *testing.T) { assert.NoError(t, err) // Unmarshal to verify the structure - var result map[string]interface{} + var result map[string]any err = json.Unmarshal(data, &result) assert.NoError(t, err) @@ -59,18 +59,18 @@ func TestToolWithRawSchema(t *testing.T) { assert.Equal(t, "Search API", result["description"]) // Verify schema was properly included - schema, ok := result["inputSchema"].(map[string]interface{}) + schema, ok := result["inputSchema"].(map[string]any) assert.True(t, ok) assert.Equal(t, "object", schema["type"]) - properties, ok := schema["properties"].(map[string]interface{}) + properties, ok := schema["properties"].(map[string]any) assert.True(t, ok) - query, ok := properties["query"].(map[string]interface{}) + query, ok := properties["query"].(map[string]any) assert.True(t, ok) assert.Equal(t, "string", query["type"]) - required, ok := schema["required"].([]interface{}) + required, ok := schema["required"].([]any) assert.True(t, ok) assert.Contains(t, required, "query") } @@ -105,12 +105,12 @@ func TestUnmarshalToolWithRawSchema(t *testing.T) { // Verify schema was properly included assert.Equal(t, "object", toolUnmarshalled.InputSchema.Type) assert.Contains(t, toolUnmarshalled.InputSchema.Properties, "query") - assert.Subset(t, toolUnmarshalled.InputSchema.Properties["query"], map[string]interface{}{ + assert.Subset(t, toolUnmarshalled.InputSchema.Properties["query"], map[string]any{ "type": "string", "description": "Search query", }) assert.Contains(t, toolUnmarshalled.InputSchema.Properties, "limit") - assert.Subset(t, toolUnmarshalled.InputSchema.Properties["limit"], map[string]interface{}{ + assert.Subset(t, toolUnmarshalled.InputSchema.Properties["limit"], map[string]any{ "type": "integer", "minimum": 1.0, "maximum": 50.0, @@ -136,7 +136,7 @@ func TestUnmarshalToolWithoutRawSchema(t *testing.T) { // Verify tool properties assert.Equal(t, tool.Name, toolUnmarshalled.Name) assert.Equal(t, tool.Description, toolUnmarshalled.Description) - assert.Subset(t, toolUnmarshalled.InputSchema.Properties["input"], map[string]interface{}{ + assert.Subset(t, toolUnmarshalled.InputSchema.Properties["input"], map[string]any{ "type": "string", "description": "Test input", }) @@ -150,13 +150,13 @@ func TestToolWithObjectAndArray(t *testing.T) { WithDescription("A tool for managing reading lists"), WithObject("preferences", Description("User preferences for the reading list"), - Properties(map[string]interface{}{ - "theme": map[string]interface{}{ + Properties(map[string]any{ + "theme": map[string]any{ "type": "string", "description": "UI theme preference", "enum": []string{"light", "dark"}, }, - "maxItems": map[string]interface{}{ + "maxItems": map[string]any{ "type": "number", "description": "Maximum number of items in the list", "minimum": 1, @@ -166,19 +166,19 @@ func TestToolWithObjectAndArray(t *testing.T) { WithArray("books", Description("List of books to read"), Required(), - Items(map[string]interface{}{ + Items(map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "title": map[string]interface{}{ + "properties": map[string]any{ + "title": map[string]any{ "type": "string", "description": "Book title", "required": true, }, - "author": map[string]interface{}{ + "author": map[string]any{ "type": "string", "description": "Book author", }, - "year": map[string]interface{}{ + "year": map[string]any{ "type": "number", "description": "Publication year", "minimum": 1000, @@ -191,7 +191,7 @@ func TestToolWithObjectAndArray(t *testing.T) { assert.NoError(t, err) // Unmarshal to verify the structure - var result map[string]interface{} + var result map[string]any err = json.Unmarshal(data, &result) assert.NoError(t, err) @@ -200,44 +200,44 @@ func TestToolWithObjectAndArray(t *testing.T) { assert.Equal(t, "A tool for managing reading lists", result["description"]) // Verify schema was properly included - schema, ok := result["inputSchema"].(map[string]interface{}) + schema, ok := result["inputSchema"].(map[string]any) assert.True(t, ok) assert.Equal(t, "object", schema["type"]) // Verify properties - properties, ok := schema["properties"].(map[string]interface{}) + properties, ok := schema["properties"].(map[string]any) assert.True(t, ok) // Verify preferences object - preferences, ok := properties["preferences"].(map[string]interface{}) + preferences, ok := properties["preferences"].(map[string]any) assert.True(t, ok) assert.Equal(t, "object", preferences["type"]) assert.Equal(t, "User preferences for the reading list", preferences["description"]) - prefProps, ok := preferences["properties"].(map[string]interface{}) + prefProps, ok := preferences["properties"].(map[string]any) assert.True(t, ok) assert.Contains(t, prefProps, "theme") assert.Contains(t, prefProps, "maxItems") // Verify books array - books, ok := properties["books"].(map[string]interface{}) + books, ok := properties["books"].(map[string]any) assert.True(t, ok) assert.Equal(t, "array", books["type"]) assert.Equal(t, "List of books to read", books["description"]) // Verify array items schema - items, ok := books["items"].(map[string]interface{}) + items, ok := books["items"].(map[string]any) assert.True(t, ok) assert.Equal(t, "object", items["type"]) - itemProps, ok := items["properties"].(map[string]interface{}) + itemProps, ok := items["properties"].(map[string]any) assert.True(t, ok) assert.Contains(t, itemProps, "title") assert.Contains(t, itemProps, "author") assert.Contains(t, itemProps, "year") // Verify required fields - required, ok := schema["required"].([]interface{}) + required, ok := schema["required"].([]any) assert.True(t, ok) assert.Contains(t, required, "books") } @@ -245,7 +245,7 @@ func TestToolWithObjectAndArray(t *testing.T) { func TestParseToolCallToolRequest(t *testing.T) { request := CallToolRequest{} request.Params.Name = "test-tool" - request.Params.Arguments = map[string]interface{}{ + request.Params.Arguments = map[string]any{ "bool_value": "true", "int64_value": "123456789", "int32_value": "123456789", diff --git a/mcp/types.go b/mcp/types.go index 53cc3283..250bc746 100644 --- a/mcp/types.go +++ b/mcp/types.go @@ -86,7 +86,7 @@ func (t *URITemplate) UnmarshalJSON(data []byte) error { /* JSON-RPC types */ // JSONRPCMessage represents either a JSONRPCRequest, JSONRPCNotification, JSONRPCResponse, or JSONRPCError -type JSONRPCMessage interface{} +type JSONRPCMessage any // LATEST_PROTOCOL_VERSION is the most recent version of the MCP protocol. const LATEST_PROTOCOL_VERSION = "2024-11-05" @@ -95,7 +95,7 @@ const LATEST_PROTOCOL_VERSION = "2024-11-05" const JSONRPC_VERSION = "2.0" // ProgressToken is used to associate progress notifications with the original request. -type ProgressToken interface{} +type ProgressToken any // Cursor is an opaque token used to represent a cursor for pagination. type Cursor string @@ -115,7 +115,7 @@ type Request struct { } `json:"params,omitempty"` } -type Params map[string]interface{} +type Params map[string]any type Notification struct { Method string `json:"method"` @@ -125,16 +125,16 @@ type Notification struct { type NotificationParams struct { // This parameter name is reserved by MCP to allow clients and // servers to attach additional metadata to their notifications. - Meta map[string]interface{} `json:"_meta,omitempty"` + Meta map[string]any `json:"_meta,omitempty"` // Additional fields can be added to this map - AdditionalFields map[string]interface{} `json:"-"` + AdditionalFields map[string]any `json:"-"` } // MarshalJSON implements custom JSON marshaling func (p NotificationParams) MarshalJSON() ([]byte, error) { // Create a map to hold all fields - m := make(map[string]interface{}) + m := make(map[string]any) // Add Meta if it exists if p.Meta != nil { @@ -155,24 +155,24 @@ func (p NotificationParams) MarshalJSON() ([]byte, error) { // UnmarshalJSON implements custom JSON unmarshaling func (p *NotificationParams) UnmarshalJSON(data []byte) error { // Create a map to hold all fields - var m map[string]interface{} + var m map[string]any if err := json.Unmarshal(data, &m); err != nil { return err } // Initialize maps if they're nil if p.Meta == nil { - p.Meta = make(map[string]interface{}) + p.Meta = make(map[string]any) } if p.AdditionalFields == nil { - p.AdditionalFields = make(map[string]interface{}) + p.AdditionalFields = make(map[string]any) } // Process all fields for k, v := range m { if k == "_meta" { // Handle Meta field - if meta, ok := v.(map[string]interface{}); ok { + if meta, ok := v.(map[string]any); ok { p.Meta = meta } } else { @@ -187,18 +187,18 @@ func (p *NotificationParams) UnmarshalJSON(data []byte) error { type Result struct { // This result property is reserved by the protocol to allow clients and // servers to attach additional metadata to their responses. - Meta map[string]interface{} `json:"_meta,omitempty"` + Meta map[string]any `json:"_meta,omitempty"` } // RequestId is a uniquely identifying ID for a request in JSON-RPC. // It can be any JSON-serializable value, typically a number or string. -type RequestId interface{} +type RequestId any // JSONRPCRequest represents a request that expects a response. type JSONRPCRequest struct { - JSONRPC string `json:"jsonrpc"` - ID RequestId `json:"id"` - Params interface{} `json:"params,omitempty"` + JSONRPC string `json:"jsonrpc"` + ID RequestId `json:"id"` + Params any `json:"params,omitempty"` Request } @@ -210,9 +210,9 @@ type JSONRPCNotification struct { // JSONRPCResponse represents a successful (non-error) response to a request. type JSONRPCResponse struct { - JSONRPC string `json:"jsonrpc"` - ID RequestId `json:"id"` - Result interface{} `json:"result"` + JSONRPC string `json:"jsonrpc"` + ID RequestId `json:"id"` + Result any `json:"result"` } // JSONRPCError represents a non-successful (error) response to a request. @@ -227,7 +227,7 @@ type JSONRPCError struct { Message string `json:"message"` // Additional information about the error. The value of this member // is defined by the sender (e.g. detailed error information, nested errors etc.). - Data interface{} `json:"data,omitempty"` + Data any `json:"data,omitempty"` } `json:"error"` } @@ -322,7 +322,7 @@ type InitializedNotification struct { // client can define its own, additional capabilities. type ClientCapabilities struct { // Experimental, non-standard capabilities that the client supports. - Experimental map[string]interface{} `json:"experimental,omitempty"` + Experimental map[string]any `json:"experimental,omitempty"` // Present if the client supports listing roots. Roots *struct { // Whether the client supports notifications for changes to the roots list. @@ -337,7 +337,7 @@ type ClientCapabilities struct { // server can define its own, additional capabilities. type ServerCapabilities struct { // Experimental, non-standard capabilities that the server supports. - Experimental map[string]interface{} `json:"experimental,omitempty"` + Experimental map[string]any `json:"experimental,omitempty"` // Present if the server supports sending log messages to the client. Logging *struct{} `json:"logging,omitempty"` // Present if the server offers any prompt templates. @@ -452,7 +452,7 @@ type ReadResourceRequest struct { // to the server how to interpret it. URI string `json:"uri"` // Arguments to pass to the resource handler - Arguments map[string]interface{} `json:"arguments,omitempty"` + Arguments map[string]any `json:"arguments,omitempty"` } `json:"params"` } @@ -599,7 +599,7 @@ type LoggingMessageNotification struct { Logger string `json:"logger,omitempty"` // The data to be logged, such as a string message or an object. Any JSON // serializable type is allowed here. - Data interface{} `json:"data"` + Data any `json:"data"` } `json:"params"` } @@ -636,7 +636,7 @@ type CreateMessageRequest struct { Temperature float64 `json:"temperature,omitempty"` MaxTokens int `json:"maxTokens"` StopSequences []string `json:"stopSequences,omitempty"` - Metadata interface{} `json:"metadata,omitempty"` + Metadata any `json:"metadata,omitempty"` } `json:"params"` } @@ -655,8 +655,8 @@ type CreateMessageResult struct { // SamplingMessage describes a message issued to or received from an LLM API. type SamplingMessage struct { - Role Role `json:"role"` - Content interface{} `json:"content"` // Can be TextContent, ImageContent or AudioContent + Role Role `json:"role"` + Content any `json:"content"` // Can be TextContent, ImageContent or AudioContent } type Annotations struct { @@ -796,7 +796,7 @@ type ModelHint struct { type CompleteRequest struct { Request Params struct { - Ref interface{} `json:"ref"` // Can be PromptReference or ResourceReference + Ref any `json:"ref"` // Can be PromptReference or ResourceReference Argument struct { // The name of the argument Name string `json:"name"` @@ -877,19 +877,19 @@ type RootsListChangedNotification struct { } // ClientRequest represents any request that can be sent from client to server. -type ClientRequest interface{} +type ClientRequest any // ClientNotification represents any notification that can be sent from client to server. -type ClientNotification interface{} +type ClientNotification any // ClientResult represents any result that can be sent from client to server. -type ClientResult interface{} +type ClientResult any // ServerRequest represents any request that can be sent from server to client. -type ServerRequest interface{} +type ServerRequest any // ServerNotification represents any notification that can be sent from server to client. -type ServerNotification interface{} +type ServerNotification any // ServerResult represents any result that can be sent from server to client. -type ServerResult interface{} +type ServerResult any diff --git a/mcp/utils.go b/mcp/utils.go index 02f12812..eaecff2d 100644 --- a/mcp/utils.go +++ b/mcp/utils.go @@ -60,7 +60,7 @@ var _ ServerResult = &ListToolsResult{} // Helper functions for type assertions // asType attempts to cast the given interface to the given type -func asType[T any](content interface{}) (*T, bool) { +func asType[T any](content any) (*T, bool) { tc, ok := content.(T) if !ok { return nil, false @@ -69,32 +69,32 @@ func asType[T any](content interface{}) (*T, bool) { } // AsTextContent attempts to cast the given interface to TextContent -func AsTextContent(content interface{}) (*TextContent, bool) { +func AsTextContent(content any) (*TextContent, bool) { return asType[TextContent](content) } // AsImageContent attempts to cast the given interface to ImageContent -func AsImageContent(content interface{}) (*ImageContent, bool) { +func AsImageContent(content any) (*ImageContent, bool) { return asType[ImageContent](content) } // AsAudioContent attempts to cast the given interface to AudioContent -func AsAudioContent(content interface{}) (*AudioContent, bool) { +func AsAudioContent(content any) (*AudioContent, bool) { return asType[AudioContent](content) } // AsEmbeddedResource attempts to cast the given interface to EmbeddedResource -func AsEmbeddedResource(content interface{}) (*EmbeddedResource, bool) { +func AsEmbeddedResource(content any) (*EmbeddedResource, bool) { return asType[EmbeddedResource](content) } // AsTextResourceContents attempts to cast the given interface to TextResourceContents -func AsTextResourceContents(content interface{}) (*TextResourceContents, bool) { +func AsTextResourceContents(content any) (*TextResourceContents, bool) { return asType[TextResourceContents](content) } // AsBlobResourceContents attempts to cast the given interface to BlobResourceContents -func AsBlobResourceContents(content interface{}) (*BlobResourceContents, bool) { +func AsBlobResourceContents(content any) (*BlobResourceContents, bool) { return asType[BlobResourceContents](content) } @@ -114,15 +114,15 @@ func NewJSONRPCError( id RequestId, code int, message string, - data interface{}, + data any, ) JSONRPCError { return JSONRPCError{ JSONRPC: JSONRPC_VERSION, ID: id, Error: struct { - Code int `json:"code"` - Message string `json:"message"` - Data interface{} `json:"data,omitempty"` + Code int `json:"code"` + Message string `json:"message"` + Data any `json:"data,omitempty"` }{ Code: code, Message: message, @@ -167,7 +167,7 @@ func NewProgressNotification( func NewLoggingMessageNotification( level LoggingLevel, logger string, - data interface{}, + data any, ) LoggingMessageNotification { return LoggingMessageNotification{ Notification: Notification{ @@ -176,7 +176,7 @@ func NewLoggingMessageNotification( Params: struct { Level LoggingLevel `json:"level"` Logger string `json:"logger,omitempty"` - Data interface{} `json:"data"` + Data any `json:"data"` }{ Level: level, Logger: logger, diff --git a/server/server.go b/server/server.go index dc4e16a9..b7a54ce3 100644 --- a/server/server.go +++ b/server/server.go @@ -479,7 +479,7 @@ func (s *MCPServer) AddNotificationHandler( func (s *MCPServer) handleInitialize( ctx context.Context, - id interface{}, + id any, request mcp.InitializeRequest, ) (*mcp.InitializeResult, *requestError) { capabilities := mcp.ServerCapabilities{} @@ -535,7 +535,7 @@ func (s *MCPServer) handleInitialize( func (s *MCPServer) handlePing( ctx context.Context, - id interface{}, + id any, request mcp.PingRequest, ) (*mcp.EmptyResult, *requestError) { return &mcp.EmptyResult{}, nil @@ -579,7 +579,7 @@ func listByPagination[T any]( func (s *MCPServer) handleListResources( ctx context.Context, - id interface{}, + id any, request mcp.ListResourcesRequest, ) (*mcp.ListResourcesResult, *requestError) { s.resourcesMu.RLock() @@ -612,7 +612,7 @@ func (s *MCPServer) handleListResources( func (s *MCPServer) handleListResourceTemplates( ctx context.Context, - id interface{}, + id any, request mcp.ListResourceTemplatesRequest, ) (*mcp.ListResourceTemplatesResult, *requestError) { s.resourcesMu.RLock() @@ -643,7 +643,7 @@ func (s *MCPServer) handleListResourceTemplates( func (s *MCPServer) handleReadResource( ctx context.Context, - id interface{}, + id any, request mcp.ReadResourceRequest, ) (*mcp.ReadResourceResult, *requestError) { s.resourcesMu.RLock() @@ -672,7 +672,7 @@ func (s *MCPServer) handleReadResource( matched = true matchedVars := template.URITemplate.Match(request.Params.URI) // Convert matched variables to a map - request.Params.Arguments = make(map[string]interface{}, len(matchedVars)) + request.Params.Arguments = make(map[string]any, len(matchedVars)) for name, value := range matchedVars { request.Params.Arguments[name] = value.V } @@ -707,7 +707,7 @@ func matchesTemplate(uri string, template *mcp.URITemplate) bool { func (s *MCPServer) handleListPrompts( ctx context.Context, - id interface{}, + id any, request mcp.ListPromptsRequest, ) (*mcp.ListPromptsResult, *requestError) { s.promptsMu.RLock() @@ -740,7 +740,7 @@ func (s *MCPServer) handleListPrompts( func (s *MCPServer) handleGetPrompt( ctx context.Context, - id interface{}, + id any, request mcp.GetPromptRequest, ) (*mcp.GetPromptResult, *requestError) { s.promptsMu.RLock() @@ -769,7 +769,7 @@ func (s *MCPServer) handleGetPrompt( func (s *MCPServer) handleListTools( ctx context.Context, - id interface{}, + id any, request mcp.ListToolsRequest, ) (*mcp.ListToolsResult, *requestError) { // Get the base tools from the server @@ -854,7 +854,7 @@ func (s *MCPServer) handleListTools( func (s *MCPServer) handleToolCall( ctx context.Context, - id interface{}, + id any, request mcp.CallToolRequest, ) (*mcp.CallToolResult, *requestError) { // First check session-specific tools @@ -926,7 +926,7 @@ func (s *MCPServer) handleNotification( return nil } -func createResponse(id interface{}, result interface{}) mcp.JSONRPCMessage { +func createResponse(id any, result any) mcp.JSONRPCMessage { return mcp.JSONRPCResponse{ JSONRPC: mcp.JSONRPC_VERSION, ID: id, @@ -935,7 +935,7 @@ func createResponse(id interface{}, result interface{}) mcp.JSONRPCMessage { } func createErrorResponse( - id interface{}, + id any, code int, message string, ) mcp.JSONRPCMessage { @@ -943,9 +943,9 @@ func createErrorResponse( JSONRPC: mcp.JSONRPC_VERSION, ID: id, Error: struct { - Code int `json:"code"` - Message string `json:"message"` - Data interface{} `json:"data,omitempty"` + Code int `json:"code"` + Message string `json:"message"` + Data any `json:"data,omitempty"` }{ Code: code, Message: message, diff --git a/server/server_race_test.go b/server/server_race_test.go index 8cc29476..c3a8d3e6 100644 --- a/server/server_race_test.go +++ b/server/server_race_test.go @@ -98,7 +98,7 @@ func TestRaceConditions(t *testing.T) { runConcurrentOperation(&wg, testDuration, "call-tools", func() { req := mcp.CallToolRequest{} req.Params.Name = "persistent-tool" - req.Params.Arguments = map[string]interface{}{"param": "test"} + req.Params.Arguments = map[string]any{"param": "test"} result, reqErr := srv.handleToolCall(ctx, "123", req) require.Nil(t, reqErr, "Tool call operation should not return an error") require.NotNil(t, result, "Tool call result should not be nil") diff --git a/server/server_test.go b/server/server_test.go index c3edd45f..7f5116a3 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -379,7 +379,7 @@ func TestMCPServer_HandleValidMessages(t *testing.T) { tests := []struct { name string - message interface{} + message any validate func(t *testing.T, response mcp.JSONRPCMessage) }{ { @@ -894,7 +894,7 @@ func TestMCPServer_HandleUndefinedHandlers(t *testing.T) { Description: "Test tool", InputSchema: mcp.ToolInputSchema{ Type: "object", - Properties: map[string]interface{}{}, + Properties: map[string]any{}, }, Annotations: mcp.ToolAnnotation{ Title: "test-tool", diff --git a/server/session.go b/server/session.go index 68ae0bb8..edf59f2f 100644 --- a/server/session.go +++ b/server/session.go @@ -105,7 +105,7 @@ func (s *MCPServer) SendNotificationToAllClients( go func(sessionID string, hooks *Hooks) { ctx := context.Background() // Use the error hook to report the blocked channel - hooks.onError(ctx, nil, "notification", map[string]interface{}{ + hooks.onError(ctx, nil, "notification", map[string]any{ "method": method, "sessionID": sessionID, }, fmt.Errorf("notification channel blocked for session %s: %w", sessionID, err)) @@ -149,7 +149,7 @@ func (s *MCPServer) SendNotificationToClient( hooks := s.hooks go func(sessionID string, hooks *Hooks) { // Use the error hook to report the blocked channel - hooks.onError(ctx, nil, "notification", map[string]interface{}{ + hooks.onError(ctx, nil, "notification", map[string]any{ "method": method, "sessionID": sessionID, }, fmt.Errorf("notification channel blocked for session %s: %w", sessionID, err)) @@ -197,7 +197,7 @@ func (s *MCPServer) SendNotificationToSpecificClient( hooks := s.hooks go func(sID string, hooks *Hooks) { // Use the error hook to report the blocked channel - hooks.onError(ctx, nil, "notification", map[string]interface{}{ + hooks.onError(ctx, nil, "notification", map[string]any{ "method": method, "sessionID": sID, }, fmt.Errorf("notification channel blocked for session %s: %w", sID, err)) @@ -253,7 +253,7 @@ func (s *MCPServer) AddSessionTools(sessionID string, tools ...ServerTool) error hooks := s.hooks go func(sID string, hooks *Hooks) { ctx := context.Background() - hooks.onError(ctx, nil, "notification", map[string]interface{}{ + hooks.onError(ctx, nil, "notification", map[string]any{ "method": "notifications/tools/list_changed", "sessionID": sID, }, fmt.Errorf("failed to send notification after adding tools: %w", err)) @@ -306,7 +306,7 @@ func (s *MCPServer) DeleteSessionTools(sessionID string, names ...string) error hooks := s.hooks go func(sID string, hooks *Hooks) { ctx := context.Background() - hooks.onError(ctx, nil, "notification", map[string]interface{}{ + hooks.onError(ctx, nil, "notification", map[string]any{ "method": "notifications/tools/list_changed", "sessionID": sID, }, fmt.Errorf("failed to send notification after deleting tools: %w", err)) diff --git a/server/session_test.go b/server/session_test.go index 42def221..3a135f83 100644 --- a/server/session_test.go +++ b/server/session_test.go @@ -130,7 +130,7 @@ func TestSessionWithTools_Integration(t *testing.T) { // Test that we can access the session-specific tool testReq := mcp.CallToolRequest{} testReq.Params.Name = "session-tool" - testReq.Params.Arguments = map[string]interface{}{} + testReq.Params.Arguments = map[string]any{} // Call using session context sessionCtx := server.WithContext(context.Background(), session) @@ -328,11 +328,11 @@ func TestMCPServer_CallSessionTool(t *testing.T) { // Call the tool using session context sessionCtx := server.WithContext(context.Background(), session) - toolRequest := map[string]interface{}{ + toolRequest := map[string]any{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", - "params": map[string]interface{}{ + "params": map[string]any{ "name": "test_tool", }, } @@ -545,7 +545,7 @@ func TestMCPServer_NotificationChannelBlocked(t *testing.T) { errorCaptured = true // Extract session ID and method from the error message metadata - if msgMap, ok := message.(map[string]interface{}); ok { + if msgMap, ok := message.(map[string]any); ok { if sid, ok := msgMap["sessionID"].(string); ok { errorSessionID = sid } diff --git a/server/sse.go b/server/sse.go index 65001ba0..98d72682 100644 --- a/server/sse.go +++ b/server/sse.go @@ -54,7 +54,7 @@ func (s *sseSession) Initialized() bool { func (s *sseSession) GetSessionTools() map[string]ServerTool { tools := make(map[string]ServerTool) - s.tools.Range(func(key, value interface{}) bool { + s.tools.Range(func(key, value any) bool { if tool, ok := value.(ServerTool); ok { tools[key.(string)] = tool } @@ -65,7 +65,7 @@ func (s *sseSession) GetSessionTools() map[string]ServerTool { func (s *sseSession) SetSessionTools(tools map[string]ServerTool) { // Clear existing tools - s.tools.Range(func(key, _ interface{}) bool { + s.tools.Range(func(key, _ any) bool { s.tools.Delete(key) return true }) @@ -251,7 +251,7 @@ func (s *SSEServer) Shutdown(ctx context.Context) error { s.mu.RUnlock() if srv != nil { - s.sessions.Range(func(key, value interface{}) bool { + s.sessions.Range(func(key, value any) bool { if session, ok := value.(*sseSession); ok { close(session.done) } @@ -473,7 +473,7 @@ func (s *SSEServer) handleMessage(w http.ResponseWriter, r *http.Request) { // writeJSONRPCError writes a JSON-RPC error response with the given error details. func (s *SSEServer) writeJSONRPCError( w http.ResponseWriter, - id interface{}, + id any, code int, message string, ) { @@ -487,7 +487,7 @@ func (s *SSEServer) writeJSONRPCError( // Returns an error if the session is not found or closed. func (s *SSEServer) SendEventToSession( sessionID string, - event interface{}, + event any, ) error { sessionI, ok := s.sessions.Load(sessionID) if !ok { diff --git a/server/sse_test.go b/server/sse_test.go index c3c8bb28..8ec8beb7 100644 --- a/server/sse_test.go +++ b/server/sse_test.go @@ -76,13 +76,13 @@ func TestSSEServer(t *testing.T) { ) // Send initialize request - initRequest := map[string]interface{}{ + initRequest := map[string]any{ "jsonrpc": "2.0", "id": 1, "method": "initialize", - "params": map[string]interface{}{ + "params": map[string]any{ "protocolVersion": "2024-11-05", - "clientInfo": map[string]interface{}{ + "clientInfo": map[string]any{ "name": "test-client", "version": "1.0.0", }, @@ -154,13 +154,13 @@ func TestSSEServer(t *testing.T) { ) // Send initialize request - initRequest := map[string]interface{}{ + initRequest := map[string]any{ "jsonrpc": "2.0", "id": sessionNum, "method": "initialize", - "params": map[string]interface{}{ + "params": map[string]any{ "protocolVersion": "2024-11-05", - "clientInfo": map[string]interface{}{ + "clientInfo": map[string]any{ "name": fmt.Sprintf( "test-client-%d", sessionNum, @@ -203,7 +203,7 @@ func TestSSEServer(t *testing.T) { strings.Split(strings.Split(endpointEvent, "data: ")[1], "\n")[0], ) - var response map[string]interface{} + var response map[string]any if err := json.NewDecoder(strings.NewReader(respFromSee)).Decode(&response); err != nil { t.Errorf( "Session %d: Failed to decode response: %v", @@ -385,13 +385,13 @@ func TestSSEServer(t *testing.T) { // The messageURL should already be correct since we set the baseURL correctly // Test message endpoint - initRequest := map[string]interface{}{ + initRequest := map[string]any{ "jsonrpc": "2.0", "id": 1, "method": "initialize", - "params": map[string]interface{}{ + "params": map[string]any{ "protocolVersion": "2024-11-05", - "clientInfo": map[string]interface{}{ + "clientInfo": map[string]any{ "name": "test-client", "version": "1.0.0", }, @@ -468,13 +468,13 @@ func TestSSEServer(t *testing.T) { // The messageURL should already be correct since we set the baseURL correctly // Test message endpoint - initRequest := map[string]interface{}{ + initRequest := map[string]any{ "jsonrpc": "2.0", "id": 1, "method": "initialize", - "params": map[string]interface{}{ + "params": map[string]any{ "protocolVersion": "2024-11-05", - "clientInfo": map[string]interface{}{ + "clientInfo": map[string]any{ "name": "test-client", "version": "1.0.0", }, @@ -598,13 +598,13 @@ func TestSSEServer(t *testing.T) { ) // Send initialize request - initRequest := map[string]interface{}{ + initRequest := map[string]any{ "jsonrpc": "2.0", "id": 1, "method": "initialize", - "params": map[string]interface{}{ + "params": map[string]any{ "protocolVersion": "2024-11-05", - "clientInfo": map[string]interface{}{ + "clientInfo": map[string]any{ "name": "test-client", "version": "1.0.0", }, @@ -639,7 +639,7 @@ func TestSSEServer(t *testing.T) { strings.Split(strings.Split(endpointEvent, "data: ")[1], "\n")[0], ) - var response map[string]interface{} + var response map[string]any if err := json.NewDecoder(strings.NewReader(respFromSSE)).Decode(&response); err != nil { t.Fatalf("Failed to decode response: %v", err) } @@ -652,11 +652,11 @@ func TestSSEServer(t *testing.T) { } // Call the tool. - toolRequest := map[string]interface{}{ + toolRequest := map[string]any{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", - "params": map[string]interface{}{ + "params": map[string]any{ "name": "test_tool", }, } @@ -688,7 +688,7 @@ func TestSSEServer(t *testing.T) { strings.Split(strings.Split(endpointEvent, "data: ")[1], "\n")[0], ) - response = make(map[string]interface{}) + response = make(map[string]any) if err := json.NewDecoder(strings.NewReader(respFromSSE)).Decode(&response); err != nil { t.Fatalf("Failed to decode response: %v", err) } @@ -699,7 +699,7 @@ func TestSSEServer(t *testing.T) { if response["id"].(float64) != 2 { t.Errorf("Expected id 2, got %v", response["id"]) } - if response["result"].(map[string]interface{})["content"].([]interface{})[0].(map[string]interface{})["text"] != "test_value" { + if response["result"].(map[string]any)["content"].([]any)[0].(map[string]any)["text"] != "test_value" { t.Errorf("Expected result 'test_value', got %v", response["result"]) } if response["error"] != nil { @@ -922,13 +922,13 @@ func TestSSEServer(t *testing.T) { } // Optionally, test sending a message to the message endpoint - initRequest := map[string]interface{}{ + initRequest := map[string]any{ "jsonrpc": "2.0", "id": 1, "method": "initialize", - "params": map[string]interface{}{ + "params": map[string]any{ "protocolVersion": "2024-11-05", - "clientInfo": map[string]interface{}{ + "clientInfo": map[string]any{ "name": "test-client", "version": "1.0.0", }, @@ -971,7 +971,7 @@ func TestSSEServer(t *testing.T) { // Extract and parse the response data respData := strings.TrimSpace(strings.Split(strings.Split(initResponseStr, "data: ")[1], "\n")[0]) - var response map[string]interface{} + var response map[string]any if err := json.NewDecoder(strings.NewReader(respData)).Decode(&response); err != nil { t.Fatalf("Failed to decode response: %v", err) } @@ -1246,7 +1246,7 @@ func TestSSEServer(t *testing.T) { WithHooks(&Hooks{ OnAfterInitialize: []OnAfterInitializeFunc{ func(ctx context.Context, id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) { - result.Result.Meta = map[string]interface{}{"invalid": func() {}} // marshal will fail + result.Result.Meta = map[string]any{"invalid": func() {}} // marshal will fail }, }, }), @@ -1276,13 +1276,13 @@ func TestSSEServer(t *testing.T) { ) // Send initialize request - initRequest := map[string]interface{}{ + initRequest := map[string]any{ "jsonrpc": "2.0", "id": 1, "method": "initialize", - "params": map[string]interface{}{ + "params": map[string]any{ "protocolVersion": "2024-11-05", - "clientInfo": map[string]interface{}{ + "clientInfo": map[string]any{ "name": "test-client", "version": "1.0.0", }, @@ -1359,13 +1359,13 @@ func TestSSEServer(t *testing.T) { strings.Split(strings.Split(endpointEvent, "data: ")[1], "\n")[0], ) - messageRequest := map[string]interface{}{ + messageRequest := map[string]any{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", - "params": map[string]interface{}{ + "params": map[string]any{ "name": "slowMethod", - "parameters": map[string]interface{}{}, + "parameters": map[string]any{}, }, } diff --git a/server/stdio_test.go b/server/stdio_test.go index 61131745..8433fd0a 100644 --- a/server/stdio_test.go +++ b/server/stdio_test.go @@ -54,13 +54,13 @@ func TestStdioServer(t *testing.T) { }() // Create test message - initRequest := map[string]interface{}{ + initRequest := map[string]any{ "jsonrpc": "2.0", "id": 1, "method": "initialize", - "params": map[string]interface{}{ + "params": map[string]any{ "protocolVersion": "2024-11-05", - "clientInfo": map[string]interface{}{ + "clientInfo": map[string]any{ "name": "test-client", "version": "1.0.0", }, @@ -84,7 +84,7 @@ func TestStdioServer(t *testing.T) { } responseBytes := scanner.Bytes() - var response map[string]interface{} + var response map[string]any if err := json.Unmarshal(responseBytes, &response); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } @@ -166,13 +166,13 @@ func TestStdioServer(t *testing.T) { }() // Create test message - initRequest := map[string]interface{}{ + initRequest := map[string]any{ "jsonrpc": "2.0", "id": 1, "method": "initialize", - "params": map[string]interface{}{ + "params": map[string]any{ "protocolVersion": "2024-11-05", - "clientInfo": map[string]interface{}{ + "clientInfo": map[string]any{ "name": "test-client", "version": "1.0.0", }, @@ -196,7 +196,7 @@ func TestStdioServer(t *testing.T) { } responseBytes := scanner.Bytes() - var response map[string]interface{} + var response map[string]any if err := json.Unmarshal(responseBytes, &response); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } @@ -216,11 +216,11 @@ func TestStdioServer(t *testing.T) { } // Call the tool. - toolRequest := map[string]interface{}{ + toolRequest := map[string]any{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", - "params": map[string]interface{}{ + "params": map[string]any{ "name": "test_tool", }, } @@ -239,7 +239,7 @@ func TestStdioServer(t *testing.T) { } responseBytes = scanner.Bytes() - response = map[string]interface{}{} + response = map[string]any{} if err := json.Unmarshal(responseBytes, &response); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } @@ -250,7 +250,7 @@ func TestStdioServer(t *testing.T) { if response["id"].(float64) != 2 { t.Errorf("Expected id 2, got %v", response["id"]) } - if response["result"].(map[string]interface{})["content"].([]interface{})[0].(map[string]interface{})["text"] != "test_value" { + if response["result"].(map[string]any)["content"].([]any)[0].(map[string]any)["text"] != "test_value" { t.Errorf("Expected result 'test_value', got %v", response["result"]) } if response["error"] != nil { diff --git a/testdata/mockstdio_server.go b/testdata/mockstdio_server.go index 9f13d554..63f7835d 100644 --- a/testdata/mockstdio_server.go +++ b/testdata/mockstdio_server.go @@ -16,9 +16,9 @@ type JSONRPCRequest struct { } type JSONRPCResponse struct { - JSONRPC string `json:"jsonrpc"` - ID *int64 `json:"id,omitempty"` - Result interface{} `json:"result,omitempty"` + JSONRPC string `json:"jsonrpc"` + ID *int64 `json:"id,omitempty"` + Result any `json:"result,omitempty"` Error *struct { Code int `json:"code"` Message string `json:"message"` @@ -49,21 +49,21 @@ func handleRequest(request JSONRPCRequest) JSONRPCResponse { switch request.Method { case "initialize": - response.Result = map[string]interface{}{ + response.Result = map[string]any{ "protocolVersion": "1.0", - "serverInfo": map[string]interface{}{ + "serverInfo": map[string]any{ "name": "mock-server", "version": "1.0.0", }, - "capabilities": map[string]interface{}{ - "prompts": map[string]interface{}{ + "capabilities": map[string]any{ + "prompts": map[string]any{ "listChanged": true, }, - "resources": map[string]interface{}{ + "resources": map[string]any{ "listChanged": true, "subscribe": true, }, - "tools": map[string]interface{}{ + "tools": map[string]any{ "listChanged": true, }, }, @@ -71,8 +71,8 @@ func handleRequest(request JSONRPCRequest) JSONRPCResponse { case "ping": response.Result = struct{}{} case "resources/list": - response.Result = map[string]interface{}{ - "resources": []map[string]interface{}{ + response.Result = map[string]any{ + "resources": []map[string]any{ { "name": "test-resource", "uri": "test://resource", @@ -80,8 +80,8 @@ func handleRequest(request JSONRPCRequest) JSONRPCResponse { }, } case "resources/read": - response.Result = map[string]interface{}{ - "contents": []map[string]interface{}{ + response.Result = map[string]any{ + "contents": []map[string]any{ { "text": "test content", "uri": "test://resource", @@ -91,19 +91,19 @@ func handleRequest(request JSONRPCRequest) JSONRPCResponse { case "resources/subscribe", "resources/unsubscribe": response.Result = struct{}{} case "prompts/list": - response.Result = map[string]interface{}{ - "prompts": []map[string]interface{}{ + response.Result = map[string]any{ + "prompts": []map[string]any{ { "name": "test-prompt", }, }, } case "prompts/get": - response.Result = map[string]interface{}{ - "messages": []map[string]interface{}{ + response.Result = map[string]any{ + "messages": []map[string]any{ { "role": "assistant", - "content": map[string]interface{}{ + "content": map[string]any{ "type": "text", "text": "test message", }, @@ -111,19 +111,19 @@ func handleRequest(request JSONRPCRequest) JSONRPCResponse { }, } case "tools/list": - response.Result = map[string]interface{}{ - "tools": []map[string]interface{}{ + response.Result = map[string]any{ + "tools": []map[string]any{ { "name": "test-tool", - "inputSchema": map[string]interface{}{ + "inputSchema": map[string]any{ "type": "object", }, }, }, } case "tools/call": - response.Result = map[string]interface{}{ - "content": []map[string]interface{}{ + response.Result = map[string]any{ + "content": []map[string]any{ { "type": "text", "text": "tool result", @@ -133,8 +133,8 @@ func handleRequest(request JSONRPCRequest) JSONRPCResponse { case "logging/setLevel": response.Result = struct{}{} case "completion/complete": - response.Result = map[string]interface{}{ - "completion": map[string]interface{}{ + response.Result = map[string]any{ + "completion": map[string]any{ "values": []string{"test completion"}, }, } From 3dfa33164fe642a2adc8908c9d4794e8fb2cf806 Mon Sep 17 00:00:00 2001 From: cryo Date: Sun, 11 May 2025 01:54:39 +0800 Subject: [PATCH 24/29] fix(server/stdio): risk of concurrent reads and data loss in readNextLine() (#257) --- server/stdio.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/stdio.go b/server/stdio.go index 79407e58..c4fe1bf6 100644 --- a/server/stdio.go +++ b/server/stdio.go @@ -171,7 +171,6 @@ func (s *StdioServer) readNextLine(ctx context.Context, reader *bufio.Reader) (s select { case errChan <- err: case <-done: - } return } @@ -179,6 +178,7 @@ func (s *StdioServer) readNextLine(ctx context.Context, reader *bufio.Reader) (s case readChan <- line: case <-done: } + return } }() From 716eabedfef62d99a04b749472b8cef27b404fa3 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sat, 10 May 2025 20:36:48 -0400 Subject: [PATCH 25/29] docs: Remove reference to `mcp.RoleSystem` (#269) --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d7dd9d09..5870713d 100644 --- a/README.md +++ b/README.md @@ -459,8 +459,8 @@ s.AddPrompt(mcp.NewPrompt("code_review", "Code review assistance", []mcp.PromptMessage{ mcp.NewPromptMessage( - mcp.RoleSystem, - mcp.NewTextContent("You are a helpful code reviewer. Review the changes and provide constructive feedback."), + mcp.RoleUser, + mcp.NewTextContent("Review the changes and provide constructive feedback."), ), mcp.NewPromptMessage( mcp.RoleAssistant, @@ -490,11 +490,11 @@ s.AddPrompt(mcp.NewPrompt("query_builder", "SQL query builder assistance", []mcp.PromptMessage{ mcp.NewPromptMessage( - mcp.RoleSystem, - mcp.NewTextContent("You are a SQL expert. Help construct efficient and safe queries."), + mcp.RoleUser, + mcp.NewTextContent("Help construct efficient and safe queries for the provided schema."), ), mcp.NewPromptMessage( - mcp.RoleAssistant, + mcp.RoleUser, mcp.NewEmbeddedResource(mcp.ResourceContents{ URI: fmt.Sprintf("db://schema/%s", tableName), MIMEType: "application/json", From 46bfb6fbb69067de5513049479408732cbea5f33 Mon Sep 17 00:00:00 2001 From: Navendu Pottekkat Date: Sun, 11 May 2025 22:04:09 +0530 Subject: [PATCH 26/29] fix: fix some obvious simplifications (#267) --- examples/everything/main.go | 67 +++++++++++++++++++++++++------------ server/session.go | 6 ++-- server/sse.go | 2 +- server/sse_test.go | 3 +- 4 files changed, 51 insertions(+), 27 deletions(-) diff --git a/examples/everything/main.go b/examples/everything/main.go index 8f119703..957571c7 100644 --- a/examples/everything/main.go +++ b/examples/everything/main.go @@ -2,9 +2,11 @@ package main import ( "context" + "encoding/base64" "flag" "fmt" "log" + "strconv" "time" "github.com/mark3labs/mcp-go/mcp" @@ -64,6 +66,7 @@ func NewMCPServer() *server.MCPServer { "1.0.0", server.WithResourceCapabilities(true, true), server.WithPromptCapabilities(true), + server.WithToolCapabilities(true), server.WithLogging(), server.WithHooks(hooks), ) @@ -79,6 +82,12 @@ func NewMCPServer() *server.MCPServer { ), handleResourceTemplate, ) + + resources := generateResources() + for _, resource := range resources { + mcpServer.AddResource(resource, handleGeneratedResource) + } + mcpServer.AddPrompt(mcp.NewPrompt(string(SIMPLE), mcp.WithPromptDescription("A simple prompt"), ), handleSimplePrompt) @@ -180,27 +189,6 @@ func generateResources() []mcp.Resource { return resources } -func runUpdateInterval() { - // for range s.updateTicker.C { - // for uri := range s.subscriptions { - // s.server.HandleMessage( - // context.Background(), - // mcp.JSONRPCNotification{ - // JSONRPC: mcp.JSONRPC_VERSION, - // Notification: mcp.Notification{ - // Method: "resources/updated", - // Params: struct { - // Meta map[string]any `json:"_meta,omitempty"` - // }{ - // Meta: map[string]any{"uri": uri}, - // }, - // }, - // }, - // ) - // } - // } -} - func handleReadResource( ctx context.Context, request mcp.ReadResourceRequest, @@ -227,6 +215,43 @@ func handleResourceTemplate( }, nil } +func handleGeneratedResource( + ctx context.Context, + request mcp.ReadResourceRequest, +) ([]mcp.ResourceContents, error) { + uri := request.Params.URI + + var resourceNumber string + if _, err := fmt.Sscanf(uri, "test://static/resource/%s", &resourceNumber); err != nil { + return nil, fmt.Errorf("invalid resource URI format: %w", err) + } + + num, err := strconv.Atoi(resourceNumber) + if err != nil { + return nil, fmt.Errorf("invalid resource number: %w", err) + } + + index := num - 1 + + if index%2 == 0 { + return []mcp.ResourceContents{ + mcp.TextResourceContents{ + URI: uri, + MIMEType: "text/plain", + Text: fmt.Sprintf("Text content for resource %d", num), + }, + }, nil + } else { + return []mcp.ResourceContents{ + mcp.BlobResourceContents{ + URI: uri, + MIMEType: "application/octet-stream", + Blob: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("Binary content for resource %d", num))), + }, + }, nil + } +} + func handleSimplePrompt( ctx context.Context, request mcp.GetPromptRequest, diff --git a/server/session.go b/server/session.go index edf59f2f..1bae612c 100644 --- a/server/session.go +++ b/server/session.go @@ -231,10 +231,8 @@ func (s *MCPServer) AddSessionTools(sessionID string, tools ...ServerTool) error newSessionTools := make(map[string]ServerTool, len(sessionTools)+len(tools)) // Copy existing tools - if sessionTools != nil { - for k, v := range sessionTools { - newSessionTools[k] = v - } + for k, v := range sessionTools { + newSessionTools[k] = v } // Add new tools diff --git a/server/sse.go b/server/sse.go index 98d72682..02526812 100644 --- a/server/sse.go +++ b/server/sse.go @@ -451,7 +451,7 @@ func (s *SSEServer) handleMessage(w http.ResponseWriter, r *http.Request) { if eventData, err := json.Marshal(response); err != nil { // If there is an error marshalling the response, send a generic error response log.Printf("failed to marshal response: %v", err) - message = fmt.Sprintf("event: message\ndata: {\"error\": \"internal error\",\"jsonrpc\": \"2.0\", \"id\": null}\n\n") + message = "event: message\ndata: {\"error\": \"internal error\",\"jsonrpc\": \"2.0\", \"id\": null}\n\n" } else { message = fmt.Sprintf("event: message\ndata: %s\n\n", eventData) } diff --git a/server/sse_test.go b/server/sse_test.go index 8ec8beb7..161cc9c4 100644 --- a/server/sse_test.go +++ b/server/sse_test.go @@ -197,7 +197,8 @@ func TestSSEServer(t *testing.T) { endpointEvent, err = readSSEEvent(sseResp) if err != nil { - t.Fatalf("Failed to read SSE response: %v", err) + t.Errorf("Failed to read SSE response: %v", err) + return } respFromSee := strings.TrimSpace( strings.Split(strings.Split(endpointEvent, "data: ")[1], "\n")[0], From e1f1b4794ea047757a1272659b9c6a6d68826800 Mon Sep 17 00:00:00 2001 From: qiangmzsx Date: Mon, 12 May 2025 00:36:02 +0800 Subject: [PATCH 27/29] optimize listByPagination (#246) --- mcp/prompts.go | 5 ++++ mcp/tools.go | 5 ++++ mcp/types.go | 14 +++++++++ server/server.go | 7 ++--- server/server_test.go | 67 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 4 deletions(-) diff --git a/mcp/prompts.go b/mcp/prompts.go index 1309cc5c..db2fc3b8 100644 --- a/mcp/prompts.go +++ b/mcp/prompts.go @@ -50,6 +50,11 @@ type Prompt struct { Arguments []PromptArgument `json:"arguments,omitempty"` } +// GetName returns the name of the prompt. +func (p Prompt) GetName() string { + return p.Name +} + // PromptArgument describes an argument that a prompt template can accept. // When a prompt includes arguments, clients must provide values for all // required arguments when making a prompts/get request. diff --git a/mcp/tools.go b/mcp/tools.go index 4f69d874..709c051d 100644 --- a/mcp/tools.go +++ b/mcp/tools.go @@ -79,6 +79,11 @@ type Tool struct { Annotations ToolAnnotation `json:"annotations"` } +// GetName returns the name of the tool. +func (t Tool) GetName() string { + return t.Name +} + // MarshalJSON implements the json.Marshaler interface for Tool. // It handles marshaling either InputSchema or RawInputSchema based on which is set. func (t Tool) MarshalJSON() ([]byte, error) { diff --git a/mcp/types.go b/mcp/types.go index 250bc746..2a8e1a2b 100644 --- a/mcp/types.go +++ b/mcp/types.go @@ -523,6 +523,11 @@ type Resource struct { MIMEType string `json:"mimeType,omitempty"` } +// GetName returns the name of the resource. +func (r Resource) GetName() string { + return r.Name +} + // ResourceTemplate represents a template description for resources available // on the server. type ResourceTemplate struct { @@ -544,6 +549,11 @@ type ResourceTemplate struct { MIMEType string `json:"mimeType,omitempty"` } +// GetName returns the name of the resourceTemplate. +func (rt ResourceTemplate) GetName() string { + return rt.Name +} + // ResourceContents represents the contents of a specific resource or sub- // resource. type ResourceContents interface { @@ -893,3 +903,7 @@ type ServerNotification any // ServerResult represents any result that can be sent from server to client. type ServerResult any + +type Named interface { + GetName() string +} diff --git a/server/server.go b/server/server.go index b7a54ce3..a68456b2 100644 --- a/server/server.go +++ b/server/server.go @@ -6,7 +6,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "reflect" "sort" "sync" @@ -541,7 +540,7 @@ func (s *MCPServer) handlePing( return &mcp.EmptyResult{}, nil } -func listByPagination[T any]( +func listByPagination[T mcp.Named]( ctx context.Context, s *MCPServer, cursor mcp.Cursor, @@ -555,7 +554,7 @@ func listByPagination[T any]( } cString := string(c) startPos = sort.Search(len(allElements), func(i int) bool { - return reflect.ValueOf(allElements[i]).FieldByName("Name").String() > cString + return allElements[i].GetName() > cString }) } endPos := len(allElements) @@ -568,7 +567,7 @@ func listByPagination[T any]( // set the next cursor nextCursor := func() mcp.Cursor { if s.paginationLimit != nil && len(elementsToReturn) >= *s.paginationLimit { - nc := reflect.ValueOf(elementsToReturn[len(elementsToReturn)-1]).FieldByName("Name").String() + nc := elementsToReturn[len(elementsToReturn)-1].GetName() toString := base64.StdEncoding.EncodeToString([]byte(nc)) return mcp.Cursor(toString) } diff --git a/server/server_test.go b/server/server_test.go index 7f5116a3..ac018731 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -6,6 +6,8 @@ import ( "encoding/json" "errors" "fmt" + "reflect" + "sort" "testing" "time" @@ -1557,3 +1559,68 @@ func TestMCPServer_WithRecover(t *testing.T) { assert.Equal(t, "panic recovered in panic-tool tool handler: test panic", errorResponse.Error.Message) assert.Nil(t, errorResponse.Error.Data) } + +func getTools(length int) []mcp.Tool { + list := make([]mcp.Tool, 0, 10000) + for i := 0; i < length; i++ { + list = append(list, mcp.Tool{ + Name: fmt.Sprintf("tool%d", i), + Description: fmt.Sprintf("tool%d", i), + }) + } + return list +} + +func listByPaginationForReflect[T any]( + ctx context.Context, + s *MCPServer, + cursor mcp.Cursor, + allElements []T, +) ([]T, mcp.Cursor, error) { + startPos := 0 + if cursor != "" { + c, err := base64.StdEncoding.DecodeString(string(cursor)) + if err != nil { + return nil, "", err + } + cString := string(c) + startPos = sort.Search(len(allElements), func(i int) bool { + return reflect.ValueOf(allElements[i]).FieldByName("Name").String() > cString + }) + } + endPos := len(allElements) + if s.paginationLimit != nil { + if len(allElements) > startPos+*s.paginationLimit { + endPos = startPos + *s.paginationLimit + } + } + elementsToReturn := allElements[startPos:endPos] + // set the next cursor + nextCursor := func() mcp.Cursor { + if s.paginationLimit != nil && len(elementsToReturn) >= *s.paginationLimit { + nc := reflect.ValueOf(elementsToReturn[len(elementsToReturn)-1]).FieldByName("Name").String() + toString := base64.StdEncoding.EncodeToString([]byte(nc)) + return mcp.Cursor(toString) + } + return "" + }() + return elementsToReturn, nextCursor, nil +} + +func BenchmarkMCPServer_Pagination(b *testing.B) { + list := getTools(10000) + ctx := context.Background() + server := createTestServer() + for i := 0; i < b.N; i++ { + _, _, _ = listByPagination[mcp.Tool](ctx, server, "dG9vbDY1NA==", list) + } +} + +func BenchmarkMCPServer_PaginationForReflect(b *testing.B) { + list := getTools(10000) + ctx := context.Background() + server := createTestServer() + for i := 0; i < b.N; i++ { + _, _, _ = listByPaginationForReflect[mcp.Tool](ctx, server, "dG9vbDY1NA==", list) + } +} From eeb7070c3dc7a3c1df64fe309a3b8433ea78096e Mon Sep 17 00:00:00 2001 From: Navendu Pottekkat Date: Sun, 11 May 2025 22:46:39 +0530 Subject: [PATCH 28/29] fix: properly marshal `ToolAnnotations` with `false` values (#260) --- client/inprocess_test.go | 20 +++++++------- client/sse_test.go | 23 ++++++++-------- mcp/tools.go | 57 ++++++++++++++++++++++++++++++++++------ mcp/utils.go | 5 ++++ server/server_test.go | 8 +++--- 5 files changed, 78 insertions(+), 35 deletions(-) diff --git a/client/inprocess_test.go b/client/inprocess_test.go index c9b63b47..beaa0c06 100644 --- a/client/inprocess_test.go +++ b/client/inprocess_test.go @@ -22,13 +22,11 @@ func TestInProcessMCPClient(t *testing.T) { "test-tool", mcp.WithDescription("Test tool"), mcp.WithString("parameter-1", mcp.Description("A string tool parameter")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: "Test Tool Annotation Title", - ReadOnlyHint: true, - DestructiveHint: false, - IdempotentHint: true, - OpenWorldHint: false, - }), + mcp.WithTitleAnnotation("Test Tool Annotation Title"), + mcp.WithReadOnlyHintAnnotation(true), + mcp.WithDestructiveHintAnnotation(false), + mcp.WithIdempotentHintAnnotation(true), + mcp.WithOpenWorldHintAnnotation(false), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return &mcp.CallToolResult{ Content: []mcp.Content{ @@ -143,10 +141,10 @@ func TestInProcessMCPClient(t *testing.T) { } testToolAnnotations := (*toolListResult).Tools[0].Annotations if testToolAnnotations.Title != "Test Tool Annotation Title" || - testToolAnnotations.ReadOnlyHint != true || - testToolAnnotations.DestructiveHint != false || - testToolAnnotations.IdempotentHint != true || - testToolAnnotations.OpenWorldHint != false { + *testToolAnnotations.ReadOnlyHint != true || + *testToolAnnotations.DestructiveHint != false || + *testToolAnnotations.IdempotentHint != true || + *testToolAnnotations.OpenWorldHint != false { t.Errorf("The annotations of the tools are invalid") } }) diff --git a/client/sse_test.go b/client/sse_test.go index 7ff2a16a..f02ed41a 100644 --- a/client/sse_test.go +++ b/client/sse_test.go @@ -2,10 +2,11 @@ package client import ( "context" - "github.com/mark3labs/mcp-go/client/transport" "testing" "time" + "github.com/mark3labs/mcp-go/client/transport" + "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) @@ -25,13 +26,11 @@ func TestSSEMCPClient(t *testing.T) { "test-tool", mcp.WithDescription("Test tool"), mcp.WithString("parameter-1", mcp.Description("A string tool parameter")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: "Test Tool Annotation Title", - ReadOnlyHint: true, - DestructiveHint: false, - IdempotentHint: true, - OpenWorldHint: false, - }), + mcp.WithTitleAnnotation("Test Tool Annotation Title"), + mcp.WithReadOnlyHintAnnotation(true), + mcp.WithDestructiveHintAnnotation(false), + mcp.WithIdempotentHintAnnotation(true), + mcp.WithOpenWorldHintAnnotation(false), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return &mcp.CallToolResult{ Content: []mcp.Content{ @@ -111,10 +110,10 @@ func TestSSEMCPClient(t *testing.T) { } testToolAnnotations := (*toolListResult).Tools[0].Annotations if testToolAnnotations.Title != "Test Tool Annotation Title" || - testToolAnnotations.ReadOnlyHint != true || - testToolAnnotations.DestructiveHint != false || - testToolAnnotations.IdempotentHint != true || - testToolAnnotations.OpenWorldHint != false { + *testToolAnnotations.ReadOnlyHint != true || + *testToolAnnotations.DestructiveHint != false || + *testToolAnnotations.IdempotentHint != true || + *testToolAnnotations.OpenWorldHint != false { t.Errorf("The annotations of the tools are invalid") } }) diff --git a/mcp/tools.go b/mcp/tools.go index 709c051d..392b837e 100644 --- a/mcp/tools.go +++ b/mcp/tools.go @@ -139,13 +139,13 @@ type ToolAnnotation struct { // Human-readable title for the tool Title string `json:"title,omitempty"` // If true, the tool does not modify its environment - ReadOnlyHint bool `json:"readOnlyHint,omitempty"` + ReadOnlyHint *bool `json:"readOnlyHint,omitempty"` // If true, the tool may perform destructive updates - DestructiveHint bool `json:"destructiveHint,omitempty"` + DestructiveHint *bool `json:"destructiveHint,omitempty"` // If true, repeated calls with same args have no additional effect - IdempotentHint bool `json:"idempotentHint,omitempty"` + IdempotentHint *bool `json:"idempotentHint,omitempty"` // If true, tool interacts with external entities - OpenWorldHint bool `json:"openWorldHint,omitempty"` + OpenWorldHint *bool `json:"openWorldHint,omitempty"` } // ToolOption is a function that configures a Tool. @@ -173,10 +173,10 @@ func NewTool(name string, opts ...ToolOption) Tool { }, Annotations: ToolAnnotation{ Title: "", - ReadOnlyHint: false, - DestructiveHint: true, - IdempotentHint: false, - OpenWorldHint: true, + ReadOnlyHint: ToBoolPtr(false), + DestructiveHint: ToBoolPtr(true), + IdempotentHint: ToBoolPtr(false), + OpenWorldHint: ToBoolPtr(true), }, } @@ -212,12 +212,53 @@ func WithDescription(description string) ToolOption { } } +// WithToolAnnotation adds optional hints about the Tool. func WithToolAnnotation(annotation ToolAnnotation) ToolOption { return func(t *Tool) { t.Annotations = annotation } } +// WithTitleAnnotation sets the Title field of the Tool's Annotations. +// It provides a human-readable title for the tool. +func WithTitleAnnotation(title string) ToolOption { + return func(t *Tool) { + t.Annotations.Title = title + } +} + +// WithReadOnlyHintAnnotation sets the ReadOnlyHint field of the Tool's Annotations. +// If true, it indicates the tool does not modify its environment. +func WithReadOnlyHintAnnotation(value bool) ToolOption { + return func(t *Tool) { + t.Annotations.ReadOnlyHint = &value + } +} + +// WithDestructiveHintAnnotation sets the DestructiveHint field of the Tool's Annotations. +// If true, it indicates the tool may perform destructive updates. +func WithDestructiveHintAnnotation(value bool) ToolOption { + return func(t *Tool) { + t.Annotations.DestructiveHint = &value + } +} + +// WithIdempotentHintAnnotation sets the IdempotentHint field of the Tool's Annotations. +// If true, it indicates repeated calls with the same arguments have no additional effect. +func WithIdempotentHintAnnotation(value bool) ToolOption { + return func(t *Tool) { + t.Annotations.IdempotentHint = &value + } +} + +// WithOpenWorldHintAnnotation sets the OpenWorldHint field of the Tool's Annotations. +// If true, it indicates the tool interacts with external entities. +func WithOpenWorldHintAnnotation(value bool) ToolOption { + return func(t *Tool) { + t.Annotations.OpenWorldHint = &value + } +} + // // Common Property Options // diff --git a/mcp/utils.go b/mcp/utils.go index eaecff2d..bf6acbdf 100644 --- a/mcp/utils.go +++ b/mcp/utils.go @@ -775,3 +775,8 @@ func ParseStringMap(request CallToolRequest, key string, defaultValue map[string v := ParseArgument(request, key, defaultValue) return cast.ToStringMap(v) } + +// ToBoolPtr returns a pointer to the given boolean value +func ToBoolPtr(b bool) *bool { + return &b +} diff --git a/server/server_test.go b/server/server_test.go index ac018731..831a48f4 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -900,10 +900,10 @@ func TestMCPServer_HandleUndefinedHandlers(t *testing.T) { }, Annotations: mcp.ToolAnnotation{ Title: "test-tool", - ReadOnlyHint: true, - DestructiveHint: false, - IdempotentHint: false, - OpenWorldHint: false, + ReadOnlyHint: mcp.ToBoolPtr(true), + DestructiveHint: mcp.ToBoolPtr(false), + IdempotentHint: mcp.ToBoolPtr(false), + OpenWorldHint: mcp.ToBoolPtr(false), }, }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return &mcp.CallToolResult{}, nil From e5121b37d7214e23c572e1b9a49ca5b8a4d648e4 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Sun, 11 May 2025 22:51:46 +0300 Subject: [PATCH 29/29] Release v0.27.0 --- examples/simple_client/main.go | 20 ++++++++++---------- server/resource_test.go | 1 - server/server.go | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/examples/simple_client/main.go b/examples/simple_client/main.go index 5532e05f..26d3dca3 100644 --- a/examples/simple_client/main.go +++ b/examples/simple_client/main.go @@ -47,18 +47,18 @@ func main() { // Create command and stdio transport command := args[0] cmdArgs := args[1:] - + // Create stdio transport with verbose logging stdioTransport := transport.NewStdio(command, nil, cmdArgs...) - + // Start the transport if err := stdioTransport.Start(ctx); err != nil { log.Fatalf("Failed to start stdio transport: %v", err) } - + // Create client with the transport c = client.NewClient(stdioTransport) - + // Set up logging for stderr if available if stderr, ok := client.GetStderr(c); ok { go func() { @@ -84,12 +84,12 @@ func main() { if err != nil { log.Fatalf("Failed to create SSE transport: %v", err) } - + // Start the transport if err := sseTransport.Start(ctx); err != nil { log.Fatalf("Failed to start SSE transport: %v", err) } - + // Create client with the transport c = client.NewClient(sseTransport) } @@ -108,15 +108,15 @@ func main() { Version: "1.0.0", } initRequest.Params.Capabilities = mcp.ClientCapabilities{} - + serverInfo, err := c.Initialize(ctx, initRequest) if err != nil { log.Fatalf("Failed to initialize: %v", err) } // Display server information - fmt.Printf("Connected to server: %s (version %s)\n", - serverInfo.ServerInfo.Name, + fmt.Printf("Connected to server: %s (version %s)\n", + serverInfo.ServerInfo.Name, serverInfo.ServerInfo.Version) fmt.Printf("Server capabilities: %+v\n", serverInfo.Capabilities) @@ -190,4 +190,4 @@ func parseCommand(cmd string) []string { } return result -} \ No newline at end of file +} diff --git a/server/resource_test.go b/server/resource_test.go index 11f1275e..05a3b279 100644 --- a/server/resource_test.go +++ b/server/resource_test.go @@ -135,7 +135,6 @@ func TestMCPServer_RemoveResource(t *testing.T) { // verify that no notifications were sent assert.Empty(t, notifications) - // The original resource should still be there resp, ok := resourcesList.(mcp.JSONRPCResponse) assert.True(t, ok) diff --git a/server/server.go b/server/server.go index a68456b2..33ab4c38 100644 --- a/server/server.go +++ b/server/server.go @@ -341,7 +341,7 @@ func (s *MCPServer) RemoveResource(uri string) { } s.resourcesMu.Unlock() - // Send notification to all initialized sessions if listChanged capability is enabled and we actually remove a resource + // Send notification to all initialized sessions if listChanged capability is enabled and we actually remove a resource if exists && s.capabilities.resources != nil && s.capabilities.resources.listChanged { s.SendNotificationToAllClients(mcp.MethodNotificationResourcesListChanged, nil) }