Skip to content

MCP Server Tool Design: Workflow Tools, Progressive Discovery, and Code Mode

State of practice as of 2026-09. The seed source is Neon’s 2026-09-08 video “MCP Just Got a Whole Lot Better” [1], watched frame-by-frame; every claim it makes about client behaviour was checked against the MCP 2026-07-28 client best-practices page [2], the tools specification [3], and the Anthropic and OpenAI platform docs [4][5][6][7]. Inline [n] cites key to sources; [1 @mm:ss] is a timestamp in the video. Code is illustrative unless it is quoted from a linked README.

1. The shift: clients own discovery and composition now

1:1 endpoint mapping was the first common design, and it has two measured failure modes. Mapping every API endpoint to its own tool (POST /projects -> create_project, GET /branches -> list_branches, …) gives an agent the whole API surface, but with hundreds of endpoints you get hundreds of tools loaded into the context window before the session starts, and near-duplicate endpoints make the agent pick the wrong tool [1 @00:31-01:10]. The numbers behind that: a typical five-server setup (GitHub, Slack, Sentry, Grafana, Splunk) costs ~55k tokens of definitions before any work happens, and selection accuracy degrades once 30-50 tools are available [4]; the MCP docs’ own illustration is ~150,000 tokens of definitions up front versus ~2,000 with on-demand loading [2].

The server-side fix was a layered meta-tool catalog. Instead of hundreds of tools, a server exposes two or three: search the available endpoints, inspect one endpoint’s definition, execute a request [1 @01:10-01:31]. The MCP docs describe the same three layers as catalog (search_tools returning names plus one-line descriptions), inspect (get_tool_details returning one full schema), execute [2].

The big change: that layer moved into the client, under the name progressive tool discovery. MCP’s client best-practices page (protocol revision 2026-07-28) recommends that hosts fetch tools/list as usual but defer injecting definitions into context, expose a lightweight search_tools meta-tool, and load full definitions only as needed, switching to this mode once definitions cross a threshold of roughly 1-5% of the context window [2]. Shipped implementations:

  • Claude Code enables tool search by default on Claude 4.5-generation and later models; a request that needs a server’s tools triggers a ToolSearch call and matching tools are fetched on demand. A discovery cache that lets servers connect on first use needs v2.1.221 or later and, since v2.1.238, is off by default unless a gradual rollout has enabled it (MCP_DISCOVERY_CACHE=1 turns it on) [6].
  • The Claude API takes defer_loading: true per tool (for MCP-connector tools it is set once on the mcp_toolset entry’s default_config, or per tool in its configs, not on the server’s own definitions) plus a tool_search_tool (regex or BM25 variant); deferred tools stay out of the prompt prefix so prompt caching survives, and the docs advise keeping the 3-5 most-used tools non-deferred [4].
  • OpenAI’s Responses API has the same defer_loading flag with hosted or client-executed search; the model initially sees only name and description, and the guidance is to keep each namespace under ten functions [7].

Programmatic tool calling (“code mode”) moved composition into the client too. Instead of calling tools one at a time, the agent writes a script that chains tools, runs it in a sandbox, and only the final result returns to the model [1 @02:01-02:19]. The MCP docs formalise it: the host generates a typed API from each tool’s arguments and outputSchema, the model writes one script against it, the sandbox intercepts calls and brokers them to servers, credentials never enter the sandbox, and only console output comes back [2]. Anthropic’s engineering post puts the same 150,000-to-2,000 token saving (98.7%) on loading only the definitions a task needs, the figure [2] also uses; the intermediate-results benefit is shown but not quantified (a 10,000-row spreadsheet filtered in the sandbox instead of flowing through context) [9]; the API feature is enabled per tool with allowed_callers: ["code_execution_20260120"] and runs in the code-execution container [5]. Cloudflare’s Code Mode (2025-09-26) converts MCP schemas to a TypeScript API, runs generated code in disposable V8 isolates, and uses bindings so API keys are never visible to the model [10].

Consequence: between progressive discovery handling selection and code mode handling composition, clients now own the problems the server-side meta-tool layer was built to solve [1 @02:19-02:30].

2. Do not go back to 1:1: the SDK argument for workflow tools

The obvious inference, “clients handle discovery, so expose every endpoint again”, is wrong for the same reason SDKs are not thin wrappers [1 @02:30-02:41]. Any real API has common tasks that require chaining several calls; if the agent has to rediscover that chain every time it wastes tokens and repeats the same mistakes [1 @02:51-03:11, @04:06-04:16]. SDKs give you the raw endpoints and bundle common workflows into single operations, and the same idea applies to the tools an MCP server exposes [1 @03:04-03:22].

