Mirror of https://github.com/modelcontextprotocol/go-sdk — The official Go SDK for Model Context Protocol servers and clients. Maintained in collaboration with Google.
  • Go 99.7%
  • Shell 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Sam Morrow c2c7edc3f0
perf: add schema caching to avoid repeated reflection (#685)
## Summary

This PR adds schema caching that dramatically reduces the cost of tool
registration in stateless server patterns.

**Contributors:** Thanks to @omgitsads and @almaleksia for collaborating
on and researching this solution!

## Problem

In stateless deployments like `github/github-mcp-server`, a new
`*mcp.Server` is created for each incoming request. This means `AddTool`
is called repeatedly for the same tools, causing:

1. **For typed handlers**: `jsonschema.ForType()` reflection is called
every time
2. **For pre-defined schemas**: `schema.Resolve()` is called every time

## Solution

Add a `schemaCache` that stores:
- Schemas by `reflect.Type` (for auto-generated schemas from typed
handlers)
- Resolved schemas by schema pointer (for pre-defined schemas)

The cache is:
- Concurrent-safe using `sync.Map`
- Unbounded (typical MCP servers have <100 tools)
- Global (lives across server instances)

## Benchmark Results

```
BenchmarkAddToolTypedHandler-22          977080    1223 ns/op     1208 B/op    21 allocs/op
BenchmarkAddToolTypedHandlerNoCache-22    6764  161463 ns/op    39262 B/op  1072 allocs/op
```

| Metric | With Cache | Without Cache | Improvement |
|--------|------------|---------------|-------------|
| Time | 1,223 ns/op | 161,463 ns/op | **132x faster** |
| Allocations | 21 allocs | 1,072 allocs | **51x fewer** |
| Memory | 1,208 B/op | 39,262 B/op | **32x less** |

## Files Changed

- `mcp/schema_cache.go` - New cache implementation
- `mcp/server.go` - Modified `setSchema` to use cache
- `mcp/schema_cache_test.go` - Unit tests for caching behavior
- `mcp/schema_cache_benchmark_test.go` - Benchmarks

## Impact for Integrators

**Automatic** - no code changes required. Integrators using:
- Typed handlers (`AddTool[In, Out]`) → cache by type
- Pre-defined schemas (`Tool{InputSchema: schema}`) → cache by pointer

Both patterns benefit from caching after the first call.

---

## Real-World Performance Validation

The following benchmarks were conducted using `github/github-mcp-server`
(a production MCP server with ~130 tools) to validate the performance
impact in a real-world scenario.

### Test Environment
- **Server:** github-mcp-server (stateless HTTP deployment)
- **Tools registered per request:** ~130 tools
- **Test methodology:** Python benchmark client, 30 iterations
(latency), 100 iterations (stress)
- **Date:** December 4, 2025

### Configurations Tested

| Configuration | Description |
|--------------|-------------|
| **main (mcp-go)** | Original implementation using mcp-go library |
| **go-sdk (no cache)** | go-sdk WITHOUT schema caching (broken) |
| **go-sdk (with cache)** | go-sdk WITH this PR's schema caching fix
|

### Latency Test Results (n=30)

#### Operation: `initialize`

| Configuration | P50 | P99 | Status |
|--------------|-----|-----|--------|
| main (mcp-go) | 11.48ms | 14.00ms | ✅ Baseline |
| go-sdk (no cache) | 20.47ms | 25.30ms | 🔴 **+78% REGRESSION** |
| go-sdk (with cache) | 10.36ms | 14.33ms | ✅ **FIXED (-10%)** |

#### Operation: `tools/list`

| Configuration | P50 | P99 | Status |
|--------------|-----|-----|--------|
| main (mcp-go) | 13.46ms | 22.47ms | ✅ Baseline |
| go-sdk (no cache) | 22.98ms | 29.11ms | 🔴 **+71% REGRESSION** |
| go-sdk (with cache) | 14.15ms | 15.91ms | ✅ **FIXED (+5%)** |

#### Operation: `prompts/list`

| Configuration | P50 | P99 | Status |
|--------------|-----|-----|--------|
| main (mcp-go) | 11.56ms | 13.81ms | ✅ Baseline |
| go-sdk (no cache) | 21.00ms | 26.42ms | 🔴 **+82% REGRESSION** |
| go-sdk (with cache) | 10.77ms | 15.25ms | ✅ **FIXED (-7%)** |

### Stress Test Results (n=100)

| Configuration | P50 (initialize) | P50 (tools/list) | P50
(prompts/list) |

|--------------|------------------|------------------|-------------------|
| main (mcp-go) | 12.06ms | 14.30ms | 11.34ms |
| go-sdk (no cache) | 20.06ms | 23.44ms | 19.59ms |
| go-sdk (with cache) | **11.83ms** | **14.58ms** | **11.05ms** |

### Memory/Allocation Comparison (from pprof)

| Configuration | Total Allocations | Comparison |
|--------------|-------------------|------------|
| main (mcp-go) | 355.91 MB | Baseline |
| go-sdk (no cache) | **1208.70 MB** | 🔴 **3.4x MORE allocations** |

#### Top Allocation Sources - go-sdk WITHOUT cache (broken)

| Function | Size | % | Issue |
|----------|------|---|-------|
| `jsonschema.UnmarshalJSON` | 324.60 MB | 27% | 🚨 Schema re-parsing |
| `encoding/json.Unmarshal` | 341.10 MB | 28% | JSON deserialization |
| `jsonschema.resolve` | 219.51 MB | 18% | 🚨 Schema re-resolution |
| `jsonschema.MarshalJSON` | 92.52 MB | 8% | Schema JSON encoding |

### Root Cause Analysis

The `google/jsonschema-go` library regenerates JSON schemas on every
request instead of caching them. In a server with ~130 tools, this
causes:
- **70-80% latency regression** on all MCP operations
- **3.4x more memory allocations** per request
- **~70% of all allocations** are schema-related operations

### Key Findings

1. ✅ **REGRESSION CONFIRMED:** go-sdk without schema caching is 70-80%
slower than mcp-go
2. ✅ **FIX VERIFIED:** Schema caching restores performance to baseline
(or better)
3. ✅ **MEMORY IMPACT:** 3.4x reduction in allocations with caching
4. ✅ **PRODUCTION READY:** Fixed version performs equivalently to mcp-go
baseline

---------

Co-authored-by: Adam Holt <4619+omgitsads@users.noreply.github.com>
Co-authored-by: Ksenia Bobrova <1885174+almaleksia@users.noreply.github.com>
2026-01-20 20:16:46 -05:00
.devcontainer feat(devcontainer): Adds Dev Container configuration (#154) 2025-07-30 12:54:41 -04:00
.github .github/workflows: add nightly conformance tests 2026-01-19 13:33:00 +01:00
auth auth: add OAuth 2.0 Protected Resource Metadata handler 2025-12-08 14:54:52 -05:00
design mcp: introduce Requests (#267) 2025-08-12 17:49:50 -04:00
docs perf: add schema caching to avoid repeated reflection (#685) 2026-01-20 20:16:46 -05:00
examples mcp: switch icon theme to named string type (#733) 2025-12-19 16:44:10 -05:00
internal perf: add schema caching to avoid repeated reflection (#685) 2026-01-20 20:16:46 -05:00
jsonrpc all: expose JSON-RPC error codes, and support URLElicitationRequired errors (#655) 2025-12-01 11:29:17 -05:00
mcp perf: add schema caching to avoid repeated reflection (#685) 2026-01-20 20:16:46 -05:00
oauthex oauthex: fix content type check in getJSON (#721) 2025-12-12 09:44:26 -05:00
scripts scripts: add conformance test runner (#694) 2025-12-08 13:03:17 -05:00
.gitignore .gitignore: add 2025-08-11 08:33:50 -04:00
CONTRIBUTING.md scripts: add conformance test runner (#694) 2025-12-08 13:03:17 -05:00
copyright_test.go all: add a test that checks for missing or mismatching file copyright 2025-09-11 13:53:22 -04:00
go.mod go.mod: upgrade to jsonschema v0.4.2 (#732) 2026-01-04 09:18:01 -05:00
go.sum go.mod: upgrade to jsonschema v0.4.2 (#732) 2026-01-04 09:18:01 -05:00
LICENSE Initial commit: add LICENSE 2025-06-23 17:17:37 +00:00
README.md docs: adds SDK version support matrix (#737) 2025-12-30 17:14:02 -05:00
SECURITY.md SECURITY.md: add a security policy (#682) 2025-12-05 16:00:32 -05:00

MCP Go SDK

Open in GitHub Codespaces

PkgGoDev

This repository contains an implementation of the official Go software development kit (SDK) for the Model Context Protocol (MCP).

Package / Feature documentation

The SDK consists of several importable packages:

The SDK endeavors to implement the full MCP spec. The docs/ directory contains feature documentation, mapping the MCP spec to the packages above.

Version Compatibility

The following table shows which versions of the Go SDK support which versions of the MCP specification:

SDK Version Latest MCP Spec All Supported MCP Specs
v1.2.0+ 2025-06-18 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05
v1.0.0 - v1.1.0 2025-06-18 2025-06-18, 2025-03-26, 2024-11-05

Getting started

To get started creating an MCP server, create an mcp.Server instance, add features to it, and then run it over an mcp.Transport. For example, this server adds a single simple tool, and then connects it to clients over stdin/stdout:

package main

import (
	"context"
	"log"

	"github.com/modelcontextprotocol/go-sdk/mcp"
)

type Input struct {
	Name string `json:"name" jsonschema:"the name of the person to greet"`
}

type Output struct {
	Greeting string `json:"greeting" jsonschema:"the greeting to tell to the user"`
}

func SayHi(ctx context.Context, req *mcp.CallToolRequest, input Input) (
	*mcp.CallToolResult,
	Output,
	error,
) {
	return nil, Output{Greeting: "Hi " + input.Name}, nil
}

func main() {
	// Create a server with a single tool.
	server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil)
	mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
	// Run the server over stdin/stdout, until the client disconnects.
	if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
		log.Fatal(err)
	}
}

To communicate with that server, create an mcp.Client and connect it to the corresponding server, by running the server command and communicating over its stdin/stdout:

package main

import (
	"context"
	"log"
	"os/exec"

	"github.com/modelcontextprotocol/go-sdk/mcp"
)

func main() {
	ctx := context.Background()

	// Create a new client, with no features.
	client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)

	// Connect to a server over stdin/stdout.
	transport := &mcp.CommandTransport{Command: exec.Command("myserver")}
	session, err := client.Connect(ctx, transport, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	// Call a tool on the server.
	params := &mcp.CallToolParams{
		Name:      "greet",
		Arguments: map[string]any{"name": "you"},
	}
	res, err := session.CallTool(ctx, params)
	if err != nil {
		log.Fatalf("CallTool failed: %v", err)
	}
	if res.IsError {
		log.Fatal("tool failed")
	}
	for _, c := range res.Content {
		log.Print(c.(*mcp.TextContent).Text)
	}
}

The examples/ directory contains more example clients and servers.

Contributing

We welcome contributions to the SDK! Please see CONTRIBUTING.md for details of how to contribute.

Acknowledgements / Alternatives

Several third party Go MCP SDKs inspired the development and design of this official SDK, and continue to be viable alternatives, notably mcp-go, originally authored by Ed Zynda. We are grateful to Ed as well as the other contributors to mcp-go, and to authors and contributors of other SDKs such as mcp-golang and go-mcp. Thanks to their work, there is a thriving ecosystem of Go MCP clients and servers.

License

This project is licensed under the MIT License - see the LICENSE file for details.