1. ai
  2. /building
  3. /mcp-servers

Building MCP Servers

MCP (Model Context Protocol) lets AI agents call your tools — query a database, file a ticket, fetch internal docs — through a standard interface.

Last reviewed: June 2026

MCP SDK APIs evolve. Check modelcontextprotocol.io and the MCP specification for the latest server templates.

MCP Anatomy

┌─────────────┐     JSON-RPC      ┌─────────────┐
│  AI Client  │ ◄──────────────► │ MCP Server  │
│ (Cursor)    │   tools/resources │ (your code) │
└─────────────┘                   └──────┬──────┘

                                    DB, API, FS
  • Tools — functions the model can invoke (query_users, create_ticket)
  • Resources — read-only data (file://, doc://)
  • Prompts — reusable prompt templates (optional)

Official intro: MCP documentation.

TypeScript Server Skeleton

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "my-dev-tools", version: "1.0.0" });

server.tool(
  "lookup_user",
  { email: z.string().email() },
  async ({ email }) => {
    const user = await db.users.findByEmail(email);
    return {
      content: [{ type: "text", text: JSON.stringify(user, null, 2) }],
    };
  }
);

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

Run via stdio — the client spawns the process and communicates over stdin/stdout.

Python Server Skeleton

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("my-dev-tools")

@mcp.tool()
def lookup_user(email: str) -> str:
    """Look up a user by email in the staging database."""
    user = db.find_user(email)
    return json.dumps(user)

if __name__ == "__main__":
    mcp.run()

Complete Minimal Server (TypeScript)

Self-contained example you can copy into mcp-server/index.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const DOCS: Record<string, string> = {
  "auth": "OAuth 2.0 with PKCE. Tokens expire in 1 hour.",
  "billing": "Stripe webhooks on /api/webhooks/stripe.",
};

const server = new McpServer({ name: "internal-docs", version: "1.0.0" });

server.tool(
  "search_docs",
  { query: z.string().min(1) },
  async ({ query }) => {
    const q = query.toLowerCase();
    const hits = Object.entries(DOCS)
      .filter(([k, v]) => k.includes(q) || v.toLowerCase().includes(q))
      .map(([k, v]) => `## ${k}\n${v}`);
    return {
      content: [{
        type: "text",
        text: hits.length ? hits.join("\n\n") : "No matching docs.",
      }],
    };
  }
);

await server.connect(new StdioServerTransport());

Save the file as mcp-server/internal-docs.ts, or use the Python skeleton below (no compile step).

Step-by-step: MCP Server Tutorial.

Cursor Configuration

Add to Cursor Settings → MCP (or .cursor/mcp.json):

{
  "mcpServers": {
    "internal-docs": {
      "command": "python",
      "args": ["/absolute/path/to/mcp_internal_docs.py"]
    }
  }
}

With secrets via environment:

{
  "mcpServers": {
    "my-dev-tools": {
      "command": "python",
      "args": ["/absolute/path/to/your_server.py"],
      "env": {
        "DATABASE_URL": "${env:DATABASE_URL}"
      }
    }
  }
}

Never commit secrets — use environment variables. Cursor MCP docs: Model Context Protocol in Cursor.

End-to-End Tutorial

1. Create the server

Use the MCP Server Tutorial — a single Python file with pip install mcp, no separate npm project or compile step. TypeScript teams can use the skeletons above with their existing bundler if preferred.

2. Configure Cursor

Add server to .cursor/mcp.json at repo root. Restart Cursor or reload MCP from settings.

Example for Python:

{
  "mcpServers": {
    "internal-docs": {
      "command": "python",
      "args": ["/absolute/path/to/mcp_internal_docs.py"]
    }
  }
}

3. Test with MCP Inspector

npx @modelcontextprotocol/inspector python /absolute/path/to/mcp_internal_docs.py

Open the inspector UI, list tools, invoke search_docs with query: "auth". Inspector repo: MCP Inspector.

4. Test in Cursor Ask mode

Use the search_docs tool to find how OAuth is configured.
Do not guess — call the tool first.

5. Debug common errors

ErrorCauseFix
Server not listedBad path in mcp.jsonUse absolute path; run script manually
Tool not calledVague promptExplicitly name the tool
Empty responseValidation or runtime errorCheck inspector stderr
Connection closedCrash on startupRun python mcp_internal_docs.py in terminal
Secrets in repoHardcoded credentialsUse ${env:VAR} only

Design Guidelines

DoDon't
Small, focused tools with clear namesOne mega-tool that does everything
Validate inputs with schemas (Zod)Pass raw user strings to SQL
Return structured JSON textReturn megabytes of data
Read-only tools for explorationDestructive ops without confirmation
Log tool calls for auditExpose production write access casually

Testing Locally

  1. Run the server standalone; verify tools with MCP inspector or client CLI
  2. Connect one tool in Cursor; test with a simple Ask prompt
  3. Expand to production data only after read-only validation
  • Internal API docs search
  • Staging database read queries
  • GitHub / Linear / Jira issue lookup
  • Run approved scripts (lint, test) with fixed arguments

For Teams

PolicyRecommendation
Approved serversMaintain an allowlist in Team AI Policy — no personal MCP servers on production repos
SecretsInject via env vars; audit mcp.json in PR review
Write accessRead-only tools first; destructive tools require human confirmation in tool design
LoggingLog tool name, args hash, and timestamp — not full PII payloads
Data scopeStaging DB only unless explicitly approved