Comparison
This page compares the three main ways to build MCP servers in Go. Each project serves a different purpose and operates at a different abstraction level.
Mental model
mcp-go Framework
mark3labs/mcp-go Community SDK
MCP Go SDK Protocol
These are complementary, not mutually exclusive. Think of it like HTTP in Go:
- MCP Go SDK →
net/http(protocol implementation) - mark3labs/mcp-go → HTTP client/server helpers
- mcp-go → Gin (application framework)
Feature comparison
| Capability | MCP Go SDK | mark3labs/mcp-go | mcp-go |
|---|---|---|---|
| Abstraction level | Protocol | SDK | Framework |
| Typed handlers | — | — | ✓ |
| Auto JSON Schema | — | — | ✓ |
| Input validation | — | Manual | Automatic |
| Middleware | — | — | ✓ |
| URI templates | — | — | ✓ |
| Production defaults | — | — | ✓ |
| Best for | SDK authors | Prototypes | Production |
Tool registration
MCP Go SDK
Manual dispatch and decoding:
// Protocol SDK style (manual dispatch)
for {
raw := readMessage()
msg := mcp.Decode(raw)
switch msg.Type {
case "tools/call":
name := msg.Params["name"].(string)
args := msg.Params["arguments"].(map[string]any)
// manually decode, call handler, encode response
}
} mark3labs/mcp-go
Server object with untyped handlers:
srv := server.NewMCPServer("my-server", "1.0.0")
srv.AddTool(mcp.Tool{
Name: "search",
Description: "Search for items",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]any{
"query": map[string]any{"type": "string"},
},
},
}, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
query, _ := req.Params.Arguments["query"].(string)
return mcp.NewToolResultText(results), nil
}) mcp-go
Typed handlers with automatic schema:
srv := mcp.NewServer(mcp.ServerInfo{Name: "my-server", Version: "1.0.0"})
type SearchInput struct {
Query string `json:"query" jsonschema:"required"`
}
srv.Tool("search").
Description("Search for items").
Handler(func(ctx context.Context, in SearchInput) ([]string, error) {
return search(in.Query), nil
}) Input validation
MCP Go SDK / mark3labs
Manual type assertions and validation:
func handler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
query, ok := req.Params.Arguments["query"].(string)
if !ok || query == "" {
return nil, errors.New("query is required")
}
limit, _ := req.Params.Arguments["limit"].(float64)
if limit < 1 || limit > 100 {
return nil, errors.New("limit must be 1-100")
}
// ...
} mcp-go
Declarative validation via struct tags:
type SearchInput struct {
Query string `json:"query" jsonschema:"required,minLength=1"`
Limit int `json:"limit" jsonschema:"minimum=1,maximum=100"`
}
srv.Tool("search").Handler(func(ctx context.Context, in SearchInput) ([]string, error) {
// in.Query and in.Limit are already validated
return search(in.Query, in.Limit), nil
}) Middleware
MCP Go SDK / mark3labs
No built-in middleware. You wrap handlers manually:
handler := func(ctx context.Context, req Request) (Response, error) { ... }
handler = withTimeout(5*time.Second, handler)
handler = withAuth(authz, handler)
handler = withLogging(logger, handler) mcp-go
First-class middleware chain:
srv.Use(
middleware.Recover(),
middleware.RequestID(),
middleware.Timeout(5*time.Second),
middleware.Logging(logger),
middleware.RateLimit(rps),
) Resources with URI templates
MCP Go SDK / mark3labs
Manual URI parsing:
srv.AddResource(mcp.Resource{
URI: "incidents://",
Name: "Incidents",
}, func(ctx context.Context, req mcp.ReadResourceRequest) (string, error) {
uri := req.Params.URI
id := strings.TrimPrefix(uri, "incidents://")
// ...
}) mcp-go
Built-in URI templates with parameter extraction:
srv.Resource("incidents://{id}").
Name("Incident").
MimeType("application/json").
Handler(func(ctx context.Context, uri string, params map[string]string) (*mcp.ResourceContent, error) {
id := params["id"] // extracted automatically
// ...
}) Prompts
mark3labs/mcp-go
Untyped arguments:
srv.AddPrompt(mcp.Prompt{
Name: "summarize",
Description: "Summarize content",
}, func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
topic, _ := req.Params.Arguments["topic"].(string)
// ...
}) mcp-go
Declared arguments with validation:
srv.Prompt("summarize").
Description("Summarize content").
Argument("topic", "Topic to summarize", true).
Handler(func(ctx context.Context, args map[string]string) (*mcp.PromptResult, error) {
topic := args["topic"] // guaranteed present
// ...
}) When to choose what
MCP Go SDK
Choose if you:
- Need maximum control over the protocol
- Are building another SDK or client
- Want to stay closest to the spec
mark3labs/mcp-go
Choose if you:
- Want a popular community SDK
- Are experimenting or prototyping
- Prefer "bring your own architecture"
mcp-go
Choose if you:
- Want typed handlers and validation
- Need middleware and production defaults
- Deploy MCP servers as real services