MCP Server for Claude and Cursor: Build, Connect, and Deploy
TL;DR — Build MCP servers for Claude Desktop, Claude Code, and Cursor using the same JSON config format. Python: use FastMCP (
pip install mcp) — decorators auto-generate tool schemas from type hints. TypeScript: use@modelcontextprotocol/sdkwith Zod for input validation. Two transports: stdio (local subprocess, zero config, best for dev) and Streamable HTTP (remote, multi-user, SSO, production). Config:~/Library/Application Support/Claude/claude_desktop_config.json(macOS) or%APPDATA%/Claude/claude_desktop_config.json(Windows). Same format works for Cursor and Claude Code. 13,000+ MCP servers in public registry, TypeScript SDK 150M+ downloads. Production: OAuth 2.1 auth, rate limiting, audit logging, input validation, environment variables for credentials. Test with MCP Inspector (npx @modelcontextprotocol/inspector). Deploy behind Nginx/Caddy with sticky sessions for stateful Streamable HTTP. Connect to self-hosted AI knowledge base for enterprise tool access. See MCP security for hardening.
The MCP ecosystem has grown to 13,000+ servers with the TypeScript SDK surpassing 150M downloads. OpenAI, Google, and Microsoft now ship first-party MCP clients. The same server works across Claude Desktop, Claude Code, Cursor, Windsurf, and VS Code — write once, use everywhere.
This guide covers building MCP servers in Python and TypeScript, connecting them to Claude and Cursor, and deploying to production.
MCP Server Architecture
or .cursor/mcp.json"] end subgraph Transport["Transport Layer"] Stdio["stdio
(local subprocess)"] HTTP["Streamable HTTP
(remote service)"] end subgraph Server["MCP Server"] Tools["Tools
(functions)"] Resources["Resources
(data)"] Prompts["Prompts
(templates)"] end subgraph Backend["Enterprise Backend"] KB["Knowledge Base
(pgvector)"] Graph["FalkorDB
(graph)"] API["Internal APIs"] end Claude --> JSON Code --> JSON Cursor --> JSON JSON --> Stdio JSON --> HTTP Stdio --> Server HTTP --> Server Server --> Tools Server --> Resources Server --> Prompts Tools --> KB Tools --> Graph Tools --> API
Python Server with FastMCP
from mcp.server.fastmcp import FastMCP
import asyncpg
import json
mcp = FastMCP("enterprise-kb")
@mcp.tool()
async def search_knowledge_base(query: str, limit: int = 10) -> str:
"""Search the enterprise knowledge base using semantic search.
Args:
query: Natural language search query
limit: Maximum number of results (default 10)
Returns:
JSON array of matching documents with title, content, and source
"""
conn = await asyncpg.connect(os.environ["DATABASE_URL"])
results = await conn.fetch(
"""SELECT title, content, source,
1 - (embedding <=> $1) AS similarity
FROM documents
ORDER BY embedding <=> $1
LIMIT $2""",
await compute_embedding(query), limit
)
await conn.close()
return json.dumps([dict(r) for r in results])
@mcp.tool()
async def get_document(doc_id: str) -> str:
"""Get full document content by ID."""
conn = await asyncpg.connect(os.environ["DATABASE_URL"])
doc = await conn.fetchrow(
"SELECT title, content, source FROM documents WHERE id = $1", doc_id
)
await conn.close()
return json.dumps(dict(doc)) if doc else "Document not found"
@mcp.tool()
async def query_graph(cypher: str) -> str:
"""Query the FalkorDB knowledge graph with Cypher.
Args:
cypher: Cypher query string
"""
# Connect to FalkorDB and execute query
results = await falkordb_query(cypher)
return json.dumps(results)
@mcp.resource("kb://stats")
async def kb_stats() -> str:
"""Get knowledge base statistics."""
conn = await asyncpg.connect(os.environ["DATABASE_URL"])
count = await conn.fetchval("SELECT count(*) FROM documents")
await conn.close()
return json.dumps({"total_documents": count})
if __name__ == "__main__":
# stdio for local, streamable-http for remote
mcp.run() # Default: stdio
# mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
TypeScript Server with MCP SDK
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: "enterprise-kb",
version: "1.0.0",
});
server.tool(
"search_knowledge_base",
{
query: z.string().describe("Natural language search query"),
limit: z.number().default(10).describe("Max results"),
},
async ({ query, limit }) => {
const results = await searchPgvector(query, limit);
return {
content: [{ type: "text", text: JSON.stringify(results) }],
};
}
);
server.tool(
"get_document",
{
doc_id: z.string().describe("Document UUID"),
},
async ({ doc_id }) => {
const doc = await getDocById(doc_id);
return {
content: [{ type: "text", text: doc ? JSON.stringify(doc) : "Not found" }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Client Configuration
Claude Desktop (macOS)
// ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"enterprise-kb": {
"command": "python",
"args": ["/path/to/server.py"],
"env": {
"DATABASE_URL": "postgresql://user:pass@localhost/db",
"FALKORDB_URL": "redis://localhost:6379"
}
}
}
}
Cursor IDE
// .cursor/mcp.json (project-level) or ~/.cursor/mcp.json (global)
{
"mcpServers": {
"enterprise-kb": {
"command": "python",
"args": ["/path/to/server.py"],
"env": {
"DATABASE_URL": "postgresql://user:pass@localhost/db"
}
}
}
}
Remote Server (Streamable HTTP)
{
"mcpServers": {
"enterprise-kb": {
"url": "https://api.internal.company.com/mcp",
"headers": {
"Authorization": "Bearer TOKEN"
}
}
}
}
Transport Comparison
| Feature | stdio | Streamable HTTP |
|---|---|---|
| Use case | Local dev, single user | Remote, multi-user, production |
| Network | None (subprocess) | HTTP endpoint (/mcp) |
| Config | command + args | url + headers |
| Auth | Environment variables | OAuth 2.1, Bearer token |
| Multi-user | ❌ | ✅ |
| Serverless | ❌ | ✅ (stateless mode) |
| Scaling | One per client | Load balancer + sticky sessions |
| Setup complexity | Minimal | Moderate (reverse proxy, TLS) |
| Spec version | Original | Nov 2025 (replaced HTTP+SSE) |
Production Deployment
# docker-compose.yml — MCP server production
services:
mcp-server:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://postgres:pass@postgres:5432/aidb
- FALKORDB_URL=redis://falkordb:6379
- OAUTH_ISSUER=https://auth.company.com
restart: always
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
nginx:
image: nginx:alpine
ports:
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- mcp-server
MCP Server Development Checklist
- [ ] Choose language: Python (FastMCP, fastest prototyping) or TypeScript (SDK, strict types)
- [ ] Install SDK:
pip install mcp(Python) ornpm install @modelcontextprotocol/sdk zod(TypeScript) - [ ] Define tools with input schemas (type hints in Python, Zod in TypeScript)
- [ ] Write clear docstrings/descriptions — the AI reads these to understand tools
- [ ] Use environment variables for all credentials (never hardcode)
- [ ] Start with stdio transport for local development
- [ ] Test with MCP Inspector:
npx @modelcontextprotocol/inspector - [ ] Verify tool discovery: Inspector shows all tools with schemas
- [ ] Test tool execution: call each tool with valid and invalid inputs
- [ ] Add input validation: Zod (TypeScript) or Pydantic (Python)
- [ ] Configure Claude Desktop: edit
claude_desktop_config.json - [ ] Configure Cursor: edit
.cursor/mcp.json - [ ] Restart host application after config changes
- [ ] Verify connection: host shows server as connected
- [ ] Test from host: ask AI to use your tools
- [ ] Switch to Streamable HTTP for remote/production deployment
- [ ] Set up OAuth 2.1 authentication for HTTP transport
- [ ] Configure Nginx reverse proxy with TLS
- [ ] Implement rate limiting per client
- [ ] Add audit logging: who called what tool, when, with what result
- [ ] Deploy with Docker Compose
- [ ] Connect to pgvector for knowledge base search
- [ ] Connect to FalkorDB for graph queries
- [ ] Expose secure ingestion as MCP tools
- [ ] Implement MCP security best practices
- [ ] Use sticky sessions if deploying multiple instances (stateful HTTP)
- [ ] Consider stateless mode (
stateless_http=True) for serverless - [ ] Log to stderr only (stdout corrupts JSON-RPC stream in stdio mode)
- [ ] Monitor server health with platform monitoring
- [ ] Document available tools and resources for your team
- [ ] Consider agentic AI governance for tool permissions
- [ ] Test with multiple MCP servers connected simultaneously
- [ ] Evaluate self-hosted AI integration
FAQ
How do you connect an MCP server to Claude Desktop?
Add the server to Claude Desktop's config file. On macOS: ~/Library/Application Support/Claude/claude_desktop_config.json. On Windows: %APPDATA%/Claude/claude_desktop_config.json. The config format: {"mcpServers": {"my-server": {"command": "python", "args": ["server.py"]}}}. For remote servers: {"mcpServers": {"my-server": {"url": "https://api.example.com/mcp", "headers": {"Authorization": "Bearer TOKEN"}}}}. Restart Claude Desktop after editing. The same JSON format works for Claude Code and Cursor as of 2026.
How do you build an MCP server in Python?
Use FastMCP from the official mcp package. Install: pip install mcp. Create a server with decorators: from mcp.server.fastmcp import FastMCP; mcp = FastMCP('my-server'); @mcp.tool(); def search(query: str, limit: int = 10) -> str: '''Search the knowledge base'''; return results. Run: mcp.run() for stdio or mcp.run(transport='streamable-http', host='0.0.0.0', port=8000) for remote. FastMCP auto-generates JSON Schema from type hints and docstrings. No manual schema writing needed.
How do you build an MCP server in TypeScript?
Use the official @modelcontextprotocol/sdk package. Install: npm install @modelcontextprotocol/sdk zod. Create server with McpServer class, define tools with Zod schemas for input validation. The SDK auto-generates JSON Schema from Zod definitions. Connect StdioServerTransport for local or StreamableHTTPServerTransport for remote. Use ESM modules: set "type": "module" in package.json and moduleResolution: "Node16" in tsconfig.json. Log to stderr only — stdout corrupts JSON-RPC stream in stdio mode.
What is the difference between stdio and Streamable HTTP transport?
stdio transport runs the MCP server as a subprocess of the client (Claude Desktop, Cursor). Zero network config, no port to expose, simplest setup. Best for local development and single-user scenarios. Streamable HTTP (replaced HTTP+SSE in Nov 2025 spec) runs the server as a network service on a single endpoint (/mcp). Supports remote access, multi-user, SSO, and serverless deployment. Best for production, team-shared servers, and cloud deployment. Rule: start with stdio, switch to Streamable HTTP when you need remote access or multi-user.
How do you secure an MCP server for production?
Secure MCP servers with: (1) OAuth 2.1 for Streamable HTTP transport — use Authorization: Bearer header, scope-based per-tool permissions, short-lived tokens. (2) Input validation with Zod (TypeScript) or Pydantic (Python) — never trust AI-generated inputs. (3) Rate limiting per client to prevent abuse. (4) Audit logging of all tool calls (who, what, when, result). (5) Environment variables for credentials — never hardcode API keys. (6) Network isolation — bind to localhost for stdio, use reverse proxy (Nginx/Caddy) for HTTP. (7) Tool allowlisting — host controls which tools the AI can call. See MCP security guide for details.
Want a self-hosted AI company brain that does all of this out of the box?
Book a demo →