MCP Server Implementation
Purpose
Practical implementation of a Model Context Protocol (MCP) server that exposes tools, resources, and prompts to AI coding assistants (Copilot, Claude, Cursor). MCP provides a standardised way for LLMs to call functions, read resources, and use prompt templates. Synthesized from: MCP Protocol, Agentic Coding, Repo RAG for Code.
Examples
Install the Python MCP SDK:
pip install mcpMinimal Python MCP server with tools:
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent, Resource
from pydantic import BaseModel
import asyncio, subprocess, pathlib, json
server = Server("code-assistant")
# ─── Tool definitions ─────────────────────────────────────────────────────────
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="read_file",
description="Read a file from the repository",
inputSchema={
"type": "object",
"properties": {
"path": {"type": "string", "description": "Relative path from repo root"},
},
"required": ["path"],
},
),
Tool(
name="run_tests",
description="Run pytest for a given test path",
inputSchema={
"type": "object",
"properties": {
"path": {"type": "string", "default": "tests/"},
},
},
),
Tool(
name="search_code",
description="Search codebase for a pattern using ripgrep",
inputSchema={
"type": "object",
"properties": {
"pattern": {"type": "string"},
"file_pattern": {"type": "string", "default": "*.py"},
},
"required": ["pattern"],
},
),
]
# ─── Tool implementations ─────────────────────────────────────────────────────
REPO_ROOT = pathlib.Path("/workspace")
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "read_file":
path = REPO_ROOT / arguments["path"]
# Sandbox: resolve and verify it stays within REPO_ROOT
resolved = path.resolve()
if not str(resolved).startswith(str(REPO_ROOT.resolve())):
return [TextContent(type="text", text="Error: path traversal denied")]
if not resolved.exists():
return [TextContent(type="text", text=f"File not found: {arguments['path']}")]
return [TextContent(type="text", text=resolved.read_text(errors="replace"))]
if name == "run_tests":
result = subprocess.run(
["python", "-m", "pytest", arguments.get("path", "tests/"), "--tb=short", "-q"],
cwd=REPO_ROOT, capture_output=True, text=True, timeout=120,
)
output = result.stdout + result.stderr
return [TextContent(type="text", text=output)]
if name == "search_code":
result = subprocess.run(
["rg", "--glob", arguments.get("file_pattern", "*.py"),
"-n", arguments["pattern"], str(REPO_ROOT)],
capture_output=True, text=True, timeout=30,
)
return [TextContent(type="text", text=result.stdout or "No matches")]
return [TextContent(type="text", text=f"Unknown tool: {name}")]
# ─── Resources ────────────────────────────────────────────────────────────────
@server.list_resources()
async def list_resources() -> list[Resource]:
return [
Resource(
uri="repo://structure",
name="Repository Structure",
description="Directory tree of the repository",
mimeType="text/plain",
),
]
@server.read_resource()
async def read_resource(uri: str) -> str:
if uri == "repo://structure":
result = subprocess.run(
["find", str(REPO_ROOT), "-name", "*.py", "-not", "-path", "*/.*"],
capture_output=True, text=True
)
return result.stdout
raise ValueError(f"Unknown resource: {uri}")
# ─── Entry point ──────────────────────────────────────────────────────────────
async def main():
async with stdio_server() as streams:
await server.run(*streams, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())Register with Claude Desktop (.claude/config.json):
{
"mcpServers": {
"code-assistant": {
"command": "python",
"args": ["/workspace/.mcp/server.py"],
"env": {}
}
}
}TypeScript MCP server (for Node.js-based tools):
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: "ts-code-assistant", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: "lint",
description: "Run ESLint on a file",
inputSchema: {
type: "object",
properties: { path: { type: "string" } },
required: ["path"],
},
}],
}));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args } = req.params;
if (name === "lint") {
const { execSync } = await import("child_process");
try {
const out = execSync(`npx eslint ${args!.path} --format compact`).toString();
return { content: [{ type: "text", text: out }] };
} catch (e: any) {
return { content: [{ type: "text", text: e.stdout?.toString() ?? "Error" }] };
}
}
throw new Error(`Unknown tool: ${name}`);
});
const transport = new StdioServerTransport();
await server.connect(transport);Architecture
AI assistant (Claude / Copilot / Cursor)
│
│ stdio or SSE transport (JSON-RPC 2.0)
▼
MCP Server
├── Tools — callable functions (read_file, run_tests, search)
├── Resources — readable content (repo structure, docs)
└── Prompts — prompt templates with arguments
Security boundary:
Server enforces path sandboxing, rate limiting, and command allowlist
All file access validated against REPO_ROOT before execution
Key design decisions:
- Use
stdiotransport for local tools (simpler, no port management) - Use SSE transport (
mcp.server.sse) for remote/multi-user servers - Always sandbox file system access — validate paths with
Path.resolve()+ prefix check - Keep tool implementations stateless where possible for safety