{"slug": "mcp-server-design-patterns-and-anti-patterns", "title": "MCP server design patterns and anti-patterns", "summary": "A practitioner guide published in October 2026 identifies four design patterns that hold up in production Model Context Protocol (MCP) servers built to the 2026-07-28 specification, led by exposing retrieval as a cheap `search` tool plus a `fetch`-by-ID tool and leaving reasoning to the calling model. The guide, part 32 of the author's MCP series, also recommends explicit handles for cross-call state instead of per-connection state, Multi Round-Trip Requests that settle inputs before returning a task handle, and shipping one capability across layered surfaces from universal tools to MCP Apps. It attributes most production problems to a short list of anti-patterns including one tool per endpoint, per-connection state, unprotected `requestState` and token passthrough, and notes OpenAI's MCP guide asks servers to implement read-only `search` and `fetch` tools to work with ChatGPT's deep research and company knowledge features.", "body_md": "# MCP server design patterns and anti-patterns\n\nThe specification tells you what a Model Context Protocol (MCP) server must do. It says much less about what holds up in practice, so here I’ve collected four recurring patterns that work and the anti-patterns behind most production problems, each with a pointer to the part of the series that explains the fix.\n\nThis is part 32 of my series on what an MCP server does under the 2026-07-28 specification, after [part 31 on which AI hosts support which features](https://pournasserian.com/writing/mcp-2026-host-support). One frequently asked architecture question, whether to put an AI agent behind a tool, gets the next part to itself. The facts are as I read them in October 2026.\n\n## In brief\n\n1. **Expose retrieval as two tools, a cheap `search` and a `fetch` by ID,** and leave the reasoning to the calling model.\n2. **Keep state across calls in explicit handles, not in the connection:** per-connection state breaks behind load balancers, and a handle is checked against the caller on every call.\n3. **When long work needs a confirmation, settle the inputs first and then return a task,** with no side effects before the last input round.\n4. **Ship one capability on several surfaces,** from tools that work everywhere to an MCP App where a host can render one, each degrading to the next.\n5. **Most production problems come from a short list of anti-patterns,** among them one tool per endpoint, per-connection state, unprotected`requestState` and token passthrough.\n\n## Four patterns that work\n\nThree of these are my advice rather than rules from the specification; the handle pattern follows the specification’s own guidance, as [part 5](https://pournasserian.com/writing/mcp-2026-stateless-requests) shows.\n\n### Search plus fetch\n\nWhen your server fronts a body of content, expose retrieval as two tools: a cheap `search` that returns ranked, citable hits, and a `fetch` that returns a full item by its ID. The calling model does the reasoning, and your server does what it’s uniquely good at: knowing the index, the schema and the ranking.\n\n- `search` returns`structuredContent` with IDs, titles, scores, short snippets and source Uniform Resource Identifiers (URIs), plus a compact text rendering in`content` .\n- `fetch` returns the full document, or a`resource_link` the host can attach.\n\nThe pattern also matches what two ChatGPT features, deep research and company knowledge, expect: OpenAI’s [MCP guide](https://developers.openai.com/api/docs/mcp) asks servers that want to work with them to implement two read-only tools, `search` and `fetch`. The example instructions in [part 4](https://pournasserian.com/writing/mcp-2026-server-instructions) tell the model to call `search_docs` first and then `fetch_doc`, which is this pattern in a sentence.\n\n### Explicit handles for state\n\nThere are no sessions under the 2026-07-28 specification, so state that spans calls needs a home of its own. Return an opaque handle from a creation tool, accept it on later calls, authorize the caller against it every time, state its lifetime in the description, and return a recoverable tool execution error when it expires. Part 5 explains why and builds one in Python, with an owner and an expiry.\n\n### Multi round-trip requests, then a task\n\nFor long-running work that needs a confirmation, resolve the inputs synchronously with Multi Round-Trip Requests (MRTR, [part 13](https://pournasserian.com/writing/mcp-2026-multi-round-trip-requests)), then return a task handle and run the work asynchronously. Cause no side effects before the final input round. [Part 26](https://pournasserian.com/writing/mcp-2026-tasks) shows how the two phases fit together.\n\n### Layered surfaces\n\nHost support is uneven, as part 31 showed, so ship one capability on several surfaces, each degrading to the next. Here is one workflow, an incident postmortem, delivered five ways: tools that work everywhere, server instructions that point to the prompt and the skill, a prompt for integrated development environment (IDE) and chat hosts, a skill for hosts with Skills, and an MCP App for hosts with Apps.\n\nA host that supports only tools still gets the incident data through `get_incident` and `list_timeline_events`. Each layer it supports adds to that. [Part 12](https://pournasserian.com/writing/mcp-2026-choosing-a-primitive) gives the reasoning behind each choice of primitive.\n\n## Anti-patterns that cause most production problems\n\nI’ve grouped the twelve by where they live. Where the specification itself sets the fix, the group says so; the rest is my advice.\n\n### In tools and their results\n\nFive concern the tools themselves and what they return. Two of the fixes are the specification’s: servers SHOULD return tools in a deterministic order, and under the MCP Apps extension, tools MUST return meaningful `content` even when a user interface (UI) is available.\n\n| Anti-pattern | Why it hurts | Instead | \n|---|---|---|\n| Throwing exceptions on bad input | Becomes a protocol error, and the model can’t self-correct | `isError: true` with an actionable message ([part 9](https://pournasserian.com/writing/mcp-2026-tool-errors-and-design) ) | \n| One tool per Representational State Transfer (REST) endpoint, 50 or more tools | Context bloat and poor tool selection | Fewer, task-shaped tools with sharp descriptions ( [part 9](https://pournasserian.com/writing/mcp-2026-tool-errors-and-design) ) | \n| Dumping whole documents into results | Hits host output limits and costs tokens | Summaries plus `resource_link` s ([part 9](https://pournasserian.com/writing/mcp-2026-tool-errors-and-design) ) | \n| UI-only answers | Useless in text-only hosts | Meaningful `content` always ([part 25](https://pournasserian.com/writing/mcp-2026-apps-building) ) | \n| Unstable list ordering | Prompt-cache misses, flaky pagination | Deterministic sort ( [part 6](https://pournasserian.com/writing/mcp-2026-caching-and-pagination) ) | \n\nOne detail for Python: in the official software development kit (SDK), `mcp` 2.3.0, an unexpected exception in an `MCPServer` tool doesn’t become a protocol error (only a deliberate `MCPError` does). The SDK turns it into an `isError` result that says only `Error executing tool <name>`, so the model still can’t tell what to fix. Raise `ToolError` with a message that says what a valid value looks like, as part 9 shows.\n\n### In state and multi-step flows\n\nFour concern state and multi-step flows, which have to work without sessions. One fix is the specification’s: if `requestState` influences authorization, resource access or business logic, the server MUST protect its integrity, for example with a Hash-based Message Authentication Code (HMAC) or Authenticated Encryption with Associated Data (AEAD).\n\n| Anti-pattern | Why it hurts | Instead | \n|---|---|---|\n| Per-connection state | Breaks behind load balancers under the 2026-07-28 specification | Explicit handles in a shared store ( [part 5](https://pournasserian.com/writing/mcp-2026-stateless-requests) ) | \n| Side effects before `input_required` | Abandoned retries leave half-done work | No writes until the last input round ( [part 14](https://pournasserian.com/writing/mcp-2026-request-state) ) | \n| Unprotected `requestState` | Tampering, replay, cross-user reuse | AEAD or HMAC, plus the principal, an expiry and a digest of the request ( [part 14](https://pournasserian.com/writing/mcp-2026-request-state) ) | \n| Elicitation-only flows | Fail in unattended agents | A non-interactive path with clear errors ( [part 15](https://pournasserian.com/writing/mcp-2026-elicitation) ) | \n\n### In security and trust\n\nThe last three are about trust, and two of the fixes are the specification’s: a server MUST NOT accept or transit any token that isn’t valid for its own resources, and servers SHOULD NOT mark sensitive parameters with `x-mcp-header`, because intermediaries can see header values.\n\n| Anti-pattern | Why it hurts | Instead | \n|---|---|---|\n| Token passthrough | Confused deputy, audience violations | Audience-bound, delegated tokens ( [part 19](https://pournasserian.com/writing/mcp-2026-oauth-step-up-and-entra) ) | \n| Secrets in skills, instructions or `x-mcp-header` | Leaks to models, logs and intermediaries | Keep secrets server-side ( [part 21](https://pournasserian.com/writing/mcp-2026-threat-model) ) | \n| Descriptions in the style of prompt injection, such as “always use this server” | Looks like tool poisoning, so hosts and reviewers distrust it | Factual, scoped descriptions ( [part 4](https://pournasserian.com/writing/mcp-2026-server-instructions) ) | \n\n## Method and caveats\n\n- Built from the patterns and the anti-patterns of my guide’s chapter on design patterns, written against the 2026-07-28 specification; product facts as of October 2026.\n- The search plus fetch, task and layered-surface patterns are my advice; the handle pattern follows the specification’s guidance. Five anti-patterns break a rule of the specification or of its MCP Apps extension (unstable tool order, UI-only answers, unprotected `requestState` , token passthrough and secrets in`x-mcp-header` ), and each group names its rules; the other seven are my advice.\n- Where OpenAI’s documentation is more precise than my notes (what ChatGPT expects `search` and`fetch` for), this article follows OpenAI’s MCP guide as checked on 8 October 2026.\n- Where the Python SDK differs from my notes (what an unexpected exception in a tool becomes), this article describes `mcp` 2.3.0, as I read its source on 9 October 2026.", "url": "https://wpnews.pro/news/mcp-server-design-patterns-and-anti-patterns", "canonical_source": "https://pournasserian.com/writing/mcp-2026-design-patterns", "published_at": "2026-10-24 00:00:00+00:00", "updated_at": "2026-10-24 12:43:43.071272+00:00", "lang": "en", "topics": ["agent-protocols", "ai-agents", "ai-tools", "developer-tools"], "entities": ["Model Context Protocol", "OpenAI", "ChatGPT", "MCP Apps"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/mcp-server-design-patterns-and-anti-patterns", "markdown": "https://wpnews.pro/news/mcp-server-design-patterns-and-anti-patterns.md", "text": "https://wpnews.pro/news/mcp-server-design-patterns-and-anti-patterns.txt", "jsonld": "https://wpnews.pro/news/mcp-server-design-patterns-and-anti-patterns.jsonld"}}