Folder tools take a bare path param, end to end
The folder_create / folder_delete tools advertise, transport, and handle a single filesystem argument named path — the same name in the server’s advertised schema, on the wire (folder.create({ path }), folder.delete({ path, recursive })), and in the toolkit handler. There is no alias and no advertised-vs-wire divergence.
Context
A DX regression run (2026-07-13) caught the model sending path to folder_create eight times — every attempt a -32602 “expected string, received undefined” — before it adapted to the then-required folder_path. Those two tools were the only ones in the run to trip param validation. On a surface where ~40 tools use the idiomatic file_path (produced correctly 60+ times, zero failures), the model “corrects” the outlier folder_path to the generic path.
folder_path had itself been a deliberate rename from path (2026-04-17), made when bare path was ambiguous across node-tree, file, and folder meanings. That ambiguity no longer holds: every sibling path param is now prefixed (file_path, node_path, parent_path, source_path/dest_path), so path is unique to these two tools, and the tool name (folder_*) already says the path is a folder. Renaming back is safe because of that uniqueness — path disambiguates on its own here.
Decision
Rename folder_path → path everywhere for folder_create and folder_delete; no alias, no shim.
- One name, all layers. The advertised
inputSchema, the wire method payload, and the toolkit handler all usepath.folder_deletekeeps itsrecursiveparam unchanged. pathstays required in the advertised schema (a raw{ path: z.string() }shape), with noadditionalPropertieslooseness.- Scoped to single-path folder tools only. Multi-path tools keep role-distinct names (
asset_import’ssource_path/dest_pathare not renamed —paththere would be ambiguous). Node-tree paths (node_path,parent_path) are never renamed topath— that collides with the filesystem meaning.
Considered options
An earlier draft of this decision advertised path while keeping folder_path on the wire, resolving the two names with a server-side z.preprocess rename — a “hidden alias” that would accept either name. That mechanism is infeasible on the current stack (MCP SDK 1.29 / zod 4): a top-level z.preprocess (a Zod pipe) loses its object shape when the SDK converts it for tools/list, so the advertised schema comes out empty — no path property, no required — and the structural smoke gate fails Zod→JSON-Schema conversion at two checks. A builder and an independent adversarial refuter both reproduced this against the real registration path; no full-ZodType form survives it. The hidden-alias approach was therefore abandoned.
Given the choice between a server-only shim and a clean rename, the full rename was chosen for cross-layer consistency: a reader tracing the wire sees the same name the model sees, with no advertised-vs-wire divergence to explain.
The belt-and-suspenders coverage the alias would have bought (silently accepting a stray folder_path) is low-cost to drop: the raw shape strips the unknown key and the resulting error names the correct, advertised param (path), which is now what the model reaches for anyway.
Consequences
- No advertised-vs-wire divergence. The name is identical at every layer.
- A stray
folder_pathnow errors (namingpath), where the abandoned alias would have accepted it. Acceptable — the run-1 evidence is that the model reaches forpath, notfolder_path. - Pattern for the deferred alias candidates changes. Any future reflex-name friction (evidence-gated) is resolved by a rename to the reflex name, provided that name is unambiguous on the surface — not by a server-side alias.
- No
contract.md/ arch-doc change. The wire contract does not enumerate per-tool params (they live in the generated tool-reference), and a param rename is not an architectural change — the blast radius is the two folder tools plus their tests.
Amendment — enum-value aliases are feasible (the channel selector)
The infeasibility finding above is specific to a parameter-name alias: an object-level key-rename (folder_path → path) needs a top-level z.preprocess pipe, which loses its object shape in the SDK’s tools/list conversion and advertises an empty schema. That failure is about renaming the key.
An enum-value alias is a different, feasible shape: when several accepted values denote the same concept and should route to one flow, a field-level z.preprocess on that field maps the legacy value to the canonical one —
channel: z
.preprocess((v) => (v === "game" ? "runtime" : v), z.enum(["editor", "runtime"]))
.optional()
Here z.toJSONSchema advertises only the inner enum(["editor", "runtime"]), so tools/list shows the canonical values while the runtime read still accepts the legacy game. Because the preprocess sits on one field (not wrapping the whole object), the object shape survives conversion — the empty-schema failure does not apply.
This is what the channel selector now uses to unify the two channel-selecting tools onto one vocabulary:
signal_emit.mode→channel(editor | runtime, defaulteditor).execute_code.context→channel(editor | runtime, defaultruntime).- On both tools, the legacy value
gameis accepted as a hidden alias forruntime— mapped by the field-level preprocess, not advertised intools/list.
The distinction stands: a key rename with no advertised divergence gets a full rename (the decision above); an enum-value consolidation, where the names mean the same thing, gets a field-level value alias so the wire keeps accepting the legacy value while advertising only the canonical one.