/**
* mcpService.js
*
* Handles registration of Model Context Protocol (MCP) tools with the browser.
*
* Key design decisions:
* 1. `navigator.modelContext` is injected by the WebMCP browser extension as a
* content script. It is NOT available synchronously at module load time.
* We must poll for it and queue tools registered before it's ready.
* 2. The spec is still in flux. We pass both `execute` (2025+ spec) and `callback`
* (older Chrome builds) so it works regardless of which draft is active.
*/
class McpService {
constructor() {
this._ready = false; // true once navigator.modelContext is confirmed available
this._pendingTools = []; // tools queued before context was available
this._registeredTools = new Map(); // name → tool definition
// Start polling for the injected context
this._waitForContext();
}
// ── Dynamic check (always reads live from window) ─────────────────────────
get isSupported() {
return typeof window !== 'undefined' && !!window.navigator?.modelContext;
}
// ── Poll until navigator.modelContext appears (up to 8 seconds) ───────────
_waitForContext() {
if (this.isSupported) {
// Already available (e.g. flag built into browser, not injected)
this._onContextReady();
return;
}
let attempts = 0;
const MAX_ATTEMPTS = 80; // 80 × 100ms = 8s
const poll = setInterval(() => {
attempts++;
if (this.isSupported) {
clearInterval(poll);
this._onContextReady();
} else if (attempts >= MAX_ATTEMPTS) {
clearInterval(poll);
console.warn(
'[MCP] navigator.modelContext not detected after 8s.\n' +
' → Ensure the WebMCP extension is installed OR Chrome flag is enabled:\n' +
' chrome://flags/#enable-webmcp-testing'
);
}
}, 100);
}
// ── Called once modelContext becomes available ────────────────────────────
_onContextReady() {
this._ready = true;
console.log(
`%c[MCP] ✓ navigator.modelContext detected — registering ${this._pendingTools.length} queued tools`,
'color: #6366f1; font-weight: bold;'
);
// Flush the queue
const pending = [...this._pendingTools];
this._pendingTools = [];
for (const tool of pending) {
this._doRegister(tool);
}
}
// ── Internal registration (only call when _ready = true) ─────────────────
_doRegister(tool) {
try {
// Idempotent: if the tool is already registered, unregister first
if (this._registeredTools.has(tool.name)) {
try {
if (window.navigator.modelContext.unregisterTool) {
window.navigator.modelContext.unregisterTool(tool.name);
}
} catch (_) { /* ignore */ }
this._registeredTools.delete(tool.name);
}
// Normalize handler function — spec has used multiple property names
const normalized = { ...tool };
const fn = tool.execute || tool.callback || tool.handler;
if (fn) {
normalized.execute = fn; // MCP spec 2025-Q2+
normalized.callback = fn; // older Chrome/extension builds
}
window.navigator.modelContext.registerTool(normalized);
this._registeredTools.set(tool.name, tool);
console.log(`%c[MCP] ✓ ${tool.name}`, 'color: #22c55e; font-weight: bold;');
} catch (err) {
console.error(`[MCP] ✗ Failed to register '${tool.name}':`, err);
}
}
// ── Public API ────────────────────────────────────────────────────────────
/**
* Register a tool. If modelContext isn't ready yet, the tool is queued
* and will be registered automatically when the context becomes available.
*/
registerTool(tool) {
if (this._ready) {
this._doRegister(tool);
} else {
// Deduplicate: replace if already queued with same name
this._pendingTools = this._pendingTools.filter(t => t.name !== tool.name);
this._pendingTools.push(tool);
console.log(`[MCP] ⏳ Queued: ${tool.name}`);
}
}
/**
* Unregister a tool by name.
*/
unregisterTool(toolName) {
// Always remove from pending queue
this._pendingTools = this._pendingTools.filter(t => t.name !== toolName);
if (!this.isSupported) return;
try {
if (window.navigator.modelContext.unregisterTool) {
window.navigator.modelContext.unregisterTool(toolName);
}
this._registeredTools.delete(toolName);
console.log(`[MCP] ↩ Unregistered: ${toolName}`);
} catch (err) {
console.error(`[MCP] Failed to unregister '${toolName}':`, err);
}
}
/** Returns true only after modelContext was confirmed available. */
isActive() {
return this._ready;
}
/** Debug helper — list all currently registered tool names. */
listRegistered() {
return [...this._registeredTools.keys()];
}
}
const mcpService = new McpService();
export default mcpService;