使用说明
mcp-go
A Go framework for building Model Context Protocol (MCP) servers — like Gin, but for MCP.
mcp-go is an opinionated, production-ready framework for building MCP servers in Go with typed handlers, automatic schema generation, middleware, and multiple transports.
If the MCP SDK gives you protocol correctness, and mark3labs/mcp-go gives you SDK convenience, mcp-go gives you application structure and production defaults.
Why mcp-go?
Building an MCP server directly on top of SDKs means repeatedly solving the same problems:
- input decoding & validation
- schema generation
- error handling
- middleware (auth, timeouts, logging)
- transport wiring (stdio vs HTTP)
mcp-go solves these once — idiomatically, safely, and with great DX.
Quickstart (5 minutes)
package main
import (
"context"
"log"
"go.klarlabs.de/mcp"
)
func main() {
srv := mcp.NewServer(mcp.ServerInfo{
Name: "example",
Version: "0.1.0",
})
srv.Tool("hello").
Description("Say hello").
Handler(func(ctx context.Context, in struct{ Name string `json:"name"` }) (string, error) {
return "Hello " + in.Name, nil
})
log.Fatal(mcp.ServeStdio(context.Background(), srv))
}
That's it:
- Typed input
- Automatic JSON Schema
- MCP-compliant responses
- No manual routing or validation
Core Features
Typed MCP tools
Define tools using strongly-typed Go structs. Invalid input never reaches your business logic.
type SearchInput struct {
Query string `json:"query" jsonschema:"required"`
Limit int `json:"limit"`
}
srv.Tool("search").
Description("Search for items").
Handler(func(ctx context.Context, input SearchInput) ([]string, error) {
// Your search logic here
return []string{"result1", "result2"}, nil
})
Automatic JSON Schema
Schemas are derived from Go structs and:
- validated automatically
- exposed via MCP introspection
- kept in sync with code
No manual schema maintenance.
Gin-style middleware
Apply cross-cutting concerns consistently:
srv.Use(
middleware.Recover(),
middleware.Timeout(5*time.Second),
middleware.RequestID(),
middleware.Logging(logger),
)
Use cases:
- caller-supplied authz (mcp-go ships no auth — terminate it in your own middleware/transport)
- rate limiting
- tracing
- metrics
- panic recovery
Multiple transports
Run the same server over:
- stdio (CLI / agent use)
- Streamable HTTP (service deployments;
ServeHTTPdefault) - WebSocket (bidirectional communication)
- gRPC (high-performance service-to-service)
// Stdio for CLI tools
mcp.ServeStdio(ctx, srv)
// Streamable HTTP (stateless 2026-07-28 by default)
mcp.ServeHTTP(ctx, srv, ":8080")
// Session-negotiated Streamable HTTP (2025-03-26)
mcp.ServeHTTP(ctx, srv, ":8080", mcp.WithStreamableStateful())
// Retired HTTP+SSE split
mcp.ServeHTTP(ctx, srv, ":8080", mcp.WithLegacyHTTP())
// WebSocket for bidirectional communication
mcp.ServeWebSocket(ctx, srv, ":8081")
// gRPC for high-performance service-to-service
mcp.ServeGRPC(ctx, srv, ":9090")
MCP Apps Support
Build tools with interactive UIs using the MCP Apps extension. Tools declare a ui:// resource URI via UIResource(), and the linked HTML resource is rendered as a sandboxed iframe in supported hosts like Claude Desktop.
srv.Tool("visualize").
Description("Visualize data interactively").
UIResource("ui://my-app/visualizer").
Handler(func(input VisualizeInput) (any, error) {
return getData(input.ID), nil
})
// Serve the UI as a resource with the MCP Apps MIME type
srv.Resource("ui://my-app/visualizer").
Name("Visualizer").
MimeType("text/html;profile=mcp-app").
Handler(func(ctx context.Context, uri string, params map[string]string) (*mcp.ResourceContent, error) {
return &mcp.ResourceContent{
URI: uri,
MimeType: "text/html;profile=mcp-app",
Text: visualizerHTML,
}, nil
})
UIResource() sets _meta.ui.resourceUri on both tools/list and tools/call responses, telling MCP hosts to render the linked resource as an interactive app alongside tool results.
Building the HTML resource
The HTML resource must be a single self-contained file (inline CSS, JS, no external requests). The recommended stack:
@modelcontextprotocol/ext-apps— Client SDK for the MCP Apps postMessage protocol (handlesui/initializehandshake, receives tool results)- Vite +
vite-plugin-singlefile— Bundles everything into one HTML file - Go
embed.FS— Embeds the built HTML files into the Go binary
Common pitfalls
Vue/React string templates and the runtime compiler. If you use Vue's defineComponent with a template string (instead of .vue SFCs), Vite's default Vue build is runtime-only and cannot compile templates at runtime. The iframe will render empty with no error. Fix this by aliasing Vue to the full build in vite.config.ts:
// vite.config.ts
export default defineConfig({
resolve: {
alias: {
vue: "vue/dist/vue.esm-bundler.js",
},
},
});
No TypeScript in runtime-compiled templates. Vue's runtime template compiler only understands JavaScript. TypeScript syntax like as any or : string in template expressions will throw SyntaxError: Unexpected identifier. Move type assertions to setup() or use computed properties.
Resource MIME type. Use text/html;profile=mcp-app (not plain text/html) so hosts recognize the resource as an MCP App.
Working example
Roady ships 14 MCP Apps (D3.js charts, task boards, interactive dashboards) built with mcp-go. See app/ for the Vue + Vite + singlefile setup and internal/infrastructure/mcp/ for the Go resource registration.
Production-ready defaults
- strict JSON decoding
- safe error mapping
- graceful shutdown
- context propagation everywhere
You can opt out — but safety is the default.
How this fits into the MCP ecosystem
| Project | What it is |
|---|---|
| MCP Go SDK | Low-level protocol implementation |
| mark3labs/mcp-go | Community SDK / helpers |
| mcp-go | Full Go framework for MCP servers |
Think:
- MCP SDK →
net/http - mcp-go → Gin
We build on top of the MCP spec — not instead of it.
When should you use mcp-go?
Use mcp-go if you:
- are building real MCP services, not just experiments
- want typed APIs and validation
- need auth, limits, observability
- deploy MCP servers in production
If you want raw protocol access only, an SDK may be a better fit.
Used by
- Roady — Planning-first system of record with 14 interactive MCP Apps (D3.js visualizations, task boards, dashboards)
- Obvia — Incident automation & AIOps tooling
- Internal MCP services and experiments
Want to add your project? Open a PR!
Installation
go get go.klarlabs.de/mcp
Requires Go 1.25 or later.
Documentation
- API Reference
- Getting Started
- Migration from mark3labs/mcp-go
- Comparison: MCP SDK vs mark3labs vs mcp-go
- Examples
- MCP Specification
Examples
Tools
Tools are functions that can be called by the AI model:
type CalculateInput struct {
Operation string `json:"operation" jsonschema:"required"`
A float64 `json:"a" jsonschema:"required"`
B float64 `json:"b" jsonschema:"required"`
}
srv.Tool("calculate").
Description("Perform arithmetic operations").
Handler(func(input CalculateInput) (float64, error) {
switch input.Operation {
case "add":
return input.A + input.B, nil
case "multiply":
return input.A * input.B, nil
default:
return 0, fmt.Errorf("unknown operation: %s", input.Operation)
}
})
Return Type Flexibility
Tool handlers can return any JSON-serializable type. The framework automatically handles serialization:
// String returns are used as-is
srv.Tool("greet").
Handler(func(input GreetInput) (string, error) {
return "Hello, " + input.Name, nil
})
// Structs are automatically JSON-serialized
type StatusResult struct {
Status string `json:"status"`
Count int `json:"count"`
Healthy bool `json:"healthy"`
}
srv.Tool("status").
Handler(func(input StatusInput) (StatusResult, error) {
return StatusResult{
Status: "ok",
Count: 42,
Healthy: true,
}, nil
})
// Response text: {"status":"ok","count":42,"healthy":true}
// Maps and slices work too
srv.Tool("list").
Handler(func(input ListInput) (map[string]any, error) {
return map[string]any{
"items": []string{"a", "b", "c"},
"total": 3,
}, nil
})
This ensures compliance with the MCP specification which requires the text field to always be a string.
Structured Content
Tools can return typed structured data alongside text content blocks using OutputSchema and StructuredResult:
type TableOutput struct {
Headers []string `json:"headers"`
Rows [][]string `json:"rows"`
}
srv.Tool("extract_table").
Description("Extract table data from a page").
OutputSchema(TableOutput{}).
Handler(func(ctx context.Context, input ExtractInput) (mcp.StructuredResult, error) {
return mcp.StructuredResult{
Content: []mcp.Content{mcp.NewTextContent("Found 3 rows")},
StructuredContent: map[string]any{"headers": []string{"name", "age"}, "rows": [][]string{{"Alice", "30"}}},
}, nil
})
The response includes both content (text blocks for display) and structuredContent (typed data matching the output schema). Clients that understand structuredContent can render it natively (tables, trees, etc.).
Dynamic Tool Registration
Add and remove tools at runtime, then notify connected clients:
// Add a tool dynamically
srv.Tool("fill_form").Handler(fillFormHandler)
// Notify clients that the tool list changed
session := mcp.SessionFromContext(ctx)
session.NotifyToolListChanged()
// Remove a tool when it's no longer relevant
srv.RemoveTool("fill_form")
session.NotifyToolListChanged()
// Same pattern works for resources and prompts
srv.RemoveResource("config://temp")
session.NotifyResourceListChanged()
srv.RemovePrompt("onboarding")
session.NotifyPromptListChanged()
Resources
Resources expose data via URI templates:
srv.Resource("file://{path}").
Name("File").
Description("Read file content").
MimeType("text/plain").
Handler(func(ctx context.Context, uri string, params map[string]string) (*mcp.ResourceContent, error) {
content, err := os.ReadFile(params["path"])
if err != nil {
return nil, err
}
return &mcp.ResourceContent{
URI: uri,
MimeType: "text/plain",
Text: string(content),
}, nil
})
Prompts
Prompts are parameterized message templates:
srv.Prompt("code-review").
Description("Generate a code review prompt").
Argument("language", "Programming language", true).
Handler(func(ctx context.Context, args map[string]string) (*mcp.PromptResult, error) {
return &mcp.PromptResult{
Messages: []mcp.PromptMessage{
{
Role: "user",
Content: mcp.TextContent{Type: "text", Text: fmt.Sprintf("Review this %s code:", args["language"])},
},
},
}, nil
})
Tool Metadata
Attach arbitrary metadata to tools via _meta, or use the UIResource shorthand for MCP Apps:
// Arbitrary metadata
srv.Tool("my-tool").
Meta(map[string]any{"custom": "data"}).
Handler(myHandler)
// MCP Apps shorthand — sets _meta.ui.resourceUri
srv.Tool("visualize").
UIResource("ui://my-app/dashboard").
Handler(vizHandler)
The _meta field is included in both tools/list and tools/call JSON-RPC responses.
Middleware
Add cross-cutting concerns with middleware:
// Use default production middleware stack
middleware := mcp.DefaultMiddlewareWithTimeout(logger, 30*time.Second)
mcp.ServeStdio(ctx, srv, mcp.WithMiddleware(middleware...))
Built-in middleware:
Recover()- Catch panics and convert to errorsRequestID()- Inject unique request IDsTimeout(d)- Enforce request deadlinesLogging(logger)- Structured request loggingRateLimit()- Request throttlingSizeLimit()- Request size limitsOTel()- OpenTelemetry tracing and metricsAudit()- Request/response audit loggingTracing()- Correlation and trace ID propagation
Authentication is out of scope. mcp-go never handles tokens, OAuth flows, or credentials. On the client, inject auth via the supplied
http.Clienttransport (mcp.WithHTTPClient). On the server, terminate auth at the transport/proxy layer or in your own middleware.
Enterprise Features
Horizontal Scaling with SessionStore
Persist sessions across server restarts for load-balanced deployments:
// Redis-backed session store
store := redis.NewSessionStore(redisClient, 24*time.Hour)
mcp.ServeHTTP(ctx, srv, ":8080",
mcp.WithSessionStore(store),
)
// In-memory store for single-instance deployments
mcp.ServeHTTP(ctx, srv, ":8080",
mcp.WithSessionStore(mcp.NewInMemorySessionStore()),
)
Server Discovery
Clients can discover MCP servers via /.well-known/mcp. For SEP-2127 Server
Cards, also publish /mcp/server-card and an AI Catalog:
manifest := srv.Manifest()
discovery := mcp.NewServerDiscovery(&manifest,
mcp.WithDiscoveryEndpoints(mcp.ServerEndpoint{
StreamableHTTP: "https://example.com/mcp",
}),
mcp.WithDiscoveryOAuthMetadata(mcp.OAuthMetadata{
AuthorizationServers: []string{"https://auth.example.com"},
ResourceIndicator: "https://example.com/mcp",
}),
mcp.WithDiscoveryAuthExtensions(mcp.AuthExtensions{
DPoP: true,
IDJAG: "https://auth.example.com/oauth/id-jag",
}),
)
card := mcp.NewServerCardFromDiscovery(discovery,
mcp.WithServerCardName("com.example/my-server"),
)
mcp.ServeHTTP(ctx, srv, ":8080",
mcp.WithDiscovery(discovery),
mcp.WithServerCard(card),
)
See docs/agent-identity.md for the advertise-only EMA / DPoP stance.
Tasks for Long-Running Operations
Register async tasks that can be created, monitored, and canceled:
srv.RegisterTask("long-task", "A long running task", func(ctx context.Context, input map[string]any) (*mcp.TaskResult, error) {
// Task runs asynchronously
return &mcp.TaskResult{Data: "completed"}, nil
})
// Create a task
task, _ := srv.Tasks().CreateTask(ctx, mcp.CreateTaskRequest{
Name: "long-task",
Params: map[string]any{"key": "value"},
})
// List and cancel tasks
tasks, _ := srv.Tasks().ListTasks(10, "")
srv.Tasks().CancelTask(task.ID)
Tool Annotations
Mark tools with behavioral hints for clients:
srv.Tool("read-config").
Description("Read configuration").
ReadOnly().
Handler(readHandler)
srv.Tool("delete-user").
Description("Delete a user account").
Destructive().
Handler(deleteHandler)
srv.Tool("update-setting").
Description("Update a setting").
Idempotent().
Handler(updateHandler)
Completion Support
Provide autocomplete suggestions for prompt arguments and resource URIs:
srv.PromptCompletion("code-review").
Argument("language", func(ctx context.Context, value string) (*mcp.CompletionResult, error) {
return &mcp.CompletionResult{
Values: []string{"go", "python", "typescript"},
}, nil
})
Bidirectional Communication
Servers can make requests back to clients via sessions:
Sampling, roots, and elicitation reach the client only when it declared the
capability. How they travel depends on the revision: a stateless 2026-07-28
request resolves them through an input_required result the client retries
with its answers, on any transport. An initialize-era session sends a real
server→client request, which ServeStdio supports; the other transports
return ErrNoRequestSender for now.
// Request LLM completion from client (sampling)
session := mcp.SessionFromContext(ctx)
result, _ := session.CreateMessage(ctx, mcp.CreateMessageRequest{
Messages: []mcp.SamplingMessage{{Role: mcp.User, Content: mcp.Content{Type: "text", Text: "Summarize this"}}},
})
// Query client workspace roots
roots, _ := session.ListRoots(ctx)
// Send log messages to client
session.LogMessage(ctx, mcp.LoggingMessage{Level: mcp.Info, Data: "Processing complete"})
// Notify clients of resource changes
session.NotifyResourceUpdated(ctx, "config://app")
// Elicitation — ask the user for structured input mid-task
elicitor := mcp.ElicitFromContext(ctx)
if elicitor != nil {
result, _ := elicitor.Elicit(ctx, &mcp.ElicitRequest{
Message: "Multiple fields match 'Name'. Which one?",
RequestedSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"field": map[string]any{"type": "string", "enum": []string{"First Name", "Last Name"}},
},
},
})
if result.Action == "accept" {
selectedField := result.Content["field"]
// Use selectedField...
}
}
// Channels — push messages proactively into the AI session
channel := mcp.ChannelFromContext(ctx)
if channel != nil {
channel.SendText("navigation", "Page navigated to /dashboard")
channel.Send(&mcp.ChannelMessage{
Channel: "network",
Content: mcp.NewTextContent("API response received"),
Data: map[string]any{"status": 200, "url": "/api/users"},
Priority: "high",
})
}
HTTP Transport
Serve over HTTP with Server-Sent Events:
mcp.ServeHTTP(ctx, srv, ":8080",
mcp.WithReadTimeout(30*time.Second),
mcp.WithWriteTimeout(30*time.Second),
)
WebSocket Transport
Serve over WebSocket for full-duplex communication:
mcp.ServeWebSocket(ctx, srv, ":8081",
mcp.WithWebSocketReadTimeout(30*time.Second),
mcp.WithWebSocketWriteTimeout(30*time.Second),
)
gRPC Transport
Serve over gRPC with Protocol Buffers for high-performance service-to-service communication:
mcp.ServeGRPC(ctx, srv, ":9090",
mcp.WithGRPCShutdownTimeout(10*time.Second),
)
Client SDK
Consume MCP servers programmatically:
transport, _ := client.NewStdioTransport("./my-server")
c := client.New(transport, client.WithTimeout(10*time.Second))
info, _ := c.Initialize(ctx)
tools, _ := c.ListTools(ctx)
result, _ := c.CallTool(ctx, "search", SearchInput{Query: "hello"})
Against a 2026-07-28 server, a call that needs sampling, elicitation, or roots
returns a *client.InputRequiredError (errors.Is(err, client.ErrInputRequired)).
Declare what the client can answer with client.WithCapabilities, answer each
input request by its id, and retry:
c := client.New(transport, client.WithCapabilities(map[string]any{"elicitation": map[string]any{}}))
_, err := c.CallTool(ctx, "greet", args)
var ir *client.InputRequiredError
if errors.As(err, &ir) {
answers := map[string]any{}
for id := range ir.InputRequests { // each is {Method, Params}
answers[id] = map[string]any{"action": "accept", "content": map[string]any{"name": "Ada"}}
}
result, err = c.CallTool(ir.Retry(ctx, answers), "greet", args)
}
Testing
Test MCP servers without transport overhead:
func TestMyServer(t *testing.T) {
srv := mcp.NewServer(mcp.ServerInfo{Name: "test", Version: "1.0.0"})
srv.Tool("greet").Handler(greetHandler)
tc := testutil.NewTestClient(t, srv)
defer tc.Close()
result, err := tc.CallTool("greet", map[string]any{"name": "World"})
if err != nil {
t.Fatal(err)
}
// result == "Hello, World!"
}
JSON Schema Tags
Use struct tags to define JSON Schema for tool inputs:
type SearchInput struct {
Query string `json:"query" jsonschema:"required,description=Search query"`
Limit int `json:"limit" jsonschema:"description=Max results,default=10"`
Tags []string `json:"tags" jsonschema:"description=Filter by tags"`
MinScore float64 `json:"minScore" jsonschema:"minimum=0,maximum=1"`
}
Supported tags:
required- Field is requireddescription=...- Field descriptiondefault=...- Default valueminimum=N/maximum=N- Numeric boundsminLength=N/maxLength=N- String length boundsenum=a|b|c- Allowed values
Philosophy
- Typed > dynamic
- Safe defaults > flexibility
- Frameworks create ecosystems
mcp-go aims to be the default Go framework for MCP — boring, predictable, and a joy to use.
Contributing
Contributions are welcome! Please read our Contributing Guide.
License
MIT License - see LICENSE for details.