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
| Error | Cause | Fix |
|---|---|---|
| Server not listed | Bad path in mcp.json | Use absolute path; run script manually |
| Tool not called | Vague prompt | Explicitly name the tool |
| Empty response | Validation or runtime error | Check inspector stderr |
| Connection closed | Crash on startup | Run python mcp_internal_docs.py in terminal |
| Secrets in repo | Hardcoded credentials | Use ${env:VAR} only |
Design Guidelines
| Do | Don't |
|---|---|
| Small, focused tools with clear names | One mega-tool that does everything |
| Validate inputs with schemas (Zod) | Pass raw user strings to SQL |
| Return structured JSON text | Return megabytes of data |
| Read-only tools for exploration | Destructive ops without confirmation |
| Log tool calls for audit | Expose production write access casually |
Testing Locally
- Run the server standalone; verify tools with MCP inspector or client CLI
- Connect one tool in Cursor; test with a simple Ask prompt
- Expand to production data only after read-only validation
Popular MCP Use Cases
- Internal API docs search
- Staging database read queries
- GitHub / Linear / Jira issue lookup
- Run approved scripts (lint, test) with fixed arguments
For Teams
| Policy | Recommendation |
|---|---|
| Approved servers | Maintain an allowlist in Team AI Policy — no personal MCP servers on production repos |
| Secrets | Inject via env vars; audit mcp.json in PR review |
| Write access | Read-only tools first; destructive tools require human confirmation in tool design |
| Logging | Log tool name, args hash, and timestamp — not full PII payloads |
| Data scope | Staging DB only unless explicitly approved |
Related
- MCP Server Tutorial — full walkthrough
- MCP Security — auth, secrets, team policy
- MCP Config Cheat Sheet — mcp.json reference
- MCP Quick Reference — config JSON, tool schemas, test commands
- RAG for Codebases — MCP as lightweight retrieval
- Context Engineering
- LLM APIs
- Security Anti-patterns