Skip to content
Draft
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
6 changes: 6 additions & 0 deletions mcp-client-go/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# An LLM-Powered Chatbot MCP Client written in Go

See the [Build an MCP client](https://modelcontextprotocol.io/docs/develop/build-client) tutorial for more information.

## Structured output

The SDK does not validate tool output, so this client compiles each declared `outputSchema` at connect time and checks results against it — the spec's client-side SHOULD. It uses [`jsonschema-go`](https://pkg.go.dev/github.com/google/jsonschema-go/jsonschema), already an SDK dependency.

The two channels go to different readers: `Content` is forwarded to the model, while `StructuredContent` is used as data — the client counts the items it returns. See [Structured Content](https://modelcontextprotocol.io/specification/draft/server/tools#structured-content).
10 changes: 6 additions & 4 deletions mcp-client-go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ go 1.25.1

require (
github.com/anthropics/anthropic-sdk-go v1.14.0
github.com/google/jsonschema-go v0.4.3
github.com/joho/godotenv v1.5.1
github.com/modelcontextprotocol/go-sdk v1.4.1
github.com/modelcontextprotocol/go-sdk v1.7.0-pre.3
)

require (
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // 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
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/time v0.15.0 // indirect
)
28 changes: 16 additions & 12 deletions mcp-client-go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ github.com/anthropics/anthropic-sdk-go v1.14.0 h1:EzNQvnZlaDHe2UPkoUySDz3ixRgNbw
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/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
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.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
github.com/google/jsonschema-go v0.4.3/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.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc=
github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s=
github.com/modelcontextprotocol/go-sdk v1.7.0-pre.3 h1:SEAY9IduDif4iApnZgpFkjFIdo3askSGZVbZIYyTy6I=
github.com/modelcontextprotocol/go-sdk v1.7.0-pre.3/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts=
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/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
Expand All @@ -32,11 +32,15 @@ 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/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
100 changes: 82 additions & 18 deletions mcp-client-go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,28 @@ import (

"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
"github.com/google/jsonschema-go/jsonschema"
"github.com/joho/godotenv"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

var model anthropic.Model = anthropic.ModelClaudeSonnet4_5_20250929
// A string literal rather than an anthropic.Model constant: the pinned SDK
// version predates this model and has no constant for it.
var model anthropic.Model = "claude-sonnet-5"

type MCPClient struct {
anthropic *anthropic.Client
session *mcp.ClientSession
tools []anthropic.ToolUnionParam
// Compiled outputSchema per tool name, for validating results.
outputSchemas map[string]*jsonschema.Resolved
}

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")
}
// .env is optional; the key may come from the environment instead.
_ = godotenv.Load()

client := anthropic.NewClient(option.WithAPIKey(apiKey))
client := anthropic.NewClient(option.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")))

return &MCPClient{
anthropic: &client,
Expand Down Expand Up @@ -80,13 +78,22 @@ func (c *MCPClient) ConnectToServer(ctx context.Context, serverArgs []string) er
var toolNames []string

// Convert MCP tools to Anthropic tool format
c.outputSchemas = make(map[string]*jsonschema.Resolved)
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)

resolved, err := resolveOutputSchema(tool)
if err != nil {
return fmt.Errorf("failed to compile output schema of tool %s: %w", tool.Name, err)
}
if resolved != nil {
c.outputSchemas[tool.Name] = resolved
}
}

fmt.Printf("Connected to server with tools: %v\n", toolNames)
Expand Down Expand Up @@ -116,6 +123,40 @@ func mcpToolToAnthropicTool(tool *mcp.Tool) (anthropic.ToolUnionParam, error) {
}, nil
}

// resolveOutputSchema compiles a tool's declared outputSchema, or returns
// (nil, nil) when it declares none. OutputSchema is `any` because the root may
// be any JSON Schema, so round-trip through JSON to accept every form.
func resolveOutputSchema(tool *mcp.Tool) (*jsonschema.Resolved, error) {
if tool.OutputSchema == nil {
return nil, nil
}
raw, err := json.Marshal(tool.OutputSchema)
if err != nil {
return nil, fmt.Errorf("failed to marshal output schema: %w", err)
}
var schema jsonschema.Schema
if err := json.Unmarshal(raw, &schema); err != nil {
return nil, fmt.Errorf("failed to unmarshal output schema: %w", err)
}
return schema.Resolve(nil)
}

// validateToolOutput checks a result against its tool's declared outputSchema.
// Error results are exempt: they carry a message, not data.
func (c *MCPClient) validateToolOutput(name string, result *mcp.CallToolResult) error {
resolved, ok := c.outputSchemas[name]
if !ok || result.IsError {
return nil
}
if result.StructuredContent == nil {
return fmt.Errorf("tool %s declares an output schema but returned no structured content", name)
}
if err := resolved.Validate(result.StructuredContent); err != nil {
return fmt.Errorf("structured content from tool %s does not match its output schema: %w", name, err)
}
return 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")
Expand Down Expand Up @@ -169,16 +210,27 @@ func (c *MCPClient) ProcessQuery(ctx context.Context, query string) (string, err
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)
if err := c.validateToolOutput(toolUseBlock.Name, mcpToolResult); err != nil {
return "", err
}

// StructuredContent is data the application can use directly.
if items, ok := mcpToolResult.StructuredContent.([]any); ok {
finalText = append(finalText, fmt.Sprintf("[%s returned %d items]", toolUseBlock.Name, len(items)))
}

// Content is a list of block types; forward only the text ones.
var texts []string
for _, block := range mcpToolResult.Content {
if text, ok := block.(*mcp.TextContent); ok {
texts = append(texts, text.Text)
}
}

anthropicToolResults = append(anthropicToolResults, anthropic.NewToolResultBlock(
toolUseBlock.ID,
string(resultJSON),
false,
strings.Join(texts, "\n"),
mcpToolResult.IsError,
))
}

Expand All @@ -187,7 +239,7 @@ func (c *MCPClient) ProcessQuery(ctx context.Context, query string) (string, err

// Make another API call with tool results
response, err = c.anthropic.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.ModelClaude3_7SonnetLatest,
Model: model,
MaxTokens: 1024,
Messages: messages,
})
Expand Down Expand Up @@ -267,6 +319,18 @@ func main() {
log.Fatalf("Failed to connect to MCP server: %v", err)
}

