@r0hitsharma/webmcp 0.12.0-rohit-fork-ci.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @r0hitsharma/webmcp
2
+
3
+ A stable React wrapper over [WebMCP](https://github.com/webmcp-org) (`@mcp-b/global`) for registering UI tools that a connected agent harness can call. It exposes a fixed interface so the fast-moving `@mcp-b/*` packages can churn behind a single seam.
4
+
5
+ Tools are registered into `document.modelContext`. A tool registered here runs in the consumer's own authenticated browser session, so it reuses the app's existing auth and APIs rather than requiring separate server credentials.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @r0hitsharma/webmcp react
11
+ ```
12
+
13
+ `react` is a peer dependency (>= 19).
14
+
15
+ ## Features
16
+
17
+ - Schema-first tool definitions (`defineTool`)
18
+ - StrictMode-safe registration that mounts/unmounts cleanly
19
+ - A React provider that initializes the WebMCP polyfill and a tool registry
20
+ - Hooks to register tools, observe the registry, and contribute view state
21
+ - Wire-protocol types shared with the relay back-channel
22
+
23
+ ## Usage
24
+
25
+ ### Wrap your app
26
+
27
+ ```tsx
28
+ import { WebMCPProvider } from '@r0hitsharma/webmcp';
29
+
30
+ export function App() {
31
+ return (
32
+ <WebMCPProvider>
33
+ <YourApp />
34
+ </WebMCPProvider>
35
+ );
36
+ }
37
+ ```
38
+
39
+ ### Define and register a tool
40
+
41
+ The handler lives on the spec. Build the spec inside the component (or with a
42
+ ref) when it needs to close over component state — `useRegisterTool` reads the
43
+ latest spec through a ref, so it re-registers only when the tool *name* changes,
44
+ never on every render.
45
+
46
+ ```tsx
47
+ import { defineTool, useRegisterTool } from '@r0hitsharma/webmcp';
48
+
49
+ function IdentityView({ onSelect }: { onSelect: (id: string) => void }) {
50
+ const selectIdentityTool = defineTool<{ identityId: string }, { selected: string }>({
51
+ name: 'explorer.selectIdentity',
52
+ description: 'Select and focus an identity node in the Explorer.',
53
+ schema: {
54
+ type: 'object',
55
+ properties: { identityId: { type: 'string' } },
56
+ required: ['identityId'],
57
+ },
58
+ handler: async ({ identityId }) => {
59
+ onSelect(identityId);
60
+ return { selected: identityId };
61
+ },
62
+ });
63
+
64
+ useRegisterTool(selectIdentityTool);
65
+ return null;
66
+ }
67
+ ```
68
+
69
+ ## Public surface
70
+
71
+ - `defineTool` — schema-first tool factory
72
+ - `WebMCPProvider` — provider that initializes the polyfill and registry
73
+ - `useRegisterTool` — register a tool while a component is mounted
74
+ - `useTool` — observe a single tool's spec by name
75
+ - `useToolRegistry` / `useToolRegistryRef` — read the full registry
76
+ - `useContributeViewState` — contribute a partial view-state slice
77
+ - `listTools` / `getViewState` — imperative helpers for non-React callers
78
+ - `useRelaySession` — drive the relay back-channel from the registry: mint/reuse a session, advertise the registered tools, run incoming `invoke`s, and gate any `mutation: true` tool behind a local confirmation
79
+ - Tool types (`ToolSpec`, `ToolHandler`, `PendingCallPrompt`, `ViewState`, ...) and the wire-protocol types re-exported from [`@r0hitsharma/mcp-relay`](../mcp-relay/README.md), the single source of truth shared with the Python relay
80
+
81
+ ## Related
82
+
83
+ - [`@r0hitsharma/mcp-connect`](../mcp-connect/README.md) — the connection UI (chat icon, status indicator, connect modal) that pairs the browser with a harness over the relay.
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Public hooks for @r0hitsharma/webmcp.
3
+ *
4
+ * All hooks require <WebMCPProvider> in the component tree.
5
+ * None of them import @mcp-b/* directly, that coupling lives in provider.tsx.
6
+ */
7
+ import { type DependencyList } from 'react';
8
+ import type { ToolSpec, ViewState } from './types.js';
9
+ /**
10
+ * Register a tool while the calling component is mounted.
11
+ *
12
+ * Registers the tool into `document.modelContext` (via the @mcp-b/global
13
+ * polyfill) so it is exposed to any connected harness, AND registers the spec
14
+ * in the local ToolRegistry so `listTools()` / `getViewState()` see it.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * import { defineTool, useRegisterTool } from '@r0hitsharma/webmcp';
19
+ *
20
+ * const selectIdentityTool = defineTool({
21
+ * name: 'explorer.selectIdentity',
22
+ * description: 'Select and focus an identity node in the Explorer.',
23
+ * schema: {
24
+ * type: 'object',
25
+ * properties: { identityId: { type: 'string' } },
26
+ * required: ['identityId'],
27
+ * },
28
+ * handler: async ({ identityId }) => {
29
+ * // drive explorer state here
30
+ * return { selected: identityId };
31
+ * },
32
+ * });
33
+ *
34
+ * function IdentityGraph() {
35
+ * useRegisterTool(selectIdentityTool);
36
+ * return <Graph />;
37
+ * }
38
+ * ```
39
+ *
40
+ * @param spec - The tool definition created via `defineTool(...)`.
41
+ * @param deps - Optional dependency array. When values change the tool is
42
+ * re-registered. Pass the same deps you would pass to `useEffect`.
43
+ */
44
+ export declare function useRegisterTool<TArgs = Record<string, unknown>, TResult = unknown>(spec: ToolSpec<TArgs, TResult>, deps?: DependencyList): void;
45
+ /**
46
+ * Access the execution state of a previously-registered tool by name.
47
+ *
48
+ * Returns `null` if no tool with that name is registered.
49
+ *
50
+ * Useful for components that want to observe tool activity (e.g., showing a
51
+ * loading spinner while `explorer.switchBranch` is executing) without owning
52
+ * the registration themselves.
53
+ */
54
+ export declare function useTool(toolName: string): ToolSpec | null;
55
+ /**
56
+ * Read-only view of the whole tool registry.
57
+ *
58
+ * @returns
59
+ * - `tools` - live snapshot of all currently-registered tool specs
60
+ * - `listTools()` - stable function returning the same snapshot
61
+ * - `getViewState()` - aggregated view state from all contributors
62
+ *
63
+ * @example
64
+ * ```tsx
65
+ * function ConnectHarnessPanel() {
66
+ * const { tools, getViewState } = useToolRegistry();
67
+ * return <pre>{JSON.stringify({ count: tools.length, state: getViewState() }, null, 2)}</pre>;
68
+ * }
69
+ * ```
70
+ */
71
+ export declare function useToolRegistry(): {
72
+ tools: ToolSpec<Record<string, unknown>, unknown>[];
73
+ listTools: () => ToolSpec[];
74
+ getViewState: () => ViewState;
75
+ };
76
+ /**
77
+ * Contribute a partial view-state slice while the calling component is mounted.
78
+ *
79
+ * The slice is merged into the object returned by `getViewState()`.
80
+ * On unmount the contribution is removed.
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * function BranchSelector({ selectedBranch }: { selectedBranch: string }) {
85
+ * useContributeViewState({ selectedBranch });
86
+ * return <select>...</select>;
87
+ * }
88
+ * ```
89
+ */
90
+ export declare function useContributeViewState(partial: ViewState, deps?: DependencyList): void;
91
+ /**
92
+ * Imperative accessor for the list of currently-registered tools.
93
+ *
94
+ * For use outside React components (e.g., WebSocket relay handlers that need
95
+ * to push a fresh `tools/list` message to the server). Requires a registry
96
+ * instance obtained from `useToolRegistry()` or direct context access.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * const registry = getRegistryFromContext(ctx);
101
+ * const tools = listTools(registry);
102
+ * ws.send(JSON.stringify({ type: 'tools/list', tools: toWireFormat(tools) }));
103
+ * ```
104
+ */
105
+ export declare function listTools(registry: {
106
+ listTools: () => ToolSpec[];
107
+ }): ToolSpec[];
108
+ /**
109
+ * Imperative accessor for the aggregated view state.
110
+ */
111
+ export declare function getViewState(registry: {
112
+ getViewState: () => ViewState;
113
+ }): ViewState;
114
+ export type ToolRegistryRef = {
115
+ listTools: () => ToolSpec[];
116
+ getViewState: () => ViewState;
117
+ };
118
+ /**
119
+ * Returns a stable ref-shaped object pointing at the registry's imperative
120
+ * methods. Useful to pass to non-React code (e.g., the WebSocket session
121
+ * handler) without causing re-renders.
122
+ */
123
+ export declare function useToolRegistryRef(): ToolRegistryRef;
package/dist/hooks.js ADDED
@@ -0,0 +1,217 @@
1
+ /* eslint-disable no-underscore-dangle -- the registry exposes internal-by-convention methods (_addSpec, _contributeViewState) that these public hooks wrap. */
2
+ /**
3
+ * Public hooks for @r0hitsharma/webmcp.
4
+ *
5
+ * All hooks require <WebMCPProvider> in the component tree.
6
+ * None of them import @mcp-b/* directly, that coupling lives in provider.tsx.
7
+ */
8
+ import { useEffect, useMemo, useRef } from 'react';
9
+ import { useToolRegistryContext } from './provider.js';
10
+ import { acquireToolRegistration } from './registration.js';
11
+ // ---------------------------------------------------------------------------
12
+ // useRegisterTool
13
+ // ---------------------------------------------------------------------------
14
+ /**
15
+ * Register a tool while the calling component is mounted.
16
+ *
17
+ * Registers the tool into `document.modelContext` (via the @mcp-b/global
18
+ * polyfill) so it is exposed to any connected harness, AND registers the spec
19
+ * in the local ToolRegistry so `listTools()` / `getViewState()` see it.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * import { defineTool, useRegisterTool } from '@r0hitsharma/webmcp';
24
+ *
25
+ * const selectIdentityTool = defineTool({
26
+ * name: 'explorer.selectIdentity',
27
+ * description: 'Select and focus an identity node in the Explorer.',
28
+ * schema: {
29
+ * type: 'object',
30
+ * properties: { identityId: { type: 'string' } },
31
+ * required: ['identityId'],
32
+ * },
33
+ * handler: async ({ identityId }) => {
34
+ * // drive explorer state here
35
+ * return { selected: identityId };
36
+ * },
37
+ * });
38
+ *
39
+ * function IdentityGraph() {
40
+ * useRegisterTool(selectIdentityTool);
41
+ * return <Graph />;
42
+ * }
43
+ * ```
44
+ *
45
+ * @param spec - The tool definition created via `defineTool(...)`.
46
+ * @param deps - Optional dependency array. When values change the tool is
47
+ * re-registered. Pass the same deps you would pass to `useEffect`.
48
+ */
49
+ export function useRegisterTool(spec, deps) {
50
+ const registry = useToolRegistryContext();
51
+ // `deps` is accepted for API familiarity, but registration intentionally does
52
+ // NOT re-run when deps change. Callers (e.g. the Explorer) build specs from
53
+ // unstable values (inline callbacks, freshly-derived maps) every render; if
54
+ // the registration effects depended on those, they would re-run each render,
55
+ // and the registry's bump()/setState would loop ("Maximum update depth").
56
+ // Instead we register once per tool name and read the latest handler/spec
57
+ // through a ref, so handlers always see current state without re-registering.
58
+ void deps;
59
+ const specRef = useRef(spec);
60
+ // Synced in an effect, not during render: refs are read-only during
61
+ // render. Safe here because `stableSpec`'s getters below are read lazily
62
+ // (only when something actually accesses `.name`/`.handler`/etc, always
63
+ // after this effect has run, whether that's this hook's own registration
64
+ // effects or a harness invoking the tool later) — no cross-component
65
+ // ordering to worry about, unlike a ref read by a child's own effects.
66
+ useEffect(() => {
67
+ specRef.current = spec;
68
+ });
69
+ // A stable spec object (per tool name) whose fields delegate to the latest
70
+ // spec via specRef. Re-renders update specRef.current; this object identity
71
+ // stays constant so the effects below only run on mount / name change.
72
+ const stableSpec = useMemo(() => ({
73
+ get name() {
74
+ return specRef.current.name;
75
+ },
76
+ get description() {
77
+ return specRef.current.description;
78
+ },
79
+ get schema() {
80
+ return specRef.current.schema;
81
+ },
82
+ get mutation() {
83
+ return specRef.current.mutation;
84
+ },
85
+ get confirmationSummary() {
86
+ return specRef.current.confirmationSummary;
87
+ },
88
+ handler: (args) => specRef.current.handler(args),
89
+ }),
90
+ // eslint-disable-next-line react-hooks/exhaustive-deps
91
+ [spec.name]);
92
+ // Register in the local ToolRegistry so listTools() sees it.
93
+ useEffect(() => {
94
+ return registry._addSpec(stableSpec);
95
+ // eslint-disable-next-line react-hooks/exhaustive-deps
96
+ }, [spec.name]);
97
+ // Register with document.modelContext through our StrictMode-safe manager
98
+ // (see registration.ts). We deliberately do not use @mcp-b's useWebMCP here:
99
+ // its effect re-runs under StrictMode in a way that races the polyfill's
100
+ // microtask-synced McpServer and throws "Tool <name> is already registered".
101
+ // The manager registers each name once, refcounted, with AbortSignal-based
102
+ // unregister.
103
+ useEffect(() => {
104
+ return acquireToolRegistration(stableSpec);
105
+ // eslint-disable-next-line react-hooks/exhaustive-deps
106
+ }, [spec.name]);
107
+ }
108
+ // ---------------------------------------------------------------------------
109
+ // useTool
110
+ // ---------------------------------------------------------------------------
111
+ /**
112
+ * Access the execution state of a previously-registered tool by name.
113
+ *
114
+ * Returns `null` if no tool with that name is registered.
115
+ *
116
+ * Useful for components that want to observe tool activity (e.g., showing a
117
+ * loading spinner while `explorer.switchBranch` is executing) without owning
118
+ * the registration themselves.
119
+ */
120
+ export function useTool(toolName) {
121
+ const registry = useToolRegistryContext();
122
+ return registry.tools.find((t) => t.name === toolName) ?? null;
123
+ }
124
+ // ---------------------------------------------------------------------------
125
+ // useToolRegistry
126
+ // ---------------------------------------------------------------------------
127
+ /**
128
+ * Read-only view of the whole tool registry.
129
+ *
130
+ * @returns
131
+ * - `tools` - live snapshot of all currently-registered tool specs
132
+ * - `listTools()` - stable function returning the same snapshot
133
+ * - `getViewState()` - aggregated view state from all contributors
134
+ *
135
+ * @example
136
+ * ```tsx
137
+ * function ConnectHarnessPanel() {
138
+ * const { tools, getViewState } = useToolRegistry();
139
+ * return <pre>{JSON.stringify({ count: tools.length, state: getViewState() }, null, 2)}</pre>;
140
+ * }
141
+ * ```
142
+ */
143
+ export function useToolRegistry() {
144
+ const registry = useToolRegistryContext();
145
+ return {
146
+ tools: registry.tools,
147
+ listTools: registry.listTools,
148
+ getViewState: registry.getViewState,
149
+ };
150
+ }
151
+ // ---------------------------------------------------------------------------
152
+ // useContributeViewState
153
+ // ---------------------------------------------------------------------------
154
+ /**
155
+ * Contribute a partial view-state slice while the calling component is mounted.
156
+ *
157
+ * The slice is merged into the object returned by `getViewState()`.
158
+ * On unmount the contribution is removed.
159
+ *
160
+ * @example
161
+ * ```tsx
162
+ * function BranchSelector({ selectedBranch }: { selectedBranch: string }) {
163
+ * useContributeViewState({ selectedBranch });
164
+ * return <select>...</select>;
165
+ * }
166
+ * ```
167
+ */
168
+ export function useContributeViewState(partial, deps) {
169
+ const registry = useToolRegistryContext();
170
+ const contribute = registry._contributeViewState;
171
+ useEffect(() => {
172
+ return contribute(partial);
173
+ },
174
+ // eslint-disable-next-line react-hooks/exhaustive-deps
175
+ deps ?? Object.values(partial));
176
+ }
177
+ // ---------------------------------------------------------------------------
178
+ // Convenience re-export: listTools / getViewState as standalone functions
179
+ // ---------------------------------------------------------------------------
180
+ /**
181
+ * Imperative accessor for the list of currently-registered tools.
182
+ *
183
+ * For use outside React components (e.g., WebSocket relay handlers that need
184
+ * to push a fresh `tools/list` message to the server). Requires a registry
185
+ * instance obtained from `useToolRegistry()` or direct context access.
186
+ *
187
+ * @example
188
+ * ```ts
189
+ * const registry = getRegistryFromContext(ctx);
190
+ * const tools = listTools(registry);
191
+ * ws.send(JSON.stringify({ type: 'tools/list', tools: toWireFormat(tools) }));
192
+ * ```
193
+ */
194
+ export function listTools(registry) {
195
+ return registry.listTools();
196
+ }
197
+ /**
198
+ * Imperative accessor for the aggregated view state.
199
+ */
200
+ export function getViewState(registry) {
201
+ return registry.getViewState();
202
+ }
203
+ /**
204
+ * Returns a stable ref-shaped object pointing at the registry's imperative
205
+ * methods. Useful to pass to non-React code (e.g., the WebSocket session
206
+ * handler) without causing re-renders.
207
+ */
208
+ export function useToolRegistryRef() {
209
+ const registry = useToolRegistryContext();
210
+ // useMemo (not useCallback-then-invoke) so the returned object keeps a stable
211
+ // identity across renders. Non-React consumers (e.g. a WebSocket session
212
+ // handler) can hold this reference without it churning every render.
213
+ return useMemo(() => ({
214
+ listTools: registry.listTools,
215
+ getViewState: registry.getViewState,
216
+ }), [registry.listTools, registry.getViewState]);
217
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @r0hitsharma/webmcp
3
+ *
4
+ * Stable wrapper over the @mcp-b/global polyfill, exposing a fixed interface
5
+ * that the rest of the codebase depends on. All @mcp-b/* churn is absorbed here.
6
+ *
7
+ * Public surface (stable contract):
8
+ * - defineTool schema-first tool factory
9
+ * - WebMCPProvider React provider (initializes polyfill + registry)
10
+ * - useRegisterTool register a tool while mounted
11
+ * - useTool observe a single tool's spec by name
12
+ * - useToolRegistry read the full registry (listTools / getViewState)
13
+ * - useContributeViewState contribute a partial view-state slice
14
+ * - useRelaySession drive a relay back-channel from the registry
15
+ * - listTools / getViewState imperative helpers for non-React callers
16
+ * - ToolSpec / ViewState / etc. shared types
17
+ * - wire-protocol types re-exported from @r0hitsharma/mcp-relay
18
+ */
19
+ export { defineTool } from './types.js';
20
+ export type { PendingCallPrompt, ToolHandler, ToolRegistry, ToolSpec, ViewState, } from './types.js';
21
+ export { WebMCPProvider } from './provider.js';
22
+ export type { WebMCPProviderProps, ToolRegistryContextValue, } from './provider.js';
23
+ export { useRegisterTool, useTool, useToolRegistry, useContributeViewState, listTools, getViewState, useToolRegistryRef, } from './hooks.js';
24
+ export type { ToolRegistryRef } from './hooks.js';
25
+ export { useRelaySession } from './useRelaySession.js';
26
+ export type { RelaySessionStatus, UseRelaySessionOptions, UseRelaySessionResult, } from './useRelaySession.js';
27
+ export type { BrowserToServerMessage, ConnectionTokenClaims, CreateSessionResponse, HarnessStatusMessage, HelloAcceptedMessage, HelloMessage, HelloRejectedMessage, InvokeMessage, PingMessage, PongMessage, ResultMessage, ServerToBrowserMessage, ToolActivityMessage, ToolDefinition, ToolInputSchema, ToolsChangedMessage, ToolsListMessage, } from './protocol.js';
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @r0hitsharma/webmcp
3
+ *
4
+ * Stable wrapper over the @mcp-b/global polyfill, exposing a fixed interface
5
+ * that the rest of the codebase depends on. All @mcp-b/* churn is absorbed here.
6
+ *
7
+ * Public surface (stable contract):
8
+ * - defineTool schema-first tool factory
9
+ * - WebMCPProvider React provider (initializes polyfill + registry)
10
+ * - useRegisterTool register a tool while mounted
11
+ * - useTool observe a single tool's spec by name
12
+ * - useToolRegistry read the full registry (listTools / getViewState)
13
+ * - useContributeViewState contribute a partial view-state slice
14
+ * - useRelaySession drive a relay back-channel from the registry
15
+ * - listTools / getViewState imperative helpers for non-React callers
16
+ * - ToolSpec / ViewState / etc. shared types
17
+ * - wire-protocol types re-exported from @r0hitsharma/mcp-relay
18
+ */
19
+ // Tool-definition contract
20
+ export { defineTool } from './types.js';
21
+ // Provider
22
+ export { WebMCPProvider } from './provider.js';
23
+ // Hooks
24
+ export { useRegisterTool, useTool, useToolRegistry, useContributeViewState, listTools, getViewState, useToolRegistryRef, } from './hooks.js';
25
+ // Relay session: connects the registry to a relay back-channel.
26
+ export { useRelaySession } from './useRelaySession.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Relay wire-protocol types for the browser side.
3
+ *
4
+ * Single source of truth lives in the browser-free core
5
+ * `@r0hitsharma/mcp-relay`; we re-export it here so the browser widget and
6
+ * the relay host can never drift. (Previously these were re-declared in full;
7
+ * the duplicate has been collapsed.)
8
+ *
9
+ * Mutation confirmation is handled browser-side (the session hook gates a
10
+ * mutation invoke behind a local dialog, then returns a normal `result`), so
11
+ * the relay's MVP wire subset is all the browser needs — there are no
12
+ * confirmation_request/response frames on the wire.
13
+ */
14
+ export type { BrowserToServerMessage, ConnectionTokenClaims, CreateSessionResponse, HarnessStatusMessage, HelloAcceptedMessage, HelloMessage, HelloRejectedMessage, InvokeMessage, PingMessage, PongMessage, ResultMessage, ServerToBrowserMessage, ToolActivityMessage, ToolDefinition, ToolInputSchema, ToolsChangedMessage, ToolsListMessage, } from '@r0hitsharma/mcp-relay';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ /**
2
+ * WebMCPProvider
3
+ *
4
+ * Wraps @mcp-b/global initialization (installs the document.modelContext
5
+ * polyfill) and exposes a ToolRegistryContext so that useRegisterTool /
6
+ * useToolRegistry hooks can work together without redundant polyfill calls.
7
+ *
8
+ * Absorbs @mcp-b churn: callers never import @mcp-b/* directly.
9
+ */
10
+ import { type ReactNode } from 'react';
11
+ import type { ToolSpec, ViewState } from './types.js';
12
+ /**
13
+ * Internal registry kept by the provider so `listTools` and `getViewState`
14
+ * have something to read synchronously.
15
+ *
16
+ * Registration is additive: each `useRegisterTool` call pushes its spec into
17
+ * the registry while mounted and removes it on unmount.
18
+ */
19
+ export interface ToolRegistryContextValue {
20
+ /**
21
+ * Snapshot of all currently-registered tool specs.
22
+ * Re-computed whenever any tool mounts or unmounts.
23
+ */
24
+ tools: ToolSpec[];
25
+ /**
26
+ * Returns a snapshot of every registered tool spec at call time.
27
+ */
28
+ listTools: () => ToolSpec[];
29
+ /**
30
+ * Returns the current view state aggregated from all registered context
31
+ * contributions. Starts empty; components hydrate it via setViewState.
32
+ */
33
+ getViewState: () => ViewState;
34
+ /**
35
+ * Called by hooks to add a spec to the registry.
36
+ * Returns a cleanup function that removes it.
37
+ */
38
+ _addSpec: (spec: ToolSpec) => () => void;
39
+ /**
40
+ * Called by view-state contributor hooks to merge partial state.
41
+ * Returns a cleanup function that removes the contribution.
42
+ */
43
+ _contributeViewState: (partial: ViewState) => () => void;
44
+ }
45
+ export interface WebMCPProviderProps {
46
+ children: ReactNode;
47
+ /**
48
+ * Pass `false` to skip polyfill initialization (useful in tests or SSR).
49
+ * @default true
50
+ */
51
+ initPolyfill?: boolean;
52
+ }
53
+ /**
54
+ * Mount this provider once near the root of your React tree before using
55
+ * any hooks from @r0hitsharma/webmcp.
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * import { WebMCPProvider } from '@r0hitsharma/webmcp';
60
+ *
61
+ * function App() {
62
+ * return (
63
+ * <WebMCPProvider>
64
+ * <Explorer />
65
+ * </WebMCPProvider>
66
+ * );
67
+ * }
68
+ * ```
69
+ */
70
+ export declare function WebMCPProvider({ children, initPolyfill, }: WebMCPProviderProps): import("react").JSX.Element;
71
+ export declare function useToolRegistryContext(): ToolRegistryContextValue;
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /* eslint-disable no-underscore-dangle -- the registry exposes internal-by-convention methods (_addSpec, _contributeViewState) that the public hooks wrap. */
3
+ import { cleanupWebModelContext, initializeWebModelContext, } from '@mcp-b/global';
4
+ /**
5
+ * WebMCPProvider
6
+ *
7
+ * Wraps @mcp-b/global initialization (installs the document.modelContext
8
+ * polyfill) and exposes a ToolRegistryContext so that useRegisterTool /
9
+ * useToolRegistry hooks can work together without redundant polyfill calls.
10
+ *
11
+ * Absorbs @mcp-b churn: callers never import @mcp-b/* directly.
12
+ */
13
+ import { createContext, useCallback, useContext, useEffect, useRef, useSyncExternalStore, } from 'react';
14
+ const ToolRegistryContext = createContext(null);
15
+ /**
16
+ * Mount this provider once near the root of your React tree before using
17
+ * any hooks from @r0hitsharma/webmcp.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * import { WebMCPProvider } from '@r0hitsharma/webmcp';
22
+ *
23
+ * function App() {
24
+ * return (
25
+ * <WebMCPProvider>
26
+ * <Explorer />
27
+ * </WebMCPProvider>
28
+ * );
29
+ * }
30
+ * ```
31
+ */
32
+ export function WebMCPProvider({ children, initPolyfill = true, }) {
33
+ // Stable refs so the context value object is referentially stable.
34
+ const toolMapRef = useRef(new Map());
35
+ const viewStateRef = useRef(new Map());
36
+ // `tools` is exposed to render via `useSyncExternalStore` — refs aren't
37
+ // safe to read during render, so `toolMapRef` itself never is; `bump`
38
+ // recomputes a snapshot array and notifies subscribers instead.
39
+ const toolsSnapshotRef = useRef([]);
40
+ const listenersRef = useRef(new Set());
41
+ const subscribeTools = useCallback((listener) => {
42
+ listenersRef.current.add(listener);
43
+ return () => {
44
+ listenersRef.current.delete(listener);
45
+ };
46
+ }, []);
47
+ const getToolsSnapshot = useCallback(() => toolsSnapshotRef.current, []);
48
+ const bump = useCallback(() => {
49
+ toolsSnapshotRef.current = Array.from(toolMapRef.current.values());
50
+ for (const listener of listenersRef.current)
51
+ listener();
52
+ }, []);
53
+ // Initialize the document.modelContext polyfill on mount.
54
+ useEffect(() => {
55
+ if (!initPolyfill)
56
+ return;
57
+ initializeWebModelContext({ autoInitialize: true });
58
+ return () => {
59
+ cleanupWebModelContext();
60
+ };
61
+ }, [initPolyfill]);
62
+ const _addSpec = useCallback((spec) => {
63
+ toolMapRef.current.set(spec.name, spec);
64
+ bump();
65
+ return () => {
66
+ toolMapRef.current.delete(spec.name);
67
+ bump();
68
+ };
69
+ }, [bump]);
70
+ const _contributeViewState = useCallback((partial) => {
71
+ // Use object identity as key.
72
+ const key = Math.random().toString(36).slice(2);
73
+ viewStateRef.current.set(key, partial);
74
+ return () => {
75
+ viewStateRef.current.delete(key);
76
+ };
77
+ }, []);
78
+ const listTools = useCallback(() => {
79
+ return Array.from(toolMapRef.current.values());
80
+ }, []);
81
+ const getViewState = useCallback(() => {
82
+ const merged = {};
83
+ for (const partial of viewStateRef.current.values()) {
84
+ Object.assign(merged, partial);
85
+ }
86
+ return merged;
87
+ }, []);
88
+ // No effect ever runs during server rendering, so `toolsSnapshotRef` is
89
+ // still at its initial (empty) value then — safe to reuse `getToolsSnapshot`
90
+ // as the server snapshot too.
91
+ const tools = useSyncExternalStore(subscribeTools, getToolsSnapshot, getToolsSnapshot);
92
+ const value = {
93
+ tools,
94
+ listTools,
95
+ getViewState,
96
+ _addSpec,
97
+ _contributeViewState,
98
+ };
99
+ return (_jsx(ToolRegistryContext.Provider, { value: value, children: children }));
100
+ }
101
+ // ---------------------------------------------------------------------------
102
+ // Internal hook to access the registry context (throws if not mounted)
103
+ // ---------------------------------------------------------------------------
104
+ export function useToolRegistryContext() {
105
+ const ctx = useContext(ToolRegistryContext);
106
+ if (!ctx) {
107
+ throw new Error('[web-mcp] useToolRegistryContext called outside <WebMCPProvider>. ' +
108
+ 'Wrap your app (or at least the component tree that uses @r0hitsharma/webmcp hooks) ' +
109
+ 'with <WebMCPProvider>.');
110
+ }
111
+ return ctx;
112
+ }