MCP Servers: What They Are and Why Every Developer Should Care
You've wired up tool use before. You've written the JSON schema, handled the function call response, parsed the structured output. It works. And then you move to a different project, or a different AI client, and you write it all again from scratch.
That's the problem MCP solves.
The Problem With Bespoke AI Integrations
Every AI integration today is essentially custom. You define tools for one client, one model, one codebase. The definitions aren't portable. If you want Claude Desktop, Cursor, and your own internal tooling to all have access to the same set of capabilities — your database, your internal APIs, your file system — you're maintaining three separate integrations.
MCP (Model Context Protocol) is Anthropic's attempt to fix that with a standard. Think of it the way HTTP standardised web communication: before it, every server-client pair spoke a custom protocol. After it, you could build once and interoperate across the ecosystem. MCP is trying to do the same for AI tool access.
The bet is that the AI tooling ecosystem benefits from standardisation the same way the web did. It's early, but the direction is right.
How the Protocol Actually Works
At its core, MCP is JSON-RPC 2.0 over one of two transports: stdio (standard input/output) or SSE (Server-Sent Events over HTTP).
When a client connects to an MCP server, it performs a capability negotiation handshake. The client calls initialize, the server responds with the protocol version it supports and its declared capabilities, and both sides confirm. From there, the client can discover what the server exposes:
- Tools — callable functions with typed input schemas (what most people use MCP for)
- Resources — readable data sources (files, database rows, API responses) the model can pull into context
- Prompts — reusable prompt templates the server can serve on request
For the vast majority of use cases, you're working with tools. The flow is:
- Client calls
tools/list— server returns a list of available tools with their JSON Schema definitions - Model decides to invoke a tool — client calls
tools/callwith the tool name and arguments - Server executes the tool, returns the result
- Result goes back into the model's context
That's the entire loop. What makes it powerful is that the client handling this — Claude Desktop, Claude Code, a custom agent — doesn't need to know anything about the tools ahead of time. It discovers them at runtime.
Building a Real MCP Server in TypeScript
The official @modelcontextprotocol/sdk package handles the protocol plumbing. Here's a server that exposes a tool to query a hypothetical internal API — not a hello world, but something structurally representative of what you'd actually build:
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: 'internal-api-server',
version: '1.0.0',
})
// Tool: fetch a user record by ID
server.tool(
'get_user',
'Fetch a user record from the internal API by user ID',
{
userId: z.string().describe('The unique identifier for the user'),
includeMetadata: z
.boolean()
.optional()
.default(false)
.describe('Whether to include extended metadata in the response'),
},
async ({ userId, includeMetadata }) => {
// Input is already validated by Zod — safe to use directly
const endpoint = includeMetadata
? `/api/users/${userId}?metadata=true`
: `/api/users/${userId}`
const res = await fetch(`https://internal.example.com${endpoint}`, {
headers: { Authorization: `Bearer ${process.env.INTERNAL_API_TOKEN}` },
})
if (!res.ok) {
return {
content: [
{
type: 'text',
text: `Error fetching user ${userId}: ${res.status} ${res.statusText}`,
},
],
isError: true,
}
}
const data = await res.json()
return {
content: [
{
type: 'text',
text: JSON.stringify(data, null, 2),
},
],
}
}
)
const transport = new StdioServerTransport()
await server.connect(transport)
A few things worth noting here:
Zod for validation. The SDK accepts Zod schemas directly and uses them to validate inputs before your handler runs. You get type inference for free — userId inside the handler is already typed as string. This is one of the better DX decisions in the SDK.
Error handling via isError. MCP distinguishes between a tool that ran and returned bad news (isError: true) and a tool that threw an exception (which the SDK catches and wraps). The model sees both, but the distinction matters for how it reasons about what to do next.
Secrets stay in the server process. The INTERNAL_API_TOKEN never crosses the MCP boundary. The client only sees the tool result, not how it was produced. This is the security boundary that makes MCP actually usable for internal tooling.
stdio vs SSE: The Transport Decision
The choice of transport is mostly a deployment decision, not a protocol one.
stdio runs the MCP server as a subprocess of the client. The client spawns the process, communicates over standard I/O pipes, and the server dies when the client disconnects. It's simple, zero-network, and the right choice for local tooling — anything you'd run on a developer's machine.
SSE runs the server as a persistent HTTP service. The client connects over HTTP, establishes a server-sent events stream for server-to-client messages, and uses regular POST requests for client-to-server calls. This is the right choice when you need a shared, always-on server — multiple clients connecting to the same tools, a server running in your infrastructure rather than on each developer's machine.
The practical question: is this tool something that runs locally for one person, or something shared across a team or system? If local → stdio. If shared → SSE.
One nuance: stdio servers need to be installed and configured per client (Claude Desktop has a JSON config for this; Claude Code uses similar config). SSE servers are just URLs. If your MCP server needs to reach internal databases or services that are only accessible from your infra, SSE is often the only practical option anyway.
MCP vs Raw Tool Use: When Is It Worth It?
If you're building a single product with a single AI integration, raw function calling / tool use is probably the right call. You define the tools inline, handle the loop yourself, and skip the protocol overhead. MCP adds moving parts.
MCP starts paying off when:
- Multiple clients need the same tools. Claude Code, Claude Desktop, a custom agent, a teammate's tool — if they all need access to the same internal APIs, one MCP server is cleaner than four separate integrations.
- You're building internal tooling for a team. A centralised MCP server that wraps your company's APIs means each new AI integration gets all the tools for free, without every engineer re-implementing the same fetch wrappers.
- The tool surface area is large or evolving. With MCP, the client discovers tools at runtime. You can add tools to the server without touching the client. With raw tool use, adding a tool means updating the client.
- You want tool reuse across the ecosystem. The MCP server you write today can be used by any MCP-compatible client in the future. That's speculative value right now, but the ecosystem is growing fast enough that it's not unreasonable to build for it.
If none of those apply, you're probably overengineering it.
What's Not Polished Yet
MCP is young and it shows in certain places.
Auth is still underdefined. The protocol doesn't specify how clients authenticate to servers. For stdio this barely matters — the server runs as a subprocess with the same user permissions. For SSE-based servers, you're implementing auth yourself (bearer tokens, API keys, whatever). There's no standard pattern yet.
Observability is limited. There's no built-in logging or tracing at the protocol level. You're instrumenting your handlers the same way you would any server. If something fails in production, the debug story is whatever you built.
Versioning is coarse. The protocol has a version, but individual tools don't. If you change a tool's schema, connected clients may break silently or handle it poorly depending on their implementation. There's no deprecation mechanism yet.
None of this makes MCP unusable — it just means you're working with a v1 standard. The core protocol is solid. The ecosystem around it is still catching up.
It's Worth Understanding Now
The window where understanding a new protocol gives you a real advantage is short. Once MCP is ubiquitous — once every AI client supports it and every internal tooling project defaults to it — the skill becomes table stakes.
The protocol mechanics are learnable in a day. The interesting judgment calls — what to expose as a tool vs a resource, how to scope your tool surface area, when to split into multiple servers — come from building with it.
If you're working on AI integrations at any level of seriousness, it's worth getting your hands on it now rather than catching up later.