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.
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)
Filesystem
Local — stdio
Postgres
Local — stdio
Linear
Remote — Streamable HTTP
Sentry
Remote — 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.
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
With MCP
A server implements MCP once
Any MCP client can call it
Auth, shapes, and docs are standardized
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.
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.
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:
npm init -ynpm install @modelcontextprotocol/server zodnpm 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:
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:
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.
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:
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:
npm publish --access publicmcp-publisher initmcp-publisher login githubmcp-publisher publish
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:
{"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:
# Local server, run over stdioclaude mcp add weather -- node /absolute/path/to/weather-server/build/index.js# Remote server, over Streamable HTTPclaude mcp add --transport http weather https://weather.example.com/mcp
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.
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.



