Skip to content

Commit 07f62a0

Browse files
committed
examples/client: add a loadtest command
Add a loadtest client example, to help confirm performance of our streamable transport implementation. For golang/go#190
1 parent ddaf35e commit 07f62a0

File tree

2 files changed

+125
-3
lines changed

2 files changed

+125
-3
lines changed

examples/client/listfeatures/main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ func main() {
3131
flag.Parse()
3232
args := flag.Args()
3333
if len(args) == 0 {
34-
fmt.Fprintf(os.Stderr, "Usage: listfeatures <command> [<args>]")
35-
fmt.Fprintf(os.Stderr, "List all features for a stdio MCP server")
34+
fmt.Fprintln(os.Stderr, "Usage: listfeatures <command> [<args>]")
35+
fmt.Fprintln(os.Stderr, "List all features for a stdio MCP server")
3636
fmt.Fprintln(os.Stderr)
37-
fmt.Fprintf(os.Stderr, "Example: listfeatures npx @modelcontextprotocol/server-everything")
37+
fmt.Fprintln(os.Stderr, "Example:\n\tlistfeatures npx @modelcontextprotocol/server-everything")
3838
os.Exit(2)
3939
}
4040

examples/client/loadtest/main.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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+
// The load command load tests a streamable MCP server
6+
//
7+
// Usage: loadtest <URL>
8+
//
9+
// For example:
10+
//
11+
// loadtest -tool=greet -args='{"name": "foo"}' http://localhost:8080
12+
package main
13+
14+
import (
15+
"context"
16+
"encoding/json"
17+
"flag"
18+
"fmt"
19+
"log"
20+
"os"
21+
"os/signal"
22+
"sync"
23+
"sync/atomic"
24+
"time"
25+
26+
"github.com/modelcontextprotocol/go-sdk/mcp"
27+
)
28+
29+
var (
30+
duration = flag.Duration("duration", 1*time.Minute, "duration of the load test")
31+
tool = flag.String("tool", "", "tool to call")
32+
jsonArgs = flag.String("args", "", "JSON arguments to pass")
33+
workers = flag.Int("workers", 10, "number of concurrent workers")
34+
timeout = flag.Duration("timeout", 1*time.Second, "request timeout")
35+
qps = flag.Int("qps", 100, "tool calls per second, per worker")
36+
v = flag.Bool("v", false, "if set, enable verbose logging of results")
37+
)
38+
39+
func main() {
40+
flag.Usage = func() {
41+
out := flag.CommandLine.Output()
42+
fmt.Fprintf(out, "Usage: loadtest [flags] <URL>")
43+
fmt.Fprintf(out, "Load test a streamable HTTP server (CTRL-C to end early)")
44+
fmt.Fprintln(out)
45+
fmt.Fprintf(out, "Example: loadtest -tool=greet -args='{\"name\": \"foo\"}' http://localhost:8080\n")
46+
fmt.Fprintln(out)
47+
fmt.Fprintln(out, "Flags:")
48+
flag.PrintDefaults()
49+
}
50+
flag.Parse()
51+
args := flag.Args()
52+
if len(args) != 1 || *tool == "" {
53+
flag.Usage()
54+
os.Exit(2)
55+
}
56+
57+
parentCtx, cancel := context.WithTimeout(context.Background(), *duration)
58+
defer cancel()
59+
parentCtx, restoreSignal := signal.NotifyContext(parentCtx, os.Interrupt)
60+
defer restoreSignal()
61+
62+
var (
63+
start = time.Now()
64+
success atomic.Int64
65+
failure atomic.Int64
66+
)
67+
68+
// Run the test.
69+
var wg sync.WaitGroup
70+
for range *workers {
71+
wg.Add(1)
72+
go func() {
73+
defer wg.Done()
74+
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
75+
cs, err := client.Connect(parentCtx, &mcp.StreamableClientTransport{Endpoint: args[0]}, nil)
76+
if err != nil {
77+
log.Fatal(err)
78+
}
79+
defer cs.Close()
80+
81+
ticker := time.NewTicker(1 * time.Second / time.Duration(*qps))
82+
defer ticker.Stop()
83+
84+
for range ticker.C {
85+
ctx, cancel := context.WithTimeout(parentCtx, *timeout)
86+
defer cancel()
87+
88+
res, err := cs.CallTool(ctx, &mcp.CallToolParams{Name: *tool, Arguments: json.RawMessage(*jsonArgs)})
89+
if err != nil {
90+
if parentCtx.Err() != nil {
91+
return // test ended
92+
}
93+
failure.Add(1)
94+
if *v {
95+
log.Printf("FAILURE: %v", err)
96+
}
97+
} else {
98+
success.Add(1)
99+
if *v {
100+
data, err := json.Marshal(res)
101+
if err != nil {
102+
log.Fatalf("marshalling result: %v", err)
103+
}
104+
log.Printf("SUCCESS: %s", string(data))
105+
}
106+
}
107+
}
108+
}()
109+
}
110+
wg.Wait()
111+
restoreSignal() // call restore signal (redundantly) here to allow ctrl-c to work again
112+
113+
// Print stats.
114+
var (
115+
dur = time.Since(start)
116+
succ = success.Load()
117+
fail = failure.Load()
118+
)
119+
fmt.Printf("Results (in %s):\n", dur)
120+
fmt.Printf("\tsuccess: %d (%g QPS)\n", succ, float64(succ)/dur.Seconds())
121+
fmt.Printf("\tfailure: %d (%g QPS)\n", fail, float64(fail)/dur.Seconds())
122+
}

0 commit comments

Comments
 (0)