Skip to content

Commit 742c6dc

Browse files
committed
mcp: add a streamable serving benchmark
Add a benchmark using the streamable HTTP handler, and a more realistic input/output signature. Also expose this handler as an example (both a go example and a server in the examples/ directory). For #190
1 parent 22f86c4 commit 742c6dc

File tree

3 files changed

+182
-2
lines changed

3 files changed

+182
-2
lines changed

mcp/event_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ func BenchmarkMemoryEventStore(b *testing.B) {
287287
payload := make([]byte, test.datasize)
288288
start := time.Now()
289289
b.ResetTimer()
290-
for i := 0; i < b.N; i++ {
290+
for i := range b.N {
291291
sessionID := sessionIDs[i%len(sessionIDs)]
292292
streamID := streamIDs[i%len(sessionIDs)][i%3]
293293
store.Append(ctx, sessionID, streamID, payload)

mcp/streamable_bench_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
2+
// Use of this source code is governed by an MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package mcp_test
6+
7+
import (
8+
"context"
9+
"net/http"
10+
"net/http/httptest"
11+
"testing"
12+
13+
"github.com/google/jsonschema-go/jsonschema"
14+
"github.com/modelcontextprotocol/go-sdk/mcp"
15+
)
16+
17+
func BenchmarkStreamableServing(b *testing.B) {
18+
customSchemas := map[any]*jsonschema.Schema{
19+
Probability(0): {Type: "number", Minimum: jsonschema.Ptr(0.0), Maximum: jsonschema.Ptr(1.0)},
20+
WeatherType(""): {Type: "string", Enum: []any{Sunny, PartlyCloudy, Cloudy, Rainy, Snowy}},
21+
}
22+
opts := &jsonschema.ForOptions{TypeSchemas: customSchemas}
23+
in, err := jsonschema.For[WeatherInput](opts)
24+
if err != nil {
25+
b.Fatal(err)
26+
}
27+
out, err := jsonschema.For[WeatherOutput](opts)
28+
if err != nil {
29+
b.Fatal(err)
30+
}
31+
32+
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
33+
mcp.AddTool(server, &mcp.Tool{
34+
Name: "weather",
35+
InputSchema: in,
36+
OutputSchema: out,
37+
}, WeatherTool)
38+
39+
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
40+
return server
41+
}, &mcp.StreamableHTTPOptions{JSONResponse: true})
42+
httpServer := httptest.NewServer(handler)
43+
defer httpServer.Close()
44+
45+
ctx, cancel := context.WithCancel(context.Background())
46+
defer cancel()
47+
session, err := mcp.NewClient(testImpl, nil).Connect(ctx, &mcp.StreamableClientTransport{Endpoint: httpServer.URL}, nil)
48+
if err != nil {
49+
b.Fatal(err)
50+
}
51+
defer session.Close()
52+
b.ResetTimer()
53+
for range b.N {
54+
if _, err := session.CallTool(ctx, &mcp.CallToolParams{
55+
Name: "weather",
56+
Arguments: WeatherInput{
57+
Location: Location{Name: "somewhere"},
58+
Days: 7,
59+
},
60+
}); err != nil {
61+
b.Errorf("CallTool failed: %v", err)
62+
}
63+
}
64+
}

mcp/tool_example_test.go

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"github.com/modelcontextprotocol/go-sdk/mcp"
1616
)
1717

