MCP in Production · Chapter 1

Why MCP Exists

A Tuesday in March

It was a Tuesday in March 2025. I was debugging a Tailscale ACL on a customer cluster, and the iteration loop looked like this: switch to a terminal, run tailscale status, copy the output, switch back to Claude, paste it into the chat, ask "why can't node A reach node B," wait, get an answer that referenced a node that wasn't in the paste, swear under my breath, paste the ACL JSON, paste the route table, paste the service tags, ask again. Twenty minutes in I had a chat window the length of a CVS receipt and an answer that was confidently wrong because I'd accidentally trimmed a trailing brace when I copied the ACL.

I had a folder on my laptop called agents-glue/. It contained eight Python scripts, each one a small wrapper that exec'd a CLI, scraped the output, shaped it into JSON, and dumped it into a prompt template. One for tailscale, one for aws, one for kubectl, one for gh, one each for npm, pnpm, docker, and a half-broken one for Vercel. Every single one of them was a bespoke duct-tape job. Every single one of them broke when the underlying CLI changed its output format. None of them composed.

A week later I deleted that folder. By then I had a working @yawlabs/tailscale-mcp running locally over stdio, plumbed into Claude Desktop, and the conversation was: "why can't node A reach node B," and Claude turned around and called tailscale_status, then tailscale_routes, then tailscale_acl_get, three tools I'd defined in a server file, and walked back the answer with the actual current state of my tailnet. I never copy-pasted CLI output again. The folder, deleted. The half-broken Vercel scraper, deleted. The Python adapters, deleted. Replaced with a thing called the Model Context Protocol -- a wire format I didn't have to invent, that any client I cared about already spoke.

That's the moment I got it. Not the moment I read the spec. Not the moment I watched the launch video. The moment I deleted the glue folder.

This book is about what happens after you delete the glue folder. The protocol that lets you delete it. The servers that fill the space. The production headaches that show up around month three. The hosting story. The auth story. The schema story. The "my tool worked yesterday and today it doesn't" story.

But before any of that, I owe you the answer to a more basic question: why does this protocol exist at all, and why did it work when so many things before it didn't?

The Integration N Times Problem

Here is what the world looked like in 2024, before MCP, if you wanted to give an AI assistant access to a tool.

You wrote an integration for ChatGPT. You wrote a different integration for Claude. If you cared about Cursor you wrote a third one for Cursor's plugin format. If Continue mattered to your users, that was a fourth. Each one was the same conceptual operation -- "let the model call my function with these arguments and return a result" -- but each one was a different SDK, a different manifest format, a different auth flow, a different way of declaring schemas, a different way of returning errors, a different developer console, and a different review process.

The math was N tools times M clients. Every tool author paid M-fold maintenance to be visible to all the clients. Every client author paid N-fold integration cost to support all the tools their users wanted. In practice, neither side paid in full. Tool authors picked one or two clients to target. Clients picked one or two tools to ship in their store. The matrix was sparse on purpose because the work to fill it was unbearable.

This is the same problem that LSP solved for editors and language servers in 2016. Before LSP, every editor had to write a Go integration, a Rust integration, a TypeScript integration. Every language toolchain had to write a VS Code plugin, a Sublime plugin, an Emacs mode, a Vim plugin. The matrix was sparse for the same reason. LSP collapsed it -- one wire format, every editor speaks it, every language server speaks it, the matrix fills in for free. MCP is the same shape of fix for the AI tooling matrix.

But MCP didn't show up first. It showed up after a couple of expensive lessons in what doesn't work.

ChatGPT Plugins, A Cautionary Tale

In March 2023 OpenAI shipped ChatGPT Plugins. The pitch was beautiful: write an OpenAPI spec, host a manifest at /.well-known/ai-plugin.json, ChatGPT can now call your service. Browse the plugin store, click install, you're cooking.

