MCP Implementation Guide: Connect AI Agents to Tools
# Model Context Protocol (MCP) lets AI agents connect to any tool or data source via a standard JSON-RPC 2.0 interface. Learn how to build, host, and secure MCP servers in production.
Model Context Protocol (MCP) is the standard that lets AI agents talk to external tools — databases, APIs, file systems — without custom glue code for every integration. One MCP server exposes your tool; any MCP-compatible agent can call it. Here's how to build one that works in production.
What MCP Actually Is
MCP is a JSON-RPC 2.0 protocol that defines three primitives:
| Primitive | What it does | Example |
|-----------|-------------|---------|
| Tools | Functions the agent can call | search_web, run_sql |
| Resources | Data the agent can read | file contents, DB rows |
| Prompts | Reusable prompt templates | structured task starters |
The agent (client) sends a tools/call request; your server (host) executes it and returns results. The transport can be stdio (local), HTTP+SSE (remote), or WebSocket.
Agent (Claude/GPT/Gemini)
│ JSON-RPC 2.0
▼
MCP Server ───▶ Your Tool (DB, API, filesystem)
Building a TypeScript MCP Server
Install the official SDK:
npm install @modelcontextprotocol/sdk
Minimal server that exposes a SQL query tool:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Pool } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const server = new McpServer({ name: "sql-tool", version: "1.0.0" });
server.tool(
"run_sql",
"Execute a read-only SQL query and return results as JSON",
{
query: z.string().describe("SQL SELECT statement"),
limit: z.number().optional().default(100).describe("Max rows to return"),
},
async ({ query, limit }) => {
// Block non-SELECT queries
if (!/^\s*SELECT/i.test(query)) {
return { content: [{ type: "text", text: "Error: only SELECT queries allowed" }], isError: true };
}
const result = await pool.query(`${query} LIMIT $1`, [limit]);
return {
content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Three things worth noting:
- Zod schema defines the tool parameters — the SDK generates the JSON schema automatically.
isError: truesignals failure without throwing — the agent sees a structured error it can reason about.- The server runs over stdio here; swap
StdioServerTransportforStreamableHTTPServerTransportfor a remote server.
HTTP Transport for Remote Servers
Most production MCP servers run over HTTP, not stdio:
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const app = express();
app.use(express.json());
app.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000);
For production, put this behind a reverse proxy (nginx/Cloudflare) with TLS. The client connects via http://your-server.com/mcp.
Connecting Claude to Your MCP Server
In your Claude Code config (~/.claude/settings.json):
{
"mcpServers": {
"sql-tool": {
"command": "node",
"args": ["/path/to/sql-server/dist/index.js"],
"env": {
"DATABASE_URL": "postgres://..."
}
}
}
}
For a remote HTTP server:
{
"mcpServers": {
"sql-tool": {
"url": "https://your-mcp-server.com/mcp"
}
}
}
Security: The Part Everyone Gets Wrong
MCP servers are privileged — they run with the credentials you give them. Three rules:
1. Scope tools narrowly. A run_sql tool that only allows SELECT is much safer than one that accepts arbitrary SQL. Enforce this server-side, not just in the description.
2. Validate all inputs. Even with Zod validation at the transport layer, re-validate inside the handler. Agents can hallucinate parameters that pass type checks but are semantically wrong.
3. Authenticate remote servers. Use API keys or OAuth 2.0 tokens on the HTTP transport. The MCP spec supports bearer token auth:
app.use((req, res, next) => {
const token = req.headers.authorization?.replace("Bearer ", "");
if (token !== process.env.MCP_SECRET) {
return res.status(401).json({ error: "Unauthorized" });
}
next();
});
Common Patterns at Shahriar Labs
At Shahriar Labs, we use MCP to connect our AI agents to internal tools:
- Code execution: sandboxed Node.js runner for agent-generated scripts
- Knowledge base: semantic search over internal docs via Qdrant
- QuantumSketch pipeline: trigger quantumsketch.app render jobs from within agent workflows
The pattern that works: one MCP server per capability domain. Don't build a monolithic server with 30 tools — agents get confused by large tool lists. Keep it to 5–8 tools per server.
Debugging MCP Connections
Use the MCP Inspector for local debugging:
npx @modelcontextprotocol/inspector node dist/index.js
This opens a UI where you can call tools manually, inspect the JSON-RPC traffic, and validate your schema definitions before connecting a real agent.
For production tracing, log every tools/call with its arguments and duration. MCP calls are the most common source of latency surprises in agentic systems.
FAQ
What's the difference between MCP and function calling? Function calling is model-specific (OpenAI, Anthropic each have their own format). MCP is a transport-level standard — the agent translates your MCP tools into the appropriate function-calling format automatically.
Can multiple agents share one MCP server? Yes. MCP servers are stateless by design. Multiple agents can connect simultaneously; each session is independent.
Does MCP work with open-source models? Any model with function/tool calling support can use MCP. Ollama, LM Studio, and most OpenAI-compatible servers have MCP client support in 2026.
How do I handle long-running tools?
Return an immediate acknowledgment, then use a separate poll_status tool to check completion. MCP doesn't have native async — model the async yourself with job IDs.
What's the performance overhead of MCP? For stdio transport: negligible (in-process). For HTTP transport: 1–5ms round-trip on the same network. The bottleneck is always the tool itself, not the protocol.