Model Context Protocol: A Practical Guide to MCP
Baljeet Dogra
A practical guide to what MCP is, how it works under the hood, and how to expose your own endpoint so any AI service can consume it.
01 — What is MCP?
Model Context Protocol (MCP) is an open standard introduced by Anthropic in late 2024. Its premise is simple: AI models are powerful reasoners, but they are locked in a box. They cannot read your database, call your internal APIs, or browse private documents—unless you build a custom integration every single time.
MCP defines a standardised, transport-agnostic protocol so that any AI client (Claude, a custom LLM wrapper, an agent framework) can discover and invoke capabilities exposed by any compliant server—without bespoke glue code.
Think of MCP as the USB-C of AI integrations—one standard shape that any device can plug into, regardless of who made the cable or the port.
Before MCP, connecting an AI to a new data source meant writing a one-off adapter, handling auth yourself, and repeating the work for every new model you adopted. With MCP, you write the server once and any compliant client can use it. The authoritative spec lives at modelcontextprotocol.io.
02 — Architecture overview
MCP follows a client–server model with three distinct roles:
Host
The AI application (e.g. Claude Desktop, your LLM agent). It orchestrates one or more clients and presents results to the user.
Client
Embedded inside the host. Maintains a 1:1 connection to a single MCP server, handling protocol framing and capability negotiation.
Server
Your service. Exposes tools, resources, and prompts through the MCP interface. Runs locally or remotely.
A single host can connect to multiple MCP servers simultaneously—one for your database, one for your calendar, one for Slack. The client inside the host manages each connection independently, and the model decides which tool to invoke based on the declared capability descriptions.
03 — Transport layer
MCP is transport-agnostic. The protocol layer is JSON-RPC 2.0; the wire layer is your choice:
- • stdio — for local processes. The host spawns your server as a subprocess and communicates over stdin/stdout. Zero networking overhead, ideal for desktop tools.
- • HTTP + SSE — for remote servers. The client opens a Server-Sent Events stream for push notifications from the server, and POSTs requests to an HTTP endpoint. Use this when exposing MCP to external consumers.
- • WebSocket — bidirectional, lower latency for high-frequency interactions. Less common but fully spec-compliant.
All messages conform to JSON-RPC 2.0: each request carries a unique id, a method name, and an optional params object. Responses echo the id with either a result or an error.
04 — Exposing an MCP endpoint
Here is a minimal MCP server in Node.js using the official @modelcontextprotocol/sdk package, exposed over HTTP + SSE so any remote service can consume it.
1. Install dependencies
npm install @modelcontextprotocol/sdk express
2. Bootstrap the server
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import express from 'express';
const app = express();
app.use(express.json());
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
});
3. Register your SSE and message routes
const transports = {};
// Client connects here to open the SSE stream
app.get('/sse', async (req, res) => {
const transport = new SSEServerTransport('/messages', res);
transports[transport.sessionId] = transport;
res.on('close', () => {
delete transports[transport.sessionId];
});
await server.connect(transport);
});
// Client POSTs JSON-RPC requests here
app.post('/messages', async (req, res) => {
const { sessionId } = req.query;
const transport = transports[sessionId];
if (!transport) {
return res.status(404).json({ error: 'Session not found' });
}
await transport.handlePostMessage(req, res, req.body);
});
app.listen(3000, () => console.log('MCP server listening on :3000'));
05 — Defining tools & resources
Tools are the core primitive—functions the AI model can invoke. Each tool has a name, a description the model reads to decide when to use it, and an input schema defined with Zod.
import { z } from 'zod';
server.tool(
'get_weather',
'Fetch current weather for a given city',
{
city: z.string().describe('City name, e.g. "London"'),
units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
},
async ({ city, units }) => {
const data = await fetchWeatherFromAPI(city, units);
return {
content: [{ type: 'text', text: JSON.stringify(data) }],
};
}
);
Beyond tools, MCP supports two other primitives:
-
•
Resources — static or dynamic content the model can read (files, database rows, config). Identified by a URI scheme like
db://users/42. - • Prompts — reusable prompt templates with parameters, surfaced to the user as slash-commands in supporting clients.
// Exposing a resource
server.resource(
'user-profile',
new ResourceTemplate('users://{userId}/profile', { list: undefined }),
async (uri, { userId }) => ({
contents: [{
uri: uri.href,
text: await getUserProfile(userId),
}],
})
);
06 — Security & authentication
A publicly reachable MCP endpoint is an API surface. Treat it accordingly.
- Use HTTPS in production. Never expose MCP over plain HTTP to untrusted networks.
- Validate an
Authorization: Bearer <token>header in Express middleware before SSE or message routes execute. - Scope tokens. Clients should only invoke the tools they need—avoid god-mode tokens.
- Rate-limit the
/messagesendpoint. A model in a loop can call a tool many times per second. - Sanitise tool inputs even when schema-validated—never pass them directly to a shell command or raw SQL query.
// Minimal auth middleware
app.use((req, res, next) => {
const token = req.headers['authorization']?.split(' ')[1];
if (token !== process.env.MCP_SECRET) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
07 — Production checklist
-
•
Pin
@modelcontextprotocol/sdkto an exact version—the spec is still evolving. -
•
Emit structured logs per request with
sessionIdso you can trace tool calls end-to-end. - • Return well-formed error objects from tools—the model reads error text and adjusts its behaviour.
-
•
Add a
/healthzendpoint returning 200 OK for load balancers. -
•
Handle
SIGTERMgracefully: drain open SSE connections before exiting. -
•
Test with the MCP Inspector CLI:
npx @modelcontextprotocol/inspector http://localhost:3000/sse -
•
Publish your server’s capability manifest for auto-discovery at
/.well-known/mcp.json
MCP is young but moving fast. For message formats, capability negotiation, and upcoming features such as OAuth 2.1 support, keep modelcontextprotocol.io as your reference. If you are shipping agents at scale, pair this with LLM cost architecture and AppSec for AI coding agents.
Building MCP servers or AI agents?
I help teams design production-ready MCP integrations, agent pipelines, and LLM products—with security, cost control, and compliance built in.
Get in Touch