AI Skill Report Card

Building MCP Servers

A88·Nov 10, 2026·Source: Extension-page
YAML
--- name: building-mcp-servers description: Guides building spec-compliant remote Model Context Protocol (MCP) servers that connect LLM applications like Claude to external APIs via tools, including transport layers, OAuth 2.1 authentication, and session management. Use when implementing an MCP server, debugging MCP connection/transport issues, designing tool definitions for an API, or adding OAuth authentication to an MCP integration. ---
14 / 15

Define a tool that wraps an existing API endpoint using the McpServer pattern:

JavaScript
server.tool( "list_recipes", // name LLM uses to select the tool { host: z.string().optional().describe("Filter by website host") }, // params schema { description: "Returns a list of your scrape recipes with filters" }, // metadata async (params, { authInfo }) => { try { const apiKey = await getApiKeyFromToken(authInfo.token); const result = await fetchRecipes(apiKey, params); return { content: [{ type: "text", text: JSON.stringify(result) }] }; } catch (error) { return handleError(error); } } );

Mount this on a Streamable HTTP transport (POST /mcp) and protect it with OAuth 2.1 — that's a working remote MCP server.

Recommendation▾
Add an example showing SSE legacy transport implementation for completeness
14 / 15

Progress:

  • Step 1: Clarify local vs. remote vs. bridged deployment target
  • Step 2: Design tool definitions that map 1:1 to existing API endpoints
  • Step 3: Implement transport layer (Streamable HTTP primary, SSE fallback for legacy clients)
  • Step 4: Implement OAuth 2.1 + PKCE authentication and required discovery endpoints
  • Step 5: Wire authInfo through tool handlers for per-user authorization
  • Step 6: Handle session state across requests
  • Step 7: Deploy, add monitoring, and verify against real clients (Claude, Cursor)
  • Step 8: Troubleshoot using structured JSON-RPC errors and client logs

Step 1 — Choose deployment model

  • Local: server runs as subprocess via STDIO, configured in claude_desktop_config.json. Simplest, but no central updates, desktop-only, credentials live in local config/env vars.
  • Bridged remote: remote server + local bridge (mcp-remote) in config, forwards requests over HTTP. Interim pattern for clients lacking native remote/OAuth support.
  • Remote (recommended for production/SaaS): server is a standard HTTPS web service, no local config, auth via OAuth 2.1 web flows, centrally updatable. Default to this unless the use case needs direct local filesystem/resource access.

Step 2 — Design tools around existing API endpoints Each tool definition needs:

  • name: unique, descriptive identifier the LLM selects (e.g., list_recipes)
  • parameter schema (zod): mirrors the actual API's query/body params
  • description: tells the LLM when to use this tool — be specific about inputs/outputs, not just restating the name
  • handler function: calls the real API, has access to { authInfo } for auth context, returns { content: [{ type: "text", text: ... }] }

Map tools 1:1 to REST endpoints where possible — don't over-abstract. If an endpoint takes host and sort, the tool schema should expose exactly that.

Step 3 — Transport layer

  • Support Streamable HTTP (2025-03-26 spec): single POST /mcp endpoint, request/response correlation via JSON-RPC 2.0.
  • Only add HTTP+SSE (2024-11-05 legacy) if you must support older clients — requires separate GET /mcp (SSE stream) and POST /messages endpoints.
  • Use the per-session McpServer instantiation pattern rather than a single global server instance, so session state doesn't leak across users.

Step 4 — OAuth 2.1 implementation

  • Implement PKCE-based OAuth 2.1 flow from the start — do not bolt it on later.
  • Expose required discovery endpoints (e.g., /.well-known/oauth-authorization-server) so clients can auto-discover auth config.
  • Issue MCP-specific access tokens; map them internally to user API keys/credentials (e.g., via Firebase or your own user store).
  • Return proper WWW-Authenticate challenges on 401s so clients know to initiate auth.

Step 5 — Authorization inside tool handlers authInfo contains token, clientId, scopes, expiresAt. Use it to:

  • Look up per-user API keys/credentials from the token
  • Enforce scope-based access control before executing the underlying API call
  • Implement per-user rate limits/quotas
  • Never trust clientId alone for authorization — it identifies the client app, not the user

Step 6 — Session management Track session state (e.g., initialized capabilities, negotiated protocol version) per connection/session ID, not globally. Streamable HTTP sessions are correlated via session IDs issued at initialization.

Step 7 — Deployment & monitoring Deploy as a standard HTTPS web service (Cloud Run, Fly.io, etc.). Log JSON-RPC request/response pairs and auth failures distinctly — most "Claude was unable to connect" errors trace back to transport mismatches or auth misconfiguration, not tool logic.

Step 8 — Troubleshooting Reproduce against the actual target client (Claude.ai integrations UI, Cursor, etc.) rather than only testing with a raw JSON-RPC client — many failures are client-specific handshake quirks.

Recommendation▾
Include a bad-output example (e.g., vague tool description causing wrong tool selection) to contrast good/bad outcomes
17 / 20

Example 1: Input: "I have a REST endpoint GET /recipes?host=&sort= and want Claude to answer 'show me my Twitter recipes'." Output: Define tool list_recipes with schema { host: z.string().optional(), sort: z.string().optional() }, description "Returns a list of your scrape recipes with optional filters and sorting", handler fetches /recipes using the user's API key resolved from authInfo.token, returns JSON as text content.

Example 2: Input: "Claude says 'unable to connect' to my remote MCP server." Output: Check (in order): is /mcp responding to POST with correct JSON-RPC envelope; is the OAuth discovery endpoint reachable and returning valid metadata; is the session ID being issued/honored across requests; is the transport the client expects (Streamable HTTP vs SSE) actually implemented.

Example 3: Input: "Need to restrict a tool to paid users only." Output: In the tool handler, check authInfo.scopes.includes("paid_tier") (or look up the user's plan via authInfo.token → user record) before calling the underlying API; return a structured JSON-RPC error/content message like "Unauthorized: requires paid plan" if the check fails, rather than silently returning empty data.

Recommendation▾
Trim the extensive frontmatter description slightly or split triggers into a bulleted list for scannability
  • Default to building a remote server with OAuth 2.1 unless there's a hard requirement for local resource access.
  • Write rich, specific tool descriptions — this is the main signal the LLM uses to pick the right tool; vague descriptions cause wrong tool selection.
  • Keep tool parameter schemas thin wrappers around real API parameters — don't invent abstractions the underlying API doesn't support.
  • Always thread authInfo through to get per-user credentials; never use a single shared API key for all MCP users.
  • Prefer the simpler per-session McpServer pattern with StreamableHTTPServerTransport over maintaining the legacy SSE dual-endpoint pattern unless legacy client support is required.
  • Return errors as structured JSON-RPC errors, not generic exceptions — clients surface these directly to users.
  • Treating "server" ambiguously — always clarify whether you mean the local process or the remote web service; this is a frequent source of confused debugging.
  • Skipping OAuth/PKCE initially and trying to retrofit auth later — build it in from the start since MCP requires OAuth 2.1 for production remote servers.
  • Using a single global McpServer instance shared across all client sessions, causing state leakage between users.
  • Assuming one transport standard — implementations that only handle Streamable HTTP will silently fail against clients still using legacy HTTP+SSE, and vice versa.
  • Forgetting to expose OAuth discovery endpoints, which breaks client auto-configuration even if the core OAuth flow is correct.
  • Storing credentials in local config files for what's meant to be a remote, multi-user service — defeats the purpose of centralized auth/updates.
0
Grade AAI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
14/15
Workflow
14/15
Examples
17/20
Completeness
19/20
Format
14/15
Conciseness
14/15