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