18-
func ExampleAddTool_customTypeSchema() {
18+
func ExampleAddTool_customMarshalling() {
1919
// Sometimes when you want to customize the input or output schema for a
2020
// tool, you need to customize the schema of a single helper type that's used
2121
// in several places.
@@ -83,6 +83,122 @@ func ExampleAddTool_customTypeSchema() {
8383
// }
8484
}
8585

86+
type WeatherInput struct {
87+
Location Location `json:"location" jsonschema:"user location"`
88+
Days int `json:"days" jsonschema:"number of days to forecast"`
89+
}
90+
91+
type Location struct {
92+
Name string `json:"name"`
93+
Latitude *float64 `json:"latitude,omitempty"`
94+
Longitude *float64 `json:"longitude,omitempty"`
95+
}
96+
97+
type Forecast struct {
98+
Forecast string `json:"forecast" jsonschema:"description of the day's weather"`
99+
Type WeatherType `json:"type" jsonschema:"type of weather"`
100+
Rain float64 `json:"rain" jsonschema:"probability of rain, between 0 and 1"`
101+
High float64 `json:"high" jsonschema:"high temperature"`
102+
Low float64 `json:"low" jsonschema:"low temperature"`
103+
}
104+
105+
type WeatherType string
106+
107+
const (
108+
Sunny WeatherType = "sun"
109+
PartlyCloudy WeatherType = "partly_cloudy"
110+
Cloudy WeatherType = "clouds"
111+
Rainy WeatherType = "rain"
112+
Snowy WeatherType = "snow"
113+
)
114+
115+
type Probability float64
116+
117+
type WeatherOutput struct {
118+
Summary string `json:"summary" jsonschema:"a summary of the weather forecast"`
119+
Confidence Probability `json:"confidence" jsonschema:"confidence, between 0 and 1"`
120+
AsOf time.Time `json:"asOf" jsonschema:"the time the weather was computed"`
121+
DailyForecast []Forecast `json:"dailyForecast" jsonschema:"the daily forecast"`
122+
Source string `json:"source,omitempty" jsonschema:"the organization providing the weather forecast"`
123+
}
124+
125+
func WeatherTool(ctx context.Context, req *mcp.CallToolRequest, in WeatherInput) (*mcp.CallToolResult, WeatherOutput, error) {
126+
perfectWeather := WeatherOutput{
127+
Summary: "perfect",
128+
Confidence: 1.0,
129+
AsOf: time.Now(),
130+
}
131+
for range in.Days {
132+
perfectWeather.DailyForecast = append(perfectWeather.DailyForecast, Forecast{
133+
Forecast: "another perfect day",
134+
Type: Sunny,
135+
Rain: 0.0,
136+
High: 72.0,
137+
Low: 72.0,
138+
})
139+
}
140+
return nil, perfectWeather, nil
141+
}
142+
143+
func ExampleAddTool_complexSchema() {
144+
// This example demonstrates a tool with a more 'realistic' input and output
145+
// schema. We use a combination of techniques to tune our input and output
146+
// schemas.
147+
148+
// Distinguished Go types allow custom schemas to be reused during inference.
149+
customSchemas := map[any]*jsonschema.Schema{
150+
Probability(0): {Type: "number", Minimum: jsonschema.Ptr(0.0), Maximum: jsonschema.Ptr(1.0)},
151+
WeatherType(""): {Type: "string", Enum: []any{Sunny, PartlyCloudy, Cloudy, Rainy, Snowy}},
152+
}
153+
opts := &jsonschema.ForOptions{TypeSchemas: customSchemas}
154+
in, err := jsonschema.For[WeatherInput](opts)
155+
if err != nil {
156+
log.Fatal(err)
157+
}
158+
159+
// Furthermore, we can tweak the inferred schema, in this case limiting
160+
// forecasts to 0-10 days.
161+
daysSchema := in.Properties["days"]
162+
daysSchema.Minimum = jsonschema.Ptr(0.0)
163+
daysSchema.Maximum = jsonschema.Ptr(10.0)
164+
165+
// Output schema inference can reuse our custom schemas from input inference.
166+
out, err := jsonschema.For[WeatherOutput](opts)
167+
if err != nil {
168+
log.Fatal(err)
169+
}
170+
171+
// Now add our tool to a server. Since we've customized the schemas, we need
172+
// to override the default schema inference.
173+
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
174+
mcp.AddTool(server, &mcp.Tool{
175+
Name: "weather",
176+
InputSchema: in,
177+
OutputSchema: out,
178+
}, WeatherTool)
179+
180+
ctx := context.Background()
181+
session, err := connect(ctx, server) // create an in-memory connection
182+
if err != nil {
183+
log.Fatal(err)
184+
}
185+
defer session.Close()
186+
187+
// Check that the client observes the correct schemas.
188+
for t, err := range session.Tools(ctx, nil) {
189+
if err != nil {
190+
log.Fatal(err)
191+
}
192+
fmt.Println("max days:", *t.InputSchema.Properties["days"].Maximum)
193+
fmt.Println("max confidence:", *t.OutputSchema.Properties["confidence"].Maximum)
194+
fmt.Println("weather types:", t.OutputSchema.Properties["dailyForecast"].Items.Properties["type"].Enum)
195+
}
196+
// Output:
197+
// max days: 10
198+
// max confidence: 1
199+
// weather types: [sun partly_cloudy clouds rain snow]
200+
}
201+
86202
func connect(ctx context.Context, server *mcp.Server) (*mcp.ClientSession, error) {
87203
t1, t2 := mcp.NewInMemoryTransports()
88204
if _, err := server.Connect(ctx, t1, nil); err != nil {

0 commit comments

Comments
 (0)