A minimal, zero-dependency Go implementation of the Model Context Protocol (MCP).
Single binary. Stdin/stdout transport. JSON-RPC 2.0. Nothing else.
MCP servers don't need HTTP frameworks, routers, or dependency trees. This project is a production-ready MCP server in pure Go covering tools, resources, prompts, logging, and progress, with auto-derived schemas and a three-state initialization handshake -- all backed by the standard library alone.
Use it directly, or scaffold your own with make init.
- MCP 2025-11-25 -- tools, resources (list/read), prompts, logging, progress
- JSON-RPC 2.0 over stdin/stdout -- newline-delimited, no LSP framing
- Auto-derived schemas -- struct tags drive tool and prompt input schemas via reflection
- Bidirectional transport -- generic server-to-client request primitive (no built-in handlers for sampling/elicitation/roots)
- Safe by default -- per-message cap (4 MB), handler timeout (30s), panic recovery
- Structured diagnostics --
slog.JSONHandlerto stderr; stdout stays protocol-only - Zero external dependencies -- standard library only
- Supply-chain ready -- cosign-signed releases, SBOMs, SLSA L3 provenance, OSS-Fuzz
- Go 1.26+
- Tested in CI on macOS, Linux, Windows. Release binaries are published for macOS and Linux (amd64, arm64).
go install github.com/andygeiss/mcp/cmd/mcp@latestOr download a signed release archive from Releases and verify it (see Verify a release).
Point any MCP client (Claude Desktop, VS Code, etc.) at the installed binary:
{
"mcpServers": {
"mcp": {
"command": "/absolute/path/to/mcp"
}
}
}Set MCP_TRACE=1 in the client's environment to log every request and response to stderr. Trace output includes full tool arguments — do not enable in production if handlers may receive credentials or PII.
Fork or clone this repo, then rewrite the module path:
make init MODULE=github.com/yourorg/yourprojectOpen internal/tools/echo.go — your first tool, wired up and ready to rename.
This rewrites all imports, repoints badge URLs (shields.io, codecov, Actions) at your repo, runs go mod tidy, and removes cmd/scaffold/. The binary directory stays at cmd/mcp/, so every scaffolded project produces a binary named mcp -- install it with go install github.com/yourorg/yourproject/cmd/mcp@latest. If two MCP servers share $GOBIN, disambiguate with go build -o <name> or rename cmd/mcp/ after init.
The rewriter refuses to run if the working tree is dirty — resetGitHistory is destructive and would wipe uncommitted edits. Commit/stash first, or pass --force to override: go run ./cmd/scaffold --force github.com/yourorg/yourproject.
After make init succeeds, the welcome banner names three steps. Here's what each one looks like:
Open the starter tool. The first line is your anchor:
// START HERE — your first tool. Edit, copy, rename. It's yours.
// Package tools holds the registered MCP tools and the registry primitives
// that wire them into the server.
package tools
import "context"
type EchoInput struct {
Message string `json:"message" description:"The message to echo back"`
}
func Echo(_ context.Context, input EchoInput) Result {
return TextResult(input.Message)
}Replace the Echo body, rename the input struct, and edit the description tag — that's the text the agent reads when deciding whether to call your tool.
Register it with one line:
if err := tools.Register(registry, "echo", "Echoes the input message", tools.Echo); err != nil {
return fmt.Errorf("register echo: %w", err)
}Need a clean copy-target for tool number two? Open internal/tools/_TOOL_TEMPLATE.go — same shape, no working logic, ready to rename.
make smokeOn success you see exactly:
Your server works. It exposes N tool(s).
On failure the target prints two diagnostic hints (forgot to register? doesn't compile?) followed by the captured stderr — usually enough to get unstuck without leaving the terminal.
Define an input struct, write a handler, register it.
// internal/tools/greet.go
package tools
import "context"
type GreetInput struct {
Name string `json:"name" description:"Name to greet"`
}
func Greet(_ context.Context, input GreetInput) Result {
return TextResult("Hello, " + input.Name + "!")
}// cmd/mcp/main.go
if err := tools.Register(registry, "greet", "Greets someone by name", tools.Greet); err != nil {
return fmt.Errorf("register greet: %w", err)
}The input schema ({"type":"object","properties":{"name":{"type":"string","description":"Name to greet"}},"required":["name"]}) is derived from struct tags. No manual schema definition needed.
Release archives are keyless-signed with cosign and ship with SLSA L3 provenance generated by slsa-framework/slsa-github-generator. Each archive has a .sigstore.json bundle; SHA-256 digests are in checksums.txt; SBOMs are attached as *.sbom.json.
# Replace <version>, <os>, <arch> with your target, e.g. 0.1.0, Linux, x86_64
cosign verify-blob \
--bundle mcp_<version>_<os>_<arch>.tar.gz.sigstore.json \
--certificate-identity-regexp "^https://github.com/andygeiss/mcp/" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
mcp_<version>_<os>_<arch>.tar.gzFor a stronger guarantee — rebuild the binary from source on your own machine and confirm the SHA matches the published checksum: see docs/reproducible-build.md.
MCP version 2025-11-25. JSON-RPC 2.0 with these specifics:
| Behavior | Implementation |
|---|---|
| Framing | Newline-delimited JSON objects |
| Batch requests | Rejected with -32700 |
Missing params |
Normalized to {} |
Request id |
Preserved as json.RawMessage, echoed exactly |
| Notifications | Never responded to |
| Unknown notifications | Silently ignored |
| Error messages | Contextual (e.g. "unknown tool: foo") |
Transport rationale and alternatives considered: docs/adr/ADR-001.
Implements tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get, logging/setLevel, plus notifications/initialized, notifications/cancelled, notifications/progress, notifications/message, and a generic server-to-client request primitive.
Not implemented -- calls are rejected with -32601: resources/subscribe, resources/unsubscribe, completion/complete, roots/list, sampling/*, elicitation/*, and the */list_changed notifications.
cmd/mcp/ main.go -- wiring only: flags, I/O injection, os.Exit
cmd/scaffold/ template rewriter -- not part of normal builds
internal/
assert/ test assertion helpers
prompts/ prompt registry, argument derivation
protocol/ JSON-RPC 2.0 codec, types, constants
resources/ resource registry, static resources, URI templates
schema/ shared JSON Schema derivation via reflection
server/ lifecycle, dispatch, notifications, bidirectional transport
tools/ tool registry, schema derivation, tool handlers
Dependency direction: cmd/mcp/ -> server/ -> protocol/; server/ imports tools/, resources/, prompts/. protocol/ and schema/ have zero internal dependencies.
Transport rules:
- stdout is protocol-only -- every byte is a valid JSON-RPC message.
- stderr is diagnostics-only via
slog.JSONHandler. - Constructors accept
io.Reader/io.Writerso tests inject buffers.
make check # build + test + lint
make test # go test -race ./...
make fuzz # fuzz the JSON decoder (FUZZTIME=5m to extend)
make coverage # enforce 90% threshold
make bench # benchstat against testdata/benchmarks/baseline.txtAuthoring guidelines, test conventions, and the full workflow matrix live in docs/development-guide.md.
MIT -- Andreas Geiß