Go
A library for establishing MCP server.
Ask AI about Go
Powered by Claude Β· Grounded in docs
I know everything about Go. Ask me about installation, configuration, usage, or troubleshooting.
0/500
Reviews
Documentation
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"
"github.com/felixgeelhaar/mcp-go"
)
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:
- auth / principals
- rate limiting
- tracing
- metrics
- panic recovery
Multiple transports
Run the same server over:
- stdio (CLI / agent use)
- HTTP + SSE (service deployments)
- WebSocket (bidirectional communication)
// Stdio for CLI tools
mcp.ServeStdio(ctx, srv)
// HTTP for web services
mcp.ServeHTTP(ctx, srv, ":8080")
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 github.com/felixgeelhaar/mcp-go
Requires Go 1.23 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.
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 loggingAuth()- API key and Bearer token authenticationRateLimit()- Request throttlingSizeLimit()- Request size limits
HTTP Transport
Serve over HTTP with Server-Sent Events:
mcp.ServeHTTP(ctx, srv, ":8080",
mcp.WithReadTimeout(30*time.Second),
mcp.WithWriteTimeout(30*time.Second),
)
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.