// Connecting and listing tools needs no credentials; querying them does.
// Matching the Python and TypeScript clients, report and exit rather than
// failing, so the connection itself can be exercised without a key.
if os.Getenv("ANTHROPIC_API_KEY") == "" {
fmt.Println("\nNo ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:")
fmt.Println(" export ANTHROPIC_API_KEY=your-api-key-here")
if err := client.Cleanup(); err != nil {
log.Printf("Cleanup error: %v", err)
}
return
}

if err := client.ChatLoop(ctx); err != nil {
log.Printf("ChatLoop error: %v", err)
}
Expand Down
Binary file added mcp-client-go/mcp-client-go.exe
Binary file not shown.
6 changes: 6 additions & 0 deletions weather-server-go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ go build -o weather
The server will communicate via stdio and expose two MCP tools:
- `get_forecast` - Get weather forecast for a location (requires latitude and longitude)
- `get_alerts` - Get weather alerts for a US state (requires two-letter state code)

## Structured output

Both tools declare an `OutputSchema` and return `StructuredContent`. `get_forecast` returns an object; `get_alerts` returns a top-level JSON array, which protocol revision `2026-07-28` is the first to allow — see [Structured Content](https://modelcontextprotocol.io/specification/draft/server/tools#structured-content) in the spec.

Note that an array-rooted schema requires a `2026-07-28` client: the SDK sends it as written on every connection rather than projecting it down, so an older client rejects the tool list.
12 changes: 8 additions & 4 deletions weather-server-go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ module github.com/modelcontextprotocol/quickstart-resources/weather-server-go

go 1.25.1

require github.com/modelcontextprotocol/go-sdk v1.4.1
require (
github.com/google/jsonschema-go v0.4.3
github.com/modelcontextprotocol/go-sdk v1.7.0-pre.3
)

require (
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/time v0.15.0 // indirect
)
28 changes: 16 additions & 12 deletions weather-server-go/go.sum
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
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.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc=
github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/modelcontextprotocol/go-sdk v1.7.0-pre.3 h1:SEAY9IduDif4iApnZgpFkjFIdo3askSGZVbZIYyTy6I=
github.com/modelcontextprotocol/go-sdk v1.7.0-pre.3/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
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/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
Loading
Loading