About thirteen months later it was deprecated. By April 2024, OpenAI had pivoted to GPTs and Actions, which were a different shape and required a different integration, and Plugins were on a sunset path. Developers who'd built plugins ate the cost.

There were several reasons Plugins failed, and they all matter for understanding why MCP looks the way it looks.

It was a walled garden. Plugins ran inside ChatGPT and only inside ChatGPT. If you'd built a plugin and Anthropic shipped a competing client tomorrow, your plugin didn't go with you. Your investment was OpenAI-shaped and OpenAI-bound. The store was OpenAI's store. The review queue was OpenAI's review queue. The auth flow, the rate-limiting, the visibility -- all OpenAI's call. When OpenAI's strategy shifted, the platform shifted under your feet.

The transport was opinionated and remote-only. Plugins were HTTP services with OpenAPI specs. There was no story for "I want this thing to talk to my local files," "I want this thing to spawn a subprocess," "I want this thing to exec a CLI on my workstation." If your tool was inherently local -- a code formatter, a git hook, a thing that needed to read your ~/.aws/credentials -- Plugins didn't have a shape for you. You had to host a public HTTPS endpoint that somehow proxied to your local machine, which was absurd.

There was no negotiation. A plugin advertised its OpenAPI spec at install time. The model used what was there. There was no notion of "the client supports feature X, the server supports feature Y, here's what we'll actually use this session." Plugins were take-it-or-leave-it.

Discovery was static. The list of operations was the list at install time. You couldn't add an operation mid-session. You couldn't have a plugin that exposed different tools depending on what folder you were in or what credentials you had. Static manifests, dynamic world, mismatch.

It was sold as a product, not a protocol. The Plugin format was an OpenAI thing. There was no working group, no neutral spec, no "here's the wire format, anyone can implement either side." If you wanted to make a competing client that ran ChatGPT Plugins, you couldn't, because the runtime was inside ChatGPT itself.

The lesson, watched in real time by everyone building in this space, was: a tool integration story owned by one vendor, hosted by one vendor, and pitched as one vendor's product, will collapse the moment that vendor's strategy shifts. Plugins were not killed by being bad. They were killed by being a product feature in a market that needed an interoperability layer.

