diff --git a/mcp-client-go/.env.example b/mcp-client-go/.env.example new file mode 100644 index 0000000..7437ef3 --- /dev/null +++ b/mcp-client-go/.env.example @@ -0,0 +1 @@ +ANTHROPIC_API_KEY=your_api_key_here diff --git a/mcp-client-go/README.md b/mcp-client-go/README.md new file mode 100644 index 0000000..0bc4879 --- /dev/null +++ b/mcp-client-go/README.md @@ -0,0 +1,3 @@ +# An LLM-Powered Chatbot MCP Client written in Go + +See the [Building MCP clients](https://modelcontextprotocol.io/tutorials/building-a-client) tutorial for more information. diff --git a/mcp-client-go/go.mod b/mcp-client-go/go.mod new file mode 100644 index 0000000..6e52f3e --- /dev/null +++ b/mcp-client-go/go.mod @@ -0,0 +1,18 @@ +module github.com/modelcontextprotocol/quickstart-resources/mcp-client-go + +go 1.25.1 + +require ( + github.com/anthropics/anthropic-sdk-go v1.14.0 + github.com/joho/godotenv v1.5.1 + github.com/modelcontextprotocol/go-sdk v1.0.0 +) + +require ( + github.com/google/jsonschema-go v0.3.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect +) diff --git a/mcp-client-go/go.sum b/mcp-client-go/go.sum new file mode 100644 index 0000000..f526563 --- /dev/null +++ b/mcp-client-go/go.sum @@ -0,0 +1,32 @@ +github.com/anthropics/anthropic-sdk-go v1.14.0 h1:EzNQvnZlaDHe2UPkoUySDz3ixRgNbwKdH8KtFpv7pi4= +github.com/anthropics/anthropic-sdk-go v1.14.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= +github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74= +github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mcp-client-go/main.go b/mcp-client-go/main.go new file mode 100644 index 0000000..62f775f --- /dev/null +++ b/mcp-client-go/main.go @@ -0,0 +1,277 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "log" + "os" + "os/exec" + "strings" + + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + "github.com/joho/godotenv" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +var model anthropic.Model = anthropic.ModelClaudeSonnet4_5_20250929 + +type MCPClient struct { + anthropic *anthropic.Client + session *mcp.ClientSession + tools []anthropic.ToolUnionParam +} + +func NewMCPClient() (*MCPClient, error) { + // Load .env file + if err := godotenv.Load(); err != nil { + return nil, fmt.Errorf("failed to load .env file: %w", err) + } + + apiKey := os.Getenv("ANTHROPIC_API_KEY") + if apiKey == "" { + return nil, fmt.Errorf("ANTHROPIC_API_KEY environment variable not set") + } + + client := anthropic.NewClient(option.WithAPIKey(apiKey)) + + return &MCPClient{ + anthropic: &client, + }, nil +} + +func (c *MCPClient) ConnectToServer(ctx context.Context, serverArgs []string) error { + if len(serverArgs) == 0 { + return fmt.Errorf("no server command provided") + } + + // Create command to spawn server process + cmd := exec.CommandContext(ctx, serverArgs[0], serverArgs[1:]...) + + // Create MCP client + client := mcp.NewClient( + &mcp.Implementation{ + Name: "mcp-client-go", + Version: "0.1.0", + }, + nil, + ) + + // Connect using CommandTransport + transport := &mcp.CommandTransport{ + Command: cmd, + } + + session, err := client.Connect(ctx, transport, nil) + if err != nil { + return fmt.Errorf("failed to connect to server: %w", err) + } + + c.session = session + + // List available tools + toolsResult, err := session.ListTools(ctx, &mcp.ListToolsParams{}) + if err != nil { + return fmt.Errorf("failed to list tools: %w", err) + } + + var toolNames []string + + // Convert MCP tools to Anthropic tool format + for _, tool := range toolsResult.Tools { + toolNames = append(toolNames, tool.Name) + anthropicTool, err := mcpToolToAnthropicTool(tool) + if err != nil { + return fmt.Errorf("failed to convert mcp tool to anthropic tool: %w", err) + } + c.tools = append(c.tools, anthropicTool) + } + + fmt.Printf("Connected to server with tools: %v\n", toolNames) + return nil +} + +func mcpToolToAnthropicTool(tool *mcp.Tool) (anthropic.ToolUnionParam, error) { + var zeroTool anthropic.ToolUnionParam + schemaJSON, err := json.Marshal(tool.InputSchema) + if err != nil { + return zeroTool, fmt.Errorf("failed to marshal input schema of mcp tool: %w", err) + } + var schema anthropic.ToolInputSchemaParam + err = json.Unmarshal(schemaJSON, &schema) + if err != nil { + return zeroTool, fmt.Errorf("failed to unmarshal to anthropic input schema: %w", err) + } + + toolParam := anthropic.ToolParam{ + Name: tool.Name, + Description: anthropic.String(tool.Description), + InputSchema: schema, + } + + return anthropic.ToolUnionParam{ + OfTool: &toolParam, + }, nil +} + +func (c *MCPClient) ProcessQuery(ctx context.Context, query string) (string, error) { + if c.session == nil { + return "", fmt.Errorf("client is not connected to any server") + } + + messages := []anthropic.MessageParam{ + anthropic.NewUserMessage(anthropic.NewTextBlock(query)), + } + + // Initial Claude API call with tools + response, err := c.anthropic.Messages.New(ctx, anthropic.MessageNewParams{ + Model: model, + MaxTokens: 1024, + Messages: messages, + Tools: c.tools, + }) + if err != nil { + return "", fmt.Errorf("anthropic API request failed: %w", err) + } + + var toolUseBlocks []anthropic.ToolUseBlock + var finalText []string + for _, block := range response.Content { + switch b := block.AsAny().(type) { + case anthropic.TextBlock: + finalText = append(finalText, b.Text) + case anthropic.ToolUseBlock: + toolUseBlocks = append(toolUseBlocks, b) + } + } + + if len(toolUseBlocks) == 0 { + return strings.Join(finalText, "\n"), nil + } + + // Append assistant's response to message history + messages = append(messages, response.ToParam()) + + // Execute each tool call and collect responses + var anthropicToolResults []anthropic.ContentBlockParamUnion + for _, toolUseBlock := range toolUseBlocks { + // Add information about the tool call to final text + finalText = append(finalText, fmt.Sprintf("[Calling tool %s with args %s]", toolUseBlock.Name, string(toolUseBlock.Input))) + + // Call the MCP server tool + mcpToolResult, err := c.session.CallTool(ctx, &mcp.CallToolParams{ + Name: toolUseBlock.Name, + Arguments: toolUseBlock.Input, + }) + if err != nil { + return "", fmt.Errorf("tool call %s failed: %w", toolUseBlock.Name, err) + } + + // Serialize tool result + resultJSON, err := json.Marshal(mcpToolResult) + if err != nil { + return "", fmt.Errorf("failed to serialize tool result: %w", err) + } + + anthropicToolResults = append(anthropicToolResults, anthropic.NewToolResultBlock( + toolUseBlock.ID, + string(resultJSON), + false, + )) + } + + // Append tool responses to message history + messages = append(messages, anthropic.NewUserMessage(anthropicToolResults...)) + + // Make another API call with tool results + response, err = c.anthropic.Messages.New(ctx, anthropic.MessageNewParams{ + Model: anthropic.ModelClaude3_7SonnetLatest, + MaxTokens: 1024, + Messages: messages, + }) + if err != nil { + return "", fmt.Errorf("anthropic API request with tool results failed: %w", err) + } + + // Collect text from final response + for _, block := range response.Content { + switch b := block.AsAny().(type) { + case anthropic.TextBlock: + finalText = append(finalText, b.Text) + } + } + + return strings.Join(finalText, "\n"), nil +} + +func (c *MCPClient) ChatLoop(ctx context.Context) error { + fmt.Println("\nMCP Client Started!") + fmt.Println("Type your queries or 'quit' to exit.") + + scanner := bufio.NewScanner(os.Stdin) + + for { + fmt.Print("\nQuery: ") + if !scanner.Scan() { + break // EOF + } + + query := strings.TrimSpace(scanner.Text()) + if strings.EqualFold(query, "quit") { + break + } + if query == "" { + continue + } + + response, err := c.ProcessQuery(ctx, query) + if err != nil { + fmt.Printf("\nError: %v\n", err) + continue + } + + fmt.Printf("\n%s\n", response) + } + + return scanner.Err() +} + +func (c *MCPClient) Cleanup() error { + if c.session != nil { + if err := c.session.Close(); err != nil { + return fmt.Errorf("failed to close session: %w", err) + } + c.session = nil + } + return nil +} + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "Usage: go run main.go [args...]") + os.Exit(1) + } + + serverArgs := os.Args[1:] + + client, err := NewMCPClient() + if err != nil { + log.Fatalf("Failed to create MCP client: %v", err) + } + + ctx := context.Background() + + if err := client.ConnectToServer(ctx, serverArgs); err != nil { + log.Fatalf("Failed to connect to MCP server: %v", err) + } + + if err := client.ChatLoop(ctx); err != nil { + log.Printf("ChatLoop error: %v", err) + } + + if err := client.Cleanup(); err != nil { + log.Printf("Cleanup error: %v", err) + } +}