Closes open points in [comment](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/3220#issuecomment-5373089321)
338 lines
11 KiB
Go
338 lines
11 KiB
Go
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
|
|
// Use of this source code is governed by an MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package mcp_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"reflect"
|
|
"time"
|
|
|
|
"github.com/google/jsonschema-go/jsonschema"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
func ExampleServer_AddTool_rawSchema() {
|
|
// In some scenarios, you may want your server to be a pass-through, with
|
|
// JSON schema coming from another source. Or perhaps you want to implement
|
|
// tool validation using a different JSON schema library.
|
|
//
|
|
// For these cases, you can use [mcp.Server.AddTool], which is the "raw" form
|
|
// of the API. Note that it is the caller's responsibility to validate inputs
|
|
// and outputs.
|
|
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
|
server.AddTool(&mcp.Tool{
|
|
Name: "greet",
|
|
InputSchema: json.RawMessage(`{"type":"object","properties":{"user":{"type":"string"}}}`),
|
|
}, func(_ context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
// Note: no validation!
|
|
var args struct{ User string }
|
|
if err := json.Unmarshal(req.Params.Arguments, &args); err != nil {
|
|
// TODO: we should use a jsonrpc error here, to be consistent with other
|
|
// SDKs.
|
|
return nil, err
|
|
}
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: "Hi " + args.User}},
|
|
}, nil
|
|
})
|
|
|
|
ctx := context.Background()
|
|
session, err := connect(ctx, server)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer session.Close()
|
|
|
|
res, err := session.CallTool(ctx, &mcp.CallToolParams{
|
|
Name: "greet",
|
|
Arguments: map[string]any{"user": "you"},
|
|
})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Println(res.Content[0].(*mcp.TextContent).Text)
|
|
// Output: Hi you
|
|
}
|
|
|
|
func ExampleAddTool_customMarshalling() {
|
|
// Sometimes when you want to customize the input or output schema for a
|
|
// tool, you need to customize the schema of a single helper type that's used
|
|
// in several places.
|
|
//
|
|
// For example, suppose you had a type that marshals/unmarshals like a
|
|
// time.Time, and that type was used multiple times in your tool input.
|
|
type MyDate struct {
|
|
time.Time
|
|
}
|
|
type Input struct {
|
|
Query string `json:"query,omitempty"`
|
|
Start MyDate `json:"start,omitempty"`
|
|
End MyDate `json:"end,omitempty"`
|
|
}
|
|
|
|
// In this case, you can use jsonschema.For along with jsonschema.ForOptions
|
|
// to customize the schema inference for your custom type.
|
|
inputSchema, err := jsonschema.For[Input](&jsonschema.ForOptions{
|
|
TypeSchemas: map[reflect.Type]*jsonschema.Schema{
|
|
reflect.TypeFor[MyDate](): {Type: "string"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
|
toolHandler := func(context.Context, *mcp.CallToolRequest, Input) (*mcp.CallToolResult, any, error) {
|
|
panic("not implemented")
|
|
}
|
|
mcp.AddTool(server, &mcp.Tool{Name: "my_tool", InputSchema: inputSchema}, toolHandler)
|
|
|
|
ctx := context.Background()
|
|
session, err := connect(ctx, server) // create an in-memory connection
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer session.Close()
|
|
|
|
for t, err := range session.Tools(ctx, nil) {
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
schemaJSON, err := json.MarshalIndent(t.InputSchema, "", "\t")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Println(t.Name, string(schemaJSON))
|
|
}
|
|
// Output:
|
|
// my_tool {
|
|
// "additionalProperties": false,
|
|
// "properties": {
|
|
// "end": {
|
|
// "type": "string"
|
|
// },
|
|
// "query": {
|
|
// "type": "string"
|
|
// },
|
|
// "start": {
|
|
// "type": "string"
|
|
// }
|
|
// },
|
|
// "type": "object"
|
|
// }
|
|
}
|
|
|
|
type Location struct {
|
|
Name string `json:"name"`
|
|
Latitude *float64 `json:"latitude,omitempty"`
|
|
Longitude *float64 `json:"longitude,omitempty"`
|
|
}
|
|
|
|
type Forecast struct {
|
|
Forecast string `json:"forecast" jsonschema:"description of the day's weather"`
|
|
Type WeatherType `json:"type" jsonschema:"type of weather"`
|
|
Rain float64 `json:"rain" jsonschema:"probability of rain, between 0 and 1"`
|
|
High float64 `json:"high" jsonschema:"high temperature"`
|
|
Low float64 `json:"low" jsonschema:"low temperature"`
|
|
}
|
|
|
|
type WeatherType string
|
|
|
|
const (
|
|
Sunny WeatherType = "sun"
|
|
PartlyCloudy WeatherType = "partly_cloudy"
|
|
Cloudy WeatherType = "clouds"
|
|
Rainy WeatherType = "rain"
|
|
Snowy WeatherType = "snow"
|
|
)
|
|
|
|
type Probability float64
|
|
|
|
// !+weathertool
|
|
|
|
type WeatherInput struct {
|
|
Location Location `json:"location" jsonschema:"user location"`
|
|
Days int `json:"days" jsonschema:"number of days to forecast"`
|
|
}
|
|
|
|
type WeatherOutput struct {
|
|
Summary string `json:"summary" jsonschema:"a summary of the weather forecast"`
|
|
Confidence Probability `json:"confidence" jsonschema:"confidence, between 0 and 1"`
|
|
AsOf time.Time `json:"asOf" jsonschema:"the time the weather was computed"`
|
|
DailyForecast []Forecast `json:"dailyForecast" jsonschema:"the daily forecast"`
|
|
Source string `json:"source,omitempty" jsonschema:"the organization providing the weather forecast"`
|
|
}
|
|
|
|
func WeatherTool(ctx context.Context, req *mcp.CallToolRequest, in WeatherInput) (*mcp.CallToolResult, WeatherOutput, error) {
|
|
perfectWeather := WeatherOutput{
|
|
Summary: "perfect",
|
|
Confidence: 1.0,
|
|
AsOf: time.Now(),
|
|
}
|
|
for range in.Days {
|
|
perfectWeather.DailyForecast = append(perfectWeather.DailyForecast, Forecast{
|
|
Forecast: "another perfect day",
|
|
Type: Sunny,
|
|
Rain: 0.0,
|
|
High: 72.0,
|
|
Low: 72.0,
|
|
})
|
|
}
|
|
return nil, perfectWeather, nil
|
|
}
|
|
|
|
// !-weathertool
|
|
|
|
func ExampleAddTool_complexSchema() {
|
|
// This example demonstrates a tool with a more 'realistic' input and output
|
|
// schema. We use a combination of techniques to tune our input and output
|
|
// schemas.
|
|
|
|
// !+customschemas
|
|
|
|
// Distinguished Go types allow custom schemas to be reused during inference.
|
|
customSchemas := map[reflect.Type]*jsonschema.Schema{
|
|
reflect.TypeFor[Probability](): {Type: "number", Minimum: jsonschema.Ptr(0.0), Maximum: jsonschema.Ptr(1.0)},
|
|
reflect.TypeFor[WeatherType](): {Type: "string", Enum: []any{Sunny, PartlyCloudy, Cloudy, Rainy, Snowy}},
|
|
}
|
|
opts := &jsonschema.ForOptions{TypeSchemas: customSchemas}
|
|
in, err := jsonschema.For[WeatherInput](opts)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Furthermore, we can tweak the inferred schema, in this case limiting
|
|
// forecasts to 0-10 days.
|
|
daysSchema := in.Properties["days"]
|
|
daysSchema.Minimum = jsonschema.Ptr(0.0)
|
|
daysSchema.Maximum = jsonschema.Ptr(10.0)
|
|
|
|
// Output schema inference can reuse our custom schemas from input inference.
|
|
out, err := jsonschema.For[WeatherOutput](opts)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Now add our tool to a server. Since we've customized the schemas, we need
|
|
// to override the default schema inference.
|
|
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "weather",
|
|
InputSchema: in,
|
|
OutputSchema: out,
|
|
}, WeatherTool)
|
|
|
|
// !-customschemas
|
|
|
|
ctx := context.Background()
|
|
session, err := connect(ctx, server) // create an in-memory connection
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer session.Close()
|
|
|
|
// Check that the client observes the correct schemas.
|
|
for t, err := range session.Tools(ctx, nil) {
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
// Formatting the entire schemas would be too much output.
|
|
// Just check that our customizations were effective.
|
|
fmt.Println("max days:", jsonPath(t.InputSchema, "properties", "days", "maximum"))
|
|
fmt.Println("max confidence:", jsonPath(t.OutputSchema, "properties", "confidence", "maximum"))
|
|
fmt.Println("weather types:", jsonPath(t.OutputSchema, "properties", "dailyForecast", "items", "properties", "type", "enum"))
|
|
}
|
|
// Output:
|
|
// max days: 10
|
|
// max confidence: 1
|
|
// weather types: [sun partly_cloudy clouds rain snow]
|
|
}
|
|
|
|
// !+contenttypes
|
|
|
|
func ExampleAddTool_contentTypes() {
|
|
chartPNG := []byte("\x89PNG\r\n\x1a\n")
|
|
summaryWAV := []byte("RIFF....WAVE")
|
|
|
|
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
|
mcp.AddTool(server, &mcp.Tool{Name: "report"}, func(context.Context, *mcp.CallToolRequest, struct{}) (*mcp.CallToolResult, any, error) {
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{
|
|
&mcp.TextContent{Text: "Q3 revenue by region."},
|
|
&mcp.ImageContent{Data: chartPNG, MIMEType: "image/png"},
|
|
&mcp.AudioContent{Data: summaryWAV, MIMEType: "audio/wav"},
|
|
// An embedded resource inlines the contents, so the client
|
|
// needs no follow-up resources/read. Binary contents go in
|
|
// ResourceContents.Blob instead of Text.
|
|
&mcp.EmbeddedResource{
|
|
Resource: &mcp.ResourceContents{
|
|
URI: "file:///reports/q3.csv",
|
|
MIMEType: "text/csv",
|
|
Text: "region,revenue\nEMEA,42\n",
|
|
},
|
|
},
|
|
// A resource link only points at a resource. Prefer it when the
|
|
// client may not need the contents, since an embedded resource
|
|
// is transferred either way.
|
|
&mcp.ResourceLink{URI: "file:///reports/q3.pdf", MIMEType: "application/pdf"},
|
|
},
|
|
}, nil, nil
|
|
})
|
|
|
|
ctx := context.Background()
|
|
session, err := connect(ctx, server)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer session.Close()
|
|
|
|
res, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "report"})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Clients type-switch over the content they receive.
|
|
for _, content := range res.Content {
|
|
switch c := content.(type) {
|
|
case *mcp.TextContent:
|
|
fmt.Println("text:", c.Text)
|
|
case *mcp.ImageContent:
|
|
fmt.Printf("image: %s, %d bytes\n", c.MIMEType, len(c.Data))
|
|
case *mcp.AudioContent:
|
|
fmt.Printf("audio: %s, %d bytes\n", c.MIMEType, len(c.Data))
|
|
case *mcp.EmbeddedResource:
|
|
fmt.Printf("embedded resource: %s (%s)\n", c.Resource.URI, c.Resource.MIMEType)
|
|
case *mcp.ResourceLink:
|
|
fmt.Printf("resource link: %s (%s)\n", c.URI, c.MIMEType)
|
|
}
|
|
}
|
|
// Output:
|
|
// text: Q3 revenue by region.
|
|
// image: image/png, 8 bytes
|
|
// audio: audio/wav, 12 bytes
|
|
// embedded resource: file:///reports/q3.csv (text/csv)
|
|
// resource link: file:///reports/q3.pdf (application/pdf)
|
|
}
|
|
|
|
// !-contenttypes
|
|
|
|
func connect(ctx context.Context, server *mcp.Server) (*mcp.ClientSession, error) {
|
|
t1, t2 := mcp.NewInMemoryTransports()
|
|
if _, err := server.Connect(ctx, t1, nil); err != nil {
|
|
return nil, err
|
|
}
|
|
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
|
|
return client.Connect(ctx, t2, nil)
|
|
}
|
|
|
|
func jsonPath(s any, path ...string) any {
|
|
if len(path) == 0 {
|
|
return s
|
|
}
|
|
return jsonPath(s.(map[string]any)[path[0]], path[1:]...)
|
|
}
|