

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
1# MCP Server Development Rules (Python + FastMCP)23You are helping build a production Model Context Protocol (MCP) server in Python using the official `mcp` SDK with FastMCP.45## What MCP Is67MCP (Model Context Protocol) lets an LLM client (Claude Desktop, Claude Code, IDEs) call your tools and read your resources via a standardized JSON-RPC protocol. Three primitives:89- **Tools** — functions the LLM can invoke (`@mcp.tool()`).10- **Resources** — read-only data the client can fetch (`@mcp.resource("uri://template/{id}")`).11- **Prompts** — reusable prompt templates the user can pick (`@mcp.prompt()`).1213Default to tools. Use resources only for genuinely read-only blobs (file contents, doc pages). Use prompts only when the user benefits from a one-click template.1415## Project Layout1617```18services/my-mcp/19├── server.py # FastMCP entrypoint20├── README.md # Tool descriptions, install, env vars21├── requirements.txt # mcp[cli] + your deps22└── .env.example # Document required env, never commit real .env23```2425Keep one server per logical capability. Don't conflate "memory" + "calendar" + "shell" into one server — each gets its own port and its own auth scope. Composition happens client-side.2627## Transport: Pick the Right One2829- **stdio** — local-only, single client (Claude Desktop). Default for personal use. Zero auth needed (process boundary is the trust boundary).30- **streamable HTTP** — multi-client, network-accessible, requires bearer token auth. Use for shared/team servers.31- **SSE (legacy)** — deprecated. Don't use for new servers.3233For HTTP servers:3435```python36from mcp.server.fastmcp import FastMCP3738mcp = FastMCP("my-server", host="0.0.0.0", port=8401)3940if __name__ == "__main__":41 mcp.run(transport="streamable-http")42```4344## Tool Definition: The Docstring IS the Prompt4546The LLM client sees your tool's docstring as the description it uses to decide whether to call your tool. Write docstrings for the LLM, not for human developers.4748```python49@mcp.tool()50async def search_memory(query: str, limit: int = 10) -> str:51 """Search the user's persistent memory graph for entries matching the query.5253 Use this when the user asks about prior decisions, past conversations,54 or facts they've stored. Returns up to `limit` snippets ranked by relevance.5556 Do NOT use for: real-time data (use get_current_state), or to write new57 memories (use add_fact).58 """59 ...60```6162Rules for tool docstrings:63- **First line**: imperative summary of what the tool does.64- **When to use**: 1-2 sentences telling the LLM the right use case.65- **When NOT to use**: 1 sentence steering it away from misuse — this prevents tool confusion when you have many tools.66- **Return shape**: describe what the LLM gets back so it can use the response.6768## Type Hints Become JSON Schema6970Parameter types map directly to the schema the client sees. Be precise:7172```python73from typing import Optional, Literal7475@mcp.tool()76async def list_contacts(77 active_only: bool = True,78 role: Optional[Literal["client", "lead", "mentor"]] = None,79 limit: int = 25,80) -> str:81 ...82```8384`Literal` types become enum constraints — the LLM can only pass valid values. Use them for any parameter with a fixed set of choices. `Optional[T]` makes a parameter nullable; default values make it optional. Don't use `Any` — the LLM gets no guidance.8586## Auth at the HTTP Layer (When Using HTTP Transport)8788FastMCP doesn't provide auth out of the box for streamable HTTP. Add a middleware:8990```python91from starlette.middleware.base import BaseHTTPMiddleware92from starlette.responses import JSONResponse9394class BearerAuthMiddleware(BaseHTTPMiddleware):95 async def dispatch(self, request, call_next):96 if request.url.path.startswith("/health"):97 return await call_next(request)98 token = request.headers.get("authorization", "").removeprefix("Bearer ").strip()99 if not token or token != EXPECTED_TOKEN:100 return JSONResponse({"error": "unauthorized"}, status_code=401)101 return await call_next(request)102103mcp.app.add_middleware(BearerAuthMiddleware)104```105106Issue tokens per-client, not a shared secret. Store hashed tokens, support rotation, and log auth failures.107108## Env Var Loading: The Multi-Candidate Pattern109110MCP servers run under different parents (Claude Desktop, systemd, dev shell). Don't trust any single .env location. Try multiple, in order:111112```python113import os114115_env_candidates = [116 os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", ".env"),117 os.path.expanduser("~/.env"),118 "/etc/myapp/env",119]120for _env_path in _env_candidates:121 if os.path.isfile(_env_path):122 with open(_env_path) as f:123 for line in f:124 line = line.strip()125 if not line or line.startswith("#"):126 continue127 if "=" in line and not os.environ.get(line.split("=", 1)[0]):128 k, v = line.split("=", 1)129 os.environ[k] = v.strip().strip("\"'")130 break131```132133Don't overwrite already-set env vars — let the parent process win. Required vars should fail loudly at startup, not silently degrade.134135## Sanitize Between Data Store and Tool Output136137The data layer often holds fields the LLM should never see (PII, internal IDs, raw passwords, foreign-key clutter). Add an explicit sanitize step:138139```python140def _sanitize_contact(contact: dict) -> dict:141 return {142 "name": contact.get("display_name"),143 "role": contact.get("role"),144 "last_contact": contact.get("last_interaction_at"),145 }146```147148Never `return contact` directly from a query. The sanitize function is the privacy contract — review it like you'd review an external API response.149150## Errors: Return Strings, Not Exceptions151152If a tool raises, the client sees a generic protocol error and the LLM has no context for retry. Catch at the tool boundary and return an actionable string:153154```python155@mcp.tool()156async def get_facts(subject: str) -> str:157 try:158 result = await _api_get(f"/facts/{subject}")159 except httpx.HTTPStatusError as e:160 if e.response.status_code == 404:161 return f"No facts found for subject '{subject}'. Try `list_subjects()` to see available subjects."162 return f"API error ({e.response.status_code}): {e.response.text[:200]}"163 except httpx.RequestError as e:164 return f"Could not reach memory API: {e}. Check MEMORY_API_URL and that the service is running."165 return _format_facts(result)166```167168The LLM uses your error message to decide whether to retry, ask the user, or move on. Vague errors waste turns.169170## Tool Output: Format for an LLM, Not a Terminal171172The LLM consumes your output as text in its context window. Optimize for that:173174- **Markdown is fine** — headings and bullets help the LLM section-scan.175- **JSON is fine** — predictable structure helps the LLM extract fields.176- **ANSI color codes are NOT fine** — they're tokens with no meaning.177- **Truncate aggressively** — if you have 1000 results, return top 20 + a count. The LLM has finite context.178- **Include counts and timestamps** — "Found 3 of 47 matches (showing top 3 by relevance)" lets the LLM tell the user there's more.179180## Async Everywhere181182FastMCP tools should be `async def` even if the body is sync. Mixing blocking calls with async causes the server to stall under concurrent client requests:183184```python185import httpx186187_client = httpx.AsyncClient(timeout=10.0)188189@mcp.tool()190async def fetch_doc(url: str) -> str:191 r = await _client.get(url)192 r.raise_for_status()193 return r.text[:5000]194```195196Reuse a module-level `httpx.AsyncClient` — don't create one per call. Set explicit timeouts; MCP clients give up after ~30s.197198## Don't Over-Tool199200Resist the urge to expose every internal function as a tool. Each tool you add:201- Eats client context (the descriptions are sent on every request).202- Increases the chance of tool confusion (LLM picks the wrong one).203- Adds to your audit/security surface.204205Aim for 5-10 tools per server. If you have more, you probably need to split the server or consolidate tools (one tool with a `mode` param often beats three separate tools).206207## Testing Tools Directly208209Before shipping, exercise each tool from a real MCP client (Claude Desktop, the `mcp` CLI, or `mcp inspector`):210211```bash212mcp dev services/my-mcp/server.py213```214215Manual checklist:216- Tool appears with correct name + description217- Parameters validate (try invalid types, missing required, out-of-enum values)218- Auth rejects bad tokens with 401219- Error paths return useful strings220- Output renders cleanly in the client221222Don't ship tools you haven't called from an actual LLM session.223224## Logging225226Log to stderr (stdout belongs to the protocol on stdio transport):227228```python229import sys, logging230logging.basicConfig(stream=sys.stderr, level=logging.INFO,231 format="%(asctime)s %(levelname)s %(message)s")232log = logging.getLogger("my-mcp")233```234235Log: tool invocations (name + sanitized args), auth failures, downstream API errors. Never log: bearer tokens, full request bodies, user PII.236237## Deploy Pattern (HTTP transport)238239systemd unit for production:240241```ini242[Unit]243Description=My MCP Server244After=network.target245246[Service]247Type=simple248User=mcpuser249WorkingDirectory=/opt/my-mcp250Environment=PYTHONUNBUFFERED=1251EnvironmentFile=/etc/my-mcp/env252ExecStart=/opt/my-mcp/.venv/bin/python server.py253Restart=on-failure254RestartSec=5255256[Install]257WantedBy=multi-user.target258```259260For client config (Claude Desktop), point at the HTTP endpoint with bearer token in the `headers` field.261262## Common Mistakes to Avoid2632641. **Returning Python objects instead of strings/dicts** — MCP serializes via JSON. Return primitives, dicts, or strings.2652. **Loading .env after `MEMORY_API_KEY = os.environ.get(...)`** — read env after the loader runs, not at module-import time before.2663. **Case-mismatched env var names** between .env and code — be exact, env vars are case-sensitive.2674. **Sharing one bearer token across all clients** — you can't revoke selectively. Issue per-client tokens.2685. **Putting secrets in tool descriptions** — descriptions go to every client; treat them as public.2696. **Long-running tools without progress** — the LLM waits the full duration. Either return fast with a "job queued" message + a status tool, or stream progress.2707. **Conflating tools and resources** — if it has side effects or takes args that change behavior, it's a tool. If it's a stable URL returning data, it's a resource.271272## When to Use This vs. a REST API273274MCP is the right choice when:275- The consumer is an LLM agent (not a frontend or a script).276- You want client-side discovery (the LLM picks tools based on descriptions).277- You're integrating with Claude Desktop, Claude Code, or another MCP client.278279Use a plain REST/GraphQL API when:280- Multiple non-LLM consumers (frontends, mobile, scripts) also need the data.281- You need fine-grained access control beyond bearer tokens.282- You need request/response patterns MCP doesn't model well (file uploads, websockets for non-LLM streams).283284It's fine to expose the same backend through both: a REST API for apps and a thin MCP server that proxies to it for LLM clients. That's the architecture this rules file is grounded in.285
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| survivorforge/cursor-rulesrules/ai-ml-python/.cursorrules · 17 | .cursorrules | teststylearchdeployment+2 | 81/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/api-microservices/.cursorrules · 17 | .cursorrules | buildteststylearch+5 | 92/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/database-sql/.cursorrules · 17 | .cursorrules | styletypessecuritydatabase+3 | 65/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/devops-docker/.cursorrules · 17 | .cursorrules | setupbuildteststyle+4 | 93/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/devops-infrastructure/.cursorrules · 17 | .cursorrules | buildteststylesecurity+3 | 93/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/django-rest/.cursorrules · 17 | .cursorrules | buildteststylearch+5 | 84/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/docker-devops/.cursorrules · 17 | .cursorrules | setupteststylearch+6 | 85/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/flutter-dart/.cursorrules · 17 | .cursorrules | teststylearchtypes+5 | 89/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/go-gin/.cursorrules · 17 | .cursorrules | testlint-formatstylearch+5 | 84/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/langchain-ai/.cursorrules · 17 | .cursorrules | testlint-formatstylearch+4 | 84/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/mobile-react-native/.cursorrules · 17 | .cursorrules | teststylearchtypes+7 | 89/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/nextjs-14-app-router/.cursorrules · 17 | .cursorrules | teststyletypestesting-strategy+3 | 71/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/nextjs-app-router/.cursorrules · 17 | .cursorrules | teststylearchtypes+5 | 81/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/nextjs-typescript/.cursorrules · 17 | .cursorrules | teststylearchtypes+5 | 81/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/nextjs/.cursorrules · 17 | .cursorrules | teststylearchtypes+5 | 81/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/nodejs-express-typescript/.cursorrules · 17 | .cursorrules | setupteststylearch+7 | 81/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/nodejs-express/.cursorrules · 17 | .cursorrules | testlint-formatstylearch+7 | 92/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/performance-optimization/.cursorrules · 17 | .cursorrules | styledatabaseapiperformance+2 | 65/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/python-django/.cursorrules · 17 | .cursorrules | testlint-formatstylearch+6 | 99/100 | 13 days ago | |
| survivorforge/cursor-rulesrules/python-fastapi/.cursorrules · 17 | .cursorrules | testlint-formatstylearch+6 | 99/100 | 13 days ago |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/survivorforge-cursor-rules-rules-mcp-server-cursorrules)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.