The worked example [1 @03:22-03:46]:

Branch creation (Neon) one tool
create the branch \
attach compute resources >----> createWithCompute (video name)
get the connection string / branches.createAndConnect (shipped name [11][12])

Anthropic’s tool-writing guidance reaches the same design independently: build consolidated, workflow-oriented tools that handle several API calls under the hood (schedule_event rather than list_users + list_events + create_event; search_logs rather than read_logs; get_customer_context rather than three lookups), because “more tools don’t always lead to better outcomes” [8].

Sizing rule from the source: if the API is simple, 1:1 mapping is fine; where there is provisioning-style complexity, ergonomic shortcuts save the agent from repeating the same mistakes [1 @04:00-04:16].

3. Reference architecture: API -> SDK -> tools package -> adapters

Neon’s stack, as described in the video and confirmed in the package READMEs [1 @04:16-05:27][11][12][13]:

+-------------+ +-------------+ +----------------+
| MCP server | | Eve adapter | | Mastra adapter |
+------^------+ +------^------+ +-------^--------+
| | |
+-----------------+------------------+
|
+------+------+
| @neon/tools | tool descriptors: id, title, description,
+------^------+ Zod inputSchema, annotations, execute()
|
+-------------+-------------+
| @neon/sdk |
| ergonomic layer | createAndConnect, resetFromParent, ...
| raw endpoints (OpenAPI) | every endpoint, 1:1, tree-shakeable
+-------------+-------------+
|
+-----+-----+
| API |
+-----------+
  • SDK: generated from the OpenAPI spec so it covers every endpoint (raw), with a thinner ergonomic layer on top (createNeonClient) that adds auth-once, { data, error } results, retries, readiness polling, auto-pagination, and the workflow methods [12].
  • Tools package: converts both layers into tool descriptors. Selectors are SDK paths ("branches.createAndConnect"); the published id is snake_case last-segment-then-resource (projects.list -> list_projects); every tool carries a Zod input schema, title, description, safety annotations, stability metadata and an execute() that strictly validates input, rejects unknown fields, and returns JSON-safe { data } [11].
  • MCP server imports the package to surface the tools; adapters wrap the same package for agent frameworks (Eve, Mastra) [1 @04:56-05:18][11].
  • Categories let a host scope which tools the agent gets [1 @05:18-05:27]; on the hosted server that is a URL query (?category=projects&category=branches, readonly=true), previewable via /api/list-tools, and scope is category metadata, not a read/write switch [13].

The one-line framing: the server controls what tools are available; the client handles discovering and executing them however it sees fit [1 @05:27-05:38].

