An MCP server is not a REST API with a different logo. It is a tool surface that a model chooses from at runtime. The model reads your tool names, descriptions and schemas, not your source code.
1. Choose the transport deliberately
Use Streamable HTTP for a hosted server. stdio is for local, single-user tools. The old HTTP+SSE transport is deprecated.
The current MCP spec (2026-07-28) is stateless: every request carries its own protocol metadata, and there is no protocol-level session. Older clients still use the initialize handshake. A good server supports both eras.
For a hosted discovery server:
- expose one endpoint, e.g.
POST /mcp; - accept JSON-RPC 2.0;
- respond with JSON or an SSE stream;
- set
Cache-Control: no-store; - validate the
Originheader; - return
405for unsupported GET/DELETE session operations; - keep discovery free and read-only.
2. Name tools like a developer would
Use verb_noun:
search_apislist_apisget_apiget_routesget_receiptcheck_reliability
Avoid vague names like query, lookup or do_thing. The model has to choose between your tools and everything else in the host.
3. Write descriptions as instructions
A good description answers:
- What does this tool do?
- When should the model use it?
- When should it not use it?
- What does it return?
- What is an example input?
Keep it under ~1,000 characters. Put the detail in the schema, not in a wall of prose.
4. Design schemas for the model
- Use JSON Schema with types, enums, limits and
additionalProperties: false. - Mark required fields.
- Add descriptions to every property.
- Use realistic examples.
- Prefer flat objects over deeply nested ones.
- Return stable IDs alongside labels.
- Include a text fallback next to structured content.
5. Make errors recoverable
Return machine-readable errors:
{
"code": "PRICE_ABOVE_CAP",
"message": "The route costs 0.05 USDC; your cap is 0.01.",
"retryable": false,
"hint": "Raise the cap or choose a lower-cost route."
}
Do not return an HTML error page. Do not return 200 with an empty body. Do not make the model guess.
6. Do not put a 402 on discovery
Discovery is free. If search_apis or get_api returns a payment challenge, the agent cannot even decide whether to pay. Payment belongs on the provider's own route.
7. Security
MCP servers are a doorway. Treat them like one.
- Validate Origin on every HTTP request to prevent DNS rebinding.
- Authenticate anything that is not public, read-only discovery. OAuth 2.1 + PKCE is the production pattern for user-scoped tools. Static API keys are not enough for sensitive actions.
- Never pass through tokens issued for another resource.
- Validate every input on the server, the schema is a hint, not a security boundary.
- Guard SSRF: block private IPs, loopback, link-local and cloud metadata endpoints; require HTTPS; validate redirects.
- Screen metadata for prompt injection. A listing description that says “ignore previous instructions” is an attack.
- Rate limit per IP, per token and per tool.
- Log tool calls with correlation IDs, but never log secrets.
- Require confirmation for destructive or money-moving actions.
- Keep the tool surface small. Too many tools reduce model accuracy and increase blast radius.
8. State and idempotency
The protocol is stateless, but your product is not. Put durable state in a database and return an opaque handle:
- job ID
- draft ID
- cursor
- transaction hash
Bind the handle to the authenticated principal. Do not treat possession of a handle as authentication. Support Idempotency-Key for writes and paid calls.
9. Observability
Track:
tools/listcalls;tools/callby tool name;- latency p50/p95;
- error rate by code;
- rate-limit hits;
- client name/version from
initialize; - payment success rate for paid tools.
If you cannot see which tool agents choose and where they fail, you cannot improve the surface.
10. Test with real clients
Run the loop in:
- MCP Inspector
- Claude Desktop
- Cursor
- Continue
- a scripted LangChain or OpenAI Agents SDK client
For each: list tools, call one, handle an error, paginate, and (for paid tools) complete a real payment. If any step needs a human or a doc read, fix it.
The checklist
- Streamable HTTP, JSON-RPC 2.0
- Free, read-only discovery
-
verb_nountool names - Descriptions with when/when-not
- Complete JSON Schema + examples
- Structured output + text fallback
- Machine-readable errors
- Origin validation
- Auth for non-public tools
- SSRF guards
- Prompt-injection screening
- Rate limits
- Idempotency for writes
- Observability
- Tested in Claude, Cursor, Inspector