Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mcp-client-go/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ANTHROPIC_API_KEY=your_api_key_here
3 changes: 3 additions & 0 deletions mcp-client-go/README.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions mcp-client-go/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
32 changes: 32 additions & 0 deletions mcp-client-go/go.sum
Original file line number Diff line number Diff line change
@@ -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=
277 changes: 277 additions & 0 deletions mcp-client-go/main.go
Original file line number Diff line number Diff line change
@@ -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 <server_script_or_binary> [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)
}
}