Every AI product eventually hits the same wall: the model is only as useful as what it can reach. It can reason brilliantly about your database, your ticket tracker, or your codebase, and still be unable to touch any of them, because reasoning and access are different problems. For a while, every team solved access its own way — a bespoke plugin here, a hand-rolled function-calling schema there, none of it reusable past the one integration it was built for.

The Model Context Protocol, or MCP, is Anthropic's answer to that: an open standard for connecting AI applications to the tools and data they need, so that connection only has to be built once per system, not once per pairing.

This post covers what MCP actually is, the specific problem it solves, and then gets practical: building a server, deploying it, and connecting it to Claude Desktop, Claude Code, and claude.ai.

01

What MCP Actually Is

MCP follows a client-server architecture. An MCP host — an AI application like Claude Code or Claude Desktop — creates one dedicated MCP client for every MCP server it talks to. A server that runs on your machine and reads local files typically serves a single client over stdio, standard input and output piped directly between two processes. A server that runs in the cloud and serves many users at once talks Streamable HTTP instead, so it can be reached over the network by any number of clients.

MCP host (e.g. Claude Code)

Client A
Client B
Client C
Client D

Filesystem

Local — stdio

Postgres

Local — stdio

Linear

Remote — Streamable HTTP

Sentry

Remote — Streamable HTTP

The host holds one dedicated client per server — local servers over stdio, remote servers over Streamable HTTP.

Underneath both transports is the same data layer: JSON-RPC 2.0 messages that carry a small set of primitives. A server can expose tools the model can call, resources it can read, and prompts it can reuse. The protocol itself is stateless — every request carries its own protocol version and capabilities, so a server can sit behind an ordinary load balancer instead of pinning a client to one instance for the life of a session.

None of this requires a model provider's SDK inside the server. An MCP server is just a program that speaks the protocol — it doesn't know or care which AI application ends up calling it.

02

The Problem It Solves

Without a shared protocol, every tool-to-assistant pairing is its own integration: its own auth flow, its own request shape, its own docs that go stale the moment either side changes something. Build for one assistant and a second one needs the same work over again — the effort scales with the number of pairings, not the number of tools.

Without a protocol

Custom auth per pairing
A different request shape for every assistant
Docs that drift the moment either side changes

With MCP

A server implements MCP once

Any MCP client can call it

Auth, shapes, and docs are standardized

N tools wired to M assistants by hand, versus one protocol both sides implement once.

MCP collapses that to one relationship on each side. A team building a product ships one MCP server for it. A team building an AI application ships one MCP client. Any server built to the spec works with any client built to the spec, so the tool builder and the AI application builder never have to coordinate directly.

That's the entire pitch: not a smarter model, but a standard boring enough that nobody has to keep reinventing the wiring between models and the systems they need to touch.

03

What a Server Exposes

An MCP server can offer a client four kinds of things. Three live on the server; the fourth lets the server ask something of the client mid-call:

Tools

Executable functions the model can invoke to take an action — query a database, call an API, write a file. Each tool declares a typed input schema, so the client can validate a call before it ever reaches your code.

Resources

File-like data the client can read for context — a config file, a set of API results, a database record. Resources are pulled in deliberately, not injected automatically into every conversation.

Prompts

Reusable templates for structuring a task, so a server author can ship the few-shot examples or system prompt a tool needs alongside the tool itself.

Elicitation

The one primitive a client offers back to servers: a way to ask the user for more input or confirmation mid-call, instead of guessing or failing outright.

Two capabilities that used to live in this list, sampling and logging, are deprecated in the current spec. New servers integrate directly with a model provider's API when they need a completion, and log to stderr or OpenTelemetry rather than routing logs through the protocol.

04

Building One

The TypeScript SDK is the fastest path to a working server. Install it alongside Zod, which the SDK uses for typed tool schemas:

bash
1
2
3
npm init -y
npm install @modelcontextprotocol/server zod
npm install -D @types/node typescript

Create the server and register a tool. registerTool takes a name, a description and input schema, and a handler — the handler's return value becomes the model's tool result:

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const server = new McpServer({ name: 'weather', version: '1.0.0' });
server.registerTool(
'get_forecast',
{
description: 'Get the weather forecast for a city',
inputSchema: z.object({
city: z.string().describe('City name, e.g. "Buenos Aires"'),
}),
},
async ({ city }) => {
const forecast = await fetchForecast(city);
return { content: [{ type: 'text', text: forecast }] };
},
);

Finally, connect the server to a transport and run it. For a local server that a host process spawns and talks to over stdio, that's this:

typescript
1
2
3
4
5
6
7
8
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main();

That's a complete, working server. Everything from here — more tools, resources, prompts, real error handling — builds on the same three pieces: a server instance, registered capabilities, and a transport.

05

Deploying It

A local stdio server is fine for tools that only ever run on the same machine as the client — a filesystem server, a local git helper. The moment a tool needs to be shared across a team or reached from the web, it needs a remote transport instead: Streamable HTTP.

The shape barely changes. Swap the transport for one that speaks HTTP, and mount it on a route in whatever web framework you're already using:

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
const app = createMcpExpressApp();
app.post('/mcp', async (req, res) => {
// Stateless: a fresh transport per request. No session to pin to an
// instance, so this handler works behind an ordinary load balancer.
const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});

Because the protocol is stateless, that handler can run behind a plain round-robin load balancer — no sticky sessions, no shared session store, no coordination between instances. Deploy it the same way you'd deploy any other HTTP service: a container behind a reverse proxy, a serverless function, whatever your stack already does for APIs.

If you want the server discoverable beyond your own team, the MCP Registry hosts metadata for public servers. Publish the package to npm first, then use the official CLI to register it:

bash
1
2
3
4
5
npm publish --access public
mcp-publisher init
mcp-publisher login github
mcp-publisher publish
06

Connecting It to Claude

How a server gets connected depends on where it's meant to run. Local stdio servers are wired into a config file; remote HTTP servers are added as connectors with a URL.

In Claude Desktop, local servers go in claude_desktop_config.json — on macOS, ~/Library/Application Support/Claude/claude_desktop_config.json:

json
1
2
3
4
5
6
7
8
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/absolute/path/to/weather-server/build/index.js"]
}
}
}

In Claude Code, the CLI does the same job without hand-editing JSON. Add a local server with a command, or a remote one with a URL:

bash
1
2
3
4
5
# Local server, run over stdio
claude mcp add weather -- node /absolute/path/to/weather-server/build/index.js
# Remote server, over Streamable HTTP
claude mcp add --transport http weather https://weather.example.com/mcp
bash
1
2
3
4
5
claude mcp add --transport http weather --scope project https://weather.example.com/mcp
# Writes the entry to .mcp.json at the repo root — commit it so the
# whole team gets the same server. Each teammate approves it once,
# the first time they open the project.

Adding --scope project writes the entry to .mcp.json at the repository root instead of your personal config, so committing that file gives the whole team the same server — Claude Code prompts each person to approve it the first time they open the project. claude mcp list shows the connection status for everything that's configured.

For a remote server, claude.ai and the Claude Desktop chat app both support it as a Custom Connector: Settings → Connectors → Add custom connector, then the server's HTTPS URL. If the server requires auth, Claude walks you through an OAuth flow before the connection goes live, and lets you scope exactly which of its tools are allowed to run.

07

Where This Is Going

MCP is still a young, actively evolving standard — recent revisions have reworked the protocol's core more than once, most significantly by dropping session state entirely in favor of self-contained requests. New capabilities now ship first as opt-in extensions rather than landing directly in the core spec, and every feature carries a minimum twelve-month deprecation window before it can be removed.

That's worth building for deliberately: pin the protocol version your server targets, read the deprecation notices when you upgrade an SDK, and treat the spec as something that will keep moving under you. The primitives — tools, resources, prompts — have been stable since the beginning; it's the plumbing around them that keeps getting sanded down.