package mcp import ( "bytes" "context" "encoding/json" "log" "net/http" "net/http/httptest" "testing" "go-micro.dev/v6/auth" "go-micro.dev/v6/client" "go-micro.dev/v6/registry" ) // benchServer creates a Server with N pre-populated tools. func benchServer(n int, opts Options) *Server { if opts.Logger == nil { opts.Logger = log.New(log.Writer(), "", 0) } if opts.Context == nil { opts.Context = context.Background() } if opts.Client == nil { opts.Client = client.DefaultClient } if opts.Registry == nil { opts.Registry = registry.DefaultRegistry } s := &Server{ opts: opts, tools: make(map[string]*Tool, n), limiters: make(map[string]*rateLimiter), } for i := 0; i < n; i++ { name := toolName(i) s.tools[name] = &Tool{ Name: name, Description: "Benchmark tool " + name, InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "id": map[string]interface{}{ "type": "string", "description": "Resource identifier", }, }, "required": []interface{}{"id"}, }, Service: "bench", Endpoint: "Handler.Method", } } return s } func toolName(i int) string { return "bench.Handler.Method" + string(rune('A'+i%26)) } // --- Benchmarks --- // BenchmarkListTools measures tool listing throughput. // This is the most common MCP operation — agents call it on every session start. func BenchmarkListTools(b *testing.B) { for _, numTools := range []int{10, 50, 100} { b.Run(toolCountLabel(numTools), func(b *testing.B) { s := benchServer(numTools, Options{}) req := httptest.NewRequest(http.MethodGet, "/mcp/tools", nil) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { w := httptest.NewRecorder() s.handleListTools(w, req) if w.Code != http.StatusOK { b.Fatalf("unexpected status %d", w.Code) } } }) } } // BenchmarkListToolsParallel measures concurrent tool listing. func BenchmarkListToolsParallel(b *testing.B) { s := benchServer(50, Options{}) req := httptest.NewRequest(http.MethodGet, "/mcp/tools", nil) b.ResetTimer() b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { w := httptest.NewRecorder() s.handleListTools(w, req) } }) } // BenchmarkToolLookup measures tool name resolution from the tools map. func BenchmarkToolLookup(b *testing.B) { for _, numTools := range []int{10, 50, 100, 500} { b.Run(toolCountLabel(numTools), func(b *testing.B) { s := benchServer(numTools, Options{}) name := toolName(numTools / 2) // look up a tool in the middle b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { s.toolsMu.RLock() _, ok := s.tools[name] s.toolsMu.RUnlock() if !ok { b.Fatal("tool not found") } } }) } } // BenchmarkAuthInspect measures auth token inspection overhead. func BenchmarkAuthInspect(b *testing.B) { ma := &mockAuth{ accounts: map[string]*auth.Account{ "valid-token": { ID: "bench-user", Scopes: []string{"read", "write"}, }, }, } b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { acc, err := ma.Inspect("valid-token") if err != nil || acc.ID != "bench-user" { b.Fatal("unexpected result") } } } // BenchmarkScopeCheck measures scope validation overhead per tool call. func BenchmarkScopeCheck(b *testing.B) { accountScopes := []string{"users:read", "users:write", "orders:read", "admin"} requiredScopes := []string{"users:write"} b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { hasScope(accountScopes, requiredScopes) } } // BenchmarkAuditRecord measures audit record creation overhead. func BenchmarkAuditRecord(b *testing.B) { var records int s := benchServer(10, Options{ AuditFunc: func(r AuditRecord) { records++ }, }) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { s.opts.AuditFunc(AuditRecord{ TraceID: "trace-123", Tool: "bench.Handler.MethodA", AccountID: "user-1", Allowed: true, }) } } // BenchmarkRateLimiter measures rate limiter check overhead. func BenchmarkRateLimiter(b *testing.B) { s := benchServer(10, Options{ RateLimit: &RateLimitConfig{ RequestsPerSecond: 1000000, // Very high so it doesn't block Burst: 1000000, }, }) // Initialize limiters for tools for name := range s.tools { s.limiters[name] = newRateLimiter(s.opts.RateLimit.RequestsPerSecond, s.opts.RateLimit.Burst) } name := toolName(0) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { s.limitersMu.RLock() l := s.limiters[name] s.limitersMu.RUnlock() l.Allow() } } // BenchmarkJSONEncodeTool measures JSON serialization of tool definitions. func BenchmarkJSONEncodeTool(b *testing.B) { tool := &Tool{ Name: "myservice.Users.GetUser", Description: "Retrieve a user by their unique ID. Returns the full profile.", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "id": map[string]interface{}{ "type": "string", "description": "User ID in UUID format", }, }, "required": []interface{}{"id"}, }, Scopes: []string{"users:read"}, Service: "myservice", Endpoint: "Users.GetUser", } b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { var buf bytes.Buffer json.NewEncoder(&buf).Encode(tool) } } // BenchmarkJSONDecodeCallRequest measures parsing of incoming tool call requests. func BenchmarkJSONDecodeCallRequest(b *testing.B) { body := []byte(`{"tool":"myservice.Users.GetUser","arguments":{"id":"user-123"}}`) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { var req struct { Tool string `json:"tool"` Arguments map[string]interface{} `json:"arguments"` } json.Unmarshal(body, &req) } } // --- Helpers --- func toolCountLabel(n int) string { switch { case n >= 500: return "500_tools" case n >= 100: return "100_tools" case n >= 50: return "50_tools" default: return "10_tools" } }