Skip to content

fix(mcp): rename wire fields to contract names before dispatch#3215

Open
bjohas wants to merge 2 commits intosuperdoc-dev:mainfrom
OpenDevEd:fix/mcp-comment-id-rename
Open

fix(mcp): rename wire fields to contract names before dispatch#3215
bjohas wants to merge 2 commits intosuperdoc-dev:mainfrom
OpenDevEd:fix/mcp-comment-id-rename

Conversation

@bjohas
Copy link
Copy Markdown

@bjohas bjohas commented May 10, 2026

Summary

The MCP wire schema (generated from apps/cli operation params) exposes the PARAM_FLAG_OVERRIDES renames commentIdid and parentCommentIdparentId. The contract validator on the receiving side still expects the canonical names, but the MCP dispatch path passes args through raw with no inverse rename — so superdoc_comment actions delete / get / update fail with comments.<action> commentId must be a non-empty string even when callers follow the documented schema.

The CLI handles this in extractInvokeInput() (apps/cli/src/lib/invoke-input.ts PARAM_RENAMES). This PR mirrors that translation for the MCP path: a small applyParamRenames helper in a standalone module, applied in executeOperation right before api.invoke.

Repro (before this fix)

  1. Open a doc with at least one comment via superdoc_open.
  2. superdoc_comment action:"list" — returns comments with valid ids.
  3. superdoc_comment action:"delete" id:"<that-id>" — fails with comments.delete commentId must be a non-empty string.

action:"create" and action:"list" are unaffected (they don't take a comment id).

Affected operations

Operation Wire field Contract field
comments.create parentId parentCommentId
comments.patch (update) id commentId
comments.delete id commentId
comments.get id commentId
getNodeById id nodeId

Changes

  • New apps/mcp/src/tools/param-renames.ts — rename map + applyParamRenames helper. Mirrors the CLI's PARAM_RENAMES; comment in the file points to it.
  • apps/mcp/src/tools/intent.tsexecuteOperation now wraps input with applyParamRenames(opId, input) before api.invoke.
  • New apps/mcp/src/__tests__/intent-param-renames.test.ts — 7 unit tests.

Test plan

  • bun test apps/mcp/src/__tests__/intent-param-renames.test.ts — 7 pass / 0 fail.
  • bun build apps/mcp/src/tools/param-renames.ts --target=node — transpiles clean.
  • CI to type-check the full MCP app.
  • Manual end-to-end: superdoc_comment action:"delete" against a doc that round-trips through list.

Notes

  • The duplication between this module and the CLI's PARAM_RENAMES is intentional for now — the rename table is small (5 entries across 4 operations) and stable. A follow-up could promote it to a shared location, ideally driven by codegen so both the wire schema and the inverse map come from a single source.
  • Alternative considered: change the wire schema to use the canonical contract names (commentId, parentCommentId, nodeId) directly. Cleaner but a breaking change for any caller relying on the current short names.

The MCP wire schema (generated from apps/cli operation params) exposes
PARAM_FLAG_OVERRIDES renames such as `commentId` → `id` and
`parentCommentId` → `parentId`. Without an inverse translation at
dispatch time, the contract validator rejects the wire field names
because it expects the canonical contract names — so
`superdoc_comment` actions `delete`/`get`/`update` fail with
`comments.<action> commentId must be a non-empty string` even when the
caller follows the documented schema.

The CLI applies the inverse rename in extractInvokeInput()
(apps/cli/src/lib/invoke-input.ts PARAM_RENAMES). This commit adds the
matching layer for the MCP path: a small `applyParamRenames` helper in
a standalone module, applied in `executeOperation` immediately before
`api.invoke`.

Affected operations: comments.create (parentId → parentCommentId),
comments.patch / comments.delete / comments.get (id → commentId),
getNodeById (id → nodeId).

Includes unit tests for the rename helper.
@bjohas bjohas requested a review from a team as a code owner May 10, 2026 11:26
Copy link
Copy Markdown
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bjohas thanks for the PR :) good fix and the tests cover the renames cleanly.

two small things inline.

*
* The CLI applies the inverse rename in extractInvokeInput()
* (apps/cli/src/lib/invoke-input.ts PARAM_RENAMES). This module is the MCP
* mirror — kept in lockstep by hand. Three operations are affected today
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

count's off (map has 5 entries) and the em-dash trips our style rule.

Suggested change
* mirror kept in lockstep by hand. Three operations are affected today
* mirror, kept in lockstep by hand. Five operations are affected today
* (comments.delete, comments.get, comments.patch, comments.create's
* parentId, and getNodeById's id), so duplication is small.

// Generated dispatch uses 'doc.' prefix (e.g. 'doc.query.match'); strip it for DocumentApi.invoke()
const opId = operationId.startsWith('doc.') ? operationId.slice(4) : operationId;
return api.invoke({ operationId: opId, input } as DynamicInvokeRequest);
return api.invoke({ operationId: opId, input: applyParamRenames(opId, input) } as DynamicInvokeRequest);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

input gets renamed (idcommentId) but the response goes back as-is, so MCP clients see commentId in the body even though their input uses id. probably fine since the agent reads it as text, but is the asymmetry intentional? worth a line in the PR body, or a follow-up if it should be inverted on the way out too.

@caio-pizzol caio-pizzol self-assigned this May 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants