From 6e006dd4724f774a9d7cc4869b6958703e2a8302 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Tue, 2 Jun 2026 04:03:10 -0700 Subject: [PATCH] [eric] browser: detect a page's declared WebMCP tools (tier 1) --- backend/apps/agents/browser/browser_schema.py | 12 ++++++ frontend/src/shared/browserCommandHandler.ts | 41 ++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/backend/apps/agents/browser/browser_schema.py b/backend/apps/agents/browser/browser_schema.py index 4d4453c8..5243fd0c 100644 --- a/backend/apps/agents/browser/browser_schema.py +++ b/backend/apps/agents/browser/browser_schema.py @@ -284,6 +284,17 @@ BROWSER_TOOLS_SCHEMA = [ "required": [], }, }, + { + "name": "BrowserDetectWebMCP", + "description": ( + "Check whether the current page declares its own WebMCP tools " + "(navigator.modelContext). If a site exposes tools this way, they " + "are the fastest, most reliable path; prefer them over scraping the " + "UI. Most sites don't support this yet, so it usually reports none; " + "in that case just use the normal browser tools. Read-only." + ), + "input_schema": {"type": "object", "properties": {}, "required": []}, + }, { "name": "RequestHumanIntervention", "description": ( @@ -329,6 +340,7 @@ ACTION_MAP = { "BrowserListInteractives": "list_interactives", "BrowserClickIndex": "click_index", "BrowserBatch": "batch", + "BrowserDetectWebMCP": "detect_webmcp", } SYSTEM_PROMPT = ( diff --git a/frontend/src/shared/browserCommandHandler.ts b/frontend/src/shared/browserCommandHandler.ts index c3d7f01a..02ffb1ef 100644 --- a/frontend/src/shared/browserCommandHandler.ts +++ b/frontend/src/shared/browserCommandHandler.ts @@ -5,7 +5,7 @@ import { rankAndCapInteractives, type RankItem } from './interactiveRanking'; let initialized = false; -export type BrowserAction = 'screenshot' | 'get_text' | 'navigate' | 'click' | 'type' | 'evaluate' | 'get_elements' | 'scroll' | 'wait' | 'press_key' | 'list_interactives' | 'click_index' | 'batch'; +export type BrowserAction = 'screenshot' | 'get_text' | 'navigate' | 'click' | 'type' | 'evaluate' | 'get_elements' | 'scroll' | 'wait' | 'press_key' | 'list_interactives' | 'click_index' | 'batch' | 'detect_webmcp'; export interface BrowserActivity { action: BrowserAction; @@ -648,6 +648,42 @@ async function handleGetElements(wv: BrowserWebview, params: Record } } +// Tier 1: detect a site's declared WebMCP tools (navigator.modelContext). When a +// site exposes its own tools the agent can prefer them over scraping the UI. The +// API is a Chrome 149 origin-trial standard; this Electron's Chromium predates it +// so real pages return "not present" today, this is forward-compatible probing, +// also covers the MCP-B convention (getRegisteredTools/listTools/tools array). +async function handleDetectWebMCP(wv: BrowserWebview): Promise> { + const code = `(() => { + const mc = navigator.modelContext; + if (!mc) return { present: false, tools: [] }; + let raw = []; + try { + if (typeof mc.getRegisteredTools === 'function') raw = mc.getRegisteredTools() || []; + else if (typeof mc.listTools === 'function') raw = mc.listTools() || []; + else if (Array.isArray(mc.tools)) raw = mc.tools; + } catch (e) {} + const tools = (raw || []).map(t => ({ + name: String((t && t.name) || ''), + description: String((t && t.description) || '').slice(0, 200), + })).filter(t => t.name); + return { present: true, tools }; + })()`; + try { + const r = await wv.executeJavaScript(code); + if (!r || !r.present) { + return { text: 'No WebMCP on this page (navigator.modelContext not present). Use the normal browser tools.', url: wv.getURL() }; + } + if (!r.tools.length) { + return { text: 'WebMCP is present but exposes no callable tools. Use the normal browser tools.', url: wv.getURL() }; + } + const lines = r.tools.map((t: any) => `- ${t.name}: ${t.description}`).join('\n'); + return { text: `WebMCP tools declared by this page:\n${lines}`, tools: r.tools, url: wv.getURL() }; + } catch (err: any) { + return { error: `WebMCP detection failed: ${err?.message || String(err)}` }; + } +} + async function handleEvaluate(wv: BrowserWebview, params: Record): Promise> { const expression = params.expression as string; if (!expression) return { error: 'expression parameter is required' }; @@ -746,6 +782,9 @@ async function handleBrowserCommand(data: Record) { case 'batch': result = await handleBatch(wv, params); break; + case 'detect_webmcp': + result = await handleDetectWebMCP(wv); + break; default: result = { error: `Unknown browser action: ${action}` }; }