e071084ebe
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
259 lines
5.9 KiB
Go
259 lines
5.9 KiB
Go
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"
|
|
}
|
|
}
|