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 SDKnet/http (protocol implementation)
  • mark3labs/mcp-go → HTTP client/server helpers
  • mcp-goGin (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