ui: Intelletto: Register tools with Chrome's WebMCP API Registers Intelletto tools via document.modelContext.registerTool so external agents that can maek use of WebMCP can control the UI more effectively.
diff --git a/ui/src/plugins/dev.perfetto.Intelletto/index.ts b/ui/src/plugins/dev.perfetto.Intelletto/index.ts index 1d346fd..3d1d5cf 100644 --- a/ui/src/plugins/dev.perfetto.Intelletto/index.ts +++ b/ui/src/plugins/dev.perfetto.Intelletto/index.ts
@@ -31,6 +31,7 @@ import {registerCoreTools} from './core_tools'; import {registerTimelineTools} from './timeline_tools'; import {ToolRegistry} from './tools'; +import {registerWebMCPTools} from './webmcp'; const SIDE_PANEL_URI = 'dev.perfetto.Intelletto#Chat'; @@ -125,6 +126,12 @@ // Pull in the Data Explorer graph tools and selected-node context. registerDataExplorerTools(this.tools, this.context, trace); + + // Register tools with Chrome's experimental WebMCP API so external agents + // (e.g. Chrome's built-in AI agent, MCP inspector extensions) can discover + // and call them. No-ops gracefully when the API is not available. + const webmcpController = registerWebMCPTools(this.tools); + trace.trash.defer(() => webmcpController?.abort()); } }
diff --git a/ui/src/plugins/dev.perfetto.Intelletto/tools.ts b/ui/src/plugins/dev.perfetto.Intelletto/tools.ts index 79ad0b6..e22bf25 100644 --- a/ui/src/plugins/dev.perfetto.Intelletto/tools.ts +++ b/ui/src/plugins/dev.perfetto.Intelletto/tools.ts
@@ -36,6 +36,15 @@ */ export class ToolRegistry { private readonly tools = new Map<string, ToolDef>(); + private readonly toolRegisteredListeners = new Set<(tool: ToolDef) => void>(); + + /** Listen for tools added to this registry. */ + addToolRegisteredListener(listener: (tool: ToolDef) => void): Disposable { + this.toolRegisteredListeners.add(listener); + return { + [Symbol.dispose]: () => this.toolRegisteredListeners.delete(listener), + }; + } /** Register a tool. Throws if the name is already registered. */ registerTool<S extends ZodRawShape>(opts: ToolRegistration<S>): void { @@ -43,13 +52,17 @@ throw new Error(`Tool "${opts.name}" already registered`); } const inputSchema = z.object(opts.shape); - this.tools.set(opts.name, { + const tool: ToolDef = { name: opts.name, description: opts.description, inputSchema: inputSchema as unknown as ZodObject<ZodRawShape>, mutating: opts.mutating ?? false, callback: opts.callback as (args: unknown) => Promise<string>, - }); + }; + this.tools.set(opts.name, tool); + for (const listener of this.toolRegisteredListeners) { + listener(tool); + } } /** Look up a tool by name, or undefined if not registered. */
diff --git a/ui/src/plugins/dev.perfetto.Intelletto/webmcp.ts b/ui/src/plugins/dev.perfetto.Intelletto/webmcp.ts new file mode 100644 index 0000000..688921b --- /dev/null +++ b/ui/src/plugins/dev.perfetto.Intelletto/webmcp.ts
@@ -0,0 +1,122 @@ +// Copyright (C) 2026 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Registers Intelletto's tools with Chrome's experimental WebMCP API +// (document.modelContext.registerTool). Instead of duplicating definitions, +// it reads the already-populated ToolRegistry and exposes each tool. +// Gracefully no-ops when the API is not available. + +import {z} from 'zod'; +import type {ToolDef, ToolRegistry} from './tools'; + +// --- WebMCP type declarations --- +// Chrome's experimental API; not in lib.dom.d.ts yet. + +interface WebMCPToolDefinition { + readonly name: string; + readonly description: string; + readonly inputSchema: Record<string, unknown>; + readonly execute: (args: Record<string, unknown>) => Promise<string>; + readonly annotations?: { + readonly readOnlyHint?: boolean; + readonly untrustedContentHint?: boolean; + }; +} + +interface WebMCPRegisterOptions { + readonly signal?: AbortSignal; + readonly exposedTo?: string[]; +} + +interface WebMCPModelContext { + registerTool( + tool: WebMCPToolDefinition, + options?: WebMCPRegisterOptions, + ): Promise<void>; +} + +interface DocumentWithWebMCP extends Document { + readonly modelContext: WebMCPModelContext; +} + +// --- Helpers --- + +function hasWebMCP(document: Document): document is DocumentWithWebMCP { + return 'modelContext' in document && document.modelContext !== undefined; +} + +/** + * Convert a Zod schema to a JSON Schema object suitable for WebMCP's + * inputSchema. + */ +function zodToJSONSchema(schema: z.ZodType<unknown>): Record<string, unknown> { + return {...z.toJSONSchema(schema)}; +} + +// --- Registration --- + +/** + * Register all Intelletto tools with WebMCP by reading the shared registry. + * Returns an AbortController whose .abort() method unregisters all tools. + * No-ops if WebMCP is not available. + */ +export function registerWebMCPTools( + registry: ToolRegistry, +): AbortController | null { + if (!hasWebMCP(document)) { + return null; + } + + const controller = new AbortController(); + const mc = document.modelContext; + + for (const tool of registry.list()) { + registerTool(mc, tool, controller.signal); + } + + const listener = registry.addToolRegisteredListener((tool) => { + registerTool(mc, tool, controller.signal); + }); + + controller.signal.addEventListener( + 'abort', + () => listener[Symbol.dispose](), + {once: true}, + ); + + return controller; +} + +/** Register a single ToolDef with WebMCP. */ +async function registerTool( + mc: WebMCPModelContext, + tool: ToolDef, + signal: AbortSignal, +): Promise<void> { + try { + return await mc.registerTool( + { + name: `perfetto_${tool.name}`, + description: tool.description, + inputSchema: zodToJSONSchema(tool.inputSchema), + execute: async (rawArgs: Record<string, unknown>): Promise<string> => { + // WebMCP passes raw args; delegate to the original callback. + return await tool.callback(rawArgs); + }, + annotations: {readOnlyHint: !tool.mutating}, + }, + {signal}, + ); + } catch {} +}