Files
backnotprop__plannotator/packages/ui/webmcp/preference.ts
Michael Ramos 6903d7a3dd feat(webmcp): expose plan review and annotate as WebMCP tools for browser agents (#1393)
Phase 1 of WebMCP support: a zero-dependency, feature-detected engine in packages/ui/webmcp plus a read-and-comment tool catalog for plan review and annotate (read_document, add_comments, update_comment, remove_comments, reveal, nudge_user, list_documents). No decision tools; the human approves. Zero footprint in browsers without document.modelContext (DOM, network, console, timers, and cookies identical to main), idle until called where the API exists, and never registered inside the annotate iframes. Adds an optional inReplyTo field on annotations for threaded replies. Client-only; no server changes.

AI-assisted (Claude) under maintainer direction.
2026-08-26 14:39:37 -07:00

51 lines
2.0 KiB
TypeScript

/**
* The user's WebMCP opt-out, kept OUT of the settings registry on purpose.
*
* `configStore.ensureLoaded` seeds every registry entry's default into a
* cookie on first settings access, which would write a cookie for every
* user of every surface (plan, annotate, code review, the guides.show
* viewer) whether or not their browser has `document.modelContext`. That is
* a footprint. This module is idle at its default: nothing is read or
* written until the Settings row (shown only when the API exists) is
* toggled, and turning the tools back on removes the cookie instead of
* writing `true`.
*
* Cookie: `plannotator-webmcp-tools=false` while opted out; absent otherwise.
*/
import { useSyncExternalStore } from 'react';
import { storage } from '../utils/storage';
export const WEBMCP_TOOLS_COOKIE = 'plannotator-webmcp-tools';
const listeners = new Set<() => void>();
/** Resolved lazily on first read; write-through afterwards so the snapshot is stable for useSyncExternalStore. */
let cached: boolean | null = null;
/** Whether the tools are enabled for this browser (default true; only an explicit opt-out disables). */
export function getWebMcpToolsEnabled(): boolean {
if (cached === null) cached = storage.getItem(WEBMCP_TOOLS_COOKIE) !== 'false';
return cached;
}
/** Persist the opt-out. `true` removes the cookie so the default stays cookie-free. */
export function setWebMcpToolsEnabled(enabled: boolean): void {
if (enabled) {
if (storage.getItem(WEBMCP_TOOLS_COOKIE) !== null) storage.removeItem(WEBMCP_TOOLS_COOKIE);
} else {
storage.setItem(WEBMCP_TOOLS_COOKIE, 'false');
}
cached = enabled;
for (const listener of listeners) listener();
}
export function subscribeWebMcpToolsEnabled(listener: () => void): () => void {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
export function useWebMcpToolsEnabled(): boolean {
return useSyncExternalStore(subscribeWebMcpToolsEnabled, getWebMcpToolsEnabled, getWebMcpToolsEnabled);
}