The problem MCP actually solves
A language model on its own is a text function. It has no filesystem, no database, no ticket tracker and no memory of your codebase. Everything useful an assistant does comes from context someone put in front of it, and from actions someone let it take.
Before the Model Context Protocol, every AI application wired those integrations by hand. A GitHub integration written for one editor did not work in another. If you had N clients and M tools, the ecosystem paid for N × M bespoke adapters. MCP, released by Anthropic in late 2024 and now maintained as an open specification, collapses that into N + M: each client implements the protocol once, each tool exposes a server once, and any client can talk to any server.
Hosts, clients and servers
MCP uses a client–server architecture with three named roles. People conflate the first two constantly, and that confusion is the source of most bad diagrams.
| Role | What it is | Responsibility | Examples |
|---|---|---|---|
| Host | The AI application itself | Owns the model loop, the UI, user consent, and the lifecycle of every client it spawns | Claude Code, Claude Desktop, VS Code, your own agent |
| Client | A protocol connector inside the host | Maintains exactly one stateful connection to one server: handshake, requests, responses, errors | The MCP client object VS Code instantiates per configured server |
| Server | A separate program exposing capabilities | Declares tools, resources and prompts; executes requests against a real system | Filesystem server, GitHub server, Sentry server, your internal Postgres server |
The one-client-per-server rule matters. A host with three configured servers holds three independent, stateful sessions. One server crashing does not tear down the others, and each connection negotiates its own capabilities and its own protocol version.
Two layers: data and transport
The spec splits cleanly into a data layer and a transport layer. The data layer defines what messages mean. The transport layer defines only how bytes are framed and delivered. Protocol semantics are identical on every transport, which is why a server written against the SDK can be moved from a local subprocess to a remote service without changing a single tool implementation.
The data layer is JSON-RPC 2.0, UTF-8 encoded. Three message shapes exist: requests (carry an id, expect a response), responses (result or error), and notifications (fire-and-forget, no id). The direction rules are strict — clients send requests and notifications, servers send responses and notifications.
Tools vs resources vs prompts
Servers expose three kinds of capability. The distinction is not about data format — it is about who is in control, and that is the part interviewers probe.
| Primitive | Controlled by | Semantics | Typical example |
|---|---|---|---|
| Tools | The model | Callable functions with a JSON Schema for inputs. May have side effects. The model decides when to invoke one. | createIssue, searchFlights, runQuery |
| Resources | The application | Read-only data addressed by URI, with a MIME type. The host decides what to expose and when to attach it. | file:///repo/README.md, db://schema/public |
| Prompts | The user | Parameterised instruction templates the user explicitly picks, often wiring specific tools and resources together. | /review-pr, /summarise-incident |
Each primitive has a symmetric pair of methods: tools/list and tools/call, resources/list and resources/read, prompts/list and prompts/get. Discovery first, then invocation. Servers that support dynamic capabilities emit a notifications/*_list_changed notification so the client can re-list instead of polling.
A tool definition is just a schema
{
"name": "searchIssues",
"title": "Search issues",
"description": "Search the issue tracker for open issues matching a query.",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Full-text search string" },
"state": { "type": "string", "enum": ["open", "closed", "all"] },
"limit": { "type": "integer", "minimum": 1, "maximum": 50 }
},
"required": ["query"]
}
}The lifecycle of one MCP session
Every connection follows the same arc: initialise, discover, then operate. Nothing happens before the handshake completes.
- initialize. The client sends its supported protocol version and its own capabilities (sampling, elicitation, roots). The server replies with its version, its capabilities, and
serverInfo. Version mismatch is negotiated here or the connection fails fast. - initialized notification. The client confirms it is ready. Only now may normal traffic flow.
- Discovery. The client calls
tools/list(and the resource and prompt equivalents), then hands the tool schemas to the model as available functions. - Invocation. The model emits a tool call. The host applies its consent policy, the client sends
tools/call, the server executes and returns content blocks — text, images, or structured data. - Feedback loop. The result goes back into the model's context and the loop repeats until the model answers.
- Teardown. The client closes the stream or terminates the subprocess. In-flight work is abandoned via a cancellation signal.
--> {"jsonrpc":"2.0","id":7,"method":"tools/call",
"params":{"name":"searchIssues","arguments":{"query":"timeout","state":"open"}}}
<-- {"jsonrpc":"2.0","id":7,
"result":{"content":[{"type":"text","text":"3 open issues: #412, #418, #431"}],
"isError":false}}isError: true — the model is supposed to see that text and adapt. Collapsing these two into one channel is the most common server-authoring mistake.stdio vs Streamable HTTP
Two standard transport bindings exist. Choosing between them is a deployment decision, not a protocol one.
| stdio | Streamable HTTP | |
|---|---|---|
| Topology | Client launches the server as a subprocess | Server is a network service at one MCP endpoint |
| Framing | Newline-delimited JSON on stdin/stdout | HTTP POST per message; reply is JSON or an SSE stream |
| Fan-out | One client per process | Many clients, many sessions |
| Auth | Inherits the local user's OS permissions | OAuth 2.1 / bearer tokens, per-session identity |
| Cancellation | notifications/cancelled | Close the request's response stream |
| Good for | Local files, git, editors, dev tooling | SaaS integrations, shared internal servers |
Streamable HTTP replaced the older HTTP+SSE pairing precisely because two endpoints made session resumption and load balancing awkward. With a single endpoint the server may answer a POST with a plain JSON body for a fast call, or upgrade that same request into an SSE stream when it needs to emit progress notifications before the final result. All protocol metadata still travels in the message body; HTTP headers only mirror it so proxies can route without parsing JSON.
Writing a minimal MCP server
The SDKs hide the JSON-RPC plumbing entirely. A working server is mostly schema declaration plus the function you would have written anyway.
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: "issues", version: "1.0.0" });
server.registerTool(
"searchIssues",
{
title: "Search issues",
description: "Search the issue tracker for issues matching a full-text query.",
inputSchema: { query: z.string(), state: z.enum(["open", "closed", "all"]).default("open") },
},
async ({ query, state }) => {
const issues = await tracker.search(query, state); // your existing code
return { content: [{ type: "text", text: formatIssues(issues) }] };
},
);
await server.connect(new StdioServerTransport());Design rules that separate a usable server from a demo:
- Few, coarse tools beat many thin ones. Every tool schema consumes context tokens on every turn and widens the model's choice space. Twenty tools is already a lot.
- Return tokens, not dumps. Paginate, truncate, and summarise. A tool that returns 200 KB of JSON will blow the context window and degrade the answer.
- Make results self-describing. Include ids and URLs the model can cite or feed into the next call.
- Keep tools idempotent where possible, and mark destructive ones clearly so the host can require confirmation.
- Validate server-side. Schemas guide the model; they do not constrain it. Treat every argument as untrusted input.
Debug with the MCP Inspector, which connects to your server as a client and lets you list and call everything by hand before an LLM is anywhere near it.
Security and the trust boundary
MCP hands a language model the ability to execute code against your systems. The protocol deliberately pushes authorisation to the host and the server, which means the security work is yours.
- Prompt injection is the headline risk. Tool output re-enters the model's context. A malicious issue body, web page or file comment can contain instructions the model then follows. Treat every tool result as untrusted data, never as instructions, and never let one server's output silently authorise another server's write.
- Confused deputy. The server acts with its own credentials, not the user's. If your server holds an admin token, every user of that server is effectively an admin. Scope tokens per user or per session.
- Consent belongs to the host. Destructive tool calls should surface a confirmation. Auto-approving everything is convenient and is how people delete production data.
- Server supply chain. Installing a community MCP server is running someone else's code with your filesystem access. Pin versions and read the source for anything touching credentials.
- Least privilege at the data source. A read-only database role for a query server costs nothing and removes an entire class of incident.
Where MCP struggles
- Context economics. Tool definitions are always resident. Connect ten servers and a meaningful slice of the window is gone before the user types anything.
- Discovery is not selection. The protocol tells the model what exists; it does nothing to help the model pick well among fifty similar tools. That remains a prompting and tool-design problem.
- No built-in orchestration. Retries, sagas, partial failure across multiple servers — all yours to build.
- Latency stacks. Each tool call is a full model round trip plus a network call. Multi-step agent flows feel slow, and streaming only masks part of it.
- It is effort, not magic. The protocol describes how things should talk; it does not run anything. The value shows up only once an ecosystem of good servers exists.
How to discuss MCP in an interview
If you are asked to design an AI assistant that touches internal systems, MCP is a strong answer — but only if you frame it as a protocol choice with consequences.
- Open with the integration-count argument: N × M adapters become N + M implementations.
- Name the three participants precisely, and say one client per server, stateful.
- Split data layer from transport, and justify stdio for local tooling versus Streamable HTTP for shared services.
- Use the control axis for the primitives: tools are model-controlled, resources are application-controlled, prompts are user-controlled.
- Volunteer the failure modes — context bloat, prompt injection through tool output, the confused deputy — before the interviewer asks. That is the signal that you have run this in production, not just read about it.
Read the primary sources rather than summaries: the architecture overview, the specification, and the reference servers. The reference implementations are short, and reading three of them teaches you more about good tool design than any article can.