Skip to content

feat: WebMCP tools for the agent-render viewer - #38

Closed
baanish wants to merge 1 commit into
mainfrom
cursor/webmcp-register-tools-868d
Closed

baanish wants to merge 1 commit into
mainfrom
cursor/webmcp-register-tools-868d

Conversation

@baanish

@baanish baanish commented Apr 18, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the WebMCP imperative API by calling navigator.modelContext.registerTool() on page load (inside ViewerShell). Tools expose the same actions users can take in the UI: read decode/fragment state, load built-in example fragments, switch artifacts in a bundle, copy/download/print, and clear the hash.

Details

  • Registration runs once on mount with an AbortController so all tools unregister on unmount, matching the agent skill guidance.
  • A ref is kept fresh with useLayoutEffect so execute callbacks always call the latest handlers without duplicate registration.
  • Adds minimal src/types/webmcp.d.ts for TypeScript and tsconfig inclusion for src/types/**/*.d.ts.
  • Unit test mocks navigator.modelContext to verify eight tools register.

The spec uses registerTool, not provideContext—the implementation follows the current draft and Chrome/WebMCP skill.

Validation

  • npm run check (lint, tests, typecheck, build)
Open in Web Open in Cursor 

Open with Devin

Summary by CodeRabbit

  • New Features
    • Added WebMCP integration to enable AI agents to interact with the app, including loading sample artifacts, accessing viewer state, and managing document operations.

Expose navigator.modelContext.registerTool definitions for decode state,
example fragments, artifact selection, copy/download/print, and clearing
the hash. Use a ref updated each frame so execute callbacks stay fresh,
and AbortController to unregister on unmount. Add minimal TS typings
and unit tests with a mocked ModelContext.

Co-authored-by: Aanish Bhirud <baanish@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces WebMCP tool registration infrastructure to integrate agent-driven tool execution into the viewer. It includes TypeScript type declarations for the navigator.modelContext API, a registration module that defines and registers tools, integration into the ViewerShell component to expose viewer state and actions, and accompanying tests.

Changes

Cohort / File(s) Summary
WebMCP Type Definitions
src/types/webmcp.d.ts, tsconfig.json
Added TypeScript declarations for navigator.modelContext API surface including tool registration, execution callbacks, and client interfaces. Updated tsconfig to include type declaration files in compilation.
WebMCP Tool Registration
src/lib/webmcp/register-agent-render-tools.ts
New module defining 6 example fragment keys, state/action contracts (WebMcpViewerState, AgentRenderWebMcpActions), and registerAgentRenderWebMcpTools() function that registers 8 read-only and action tools on navigator.modelContext with cleanup via AbortController. Includes utility for mapping sample hashes to example keys.
ViewerShell Integration
src/components/viewer-shell.tsx
Added WebMCP actions reference and layout effect that populates viewer state (decode status, active/available artifacts, examples) and action handlers (getViewerState, loadSampleByKey, loadSampleByTitle, artifact operations). Registered tools on component mount with cleanup on unmount.
Tests
tests/webmcp/register-agent-render-tools.test.ts
New test suite verifying tool registration count (8 tools), registered tool names, AbortSignal cleanup binding, and no-op behavior when API unavailable.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 With tools now registered, we hop with glee,
WebMCP agents can call and see,
Viewer state flows through the agent's mind,
Examples and artifacts of every kind!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing WebMCP tools integration for the agent-render viewer component.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/webmcp-register-tools-868d

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread src/types/webmcp.d.ts
requestUserInteraction(callback: () => Promise<unknown>): Promise<unknown>;
}

type ToolExecuteCallback = (input: object, client: ModelContextClient) => Promise<unknown>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Type signature mismatch - ToolExecuteCallback requires two parameters (input, client), but all tool execute implementations only accept (input) or (). This violates the declared type and would fail strict type checking. Make client optional: (input: object, client?: ModelContextClient) => Promise<unknown>.


/** Maps example keys to sample link hashes (same order as `WEBMCP_EXAMPLE_KEYS`). */
export function buildExampleHashByKey(sampleHashes: readonly string[]): Record<WebMcpExampleKey, string> {
const out = {} as Record<WebMcpExampleKey, string>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Return type incorrect - buildExampleHashByKey returns Record<WebMcpExampleKey, string> (all keys required) but only assigns keys conditionally when h !== undefined. If sampleHashes is shorter than WEBMCP_EXAMPLE_KEYS, the returned object is missing keys, violating the type contract. Change return type to Partial<Record<WebMcpExampleKey, string>> or ensure assignment always happens.

@kilo-code-bot

kilo-code-bot Bot commented Apr 18, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
src/types/webmcp.d.ts 10 ToolExecuteCallback type requires (input, client) but implementations only accept (input) or () - violates type contract
src/lib/webmcp/register-agent-render-tools.ts 268 buildExampleHashByKey returns Record<WebMcpExampleKey, string> but only conditionally assigns keys - return type is inaccurate
Other Observations (not in diff)

No issues found outside the diff.

Files Reviewed (5 files)
  • src/components/viewer-shell.tsx - WebMCP integration
  • src/lib/webmcp/register-agent-render-tools.ts - New tool registration module
  • src/types/webmcp.d.ts - New WebMCP type definitions
  • tests/webmcp/register-agent-render-tools.test.ts - New unit tests
  • tsconfig.json - TypeScript config update

Reviewed by step-3.5-flash · 369,860 tokens

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying agent-render with  Cloudflare Pages  Cloudflare Pages

Latest commit: 970a863
Status: ✅  Deploy successful!
Preview URL: https://b6ce9d32.agent-render.pages.dev
Branch Preview URL: https://cursor-webmcp-register-tools.agent-render.pages.dev

View logs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 970a86318d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +181 to +182
read().selectArtifact(id);
return { ok: true, artifactId: id };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate artifact ID before claiming selection succeeded

agent_render.select_artifact always returns { ok: true } for any non-empty artifactId, but it never verifies that the ID exists in the current bundle before calling selectArtifact. With a typo or stale ID, the viewer path normalizes activeArtifactId to the first artifact (via envelope normalization) rather than selecting the requested artifact, so the tool reports success while navigating to an unintended artifact. This can mislead agent workflows that rely on the response to confirm state changes.

Useful? React with 👍 / 👎.

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +198 to +201
execute: async () => {
await read().copyActiveArtifact();
return { ok: true };
},

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.

🟡 copy_active_artifact tool always returns { ok: true } even when clipboard copy fails

The execute handler for agent_render.copy_active_artifact unconditionally returns { ok: true } after awaiting copyActiveArtifact(). However, the underlying handleArtifactCopy at src/components/viewer-shell.tsx:397-419 catches clipboard errors internally (setting UI state to "failed") without re-throwing them. It also silently returns when there is no active artifact. This means the tool reports success to the agent caller in all cases—including when the clipboard write actually failed or when no artifact was loaded—giving incorrect feedback that could mislead an agent into thinking the content was copied when it was not.

Prompt for agents
The copy_active_artifact tool always returns { ok: true } because handleArtifactCopy (viewer-shell.tsx:397-419) catches clipboard errors internally and never propagates them. Two failure modes are silently swallowed: (1) no active artifact loaded, and (2) copyTextToClipboard throws a clipboard permission/API error.

To fix this, the tool needs visibility into the copy outcome. One approach: refactor handleArtifactCopy (or add a variant for the WebMCP path) so it returns or throws on failure, letting the tool execute handler return { ok: false, error: 'no_active_artifact' } or { ok: false, error: 'clipboard_failed' } as appropriate. Alternatively, the AgentRenderWebMcpActions.copyActiveArtifact contract could be changed to return a boolean or a result object instead of Promise<void>.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/lib/webmcp/register-agent-render-tools.ts (1)

267-277: Avoid returning a partial object as a complete Record.

buildExampleHashByKey() skips missing hashes, but the return type promises every WebMcpExampleKey is present. Return Partial<Record<...>> or fail fast when a key has no matching hash.

♻️ Proposed type-safe return
 /** Maps example keys to sample link hashes (same order as `WEBMCP_EXAMPLE_KEYS`). */
-export function buildExampleHashByKey(sampleHashes: readonly string[]): Record<WebMcpExampleKey, string> {
-  const out = {} as Record<WebMcpExampleKey, string>;
+export function buildExampleHashByKey(sampleHashes: readonly string[]): Partial<Record<WebMcpExampleKey, string>> {
+  const out: Partial<Record<WebMcpExampleKey, string>> = {};
   for (let i = 0; i < WEBMCP_EXAMPLE_KEYS.length; i += 1) {
     const key = WEBMCP_EXAMPLE_KEYS[i];
     const h = sampleHashes[i];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/webmcp/register-agent-render-tools.ts` around lines 267 - 277, The
function buildExampleHashByKey currently returns a complete
Record<WebMcpExampleKey, string> while skipping undefined sampleHashes; change
its signature to return Partial<Record<WebMcpExampleKey, string>> OR
validate/throw when any WEBMCP_EXAMPLE_KEYS entry has no corresponding
sampleHashes[i]; locate buildExampleHashByKey and the loop using
WEBMCP_EXAMPLE_KEYS and either adjust the return type to
Partial<Record<WebMcpExampleKey,string>> and keep current behavior, or add a
check inside the loop (or after) to throw a descriptive error if any h is
undefined so the function can safely continue to return
Record<WebMcpExampleKey,string>.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/webmcp/register-agent-render-tools.ts`:
- Around line 176-183: The execute handler currently calls
read().selectArtifact(id) and returns { ok: true } without verifying the
artifactId exists; before mutating, check that the given artifactId is present
in the viewer's decoded envelope or artifact list (e.g. via a non-mutating
lookup such as read().getArtifactById(id) or checking
read().decodedEnvelope.artifacts/includes(id)), and if it is missing return {
ok: false, error: "unknown_artifactId" } instead of calling
read().selectArtifact; only call read().selectArtifact(id) and return success
when that lookup confirms the id exists.
- Around line 188-240: The three registered tools use an incorrect execute
signature and need to accept (input: object, client: ModelContextClient); update
each execute to async (input, client) => { await
client.requestUserInteraction(() => read().copyActiveArtifact()) ; return { ok:
true } } (and similarly for read().downloadActiveArtifact and
read().printActiveMarkdown) so the clipboard, download and print calls run
inside client.requestUserInteraction() and are properly user-activation gated
while preserving the existing return shape.

In `@tests/webmcp/register-agent-render-tools.test.ts`:
- Around line 8-18: The afterEach hook currently restores navigator.modelContext
and calls vi.restoreAllMocks(), but it does not unset globals stubbed with
vi.stubGlobal (e.g., isSecureContext), so add a call to vi.unstubAllGlobals()
inside the afterEach block (alongside the existing Object.defineProperty restore
and vi.restoreAllMocks()) to ensure any globals stubbed in tests are fully
unstubbed and do not leak into subsequent tests.

---

Nitpick comments:
In `@src/lib/webmcp/register-agent-render-tools.ts`:
- Around line 267-277: The function buildExampleHashByKey currently returns a
complete Record<WebMcpExampleKey, string> while skipping undefined sampleHashes;
change its signature to return Partial<Record<WebMcpExampleKey, string>> OR
validate/throw when any WEBMCP_EXAMPLE_KEYS entry has no corresponding
sampleHashes[i]; locate buildExampleHashByKey and the loop using
WEBMCP_EXAMPLE_KEYS and either adjust the return type to
Partial<Record<WebMcpExampleKey,string>> and keep current behavior, or add a
check inside the loop (or after) to throw a descriptive error if any h is
undefined so the function can safely continue to return
Record<WebMcpExampleKey,string>.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b3a4870-c852-492c-b9c4-df537597bc77

📥 Commits

Reviewing files that changed from the base of the PR and between a88b6ee and 970a863.

📒 Files selected for processing (5)
  • src/components/viewer-shell.tsx
  • src/lib/webmcp/register-agent-render-tools.ts
  • src/types/webmcp.d.ts
  • tests/webmcp/register-agent-render-tools.test.ts
  • tsconfig.json

Comment on lines +176 to +183
execute: async (input) => {
const id = (input as { artifactId?: string }).artifactId;
if (!id) {
return { ok: false, error: "missing_artifactId" };
}
read().selectArtifact(id);
return { ok: true, artifactId: id };
},

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.

⚠️ Potential issue | 🟠 Major

Validate artifactId before mutating the fragment.

This tool accepts arbitrary agent input but returns { ok: true } even when the ID is not in the decoded envelope. With the current viewer handler, that can rewrite the URL with an invalid activeArtifactId.

🛡️ Proposed validation
       execute: async (input) => {
         const id = (input as { artifactId?: string }).artifactId;
         if (!id) {
           return { ok: false, error: "missing_artifactId" };
         }
-        read().selectArtifact(id);
+        const actions = read();
+        const state = actions.getViewerState();
+        if (!state.decodeOk) {
+          return { ok: false, error: "no_decoded_envelope" };
+        }
+        if (!state.artifactIds.includes(id)) {
+          return { ok: false, error: "unknown_artifact_id" };
+        }
+        actions.selectArtifact(id);
         return { ok: true, artifactId: id };
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
execute: async (input) => {
const id = (input as { artifactId?: string }).artifactId;
if (!id) {
return { ok: false, error: "missing_artifactId" };
}
read().selectArtifact(id);
return { ok: true, artifactId: id };
},
execute: async (input) => {
const id = (input as { artifactId?: string }).artifactId;
if (!id) {
return { ok: false, error: "missing_artifactId" };
}
const actions = read();
const state = actions.getViewerState();
if (!state.decodeOk) {
return { ok: false, error: "no_decoded_envelope" };
}
if (!state.artifactIds.includes(id)) {
return { ok: false, error: "unknown_artifact_id" };
}
actions.selectArtifact(id);
return { ok: true, artifactId: id };
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/webmcp/register-agent-render-tools.ts` around lines 176 - 183, The
execute handler currently calls read().selectArtifact(id) and returns { ok: true
} without verifying the artifactId exists; before mutating, check that the given
artifactId is present in the viewer's decoded envelope or artifact list (e.g.
via a non-mutating lookup such as read().getArtifactById(id) or checking
read().decodedEnvelope.artifacts/includes(id)), and if it is missing return {
ok: false, error: "unknown_artifactId" } instead of calling
read().selectArtifact; only call read().selectArtifact(id) and return success
when that lookup confirms the id exists.

Comment on lines +188 to +240
modelContext.registerTool(
{
name: "agent_render.copy_active_artifact",
title: "Copy active artifact",
description: "Copies the current artifact body (text) to the clipboard, same as the Copy button.",
inputSchema: {
type: "object",
properties: {},
additionalProperties: false,
},
execute: async () => {
await read().copyActiveArtifact();
return { ok: true };
},
},
{ signal },
);

modelContext.registerTool(
{
name: "agent_render.download_active_artifact",
title: "Download active artifact",
description: "Downloads the active artifact as a file, same as the Download button.",
inputSchema: {
type: "object",
properties: {},
additionalProperties: false,
},
execute: async () => {
read().downloadActiveArtifact();
return { ok: true };
},
},
{ signal },
);

modelContext.registerTool(
{
name: "agent_render.print_markdown_artifact",
title: "Print markdown",
description:
"If the active artifact is markdown, opens the browser print dialog for print-to-PDF. No-op for other kinds.",
inputSchema: {
type: "object",
properties: {},
additionalProperties: false,
},
execute: async () => {
read().printActiveMarkdown();
return { ok: true };
},
},
{ signal },

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

WebMCP navigator.modelContext registerTool requestUserInteraction clipboard download print user activation

💡 Result:

WebMCP (Web Model Context Protocol) is a W3C draft browser API that extends navigator with navigator.modelContext, allowing websites to register tools callable by AI agents. Key methods include navigator.modelContext.registerTool to register tools with name, description, inputSchema, and execute callback. Tools receive input and a ModelContextClient during execution. requestUserInteraction is a method on ModelContextClient: client.requestUserInteraction(callback) pauses tool execution to request user input via an async callback (e.g., confirm dialog), resolving with the result for human-in-the-loop flows like confirmations before destructive actions. clipboard, download, and print relate to standard Navigator APIs (navigator.clipboard for Clipboard API; downloads/print via a[download] or window.print) and navigator.userActivation (UserActivation state for checking user gestures, e.g., navigator.userActivation.isActive). These are separate from WebMCP but relevant for tools needing permissions: user activation gates many browser features (autoplay, clipboard writes), so WebMCP tools performing such actions must check/await user activation, often via requestUserInteraction. No direct integration found between WebMCP and these specific APIs; tools can use them internally if activation allows. Spec is evolving (Chrome 146+ preview), requires secure contexts (HTTPS).

Citations:


🏁 Script executed:

# First, check the file structure and current implementation
head -20 src/lib/webmcp/register-agent-render-tools.ts

Repository: baanish/agent-render

Length of output: 611


🏁 Script executed:

# Check if requestUserInteraction is used elsewhere in the codebase
rg "requestUserInteraction" --type ts --type tsx -B 2 -A 2

Repository: baanish/agent-render

Length of output: 91


🏁 Script executed:

# Search for other tool execute callbacks to see the current pattern
rg "execute:\s*async" src/lib/webmcp/ --type ts --type tsx -A 3

Repository: baanish/agent-render

Length of output: 91


🏁 Script executed:

# Check the actual implementation of the methods being called
rg "copyActiveArtifact|downloadActiveArtifact|printActiveMarkdown" --type ts --type tsx -B 2 -A 5

Repository: baanish/agent-render

Length of output: 91


🏁 Script executed:

# Look for any WebMCP-related imports or documentation
rg "registerTool|modelContext" src/lib/webmcp/ --type ts --type tsx -B 1 -A 1 | head -40

Repository: baanish/agent-render

Length of output: 91


🏁 Script executed:

# Find TypeScript files in the webmcp directory
fd "\.ts$" src/lib/webmcp/

Repository: baanish/agent-render

Length of output: 109


🏁 Script executed:

# Search for requestUserInteraction usage
rg "requestUserInteraction" -A 3 -B 2

Repository: baanish/agent-render

Length of output: 400


🏁 Script executed:

# Read the actual file to see the tool registrations
wc -l src/lib/webmcp/register-agent-render-tools.ts

Repository: baanish/agent-render

Length of output: 113


🏁 Script executed:

# Get the specific lines mentioned in the review (188-240)
sed -n '180,245p' src/lib/webmcp/register-agent-render-tools.ts

Repository: baanish/agent-render

Length of output: 1727


🏁 Script executed:

# Search for other tool registrations to see the callback pattern
rg "execute:" src/lib/webmcp/register-agent-render-tools.ts -A 4 -B 2

Repository: baanish/agent-render

Length of output: 1691


🏁 Script executed:

# Search for the implementation of copyActiveArtifact
rg "copyActiveArtifact" -B 3 -A 10

Repository: baanish/agent-render

Length of output: 4259


🏁 Script executed:

# Search for downloadActiveArtifact implementation
rg "downloadActiveArtifact" -B 3 -A 10

Repository: baanish/agent-render

Length of output: 4282


🏁 Script executed:

# Search for printActiveMarkdown implementation
rg "printActiveMarkdown" -B 3 -A 10

Repository: baanish/agent-render

Length of output: 4191


🏁 Script executed:

# Check the file structure to find where read() comes from
head -50 src/lib/webmcp/register-agent-render-tools.ts

Repository: baanish/agent-render

Length of output: 1673


🏁 Script executed:

# Find and read the handler implementations
rg "handleArtifactCopy|handleArtifactDownload|handleMarkdownPrint" src/components/viewer-shell.tsx -B 2 -A 15

Repository: baanish/agent-render

Length of output: 4359


🏁 Script executed:

# Find the copyTextToClipboard implementation to confirm it uses Clipboard API
rg "copyTextToClipboard" -B 2 -A 8

Repository: baanish/agent-render

Length of output: 3940


Update tool callbacks to accept the client parameter and wrap user-interaction operations with requestUserInteraction.

The execute callbacks have an incorrect signature. Per the WebMCP spec (defined in src/types/webmcp.d.ts), callbacks must accept (input: object, client: ModelContextClient). The three operations—clipboard write, download, and print—use browser APIs that require user activation and will fail silently without proper gating. Wrap them with client.requestUserInteraction() as the host provides the activation gate.

Required changes
-      execute: async () => {
-        await read().copyActiveArtifact();
+      execute: async (_input, client) => {
+        await client.requestUserInteraction(async () => {
+          await read().copyActiveArtifact();
+        });
         return { ok: true };
       },

-      execute: async () => {
-        read().downloadActiveArtifact();
+      execute: async (_input, client) => {
+        await client.requestUserInteraction(async () => {
+          read().downloadActiveArtifact();
+        });
         return { ok: true };
       },

-      execute: async () => {
-        read().printActiveMarkdown();
+      execute: async (_input, client) => {
+        await client.requestUserInteraction(async () => {
+          read().printActiveMarkdown();
+        });
         return { ok: true };
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/webmcp/register-agent-render-tools.ts` around lines 188 - 240, The
three registered tools use an incorrect execute signature and need to accept
(input: object, client: ModelContextClient); update each execute to async
(input, client) => { await client.requestUserInteraction(() =>
read().copyActiveArtifact()) ; return { ok: true } } (and similarly for
read().downloadActiveArtifact and read().printActiveMarkdown) so the clipboard,
download and print calls run inside client.requestUserInteraction() and are
properly user-activation gated while preserving the existing return shape.

Comment on lines +8 to +18
afterEach(() => {
Object.defineProperty(navigator, "modelContext", {
configurable: true,
value: originalModelContext,
});
vi.restoreAllMocks();
});

it("registers tools when modelContext.registerTool exists", () => {
vi.stubGlobal("isSecureContext", true);

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether Vitest globals stubbed with vi.stubGlobal are automatically restored.
# Expect: either test config has unstubGlobals: true, or tests call vi.unstubAllGlobals().
rg -n -C3 'unstubGlobals|stubGlobal\(|unstubAllGlobals|restoreAllMocks' --glob '!node_modules'

Repository: baanish/agent-render

Length of output: 1773


🏁 Script executed:

fd -name 'vitest.config.*' -o -name 'vite.config.*'

Repository: baanish/agent-render

Length of output: 294


🏁 Script executed:

cat -n tests/webmcp/register-agent-render-tools.test.ts | head -40

Repository: baanish/agent-render

Length of output: 1691


🏁 Script executed:

fd 'vitest.config' -o -type f -name 'vite.config*'

Repository: baanish/agent-render

Length of output: 180


🏁 Script executed:

find . -maxdepth 2 -name 'vitest.config.*' -o -name 'vite.config.*' 2>/dev/null

Repository: baanish/agent-render

Length of output: 82


🏁 Script executed:

cat vitest.config.ts

Repository: baanish/agent-render

Length of output: 504


🏁 Script executed:

rg 'unstubAllGlobals|unstubGlobals' --glob '!node_modules'

Repository: baanish/agent-render

Length of output: 46


🏁 Script executed:

rg '^\s*it\(' tests/webmcp/register-agent-render-tools.test.ts

Repository: baanish/agent-render

Length of output: 189


🏁 Script executed:

cat -n tests/webmcp/register-agent-render-tools.test.ts | sed -n '40,80p'

Repository: baanish/agent-render

Length of output: 1157


Add vi.unstubAllGlobals() to the afterEach hook.

The isSecureContext stub from the first test will leak into the second test. Vitest does not automatically restore stubbed globals, so without explicit unstubbing, subsequent tests inherit previous stub state and experience unintended side effects.

🧪 Test isolation fix
  afterEach(() => {
    Object.defineProperty(navigator, "modelContext", {
      configurable: true,
      value: originalModelContext,
    });
    vi.restoreAllMocks();
+   vi.unstubAllGlobals();
  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
afterEach(() => {
Object.defineProperty(navigator, "modelContext", {
configurable: true,
value: originalModelContext,
});
vi.restoreAllMocks();
});
it("registers tools when modelContext.registerTool exists", () => {
vi.stubGlobal("isSecureContext", true);
afterEach(() => {
Object.defineProperty(navigator, "modelContext", {
configurable: true,
value: originalModelContext,
});
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
it("registers tools when modelContext.registerTool exists", () => {
vi.stubGlobal("isSecureContext", true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/webmcp/register-agent-render-tools.test.ts` around lines 8 - 18, The
afterEach hook currently restores navigator.modelContext and calls
vi.restoreAllMocks(), but it does not unset globals stubbed with vi.stubGlobal
(e.g., isSecureContext), so add a call to vi.unstubAllGlobals() inside the
afterEach block (alongside the existing Object.defineProperty restore and
vi.restoreAllMocks()) to ensure any globals stubbed in tests are fully unstubbed
and do not leak into subsequent tests.

@baanish

baanish commented May 8, 2026

Copy link
Copy Markdown
Owner Author

Closing after triage: the WebMCP surface is speculative and expands product scope beyond static artifact viewing.

@baanish baanish closed this May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants