Repository navigation
fix(studio): tolerate WebMCP execute calls without an options object - #3860
Merged
Merged
Conversation
Every Studio write tool (studio_set_text, studio_set_style,
studio_transform, studio_add_animation, studio_update_animation,
studio_add_keyframe, studio_delete_animation) registered its handler as
`execute: (input, { signal }) => ...`. The W3C shape passes an options
object, but the bundled `@mcp-b/global` polyfill invokes a registered
`execute` with the input alone, both from its in-page BrowserMcpServer
wrapper and from the descriptor it mirrors into a native
`document.modelContext`. The destructure therefore threw
`TypeError: Cannot destructure property 'signal' of 'undefined'` before
the handler ran, while the read tools, which ignore the second
parameter, kept working.
Route the seven signal-taking tools through one `writeTool` helper in
`buildStudioTools` that reads `options?.signal`, so a missing options
object yields an undefined signal at a single boundary. The handlers
already default an undefined signal to a never-aborted one, so nothing
downstream changes. `ModelContextTool.execute` now declares `options`
optional to match what callers actually do.
Upstream check: `@mcp-b/global` 5.0.1 (pinned), 5.0.3 and 5.1.0 ship a
byte-identical `@mcp-b/webmcp-polyfill` chunk and the same one-argument
call in `@mcp-b/webmcp-ts-sdk`, so a dependency bump would not fix this.
Tests: drive `studio_set_text` through the real `@mcp-b/global`
package under jsdom (registry entry `execute` and Chromium-style
`executeTool`) and assert a saved result; call every write tool with
one argument against a fake model context and assert none reports an
`internal` failure. The existing spec-shaped tests, including the early
abort path, still pass, which proves a provided signal still reaches
the handler. Shared inert deps builders move to `webmcpTestUtils.ts`.
Closes #3858
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Install a native-looking `document.modelContext` before importing `@mcp-b/global` so the bridge wraps it and mirrors every Studio registration into it. One setup then exercises both one-argument call paths the package ships: the in-page BrowserMcpServer wrapper (registry entry and Chromium-style `executeTool`) and the descriptor mirrored into the native context. Both fail with "Cannot destructure property 'signal' of 'undefined'" without the fix and save with it. Co-Authored-By: Claude Fable 5.1 <[email protected]>
miguel-heygen
approved these changes
Sep 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Every Studio write tool (
studio_set_text,studio_set_style,studio_transform,studio_add_animation,studio_update_animation,studio_add_keyframe,studio_delete_animation) registered its handler asexecute: (input, { signal }) => …. That is the W3C WebMCP shape, but the bundled@mcp-b/globalpolyfill invokes a registeredexecutewith the input alone, on both of its paths: the in-pageBrowserMcpServerwrapper (Reflect.apply(r.execute, void 0, [L])) and the descriptor it mirrors into a nativedocument.modelContext(execute: r => Reflect.apply(o.execute, void 0, [r])). The destructure threwTypeError: Cannot destructure property 'signal' of 'undefined'before the handler ran, whichrunToolBodysurfaced as aninternalfailure. The read tools ignore the second parameter, which is why they kept working.What changed
useStudioAgentTools.ts: the seven signal-taking tools now go through onewriteToolhelper insidebuildStudioToolsthat readsoptions?.signal. A missing options object yields an undefined signal at a single boundary; the handlers already default an undefined signal to a never-aborted one, so nothing downstream changes. When a caller does pass{ signal }, it still reaches the handler unchanged.types.ts:ModelContextTool.executedeclaresoptionsoptional, matching what callers actually do.useStudioAgentTools.polyfill.test.tsxdrivesstudio_set_textthrough the real@mcp-b/globalpackage under jsdom. A native-lookingmodelContextis installed before the import so the bridge wraps it and mirrors every registration into it, which covers both paths from one setup: the in-page wrapper (registry entryexecuteand Chromium-styleexecuteTool) and the descriptor mirrored into the native context. Both assert asavedresult.useStudioAgentTools.test.tsxcalls every write tool with one argument against a fake model context and asserts none reports aninternalfailure. The existing spec-shaped tests, including the early-abort path (cancelRequested: true), still pass, which proves a provided signal still reaches the handler.webmcpTestUtils.tsso both suites share them.Trade-off
When the polyfill omits the options object there is no signal to observe, so caller cancellation cannot propagate to the handler on either polyfill path; the write runs to completion exactly as it did before the tool was reachable at all. Callers that do pass
{ signal }(a spec-conformant user agent, or Studio's own tests) keep pre-dispatch cancellation. The polyfill's ownthrowIfAborted()check still runs before the handler is invoked.Upstream finding
@mcp-b/globalis pinned at^5.0.1(lockfile 5.0.1). npm has 5.0.3 and 5.1.0. The@mcp-b/webmcp-polyfillchunk is byte-identical (same md5) across 5.0.1, 5.0.3 and 5.1.0, and the@mcp-b/webmcp-ts-sdkdiff between 5.0.1 and 5.1.0 does not touch thetool.execute(args)call or the(input) => execute(input)native-mirror adapter. No released version forwards{ signal }toexecute, and no open pull request in the upstream repository addresses it, so this PR does not bump the dependency; a bump is unrelated to this fix and can be a follow-up if wanted.Closes #3858
Test plan
Run from the repo root after
bun installandbun run --filter '@hyperframes/{parsers,lint,studio-server}' build(the studio vite config resolves those at load time).cd packages/studio && NODE_ENV=test bunx vitest run src/webmcp— 15 files, 180 tests passed.useStudioAgentTools.polyfill.test.tsxwith the source fix swapped for theorigin/mainversions ofuseStudioAgentTools.tsandtypes.ts— both tests (wrapper path and native-mirror path) fail withTypeError: Cannot destructure property 'signal' of 'undefined', reproducing the report; with the fix, both pass.cd packages/studio && bun run typecheck— exit 0.bunx oxlint <changed files>— 0 warnings, 0 errors.bunx oxfmt --check <changed files>— all files formatted.bun run --filter @hyperframes/studio build— exit 0; the built chunk still contains the polyfill's one-argument wrappers quoted above, which the polyfill test exercises through the package itself.— Miga
🤖 Generated with Claude Code