MCP in Practice: Connecting LLMs to Real Systems
Every team that ships an LLM feature eventually builds the same thing: a pile of glue code that lets the model call internal APIs, query databases, and read documents. Every pile is slightly different, none of it is reusable, and integrating the next model means rewriting half of it.
The Model Context Protocol (MCP) exists to end that. It's an open standard that defines how AI applications discover and call external capabilities — and after building several integrations with it, I think it's one of the most quietly important pieces of the current AI stack.
The Problem MCP Solves
Classic function calling couples three things that should be independent:
- —The model (Claude, GPT, Gemini, an open-source model)
- —The application hosting the conversation
- —The capability (your CRM API, your database, your file store)
Without a standard, each application hand-wires each capability for each model. That's an N×M integration matrix, and you pay for it on every change.
MCP cuts the matrix: capabilities live in MCP servers, applications act as MCP clients, and any client can talk to any server. Write the server once; every MCP-capable host — Claude Desktop, IDEs, your own app — can use it.
The Core Concepts
An MCP server exposes three kinds of things:
- —Tools — functions the model can invoke ("create invoice", "query orders"). The model decides when to call them.
- —Resources — data the application can attach as context (a file, a database schema, a dashboard snapshot). The application controls these.
- —Prompts — reusable, parameterized prompt templates the server offers to users.
That tools/resources split matters more than it looks: tools are model-driven actions, resources are app-driven context. Keeping them separate keeps the security story sane.
A Minimal Server
Here's the shape of a real server in TypeScript:
1import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2import { z } from "zod"; 3 4const server = new McpServer({ name: "orders", version: "1.0.0" }); 5 6server.tool( 7 "lookup_order", 8 "Look up an order by ID and return its status and items", 9 { orderId: z.string().describe("The order ID, e.g. ORD-1042") }, 10 async ({ orderId }) => { 11 const order = await db.orders.findById(orderId); 12 if (!order) { 13 return { content: [{ type: "text", text: "No order " + orderId }] }; 14 } 15 return { 16 content: [{ type: "text", text: JSON.stringify(order, null, 2) }], 17 }; 18 } 19);
A few dozen lines, and suddenly the same "lookup_order" capability works from Claude Desktop, from an agent framework, and from the customer-support chatbot you'll build next quarter.
What I've Learned Shipping MCP Integrations
1. Tool descriptions are prompts. The model picks tools based on names and descriptions. "Query the orders database" is bad; "Look up a single order by its ID and return status, items, and shipping info — use when the user references a specific order" is good. Iterate on descriptions like you iterate on prompts, because that's what they are.
2. Few good tools beat many thin ones. Twenty granular CRUD tools force the model to plan multi-step workflows and multiply failure points. One search_orders with rich filters outperforms five narrow lookups.
3. Return errors the model can act on. A raw stack trace is useless to an LLM. "Order not found — IDs look like ORD-XXXX; ask the user to confirm" lets the conversation recover gracefully.
4. Treat the server boundary as a security boundary. The model is untrusted input. Scope database credentials to read-only where possible, allowlist what each tool can touch, require human confirmation for destructive actions, and log every call with its arguments. Prompt-injected instructions will eventually reach your tools; design assuming it.
5. Keep payloads small. Returning 200 KB of JSON blows the context budget and buries the signal. Summarize, paginate, and let the model ask for more.
When Not to Use MCP
- —A single hard-coded integration inside one app, with no reuse in sight — direct function calling is less machinery.
- —Hot paths where the extra hop matters — sub-100ms budgets don't want a protocol layer.
- —Workflows that are fully deterministic — if no decision is needed, you don't need a model in the loop at all. Write normal code.
Closing Thought
The interesting shift with MCP isn't technical, it's organizational: capabilities become products. A team can own the "billing MCP server" the way they own a REST API today — versioned, documented, reused by every AI surface in the company. That's what a maturing ecosystem looks like.