Summary
The eight cad_* tools are generated from a stub that declares only sessionId and sets additionalProperties: true. Every other argument — including all arrays — travels undeclared.
Some MCP clients do not preserve structured values for parameters a tool does not declare. On such a client, an array argument arrives at PartMode as a non-array and is rejected. Because every CAD mutation is carried in undeclared arguments (sketch.shapes, poly.pts, edges, and so on), the practical effect is that no CAD transaction can be sent at all from that client, even though session setup, approval, inspection and capability discovery all work.
This is not a defect in PartMode's server or validator — I verified both behave correctly (evidence below). It is a robustness and discoverability gap that PartMode is well placed to close, and the fix already exists in the same file for cad_artifact.
Environment
- Repo at
fe88558a34c8d7a3e03b34c2a374ba5ce6febe9f
- Hosted endpoint
https://partmode.com/mcp, studio 8.0.0, kernel replicad-open-cascade/runtime-5A
- Browser-approved session,
preview-required and read-only both tried
- Client: Claude Code
Reproduction
In one approved session, three calls minutes apart:
| # |
parameter |
declared in tool schema? |
value sent |
result |
| 1 |
partmode_connect.permissions |
yes, typed array |
["project.read"] |
arrived intact — session echoed exactly ["project.read"], not the default permission set |
| 2 |
cad_capabilities.detail |
no |
"schemas" (string) |
arrived intact — accepted and acted on |
| 3 |
cad_capabilities.operationKinds |
no |
["feature.revolve"] |
rejected: operationKinds must be a bounded list of at most 20 stable capability IDs. |
Rows 1 and 2 are the controls. A declared array survives; an undeclared scalar survives; an undeclared array does not. So the failure is specific to structured values on undeclared parameters.
Why this is not a server-side bug
- The kind name was valid.
selectCadCapabilities checks names against the manifest immediately after the bounded-list check and raises a different error, so the request failed at Array.isArray, not on an unknown kind.
- Field names were correct. Probing
kinds and a nested capabilityDiscovery both returned Unknown capability discovery fields: …, while operationKinds never did.
- The server does not touch the value.
src/mcp.ts:466-473 strips sessionId and forwards {...args} verbatim.
// src/static/studio-agent-service.js:1426
function boundedCapabilityIds(value, name) {
if (value == null) return [];
if (!Array.isArray(value) || value.length > 20 || value.some(...)) {
invalidCapabilityQuery(`${name} must be a bounded list of at most 20 stable capability IDs.`);
}
The validator is correct. The value simply is not an array by the time it arrives.
Suggested fix
Give the eight generated tools real input schemas. src/mcp.ts:176-186 currently produces:
inputSchema: {
type: 'object',
required: ['sessionId'],
properties: { sessionId: SESSION_ID_SCHEMA },
additionalProperties: true,
},
cad_artifact, immediately above at src/mcp.ts:135-164, already does the right thing — it declares format, scope, and a typed entities array. Extending that treatment to cad_capabilities, cad_inspect, cad_query, cad_preview, cad_commit, cad_history, cad_ui and cad_events would:
- make array arguments survive on clients that only marshal declared parameters;
- let clients validate transactions locally before spending a round-trip and a commit budget;
- make the tools self-describing in
tools/list, which removes the cad_capabilities detail: "schemas" round-trip for the common cases.
Even a partial step helps: declaring just detail, operationKinds and queryKinds on cad_capabilities would unblock schema discovery, which is the entry point for everything else.
Secondary observation
cad_capabilities with detail: "full" times out on a browser-approved session against the 164-operation manifest, and leaves the session SESSION_BUSY for a while afterwards. With detail: "schemas" unreachable (above) and full timing out, summary is the only working detail level — which is what made this a hard stop rather than an inconvenience. A paginated or chunked full would be a useful fallback.
What I was trying to do
Rebuild a single parametric lens barrel in PartMode that had already been built in build123d, export STEP from both, and compare named critical dimensions to ≤0.01 mm — an independent-toolchain check on the same OpenCascade kernel. Session setup, visible approval, cad_inspect and cad_capabilities at summary detail all worked exactly as documented; the operation schemas I eventually read from this repository's source. Only argument transport blocked it.
Summary
The eight
cad_*tools are generated from a stub that declares onlysessionIdand setsadditionalProperties: true. Every other argument — including all arrays — travels undeclared.Some MCP clients do not preserve structured values for parameters a tool does not declare. On such a client, an array argument arrives at PartMode as a non-array and is rejected. Because every CAD mutation is carried in undeclared arguments (
sketch.shapes,poly.pts,edges, and so on), the practical effect is that no CAD transaction can be sent at all from that client, even though session setup, approval, inspection and capability discovery all work.This is not a defect in PartMode's server or validator — I verified both behave correctly (evidence below). It is a robustness and discoverability gap that PartMode is well placed to close, and the fix already exists in the same file for
cad_artifact.Environment
fe88558a34c8d7a3e03b34c2a374ba5ce6febe9fhttps://partmode.com/mcp, studio 8.0.0, kernelreplicad-open-cascade/runtime-5Apreview-requiredandread-onlyboth triedReproduction
In one approved session, three calls minutes apart:
partmode_connect.permissions["project.read"]["project.read"], not the default permission setcad_capabilities.detail"schemas"(string)cad_capabilities.operationKinds["feature.revolve"]operationKinds must be a bounded list of at most 20 stable capability IDs.Rows 1 and 2 are the controls. A declared array survives; an undeclared scalar survives; an undeclared array does not. So the failure is specific to structured values on undeclared parameters.
Why this is not a server-side bug
selectCadCapabilitieschecks names against the manifest immediately after the bounded-list check and raises a different error, so the request failed atArray.isArray, not on an unknown kind.kindsand a nestedcapabilityDiscoveryboth returnedUnknown capability discovery fields: …, whileoperationKindsnever did.src/mcp.ts:466-473stripssessionIdand forwards{...args}verbatim.The validator is correct. The value simply is not an array by the time it arrives.
Suggested fix
Give the eight generated tools real input schemas.
src/mcp.ts:176-186currently produces:cad_artifact, immediately above atsrc/mcp.ts:135-164, already does the right thing — it declaresformat,scope, and a typedentitiesarray. Extending that treatment tocad_capabilities,cad_inspect,cad_query,cad_preview,cad_commit,cad_history,cad_uiandcad_eventswould:tools/list, which removes thecad_capabilitiesdetail: "schemas"round-trip for the common cases.Even a partial step helps: declaring just
detail,operationKindsandqueryKindsoncad_capabilitieswould unblock schema discovery, which is the entry point for everything else.Secondary observation
cad_capabilitieswithdetail: "full"times out on a browser-approved session against the 164-operation manifest, and leaves the sessionSESSION_BUSYfor a while afterwards. Withdetail: "schemas"unreachable (above) andfulltiming out,summaryis the only working detail level — which is what made this a hard stop rather than an inconvenience. A paginated or chunkedfullwould be a useful fallback.What I was trying to do
Rebuild a single parametric lens barrel in PartMode that had already been built in build123d, export STEP from both, and compare named critical dimensions to ≤0.01 mm — an independent-toolchain check on the same OpenCascade kernel. Session setup, visible approval,
cad_inspectandcad_capabilitiesatsummarydetail all worked exactly as documented; the operation schemas I eventually read from this repository's source. Only argument transport blocked it.