AI Cert Prep
Type to search documentation.

Appendix · Claude

MCP Cheat Sheet

Model Context Protocol architecture, primitives, transports, lifecycle, security, server authoring in Python and TypeScript, and when to use MCP versus alternatives.

Architecture

text
┌──────────────── Host (Claude Desktop / Claude Code / your app) ────────────────┐
│ │
│ ┌─────────────┐ 1:1 ┌─────────────┐ ┌─────────────┐ │
│ │ MCP Client │◄──────────►│ MCP Server │ │ MCP Server │ │
│ │ (per server)│ JSON-RPC │ github │ │ postgres │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ▲ transport: stdio transport: Streamable HTTP │
│ │ (local subprocess) (remote, OAuth 2.1) │
│ Claude model ── sees tools/resources/prompts exposed by all servers │
└────────────────────────────────────────────────────────────────────────────────┘
  • Host embeds one client per server; clients speak JSON-RPC 2.0 to servers.
  • Servers expose capabilities; the host decides what the model sees and enforces permissions.

Primitives

PrimitiveControlled byWhat it isExample
ToolsModelCallable functions with JSON Schema inputscreate_issue, query_db
ResourcesApplicationRead-only data addressed by URIfile:///docs/spec.md, db://schema
PromptsUserReusable prompt templates with arguments/summarise-pr
SamplingServer → clientServer asks the host’s model to complete somethingServer-side summarisation
RootsClient → serverFilesystem/URI boundaries the server may operate inProject directory
Logging / progressServer → clientDiagnostics and long-task progressIndexing progress

Lifecycle

text
client ──initialize (protocolVersion, capabilities, clientInfo)──► server
client ◄──result (capabilities, serverInfo)────────────────────── server
client ──notifications/initialized──────────────────────────────► server
client ──tools/list ──► server client ──resources/list ──► server
client ──tools/call {name, arguments} ──► server ──► result {content[], isError}

Capability negotiation at initialize tells each side which primitives (tools, resources, prompts, sampling, logging) are supported.

Transports

TransportWhereAuthNotes
stdioLocal subprocessProcess/user permissionsSimplest; Claude Desktop/Code default for local servers
Streamable HTTPRemote serverOAuth 2.1 (PKCE), bearer tokensCurrent remote standard; supports streaming responses
HTTP + SSERemote (legacy)OAuth / tokensSuperseded by Streamable HTTP; still seen

Authoring a server

python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("orders")
@mcp.tool()
def get_order(order_id: str) -> dict:
"""Look up one order by ID. Use when the user references an order number.
Returns status, eta and items. Read-only."""
order = db.fetch_order(order_id)
if order is None:
raise ValueError(f"not_found: no order {order_id}") # surfaces as isError with message
return order
@mcp.resource("orders://schema")
def schema() -> str:
"""Orders table schema (read-only reference)."""
return open("schema.sql").read()
@mcp.prompt()
def triage(order_id: str) -> str:
return f"Triage order {order_id}: check status, delays and next action."
if __name__ == "__main__":
mcp.run(transport="stdio") # or transport="streamable-http"

Server design checklist

  • One narrow purpose per tool; 4–8 tools per server is typical. Beyond ~10 exposed to one agent, rely on tool search / defer_loading.
  • Descriptions state what, when, when-not, and return shape.
  • Structured errors (isError: true with category, retryable, message) – never empty success.
  • Idempotency for anything that writes; accept an idempotency key.
  • Pagination for list operations; cap page sizes.
  • Least privilege: expose read tools by default; put destructive tools behind separate servers or confirmation.
  • Identity propagation: for multi-user apps, tools must act as the end user (OAuth on behalf of), not a shared super-user.
  • Treat tool output as untrusted downstream (indirect injection).
  • Version the server; add tools rather than changing semantics.

Connecting from Claude

Terminal window
claude mcp add orders -- python orders_server.py # stdio, local scope
claude mcp add --scope project orders -- python orders_server.py # .mcp.json, shared
claude mcp add --transport http linear https://mcp.linear.app/mcp # remote
/mcp # inspect, authenticate

MCP vs alternatives

NeedPreferReason
Reusable connector to an external system used by several agents/hostsMCP serverStandard interface, discoverable, host-managed permissions
One-off function inside a single appCustom tool in the API requestLess infrastructure
A procedure/knowledge Claude should followSkillProgressive disclosure, no runtime
Deterministic scripted step with no model judgmentPlain API/CLI call in codeCheaper, testable
Two autonomous systems negotiatingAgent-to-agent protocol / orchestration layerMCP is model↔tool, not agent↔agent

Security quick list

RiskControl
Over-broad tools (delete/refund exposed)Remove them; separate servers; hooks
Indirect injection via tool resultsBoundaries (XML), treat as data, output validation
Shared credentialsOAuth per user; short-lived tokens; scopes
Secret leakageEnv/secret manager; never in prompts, CLAUDE.md or logs
Untrusted serversAllowlist servers; pin versions; review source
Excessive agencyHuman approval for irreversible actions; permissions.ask

Capability negotiation (the JSON)

At initialize each side advertises what it supports. The host only exposes primitives both sides negotiated.

json
// client → server
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
"protocolVersion": "2025-06-18",
"capabilities": { "roots": { "listChanged": true }, "sampling": {} },
"clientInfo": { "name": "claude-code", "version": "2.1.0" } } }
// server → client
{ "jsonrpc": "2.0", "id": 1, "result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true, "listChanged": true },
"prompts": { "listChanged": true },
"logging": {} },
"serverInfo": { "name": "orders", "version": "1.4.0" } } }
// client → server (handshake complete)
{ "jsonrpc": "2.0", "method": "notifications/initialized" }

If the server does not advertise sampling, the host will not route sampling requests to it — and vice-versa for client roots.

Resources, prompts and sampling – worked examples

Resources (application-controlled, read-only)

json
// list
{ "jsonrpc": "2.0", "id": 2, "method": "resources/list" }
{ "jsonrpc": "2.0", "id": 2, "result": { "resources": [
{ "uri": "orders://schema", "name": "Orders schema", "mimeType": "text/sql" },
{ "uri": "orders://policy/refunds", "name": "Refund policy", "mimeType": "text/markdown" } ] } }
// read
{ "jsonrpc": "2.0", "id": 3, "method": "resources/read", "params": { "uri": "orders://schema" } }
{ "jsonrpc": "2.0", "id": 3, "result": { "contents": [
{ "uri": "orders://schema", "mimeType": "text/sql", "text": "CREATE TABLE orders (…)" } ] } }

Resources are data, not actions: no side effects, addressed by URI, chosen by the application (not the model). Use them for schemas, policies and reference docs.

Prompts (user-controlled templates)

json
{ "jsonrpc": "2.0", "id": 4, "method": "prompts/get",
"params": { "name": "triage", "arguments": { "order_id": "ORD-12345" } } }
{ "jsonrpc": "2.0", "id": 4, "result": { "messages": [
{ "role": "user", "content": { "type": "text",
"text": "Triage order ORD-12345: check status, delays and next action." } } ] } }

Prompts surface as slash commands (/triage) — the user invokes them, unlike tools (model-invoked) or resources (app-selected).

Sampling (server asks the host’s model)

The server can request a completion from the host’s model — e.g. to summarise before returning. The host stays in control and can deny, redact or rate-limit.

json
// server → client
{ "jsonrpc": "2.0", "id": 5, "method": "sampling/createMessage", "params": {
"messages": [{ "role": "user", "content": { "type": "text", "text": "Summarise: <500 rows>" } }],
"maxTokens": 300, "modelPreferences": { "intelligencePriority": 0.3, "speedPriority": 0.8 } } }
// client → server (after the host runs its model, with user approval)
{ "jsonrpc": "2.0", "id": 5, "result": {
"role": "assistant", "content": { "type": "text", "text": "12 orders delayed, avg 3 days…" },
"model": "claude-haiku-4-5", "stopReason": "endTurn" } }

Sampling is a trust boundary

Sampling lets a server spend the host’s tokens and see model output. The host must gate it (approval, rate limits) and never auto-approve for untrusted servers.

Streamable HTTP server example

python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("orders")
@mcp.tool()
def get_order(order_id: str) -> dict:
"""Look up one order by ID. Read-only."""
return db.fetch_order(order_id)
if __name__ == "__main__":
# Serves POST /mcp with streaming responses; put OAuth 2.1 in front (reverse proxy / gateway)
mcp.run(transport="streamable-http", host="0.0.0.0", port=8080, path="/mcp")
text
Client Reverse proxy / gateway MCP server
│ POST /mcp (Bearer token) │ │
├────────────────────────────────► validate token, scopes │
│ ├────────────────────────────────► initialize / tools/call
│ │ │
│ ◄─────── streamed JSON-RPC responses (chunked) ─────────────────┤

Streamable HTTP is a single endpoint that supports request/response and server-streamed messages. It supersedes the older HTTP+SSE two-endpoint transport.

OAuth 2.1 flow (remote servers)

text
┌── Host (MCP client) ──┐ ┌── Authorization server ──┐ ┌── MCP server ──┐
│ 1. discover metadata │────────►│ /.well-known/oauth-* │ │ (resource) │
│ 2. PKCE: code_verifier│ │ │ │ │
│ + code_challenge │ │ │ │ │
│ 3. authorize (browser)│────────►│ user logs in, consents │ │ │
│ 4. redirect w/ code │◄────────│ auth code │ │ │
│ 5. token (code + │────────►│ /token │ │ │
│ code_verifier) │◄────────│ access + refresh token │ │ │
│ 6. call with Bearer │───────────────────────────────────┼─────►│ validate scope │
│ 7. refresh on expiry │────────►│ /token (refresh_token) │ │ │
└───────────────────────┘ └──────────────────────────┘ └────────────────┘
  • PKCE is mandatory in OAuth 2.1 (no implicit flow).
  • Tokens are short-lived; refresh silently.
  • Scopes map to tool permissions; the token carries the end user’s identity so tools act as that user.
  • The MCP server is a resource server; a separate authorization server issues tokens.

Error shapes

MCP distinguishes protocol errors (JSON-RPC level) from tool execution errors (a successful call whose result says it failed).

json
// Protocol / JSON-RPC error (method missing, bad params)
{ "jsonrpc": "2.0", "id": 7, "error": { "code": -32602, "message": "Invalid params: order_id required" } }
// Tool execution error (call succeeded, tool failed) — isError on the result
{ "jsonrpc": "2.0", "id": 8, "result": {
"isError": true,
"content": [{ "type": "text", "text": "{\"category\":\"not_found\",\"retryable\":false,\"message\":\"No order ORD-999\"}" }] } }
JSON-RPC codeMeaning
-32700Parse error
-32600Invalid request
-32601Method not found
-32602Invalid params
-32603Internal error

Rule: business failures use isError: true on the result (the model can see and react), not a JSON-RPC error. Reserve JSON-RPC errors for genuinely malformed calls.

Testing with the MCP Inspector

Terminal window
# Launch the inspector against a local stdio server
npx @modelcontextprotocol/inspector python orders_server.py
# Against a remote Streamable HTTP server (walks the OAuth flow)
npx @modelcontextprotocol/inspector --transport http https://mcp.example.com/mcp

Checklist in the inspector: initialize returns the expected capabilities; tools/list shows correct names, descriptions and schemas; each tool call returns structured content; error paths set isError; resources read cleanly; prompts render with arguments. Test before wiring the server into Claude — most “the model won’t call my tool” bugs are description/schema bugs the inspector surfaces immediately.

Versioning

ChangeCompatibilityDo
Add a new toolBackward-compatibleShip freely; bump minor version
Add an optional fieldBackward-compatibleShip; document
Rename/remove a tool or required fieldBreakingNew tool name; keep old one deprecated for a window
Change a tool’s semantics silentlyDangerousNever — agents encoded the old behaviour; version and communicate
Protocol versionNegotiated at initializeSupport a range; advertise the highest you speak

Prefer additive evolution. Because agents and prompts encode tool names and behaviours, a silent semantic change breaks callers with no error — versioning and deprecation windows are the exam-correct approach.

Common misconceptions

MisconceptionRealityWhy it matters on the exam
“Tools, resources and prompts are interchangeable”Tools = model-invoked actions; resources = app-selected data; prompts = user-invoked templatesPrimitive-confusion distractor
“MCP is agent-to-agent”MCP is model↔tool; use an orchestration/A2A layer for agent↔agentWrong-protocol distractor
“A tool that finds nothing should return {}”Return isError with a category; never empty successSilent-failure anti-pattern
“Expose everything the server can do”Least privilege; separate destructive tools; remove unneeded onesOver-broad-tools distractor
“OAuth is optional for remote servers”Streamable HTTP remote servers need OAuth 2.1 + PKCE; propagate user identityShared-super-user authz gap
“20 tools on one server is fine”4–8 typical; beyond ~10 use tool search + defer_loadingToo-many-tools anti-pattern
“Business errors should be JSON-RPC errors”Use isError on the result; JSON-RPC errors are for malformed callsError-shape distractor

Scenario walkthrough

A SaaS company wants Claude, embedded in several internal apps, to read and act on customers’ CRM records. Multi-tenant: each end user may only see their own accounts. The CRM already has an OAuth provider. How should the MCP integration be designed?

  1. MCP server, not per-app custom tools — the connector is reused across several hosts/apps; a standard server is discoverable and host-permission-managed.
  2. Streamable HTTP transport — remote, multi-user; stdio is for local subprocesses only.
  3. OAuth 2.1 + PKCE, per-user tokens — the token carries the end user’s identity so tools enforce that user’s CRM permissions. A shared super-user credential is the authz-gap distractor.
  4. Narrow tools — search_accounts, get_account, create_note (4–8). Destructive operations (delete_account) live behind a separate server or a confirmation, or are omitted (least privilege).
  5. Structured errors — isError with category/retryable; never empty success.
  6. Treat tool output as untrusted — CRM notes could carry indirect injection; keep boundaries and validate downstream.
  7. Version additively — add tools over time; never silently change semantics.

Rejected alternatives: stdio (not remote/multi-user), a shared API key (breaks per-user authz), exposing every CRM verb (over-broad), and returning {} on “no accounts found” (silent failure).

Last updated Sep 18, 2026