Giving a model new abilities is an exercise in interface design
A good Tool is not a good API — because the consumer is a model, not a human
Model Context Protocol — a standard adapter between a model and the outside world
| Lens | Before MCP | After MCP |
|---|---|---|
| Integration | A bespoke plugin per host (the N×M problem) | 1 server → every host can use it (N+M) |
| Standard | Ad-hoc function-calling formats | A common spec over JSON-RPC 2.0 |
| Ecosystem | Vendor lock-in | Broadly adopted by major coding agents in 2026; governance moved under the Linux Foundation |
| Role | "What the model knows" | "What the model can do" |
* Ecosystem figures and adoption are a June 2026 snapshot. Check modelcontextprotocol.io for the current state.
Everything an MCP server exposes to a host comes down to these three
Search something, create an issue, run a query — active actions the model decides on and calls. Each has an input schema (JSON Schema) and a description, and most of §4's design principles live here. Because side effects are possible, you flag read-only vs. destructive behavior via annotations.
File contents, DB records, documents — read-only data identified by a URI. The key difference from a Tool is that it has no side effects and is usually driven by the host (app) to pull in context. A flow like "add this PDF to context" is a Resource.
Templates like "review this code" or "draft release notes" that take arguments and expand. Typically the user explicitly chooses them (slash command, menu). The server offers common tasks in a vetted form so you don't rewrite the prompt every time.
Message format, transport, auth — the skeleton to know before you build
The host is the app that holds the model (Claude, an IDE). It spins up one client per server connection and talks 1:1.
The side you build. It exposes Tools, Resources, and Prompts — and works without knowing which host is calling.
The server is host-independent — build it once and every host that speaks the protocol reuses it. That's MCP's core payoff.
Every message follows the JSON-RPC 2.0 spec. There are two transports — local processes use stdio (standard I/O), remote servers use Streamable HTTP. Same message format, so write server logic decoupled from transport.
Remote HTTP servers use OAuth 2.1-based auth as the standard. The core rule is least privilege — issue token scopes only as broad as the task needs, and keep secret keys on the server side only. Never let credentials leak into the tool definitions the model sees.
The consumer being a model, not a human, changes everything
Use names where the action reads instantly — search_orders, create_issue. Vague names like do_action or handle confuse the model. The name is the first signal it uses to pick a tool.
The description isn't a human comment; it's an instruction to the model. Write not just "what it does" but "when it's appropriate and when to reach for another tool instead." It's the highest-ROI single line you'll write.
Free-text params are easy to get wrong. When the valid values are known, pin them with enum; mark mandatory fields required. The more params, the higher the odds of filling one wrong — keep only what's essential.
The model parses your response again. Return lean, structured results and strip human-facing decoration (HTML, excessive metadata). Stay aware that every response is context tokens.
Don't throw a raw stack trace. Say "order ID not found — the format is ORD-XXXX," giving why it failed and how to fix it in natural language. That's what lets the model self-recover with a retry.
Hard-to-undo actions — delete, charge, external send — should require human approval before they run. Flag them with annotations.destructiveHint so the host can surface a confirmation step. Safety before the convenience of automation.
Throw 50 tools at once and the model picks the wrong one (context rot). Merge similar capabilities and route which tools to expose by context. The rule isn't "expose every feature" but "only what's needed now."
State whether a tool is read-only (readOnlyHint) and whether repeating the same call is safe (idempotentHint). This is how the host and model safely decide on retries and parallel calls.
Return 100K rows and you blow up context and cost. Set a default limit and cursor-based pagination, and let the model ask for more when it needs it.
A tool's privileges are your blast radius when something goes wrong. Grant only the minimal scope the task needs, and keep API keys and tokens inside the server. Never expose credentials in the interface the model sees.
Most MCP servers trip over the same spots
The most common waste. Major systems — GitHub, Slack, databases — usually already have an official or community server.
Turning 50 existing API endpoints into 50 tools. The model uses tools differently than a human — they end up too granular and too many.
get_user+get_orders → get_customer_summary.Settling for "Order lookup API." The model gets no cue about when to reach for this tool.
Handing back a whole table or full log. Context blows up and token cost spikes.
Letting the model delete, transfer money, or send mail directly. One misjudgment becomes irreversible.
"More tools = smarter" is backwards. More choices means more wrong picks and higher cost.
A 3-step decision before you stand up a new server
Search the official and community MCP server registries first. GitHub, Slack, Google Drive, Postgres and the like mostly already exist. If it does, use it — a custom build is maintenance debt.
Build your own only when no public server can exist — internal systems, your own DB, domain logic. Start from an official SDK (Python, TypeScript, etc.) and use §4's principles as your checklist.
Ship read-only tools first and watch whether the model uses them well. Open up writes and destructive actions gradually, behind an approval gate. Don't hand over full power on day one.