Repository navigation
Description
Follow-up to #173 (MCP spec 2026-07-28 migration) and #174. Two flags now look like they cost more than they return. Both are documented public options, so this issue is for deciding what to do rather than a patch — and either half can be split out.
All figures below were measured on main after #178, 62 tools enabled, Node 24.
1. --dynamic-toolsets is unsound on the HTTP transport
MCP 2026-07-28 removed protocol sessions, so there is no per-client scope left to hold toolset state. #173 responded by sharing one ToolsetGroup across every per-request server, which is the only thing that makes enable_toolset mean anything — but it also makes the state process-wide.
src/createBacklogMcpServer.ts says as much:
Sharing it makes toolset state process-wide, which is the only scope left now that the protocol has no sessions.
Verified against a running server (--transport http --dynamic-toolsets --enable-toolsets space):
tools/list -> 10 tools
tools/call enable_toolset {issue} -> "Toolset issue enabled"
tools/list -> 37 tools
The 10 → 37 change is visible to every client of that process, not just the caller. On a single-user desktop setup that is the intended behaviour. On a shared or multi-tenant HTTP deployment one client silently rewrites what every other client sees, and there is no way to undo it (there is no disable_toolset).
The related tools/list cache hint is correctly withheld in this mode, so at least clients are not caching a list that can grow.
Options
- Reject the combination at startup —
--dynamic-toolsetswith--transport httpexits with an explanatory error. Simplest, and honest about what the protocol can express. - Warn and keep going — cheaper for existing users, but leaves the surprise in place.
- stdio only — same as (1) but framed as a documented restriction.
- Per-request scope — derive the enabled set from a request header or the tool call itself. Real work, and no spec support for it.
I lean towards (1): the feature's contract ("enabling persists for me") cannot be honoured for a single client on this transport, so failing fast beats a silent shared mutation.
2. --optimize-response pays a permanent prompt cost to save a variable one
The flag exists to shrink tool responses by letting the client select fields. It does that by adding a fields parameter whose description embeds a generated type definition — on every tool, in every tools/list.
| mode | tools/list |
|---|---|
| default | 38.8 KB |
--optimize-response |
71.7 KB (+85%) |
Of that, 31.4 KB (44% of the whole payload) is fields descriptions. It is a fixed, up-front cost paid on every session, against a saving that only materialises when the agent actually uses field selection on a large response.
Three things make the trade worse
fields is a required parameter. extendSchema builds it as z.string().describe(desc) with no .optional(), so enabling the flag makes every call fail unless fields is supplied:
tools/call get_space {}
-> Invalid arguments for tool get_space: fields: Invalid input: expected string, received undefined
This is long-standing, not a regression — I built v0.14.0 and reproduced it identically. Whether it is intended is worth settling either way.
The generated type definition is very coarse. mapZodTypeToGraphQLType only distinguishes String / Int / Boolean / JSON plus nullability. ZodArray has no branch at all and falls through to String, so arrays are described incorrectly:
type Document {
id: String!
attachments: String <- z.array(DocumentFileInfoSchema)
tags: String <- z.array(DocumentTagSchema)
createdUser: JSON <- nested objects collapse to JSON
}
It is the only consumer of a large hand-written surface. outputSchema is never passed to the MCP SDK (registerOnce sends inputSchema only) and never validates a response. Its only runtime use is generating these descriptions. Dropping the flag would make redundant:
src/types/zod/backlogOutputDefinition.ts— 549 lines, 56 schemas, hand-maintained in parallel with the typesbacklog-jsalready shipssrc/utils/generateFieldsDescription.tsandsrc/handlers/transformers/wrapWithFieldPicking.ts- the
graphqldependency (~1 MB), used only forparse/SelectionSetNodein one file
The duplication is not theoretical: #178 deleted a DocumentSchema that matched neither backlog-js's Document.Document nor the API — wrong id type, name/content instead of title/plain/json — and nothing noticed, because nothing validates against it.
Options
- Keep it, make
fieldsoptional. Smallest change; leaves the 31.4 KB in place. - Keep it, shrink the description. Emit field names only and drop the type definition block. Most of the 31.4 KB is the type def, which is coarse enough to be of limited use.
- Deprecate and remove it. Recovers the payload, 549 lines, and a 1 MB dependency. Breaking, so it needs a version decision.
- Compromise: default off (already is) plus (2), and revisit removal later.
Not proposed: publishing outputSchema over MCP
Worth recording since it comes up naturally — the spec has supported tool output schemas since 2025-06-18, and these zod schemas already exist. Converted to JSON Schema they total 220.2 KB across 62 tools (versus 33.0 KB for all input schemas). Publishing them would take tools/list past 260 KB. Not viable at this size.
Method
Both payload figures come from driving the built server over stdio with a tools/list call and measuring JSON.stringify(result.tools).length, with and without the flag. The dynamic-toolsets behaviour was reproduced against the HTTP transport with real requests. Happy to attach the scripts if useful.
Activity
katayama8000 commentedon Aug 3, 2026
10 → 37 tools がプロセス全体に見えることを実測ログ付きで記載。選択肢を4つ挙げて、(1) HTTP と併用したら起動時にエラーで落とすを推しています。「有効化が自分に対して永続する」という契約がこのトランスポートでは1クライアントに対して守れないので、黙って共有変更するより即座に失敗する方が誠実、という理由です。
┌─────────────────────┬─────────────────┐
│ モード │ tools/list │
├─────────────────────┼─────────────────┤
│ デフォルト │ 38.8 KB │
├─────────────────────┼─────────────────┤
│ --optimize-response │ 71.7 KB(+85%) │
└─────────────────────┴─────────────────┘
うち fields の説明文が 31.4 KB(全体の44%)。加えて3つの補強材料を書きました:
選択肢は「fields を optional にするだけ」から「廃止」まで4段階。
補足として却下した案も記録しました: outputSchema の MCP 公開は JSON Schema 化で 220.2 KB(入力スキーマ全部で 33.0 KB)になるため非現実的。
fix: only warn about dynamic toolsets when they were switched on
refactor: remove dynamic toolsets
fix: only warn about dynamic toolsets when they were switched on
perf: drop the output type definition from the `fields` description
perf: publish `fields` only on tools that return a list
fix: stop requiring `fields` under --optimize-response
refactor: make `fields` an enum of the tool's own field names