-
Notifications
You must be signed in to change notification settings - Fork 0
feat: WebMCP tools for the agent-render viewer #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,277 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { RefObject } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Stable keys for built-in example fragments (order matches `sampleLinks` in examples). */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export const WEBMCP_EXAMPLE_KEYS = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "maintainer-kickoff", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "viewer-bootstrap", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "phase-1-sample-diff", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "data-export-preview", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "arx-showcase", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "malformed-manifest", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ] as const; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export type WebMcpExampleKey = (typeof WEBMCP_EXAMPLE_KEYS)[number]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export type WebMcpViewerState = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| hasFragment: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fragmentLength: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| decodeOk: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| parseMessage?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| envelopeTitle?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| codec?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| artifactIds: string[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| activeArtifactId?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| activeArtifactKind?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| activeArtifactTitle?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| exampleKeys: readonly string[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| exampleTitles: readonly string[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Latest imperative actions for WebMCP tool callbacks. Updated each render so tools always | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * invoke current viewer behavior without re-registering. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export type AgentRenderWebMcpActions = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| getViewerState: () => WebMcpViewerState; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| loadSampleByKey: (key: string) => boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| loadSampleByTitle: (substring: string) => boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| selectArtifact: (artifactId: string) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| copyActiveArtifact: () => Promise<void>; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| downloadActiveArtifact: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| printActiveMarkdown: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| goHome: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Registers agent-render tools on `navigator.modelContext` when the WebMCP API is present. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Uses one `AbortController` so all tools unregister together on cleanup. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param actionsRef Ref updated each render with fresh callbacks and `getViewerState`. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns Cleanup to run on unmount (aborts registration). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export function registerAgentRenderWebMcpTools(actionsRef: RefObject<AgentRenderWebMcpActions | null>): () => void { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window === "undefined" || !window.isSecureContext) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const modelContext = navigator.modelContext; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!modelContext || typeof modelContext.registerTool !== "function") { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const abort = new AbortController(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { signal } = abort; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const read = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const current = actionsRef.current; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!current) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("agent-render WebMCP actions are not initialized"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return current; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| modelContext.registerTool( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name: "agent_render.get_viewer_state", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Get viewer state", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "Returns the current agent-render URL fragment status: decode result, envelope summary, active artifact, and available example keys. Read-only.", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| inputSchema: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type: "object", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| properties: {}, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| additionalProperties: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| annotations: { readOnlyHint: true }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| execute: async () => read().getViewerState(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { signal }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| modelContext.registerTool( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name: "agent_render.list_examples", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| title: "List example fragments", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "Returns the stable example keys and titles for built-in sample fragments users can load into the viewer.", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| inputSchema: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type: "object", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| properties: {}, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| additionalProperties: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| annotations: { readOnlyHint: true }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| execute: async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const state = read().getViewerState(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { exampleKeys: [...state.exampleKeys], titles: [...state.exampleTitles] }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { signal }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| modelContext.registerTool( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name: "agent_render.load_example_fragment", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Load example fragment", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "Navigates the viewer to a built-in sample by stable example key (preferred), by title substring, or by index 0–5. Updates the URL hash.", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| inputSchema: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type: "object", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| properties: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| exampleKey: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type: "string", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "Stable key: maintainer-kickoff, viewer-bootstrap, phase-1-sample-diff, data-export-preview, arx-showcase, malformed-manifest", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| enum: [...WEBMCP_EXAMPLE_KEYS], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| titleContains: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type: "string", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "Case-insensitive substring match against sample titles", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| index: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type: "integer", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| minimum: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| maximum: 5, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "Zero-based index into the sample list (same order as the homepage)", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| additionalProperties: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| execute: async (input) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const obj = input as Record<string, unknown>; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof obj.exampleKey === "string") { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const ok = read().loadSampleByKey(obj.exampleKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return ok ? { ok: true, method: "exampleKey", exampleKey: obj.exampleKey } : { ok: false, error: "unknown_example_key" }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof obj.titleContains === "string" && obj.titleContains.trim()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const ok = read().loadSampleByTitle(obj.titleContains.trim()); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return ok ? { ok: true, method: "titleContains" } : { ok: false, error: "no_matching_title" }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof obj.index === "number" && Number.isInteger(obj.index)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const key = WEBMCP_EXAMPLE_KEYS[obj.index]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!key) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { ok: false, error: "bad_index" }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const ok = read().loadSampleByKey(key); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return ok ? { ok: true, method: "index", exampleKey: key } : { ok: false, error: "bad_index" }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { ok: false, error: "provide_exampleKey_titleContains_or_index" }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { signal }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| modelContext.registerTool( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name: "agent_render.select_artifact", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Select artifact", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "When a multi-artifact bundle is loaded, switches the active artifact by id and rewrites the URL fragment. No-op if the id is already active or the bundle is missing.", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| inputSchema: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type: "object", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| properties: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| artifactId: { type: "string", minLength: 1, description: "Artifact id from the decoded envelope" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| required: ["artifactId"], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| additionalProperties: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+176
to
+183
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate This tool accepts arbitrary agent input but returns 🛡️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { signal }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+198
to
+201
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 The Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { 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 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+188
to
+240
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 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.tsRepository: 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 2Repository: 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 3Repository: 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 5Repository: 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 -40Repository: 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 2Repository: 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.tsRepository: 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.tsRepository: 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 2Repository: baanish/agent-render Length of output: 1691 🏁 Script executed: # Search for the implementation of copyActiveArtifact
rg "copyActiveArtifact" -B 3 -A 10Repository: baanish/agent-render Length of output: 4259 🏁 Script executed: # Search for downloadActiveArtifact implementation
rg "downloadActiveArtifact" -B 3 -A 10Repository: baanish/agent-render Length of output: 4282 🏁 Script executed: # Search for printActiveMarkdown implementation
rg "printActiveMarkdown" -B 3 -A 10Repository: 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.tsRepository: 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 15Repository: baanish/agent-render Length of output: 4359 🏁 Script executed: # Find the copyTextToClipboard implementation to confirm it uses Clipboard API
rg "copyTextToClipboard" -B 2 -A 8Repository: baanish/agent-render Length of output: 3940 Update tool callbacks to accept the The 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 |
||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| modelContext.registerTool( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name: "agent_render.clear_fragment", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Clear fragment / home", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "Clears the URL hash and returns to the empty state and link creator, like the site logo.", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| inputSchema: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type: "object", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| properties: {}, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| additionalProperties: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| execute: async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| read().goHome(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { ok: true }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { signal }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| abort.abort(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** 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>; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: Return type incorrect - |
||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < WEBMCP_EXAMPLE_KEYS.length; i += 1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const key = WEBMCP_EXAMPLE_KEYS[i]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const h = sampleHashes[i]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (h !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| out[key] = h; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return out; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agent_render.select_artifactalways returns{ ok: true }for any non-emptyartifactId, but it never verifies that the ID exists in the current bundle before callingselectArtifact. With a typo or stale ID, the viewer path normalizesactiveArtifactIdto 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 👍 / 👎.