What Function Calling Got Right (and What It Didn't)

In June 2023 OpenAI shipped function calling, which was a much better idea. The model would emit a structured object -- a function name and a JSON-shaped argument blob -- and your application code would handle the actual call. The model was not making HTTP requests. The model was producing a typed intent. Your code was the runtime.

Anthropic shipped tool use shortly after. Google shipped a function calling API for Gemini. Every serious LLM vendor now has some version of this.

What function calling got right:

What function calling didn't solve:

Function calling was a good primitive that didn't compose. MCP is the composition layer.

November 25, 2024

Anthropic released the Model Context Protocol on November 25, 2024. The launch wasn't loud. It was a spec, a reference implementation, a handful of example servers, and a Claude Desktop integration that supported it on day one.

The thing that mattered, and the thing that not enough people noticed at the time, is that the people who designed MCP had been watching the Plugins burn and the function-calling matrix bloat for the previous eighteen months. The design choices were not theoretical. They were scar tissue.

You can see this in the protocol's surface. JSON-RPC 2.0 -- a sixteen-year-old, profoundly boring, profoundly well-understood request-response framing (the spec was finalized in 2010). Capability negotiation -- because Plugins didn't have one and that hurt. Transport-agnostic -- because Plugins were HTTPS-only and that hurt. Dynamic discovery -- because static manifests didn't survive contact with reality. JSON Schema for inputs -- because function calling proved this works. Stdio as a first-class transport -- because so many useful tools are local processes and forcing them through HTTP was the original sin.

And the framing: this is a protocol, not a product. The spec is at modelcontextprotocol.io. The reference SDKs are open source. Anthropic ships a client (Claude Desktop, Claude Code) that speaks it, but so does Cursor, so does Continue, so does Cline, so does Zed. ChatGPT joined via the OpenAI Agents SDK. Anthropic does not own the runtime. Anthropic does not run a store. Anthropic publishes the spec, ships SDKs, and otherwise gets out of the way.

This is the bet: vendor neutrality over vendor control. Anthropic gave up the ability to be the only client that runs MCP servers. In exchange, the matrix collapsed, and a tool author writing an MCP server in November 2024 has, by April 2026, six or seven mainstream clients that run their server with no porting work.

Why MCP Caught Fire When Plugins Didn't

I have a personal theory, watching from the inside as someone running Yaw MCP and shipping fourteen MCP servers under @yawlabs, about why MCP caught fire.

Open from day one. The spec was published. The SDKs were Apache 2.0. Anyone could implement either side. There was no "apply for plugin developer access," no review queue, no store gatekeeper.

Multiple clients early. Within weeks of launch, Claude Desktop wasn't the only thing speaking it. Cursor announced support quickly. The community wrote shims for VS Code. Continue's MCP support landed before Christmas. By March 2025 you could write a server and trust it ran in three or four clients (Claude Desktop, Cursor, Continue, and a then-rough version of Cline). The longer tail -- Zed, the consumer chat apps, ChatGPT via the OpenAI Agents SDK -- arrived through the rest of 2025 and into 2026, but the matrix was already non-trivial within the first quarter.

Transport-agnostic. stdio for local, HTTP+SSE for remote (legacy), and now Streamable HTTP as the modern remote transport. You picked the transport that fit your tool, not the one the protocol forced on you.

Framed as protocol, not product. Anthropic resisted -- and continues to resist -- the temptation to build the MCP store, the MCP marketplace, the MCP registry-with-a-billing-relationship. This is a hard temptation to resist because there is real money in being the chokepoint. They have, so far, not taken it.

Boring on purpose. JSON-RPC 2.0 is not exciting. That is the point. You can implement a minimal MCP client in an afternoon if you have a JSON-RPC library lying around. There is no novel framing, no clever encoding, no dependency on a custom transport. The protocol is dull, which means it gets out of the way.

There is one more reason, which is the timing. In November 2024 the agentic-coding wave had just hit. Cursor was exploding. Claude 3.5 Sonnet (the October 2024 v2 release, often called "Sonnet 3.5 new" to distinguish it from the original June 2024 model) had landed weeks earlier and was visibly better at tool use than anything before it. Developers were trying to plumb their tools into AI assistants and finding the experience miserable. MCP showed up exactly when the pain was acute.

From the field: I shipped @yawlabs/tailscale-mcp in March 2025, four months after the spec dropped. By the time I was writing it, the SDK was stable enough that the server file was ~300 lines including JSDoc, the transport just worked, and Claude Desktop, Claude Code, and Cursor all picked it up with zero per-client work. Nobody who was around for the Plugins era had that experience.

The Protocol's Design Choices

Let me name the choices the spec makes, with my read on why each one matters.

JSON-RPC 2.0 as the wire format

Boring. Sixteen years old. Every language has a library. Bidirectional notifications work without invention. Request IDs, error codes, batching -- all already standard. The bytes on the wire are unsurprising.

What this gets you in production: you can debug with tail -f, you can mock with a few lines of Node, you can write a test harness that doesn't depend on a vendor SDK. When something breaks at the wire level (and in Chapter 3 we will look at exactly such a breakage), you reach for the same tools you'd reach for to debug any JSON-RPC service.

Capability negotiation

Server says "here's what I can do," client says "here's what I support," and they intersect during the initialize handshake. Tools, resources, prompts, sampling, logging, roots -- each is a capability. If the client doesn't support one, the server knows not to send it.

This is the thing Plugins didn't have. It's the thing that lets the protocol grow without breaking older clients. A new feature can land in the spec, in clients that want it, without breaking servers that don't yet implement it.

Dynamic tool discovery

The client calls tools/list and the server returns the current tool set. It can return a different set tomorrow. It can return a different set if the user changes folder. It can stream notifications/tools/list_changed to say "list changed, ask again."

Static manifests would have failed in the same ways Plugins manifests failed. Dynamic discovery means a server can present aws_s3_* tools when it sees AWS credentials, and not present them otherwise, without redeploying.

Transport-pluggable

The protocol does not specify the transport. There is a stdio transport for local servers (the dominant one in practice today), there is HTTP+SSE for remote (legacy), there is Streamable HTTP for modern remote, and there is nothing stopping a future transport for, say, WebRTC or named pipes. The wire format is the protocol; how the bytes get there is your problem.

JSON Schema for inputs

Tool input is described by a JSON Schema. The model is conditioned (or, in some clients, structurally constrained) to produce arguments that match the schema. This is the function-calling lesson, preserved.

stdio as first-class

You can write an MCP server as a Python script that reads stdin and writes stdout. That is the entire deployment story. No port to bind, no TLS, no DNS, no host. The client spawns your process, talks to it over pipes, kills it when the session ends. This is the affordance that makes MCP servers cheap to write, which is why there are now thousands of them.

Streaming support

Tool results can stream. Long-running tools can emit progress notifications. Sampling -- where the server asks the client to do an LLM call on its behalf -- supports incremental responses. The protocol is designed for the reality that some operations take time.

Where MCP Leaks

I am not here to sell you a flawless protocol. I have shipped enough of these servers, and run enough of them in production for paying customers via Yaw MCP, to know exactly where the pavement ends. Some of these tighten in future versions. Others are inherent to the design.

Schemas describe, they do not enforce

The protocol declares tool inputs with JSON Schema, but the schema is not enforced by the protocol. Some clients validate; some don't. Most servers validate themselves. If your schema says port: integer and the model sends "port": "443", what happens depends on whether the client coerces, whether the server validates, and whether your handler is permissive.

We will spend most of Chapter 5 on this. The short version is: validate aggressively in your server, even when the client says it validated. Trust nothing.

Error message conventions are still settling

JSON-RPC has standard error codes (-32600 parse error, -32601 method not found, etc.). MCP layers tool-call errors on top, and the convention for distinguishing "the user gave me bad input, retry with different input" from "the upstream API is down, do not retry, surface this to the user" is not crisp. Different servers do it differently. Different clients render the difference differently. We will have a strong opinion about this in Chapter 7.

Tool composition is fuzzy

If your server exposes read_file and write_file, the model figures out composition. If you want a higher-level move_file that itself calls read_file then write_file, that's your job inside the server. There is no protocol-level pipelining of tool calls. The model is the composer; it is sometimes a bad composer.

Auth is server-by-server

There is no shared auth story across servers. Each server decides how it authenticates. @yawlabs/aws-mcp uses your local AWS profile via the SDK credential chain. @yawlabs/lemonsqueezy-mcp reads an API key from env. @yawlabs/tailscale-mcp uses a tag-scoped Tailscale OAuth client and mints a short-lived bearer token at startup. @yawlabs/npmjs-mcp uses an npm automation token. There is no single "log in to MCP" because there is no single MCP back-end to log in to.

This is a feature, not a bug -- the protocol stays small -- but it is also the source of the most operational pain. Chapter 4 is the auth chapter and we will get specific.

Resources are under-used

The protocol has a resources capability that is, in spirit, "here are URIs the server can read for the model." In practice, almost everyone shoehorns everything into tools, because tools are the bit clients support best and resources are the bit clients render unevenly. This is unfortunate. Resources are a great primitive for "expose a corpus of files to the model with consistent semantics," but the rest of this book defaults to tools too -- Chapter 2 opens the box on what resources are, and after that I lean almost entirely on tools, because that is what every server I ship does in practice.

Cancellation is best-effort

If the user hits stop, the client sends a notifications/cancelled message, and the server is expected to abort whatever it's doing. Whether it actually does depends entirely on the server implementation. Streaming tools that hold network connections don't always abort cleanly. Tools that have already side-effected can't really cancel. Treat cancellation as best-effort: design tools so that if the cancellation lands cleanly the user is happy, and if it doesn't the world isn't broken. This book doesn't have a dedicated cancellation chapter; it's a thread you'll see picked up in passing in Chapter 6 (composition) and Chapter 9 (hosting).

Pitfall: if you're writing your first MCP server and you take only one pitfall warning from this chapter, take this one: the schema is descriptive, not enforcing. Validate every tool input inside your handler. Treat the model's argument blob as untrusted input. We will see, in Chapter 3, exactly what happens when you don't.

A Concrete Code Comparison

Let's make the abstraction concrete. Here is the same conceptual tool -- "list issues on a GitHub repo" -- written first as an OpenAI function call, then as an MCP server tool. This is the same list_issues tool we will build end to end in Chapter 3, so the shape will look familiar when we get there. I am keeping both versions small for clarity.

As an OpenAI function call

ts// client side, OpenAI SDK import OpenAI from "openai"; const openai = new OpenAI(); const tools = [ { type: "function" as const, function: { name: "list_issues", description: "List issues on a GitHub repository, filtered by state.", parameters: { type: "object", properties: { owner: { type: "string", description: "Repository owner." }, name: { type: "string", description: "Repository name." }, state: { type: "string", enum: ["open", "closed", "all"], default: "open" }, }, required: ["owner", "name"], }, }, }, ]; const res = await openai.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: "show me the open issues in vercel/next.js" }], tools, }); // you handle the tool call yourself const toolCall = res.choices[0].message.tool_calls?.[0]; if (toolCall?.function.name === "list_issues") { const args = JSON.parse(toolCall.function.arguments); const result = await listGithubIssues(args); // feed result back into a follow-up completion ... }

This works, for OpenAI. It does not work for Claude without rewriting the schema into Anthropic's tool-use format. It does not work for Gemini without rewriting again. It does not run inside Cursor without writing a Cursor extension. It is locked to whichever client you wrote it for.

As an MCP server

ts// server side, MCP TypeScript SDK import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const server = new McpServer({ name: "github-tools-mcp", version: "0.1.0" }); server.registerTool( "list_issues", { description: "List issues on a GitHub repository, filtered by state.", inputSchema: { owner: z.string().describe("Repository owner."), name: z.string().describe("Repository name."), state: z.enum(["open", "closed", "all"]).default("open"), }, }, async ({ owner, name, state }) => { const result = await listGithubIssues({ owner, name, state }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; }, ); const transport = new StdioServerTransport(); await server.connect(transport);

Same shape, same JSON Schema. But now: Claude Desktop runs it. Claude Code runs it. Cursor runs it. Continue runs it. Cline runs it. Zed runs it. ChatGPT runs it via the OpenAI Agents SDK's MCP support. Future clients run it. The model in the chat does not need to know the protocol; it just calls the tool. The tool author writes the server once.

That's the trade. You give up the convenience of a single SDK call for the structural property of being a separate process that any compliant client can talk to. In a world with one client you'd take the SDK call. In a world with seven, you take the protocol.

We will build a non-trivial server end-to-end in Chapter 3, but I wanted you to see the shape now so that when I say "write a server" the picture in your head is roughly correct.

The 2026 Landscape

As I write this in 2026, here is the lay of the land.

Clients that speak MCP natively. Claude Desktop and Claude Code on the Anthropic side. Cursor. Continue. Cline. Zed. ChatGPT, via the OpenAI Agents SDK, which speaks MCP as a first-class peer to OpenAI's own tool format. Most agentic-coding wrappers now ship MCP support before they ship anything else.

Reference servers. The official modelcontextprotocol/servers repo has a reference implementation set covering filesystem, fetch, git, github, postgres, and a handful more. Beyond that, the ecosystem is well into the thousands. There is something like five thousand published MCP servers as of this writing, ranging from "ten-line hobby project" to "production-grade hosted multi-tenant service." The long tail is enormous and very uneven.

Registries and discovery. The de-facto third-party registries are glama.ai, Yaw MCP, and Smithery, each with somewhat different curation models. glama.ai tilts toward broad, browsable directory; Yaw MCP toward hosted-and-graded production deployments; Smithery toward one-click install for end users. Anthropic does not run an official registry, which is part of why three credible ones exist.

The @yawlabs scope. I maintain fourteen production servers under the @yawlabs scope. Tailscale, npmjs, AWS, Electron, ctxlint, LemonSqueezy, and another eight I'll introduce as we go. Each one is a real server I run for myself and ship to npm. Throughout the book I will reference these as concrete examples, not as case studies of perfect engineering -- they have warts, the warts have stories, and the stories are useful.

Hosting. Three rough lanes: run on your laptop over stdio (free, single-user), host yourself behind a Streamable HTTP endpoint (your ops, your bill), or hand it to one of the managed MCP platforms. Disclosure up front: I run Yaw MCP, one of the managed options. Chapter 9 walks through the alternatives -- glama.ai, Smithery, Cloudflare Workers, Fly.io, ECS, your own VPS or Kubernetes -- with the costs and trade-offs of each.

Why This Book Exists

There are, by my count, three categories of MCP documentation in the world right now.

The spec. modelcontextprotocol.io. Authoritative, complete, dry. It tells you exactly what bytes go on the wire and what they mean. It does not tell you what to actually do. Reading the spec end-to-end is like reading the HTTP/1.1 RFC end-to-end -- valuable but not the same as knowing how to build a web service.

The tutorials. There are now hundreds of "build your first MCP server in fifteen minutes" tutorials. Most of them are decent. They get you to a hello-world server. They almost never address what happens at month three: schemas drifting, auth keys rotating, error messages confusing your users, your server dying under concurrent requests, your tool descriptions being just slightly wrong in a way that makes the model use the wrong tool 30% of the time, your stdio process leaking memory.

The missing book. The thing in between. The practitioner's view. War stories. What I learned shipping fourteen of these servers and operating a hosting platform for many more. What I would tell you over a beer if you said "I'm about to ship an MCP server, what should I know."

This book is that third thing. It is not an introduction; you have written backend code, you have used Claude Code, you do not need me to explain what an LLM is. It is also not a spec; the spec is the spec, and you should bookmark it. It is the book that lives between, and that I wish someone had handed me in March 2025 when I was deleting agents-glue/.

I am opinionated. The opinions are earned. They will sometimes contradict the official guidance, sometimes contradict popular tutorials, sometimes contradict things I've said publicly before that I've since changed my mind on. When I am opinionated I will tell you why. When I am uncertain I will tell you that too.

Forward Map

Here is what the rest of the book covers. Each chapter is meant to be useful on its own; you do not have to read in order, although Chapters 2-4 build on each other and are best read in sequence.

Chapter 2: Anatomy of an MCP Server. What is in the box. The lifecycle: initialize handshake, list tools, list resources, call tools, shutdown. The server SDKs. The transport options and when to pick which. The shape of the JSON on the wire, with annotated traces. Read this if you want to understand the moving parts before you write any of them.

Chapter 3: A Worked Example, End to End. We build a GitHub issues server, properly. Schema, handlers, error paths, tests, packaging, Claude Code integration. Real code, not pseudocode. By the end you have a server you could ship.

Chapter 4: Auth, Or, How To Not Leak Tokens. Local secrets, env vars, OS keychains, OAuth flows for remote servers, the "stdio assumes the user is the user" model and where it breaks. Why I dislike most of the patterns I see in the wild and what I do instead. War stories from running auth-bearing servers in production.

Chapter 5: Schemas, Types, and Why The Model Calls The Wrong Tool. JSON Schema as you actually need to write it for MCP. How the model reads your descriptions. What goes wrong when your description field is an afterthought. Validation strategies. The schema-vs-runtime mismatch that bit me on @yawlabs/aws-mcp and how to avoid it.

Chapter 6: Tool Composition. When one tool's output is the next tool's input. The agentic loop. Output-shape-as-input-shape, pagination as a composability problem, list-then-detail patterns, idempotency under model retries, the read-vs-write naming and confirmation discipline, sampling, the macro-tool anti-pattern, and cross-server composition. Closes with a four-tool flow on @yawlabs/aws-mcp that ties the patterns together.

Chapter 7: Errors That Help, Not Hide. The three error conventions MCP gives you and when to use each. Why throws are almost always the wrong choice in handlers. Trigger phrases. Retry budgets. Network-layer error normalization. Logging without leaking secrets. A six-axis rubric for grading your error handling before you ship.

Chapter 8: Testing a Probabilistic Consumer. The four layers: unit tests against handlers, integration tests over the transport, protocol conformance, and end-to-end evals against a real model. The harness pattern that makes E2E tests tolerable. Why eval-driven development is the layer you skip at your peril, and what it caught for me.

Chapter 9: Hosting, Scaling, and Not Going Broke on Idle. Container packaging, reproducible builds, and a tour of the realistic hosting options -- managed MCP platforms (glama.ai, Yaw MCP, Smithery), Cloudflare Workers, Fly.io, ECS/Fargate, your own VPS, your own Kubernetes -- with the cost/complexity/availability tradeoffs of each. HTTP transport specifics: sessions, stickiness, reconnects. Migrations on boot. Observability for a multi-tenant service.

Chapter 10: Security Review Survival. The MCP threat model -- different for stdio and HTTP. Supply-chain hygiene for stdio (lockfiles, provenance, no postinstall scripts). Network hygiene for HTTP (TLS, auth on every request, three-axis rate limiting). Prompt injection via tool output and the trust-boundary pattern that fixes it. Sandboxing, allowlists, the destructive-tool annotation, and the questions a real auditor will ask.

Chapter 11: Case Studies from the @yawlabs Portfolio. Four deep dives, one server each: tailscale-mcp (the first), npmjs-mcp (auth-shaped), aws-mcp (schema-shaped), and lemonsqueezy-mcp (error-and-money-shaped). Each one walks through why I built it, the bugs I shipped, the bugs I caught at 11pm, and what a v2 would look like. The cross-cutting lessons section at the end is the most reread part of my own copy.

Chapter 12: The Future of MCP. Open questions. Where the protocol is going. The bits I expect to change. The bits I expect to ossify. What the post-MCP world might look like, if there is one.

We start in Chapter 2 by opening the box and naming the parts. If you have ever wondered what specifically happens between the moment Claude decides to call your tool and the moment your code runs, that's where we go next.

But before you turn the page, do me a favor. Open your editor. Look at your own version of agents-glue/ -- the folder of one-off scripts, the wrappers around CLIs, the home-grown adapters between an LLM and your tools. They might be in a folder called prompts/, or ai-helpers/, or just sitting at the root of a project pretending to be utilities. Find them.

By the end of this book, you should be able to delete most of them.

That is the whole pitch.


Hands-on

If you haven't yet installed an MCP server in a real client and watched a model call a tool, do it now. The "Using MCP servers" pre-chapter in the front matter walks through the install flow for Claude Desktop and Claude Code in about ten minutes. Follow it through to the point where you have at least one server registered and you've watched the model pick a tool out of its tools list and call it correctly. The rest of the book is much easier to ground once you've seen that loop close once.

Companion repo: the exercises/module-1/ directory (the companion's exercise modules map 1:1 to chapters) walks through the install with screenshots, troubleshooting tips, and a short checklist for "did the install actually work." The companion repo is public -- just clone it from https://github.com/YawLabs/mcp-in-production-companion.