MCP Server
с нуля
Platform-инженеру, которому нужно отдать агенту доступ к internal API без N×M обёрток под каждый Host, этот текст даёт полный стек: Model Context Protocol поверх JSON-RPC 2.0, реализация на Python/FastMCP, stdio → streamable HTTP, asyncio handlers, structured errors, ChromaDB knowledge base. Внутри — wire-level разбор транспортов, матрица REST vs MCP, семь шагов HowTo, три метрики производительности и финальный блок про bare-metal Apple Silicon для macOS-only toolchains (Keychain, TCC, osascript).
Оглавление
01 · Зачем писать свой MCP Server
Готовые server-filesystem/postgres/github не знают ваш entitlement layer, billing rules и redaction policy. Custom MCP — тонкая façade: 3–7 операций с JSON Schema, scoped tokens, audit log каждого tools/call. Паттерн 2026: REST внутри, MCP снаружи.
Протокол — в обзоре MCP как HTTP эпохи ИИ; здесь — implementation depth.
02 · Три системных боли
1. N×M интеграций. Cursor, Claude Desktop, CI — три адаптера на один «lookup order». MCP коллапсирует surface в один процесс/endpoint.
2. Privilege inheritance. stdio-server — дочерний процесс Host с UID пользователя, полный доступ к ~/.ssh, Keychain items, env с production tokens.
3. Opaque failure modes. Любой байт на stdout ломает JSON-RPC framing; sync blocking IO даёт timeout без stack trace в Host UI.
03 · Wire protocol: Host / Client / Server
Host (Cursor) ──spawn──▶ MCP Server (stdio)
│ JSON-RPC 2.0 frames on stdin/stdout
│ initialize → capabilities negotiate
│ tools/list → JSON Schema[]
└ tools/call(name, args) → structured content
stdio: subprocess, zero network attack surface — идеален для local dev. Streamable HTTP: single endpoint, bidirectional JSON-RPC, OAuth 2.1 на edge — default для team deploy. Legacy SSE уходит в 2025–2026 миграциях.
{ "jsonrpc": "2.0", "id": 42, "method": "tools/call", "params": { "name": "lookup_account", "arguments": { "email": "dev@example.com" } } }
04 · Матрица: REST vs Function Calling vs MCP
| Ось | REST | Function Calling | MCP |
|---|---|---|---|
| Discovery | OpenAPI (human) | Embedded in prompt | tools/list machine-readable |
| Session | Stateless | Per-request ad hoc | Capability negotiation |
| Latency overhead | Baseline HTTP | In-process | +JSON-RPC frame (~µs stdio) |
| Metal/macOS tools | N/A | Host-specific | stdio on bare Mac |
Метрика 1: FastMCP сокращает boilerplate на 60–70 % vs raw SDK (five-tool benchmark). Метрика 2: повторные ops tasks — −38…−55 % input tokens после замены pasted API docs. Метрика 3: idle stdio process 50–120 MB RSS; budget 256 MB в prod.
05 · Окружение: Python 3.11+ и uv
mkdir my-mcp-server && cd my-mcp-serveruv init && uv venv && source .venv/bin/activateuv add "mcp[cli]" fastmcp httpx pydantic chromadbmcp --help
На macOS system Python устаревший — Homebrew/pyenv. Node 20+ для Inspector. Working server за <20 мин vs ~90 мин manual schema wiring.
06 · Hello World: stdio framing
from fastmcp import FastMCPmcp = FastMCP("hello-mcp")@mcp.tool()def greet(name: str) -> str: return f"Hello, {name}"if __name__ == "__main__": mcp.run()
Cursor config: absolute path к venv python + server.py. Failures: stderr pollution, wrong interpreter без fastmcp in site-packages.
07 · Tools: sync, async, guardrails
| Pattern | Handler | Constraint |
|---|---|---|
| lookup_account | sync | email validation |
| fetch_weather | async httpx | timeout 10s |
| query_tickets | async pg | LIMIT ≤50 |
| write_note | sync Path | resolve + prefix check |
| summarize_metrics | async gather | ≤10 service_ids |
from mcp.server.fastmcp.exceptions import ToolError@mcp.tool()async def create_refund(ticket_id: str, cents: int) -> str: try: return (await billing.preview(ticket_id, cents)).summary except billing.NotFound: raise ToolError(f"{ticket_id} not found")
Cap tools at 10–15; >20 — measurable wrong-tool rate в agent logs.
08 · Resources: read-only URI layer
@mcp.resource("kb://doc/{path:path}")def kb_doc(path: str) -> str: full = (DOCS_ROOT / path).resolve() if not str(full).startswith(str(DOCS_ROOT)): raise PermissionError return full.read_text()
Hosts часто auto-fetch resources без approval — zero secrets, zero raw PII dumps.
09 · Prompts: orchestration templates
@mcp.prompt()def triage_ticket(ticket_id: str) -> str: return f"Triage {ticket_id}: query_tickets → hypothesis → no prod writes"
См. Agent Skill guide — Skill = procedure, Prompt = dialog skeleton.
10 · Streamable HTTP: prod transport
mcp.run(transport="streamable-http", host="0.0.0.0", port=8080)
TLS terminate at nginx/Caddy; rate limit 60 req/min/token; log request_id per tools/call. Public stdio tunnel = monthly credential leak reports.
11 · Debug pipeline (4 layers)
npx @modelcontextprotocol/inspector python server.pylogging.basicConfig(stream=sys.stderr)— never print stdout- pytest +
ClientSession+ stdio_client - Host smoke: read-only, bounded write, approval dialog
| Симптом | Причина | Fix |
|---|---|---|
| Connection closed | crash / stdout pollution | run server solo, check stderr |
| tools/call timeout | sync block IO | async + timeouts |
| HTTP 401 | expired bearer | rotate token |
12 · Production: 7-step hardening
- Pin mcp/fastmcp versions
- Vault-injected secrets
- systemd Restart=always, MemoryMax=256M
- /health off MCP route
- Prometheus: alert error_rate >5% / 5min
- OAuth minimal scopes
- Staging on isolated Mac — OpenClaw MCP
13 · Knowledge base: ChromaDB + sqlite-fts5
Enterprise default: index markdown, tools search_docs/get_doc, resource kb://. Wiki 2000 pages ≈ 180 MB sqlite-fts5, search p95 <200 ms on M4 (unified memory, no PCIe hop to discrete GPU).
@mcp.tool()async def search_docs(q: str, limit: int = 5) -> list[dict]: hits = await index.search(q, top_k=min(limit, 20)) return [{"path": h.path, "score": h.score} for h in hits]
14 · Экосystem June 2026
10 000+ public servers (Glama, Smithery). SDKs: Python FastMCP, TS @modelcontextprotocol/sdk, Go/Rust gateways. MCP ≠ A2A: tools vs agent routing. Registry search before rewrite.
15 · Bare-metal Mac: wipe before return
Полный path: Hello World → typed tools → HTTP → KB → observability. Custom MCP beats dumping OpenAPI into context.
Limits daily driver: stdio inherits Keychain + TCC; Linux Docker не эмулирует Seatbelt/security CLI; cloud VM latency skews stdio benchmarks used for capacity planning; shared machine with App Store signing keys = blast radius.
Sprint MCP dev с write-capable tools: аренда Mac mini M4 — Apple Silicon как в prod, clean user, SSH/VNC для GUI approvals, wipe без следов. Day billing = 1–2 week pilot без CapEx.
MacDate · MCP Dev
Собирайте MCP на Mac, который можно стереть — не на машине с signing keys.
Mac mini M4 / Mac Studio: SSH/VNC, посуточная оплата, чеклисты для FastMCP + Inspector + multi-Host smoke.