Build an MCP Server in Go (for Claude Code) A developer built an MCP (Model Context Protocol) server in Go to allow Claude Code to query an internal billing API without manual copy-pasting. The server uses the official Go SDK, which matured in 2025, and the developer emphasizes the importance of choosing between stdio and HTTP transports based on whether the tool is personal or shared. The key insight is that tool descriptions, not prompts, define the interface for the LLM. Claude Code can read your repo, run your CLI, query your database — as long as you give it the entry point. That entry point is an MCP server Model Context Protocol . The day I wanted Claude to query our internal billing API without me copy-pasting JSON responses by hand, I wrote a small MCP server. In Go, not Python — and in hindsight that was the right call, for one specific reason we'll get to. The official Go SDK github.com/modelcontextprotocol/go-sdk matured in 2025. It's perfectly usable in production now, but the docs still center on the "hello world." Here's what actually matters once the server leaves your machine: the transport choice, typed tools, auth, and the design traps I walked straight into. MCP is a client-server protocol. The client Claude Code, Claude Desktop, your own agent connects to one or more servers , each exposing three things: tools functions the model can call , resources data it can read , and prompts reusable templates . 90% of real-world use is tools. Claude Code MCP client talks to your Go MCP server over stdio or HTTP; the server calls your API or database Claude Code MCP client MCP server in Go Your API DB, services… stdio / HTTP Go calls The model decides to call a "tool" → the server runs it → returns the result The MCP server is the adapter between the LLM and your system. The key point: you don't write a prompt . You declare tools with a name, a description and an input schema. The model reads those descriptions and decides on its own when to call them. The quality of your descriptions is your interface. Three steps: create the server, register a tool with its typed handler, run it on a transport. The SDK infers the tool's JSON schema directly from your Go struct. package main import "context" "log" "github.com/modelcontextprotocol/go-sdk/mcp" // The tool input: the SDK derives the JSON schema exposed to the model from it. type InvoiceArgs struct { ClientID string json:"client id" jsonschema:"the client account ID" Year int json:"year" jsonschema:"fiscal year, e.g. 2026" } func getInvoices ctx context.Context, req mcp.CallToolRequest, args InvoiceArgs mcp.CallToolResult, any, error { rows, err := billing.Lookup ctx, args.ClientID, args.Year // your real code if err = nil { return nil, nil, err } return &mcp.CallToolResult{ Content: mcp.Content{&mcp.TextContent{Text: rows.Markdown }}, }, nil, nil } func main { server := mcp.NewServer &mcp.Implementation{ Name: "billing", Version: "v1.0.0", }, nil mcp.AddTool server, &mcp.Tool{ Name: "get invoices", Description: "List a client's invoices for a given fiscal year.", }, getInvoices // stdio transport: Claude Code launches this binary as a subprocess if err := server.Run context.Background , &mcp.StdioTransport{} ; err = nil { log.Fatal err } } On the Claude Code side, you declare this server in the MCP config command + arguments . At startup, Claude launches the binary, negotiates the protocol, and discovers get invoices . No prompt to write: the description is enough. The SDK supports two transports, and this is the architecture decision. They don't serve the same use case: stdio streamable HTTP Execution local subprocess launched by the client standalone network service Clients one, local many, remote Auth inherited from the machine your responsibility token, mTLS… Typical use personal tool, dev, CLI team server, SaaS, shared prod The simple rule: stdio for a tool only you use on your machine, HTTP as soon as it's shared . The classic trap is to prototype in stdio then want to expose it to the team without realizing you're moving from a "zero auth" model to a "network attack surface." The HTTP transport plugs in almost like a regular HTTP handler https://www.web-developpeur.com/en/blog/go-middleware-best-practices-production — which means all the middleware best practices recover, timeout, auth, logs apply: handler := mcp.NewStreamableHTTPHandler func r http.Request mcp.Server { return server }, nil, // wrap it with the same middleware as any API http.ListenAndServe ":8080", Chain handler, Recover, Auth, Logger This is where Go beats Python for this particular job. The JSON schema exposed to the model is derived from your input struct. No hand-written schema, no drift between validation and docs: // ❌ manual schema: drifts from the code, validates nothing tool := &mcp.Tool{ Name: "search", InputSchema: rawJSON {"type":"object","properties":{"q":{"type":"string"}}} , } // ✅ typed struct: schema inferred, validation for free, one source of truth type SearchArgs struct { Query string json:"query" jsonschema:"full-text search query" Limit int json:"limit" jsonschema:"max results, default 10" } When the model sends arguments, the SDK validates them against the schema before calling your handler. A year sent as a string? Rejected before your code runs. It's exactly the Go philosophy of making invalid state impossible https://www.web-developpeur.com/en/blog/interfaces-go-philosophie-accept-return , applied at the boundary with the LLM — the least reliable layer of your system. An MCP server runs code on a model's request , and that model is driven by a potentially adversarial prompt prompt injection . Three rules I set for myself after the fact: 1. Least privilege per tool. Don't expose run sql with write access "just in case." Expose get invoices client id, year — bounded, read-only. Each tool is a capability granted to the model — treat it like a public API route. 2. Over HTTP, auth is non-negotiable. A bearer token checked in a middleware before reaching the MCP server, like any API. An MCP server open on the network with no auth is self-service RCE. 3. The context is your cancellation thread. If the client disconnects, ctx is cancelled — propagate it down to your DB calls. An MCP handler that ignores ctx.Done is a goroutine leak https://www.web-developpeur.com/en/blog/goroutine-leaks-golang waiting to happen under load. Tools too granular. My first version exposed get client , get invoice , get line item separately. The model chained five calls where a single well-designed get client summary would do — and every call costs a round-trip and context tokens. Design your tools for the model's task , not for your database schema. Responses too big. Returning 2,000 lines of raw JSON saturates the context window and makes the model and the bill pay for nothing. Summarize, paginate, return readable Markdown instead of a dump. A tool result isn't a REST response: it's reasoning material for a human-model. Vague descriptions. "search" says nothing. "Full-text search across invoices, returns up to limit matches sorted by date" tells the model when and how to call it. Your descriptions are literally your tool's system prompt — treat them like copywriting. Writing an MCP server in Go isn't writing AI — it's writing a clean, typed, secure API whose only client happens to be a model. Everything you already know about good Go services applies: strict types at the boundary, propagated context, middleware, least privilege. MCP just adds a new category of client, the most unpredictable of them all. And that's exactly why Go wins here: facing a non-deterministic caller, you want the maximum number of guarantees at compile time. Python lets you write the server faster; Go lets you sleep at night when the model calls it in a way you never anticipated.