Skip to content

AI Integration · AI Standards

Model Context Protocol in Production: How MCP Is Connecting the AI Tool Ecosystem

MCP has become the standard for connecting AI models to external systems. Here's how it works, how to implement an MCP server, and what to watch out for before going to production.

Anurag Verma

Anurag Verma

7 min read

Model Context Protocol in Production: How MCP Is Connecting the AI Tool Ecosystem

Sponsored

Share

A year ago, every AI integration was a bespoke implementation. You wrote a function, the AI could call it, and the wiring was buried inside a single codebase. The next model, the next tool, the next team — all starting from scratch. Model Context Protocol (MCP) was designed to end that duplication, and in 2026, it’s become the closest thing the AI tooling space has to a shared standard.

MCP defines a common interface for connecting an AI model to external resources: files, databases, APIs, and services. Once you write an MCP server for your Postgres database, any MCP-compatible AI client can use it without modification. The protocol separates the AI model from the integration layer.

What MCP Actually Is

MCP is a client-server protocol built on JSON-RPC 2.0. It runs over a transport layer, originally stdio (for local tools) and HTTP with server-sent events (for remote services). The spec defines three primitives:

Tools — functions the AI model can call. A query_database tool that takes a SQL string and returns results. A send_email tool that takes a recipient and body.

Resources — data the model can read. A file system resource exposes the contents of files. A documentation resource exposes structured text. Resources give the model context without requiring tool calls.

Prompts — reusable templates exposed by the server. A code review prompt, a summarization prompt. The client can list available prompts and use them.

The model doesn’t directly call your database or API. It calls MCP tools, the MCP server handles the actual implementation, and the result comes back through the protocol. The security boundary is at the MCP server, not inside the prompt.

Implementing an MCP Server

The reference implementation is in TypeScript. Install the SDK:

npm install @modelcontextprotocol/sdk

A minimal server exposing a tool:

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

const server = new Server(
  { name: 'my-database-server', version: '1.0.0' },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: 'query_users',
      description: 'Query users by email or ID',
      inputSchema: {
        type: 'object',
        properties: {
          email: { type: 'string', description: 'Email to search' },
          limit: { type: 'number', description: 'Max results', default: 10 },
        },
        required: [],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === 'query_users') {
    const { email, limit = 10 } = request.params.arguments as {
      email?: string;
      limit?: number;
    };

    // Your actual database query here
    const users = await db.query(
      `SELECT id, email, name, created_at FROM users
       WHERE ($1::text IS NULL OR email ILIKE $1)
       LIMIT $2`,
      [email ? `%${email}%` : null, limit]
    );

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(users.rows, null, 2),
        },
      ],
    };
  }

  throw new Error(`Unknown tool: ${request.params.name}`);
});

const transport = new StdioServerTransport();
await server.connect(transport);

Run this as a subprocess, and any MCP client can discover and call your query_users tool.

Exposing Resources

Tools are for actions. Resources are for data the model should have in context. Expose a resource that gives the model your API documentation:

import { ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [
    {
      uri: 'docs://api/overview',
      name: 'API Overview',
      mimeType: 'text/markdown',
    },
    {
      uri: 'docs://api/endpoints',
      name: 'API Endpoints Reference',
      mimeType: 'text/markdown',
    },
  ],
}));

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  if (uri === 'docs://api/overview') {
    const content = await fs.readFile('./docs/api-overview.md', 'utf-8');
    return {
      contents: [{ uri, mimeType: 'text/markdown', text: content }],
    };
  }

  throw new Error(`Unknown resource: ${uri}`);
});

When the AI client loads this resource into context before responding to a user question about your API, it has accurate documentation without the model needing to guess.

HTTP Transport for Remote Servers

Stdio transport works for local tools running on the same machine as the client. For remote services, MCP supports HTTP with server-sent events. A minimal Express implementation:

import express from 'express';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';

const app = express();
const transports = new Map<string, SSEServerTransport>();

app.get('/mcp', async (req, res) => {
  const transport = new SSEServerTransport('/mcp/messages', res);
  const sessionId = crypto.randomUUID();
  transports.set(sessionId, transport);

  const server = createServer(); // your server factory
  await server.connect(transport);

  res.on('close', () => {
    transports.delete(sessionId);
  });
});

app.post('/mcp/messages', express.json(), async (req, res) => {
  const sessionId = req.headers['x-session-id'] as string;
  const transport = transports.get(sessionId);
  if (!transport) {
    res.status(404).json({ error: 'Session not found' });
    return;
  }
  await transport.handlePostMessage(req, res);
});

app.listen(3000);

The client opens an SSE connection to /mcp, gets a session, and sends messages to /mcp/messages. Each session has its own server instance.

Python Server

The Python SDK covers the same surface area:

import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types

server = Server("my-server")

@server.list_tools()
async def list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="run_query",
            description="Run a read-only SQL query against the analytics database",
            inputSchema={
                "type": "object",
                "properties": {
                    "sql": {"type": "string"},
                },
                "required": ["sql"],
            },
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    if name == "run_query":
        sql = arguments["sql"]
        # Validate: only allow SELECT
        if not sql.strip().upper().startswith("SELECT"):
            raise ValueError("Only SELECT queries are allowed")
        results = await db.fetch(sql)
        return [types.TextContent(type="text", text=str(results))]
    raise ValueError(f"Unknown tool: {name}")

async def main():
    async with stdio_server() as streams:
        await server.run(*streams, server.create_initialization_options())

asyncio.run(main())

Security Considerations Before Going to Production

MCP servers are trust boundaries. The AI model determines what to call and with what arguments, but the server controls what actually executes. A few things to lock down:

Validate inputs. The model can construct any valid JSON. If your query_users tool takes a SQL string and passes it directly to the database, you have SQL injection from AI-generated queries. Validate, parameterize, and use allowlists.

Scope permissions tightly. An MCP server for reading user data should connect to the database with a read-only role. An MCP server for sending emails should have rate limits baked in, not rely on the model to not abuse the capability.

Log everything. Every tool call and result is an audit trail. If a model hallucinates bad parameters or an MCP tool is called with unexpected arguments, you need to know. The mcpToolCall event is where to hook your logging.

Authentication for remote servers. HTTP transport MCP servers need to verify that the connecting client is authorized. OAuth 2.0 is the spec-recommended approach. At minimum, bearer token validation on the SSE endpoint.

What to Implement First

If you’re adding MCP support to an existing application, the highest-value servers are:

  1. Read-only database queries — the model can answer questions about your data without you writing 20 endpoints
  2. Search — semantic or keyword search over your content, with the model interpreting results
  3. File and document access — your internal docs, runbooks, and specifications as resources

Tool servers that take actions (write, send, update) need more careful scoping and should come after you’ve established what the model does with read-only access.

The ecosystem of pre-built MCP servers (for GitHub, Slack, databases, filesystems) has grown fast enough that you often don’t need to write one from scratch. Check the registry before building. When you do write your own, keep the surface area small: a server with 3 well-scoped tools is safer and easier to debug than one with 20 general-purpose capabilities.

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored