@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 +83 -0
- package/dist/hooks.d.ts +123 -0
- package/dist/hooks.js +217 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +26 -0
- package/dist/protocol.d.ts +14 -0
- package/dist/protocol.js +1 -0
- package/dist/provider.d.ts +71 -0
- package/dist/provider.js +112 -0
- package/dist/registration.d.ts +24 -0
- package/dist/registration.js +92 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.js +37 -0
- package/dist/useRelaySession.d.ts +30 -0
- package/dist/useRelaySession.js +317 -0
- package/package.json +48 -0
- package/src/hooks.ts +260 -0
- package/src/index.ts +76 -0
- package/src/protocol.ts +32 -0
- package/src/provider.tsx +210 -0
- package/src/registration.ts +139 -0
- package/src/types.ts +108 -0
- package/src/useRelaySession.ts +402 -0
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.
|
package/dist/hooks.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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';
|
package/dist/protocol.js
ADDED
|
@@ -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;
|
package/dist/provider.js
ADDED
|
@@ -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
|
+
}
|