MCP Patterns Patterns for building, composing, and securing Model Context Protocol servers. Based on the 2025-11-25 specification — the latest stable release maintained by the Agentic AI Foundation (Linux Foundation), co-founded by Anthropic, Block, and OpenAI. Scaffolding a new server? Use Anthropic's skill ( ) for project setup and evaluation creation. This skill focuses on patterns, security, and advanced features after initial setup. Deploying to Cloudflare? See the skill for Workers-specific deployment patterns. Decision Tree — Which Rule to Read Quick Reference | Category | Rule | Impac…

audit.log\n```\n\nIf you maintain an external system that tracks tool counts via the audit log alone (e.g., a \"MCP tool sprawl\" alert), prefer the registry-discovery walk (`registry.modelcontextprotocol.io`) or query the server's `tools/list` directly via Inspector for an authoritative count.\n\n## Related\n\n- `mcp-version-matrix.md` — the current-state matrix this runbook maintains\n- `.mcp.json` — the target of pin changes\n- Issue #1462 — doctor check that will warn when HIGH-tier servers are unpinned\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4804,"content_sha256":"f7e4bc6f613b8a5d66208dcc0f28b435dceaf550edfb2fbda69136e29ced172e"},{"filename":"references/mcp-version-matrix.md","content":"# MCP Version Matrix\n\nTracks OrchestKit's integrated MCP servers against upstream latest. Sourced from `.mcp.json` (project-level) and documented user-level integrations.\n\n**Last audited:** 2026-04-26 (refresh after M117 + M122; pins HIGH-tier `21st-dev-magic`)\n\n## Audit Method\n\n1. `.mcp.json` enumerates project-level servers\n2. `npm view \u003cpkg> version` + `time.modified` for each\n3. Primary consumer located via `grep -rln \"mcp__\u003cname>__\"` in `src/skills/` and `src/agents/`\n\n## Matrix\n\n| MCP server | Package | Pin in `.mcp.json` | Upstream latest | Last published | Δ since prior audit | Primary consumer | Status |\n|---|---|---|---|---|---|---|---|\n| **context7** | `@upstash/context7-mcp` | `@latest` | **2.2.0** | 2026-04-24 | 2.1.8 → 2.2.0 (minor) | Many (chain-patterns, implement, fix-issue, cover, ...) | Active, recent release |\n| **sequential-thinking** | `@modelcontextprotocol/server-sequential-thinking` | unpinned | **2025.12.18** | 2026-02-06 | unchanged | chain-patterns, brainstorm, setup | Stable, calendar-versioned |\n| **memory** | `@modelcontextprotocol/server-memory` | unpinned | **2026.1.26** | 2026-02-06 | unchanged | 46 files (cross-cutting) | Stable, calendar-versioned |\n| **tavily** | `tavily-mcp` | `@latest` | **0.2.19** | 2026-04-24 | 0.2.18 → 0.2.19 (patch) | chain-patterns fallback (tier-fallbacks.md) | Low direct usage |\n| **agentation** | `agentation-mcp` | `@latest` (disabled) | **1.2.0** | 2026-02-15 | unchanged | `agents/ui-feedback.md`, `skills/verify` | Disabled by default in `.mcp.json` |\n| **21st-dev-magic** | `@21st-dev/magic` | **`@0.1.0` (pinned)** | **0.1.0** | 2025-12-23 | unchanged | None in core (mentioned as option in `component-search`) | Pre-1.0, stale upstream — pinned 2026-04-26 to lock current state |\n| **fal** | `fal-ai-mcp` | `@latest` | **0.2.1** | 2026-03-07 | unchanged | None in core (available for multimodal skills) | Active upstream |\n| **ork-elicit** | local `plugins/ork/mcp-server/server.mjs` | n/a (local) | n/a | versioned with repo | n/a | All skills via elicitation | In-tree, no external pin |\n\n## User-level MCPs (referenced but not in project `.mcp.json`)\n\n| MCP server | Package | Referenced by | Note |\n|---|---|---|---|\n| **notebooklm-mcp** | `notebooklm-mcp-cli` (PyPI, **0.6.1**, 2026-04-28) — installed via `uv tool install notebooklm-mcp-cli`; ships both `nlm` CLI and `notebooklm-mcp` MCP server. NOTE: the npm `[email protected]` (2025-12-27) is a separate, stale package — do NOT use it. | `src/skills/release-sync/SKILL.md`, `src/skills/notebooklm/SKILL.md` | Configured user-level in `~/.claude.json`; `release-sync` assumes availability |\n\n## Status: One HIGH-Tier Server Pinned, Rest on `@latest`\n\nOf 7 active remote MCPs, 1 is now version-pinned (`@21st-dev/[email protected]`) and 6 still resolve to `@latest`. The pinning of the HIGH-tier server addresses the original audit's primary risk: a breaking pre-1.0 upstream change can no longer propagate silently on the next `npx -y` fetch.\n\n**Remaining MEDIUM-tier @latest entries** (`context7`, `tavily`, `fal`) are intentionally left on `@latest` — they're semver-disciplined upstream and benefit from automatic patch/minor uptake. The doctor check (`/ork:doctor` Category 12 sub-check) emits an informational note but does not warn, matching the tier policy.\n\n## Risk Tier (unchanged)\n\n| Tier | Criteria | Servers |\n|---|---|---|\n| **LOW** | Stable API, calendar-versioned, low release velocity | `sequential-thinking`, `memory` |\n| **MEDIUM** | Active upstream, semver, used in many skills | `context7`, `tavily`, `fal` |\n| **HIGH** | Pre-1.0 upstream, API may change without notice | `21st-dev-magic` (0.1.0, **now pinned**), `agentation` (1.2.0 beta surface, **disabled**) |\n\n## Recommendations Status\n\n| # | Action | Status | Notes |\n|---|---|---|---|\n| 1 | Pin HIGH-tier servers to concrete versions in `.mcp.json` | **DONE** (2026-04-26) | `21st-dev-magic` pinned to `@0.1.0`; `agentation` already disabled |\n| 2 | Add an `ork:doctor` check that warns when `.mcp.json` uses `@latest` on HIGH-tier servers | **DONE** (M117 #1462) | Implemented in `src/skills/doctor/scripts/check-mcp-pinning.sh` (PR #1496) |\n| 3 | Document in `ork:release-sync` that NotebookLM MCP is user-configured and not auto-installed | OPEN | Small skill edit, low priority |\n| 4 | Re-run this audit every 90 days; update `Last audited` header | RECURRING | Next due: 2026-07-25 |\n| 5 *(new)* | Re-evaluate `agentation` HIGH→MEDIUM tier after upstream stabilizes (still beta surface as of 1.2.0, 2026-02-15) | OPEN | Re-check at 2026-07-25 audit |\n\n## Audit Cadence Calibration\n\nThe 90-day cadence is appropriate. Between audits 1 (2026-04-22) and 2 (2026-04-26, this refresh), only 4 days elapsed but 2 patch-level upstream changes landed (`context7 2.2.0`, `tavily 0.2.19`) — both auto-uptaken via `@latest`, no user action required. The 90-day cadence catches *tier reclassification* signals (e.g., a server going stale, hitting 1.0.0, or losing maintainer activity), not version-tracking — which `@latest` handles continuously.\n\n## How to Re-run This Audit\n\nSee `mcp-audit-runbook.md` (sibling reference) for the re-run script, interpretation rules, and when to escalate a drift. One-liner refresh:\n\n```bash\nfor pkg in @upstash/context7-mcp tavily-mcp fal-ai-mcp @21st-dev/magic agentation-mcp \\\n @modelcontextprotocol/server-sequential-thinking @modelcontextprotocol/server-memory \\\n notebooklm-mcp; do\n v=$(npm view \"$pkg\" version 2>/dev/null)\n pub=$(npm view \"$pkg\" time.modified 2>/dev/null)\n printf \"%-50s %-12s %s\\n\" \"$pkg\" \"${v:-???}\" \"${pub:-unknown}\"\ndone\n```\n\n## References\n\n- `.mcp.json` — project-level MCP server config\n- `~/.claude.json` — user-level MCP server config (not in repo)\n- Issue #1446 — the audit request that produced this matrix\n- PR #1496 (M117) — `/ork:doctor` MCP pinning sub-check\n- `src/skills/mcp-patterns/SKILL.md` — skill that owns this reference\n- `src/skills/doctor/references/mcp-pinning-check.md` — tier source-of-truth for the doctor check\n- `mcp-audit-runbook.md` — the operational procedure\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6127,"content_sha256":"45496a49fdcaaa6d1722d7cb9c1aa7d4578a33ceac8c46fa63095b25da7b38b5"},{"filename":"rules/_sections.md","content":"---\ntitle: MCP Patterns Rule Categories\nversion: 2.0.0\nspec-version: \"2025-11-25\"\n---\n\n# Rule Categories\n\n## 1. Server (server) — HIGH — 2 rules\n\nCore MCP server setup and transport configuration. Wrong transport choice or missing lifecycle management causes resource leaks and connection failures.\n\n- `server-setup.md` — FastMCP lifespan, Tool/Resource/Prompt primitives, error handling\n- `server-transport.md` — stdio vs Streamable HTTP (SSE deprecated), session management\n\n## 2. Auth (auth) — HIGH — 1 rule\n\nOAuth 2.1 authentication and authorization for remote MCP servers. Required for any server exposed over Streamable HTTP.\n\n- `auth-oauth21.md` — PKCE (S256), RFC 8707 resource indicators, token validation, dynamic client registration\n\n## 3. Advanced (advanced) — MEDIUM — 5 rules\n\nTool composition, resource management, and new spec features for production-grade MCP servers.\n\n- `advanced-composition.md` — Pipeline, parallel, and branching tool composition patterns\n- `advanced-resources.md` — Resource caching with TTL, LRU eviction, memory caps\n- `elicitation.md` — Server-initiated structured user input (form + URL modes)\n- `sampling-tools.md` — Server-side agent loops with tool calling and parallel execution\n- `apps-ui.md` — Interactive UI via MCP Apps extension + @mcp-ui/* SDK\n\n## 4. Client (client) — MEDIUM — 1 rule\n\nPatterns for consuming MCP servers from your application as a client.\n\n- `client-patterns.md` — TypeScript/Python client setup, session management, reconnection\n\n## 5. Security (security) — HIGH — 2 rules\n\nDefense-in-depth against prompt injection, tool poisoning, and data exfiltration through MCP integrations.\n\n- `security-injection.md` — Description sanitization, encoding normalization, threat detection\n- `security-hardening.md` — Zero-trust allowlist, hash verification, rug pull detection, capability enforcement\n\n## 6. Quality (quality) — MEDIUM — 1 rule\n\nTesting, debugging, and observability for MCP servers.\n\n- `testing-debugging.md` — MCP Inspector, unit testing tools, integration testing transports\n\n## 7. Ecosystem (ecosystem) — LOW — 2 rules\n\nDiscovery, registries, and complementary protocols.\n\n- `registry-discovery.md` — Official MCP Registry API, server metadata, programmatic discovery\n- `webmcp-browser.md` — W3C WebMCP browser-native agent tools (complementary to MCP)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2389,"content_sha256":"37e4dec4cdc2799d3813bd080e19adbfe0edf2ec70c5aeeeed8917b3508cfec0"},{"filename":"rules/_template.md","content":"---\ntitle: \"[Rule Name]\"\nimpact: \"[CRITICAL | HIGH | MEDIUM | LOW]\"\nimpactDescription: \"[What goes wrong without this rule]\"\ntags: []\n---\n\n## [Rule Name]\n\n[Brief description — 1-2 sentences.]\n\n**Incorrect:**\n```\n// Bad pattern\n```\n\n**Correct:**\n```\n// Good pattern\n```\n\n**Key rules:**\n- [Rule 1]\n- [Rule 2]\n- [Rule 3]\n\nReference: [link]\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":339,"content_sha256":"1f6b17cb8c02a543b46a648f062ff0a0c93635b93d51663bc299033be4dc3f83"},{"filename":"rules/advanced-composition.md","content":"---\ntitle: Compose multi-tool MCP workflows with error isolation to avoid brittle spaghetti code\nimpact: MEDIUM\nimpactDescription: \"Without composition patterns, complex multi-tool workflows become brittle spaghetti code with no error isolation\"\ntags: composition, pipeline, parallel, branching, tool-orchestration\n---\n\n## Advanced Composition\n\nCompose multiple MCP tools into pipelines, parallel fans, or conditional branches.\n\n**Incorrect -- manual sequential calls with no error handling:**\n```python\nresult1 = await tool_a(data)\nresult2 = await tool_b(result1) # Crashes if tool_a fails\nresult3 = await tool_c(result2) # No way to recover\n```\n\n**Correct -- pipeline composition with error propagation:**\n```python\nfrom dataclasses import dataclass, field\nfrom typing import Any, Callable, Awaitable\n\n@dataclass\nclass ToolResult:\n success: bool\n data: Any\n error: str | None = None\n\n@dataclass\nclass ComposedTool:\n name: str\n tools: dict[str, Callable[..., Awaitable[ToolResult]]]\n pipeline: list[str]\n\n async def execute(self, input_data: dict[str, Any]) -> ToolResult:\n result = ToolResult(success=True, data=input_data)\n for tool_name in self.pipeline:\n if not result.success:\n break\n try:\n result = await self.tools[tool_name](result.data)\n except Exception as e:\n result = ToolResult(success=False, data=None,\n error=f\"'{tool_name}' failed: {e}\")\n return result\n\n# Usage: search then summarize\nsearch_summarize = ComposedTool(\n name=\"search_and_summarize\",\n tools={\"search\": search_docs, \"summarize\": summarize_content},\n pipeline=[\"search\", \"summarize\"],\n)\n```\n\n**Correct -- parallel composition with error isolation:**\n```python\nimport asyncio\n\nasync def parallel_execute(\n tools: dict[str, Callable],\n input_data: dict,\n) -> list[ToolResult]:\n tasks = [\n asyncio.create_task(tool(input_data))\n for tool in tools.values()\n ]\n results = await asyncio.gather(*tasks, return_exceptions=True)\n\n return [\n ToolResult(success=False, data=None, error=str(r))\n if isinstance(r, Exception) else r\n for r in results\n ]\n```\n\n**Correct -- conditional branching:**\n```python\ndef content_router(data: dict) -> str:\n return {\n \"text\": \"text_processor\",\n \"image\": \"image_analyzer\",\n \"audio\": \"audio_transcriber\",\n }.get(data.get(\"type\", \"text\"), \"text_processor\")\n\n# Route to the right tool based on input\ntool_name = content_router(input_data)\nresult = await tools[tool_name](input_data)\n```\n\n**Key rules:**\n- Pipeline: stop on first failure, propagate error context\n- Parallel: use `return_exceptions=True` to isolate failures\n- Branching: always include a default/fallback route\n- Keep composition depth shallow (3-4 steps max)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2874,"content_sha256":"28432c00ccc879d60c0f43097a3ecab58682b1ae0efd8ec4d9944cf9e4f66b39"},{"filename":"rules/advanced-resources.md","content":"---\ntitle: Manage MCP resource caching and lifecycles to prevent memory leaks and redundant calls\nimpact: MEDIUM\nimpactDescription: \"Without resource caching and lifecycle management, MCP servers leak memory and make redundant expensive calls\"\ntags: resources, caching, ttl, lru, lifecycle, memory-management\n---\n\n## Advanced Resources\n\nCache MCP resources with TTL and LRU eviction. Always track memory usage and clean up expired entries.\n\n**Incorrect -- no caching, no cleanup:**\n```python\[email protected](\"user://{id}/profile\")\nasync def get_profile(id: str) -> dict:\n return await db.query(f\"SELECT * FROM users WHERE id = {id}\") # SQL injection + no cache\n```\n\n**Correct -- resource manager with TTL and LRU eviction:**\n```python\nfrom dataclasses import dataclass\nfrom datetime import datetime, timedelta\nfrom typing import Any\nimport asyncio\n\n@dataclass\nclass CachedResource:\n data: Any\n created_at: datetime\n last_accessed: datetime\n size_bytes: int = 0\n\n def touch(self) -> None:\n self.last_accessed = datetime.now()\n\nclass MCPResourceManager:\n def __init__(\n self,\n cache_ttl: timedelta = timedelta(minutes=15),\n max_cache_size: int = 100,\n max_memory_bytes: int = 100 * 1024 * 1024, # 100MB\n ):\n self.cache_ttl = cache_ttl\n self.max_cache_size = max_cache_size\n self.max_memory_bytes = max_memory_bytes\n self._cache: dict[str, CachedResource] = {}\n self._lock = asyncio.Lock()\n\n async def get(self, uri: str, loader: callable) -> Any:\n async with self._lock:\n if uri in self._cache:\n resource = self._cache[uri]\n if datetime.now() - resource.created_at \u003c= self.cache_ttl:\n resource.touch()\n return resource.data\n del self._cache[uri] # Expired\n\n data = await loader(uri)\n await self._store(uri, data)\n return data\n\n async def _store(self, uri: str, data: Any) -> None:\n import sys\n size = sys.getsizeof(data)\n # Evict LRU entries if needed\n while (len(self._cache) >= self.max_cache_size\n or self._total_size() + size > self.max_memory_bytes):\n if not self._cache:\n break\n lru_uri = min(self._cache, key=lambda k: self._cache[k].last_accessed)\n del self._cache[lru_uri]\n\n now = datetime.now()\n self._cache[uri] = CachedResource(\n data=data, created_at=now, last_accessed=now, size_bytes=size,\n )\n\n def _total_size(self) -> int:\n return sum(r.size_bytes for r in self._cache.values())\n\n async def cleanup_expired(self) -> int:\n async with self._lock:\n now = datetime.now()\n expired = [\n uri for uri, r in self._cache.items()\n if now - r.created_at > self.cache_ttl\n ]\n for uri in expired:\n del self._cache[uri]\n return len(expired)\n```\n\n**Correct -- FastMCP lifespan with resource lifecycle:**\n```python\nfrom contextlib import asynccontextmanager\nfrom mcp.server.fastmcp import FastMCP\n\n@asynccontextmanager\nasync def app_lifespan(server: FastMCP):\n resources = MCPResourceManager(\n cache_ttl=timedelta(minutes=10),\n max_memory_bytes=50 * 1024 * 1024,\n )\n try:\n yield {\"resources\": resources}\n finally:\n await resources.cleanup_expired() # Final cleanup\n\nmcp = FastMCP(\"cached-server\", lifespan=app_lifespan)\n```\n\n**Key rules:**\n- Always set `max_cache_size` and `max_memory_bytes` caps\n- Use `asyncio.Lock` for thread-safe cache access\n- Run `cleanup_expired()` on shutdown and periodically\n- Parameterize queries -- never interpolate user input into SQL\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3783,"content_sha256":"c94e8f7d03de7024514282e27e087585d9ffd64ed6be1d7890910d4fdaef474b"},{"filename":"rules/apps-ui.md","content":"---\ntitle: Configure MCP Apps UI sandboxing, CSP declarations, and visibility controls correctly\nimpact: \"MEDIUM\"\nimpactDescription: \"Without proper sandbox and CSP declarations, UI resources render without network access or fail to load external assets; missing visibility controls expose internal tools to the model\"\ntags: [mcp-apps, ui-resource, sandbox, csp, iframe, ext-apps, visibility]\n---\n\n## MCP Apps UI\n\nMCP Apps (SEP-1865) let tools return interactive UIs rendered in sandboxed iframes. Declare `ui://` resources, link them to tools via `_meta.ui.resourceUri`, and configure CSP domains for secure external access.\n\n**Incorrect -- no CSP, no sandbox awareness, no visibility control:**\n```typescript\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nconst server = new McpServer({ name: \"my-app\", version: \"1.0.0\" });\n\n// BAD: resource uses generic mimeType, no ui:// scheme\nserver.registerResource(\"dashboard\", \"https://my-app.com/dashboard\", {\n mimeType: \"text/html\",\n});\n\n// BAD: no _meta.ui linkage, no visibility — internal tool exposed to model\nserver.registerTool(\"refresh_dashboard\", {\n description: \"Refresh dashboard data\",\n inputSchema: { type: \"object\" },\n}, async () => ({\n content: [{ type: \"text\", text: \"refreshed\" }],\n}));\n```\n\n**Correct -- `registerAppTool`/`registerAppResource` with CSP and visibility:**\n```typescript\nimport {\n registerAppTool,\n registerAppResource,\n RESOURCE_MIME_TYPE, // \"text/html;profile=mcp-app\"\n} from \"@modelcontextprotocol/ext-apps/server\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult, ReadResourceResult } from \"@modelcontextprotocol/sdk/types.js\";\n\nconst server = new McpServer({ name: \"my-app\", version: \"1.0.0\" });\nconst RESOURCE_URI = \"ui://my-app/dashboard\";\n\n// Declare CSP domains for external tile/API access\nconst cspMeta = {\n ui: {\n csp: {\n connectDomains: [\"https://api.example.com\"], // fetch/XHR/WebSocket\n resourceDomains: [\"https://cdn.jsdelivr.net\"], // scripts, images, styles\n frameDomains: [\"https://www.youtube.com\"], // nested iframes\n },\n prefersBorder: true,\n },\n};\n\n// Register UI resource with CSP metadata\nregisterAppResource(server, RESOURCE_URI, RESOURCE_URI,\n { mimeType: RESOURCE_MIME_TYPE },\n async (): Promise\u003cReadResourceResult> => ({\n contents: [{\n uri: RESOURCE_URI,\n mimeType: RESOURCE_MIME_TYPE,\n text: htmlContent,\n _meta: cspMeta,\n }],\n }),\n);\n\n// Tool visible to both model and app (default)\nregisterAppTool(server, \"get-dashboard\", {\n title: \"Get Dashboard\",\n description: \"Show interactive analytics dashboard.\",\n inputSchema: {},\n _meta: { ui: { resourceUri: RESOURCE_URI } },\n}, async (): Promise\u003cCallToolResult> => ({\n content: [{ type: \"text\", text: JSON.stringify(data) }],\n}));\n\n// App-only tool — hidden from model, callable only by the UI\nregisterAppTool(server, \"refresh_data\", {\n title: \"Refresh Data\",\n description: \"Refresh dashboard data (internal).\",\n inputSchema: {},\n _meta: {\n ui: {\n resourceUri: RESOURCE_URI,\n visibility: [\"app\"], // hidden from model tool list\n },\n },\n}, async (): Promise\u003cCallToolResult> => ({\n content: [{ type: \"text\", text: JSON.stringify(freshData) }],\n}));\n```\n\n**Correct -- React app using `@modelcontextprotocol/ext-apps/react`:**\n```tsx\nimport { useToolResult } from \"@modelcontextprotocol/ext-apps/react\";\n\nfunction Dashboard() {\n const result = useToolResult(); // receives tool call data\n const data = JSON.parse(result?.content?.[0]?.text ?? \"{}\");\n return \u003cdiv>{/* render interactive UI from data */}\u003c/div>;\n}\n```\n\n**Key rules:**\n- Use `ui://` URI scheme for all UI resources, with `text/html;profile=mcp-app` mimeType\n- Use `registerAppTool` and `registerAppResource` from `@modelcontextprotocol/ext-apps/server`\n- Link tools to UIs via `_meta.ui.resourceUri` on the tool definition\n- Declare CSP domains explicitly: `connectDomains` (fetch), `resourceDomains` (CDN), `frameDomains` (iframes)\n- Omitting CSP defaults to `connect-src 'none'` -- no external network access\n- Set `visibility: [\"app\"]` for tools only the UI should call (hides from model)\n- Default visibility is `[\"model\", \"app\"]` -- tool visible to both model and UI\n- Host renders UI in sandboxed iframe; never assume permissions are granted\n- Content MUST be valid HTML5 provided via `text` (string) or `blob` (base64)\n\nReference: [MCP Apps Extension (SEP-1865)](https://github.com/modelcontextprotocol/ext-apps)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4541,"content_sha256":"94d94cc69af7a4bfdfa1c503480f3588c10defed263fb8e8fb296b476646df04"},{"filename":"rules/auth-oauth21.md","content":"---\ntitle: \"OAuth 2.1 Authorization for MCP Servers\"\nimpact: \"HIGH\"\nimpactDescription: \"Without proper OAuth 2.1 + RFC 8707, tokens leak across services (confused deputy), clients accept any server's token, and attackers replay authorization codes\"\ntags: [oauth, authorization, pkce, rfc8707, resource-indicators, mcp-spec-2025-11-25, mtls, oidc]\n---\n\n## OAuth 2.1 Authorization for MCP Servers\n\nMCP servers are OAuth 2.1 Resource Servers (spec 2025-11-25). Clients MUST use PKCE with S256, bind tokens to the target resource via RFC 8707, and never pass tokens through to downstream services.\n\n**Incorrect -- no PKCE, no resource indicator, token passthrough:**\n```typescript\n// BAD: Missing PKCE and resource parameter\nconst authUrl = `${authServer}/authorize?client_id=${clientId}&redirect_uri=${redirect}`;\n\n// BAD: Passing client's token to upstream API (confused deputy)\nasync function callUpstreamApi(clientToken: string) {\n return fetch(\"https://api.example.com/data\", {\n headers: { Authorization: `Bearer ${clientToken}` }, // NEVER DO THIS\n });\n}\n\n// BAD: No audience validation on the resource server\nfunction validateToken(token: string) {\n const decoded = jwt.verify(token, publicKey);\n return decoded; // Missing audience check — accepts ANY valid token\n}\n```\n\n**Correct -- PKCE S256 + RFC 8707 resource binding:**\n```typescript\nimport crypto from \"node:crypto\";\n\n// 1. PKCE: Generate verifier and S256 challenge\nfunction createPkce() {\n const verifier = crypto.randomBytes(32).toString(\"base64url\");\n const challenge = crypto.createHash(\"sha256\").update(verifier).digest(\"base64url\");\n return { verifier, challenge };\n}\n\n// 2. Authorization request with resource indicator (RFC 8707)\nfunction buildAuthUrl(\n authServer: string, clientId: string, redirectUri: string,\n mcpServerUri: string, scopes: string[],\n) {\n const { verifier, challenge } = createPkce();\n const state = crypto.randomBytes(16).toString(\"base64url\");\n const params = new URLSearchParams({\n response_type: \"code\",\n client_id: clientId,\n redirect_uri: redirectUri,\n code_challenge: challenge,\n code_challenge_method: \"S256\",\n resource: mcpServerUri, // MUST match MCP server's canonical URI\n scope: scopes.join(\" \"),\n state,\n });\n return { url: `${authServer}/authorize?${params}`, verifier, state };\n}\n\n// 3. Token exchange — resource parameter MUST match authorization request\nasync function exchangeCode(\n tokenEndpoint: string, code: string, verifier: string,\n clientId: string, redirectUri: string, mcpServerUri: string,\n) {\n const res = await fetch(tokenEndpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n grant_type: \"authorization_code\", code,\n code_verifier: verifier, client_id: clientId,\n redirect_uri: redirectUri, resource: mcpServerUri,\n }),\n });\n return res.json();\n}\n```\n\n**Correct -- token validation + confused deputy prevention:**\n```typescript\n// 4. MCP server validates audience (RFC 8707 + RFC 9068)\nfunction validateAccessToken(token: string, expectedAudience: string) {\n const decoded = jwt.verify(token, publicKey, {\n algorithms: [\"RS256\"],\n audience: expectedAudience, // MUST be this server's canonical URI\n issuer: trustedIssuer,\n });\n return decoded;\n}\n\n// 5. Upstream calls use a SEPARATE token — never forward the client's token\nasync function callUpstream(upstreamTokenEndpoint: string) {\n const { access_token } = await fetch(upstreamTokenEndpoint, {\n method: \"POST\",\n body: new URLSearchParams({ grant_type: \"client_credentials\", scope: \"upstream:read\" }),\n }).then((r) => r.json());\n return access_token; // Scoped to upstream, NOT the client's token\n}\n```\n\n**Correct -- discovery, registration, and incremental scope consent:**\n```typescript\n// 6. Protected Resource Metadata discovery (RFC 9728)\nasync function discoverAuthServer(mcpServerUrl: string) {\n const origin = new URL(mcpServerUrl).origin;\n const meta = await fetch(`${origin}/.well-known/oauth-protected-resource`).then((r) => r.json());\n const asUrl = meta.authorization_servers[0];\n // Try OAuth 2.0 AS Metadata, then OIDC Discovery\n for (const p of [\"/.well-known/oauth-authorization-server\", \"/.well-known/openid-configuration\"]) {\n const res = await fetch(`${asUrl}${p}`);\n if (res.ok) return res.json();\n }\n throw new Error(\"No authorization server metadata found\");\n}\n\n// 7. Dynamic Client Registration (RFC 7591) — fallback when no pre-registration\nasync function registerClient(registrationEndpoint: string) {\n return fetch(registrationEndpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n client_name: \"My MCP Client\",\n redirect_uris: [\"http://127.0.0.1:3000/callback\"],\n grant_types: [\"authorization_code\"],\n token_endpoint_auth_method: \"none\",\n }),\n }).then((r) => r.json());\n}\n\n// 8. Incremental scope consent — handle 403 insufficient_scope\nfunction handleInsufficientScope(wwwAuth: string) {\n const match = wwwAuth.match(/scope=\"([^\"]+)\"/);\n if (match) return match[1].split(\" \"); // Re-authorize with these scopes\n}\n```\n\n**Correct -- OAuth URL paste fallback (headless/SSH environments):**\n```typescript\n// 9. When browser can't open (SSH, containers, headless), print URL for manual paste\nasync function authorizeWithFallback(authUrl: string) {\n const canOpenBrowser = process.env.DISPLAY || process.platform === \"darwin\";\n if (canOpenBrowser) {\n await open(authUrl); // Opens default browser\n } else {\n // Fallback: print URL for user to paste in their local browser\n console.log(\"\\nOpen this URL in your browser to authorize:\");\n console.log(`\\n ${authUrl}\\n`);\n console.log(\"After authorizing, paste the callback URL here:\");\n const callbackUrl = await readline.question(\"> \");\n const code = new URL(callbackUrl).searchParams.get(\"code\");\n if (!code) throw new Error(\"No authorization code found in callback URL\");\n return code;\n }\n}\n```\n\n**Key rules:**\n- PKCE with S256 is mandatory; refuse to proceed if AS lacks `code_challenge_methods_supported`\n- Include `resource` parameter (RFC 8707) in both authorization and token requests, set to the MCP server's canonical URI\n- MCP servers MUST validate the `aud` claim matches their own URI — reject all other tokens\n- NEVER pass the client's access token to upstream APIs (confused deputy); obtain a separate token via client credentials or token exchange (RFC 8693)\n- Use Protected Resource Metadata (RFC 9728) for AS discovery; support both OAuth 2.0 AS Metadata and OIDC Discovery\n- Prefer Client ID Metadata Documents over Dynamic Client Registration (RFC 7591) for new implementations\n- Handle `403 insufficient_scope` by re-authorizing with scopes from the `WWW-Authenticate` header\n- For high-security deployments, bind tokens to client certificates via mTLS (RFC 8705) to prevent token theft and replay\n- In headless environments (SSH, containers, CI), implement URL paste fallback — print the auth URL for the user to open manually and accept the callback URL pasted back (see example 9 above)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7164,"content_sha256":"660749a370ff607f6ea89a335977c633f530a32fabce129628db297b12d69c3b"},{"filename":"rules/client-patterns.md","content":"---\ntitle: Implement MCP client patterns for reliable connections and multi-server orchestration\nimpact: \"MEDIUM\"\nimpactDescription: \"Without proper client patterns, connections leak, tool calls fail silently, and multi-server orchestration becomes brittle\"\ntags: [client, session, transport, tool-discovery, reconnection, multi-server]\n---\n\n## Client Patterns\n\nSet up MCP clients with proper session management, error handling, and reconnection. Covers TypeScript and Python SDKs for consuming MCP servers from applications.\n\n**Incorrect -- no error handling, no cleanup:**\n```typescript\nimport { Client, StreamableHTTPClientTransport } from \"@modelcontextprotocol/client\";\n\nconst client = new Client({ name: \"app\", version: \"1.0.0\" });\nconst transport = new StreamableHTTPClientTransport(new URL(\"http://localhost:3000/mcp\"));\nawait client.connect(transport);\nconst result = await client.callTool({ name: \"search\", arguments: { q: \"test\" } });\nconsole.log(result.content[0].text); // Crashes if tool errors or content empty\n// Transport never closed -- connection leaked\n```\n\n```python\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp import ClientSession\n\n# No context manager -- session never cleaned up\nread, write = await streamable_http_client(\"http://localhost:3000/mcp\").__aenter__()\nsession = ClientSession(read, write)\nawait session.initialize()\nresult = await session.call_tool(\"search\", arguments={\"q\": \"test\"})\nprint(result.content[0].text) # No type check, no error handling\n```\n\n**Correct -- TypeScript client with reconnection and capability negotiation:**\n```typescript\nimport { Client, StreamableHTTPClientTransport } from \"@modelcontextprotocol/client\";\n\nconst transport = new StreamableHTTPClientTransport(\n new URL(\"http://localhost:3000/mcp\"),\n {\n sessionId: cachedSessionId, // Reconnect to existing session\n reconnectionOptions: {\n maxRetries: 5,\n initialReconnectionDelay: 1000,\n maxReconnectionDelay: 30000,\n reconnectionDelayGrowFactor: 1.5,\n },\n }\n);\n\nconst client = new Client(\n { name: \"my-app\", version: \"1.0.0\" },\n { capabilities: { sampling: {} } } // Declare client capabilities\n);\n\ntry {\n await client.connect(transport);\n const caps = client.getServerCapabilities();\n\n // Discover tools before calling\n const { tools } = await client.listTools();\n const hasTool = tools.some((t) => t.name === \"search\");\n if (!hasTool) throw new Error(\"Required tool 'search' not available\");\n\n const result = await client.callTool({ name: \"search\", arguments: { q: \"test\" } });\n for (const content of result.content) {\n if (content.type === \"text\") console.log(content.text);\n }\n} finally {\n await transport.terminateSession();\n await transport.close();\n}\n```\n\n**Correct -- Python client with context managers:**\n```python\nimport asyncio\nfrom mcp import ClientSession, StdioServerParameters, types\nfrom mcp.client.stdio import stdio_client\nfrom mcp.client.streamable_http import streamable_http_client\n\nasync def run_stdio_client():\n server_params = StdioServerParameters(\n command=\"python\", args=[\"my_server.py\"]\n )\n async with stdio_client(server_params) as (read, write):\n async with ClientSession(read, write) as session:\n await session.initialize()\n\n tools = await session.list_tools()\n result = await session.call_tool(\"add\", arguments={\"a\": 5, \"b\": 3})\n for content in result.content:\n if isinstance(content, types.TextContent):\n print(content.text)\n\nasync def run_http_client():\n async with streamable_http_client(\"http://localhost:8000/mcp\") as (read, write):\n async with ClientSession(read, write) as session:\n await session.initialize()\n tools = await session.list_tools()\n print([t.name for t in tools.tools])\n```\n\n**Correct -- multi-server orchestration (TypeScript):**\n```typescript\nasync function connectServers(urls: string[]) {\n const clients = await Promise.all(\n urls.map(async (url) => {\n const transport = new StreamableHTTPClientTransport(new URL(url));\n const client = new Client({ name: \"orchestrator\", version: \"1.0.0\" });\n await client.connect(transport);\n const { tools } = await client.listTools();\n return { client, transport, tools, url };\n })\n );\n\n // Build unified tool registry across servers\n const toolMap = new Map\u003cstring, typeof clients[0]>();\n for (const entry of clients) {\n for (const tool of entry.tools) {\n toolMap.set(`${tool.name}@${entry.url}`, entry);\n }\n }\n return { clients, toolMap };\n}\n```\n\n**Key rules:**\n- Always close transports in `finally` blocks (TS) or use context managers (Python)\n- Call `initialize()` before any other session method in Python\n- Discover tools with `listTools()` before calling -- never assume tool availability\n- Use `reconnectionOptions` with exponential backoff for remote HTTP servers\n- Cache `sessionId` to resume sessions after reconnection\n- Check `content.type` before accessing `.text` -- tools may return images or errors\n- For multi-server setups, namespace tools by server to avoid name collisions\n- Declare client capabilities (`sampling`, `elicitation`) during construction\n\nReference: https://modelcontextprotocol.io/specification/2025-11-25/architecture\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5334,"content_sha256":"0e4f6c20b704637ebb50df61d45baf3c043338343890651399b5de118735cee8"},{"filename":"rules/elicitation.md","content":"---\ntitle: Use MCP elicitation safely with consent handling and secure form-mode data collection\nimpact: \"MEDIUM\"\nimpactDescription: \"Requesting sensitive data via form mode exposes credentials to the LLM context; skipping user consent or mishandling cancel/decline breaks trust and leaves servers in inconsistent state\"\ntags: [elicitation, form-mode, url-mode, security, json-schema, oauth]\n---\n\n## Elicitation\n\nMCP elicitation lets servers request structured input from users at runtime via form mode (JSON Schema) or URL mode (external flows). Form mode collects non-sensitive data in-band; URL mode redirects users to secure pages for credentials, OAuth, or payments.\n\n**Incorrect -- requesting secrets via form mode, ignoring decline/cancel:**\n```python\[email protected]()\nasync def connect_api(ctx: Context) -> str:\n # WRONG: form mode exposes secrets to the LLM context\n result = await ctx.session.create_elicitation(\n mode=\"form\",\n message=\"Enter your API key\",\n requestedSchema={\n \"type\": \"object\",\n \"properties\": {\n \"api_key\": {\"type\": \"string\"},\n # WRONG: nested objects not allowed in elicitation schemas\n \"config\": {\"type\": \"object\", \"properties\": {\"timeout\": {\"type\": \"number\"}}},\n },\n },\n )\n # WRONG: assumes accept, crashes on decline/cancel\n return call_api(result.content[\"api_key\"])\n```\n\n**Correct -- form mode for non-sensitive data, flat schema, handle all actions:**\n```python\[email protected]()\nasync def configure_search(ctx: Context) -> str:\n result = await ctx.session.create_elicitation(\n mode=\"form\",\n message=\"Configure your search preferences\",\n requestedSchema={\n \"type\": \"object\",\n \"properties\": {\n \"query\": {\"type\": \"string\", \"minLength\": 1, \"description\": \"Search terms\"},\n \"category\": {\n \"type\": \"string\",\n \"enum\": [\"docs\", \"code\", \"issues\"],\n \"default\": \"docs\",\n },\n \"max_results\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 50,\n \"default\": 10,\n },\n },\n \"required\": [\"query\"],\n },\n )\n\n if result.action == \"accept\":\n return search(result.content)\n elif result.action == \"decline\":\n return \"Search cancelled. Let me know if you'd like to try different options.\"\n else: # cancel\n return \"Search dismissed. I can search with defaults if you'd like.\"\n```\n\n**Correct -- URL mode for sensitive data (API keys, OAuth):**\n```python\[email protected]()\nasync def connect_service(ctx: Context) -> str:\n elicitation_id = str(uuid.uuid4())\n\n result = await ctx.session.create_elicitation(\n mode=\"url\",\n message=\"Please authorize access to your account.\",\n elicitation_id=elicitation_id,\n url=f\"https://myserver.example.com/connect?eid={elicitation_id}\",\n )\n\n if result.action == \"accept\":\n # User consented to open URL -- interaction happens out-of-band.\n # Server sends notifications/elicitation/complete when done.\n return \"Authorization started. I'll proceed once you complete the flow.\"\n elif result.action == \"decline\":\n return \"Authorization declined. Some features will be unavailable.\"\n else: # cancel\n return \"Authorization dismissed.\"\n```\n\n**Correct -- client declares elicitation capabilities:**\n```typescript\nconst client = new Client({\n name: \"my-client\",\n version: \"1.0.0\",\n}, {\n capabilities: {\n elicitation: { form: {}, url: {} }, // declare supported modes\n },\n});\n```\n\n**Key rules:**\n- Never request secrets (API keys, passwords, tokens) via form mode -- use URL mode instead\n- Schemas must be flat objects with primitive properties only (string, number, integer, boolean, enum) -- no nested objects or `$ref`\n- Always handle all three response actions: `accept`, `decline`, `cancel`\n- URL mode `accept` means user consented to open the URL, not that the flow is complete -- listen for `notifications/elicitation/complete`\n- Clients must show the full URL and get explicit consent before opening; never auto-fetch or auto-navigate\n- Servers must verify the user who completes a URL flow is the same user who initiated it (prevent phishing/account takeover)\n- Check client capabilities before sending elicitation requests -- clients may support only `form`, only `url`, or both\n\nReference: https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4619,"content_sha256":"4f7a3ae80c63bb67aaec8ce1a38ee72bdd87af39edd76a15d2a7c01f997eac4c"},{"filename":"rules/registry-discovery.md","content":"---\ntitle: Vet MCP servers from registries to prevent supply-chain attacks and data exfiltration\nimpact: LOW\nimpactDescription: \"Without proper vetting, blindly installing community MCP servers risks supply-chain attacks, data exfiltration, or rug-pull tool modifications\"\ntags: registry, discovery, vetting, security, trust, smithery, mcp-run\n---\n\n## Registry Discovery\n\nUse the official MCP Registry API for programmatic server discovery and apply a vetting checklist before installing any third-party server.\n\n**Incorrect -- blindly install unvetted servers:**\n```python\n# Grabbed a random server name from a blog post\nconfig = {\"mcpServers\": {\"sketchy-db\": {\"command\": \"npx\", \"args\": [\"@unknown/mcp-db\"]}}}\n# No source review, no version pinning, no permission audit\n```\n\n**Correct -- query the official registry and vet before installing:**\n```python\nimport httpx\n\nREGISTRY = \"https://registry.modelcontextprotocol.io\"\n\nasync def discover_servers(query: str) -> list[dict]:\n \"\"\"Search the official MCP Registry API.\"\"\"\n async with httpx.AsyncClient() as client:\n resp = await client.get(f\"{REGISTRY}/v0.1/servers\", params={\n \"search\": query, \"version\": \"latest\", \"limit\": 20,\n })\n resp.raise_for_status()\n return resp.json()[\"servers\"]\n\nasync def get_server_detail(name: str, version: str = \"latest\") -> dict:\n \"\"\"Fetch full metadata for a specific server.\"\"\"\n async with httpx.AsyncClient() as client:\n resp = await client.get(f\"{REGISTRY}/v0.1/servers/{name}/versions/{version}\")\n resp.raise_for_status()\n return resp.json()\n\ndef vet_server(server: dict) -> list[str]:\n \"\"\"Return warnings if server fails vetting checks.\"\"\"\n warnings = []\n s = server.get(\"server\", server)\n if not s.get(\"repository\", {}).get(\"url\"):\n warnings.append(\"No public source repository\")\n if not s.get(\"packages\"):\n warnings.append(\"Not published to any package registry\")\n meta = server.get(\"_meta\", {}).get(\"io.modelcontextprotocol.registry/official\", {})\n if meta.get(\"status\") != \"active\":\n warnings.append(f\"Registry status: {meta.get('status', 'unknown')}\")\n return warnings\n```\n\n**Community directories for broader discovery:**\n\n| Directory | URL | Notes |\n|-----------|-----|-------|\n| Official Registry | registry.modelcontextprotocol.io | API-accessible, moderation |\n| mcp.run | mcp.run | Hosted runtime, sandboxed |\n| Smithery | smithery.ai | Install counts, reviews |\n| Glama | glama.ai/mcp/servers | Curated catalog |\n| MCP Servers | mcpservers.org | Community-maintained list |\n\n**Vetting checklist before installing any server:**\n```markdown\n- [ ] Source code in a public repository with commit history\n- [ ] Published to npm/PyPI (not just a git clone)\n- [ ] Version pinned in config (no `@latest` in production)\n- [ ] README documents all tools, resources, and required permissions\n- [ ] No overly broad capabilities (filesystem root, network wildcard)\n- [ ] Active maintenance (commits within last 90 days)\n- [ ] Listed in official registry or reputable directory\n```\n\n**Icon metadata (spec 2025-11-25) -- expose icons for tools/resources:**\n```python\[email protected](metadata={\"icon\": \"https://example.com/icons/search.svg\"})\ndef search(query: str) -> str:\n \"\"\"Search documents.\"\"\"\n ...\n```\n\n**Key rules:**\n- Always query the official registry at `registry.modelcontextprotocol.io/v0.1/servers` first\n- Never install a server without checking its source repository and package provenance\n- Pin exact versions in MCP server configurations -- avoid `@latest` in production\n- Cross-reference multiple directories (registry, smithery, mcp.run) for trust signals\n- Treat community servers as untrusted by default; apply allowlist patterns from security-hardening\n- Use `vet_server()` checks programmatically when building multi-server orchestrations\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3866,"content_sha256":"c75f55e382c2881ef6a73f7a13872d04903b3a9f14f08a72a6e43c88b983bb8c"},{"filename":"rules/sampling-tools.md","content":"---\ntitle: Bound MCP sampling loops with user approval to prevent unbounded LLM call chains\nimpact: MEDIUM\nimpactDescription: \"Without bounded loops and user approval, sampling-based agents run unbounded LLM calls, skip human review, and leak sensitive tool results\"\ntags: sampling, tools, agent-loop, security, human-in-the-loop\n---\n\n## Sampling with Tool Calling\n\nMCP sampling lets servers request LLM completions from clients, with optional tool definitions for agentic multi-turn loops. The client controls model access and user approval throughout.\n\n**Incorrect -- no iteration cap, skips user approval:**\n```python\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"agent-server\")\n\[email protected]()\nasync def run_agent(task: str, ctx) -> str:\n messages = [{\"role\": \"user\", \"content\": {\"type\": \"text\", \"text\": task}}]\n tools = [{\"name\": \"search\", \"description\": \"Search docs\",\n \"inputSchema\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}]\n\n # Unbounded loop -- runs forever if LLM keeps calling tools\n while True:\n result = await ctx.session.create_message(\n messages=messages, tools=tools, max_tokens=2000\n )\n if result.stop_reason != \"toolUse\":\n return result.content.text\n # Blindly append and continue without any limit\n messages.append({\"role\": \"assistant\", \"content\": result.content})\n tool_results = [execute_tool(tc) for tc in result.content]\n messages.append({\"role\": \"user\", \"content\": tool_results})\n```\n\n**Correct -- bounded loop, tool choice control, proper message structure:**\n```python\nfrom mcp.server.fastmcp import FastMCP, Context\n\nmcp = FastMCP(\"agent-server\")\n\nMAX_ITERATIONS = 5\n\nTOOLS = [{\n \"name\": \"search\",\n \"description\": \"Search documentation by keyword\",\n \"inputSchema\": {\n \"type\": \"object\",\n \"properties\": {\"q\": {\"type\": \"string\", \"description\": \"Search query\"}},\n \"required\": [\"q\"],\n },\n}]\n\[email protected]()\nasync def run_agent(task: str, ctx: Context) -> str:\n \"\"\"Run a bounded agent loop with tool access via sampling.\"\"\"\n messages = [{\"role\": \"user\", \"content\": {\"type\": \"text\", \"text\": task}}]\n\n for i in range(MAX_ITERATIONS):\n # Force text-only response on final iteration\n tool_choice = (\n {\"mode\": \"none\"} if i == MAX_ITERATIONS - 1\n else {\"mode\": \"auto\"}\n )\n result = await ctx.session.create_message(\n messages=messages,\n tools=TOOLS,\n tool_choice=tool_choice,\n max_tokens=2000,\n )\n\n # LLM chose not to use tools -- return final answer\n if result.stop_reason != \"toolUse\":\n return result.content.text if hasattr(result.content, \"text\") else str(result.content)\n\n # Execute each tool call, build tool_result messages\n assistant_content = result.content if isinstance(result.content, list) else [result.content]\n messages.append({\"role\": \"assistant\", \"content\": assistant_content})\n\n # Tool results MUST be in their own user message -- no mixed content\n tool_results = []\n for block in assistant_content:\n if block.type == \"tool_use\":\n output = await execute_tool(block.name, block.input)\n tool_results.append({\n \"type\": \"tool_result\",\n \"toolUseId\": block.id,\n \"content\": [{\"type\": \"text\", \"text\": str(output)}],\n })\n messages.append({\"role\": \"user\", \"content\": tool_results})\n\n return \"Agent reached iteration limit without a final answer.\"\n```\n\n**Declaring sampling capability with tool support (client-side):**\n```python\n# Client must advertise sampling.tools capability during initialization\ncapabilities = {\n \"sampling\": {\n \"tools\": {} # Required for tool-enabled sampling requests\n }\n}\n```\n\n**Key rules:**\n- Always cap iteration count and use `toolChoice: {mode: \"none\"}` on the final turn to force a text response\n- Tool result messages MUST contain only `tool_result` blocks -- never mix with text or image content\n- Every `tool_use` block (by `id`) must have a matching `tool_result` (by `toolUseId`) before the next assistant turn\n- Clients MUST declare `sampling.tools` capability; servers MUST NOT send tool-enabled requests without it\n- Human-in-the-loop: clients SHOULD present sampling requests and tool calls for user review before execution\n- Use `toolChoice` modes: `auto` (LLM decides), `required` (must call a tool), `none` (text only)\n- Parallel tool calls are supported -- handle arrays of `tool_use` blocks in a single assistant message\n- Implement rate limiting on the client side to prevent runaway sampling loops\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4754,"content_sha256":"419763a35887a2eec8297595e7cf660349c8b45cc52ee4fa460c0144f6770097"},{"filename":"rules/security-hardening.md","content":"---\ntitle: Apply zero-trust verification to MCP servers to prevent rug-pull and data exfiltration\nimpact: HIGH\nimpactDescription: \"Without zero-trust verification, a compromised MCP server can silently change tool behavior (rug pull) or exfiltrate data through tool responses\"\ntags: security, allowlist, hash-verification, rug-pull, capabilities, zero-trust, session\n---\n\n## Security Hardening\n\nVerify every tool with hash-based integrity checks. Use zero-trust allowlists, capability enforcement, and secure sessions.\n\n**Incorrect -- trust all tools without verification:**\n```python\ntools = await mcp.list_tools() # No vetting!\nresult = await mcp.call_tool(name, args) # No integrity check!\nsession_id = f\"{user_id}:{auth_token}\" # CREDENTIAL LEAK in session ID!\n```\n\n**Correct -- zero-trust tool allowlist with hash verification:**\n```python\nfrom hashlib import sha256\nfrom dataclasses import dataclass\nfrom datetime import datetime, timezone\n\n@dataclass\nclass AllowedTool:\n name: str\n description_hash: str\n capabilities: list[str]\n approved_by: str\n max_calls_per_minute: int = 60\n\nclass MCPToolAllowlist:\n def __init__(self):\n self._allowed: dict[str, AllowedTool] = {}\n self._call_counts: dict[str, list[datetime]] = {}\n\n def register(self, tool: AllowedTool) -> None:\n self._allowed[tool.name] = tool\n self._call_counts[tool.name] = []\n\n def validate(self, name: str, description: str) -> tuple[bool, str]:\n if name not in self._allowed:\n return False, f\"Tool '{name}' not in allowlist\"\n\n expected = self._allowed[name]\n actual_hash = sha256(description.encode('utf-8')).hexdigest()\n if actual_hash != expected.description_hash:\n return False, \"Description changed (possible rug pull)\"\n\n # Rate limit\n now = datetime.now(timezone.utc)\n recent = [t for t in self._call_counts[name]\n if (now - t).total_seconds() \u003c 60]\n if len(recent) >= expected.max_calls_per_minute:\n return False, \"Rate limit exceeded\"\n\n self._call_counts[name] = recent + [now]\n return True, \"OK\"\n```\n\n**Correct -- capability enforcement (least privilege):**\n```python\nfrom enum import Enum\n\nclass ToolCapability(Enum):\n READ_FILE = \"read:file\"\n WRITE_FILE = \"write:file\"\n EXECUTE_COMMAND = \"execute:command\"\n NETWORK_REQUEST = \"network:request\"\n\nSENSITIVE_PATHS = [\"/etc/passwd\", \"~/.ssh\", \".env\", \"credentials\"]\n\nclass CapabilityEnforcer:\n def __init__(self):\n self._declarations: dict[str, set[ToolCapability]] = {}\n\n def register(self, tool_name: str, caps: set[ToolCapability]) -> None:\n self._declarations[tool_name] = caps\n\n def check(self, tool_name: str, cap: ToolCapability, resource: str = \"\") -> tuple[bool, str]:\n if tool_name not in self._declarations:\n return False, \"No capability declaration\"\n if cap not in self._declarations[tool_name]:\n return False, f\"Capability {cap.value} not allowed\"\n if cap in (ToolCapability.READ_FILE, ToolCapability.WRITE_FILE):\n if any(s in resource for s in SENSITIVE_PATHS):\n return False, \"Sensitive path denied\"\n return True, \"Allowed\"\n```\n\n**Correct -- secure session management:**\n```python\nimport secrets\n\ndef generate_session_id() -> str:\n return secrets.token_urlsafe(32) # 256 bits of entropy\n\n# NEVER: session_id = f\"{user_id}:{auth_token}\"\n# ALWAYS: session_id = secrets.token_urlsafe(32)\n```\n\n**Rug pull detection -- hash comparison on every call:**\n```python\nclass ToolIntegrityMonitor:\n def __init__(self):\n self._fingerprints: dict[str, str] = {}\n\n def register(self, tool: dict) -> None:\n desc = tool.get(\"description\", \"\")\n params = json.dumps(tool.get(\"parameters\", {}), sort_keys=True)\n combined = sha256(f\"{desc}:{params}\".encode()).hexdigest()\n self._fingerprints[tool[\"name\"]] = combined\n\n def verify(self, tool: dict) -> tuple[bool, str | None]:\n name = tool[\"name\"]\n if name not in self._fingerprints:\n return False, \"Tool not registered\"\n desc = tool.get(\"description\", \"\")\n params = json.dumps(tool.get(\"parameters\", {}), sort_keys=True)\n current = sha256(f\"{desc}:{params}\".encode()).hexdigest()\n if current != self._fingerprints[name]:\n return False, f\"Tool '{name}' modified since registration\"\n return True, None\n```\n\n**Key rules:**\n- Every tool must be explicitly vetted before use (zero-trust)\n- Hash-verify description + parameters on every invocation\n- Use `secrets.token_urlsafe(32)` for session IDs, never embed auth tokens\n- Enforce least-privilege capabilities per tool\n- Rate limit tool calls (per-tool and per-session)\n- Auto-suspend tools that fail integrity checks\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4844,"content_sha256":"89f58fa1294d632b397fb68c85f9e095966b835c0bc8d903dcc1e62c2f3d0a8c"},{"filename":"rules/security-injection.md","content":"---\ntitle: Defend against prompt injection in MCP tool descriptions that can hijack LLM behavior\nimpact: HIGH\nimpactDescription: \"Unsanitized tool descriptions allow prompt injection attacks that can hijack LLM behavior, exfiltrate data, or override system instructions\"\ntags: security, prompt-injection, sanitization, encoding, tool-descriptions\n---\n\n## Security Injection Defense\n\nTreat ALL tool descriptions as untrusted input. Normalize encodings, detect injection patterns, and sanitize before LLM exposure.\n\n**Incorrect -- raw tool description passed to LLM:**\n```python\n# INJECTION RISK: description may contain \"ignore previous instructions...\"\nprompt = f\"Use this tool: {tool.description}\"\ntools = await mcp.list_tools() # No validation!\n```\n\n**Correct -- sanitize tool descriptions before use:**\n```python\nimport re\n\nFORBIDDEN_PATTERNS = {\n \"critical\": [\n (r\"ignore\\s+(all\\s+)?previous\", \"instruction_override\"),\n (r\"you\\s+are\\s+now\", \"role_hijack\"),\n (r\"forget\\s+(everything|all|above)\", \"context_wipe\"),\n (r\"system\\s*prompt\", \"system_access\"),\n ],\n \"high\": [\n (r\"IMPORTANT\\s*:\", \"attention_hijack\"),\n (r\"override\\s+(all\\s+)?settings\", \"config_override\"),\n (r\"\u003c\\|.*?\\|>\", \"delimiter_attack\"),\n (r\"reveal\\s+(your|the)\\s+(prompt|instructions)\", \"prompt_extraction\"),\n ],\n}\n\ndef sanitize_description(description: str) -> tuple[str, list[str]]:\n \"\"\"Sanitize tool description. Returns (sanitized, detected_threats).\"\"\"\n if not description:\n return \"\", []\n\n threats = []\n sanitized = normalize_encodings(description)\n\n for level in [\"critical\", \"high\"]:\n for pattern, name in FORBIDDEN_PATTERNS[level]:\n if re.search(pattern, sanitized, re.I):\n threats.append(f\"{level}:{name}\")\n sanitized = re.sub(pattern, \"[REDACTED]\", sanitized, flags=re.I)\n\n return sanitized.strip(), threats\n```\n\n**Correct -- normalize encodings to reveal hidden attacks:**\n```python\nimport html\nimport urllib.parse\nimport unicodedata\n\nHOMOGLYPHS = {\n '\\u0430': 'a', '\\u0435': 'e', '\\u043e': 'o',\n '\\u0440': 'p', '\\u0441': 'c', '\\u0443': 'y',\n}\n\ndef normalize_encodings(text: str) -> str:\n \"\"\"Decode HTML entities, URL encoding, hex escapes, homoglyphs.\"\"\"\n result = html.unescape(text) # I -> I\n result = urllib.parse.unquote(result) # %69 -> i\n result = re.sub( # \\x69 -> i\n r'\\\\x([0-9a-fA-F]{2})',\n lambda m: chr(int(m.group(1), 16)),\n result,\n )\n result = unicodedata.normalize('NFKC', result) # Unicode normalization\n for glyph, latin in HOMOGLYPHS.items(): # Cyrillic -> Latin\n result = result.replace(glyph, latin)\n return result\n```\n\n**Correct -- filter sensitive data from tool responses:**\n```python\nRESPONSE_FILTERS = [\n (r\"api[_-]?key\\s*[:=]\\s*\\S+\", \"[API_KEY_REDACTED]\"),\n (r\"password\\s*[:=]\\s*\\S+\", \"[PASSWORD_REDACTED]\"),\n (r\"bearer\\s+\\S+\", \"[TOKEN_REDACTED]\"),\n (r\"-----BEGIN.*KEY-----[\\s\\S]*-----END.*KEY-----\", \"[PRIVATE_KEY_REDACTED]\"),\n]\n\ndef filter_tool_response(response: str) -> str:\n for pattern, replacement in RESPONSE_FILTERS:\n response = re.sub(pattern, replacement, response, flags=re.I)\n return response\n```\n\n**Key rules:**\n- Always normalize encodings BEFORE pattern matching\n- Block on critical threats (instruction override, role hijack)\n- Redact high-severity patterns but allow the tool through\n- Filter tool responses for secrets before they reach the LLM\n- Test with known attack payloads: base64, homoglyphs, HTML entities\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3669,"content_sha256":"2e57e38fa810211717bdba4302c1cc5dc3d20167c3166b063d904899bbd5f652"},{"filename":"rules/server-setup.md","content":"---\ntitle: Set up MCP servers with proper lifecycle management and structured error handling\nimpact: HIGH\nimpactDescription: \"Missing lifecycle management causes resource leaks; wrong error handling crashes the server instead of returning useful feedback to Claude\"\ntags: server, fastmcp, lifespan, tools, resources, prompts, error-handling\n---\n\n## Server Setup\n\nUse FastMCP with lifespan context for shared resources. Define tools with explicit schemas and return errors as text content.\n\n**Incorrect -- no lifecycle, raw exception:**\n```python\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"my-server\")\ndb = Database.connect() # Global -- never cleaned up\n\[email protected]()\ndef query(sql: str) -> str:\n return db.query(sql) # Crashes on connection failure\n```\n\n**Correct -- FastMCP with lifespan and error handling:**\n```python\nfrom contextlib import asynccontextmanager\nfrom collections.abc import AsyncIterator\nfrom dataclasses import dataclass\nfrom mcp.server.fastmcp import Context, FastMCP\n\n@dataclass\nclass AppContext:\n db: Database\n cache: CacheService\n\n@asynccontextmanager\nasync def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:\n db = await Database.connect()\n cache = await CacheService.connect()\n try:\n yield AppContext(db=db, cache=cache)\n finally:\n await cache.disconnect()\n await db.disconnect()\n\nmcp = FastMCP(\"my-server\", lifespan=app_lifespan)\n\[email protected]()\ndef query(sql: str, ctx: Context) -> str:\n \"\"\"Execute a read-only SQL query. Returns up to 100 rows.\"\"\"\n try:\n app = ctx.request_context.lifespan_context\n return app.db.query(sql)\n except DatabaseError as e:\n return f\"Error: {e}\" # Claude sees and can retry\n```\n\n**Tool definition best practices:**\n```python\nfrom mcp.types import Tool\n\nTool(\n name=\"search_products\",\n description=\"Search product catalog. Returns up to 10 results.\",\n inputSchema={\n \"type\": \"object\",\n \"properties\": {\n \"query\": {\"type\": \"string\", \"description\": \"Search terms\"},\n \"category\": {\n \"type\": \"string\",\n \"enum\": [\"electronics\", \"clothing\", \"books\"],\n },\n \"max_results\": {\n \"type\": \"integer\", \"minimum\": 1, \"maximum\": 50, \"default\": 10,\n },\n },\n \"required\": [\"query\"],\n },\n)\n```\n\n**Key rules:**\n- Always use lifespan for database connections, caches, HTTP clients\n- Return errors as `TextContent` -- never raise unhandled exceptions\n- Include `description` for every schema property\n- Use `enum` for fixed option sets, `minimum`/`maximum` for numbers\n- Use `asyncio.to_thread()` for blocking synchronous operations\n- Limit response sizes (Claude has context limits). For intentionally large results (DB schemas, API specs), use `_meta[\"anthropic/maxResultSizeChars\"]` annotation (up to 500K) so clients and hooks respect the declared size instead of truncating:\n\n```python\[email protected]()\nasync def get_schema() -> dict:\n schema = await db.get_full_schema()\n return {\n \"_meta\": {\"anthropic/maxResultSizeChars\": 100000},\n \"content\": [{\"type\": \"text\", \"text\": schema}]\n }\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3177,"content_sha256":"c8a68ecc32ea14f7df99a4ce0fc64fb58b28c2afb68b1aa6e8a60c3354cf18ba"},{"filename":"rules/server-transport.md","content":"---\ntitle: Choose the right MCP transport for production multi-client and deployment requirements\nimpact: HIGH\nimpactDescription: \"Wrong transport choice leads to connection failures in production or inability to serve multiple clients\"\ntags: transport, stdio, sse, streamable-http, claude-desktop, configuration\n---\n\n## Server Transport\n\nChoose stdio for CLI/Desktop, Streamable HTTP for web apps and production multi-client. SSE is deprecated.\n\n**Transport decision matrix:**\n\n| Transport | Use Case | Pros | Cons |\n|-----------|----------|------|------|\n| stdio | CLI, Claude Desktop | Simple, no network | Single client only |\n| SSE | **Deprecated** | Browser-compatible | Deprecated since March 2025 |\n| Streamable HTTP | Web apps, production APIs | Multi-client, scalable, stateless option | More setup |\n\n**Incorrect -- hardcoded transport, no configuration:**\n```python\n# Forces stdio -- can't switch to web deployment\nfrom mcp.server.stdio import stdio_server\n\nasync def main():\n async with stdio_server() as (read, write):\n await server.run(read, write, server.create_initialization_options())\n```\n\n**Correct -- Python stdio server:**\n```python\nfrom mcp.server import Server\nfrom mcp.server.stdio import stdio_server\n\nserver = Server(\"my-tools\")\n\n# Register handlers...\n\nasync def main():\n async with stdio_server() as (read, write):\n await server.run(read, write, server.create_initialization_options())\n\nif __name__ == \"__main__\":\n import asyncio\n asyncio.run(main())\n```\n\n**Correct -- TypeScript stdio server:**\n```typescript\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nconst server = new Server(\n { name: \"my-tools\", version: \"1.0.0\" },\n { capabilities: { tools: {} } }\n);\n\n// Register handlers...\n\nawait server.connect(new StdioServerTransport());\n```\n\n**Deprecated -- SSE for web deployment (use Streamable HTTP instead):**\n\n> SSE transport was deprecated in March 2025. Migrate to Streamable HTTP for\n> new projects. SSE remains functional but receives no new features.\n\n```python\nfrom mcp.server.sse import SseServerTransport\nfrom starlette.applications import Starlette\nfrom starlette.routing import Route\n\nsse = SseServerTransport(\"/messages\")\n\nasync def handle_sse(request):\n async with sse.connect_sse(\n request.scope, request.receive, request._send\n ) as streams:\n await server.run(\n streams[0], streams[1],\n server.create_initialization_options()\n )\n\napp = Starlette(routes=[\n Route(\"/sse\", endpoint=handle_sse),\n Route(\"/messages\", endpoint=sse.handle_post_message, methods=[\"POST\"]),\n])\n```\n\n**Correct -- Streamable HTTP server (Python, recommended):**\n```python\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(\"my-tools\")\n\[email protected]()\ndef greet(name: str = \"World\") -> str:\n \"\"\"Greet someone by name.\"\"\"\n return f\"Hello, {name}!\"\n\nif __name__ == \"__main__\":\n # Stateless with JSON responses -- best for production\n mcp.run(transport=\"streamable-http\", stateless_http=True, json_response=True)\n # Stateful with session persistence (when needed):\n # mcp.run(transport=\"streamable-http\")\n```\n\n**Correct -- Streamable HTTP server (TypeScript, recommended):**\n```typescript\nimport { createServer } from \"node:http\";\nimport { NodeStreamableHTTPServerTransport } from \"@modelcontextprotocol/node\";\nimport { McpServer } from \"@modelcontextprotocol/server\";\n\nconst server = new McpServer({ name: \"my-tools\", version: \"1.0.0\" });\n\n// Register handlers...\n\ncreateServer(async (req, res) => {\n const transport = new NodeStreamableHTTPServerTransport({\n sessionIdGenerator: undefined, // stateless; use () => randomUUID() for sessions\n });\n await server.connect(transport);\n await transport.handleRequest(req, res);\n}).listen(3000);\n```\n\n**Migrating SSE → Streamable HTTP:**\n- Python: Replace `SseServerTransport` with `MCPServer.run(transport=\"streamable-http\")`\n- TypeScript: Replace `SSEServerTransport` with `NodeStreamableHTTPServerTransport`\n- Client endpoint changes from `/sse` + `/messages` to single `/mcp` path\n- Streamable HTTP supports both stateless (scalable) and stateful (session) modes\n\n**Claude Desktop configuration:**\n```json\n{\n \"mcpServers\": {\n \"my-tools\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@myorg/my-tools\"],\n \"env\": { \"DATABASE_URL\": \"postgres://...\" }\n },\n \"python-tools\": {\n \"command\": \"uv\",\n \"args\": [\"run\", \"python\", \"-m\", \"my_mcp_server\"],\n \"cwd\": \"/path/to/project\"\n }\n }\n}\n```\n\n**Key rules:**\n- Use Streamable HTTP for all new web/production deployments (SSE is deprecated)\n- Use `uv` (not `pip`) for Python MCP server commands in Claude Desktop config\n- Set `cwd` when the server needs access to project files\n- Pass secrets via `env`, never hardcode in args\n- TypeScript servers: use `npx -y` for zero-install execution\n- Prefer stateless mode (`stateless_http=True`) unless session persistence is required\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5031,"content_sha256":"8e54a225e085bd7843652c3db85ef353fb8c683a9fd7923e5f1909e9f8674ab5"},{"filename":"rules/testing-debugging.md","content":"---\ntitle: Test and debug MCP servers to catch broken tools and transport failures before production\nimpact: \"MEDIUM\"\nimpactDescription: \"Untested MCP servers ship broken tools and opaque transport failures that are impossible to diagnose in production\"\ntags: [testing, debugging, pytest, inspector, fixtures]\n---\n\n## Testing & Debugging\n\nWrite automated tests for every tool using the SDK's in-process `Client`, and use MCP Inspector for interactive debugging of transports and auth.\n\n**Incorrect -- manual testing only, no assertions:**\n```python\n# \"I'll just test it in Claude Desktop\"\nmcp = FastMCP(\"my-server\")\n\[email protected]()\ndef search(query: str) -> str:\n return db.search(query)\n\n# No tests, no fixtures, no CI -- bugs found by end users\n```\n\n**Correct -- unit tests with in-process Client:**\n```python\nimport pytest\nfrom mcp import Client\nfrom mcp.types import CallToolResult, TextContent\nfrom server import app\n\[email protected]\ndef anyio_backend():\n return \"asyncio\"\n\[email protected]\nasync def client():\n async with Client(app, raise_exceptions=True) as c:\n yield c\n\[email protected]\nasync def test_search_returns_results(client: Client):\n result = await client.call_tool(\"search\", {\"query\": \"test\"})\n assert isinstance(result, CallToolResult)\n assert len(result.content) > 0\n assert result.content[0].type == \"text\"\n\[email protected]\nasync def test_search_empty_query(client: Client):\n result = await client.call_tool(\"search\", {\"query\": \"\"})\n assert \"Error\" in result.content[0].text # Graceful error, not crash\n```\n\n**Correct -- parametrized edge-case tests:**\n```python\[email protected]\[email protected](\"args\", [{\"query\": \"\"}, {\"max_results\": -1}, {}])\nasync def test_invalid_inputs_return_errors(client: Client, args):\n result = await client.call_tool(\"search\", args)\n assert result.isError or \"Error\" in result.content[0].text\n```\n\n**Correct -- integration test with stdio transport:**\n```python\nimport subprocess, json\n\ndef test_stdio_transport_connects():\n \"\"\"Verify the server starts and responds to initialize over stdio.\"\"\"\n proc = subprocess.Popen(\n [\"uv\", \"run\", \"server.py\"],\n stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n )\n init_msg = {\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"initialize\",\n \"params\": {\"capabilities\": {}, \"clientInfo\": {\"name\": \"test\"},\n \"protocolVersion\": \"2025-03-26\"}}\n proc.stdin.write(json.dumps(init_msg).encode() + b\"\\n\")\n proc.stdin.flush()\n line = proc.stdout.readline()\n assert b'\"result\"' in line # Server responded to init\n proc.terminate()\n```\n\n**Interactive debugging with MCP Inspector:**\n```bash\n# Inspect a local Python server\nnpx @modelcontextprotocol/inspector uv run server.py\n\n# Inspect a PyPI package\nnpx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/repo\n\n# Inspect with environment variables\nnpx @modelcontextprotocol/inspector -e API_KEY=xxx uv run server.py\n\n# Use Inspector to: list tools/resources, test tool calls with custom\n# inputs, check capability negotiation, and view server logs.\n# For scaffolding new servers, see the mcp-builder skill.\n```\n\n**Debug common connection failures:**\n```bash\n# Timeout: slow lifespan init blocks connection -- keep lifespan under 5s\n# Auth 401: pass secrets via Inspector's -e flag or .env file\n# \"Connection refused\": wrong transport -- match stdio vs Streamable HTTP\n# Hang on tool call: blocking sync code -- wrap with asyncio.to_thread()\n```\n\n**Key rules:**\n- Use `Client(app, raise_exceptions=True)` for unit tests -- no transport overhead\n- Test both valid inputs and edge cases (empty, missing, out-of-range)\n- Use `@pytest.mark.anyio` with `anyio_backend` fixture for async tests\n- Use MCP Inspector (`npx @modelcontextprotocol/inspector`) for interactive debugging\n- Keep lifespan initialization under 5s so Inspector and clients can connect\n- Test stdio transport separately with `subprocess` for integration coverage\n- Install test deps: `pip install inline-snapshot pytest anyio`\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4082,"content_sha256":"7086a9d6427f113f071e61fb7425f9747b3bf105c1606eb064ac0ba93e3be39a"},{"filename":"rules/webmcp-browser.md","content":"---\ntitle: Integrate WebMCP browser mediation correctly to avoid confusing it with standard MCP\nimpact: LOW\nimpactDescription: \"Confusing WebMCP with MCP leads to wrong transport choice; skipping browser mediation exposes tools without user consent\"\ntags: [webmcp, browser, navigator, client-side, w3c]\n---\n\n## WebMCP Browser Integration\n\nWebMCP is a W3C Community Group standard that exposes structured tools to AI agents inside the browser via `navigator.modelContext`. It complements MCP (not a replacement) — MCP handles AI-to-backend over JSON-RPC, WebMCP handles AI-to-browser-UI via in-page callbacks.\n\n**Incorrect -- registering tools without input schema or user mediation:**\n```typescript\n// No schema, no description, no user interaction handling\nnavigator.modelContext.registerTool({\n name: \"submit-order\",\n description: \"Submit order\",\n execute: async (input) => {\n // Directly mutates state with no user confirmation\n await fetch(\"/api/orders\", { method: \"POST\", body: JSON.stringify(input) });\n return { status: \"submitted\" };\n },\n});\n```\n\n**Correct -- full schema, annotations, and user interaction request:**\n```typescript\nnavigator.modelContext.registerTool({\n name: \"submit-order\",\n description: \"Submit the current shopping cart as an order. Requires user confirmation.\",\n inputSchema: {\n type: \"object\",\n properties: {\n cartId: { type: \"string\", description: \"Cart identifier\" },\n shipping: { type: \"string\", enum: [\"standard\", \"express\"] },\n },\n required: [\"cartId\"],\n },\n annotations: { readOnlyHint: false },\n execute: async (input, client) => {\n // Request explicit user confirmation before mutating state\n const confirmed = await client.requestUserInteraction(async () => {\n return window.confirm(`Place order for cart ${input.cartId}?`);\n });\n if (!confirmed) return { status: \"cancelled_by_user\" };\n const res = await fetch(\"/api/orders\", {\n method: \"POST\",\n body: JSON.stringify(input),\n });\n return { status: \"submitted\", orderId: (await res.json()).id };\n },\n});\n```\n\n**Read-only tool with annotations:**\n```typescript\nnavigator.modelContext.registerTool({\n name: \"get-product-details\",\n description: \"Retrieve product name, price, and availability from the current page.\",\n inputSchema: {\n type: \"object\",\n properties: {\n productId: { type: \"string\", description: \"Product ID visible on page\" },\n },\n required: [\"productId\"],\n },\n annotations: { readOnlyHint: true },\n execute: async (input) => {\n const el = document.querySelector(`[data-product-id=\"${input.productId}\"]`);\n return el ? { name: el.dataset.name, price: el.dataset.price } : { error: \"Not found\" };\n },\n});\n```\n\n**When to use MCP vs WebMCP:**\n\n| Concern | MCP | WebMCP |\n|---------|-----|--------|\n| Transport | JSON-RPC (stdio / SSE / HTTP) | In-page callbacks |\n| Runs on | Server / backend | Browser (SecureContext) |\n| Use case | DB queries, APIs, file I/O | DOM access, form fill, UI actions |\n| Auth | OAuth 2.1 / tokens | Browser-mediated permission |\n\n**Key rules:**\n- WebMCP complements MCP — use MCP for backend services, WebMCP for browser-side UI tools\n- Always provide `inputSchema` with property descriptions so agents understand parameters\n- Set `annotations.readOnlyHint: true` on tools that only read data (no side effects)\n- Use `client.requestUserInteraction()` before any state-mutating operation\n- WebMCP requires `SecureContext` (HTTPS only) — `navigator.modelContext` is undefined on HTTP\n- Call `unregisterTool(name)` or `clearContext()` during SPA route teardown to prevent stale tools\n- Keep tool descriptions specific — agents select tools by description, not by probing\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3716,"content_sha256":"3c7bd4bb9d4bffa6584b7427f32756f1d4f9e60e3f99355e2d420a4c5e85401c"},{"filename":"test-cases.json","content":"{\n \"skill\": \"mcp-patterns\",\n \"version\": \"1.0.0\",\n \"testCases\": [\n {\n \"id\": \"negative-build-an-mcp-server\",\n \"rule\": \"\",\n \"query\": \"Build an MCP server that exposes a document search tool and a user profile resource. It needs OAuth 2.1 authentication and should use Streamable HTTP transport for production deployment.\",\n \"expectedBehavior\": [\n \"Claude uses FastMCP with lifespan for resource management\",\n \"Claude implements @mcp.tool() for the search tool with input validation\",\n \"Claude implements a Resource for user profiles\",\n \"Claude configures OAuth 2.1 with PKCE (S256) and RFC 8707 resource indicators\",\n \"Claude uses Streamable HTTP transport (not deprecated SSE)\",\n \"Claude returns errors as text content for Claude to interpret\",\n \"Claude does NOT return secrets or API keys in tool output\"\n ]\n },\n {\n \"id\": \"negative-im-worried-about-prompt\",\n \"rule\": \"\",\n \"query\": \"I'm worried about prompt injection attacks on my MCP server. How do I harden the tool descriptions and implement zero-trust verification for tool invocations?\",\n \"expectedBehavior\": [\n \"Claude applies description sanitization and encoding normalization\",\n \"Claude implements zero-trust allowlist for tool invocations\",\n \"Claude adds hash verification to detect rug pull attacks on tool definitions\",\n \"Claude references rules/security-injection.md and rules/security-hardening.md patterns\",\n \"Claude does NOT trust tool descriptions without sanitization\"\n ]\n },\n {\n \"id\": \"negative-create-a-rest-api\",\n \"rule\": \"\",\n \"query\": \"Create a REST API with Express.js for a blog application with CRUD endpoints for posts and comments.\",\n \"expectedBehavior\": [\n \"Claude does NOT invoke the mcp-patterns skill\",\n \"Claude builds a standard Express.js REST API\",\n \"Claude does not use FastMCP, MCP tools, or MCP transport patterns for a regular REST API\"\n ]\n },\n {\n \"id\": \"advanced-composition-pipeline-happy-path\",\n \"rule\": \"advanced-composition\",\n \"query\": \"I need to chain three MCP tools in sequence: first search documents, then extract entities, then summarize findings. How do I compose them with proper error handling?\",\n \"expectedBehavior\": [\n \"Claude implements a pipeline composition pattern with sequential tool calls\",\n \"Claude adds error propagation so pipeline stops on first failure\",\n \"Claude wraps each tool call in try/except to capture error context\",\n \"Claude keeps composition depth shallow at three to four steps maximum\"\n ]\n },\n {\n \"id\": \"advanced-resources-caching-happy-path\",\n \"rule\": \"advanced-resources\",\n \"query\": \"My MCP server fetches user profiles from the database on every request. How do I add resource caching with TTL and memory limits to avoid redundant calls?\",\n \"expectedBehavior\": [\n \"Claude implements a resource manager with TTL-based cache expiration\",\n \"Claude adds LRU eviction with max_cache_size and max_memory_bytes caps\",\n \"Claude uses asyncio.Lock for thread-safe concurrent cache access\",\n \"Claude integrates cleanup_expired into FastMCP lifespan for proper lifecycle management\"\n ]\n },\n {\n \"id\": \"apps-ui-sandbox-happy-path\",\n \"rule\": \"apps-ui\",\n \"query\": \"I want my MCP tool to return an interactive React dashboard that loads data from an external API. How do I set up the UI resource with proper sandboxing and CSP?\",\n \"expectedBehavior\": [\n \"Claude uses registerAppTool and registerAppResource from ext-apps/server package\",\n \"Claude declares CSP domains with connectDomains for external API access\",\n \"Claude uses the ui:// URI scheme with text/html;profile=mcp-app mimeType\",\n \"Claude links the tool to the UI via _meta.ui.resourceUri on the tool definition\"\n ]\n },\n {\n \"id\": \"client-patterns-session-happy-path\",\n \"rule\": \"client-patterns\",\n \"query\": \"I need to build a Python MCP client that connects to a remote server, discovers available tools, and handles reconnections gracefully. What's the proper setup?\",\n \"expectedBehavior\": [\n \"Claude uses async context managers for session and transport cleanup\",\n \"Claude calls session.initialize() before any other session method\",\n \"Claude discovers tools with list_tools() before calling any specific tool\",\n \"Claude checks content type before accessing .text on tool call results\"\n ]\n },\n {\n \"id\": \"elicitation-form-mode-happy-path\",\n \"rule\": \"elicitation\",\n \"query\": \"My MCP tool needs to collect search preferences from the user at runtime, like category and max results. How do I use MCP elicitation safely without exposing sensitive data?\",\n \"expectedBehavior\": [\n \"Claude uses form mode elicitation with a flat JSON Schema for non-sensitive data\",\n \"Claude handles all three response actions: accept, decline, and cancel\",\n \"Claude does not request secrets like API keys via form mode\",\n \"Claude uses URL mode for any sensitive data collection such as credentials\"\n ]\n },\n {\n \"id\": \"registry-discovery-vetting-happy-path\",\n \"rule\": \"registry-discovery\",\n \"query\": \"I found an MCP server for database access on a community forum. How do I properly vet it before installing and should I use the official MCP registry instead?\",\n \"expectedBehavior\": [\n \"Claude queries the official MCP Registry API at registry.modelcontextprotocol.io first\",\n \"Claude applies the vetting checklist: public repo, package registry, version pinning\",\n \"Claude warns against installing unvetted servers without source code review\",\n \"Claude recommends pinning exact versions and never using @latest in production\"\n ]\n },\n {\n \"id\": \"sampling-tools-bounded-loop-happy-path\",\n \"rule\": \"sampling-tools\",\n \"query\": \"I want my MCP server to request LLM completions from the client with tool access for an agentic loop. How do I implement sampling with tools safely?\",\n \"expectedBehavior\": [\n \"Claude implements a bounded iteration loop with a maximum iteration cap\",\n \"Claude uses toolChoice mode none on the final turn to force a text response\",\n \"Claude ensures tool result messages contain only tool_result blocks without mixing content types\",\n \"Claude checks that the client declares sampling.tools capability before sending requests\"\n ]\n },\n {\n \"id\": \"security-hardening-zero-trust-happy-path\",\n \"rule\": \"security-hardening\",\n \"query\": \"How do I implement zero-trust tool verification for my MCP client to prevent rug-pull attacks where a server silently changes tool behavior between calls?\",\n \"expectedBehavior\": [\n \"Claude implements hash-based integrity checks on tool descriptions and parameters\",\n \"Claude builds a tool allowlist that validates tools before every invocation\",\n \"Claude uses secrets.token_urlsafe(32) for session IDs instead of embedding auth tokens\",\n \"Claude adds rate limiting per tool and per session to prevent abuse\"\n ]\n },\n {\n \"id\": \"security-injection-sanitization-happy-path\",\n \"rule\": \"security-injection\",\n \"query\": \"How do I protect my MCP client from prompt injection attacks hidden in tool descriptions using Unicode homoglyphs and HTML entity encoding?\",\n \"expectedBehavior\": [\n \"Claude normalizes encodings including HTML entities, URL encoding, and homoglyphs before pattern matching\",\n \"Claude detects and redacts forbidden injection patterns like instruction overrides and role hijacks\",\n \"Claude filters tool responses for leaked secrets such as API keys and passwords\",\n \"Claude tests sanitization with known attack payloads including base64 and Unicode variants\"\n ]\n },\n {\n \"id\": \"server-setup-lifespan-happy-path\",\n \"rule\": \"server-setup\",\n \"query\": \"I'm building an MCP server with FastMCP that needs a database connection and cache. How do I set up proper lifecycle management so resources are cleaned up on shutdown?\",\n \"expectedBehavior\": [\n \"Claude uses an asynccontextmanager lifespan function for shared resource initialization\",\n \"Claude yields an AppContext dataclass from the lifespan to share resources across tools\",\n \"Claude returns errors as TextContent instead of raising unhandled exceptions\",\n \"Claude includes description for every input schema property with enum and min/max constraints\"\n ]\n },\n {\n \"id\": \"server-transport-selection-happy-path\",\n \"rule\": \"server-transport\",\n \"query\": \"I need to deploy my MCP server for a web application that serves multiple clients simultaneously. Should I use stdio, SSE, or Streamable HTTP transport?\",\n \"expectedBehavior\": [\n \"Claude recommends Streamable HTTP for web and production multi-client deployments\",\n \"Claude explicitly warns that SSE transport is deprecated since March 2025\",\n \"Claude shows stateless mode with stateless_http=True for scalable production use\",\n \"Claude passes secrets via env configuration and never hardcodes them in args\"\n ]\n },\n {\n \"id\": \"testing-debugging-unit-tests-happy-path\",\n \"rule\": \"testing-debugging\",\n \"query\": \"How do I write automated tests for my MCP server's tools? I've been manually testing them in Claude Desktop but want proper CI coverage.\",\n \"expectedBehavior\": [\n \"Claude uses the SDK's in-process Client with raise_exceptions=True for unit tests\",\n \"Claude writes parametrized edge-case tests for invalid inputs like empty and missing values\",\n \"Claude uses @pytest.mark.anyio with anyio_backend fixture for async test execution\",\n \"Claude recommends MCP Inspector for interactive debugging of transports and auth\"\n ]\n },\n {\n \"id\": \"webmcp-browser-integration-happy-path\",\n \"rule\": \"webmcp-browser\",\n \"query\": \"I want to expose browser-side tools like form filling and DOM access to AI agents on my web app. Should I use MCP or is there something else for in-browser tools?\",\n \"expectedBehavior\": [\n \"Claude recommends WebMCP for browser-side UI tools using navigator.modelContext\",\n \"Claude explains that WebMCP complements MCP and is not a replacement for backend tools\",\n \"Claude requires inputSchema with property descriptions so agents understand tool parameters\",\n \"Claude uses client.requestUserInteraction() before any state-mutating browser operation\"\n ]\n },\n {\n \"id\": \"auth-oauth21-pkce-happy-path\",\n \"rule\": \"auth-oauth21\",\n \"query\": \"I need to add OAuth 2.1 authentication to my MCP server. How do I implement PKCE with S256 and prevent confused deputy attacks when calling upstream APIs?\",\n \"expectedBehavior\": [\n \"Claude implements PKCE with S256 code challenge method for the authorization request\",\n \"Claude includes the RFC 8707 resource parameter bound to the MCP server's canonical URI\",\n \"Claude validates the aud claim on the resource server to reject tokens meant for other services\",\n \"Claude obtains a separate token via client credentials for upstream API calls instead of forwarding client tokens\"\n ]\n }\n ]\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":11484,"content_sha256":"26bac401f21e9a3a4ae1b3a014190ce59e6a0f26213a24e442e7032fb97fa88e"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"MCP Patterns","type":"text"}]},{"type":"paragraph","content":[{"text":"Patterns for building, composing, and securing Model Context Protocol servers. Based on the ","type":"text"},{"text":"2025-11-25 specification","type":"text","marks":[{"type":"strong"}]},{"text":" — the latest stable release maintained by the ","type":"text"},{"text":"Agentic AI Foundation","type":"text","marks":[{"type":"link","attrs":{"href":"https://agenticaifoundation.org/","title":null}}]},{"text":" (Linux Foundation), co-founded by Anthropic, Block, and OpenAI.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Scaffolding a new server?","type":"text","marks":[{"type":"strong"}]},{"text":" Use Anthropic's ","type":"text"},{"text":"mcp-builder","type":"text","marks":[{"type":"code_inline"}]},{"text":" skill (","type":"text"},{"text":"claude install anthropics/skills","type":"text","marks":[{"type":"code_inline"}]},{"text":") for project setup and evaluation creation. This skill focuses on ","type":"text"},{"text":"patterns, security, and advanced features","type":"text","marks":[{"type":"strong"}]},{"text":" after initial setup.","type":"text"}]},{"type":"paragraph","content":[{"text":"Deploying to Cloudflare?","type":"text","marks":[{"type":"strong"}]},{"text":" See the ","type":"text"},{"text":"building-mcp-server-on-cloudflare","type":"text","marks":[{"type":"code_inline"}]},{"text":" skill for Workers-specific deployment patterns.","type":"text"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Decision Tree — Which Rule to Read","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"What are you building?\n│\n├── New MCP server\n│ ├── Setup & primitives ──────► rules/server-setup.md\n│ ├── Transport selection ─────► rules/server-transport.md\n│ └── Scaffolding ─────────────► mcp-builder skill (anthropics/skills)\n│\n├── Authentication & authorization\n│ └── OAuth 2.1 + OIDC ───────► rules/auth-oauth21.md\n│\n├── Advanced server features\n│ ├── Tool composition ────────► rules/advanced-composition.md\n│ ├── Resource caching ────────► rules/advanced-resources.md\n│ ├── Elicitation (user input) ► rules/elicitation.md\n│ ├── Sampling (agent loops) ──► rules/sampling-tools.md\n│ └── Interactive UI ──────────► rules/apps-ui.md\n│\n├── Client-side consumption\n│ └── Connecting to servers ───► rules/client-patterns.md\n│\n├── Security hardening\n│ ├── Prompt injection defense ► rules/security-injection.md\n│ └── Zero-trust & verification ► rules/security-hardening.md\n│\n├── Testing & debugging\n│ └── Inspector + unit tests ──► rules/testing-debugging.md\n│\n├── Discovery & ecosystem\n│ └── Registries & catalogs ──► rules/registry-discovery.md\n│\n└── Browser-native tools\n └── WebMCP (W3C) ───────────► rules/webmcp-browser.md","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Reference","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Category","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rule","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Impact","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Key Pattern","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Server","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"server-setup.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FastMCP lifespan, Tool/Resource/Prompt primitives","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Server","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"server-transport.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"stdio for CLI, Streamable HTTP for production","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Auth","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"auth-oauth21.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PKCE, RFC 8707 resource indicators, token validation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Advanced","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"advanced-composition.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pipeline, parallel, and branching tool composition","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Advanced","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"advanced-resources.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Resource caching with TTL, LRU eviction, lifecycle","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Advanced","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"elicitation.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Server-initiated structured input from users","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Advanced","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sampling-tools.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Server-side agent loops with tool calling","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Advanced","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"apps-ui.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Interactive UI via MCP Apps + @mcp-ui/* SDK","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Client","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"client-patterns.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TypeScript/Python MCP client connection patterns","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Security","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"security-injection.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description sanitization, encoding normalization","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Security","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"security-hardening.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Zero-trust allowlist, hash verification, rug pull detection","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Quality","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"testing-debugging.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MCP Inspector, unit tests, transport debugging","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ecosystem","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"registry-discovery.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LOW","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Official registry API, server metadata","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ecosystem","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"webmcp-browser.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LOW","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"W3C browser-native agent tools (complementary)","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Total: 14 rules across 6 categories","type":"text","marks":[{"type":"strong"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Key Decisions","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Decision","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Recommendation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Transport","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"stdio for CLI/Desktop, Streamable HTTP for production (SSE deprecated)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Language","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TypeScript for production (better SDK support, type safety)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Auth","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OAuth 2.1 with PKCE (S256) + RFC 8707 resource indicators","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Server lifecycle","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Always use FastMCP lifespan for resource management","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error handling","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Return errors as text content (Claude can interpret and retry)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tool composition","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pipeline for sequential, ","type":"text"},{"text":"asyncio.gather","type":"text","marks":[{"type":"code_inline"}]},{"text":" for parallel","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Resource caching","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TTL + LRU eviction with memory cap","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tool trust model","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Zero-trust: explicit allowlist + hash verification","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"User input","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Elicitation for runtime input; never request PII via elicitation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Interactive UI","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MCP Apps with @mcp-ui/* SDK; sandbox all iframes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Token handling","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Never pass through client tokens to downstream services","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Large results","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"_meta[\"anthropic/maxResultSizeChars\"]","type":"text","marks":[{"type":"code_inline"}]},{"text":" annotation (up to 500K) for results that lose meaning when truncated (CC 2.1.91)","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Spec & Governance","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Protocol","type":"text","marks":[{"type":"strong"}]},{"text":": Model Context Protocol, spec version ","type":"text"},{"text":"2025-11-25","type":"text","marks":[{"type":"strong"}]},{"text":" (latest stable)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Governance","type":"text","marks":[{"type":"strong"}]},{"text":": Agentic AI Foundation (Linux Foundation, Dec 2025)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Platinum members","type":"text","marks":[{"type":"strong"}]},{"text":": AWS, Anthropic, Block, Bloomberg, Cloudflare, Google, Microsoft, OpenAI","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Adoption","type":"text","marks":[{"type":"strong"}]},{"text":": 10,000+ servers; Claude, Cursor, Copilot, Gemini, ChatGPT, VS Code","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Spec URL","type":"text","marks":[{"type":"strong"}]},{"text":": https://modelcontextprotocol.io/specification/2025-11-25","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"2026 model","type":"text","marks":[{"type":"strong"}]},{"text":": Working Groups and Interest Groups are now the primary vehicle for protocol evolution (no more milestone-based releases). Enterprise readiness lands as extensions, not core spec changes.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Feature Maturity","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Feature","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Spec Version","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Status","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tools, Resources, Prompts","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2024-11-05","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stable","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Streamable HTTP transport","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2025-03-26","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stable (replaces SSE)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OAuth 2.1 + Elicitation (form)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2025-06-18","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stable","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sampling with tool calling","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2025-11-25","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stable","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Elicitation URL mode","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2025-11-25","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stable","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MCP Apps (UI extension)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2026-01-26","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Extension (ext-apps)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"WebMCP (browser-native)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2026-02-14","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"W3C Community Draft","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"SDK landscape (2026-Q2)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Package","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"What it is","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"When to use","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"mcp","type":"text","marks":[{"type":"code_inline"}]},{"text":" (PyPI) ","type":"text"},{"text":">=1.27","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Official Python SDK — includes the ","type":"text"},{"text":"FastMCP","type":"text","marks":[{"type":"code_inline"}]},{"text":" helper, transport adapters, Inspector","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"New Python servers. This is the canonical package.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"@modelcontextprotocol/sdk","type":"text","marks":[{"type":"code_inline"}]},{"text":" (npm) ","type":"text"},{"text":">=1.29","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Official TypeScript SDK","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"New TS servers","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"fastmcp","type":"text","marks":[{"type":"code_inline"}]},{"text":" (PyPI)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Standalone fork by jlowin — predates ","type":"text"},{"text":"mcp","type":"text","marks":[{"type":"code_inline"}]},{"text":"; API-compatible but diverges on lifespan and middleware","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Existing projects pinned to it. New projects should prefer ","type":"text"},{"text":"mcp","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"fastmcp","type":"text","marks":[{"type":"code_inline"}]},{"text":" fork and the ","type":"text"},{"text":"mcp.server.fastmcp","type":"text","marks":[{"type":"code_inline"}]},{"text":" module are ","type":"text"},{"text":"not the same package","type":"text","marks":[{"type":"em"}]},{"text":". Imports and ","type":"text"},{"text":"pyproject.toml","type":"text","marks":[{"type":"code_inline"}]},{"text":" entries must agree or stacktraces become cryptic.","type":"text"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Debugging with Claude Code","type":"text"}]},{"type":"paragraph","content":[{"text":"Pass ","type":"text"},{"text":"--mcp-debug","type":"text","marks":[{"type":"code_inline"}]},{"text":" to Claude Code when troubleshooting server wiring — it surfaces the raw JSON-RPC frames, handshake failures, and tool-registration events that the default logger swallows:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"claude --mcp-debug \"query the local test server\"\n# or per-session:\nexport CLAUDE_MCP_DEBUG=1","type":"text"}]},{"type":"paragraph","content":[{"text":"Use alongside the MCP Inspector (","type":"text"},{"text":"npx @modelcontextprotocol/inspector \u003ccmd>","type":"text","marks":[{"type":"code_inline"}]},{"text":") — Inspector gives you the client-side frame view, ","type":"text"},{"text":"--mcp-debug","type":"text","marks":[{"type":"code_inline"}]},{"text":" gives you what Claude actually saw.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"CC 2.1.128 — reconnect tool summarization","type":"text","marks":[{"type":"strong"}]},{"text":": when a server reconnects mid-session, re-announced tools are summarized as ","type":"text"},{"text":"mcp__\u003cserver>__* (N tools re-registered)","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead of being enumerated line-by-line. Use the ","type":"text"},{"text":"initial connect","type":"text","marks":[{"type":"strong"}]},{"text":" event as the source of truth for tool inventory; treat reconnect summaries as deltas only. See ","type":"text"},{"text":"references/mcp-audit-runbook.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for grep recipes that work across both formats.","type":"text"}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"CC 2.1.133 — MCP OAuth honors HTTP(S)_PROXY / NO_PROXY / mTLS","type":"text","marks":[{"type":"strong"}]},{"text":": the full MCP OAuth flow (discovery, dynamic client registration, token exchange, token refresh) now respects standard proxy and client-certificate env vars end-to-end. Enterprise deployments behind corporate proxies no longer need OAuth-specific workarounds — the same ","type":"text"},{"text":"HTTPS_PROXY","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"NO_PROXY","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"NODE_EXTRA_CA_CERTS","type":"text","marks":[{"type":"code_inline"}]},{"text":" config that already routes MCP transport now also routes auth. See ","type":"text"},{"text":"configure/references/cc-version-settings.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (CC 2.1.133 section) for the env-var example. The companion deployment skill ","type":"text"},{"text":"building-mcp-server-on-cloudflare","type":"text","marks":[{"type":"code_inline"}]},{"text":" can drop any prior \"proxy-aware OAuth requires manual handling\" caveat at this floor.","type":"text"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Example","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"my-server\")\n\[email protected]()\nasync def search(query: str) -> str:\n \"\"\"Search documents. Returns matching results.\"\"\"\n results = await db.search(query)\n return \"\\n\".join(r.title for r in results[:10])","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Mistakes","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No lifecycle management (connection/resource leaks on shutdown)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Missing input validation on tool arguments","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Returning secrets in tool output (API keys, credentials)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Unbounded response sizes without ","type":"text"},{"text":"_meta","type":"text","marks":[{"type":"code_inline"}]},{"text":" annotation — use ","type":"text"},{"text":"_meta[\"anthropic/maxResultSizeChars\"]","type":"text","marks":[{"type":"code_inline"}]},{"text":" to declare intentionally large results (DB schemas, API specs) so clients/hooks don't truncate them","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Trusting tool descriptions without sanitization (injection risk)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No hash verification on tool invocations (rug pull vulnerability)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Storing auth tokens in session IDs (credential leak)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Blocking synchronous code in async server (use ","type":"text"},{"text":"asyncio.to_thread()","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Using SSE transport instead of Streamable HTTP (deprecated since March 2025)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Passing through client tokens to downstream services (confused deputy)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Ecosystem","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Resource","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"What For","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"mcp-builder","type":"text","marks":[{"type":"code_inline"}]},{"text":" skill (anthropics/skills)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Scaffold new MCP servers + create evals","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"building-mcp-server-on-cloudflare","type":"text","marks":[{"type":"code_inline"}]},{"text":" skill","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Deploy MCP servers on Cloudflare Workers","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"@mcp-ui/*","type":"text","marks":[{"type":"code_inline"}]},{"text":" packages (npm)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Implement MCP Apps UI standard","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MCP Registry","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Discover servers: https://registry.modelcontextprotocol.io/","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MCP Inspector","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Debug and test servers interactively","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Related Skills","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ork:llm-integration","type":"text","marks":[{"type":"code_inline"}]},{"text":" — LLM function calling patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ork:security-patterns","type":"text","marks":[{"type":"code_inline"}]},{"text":" — General input sanitization and layered security","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ork:api-design","type":"text","marks":[{"type":"code_inline"}]},{"text":" — REST/GraphQL API design patterns","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"mcp-patterns","tags":["mcp","server","tools","resources","security","prompt-injection","oauth","elicitation","sampling","mcp-apps","fastmcp"],"paths":[".mcp.json","**/*.mcp.json"],"author":"@skillopedia","effort":"high","source":{"stars":180,"repo_name":"orchestkit","origin_url":"https://github.com/yonatangross/orchestkit/blob/HEAD/src/skills/mcp-patterns/SKILL.md","repo_owner":"yonatangross","body_sha256":"22dbae990d0faae215c019ee92a9c67d41a7064f79b04e937da5661577906bd9","cluster_key":"a734e281d246a0dad44aa4259c2877bb0260e22b0b123caba4af694245913561","clean_bundle":{"format":"clean-skill-bundle-v1","source":"yonatangross/orchestkit/src/skills/mcp-patterns/SKILL.md","attachments":[{"id":"a0297179-eda8-5a7a-b6ed-7c657a9ba89d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a0297179-eda8-5a7a-b6ed-7c657a9ba89d/attachment.md","path":"checklists/mcp-server-checklist.md","size":1152,"sha256":"e5c5ab61c8cebfd3212030e366301f96c0365f7cf16d1933ea4de2b5b7263793","contentType":"text/markdown; charset=utf-8"},{"id":"a4cb52c0-1f2f-51ef-9287-f1d43fd55c53","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a4cb52c0-1f2f-51ef-9287-f1d43fd55c53/attachment.md","path":"references/mcp-audit-runbook.md","size":4804,"sha256":"f7e4bc6f613b8a5d66208dcc0f28b435dceaf550edfb2fbda69136e29ced172e","contentType":"text/markdown; charset=utf-8"},{"id":"d4c902dc-4f80-5eef-98e8-e386a8275d6a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d4c902dc-4f80-5eef-98e8-e386a8275d6a/attachment.md","path":"references/mcp-version-matrix.md","size":6127,"sha256":"45496a49fdcaaa6d1722d7cb9c1aa7d4578a33ceac8c46fa63095b25da7b38b5","contentType":"text/markdown; charset=utf-8"},{"id":"96bc2936-0da6-536d-a6c7-a3f75b233d32","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/96bc2936-0da6-536d-a6c7-a3f75b233d32/attachment.md","path":"rules/_sections.md","size":2389,"sha256":"37e4dec4cdc2799d3813bd080e19adbfe0edf2ec70c5aeeeed8917b3508cfec0","contentType":"text/markdown; charset=utf-8"},{"id":"5cc576b9-aebc-5a7b-b04e-9cb624052b87","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5cc576b9-aebc-5a7b-b04e-9cb624052b87/attachment.md","path":"rules/_template.md","size":339,"sha256":"1f6b17cb8c02a543b46a648f062ff0a0c93635b93d51663bc299033be4dc3f83","contentType":"text/markdown; charset=utf-8"},{"id":"84de79c8-ba99-50bb-aaaf-e083d5f6b3f4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/84de79c8-ba99-50bb-aaaf-e083d5f6b3f4/attachment.md","path":"rules/advanced-composition.md","size":2874,"sha256":"28432c00ccc879d60c0f43097a3ecab58682b1ae0efd8ec4d9944cf9e4f66b39","contentType":"text/markdown; charset=utf-8"},{"id":"f9259ab9-e64f-56f6-b50a-8c1ff2af0b35","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f9259ab9-e64f-56f6-b50a-8c1ff2af0b35/attachment.md","path":"rules/advanced-resources.md","size":3783,"sha256":"c94e8f7d03de7024514282e27e087585d9ffd64ed6be1d7890910d4fdaef474b","contentType":"text/markdown; charset=utf-8"},{"id":"c3bdc438-7f08-57f9-ba16-18d990bf99f5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c3bdc438-7f08-57f9-ba16-18d990bf99f5/attachment.md","path":"rules/apps-ui.md","size":4541,"sha256":"94d94cc69af7a4bfdfa1c503480f3588c10defed263fb8e8fb296b476646df04","contentType":"text/markdown; charset=utf-8"},{"id":"51cdbec3-12e4-5fcc-bcca-9d88f1737d57","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/51cdbec3-12e4-5fcc-bcca-9d88f1737d57/attachment.md","path":"rules/auth-oauth21.md","size":7164,"sha256":"660749a370ff607f6ea89a335977c633f530a32fabce129628db297b12d69c3b","contentType":"text/markdown; charset=utf-8"},{"id":"3dfa1d0d-e800-5239-8451-98963404bb8f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3dfa1d0d-e800-5239-8451-98963404bb8f/attachment.md","path":"rules/client-patterns.md","size":5334,"sha256":"0e4f6c20b704637ebb50df61d45baf3c043338343890651399b5de118735cee8","contentType":"text/markdown; charset=utf-8"},{"id":"b8bab16e-a74b-5a97-a0f1-07234b203b16","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b8bab16e-a74b-5a97-a0f1-07234b203b16/attachment.md","path":"rules/elicitation.md","size":4619,"sha256":"4f7a3ae80c63bb67aaec8ce1a38ee72bdd87af39edd76a15d2a7c01f997eac4c","contentType":"text/markdown; charset=utf-8"},{"id":"fae8f3cd-2b50-5945-b869-2b07f3959816","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fae8f3cd-2b50-5945-b869-2b07f3959816/attachment.md","path":"rules/registry-discovery.md","size":3866,"sha256":"c75f55e382c2881ef6a73f7a13872d04903b3a9f14f08a72a6e43c88b983bb8c","contentType":"text/markdown; charset=utf-8"},{"id":"b718766a-925c-5bde-be95-67b64921b92e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b718766a-925c-5bde-be95-67b64921b92e/attachment.md","path":"rules/sampling-tools.md","size":4754,"sha256":"419763a35887a2eec8297595e7cf660349c8b45cc52ee4fa460c0144f6770097","contentType":"text/markdown; charset=utf-8"},{"id":"59ceeec1-c7ab-5d57-b1a3-33a8a649677d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/59ceeec1-c7ab-5d57-b1a3-33a8a649677d/attachment.md","path":"rules/security-hardening.md","size":4844,"sha256":"89f58fa1294d632b397fb68c85f9e095966b835c0bc8d903dcc1e62c2f3d0a8c","contentType":"text/markdown; charset=utf-8"},{"id":"6b093eb3-6d4f-51ea-b016-59dfc1aed612","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6b093eb3-6d4f-51ea-b016-59dfc1aed612/attachment.md","path":"rules/security-injection.md","size":3669,"sha256":"2e57e38fa810211717bdba4302c1cc5dc3d20167c3166b063d904899bbd5f652","contentType":"text/markdown; charset=utf-8"},{"id":"ecde63c0-50f6-549d-9a3c-439992e95839","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ecde63c0-50f6-549d-9a3c-439992e95839/attachment.md","path":"rules/server-setup.md","size":3177,"sha256":"c8a68ecc32ea14f7df99a4ce0fc64fb58b28c2afb68b1aa6e8a60c3354cf18ba","contentType":"text/markdown; charset=utf-8"},{"id":"dddf9e70-3048-5283-bec4-8bc294680c1a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dddf9e70-3048-5283-bec4-8bc294680c1a/attachment.md","path":"rules/server-transport.md","size":5031,"sha256":"8e54a225e085bd7843652c3db85ef353fb8c683a9fd7923e5f1909e9f8674ab5","contentType":"text/markdown; charset=utf-8"},{"id":"b4c02543-6070-5435-911b-df898583c103","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b4c02543-6070-5435-911b-df898583c103/attachment.md","path":"rules/testing-debugging.md","size":4082,"sha256":"7086a9d6427f113f071e61fb7425f9747b3bf105c1606eb064ac0ba93e3be39a","contentType":"text/markdown; charset=utf-8"},{"id":"99ba27f6-ba24-5dc3-bc0f-6be610be7a65","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/99ba27f6-ba24-5dc3-bc0f-6be610be7a65/attachment.md","path":"rules/webmcp-browser.md","size":3716,"sha256":"3c7bd4bb9d4bffa6584b7427f32756f1d4f9e60e3f99355e2d420a4c5e85401c","contentType":"text/markdown; charset=utf-8"},{"id":"a3fbfc9c-7ef5-5deb-925b-6ebd6d9905cb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a3fbfc9c-7ef5-5deb-925b-6ebd6d9905cb/attachment.json","path":"test-cases.json","size":11484,"sha256":"26bac401f21e9a3a4ae1b3a014190ce59e6a0f26213a24e442e7032fb97fa88e","contentType":"application/json; charset=utf-8"}],"bundle_sha256":"c3e6eb4427d8d47c458b78dff042387a60286f9dd1d39961b47281b9b1e8d0bf","attachment_count":20,"text_attachments":20,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":2,"skill_md_path":"src/skills/mcp-patterns/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":1},"context":"fork","license":"MIT","targets":[{"library":"@modelcontextprotocol/sdk","version":">=1.29.0"},{"library":"mcp","version":">=1.27.0"}],"version":"v1","category":"security","metadata":{"category":"mcp-enhancement","spec-version":"2025-11-25"},"complexity":"high","import_tag":"clean-skills-v1","description":"MCP server building, advanced patterns, and security hardening. Use when building MCP servers, implementing tool handlers, adding authentication, creating interactive UIs, hardening MCP security, or debugging MCP integrations.","allowed-tools":["Read","Glob","Grep","WebFetch","WebSearch"],"compatibility":"Claude Code 2.1.148+.","user-invocable":false,"persuasion-type":"reference","disable-model-invocation":true}},"renderedAt":1782981628547}

MCP Patterns Patterns for building, composing, and securing Model Context Protocol servers. Based on the 2025-11-25 specification — the latest stable release maintained by the Agentic AI Foundation (Linux Foundation), co-founded by Anthropic, Block, and OpenAI. Scaffolding a new server? Use Anthropic's skill ( ) for project setup and evaluation creation. This skill focuses on patterns, security, and advanced features after initial setup. Deploying to Cloudflare? See the skill for Workers-specific deployment patterns. Decision Tree — Which Rule to Read Quick Reference | Category | Rule | Impac…