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 {}
+}