diff --git a/AGENTS.md b/AGENTS.md index f2df45e1..932864b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -382,6 +382,7 @@ DEBUG_COLORS=0 DEBUG=* ./awmg --config config.toml - `{serverID}.log` - Per-server logs (e.g., `github.log`, `slack.log`) for easier troubleshooting - `gateway.md` - Markdown-formatted logs for GitHub workflow previews - `rpc-messages.jsonl` - Machine-readable JSONL format for RPC message analysis + - `tools.json` - Available tools from all backend MCP servers (mapping server IDs to their tool names and descriptions) - Logs include: startup, client interactions, backend operations, auth events, errors **Per-ServerID Logging:** @@ -406,6 +407,29 @@ DEBUG_COLORS=0 DEBUG=* ./awmg --config config.toml - The `payloadPreview` shows the first 500 characters of the JSON for quick reference - To access the full data with all actual values, read the JSON file at `payloadPath` +**Tools Catalog (tools.json):** +- The gateway maintains a catalog of all available tools from backend MCP servers in `tools.json` +- Located in the log directory (e.g., `/tmp/gh-aw/mcp-logs/tools.json`) +- Updated automatically during gateway startup when backend servers are registered +- Format: JSON mapping of server IDs to arrays of tool information +- Each tool includes: `name` (tool name without server prefix) and `description` +- Example structure: + ```json + { + "servers": { + "github": [ + {"name": "search_code", "description": "Search for code in repositories"}, + {"name": "get_file_contents", "description": "Get the contents of a file"} + ], + "slack": [ + {"name": "send_message", "description": "Send a message to a Slack channel"} + ] + } + } + ``` +- Useful for discovering available tools across all configured backend servers +- Can be used by clients or monitoring tools to understand gateway capabilities + ## Error Debugging **Enhanced Error Context**: Command failures include: diff --git a/README.md b/README.md index 7e21768f..e33cad54 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ For detailed setup instructions, building from source, and local development, se - `{serverID}.log`: Per-server logs for easier troubleshooting - `gateway.md`: Markdown-formatted logs for GitHub workflow previews - `rpc-messages.jsonl`: Machine-readable RPC message logs + - `tools.json`: Available tools from all backend MCP servers - `-p 8000:8000`: Port mapping must match `MCP_GATEWAY_PORT` MCPG will start in routed mode on `http://0.0.0.0:8000` (using `MCP_GATEWAY_PORT`), proxying MCP requests to your configured backend servers. @@ -400,6 +401,7 @@ The gateway creates multiple log files for different purposes: 2. **`{serverID}.log`** - Per-server logs (e.g., `github.log`, `slack.log`) for easier troubleshooting of specific backend servers 3. **`gateway.md`** - Markdown-formatted logs for GitHub workflow previews 4. **`rpc-messages.jsonl`** - Machine-readable JSONL format for RPC message analysis +5. **`tools.json`** - Available tools from all backend MCP servers (mapping server IDs to their tool names and descriptions) ### Log File Location @@ -427,7 +429,8 @@ Example log directory structure: ├── slack.log # Only Slack server logs ├── notion.log # Only Notion server logs ├── gateway.md # Markdown format -└── rpc-messages.jsonl # RPC messages +├── rpc-messages.jsonl # RPC messages +└── tools.json # Available tools ``` **Using the environment variable:** diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 268b51c3..9daca205 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -143,6 +143,7 @@ func postRun(cmd *cobra.Command, args []string) { logger.CloseMarkdownLogger() logger.CloseJSONLLogger() logger.CloseServerFileLogger() + logger.CloseToolsLogger() logger.CloseGlobalLogger() } @@ -171,6 +172,11 @@ func run(cmd *cobra.Command, args []string) error { log.Printf("Warning: Failed to initialize JSONL logger: %v", err) } + // Initialize tools logger for tracking available tools + if err := logger.InitToolsLogger(logDir, "tools.json"); err != nil { + log.Printf("Warning: Failed to initialize tools logger: %v", err) + } + logger.LogInfoMd("startup", "MCPG Gateway version: %s", cliVersion) // Log config source based on what was provided diff --git a/internal/logger/global_helpers.go b/internal/logger/global_helpers.go index f8b2c0e4..17cc9e1d 100644 --- a/internal/logger/global_helpers.go +++ b/internal/logger/global_helpers.go @@ -17,9 +17,9 @@ package logger import "sync" // closableLogger is a constraint for types that have a Close method. -// This is satisfied by *FileLogger, *JSONLLogger, *MarkdownLogger, and *ServerFileLogger. +// This is satisfied by *FileLogger, *JSONLLogger, *MarkdownLogger, *ServerFileLogger, and *ToolsLogger. type closableLogger interface { - *FileLogger | *JSONLLogger | *MarkdownLogger | *ServerFileLogger + *FileLogger | *JSONLLogger | *MarkdownLogger | *ServerFileLogger | *ToolsLogger Close() error } diff --git a/internal/logger/tools_logger.go b/internal/logger/tools_logger.go new file mode 100644 index 00000000..56676237 --- /dev/null +++ b/internal/logger/tools_logger.go @@ -0,0 +1,160 @@ +// Package logger provides structured logging for the MCP Gateway. +// +// This file implements logging of MCP server tools to a JSON file (tools.json). +// It maintains a mapping of server IDs to their available tools with names and descriptions. +package logger + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "sync" +) + +// ToolInfo represents information about a single tool +type ToolInfo struct { + Name string `json:"name"` + Description string `json:"description"` +} + +// ToolsData represents the structure of tools.json +type ToolsData struct { + // Map of serverID to array of tools + Servers map[string][]ToolInfo `json:"servers"` +} + +// ToolsLogger manages logging of MCP server tools to a JSON file +type ToolsLogger struct { + logDir string + fileName string + data *ToolsData + mu sync.Mutex + useFallback bool +} + +var ( + globalToolsLogger *ToolsLogger + globalToolsMu sync.RWMutex +) + +// InitToolsLogger initializes the global tools logger +// If the log directory doesn't exist and can't be created, falls back to no-op +func InitToolsLogger(logDir, fileName string) error { + logger, err := initLogger( + logDir, fileName, os.O_TRUNC, // Truncate existing file to start fresh + // Setup function: configure the logger after directory is ready + func(file *os.File, logDir, fileName string) (*ToolsLogger, error) { + // Close the file immediately - we'll write directly later + if file != nil { + file.Close() + } + + tl := &ToolsLogger{ + logDir: logDir, + fileName: fileName, + data: &ToolsData{ + Servers: make(map[string][]ToolInfo), + }, + } + log.Printf("Tools logging to file: %s", filepath.Join(logDir, fileName)) + return tl, nil + }, + // Error handler: fallback to no-op on error + func(err error, logDir, fileName string) (*ToolsLogger, error) { + log.Printf("WARNING: Failed to initialize tools log file: %v", err) + log.Printf("WARNING: Tools logging disabled") + tl := &ToolsLogger{ + logDir: logDir, + fileName: fileName, + useFallback: true, + data: &ToolsData{ + Servers: make(map[string][]ToolInfo), + }, + } + return tl, nil + }, + ) + + initGlobalToolsLogger(logger) + return err +} + +// LogTools logs the tools for a specific server +func (tl *ToolsLogger) LogTools(serverID string, tools []ToolInfo) error { + tl.mu.Lock() + defer tl.mu.Unlock() + + if tl.useFallback { + return nil // Silently skip if in fallback mode + } + + // Update the data structure + tl.data.Servers[serverID] = tools + + // Write the updated data to file + return tl.writeToFile() +} + +// writeToFile writes the current tools data to the JSON file +// Caller must hold tl.mu lock +func (tl *ToolsLogger) writeToFile() error { + filePath := filepath.Join(tl.logDir, tl.fileName) + + // Marshal to JSON with indentation for readability + jsonData, err := json.MarshalIndent(tl.data, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal tools data: %w", err) + } + + // Write to file atomically using a temp file + rename + tempPath := filePath + ".tmp" + if err := os.WriteFile(tempPath, jsonData, 0644); err != nil { + return fmt.Errorf("failed to write temp file: %w", err) + } + + if err := os.Rename(tempPath, filePath); err != nil { + // Clean up temp file on error + os.Remove(tempPath) + return fmt.Errorf("failed to rename temp file: %w", err) + } + + return nil +} + +// Close is a no-op for ToolsLogger (implements closableLogger interface) +func (tl *ToolsLogger) Close() error { + // No file handle to close since we write directly each time + return nil +} + +// Global logging function that uses the global tools logger + +// LogToolsForServer logs the tools for a specific server +func LogToolsForServer(serverID string, tools []ToolInfo) { + globalToolsMu.RLock() + defer globalToolsMu.RUnlock() + + if globalToolsLogger != nil { + if err := globalToolsLogger.LogTools(serverID, tools); err != nil { + // Log errors using the standard logger to avoid recursion + log.Printf("WARNING: Failed to log tools for server %s: %v", serverID, err) + } + } +} + +// CloseToolsLogger closes the global tools logger +func CloseToolsLogger() error { + return closeGlobalToolsLogger() +} + +// initGlobalToolsLogger initializes the global ToolsLogger using the generic helper. +func initGlobalToolsLogger(logger *ToolsLogger) { + initGlobalLogger(&globalToolsMu, &globalToolsLogger, logger) +} + +// closeGlobalToolsLogger closes the global ToolsLogger using the generic helper. +func closeGlobalToolsLogger() error { + return closeGlobalLogger(&globalToolsMu, &globalToolsLogger) +} diff --git a/internal/logger/tools_logger_test.go b/internal/logger/tools_logger_test.go new file mode 100644 index 00000000..490461f8 --- /dev/null +++ b/internal/logger/tools_logger_test.go @@ -0,0 +1,253 @@ +package logger + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInitToolsLogger(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Create a temp directory for testing + tmpDir := t.TempDir() + + // Initialize the tools logger + err := InitToolsLogger(tmpDir, "tools.json") + require.NoError(err, "InitToolsLogger failed") + + // Verify the global logger was initialized + globalToolsMu.RLock() + assert.NotNil(globalToolsLogger, "Global tools logger should be initialized") + globalToolsMu.RUnlock() + + // Clean up + err = CloseToolsLogger() + assert.NoError(err, "CloseToolsLogger failed") +} + +func TestToolsLoggerLogTools(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Create a temp directory for testing + tmpDir := t.TempDir() + + // Initialize the tools logger + err := InitToolsLogger(tmpDir, "tools.json") + require.NoError(err, "InitToolsLogger failed") + + // Log some tools for a server + tools := []ToolInfo{ + {Name: "tool1", Description: "First tool"}, + {Name: "tool2", Description: "Second tool"}, + } + LogToolsForServer("server1", tools) + + // Read the tools.json file + toolsPath := filepath.Join(tmpDir, "tools.json") + data, err := os.ReadFile(toolsPath) + require.NoError(err, "Failed to read tools.json") + + // Parse the JSON + var toolsData ToolsData + err = json.Unmarshal(data, &toolsData) + require.NoError(err, "Failed to parse tools.json") + + // Verify the structure + assert.Contains(toolsData.Servers, "server1", "Server should be in the map") + assert.Len(toolsData.Servers["server1"], 2, "Should have 2 tools") + assert.Equal("tool1", toolsData.Servers["server1"][0].Name) + assert.Equal("First tool", toolsData.Servers["server1"][0].Description) + assert.Equal("tool2", toolsData.Servers["server1"][1].Name) + assert.Equal("Second tool", toolsData.Servers["server1"][1].Description) + + // Clean up + err = CloseToolsLogger() + assert.NoError(err, "CloseToolsLogger failed") +} + +func TestToolsLoggerMultipleServers(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Create a temp directory for testing + tmpDir := t.TempDir() + + // Initialize the tools logger + err := InitToolsLogger(tmpDir, "tools.json") + require.NoError(err, "InitToolsLogger failed") + + // Log tools for multiple servers + tools1 := []ToolInfo{ + {Name: "tool1", Description: "Server 1 tool 1"}, + {Name: "tool2", Description: "Server 1 tool 2"}, + } + LogToolsForServer("server1", tools1) + + tools2 := []ToolInfo{ + {Name: "tool3", Description: "Server 2 tool 1"}, + } + LogToolsForServer("server2", tools2) + + // Read the tools.json file + toolsPath := filepath.Join(tmpDir, "tools.json") + data, err := os.ReadFile(toolsPath) + require.NoError(err, "Failed to read tools.json") + + // Parse the JSON + var toolsData ToolsData + err = json.Unmarshal(data, &toolsData) + require.NoError(err, "Failed to parse tools.json") + + // Verify both servers are present + assert.Contains(toolsData.Servers, "server1", "Server1 should be in the map") + assert.Contains(toolsData.Servers, "server2", "Server2 should be in the map") + assert.Len(toolsData.Servers["server1"], 2, "Server1 should have 2 tools") + assert.Len(toolsData.Servers["server2"], 1, "Server2 should have 1 tool") + + // Clean up + err = CloseToolsLogger() + assert.NoError(err, "CloseToolsLogger failed") +} + +func TestToolsLoggerUpdate(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Create a temp directory for testing + tmpDir := t.TempDir() + + // Initialize the tools logger + err := InitToolsLogger(tmpDir, "tools.json") + require.NoError(err, "InitToolsLogger failed") + + // Log initial tools + tools1 := []ToolInfo{ + {Name: "tool1", Description: "Original tool"}, + } + LogToolsForServer("server1", tools1) + + // Update with new tools + tools2 := []ToolInfo{ + {Name: "tool2", Description: "Updated tool"}, + {Name: "tool3", Description: "Another tool"}, + } + LogToolsForServer("server1", tools2) + + // Read the tools.json file + toolsPath := filepath.Join(tmpDir, "tools.json") + data, err := os.ReadFile(toolsPath) + require.NoError(err, "Failed to read tools.json") + + // Parse the JSON + var toolsData ToolsData + err = json.Unmarshal(data, &toolsData) + require.NoError(err, "Failed to parse tools.json") + + // Verify the tools were updated (not appended) + assert.Len(toolsData.Servers["server1"], 2, "Should have 2 tools (updated, not appended)") + assert.Equal("tool2", toolsData.Servers["server1"][0].Name) + assert.Equal("tool3", toolsData.Servers["server1"][1].Name) + + // Clean up + err = CloseToolsLogger() + assert.NoError(err, "CloseToolsLogger failed") +} + +func TestToolsLoggerEmptyTools(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Create a temp directory for testing + tmpDir := t.TempDir() + + // Initialize the tools logger + err := InitToolsLogger(tmpDir, "tools.json") + require.NoError(err, "InitToolsLogger failed") + + // Log empty tools array + tools := []ToolInfo{} + LogToolsForServer("server1", tools) + + // Read the tools.json file + toolsPath := filepath.Join(tmpDir, "tools.json") + data, err := os.ReadFile(toolsPath) + require.NoError(err, "Failed to read tools.json") + + // Parse the JSON + var toolsData ToolsData + err = json.Unmarshal(data, &toolsData) + require.NoError(err, "Failed to parse tools.json") + + // Verify empty array is stored + assert.Contains(toolsData.Servers, "server1", "Server should be in the map") + assert.Empty(toolsData.Servers["server1"], "Should have 0 tools") + + // Clean up + err = CloseToolsLogger() + assert.NoError(err, "CloseToolsLogger failed") +} + +func TestToolsLoggerFallback(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Try to initialize with an invalid directory + err := InitToolsLogger("/nonexistent/invalid/path", "tools.json") + // Should not error even if directory creation fails (fallback mode) + require.NoError(err, "InitToolsLogger should not fail on fallback") + + // Logging should not cause errors in fallback mode + tools := []ToolInfo{ + {Name: "tool1", Description: "Test tool"}, + } + LogToolsForServer("server1", tools) + + // Clean up + err = CloseToolsLogger() + assert.NoError(err, "CloseToolsLogger failed") +} + +func TestToolsLoggerJSONFormat(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Create a temp directory for testing + tmpDir := t.TempDir() + + // Initialize the tools logger + err := InitToolsLogger(tmpDir, "tools.json") + require.NoError(err, "InitToolsLogger failed") + + // Log tools with special characters + tools := []ToolInfo{ + {Name: "tool-with-dashes", Description: "Description with \"quotes\" and newlines\ntest"}, + {Name: "tool_with_underscores", Description: "Description with 'single quotes'"}, + } + LogToolsForServer("server-1", tools) + + // Read the tools.json file + toolsPath := filepath.Join(tmpDir, "tools.json") + data, err := os.ReadFile(toolsPath) + require.NoError(err, "Failed to read tools.json") + + // Verify it's valid JSON + var toolsData ToolsData + err = json.Unmarshal(data, &toolsData) + require.NoError(err, "Should be valid JSON") + + // Verify special characters were preserved + assert.Equal("tool-with-dashes", toolsData.Servers["server-1"][0].Name) + assert.Contains(toolsData.Servers["server-1"][0].Description, "\"quotes\"") + assert.Contains(toolsData.Servers["server-1"][0].Description, "\n") + + // Clean up + err = CloseToolsLogger() + assert.NoError(err, "CloseToolsLogger failed") +} diff --git a/internal/server/unified.go b/internal/server/unified.go index afc85b9e..f6c830b3 100644 --- a/internal/server/unified.go +++ b/internal/server/unified.go @@ -291,6 +291,18 @@ func (us *UnifiedServer) registerToolsFromBackend(serverID string) error { return fmt.Errorf("failed to parse tools: %w", err) } + // Collect tools for logging + toolsForLogging := make([]logger.ToolInfo, 0, len(listResult.Tools)) + for _, tool := range listResult.Tools { + toolsForLogging = append(toolsForLogging, logger.ToolInfo{ + Name: tool.Name, + Description: tool.Description, + }) + } + + // Log tools to tools.json + logger.LogToolsForServer(serverID, toolsForLogging) + // Register each tool with prefixed name toolNames := []string{} for _, tool := range listResult.Tools { diff --git a/test/integration/tools_json_test.go b/test/integration/tools_json_test.go new file mode 100644 index 00000000..8d7b4388 --- /dev/null +++ b/test/integration/tools_json_test.go @@ -0,0 +1,256 @@ +package integration + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// startGatewayWithJSONConfigAndLogDir starts the gateway with JSON config and custom log directory +func startGatewayWithJSONConfigAndLogDir(ctx context.Context, t *testing.T, jsonConfig string, logDir string) *exec.Cmd { + t.Helper() + + // Find the binary + binaryPath := findBinary(t) + t.Logf("Using binary: %s", binaryPath) + + // Extract port from config if possible, otherwise use default + port := "13120" // Default port + var configMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonConfig), &configMap); err == nil { + if gateway, ok := configMap["gateway"].(map[string]interface{}); ok { + if portNum, ok := gateway["port"].(float64); ok { + port = fmt.Sprintf("%d", int(portNum)) + } + } + } + + cmd := exec.CommandContext(ctx, binaryPath, + "--config-stdin", + "--listen", "127.0.0.1:"+port, + "--log-dir", logDir, + "--routed", + ) + + // Set stdin to the JSON config + cmd.Stdin = bytes.NewBufferString(jsonConfig) + + // Capture output for debugging + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Start(); err != nil { + t.Fatalf("Failed to start gateway: %v\nSTDOUT: %s\nSTDERR: %s", err, stdout.String(), stderr.String()) + } + + // Start a goroutine to log output if test fails + go func() { + <-ctx.Done() + if t.Failed() { + t.Logf("Gateway STDOUT: %s", stdout.String()) + t.Logf("Gateway STDERR: %s", stderr.String()) + } + }() + + return cmd +} + +// TestToolsJSONLogging tests that tools.json is created and populated correctly +func TestToolsJSONLogging(t *testing.T) { + if testing.Short() { + t.Skip("Skipping tools.json integration test in short mode") + } + + assert := assert.New(t) + require := require.New(t) + + // Create a mock MCP backend that returns tools + mockBackend := createMockToolsServer(t) + defer mockBackend.Close() + + t.Logf("✓ Mock MCP backend started at %s", mockBackend.URL) + + // Create a temporary directory for logs + tmpDir := t.TempDir() + + // Create JSON config for the gateway + configContent := fmt.Sprintf(`{ + "mcpServers": { + "test-server": { + "type": "http", + "url": "%s" + }, + "another-server": { + "type": "http", + "url": "%s" + } + }, + "gateway": { + "port": 13120, + "domain": "localhost", + "apiKey": "test-tools-key" + } +}`, mockBackend.URL, mockBackend.URL) + + t.Logf("✓ Created config with log directory: %s", tmpDir) + + // Start the gateway with the config via stdin + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Start gateway with JSON config via stdin and custom log directory + gatewayCmd := startGatewayWithJSONConfigAndLogDir(ctx, t, configContent, tmpDir) + defer gatewayCmd.Process.Kill() + + // Wait for gateway to start + gatewayURL := "http://127.0.0.1:13120" + if !waitForServer(t, gatewayURL+"/health", 15*time.Second) { + t.Fatal("Gateway did not start in time") + } + t.Logf("✓ Gateway started at %s", gatewayURL) + + // Give the gateway time to initialize and register tools + time.Sleep(2 * time.Second) + + // Check that tools.json was created + toolsPath := filepath.Join(tmpDir, "tools.json") + toolsData, err := os.ReadFile(toolsPath) + require.NoError(err, "tools.json should exist") + t.Logf("✓ tools.json found at %s", toolsPath) + + // Parse the tools.json file + var tools struct { + Servers map[string][]struct { + Name string `json:"name"` + Description string `json:"description"` + } `json:"servers"` + } + err = json.Unmarshal(toolsData, &tools) + require.NoError(err, "tools.json should be valid JSON") + + // Verify both servers are present + assert.Contains(tools.Servers, "test-server", "test-server should be in tools.json") + assert.Contains(tools.Servers, "another-server", "another-server should be in tools.json") + + // Verify test-server has the expected tools + testServerTools := tools.Servers["test-server"] + require.Len(testServerTools, 3, "test-server should have 3 tools") + + toolNames := make(map[string]string) + for _, tool := range testServerTools { + toolNames[tool.Name] = tool.Description + } + + assert.Contains(toolNames, "tool1", "tool1 should be present") + assert.Contains(toolNames, "tool2", "tool2 should be present") + assert.Contains(toolNames, "tool3", "tool3 should be present") + assert.Equal("First test tool", toolNames["tool1"]) + assert.Equal("Second test tool", toolNames["tool2"]) + assert.Equal("Third test tool", toolNames["tool3"]) + + t.Logf("✓ tools.json contains correct data") +} + +// createMockToolsServer creates a mock HTTP MCP server that returns tools +func createMockToolsServer(t *testing.T) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Read the request body + body, err := io.ReadAll(r.Body) + if err != nil { + t.Logf("Failed to read request body: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Parse the JSON-RPC request + var req struct { + Method string `json:"method"` + ID json.RawMessage `json:"id"` + } + if err := json.Unmarshal(body, &req); err != nil { + t.Logf("Failed to parse request: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + // Handle different methods + switch req.Method { + case "initialize": + response := map[string]interface{}{ + "jsonrpc": "2.0", + "id": req.ID, + "result": map[string]interface{}{ + "protocolVersion": "2024-11-05", + "serverInfo": map[string]interface{}{ + "name": "mock-server", + "version": "1.0.0", + }, + "capabilities": map[string]interface{}{ + "tools": map[string]interface{}{}, + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + + case "tools/list": + response := map[string]interface{}{ + "jsonrpc": "2.0", + "id": req.ID, + "result": map[string]interface{}{ + "tools": []map[string]interface{}{ + { + "name": "tool1", + "description": "First test tool", + "inputSchema": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + }, + }, + { + "name": "tool2", + "description": "Second test tool", + "inputSchema": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + }, + }, + { + "name": "tool3", + "description": "Third test tool", + "inputSchema": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + }, + }, + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + + default: + // Return success for any other method + response := map[string]interface{}{ + "jsonrpc": "2.0", + "id": req.ID, + "result": map[string]interface{}{}, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + } + })) +}