DEV Community

Binary Ink
Binary Ink

Posted on

I wrote the first book on building production MCP servers with Go

Most MCP tutorials use Python. That's fine for prototypes. But when you need a server that handles thousands of concurrent connections on 128 MB of RAM, starts in 50ms, and deploys as a single binary — you need Go.

I spent the last few months building MCP servers in Go for production systems. Eight different servers, 4,000+ lines of production code, handling real workloads across project management, browser automation, knowledge bases, and multi-agent orchestration.

Then I realized: there is no book on this. Not one. The MCP docs cover the protocol. There are Python quickstarts. TypeScript examples. But nothing that shows you how to build a production Go MCP server with authentication, database integration, deployment, and billing.

So I wrote one.

Why Go for MCP Servers?

Python TypeScript Go
Memory ~50-100 MB ~30-60 MB ~5-15 MB
Startup 1-3s 0.5-1s <50ms
Concurrency asyncio event loop goroutines
Deployment venv + pip node_modules single binary
Cross-compile painful painful GOOS=linux go build

The numbers matter when you run multiple MCP servers. A Go MCP server uses 10x less memory than Python, starts 50x faster, and deploys as a single file with zero dependencies.

What I learned from production

Here are patterns that aren't in any tutorial:

1. Serve SSE and Streamable HTTP on the same port

Different AI clients use different transports. Claude uses SSE. Codex uses Streamable HTTP. Don't make users configure which one — serve both:

mux := http.NewServeMux()
mux.Handle("/sse", sseHandler)       // Claude, Gemini
mux.Handle("/mcp", streamHandler)    // Codex, newer clients
mux.HandleFunc("/health", healthCheck)
http.ListenAndServe(":8080", mux)
Enter fullscreen mode Exit fullscreen mode

One port. Every client works.

2. Business errors vs. system errors

This is the #1 mistake in MCP server code. Tool handlers return two things: a result and an error. They mean different things:

// Business error — the AI sees this and can retry
return mcp.NewToolResultError("user not found"), nil

// System error — crashes the request (database down, etc.)
return nil, fmt.Errorf("connection lost")
Enter fullscreen mode Exit fullscreen mode

Return NewToolResultError for "that didn't work, try something else." Return Go error for "something is fundamentally broken." The AI handles the first kind gracefully. The second kind may close the connection.

3. Bearer token auth with browser fallback

Browser EventSource API cannot set custom headers. Period. So when a browser-based MCP client connects via SSE, the token goes in the URL:

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        auth := r.Header.Get("Authorization")
        if auth == "" {
            // Browser fallback — EventSource can't set headers
            auth = "Bearer " + r.URL.Query().Get("token")
        }
        if auth != "Bearer "+expectedToken {
            http.Error(w, "Unauthorized", 401)
            return
        }
        next.ServeHTTP(w, r)
    })
}
Enter fullscreen mode Exit fullscreen mode

Yes, the token appears in server logs. Use HTTPS, rotate tokens, and strip query params from access logs.

4. Session cleanup via MCP hooks

MCP clients hold long-lived connections. When they disconnect (laptop closes, network drops), you need to clean up. The mcp-go library fires lifecycle hooks:

hooks.AddOnUnregisterSession(func(ctx context.Context, session server.ClientSession) {
    sessionID := session.SessionID()
    if agentID, ok := sessionAgents.LoadAndDelete(sessionID); ok {
        disconnectAgent(agentID.(string))
    }
})
Enter fullscreen mode Exit fullscreen mode

If you skip this, you leak memory. Every disconnected session stays in your maps forever.

5. Symlinks break your path validation

Most file-handling tools do this:

// LOOKS safe but ISN'T
abs := filepath.Abs(filepath.Join(root, userPath))
if !strings.HasPrefix(abs, root) { return error }
Enter fullscreen mode Exit fullscreen mode

An attacker creates a symlink workspace/data → /etc and requests data/shadow. The prefix check passes. The symlink resolves to /etc/shadow.

Fix: call filepath.EvalSymlinks before the prefix check.

What the book covers

12 chapters, 110+ pages, every example from production:

  1. MCP Protocol — architecture, transports, JSON-RPC flow
  2. Quick Start — running server in 5 minutes
  3. Server Scaffold — dual transport, health checks, graceful shutdown
  4. Tool Development — schemas, validation, rate limiting, long-running ops
  5. Resources & Prompts — fixed/template resources, context-bundling prompts
  6. Authentication & Security — bearer tokens, symlink defense, risk classification, API keys
  7. Database Integration — pgxpool, embedded migrations, pgvector semantic search
  8. Testing — unit, integration, testcontainers, CI/CD
  9. Deployment — multi-stage Docker, Compose, Caddy HTTPS, Prometheus
  10. Production Patterns — sessions, events, multi-tenant, circuit breakers, "mistakes I made"
  11. Monetization — Stripe billing, pricing models, distribution, case study ($10K MRR in 6 weeks)
  12. Appendix — client compatibility matrix, quick reference, LLM uncertainty handling

The MCP economy is wide open

17,000+ MCP servers exist. Less than 5% are monetized. The SDK gets 97 million monthly downloads. This is the mobile app store in 2009 — massive developer activity, almost no established business models.

The book's final chapter covers how to monetize: freemium, usage-based, hybrid pricing, Stripe metering integration, and distribution across MCP marketplaces.

Get the book

Production MCP Servers with Go — $39 on Gumroad.

PDF + EPUB. 110 pages. 12 chapters. All code from production systems.


Questions? Drop them in the comments. I'll answer everything about building MCP servers with Go.

Top comments (0)