Details in the tools package worth copying [11]:

  • Paginated list methods become .all() tools with the cursor removed from the schema and a limit cap, so the agent never pages by hand.
  • Write tools wait for readiness (default five minutes); set that below the host’s tool-call timeout or the host gives up first. An abort stops the poll, not the create: list before retrying.
  • Some client methods are deliberately not tools (operations.waitFor, credentials.reveal, storage.objects.get): waiting is built into the write tools, and secrets should not be a tool result.
  • Hosts can override descriptions per tool, rewrite ids (names, name: (id) => \neon_${id}`), and inject project_id/branch_idwithomitFromSchema: true` so the injector is the only source.
  • The MCP adapter returns text content and structuredContent; failures use isError: true with a structured { error: { message, kind, status, code, requestId, ... } }.
  • Approval is carried as neon/requiresApproval in _meta and every non-read operation is marked as requiring it, because MCP annotations are advisory and the protocol does not enforce approval.

4. Server-side checklist (spec-grounded)

ConcernDo thisWhy / source
Names1-128 chars, [A-Za-z0-9_.-], unique per server; expect clients to prefix with a server id when aggregatingSpec SHOULDs; server name is not unique across servers [3]
NamespacingPrefix by service or resource (asana_projects_search); unambiguous parameter names (user_id, not user)A shared prefix lets one search match the whole group [4]; Anthropic found prefix-vs-suffix placement had non-trivial effects on its tool-use evaluations [8]
DescriptionsWrite as if onboarding a new hire: make query formats and terminology explicit; include the words users sayAnthropic reports that small description refinements yielded dramatic improvements on its held-out Slack and Asana evaluations (the post shows the curves but gives no percentage); tool search indexes names, descriptions, argument names and argument descriptions [4][8]
Input schemaValid JSON Schema object; no-parameter tools use { "type": "object", "additionalProperties": false }; strict, typed inputsSpec MUST (a valid JSON Schema object, not null); the empty-object shape is the spec’s recommended form for no-parameter tools; strict data models avoid ambiguity [3][8]
Output schemaProvide outputSchema and return structuredContent plus the serialised JSON as textServers MUST conform when declared; code-mode hosts generate typed stubs from it, and without it the fallback is any [2][3]
AnnotationsSet readOnlyHint/destructiveHint/idempotentHint/openWorldHint, but never rely on them for safetyClients MUST treat annotations as untrusted unless they come from trusted servers; enforce approval in the host policy (Neon: _meta flag) [3][11]
ErrorsExecution errors as isError: true with actionable text (“date must be in the future, today is …”); protocol errors only for unknown tool / malformed requestClients SHOULD feed execution errors back for self-correction; opaque codes do not help [3][8]
Result sizePaginate, filter, truncate; offer DETAILED vs CONCISE response formats; prefer semantic fields (name) over identifiers (uuid)Claude Code caps MCP results at 25k tokens by default (MAX_MCP_OUTPUT_TOKENS, per-tool anthropic/maxResultSizeChars in _meta); concise Slack responses used ~1/3 of the tokens; resolving UUIDs to names improved precision [6][8]
List stabilityDeterministic tools/list order; declare listChanged and send notifications/tools/list_changed; honour ttlMs/cacheScopeStable order improves prompt-cache hits; hosts re-index their search catalog on list_changed [2][3]
StateNo protocol session: return an explicit handle from a creation tool, validate authorization against it every call, state the retention policy in the description, return an execution error on expirySpec’s stateful-tools guidance [3]
SecurityValidate all inputs, implement access control, rate-limit, sanitise outputs; never mark secrets with x-mcp-headerThe first four are spec MUSTs for servers; the x-mcp-header rule is a SHOULD NOT [3]
Catalog sizeKeep the always-on set small (3-5 non-deferred tools; OpenAI: fewer than ten per namespace) and expose categories so hosts can scope grantsSelection degrades past 30-50 tools; category URLs are how Neon scopes [4][7][13]

5. Evaluate before you ship

Anthropic’s loop, which produced the accuracy gains above [8]: generate dozens of realistic prompt/response pairs that need many tool calls; run them and track accuracy, runtime, tool-call count, token consumption and tool errors; read the agent’s reasoning and raw transcripts for where it stalled; let an agent rewrite descriptions from the transcripts; keep a held-out set so you are not tuning to the test.

Agreed vs folklore (compressed)

  • Agreed: definition bloat and selection degradation are measured, not anecdotal [2][4]; progressive discovery is a client responsibility with three shipped implementations [4][6][7]; code mode is standardised guidance with vendor implementations [2][5][10]; workflow tools remain necessary and vendors converge on it independently [1][8]; annotations are advisory [3][11].
  • Folklore, rebutted by the source: “clients do discovery now, so expose every endpoint” [1 @02:30-02:41]; “put a search/inspect/execute layer in the server” is now redundant for tool-search clients [1 @01:31-01:50][2].
  • Overstated in the video: it names Claude Code and Codex as implementing code mode [1 @01:57-02:05]. As of 2026-09-12 Claude Code’s MCP docs document tool search but no code-mode execution of MCP tools [6]; programmatic tool calling is a Claude API feature [5]. Treat the Codex half as the video’s claim until a primary Codex source is added here.

Synthesis (inferred)

  • Decision rule. Count the tools a client will see across all its servers, not just yours. Under ~10 tools with small schemas, 1:1 mapping and upfront loading are fine. Past that, do not add a meta-tool layer; instead ship (a) workflow tools for every multi-call task you see agents repeat, (b) outputSchema on everything so code-mode stubs are typed, (c) categories so the host can scope the grant. The server’s job is a well-described, scoped catalog; the client’s job is retrieval and composition.
  • Keep a server-side meta-tool layer only as a fallback for clients that cannot search: Claude Code disables tool search under a custom ANTHROPIC_BASE_URL, ENABLE_TOOL_SEARCH=false, or pre-4.5 models [6]. Gate it behind a flag rather than making it the default surface.
  • outputSchema is the cheapest high-leverage change for existing servers: it costs nothing at call time and is what turns a code-mode stub from Promise<any> into a typed function the model can chain without a round trip [2].
  • Applied to this corpus’s own servers. The mx MCP server publishes 47 tools in one flat list; with two or three other servers attached that is inside the 30-50 band where selection degrades [4], so category scoping (as in Neon’s ?category= grants) and a handful of workflow tools (for example one “scaffold and start a service” tool instead of add-service + build + dev + ps) are the two changes this document argues for. vidwatch’s seven tools with ranked inline frames are already in the “small catalog, rich results” shape; its remaining gap is outputSchema on watch/frames so a code-mode host can iterate over frames without re-reading the timeline.