- AI
- A
How I rewrote Model Context Protocol in Go and achieved 100K ops/sec
Hello! Today I will talk about GoMCP - a production-grade alternative to the official MCP SDK from Anthropic. Spoiler: it turned out to be 10 times faster, with multi-tenancy and enterprise features out of the box.
Hello everyone! My name is Dima, some of you already know me here, and today I will tell you about how I created GoMCP — a production-grade alternative to the official MCP SDK from Anthropic. Spoiler: it turned out to be 10 times faster, with multi-tenancy and enterprise features out of the box.
100K+ tool calls/sec (vs ~10K for the Python SDK)
Security hardening: input validation, audit logging, rate limiting
Multi-tenancy: namespace isolation + quotas
3 adapters: stdio (MCP v1), gRPC, HTTP REST
213 tests, 430+ Full Ralph iterations
Why not the official SDK?
At the end of 2024, Anthropic introduced the Model Context Protocol — a standard for connecting LLMs to external tools. The idea is great, but the implementation... let's just say, it's not for production:
Problems with the official SDK
# Typical MCP server in Python
@server.tool()
async def my_tool(args: dict) -> str:
# Where's the validation? Where's the rate limiting?
# Where's the audit logging?
return do_dangerous_stuff(args) # 🔥
Problem |
|---|
Python SDK
GoMCP
Input validation
❌
✅ Regex patterns, depth limits
Audit logging
❌
✅ Structured, ring buffer
Rate limiting
❌
✅ Per-client, configurable
Multi-tenancy
❌
✅ Quotas, tool ACL
Hot-reload
❌
✅ Zero-downtime
Architecture
┌─────────────────────────────────────────────────────────┐
│ gomcp-server │
├─────────────┬─────────────┬─────────────┬──────────────┤
│ Stdio │ gRPC │ HTTP │ Health │
│ Adapter │ Server │ Mode │ Endpoints │
├─────────────┴─────────────┴─────────────┴──────────────┤
│ Supervisor │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │ Security │ │ Tenant │ │ Batching │ │ HotReload │ │
│ └──────────┘ └──────────┘ └──────────┘ └───────────┘ │
└─────────────────────────────────────────────────────────┘
Key decisions
1. Supervisor pattern — centralized management of workers:
sup := supervisor.New(supervisor.Config{
DefaultTimeout: 30 * time.Second,
MaxWorkers: 100,
HeartbeatPeriod: 5 * time.Second,
})
// Graceful shutdown
sup.Shutdown() // Waits for all calls to complete
2. Security-first — input validation:
validator := security.DefaultValidator()
// Checks:
// - Max string length: 100KB
// - Max array length: 10K items
// - Max nesting depth: 20
// - XSS patterns: tm := tenant.NewManager()
t, _ := tm.CreateTenant("company-a", "Company A", tenant.Quotas{
MaxToolCalls: 1000,
MaxBatchSize: 50,
MaxConcurrent: 10,
})
// Access restriction to tools
t.SetAllowedTools([]string{"read_file", "list_dir"})
// company-a will NOT be able to call "delete_file"
Performance
Tested on AMD Ryzen 9 5900X, 32GB RAM:
Operation | ops/sec | Latency p99 |
|---|---|---|
Supervisor.CallTool | 100,000 | 10ms |
Security.ValidateJSON | 500,000 | 2ms |
AuditLogger.Log | 1,000,000 | 1ms |
Tenant.CheckQuota | 2,000,000 | 0.5ms |
Batching
batch := batching.NewBuilder().
Add("r1", "tool1", args1).
Add("r2", "tool2", args2).
Add("r3", "tool3", args3).
Parallel(5). // Up to 5 parallel calls
Build()
result := processor.Process(ctx, batch)
// Instead of 3 sequential calls — 1 parallel batch
Three adapters — one interface
1. Stdio (MCP v1 compatible)
gomcp-server -mode=stdio
JSON-RPC 2.0 over stdin/stdout. Fully compatible with Claude Desktop:
{"jsonrpc":"2.0","id":"1","method":"tools/list"}
2. gRPC (native)
gomcp-server -mode=grpc -addr=:50051
For microservices architecture. Supports streaming, TLS.
3. HTTP REST (Docker-native)
gomcp-server -mode=http -addr=:8080
# List tools
curl http://localhost:8080/v1/tools
# Call tool
curl -X POST http://localhost:8080/v1/tools/call \
-d '{"tool":"echo","arguments":{"msg":"hello"}}'
# Batch
curl -X POST http://localhost:8080/v1/tools/batch \
-d '{"requests":[...], "parallel": true}'
Docker
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o /gomcp-server ./cmd/gomcp-server
FROM alpine:3.19
RUN adduser -D gomcp
USER gomcp
COPY --from=builder /gomcp-server /app/gomcp-server
HEALTHCHECK CMD wget --spider http://localhost:8080/healthz
ENTRYPOINT ["/app/gomcp-server"]
# docker-compose.yml
services:
gomcp-server:
build: .
ports: ["8080:8080"]
deploy:
resources:
limits:
memory: 512M
TypeScript SDK
import { GoMCPClient } from '@gomcp/sdk';
const client = new GoMCPClient({
baseUrl: 'http://localhost:8080'
});
// List tools
const tools = await client.listTools();
// Call tool
const result = await client.callTool('echo', { msg: 'hello' });
// Batch
const batch = await client.batchCall([
{ tool: 't1', arguments: {} },
{ tool: 't2', arguments: {} }
], { parallel: true });
Testing
I used the Full Ralph methodology — repeated test execution to identify flaky tests and race conditions:
Go packages: 12
Total tests: 174
Full Ralph iterations: 430+
TypeScript SDK: 39 tests
Each package went through at least 10 iterations of the full test suite.
Write comment