usewebmcp 0.2.3 → 0.3.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 +271 -0
- package/dist/index.d.ts +388 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -0
- package/package.json +22 -12
package/README.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# usewebmcp
|
|
2
|
+
|
|
3
|
+
Standalone React hooks for strict core WebMCP tool registration via `navigator.modelContext`.
|
|
4
|
+
|
|
5
|
+
`usewebmcp` is intentionally separate from `@mcp-b/react-webmcp`:
|
|
6
|
+
|
|
7
|
+
- Use `usewebmcp` for strict core WebMCP workflows.
|
|
8
|
+
- Use `@mcp-b/react-webmcp` for full MCP-B runtime features (resources, prompts, client/provider flows, etc.).
|
|
9
|
+
|
|
10
|
+
## Package Selection
|
|
11
|
+
|
|
12
|
+
| Package | Use When |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `usewebmcp` | React hooks for strict core `navigator.modelContext` tools |
|
|
15
|
+
| `@mcp-b/react-webmcp` | React hooks for full MCP-B runtime surface |
|
|
16
|
+
| `@mcp-b/webmcp-polyfill` | You need a strict core runtime polyfill |
|
|
17
|
+
| `@mcp-b/global` | You need full MCP-B runtime (core + extensions) |
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add usewebmcp react
|
|
23
|
+
# or
|
|
24
|
+
npm install usewebmcp react
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Optional (only if you want Standard Schema authoring like Zod v4 input schemas):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm add zod
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Runtime Prerequisite
|
|
34
|
+
|
|
35
|
+
`usewebmcp` expects `window.navigator.modelContext` to exist.
|
|
36
|
+
|
|
37
|
+
You can provide it via:
|
|
38
|
+
|
|
39
|
+
- Browser-native WebMCP implementation, or
|
|
40
|
+
- `@mcp-b/webmcp-polyfill`, or
|
|
41
|
+
- `@mcp-b/global`
|
|
42
|
+
|
|
43
|
+
If `navigator.modelContext` is missing, the hook logs a warning and skips registration.
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { initializeWebMCPPolyfill } from '@mcp-b/webmcp-polyfill';
|
|
49
|
+
import { useWebMCP } from 'usewebmcp';
|
|
50
|
+
|
|
51
|
+
initializeWebMCPPolyfill();
|
|
52
|
+
|
|
53
|
+
const COUNTER_INPUT_SCHEMA = {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {},
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
const COUNTER_OUTPUT_SCHEMA = {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
count: { type: 'integer' },
|
|
62
|
+
},
|
|
63
|
+
required: ['count'],
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
export function CounterTool() {
|
|
68
|
+
const counterTool = useWebMCP({
|
|
69
|
+
name: 'counter_get',
|
|
70
|
+
description: 'Get current count',
|
|
71
|
+
inputSchema: COUNTER_INPUT_SCHEMA,
|
|
72
|
+
outputSchema: COUNTER_OUTPUT_SCHEMA,
|
|
73
|
+
execute: async () => ({ count: 42 }),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div>
|
|
78
|
+
<p>Executions: {counterTool.state.executionCount}</p>
|
|
79
|
+
<p>Last count: {counterTool.state.lastResult?.count ?? 'none'}</p>
|
|
80
|
+
{counterTool.state.error && <p>Error: {counterTool.state.error.message}</p>}
|
|
81
|
+
<button
|
|
82
|
+
onClick={async () => {
|
|
83
|
+
await counterTool.execute({});
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
Run Tool Locally
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## How `useWebMCP` Works
|
|
94
|
+
|
|
95
|
+
- Registers a tool on mount with `navigator.modelContext.registerTool(...)`.
|
|
96
|
+
- Unregisters on unmount with `navigator.modelContext.unregisterTool(name)`.
|
|
97
|
+
- Exposes local execution state:
|
|
98
|
+
- `state.isExecuting`
|
|
99
|
+
- `state.lastResult`
|
|
100
|
+
- `state.error`
|
|
101
|
+
- `state.executionCount`
|
|
102
|
+
- Returns `execute(input)` for manual in-app invocation and `reset()` for state reset.
|
|
103
|
+
|
|
104
|
+
Your tool implementation (`config.execute` or `config.handler`) can be synchronous or asynchronous.
|
|
105
|
+
|
|
106
|
+
## `config.execute` vs returned `execute(...)`
|
|
107
|
+
|
|
108
|
+
- `config.execute`: preferred config field for tool logic.
|
|
109
|
+
- `config.handler`: backward-compatible alias for `config.execute`.
|
|
110
|
+
- returned `execute(input)`: hook return function for local manual invocation from your UI/tests.
|
|
111
|
+
|
|
112
|
+
If both `config.execute` and `config.handler` are provided, `config.execute` is used.
|
|
113
|
+
|
|
114
|
+
Both paths run the same underlying tool logic and update the hook state.
|
|
115
|
+
|
|
116
|
+
## Type Inference
|
|
117
|
+
|
|
118
|
+
### Input inference
|
|
119
|
+
|
|
120
|
+
`inputSchema` supports:
|
|
121
|
+
|
|
122
|
+
- JSON Schema literals (`as const`) via `InferArgsFromInputSchema`
|
|
123
|
+
- Standard Schema v1 input typing (for example Zod v4 / Valibot / ArkType) via `~standard.types.input`
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
const INPUT_SCHEMA = {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
query: { type: 'string' },
|
|
130
|
+
limit: { type: 'integer' },
|
|
131
|
+
},
|
|
132
|
+
required: ['query'],
|
|
133
|
+
additionalProperties: false,
|
|
134
|
+
} as const;
|
|
135
|
+
|
|
136
|
+
useWebMCP({
|
|
137
|
+
name: 'search',
|
|
138
|
+
description: 'Search docs',
|
|
139
|
+
inputSchema: INPUT_SCHEMA,
|
|
140
|
+
execute(input) {
|
|
141
|
+
// input is inferred as { query: string; limit?: number }
|
|
142
|
+
return { total: 1 };
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Output inference
|
|
148
|
+
|
|
149
|
+
When `outputSchema` is provided as a literal JSON object schema:
|
|
150
|
+
|
|
151
|
+
- implementation return type is inferred from `outputSchema`
|
|
152
|
+
- `state.lastResult` is inferred to the same type
|
|
153
|
+
- MCP response includes `structuredContent`
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
const OUTPUT_SCHEMA = {
|
|
157
|
+
type: 'object',
|
|
158
|
+
properties: {
|
|
159
|
+
total: { type: 'integer' },
|
|
160
|
+
},
|
|
161
|
+
required: ['total'],
|
|
162
|
+
additionalProperties: false,
|
|
163
|
+
} as const;
|
|
164
|
+
|
|
165
|
+
const tool = useWebMCP({
|
|
166
|
+
name: 'count_items',
|
|
167
|
+
description: 'Count items',
|
|
168
|
+
outputSchema: OUTPUT_SCHEMA,
|
|
169
|
+
execute: () => ({ total: 3 }),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// tool.state.lastResult is inferred as { total: number } | null
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Manual `execute(...)` Calls
|
|
176
|
+
|
|
177
|
+
You can call the returned `execute(...)` function directly from your component.
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
function SearchToolPanel() {
|
|
181
|
+
const searchTool = useWebMCP({
|
|
182
|
+
name: 'search_local',
|
|
183
|
+
description: 'Run local search',
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: { query: { type: 'string' } },
|
|
187
|
+
required: ['query'],
|
|
188
|
+
additionalProperties: false,
|
|
189
|
+
} as const,
|
|
190
|
+
execute: async ({ query }) => ({ query, total: query.length }),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<button
|
|
195
|
+
onClick={async () => {
|
|
196
|
+
await searchTool.execute({ query: 'webmcp' });
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
Run Search
|
|
200
|
+
</button>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Output Schema Contract
|
|
206
|
+
|
|
207
|
+
If `outputSchema` is defined, your tool implementation must return a JSON-serializable object result.
|
|
208
|
+
|
|
209
|
+
Returning a non-object value (`string`, `null`, array, etc.) causes an error response from the registered MCP tool.
|
|
210
|
+
|
|
211
|
+
## Re-Registration and Performance
|
|
212
|
+
|
|
213
|
+
The tool re-registers when any of these change:
|
|
214
|
+
|
|
215
|
+
- `name`
|
|
216
|
+
- `description`
|
|
217
|
+
- `inputSchema` reference
|
|
218
|
+
- `outputSchema` reference
|
|
219
|
+
- `annotations` reference
|
|
220
|
+
- values in `deps`
|
|
221
|
+
|
|
222
|
+
The hook avoids re-registration when only callback references change:
|
|
223
|
+
|
|
224
|
+
- `execute`
|
|
225
|
+
- `handler`
|
|
226
|
+
- `onSuccess`
|
|
227
|
+
- `onError`
|
|
228
|
+
- `formatOutput`
|
|
229
|
+
|
|
230
|
+
Latest callback versions are still used at execution time.
|
|
231
|
+
|
|
232
|
+
Recommendation:
|
|
233
|
+
|
|
234
|
+
- Define schemas/annotations outside render or memoize them.
|
|
235
|
+
- Keep `deps` primitive when possible.
|
|
236
|
+
|
|
237
|
+
## API
|
|
238
|
+
|
|
239
|
+
### `useWebMCP(config, deps?)`
|
|
240
|
+
|
|
241
|
+
`config` fields:
|
|
242
|
+
|
|
243
|
+
- `name: string`
|
|
244
|
+
- `description: string`
|
|
245
|
+
- `inputSchema?`
|
|
246
|
+
- `outputSchema?`
|
|
247
|
+
- `annotations?`
|
|
248
|
+
- `execute(input)` (preferred)
|
|
249
|
+
- `handler(input)` (backward-compatible alias)
|
|
250
|
+
- `formatOutput?(output)` (deprecated)
|
|
251
|
+
- `onSuccess?(result, input)`
|
|
252
|
+
- `onError?(error, input)`
|
|
253
|
+
|
|
254
|
+
Return value:
|
|
255
|
+
|
|
256
|
+
- `state`
|
|
257
|
+
- `execute(input)`
|
|
258
|
+
- `reset()`
|
|
259
|
+
|
|
260
|
+
`execute(input)` is a local direct call to your configured tool implementation for in-app control/testing.
|
|
261
|
+
Tool calls coming from MCP clients still go through `navigator.modelContext`.
|
|
262
|
+
|
|
263
|
+
## Important Notes
|
|
264
|
+
|
|
265
|
+
- This is a client-side hook package (`'use client'`).
|
|
266
|
+
- `formatOutput` is deprecated; prefer `outputSchema` + structured output.
|
|
267
|
+
- When tool output is not a string, default text content is pretty-printed JSON.
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,388 @@
|
|
|
1
|
-
|
|
1
|
+
import { DependencyList } from "react";
|
|
2
|
+
import { ToolInputSchema } from "@mcp-b/webmcp-polyfill";
|
|
3
|
+
import { InferArgsFromInputSchema, InferJsonSchema, InputSchema, JsonSchemaObject, ToolAnnotations } from "@mcp-b/webmcp-types";
|
|
4
|
+
|
|
5
|
+
//#region src/types.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Infers tool input type from either a Standard Schema or JSON Schema.
|
|
9
|
+
*
|
|
10
|
+
* - **Standard Schema** (Zod v4, Valibot, ArkType): extracts `~standard.types.input`
|
|
11
|
+
* - **JSON Schema** (`as const`): uses `InferArgsFromInputSchema` for structural inference
|
|
12
|
+
* - **Fallback**: `Record<string, unknown>`
|
|
13
|
+
*
|
|
14
|
+
* @template T - The input schema type
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
type InferToolInput<T> = T extends {
|
|
18
|
+
readonly '~standard': {
|
|
19
|
+
readonly types?: infer Types;
|
|
20
|
+
};
|
|
21
|
+
} ? Types extends {
|
|
22
|
+
readonly input: infer I;
|
|
23
|
+
} ? I : Record<string, unknown> : T extends InputSchema ? InferArgsFromInputSchema<T> : Record<string, unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* Utility type to infer the output type from a JSON Schema object.
|
|
26
|
+
*
|
|
27
|
+
* When `TOutputSchema` is a literal `JsonSchemaObject`, this resolves to
|
|
28
|
+
* `InferJsonSchema<TOutputSchema>`. When it's `undefined`,
|
|
29
|
+
* it falls back to the provided `TFallback` type.
|
|
30
|
+
*
|
|
31
|
+
* @template TOutputSchema - JSON Schema object for output inference
|
|
32
|
+
* @template TFallback - Fallback type when no schema is provided
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
type InferOutput<TOutputSchema extends JsonSchemaObject | undefined = undefined, TFallback = unknown> = TOutputSchema extends JsonSchemaObject ? InferJsonSchema<TOutputSchema> : TFallback;
|
|
36
|
+
/**
|
|
37
|
+
* Represents the current execution state of a tool, including loading status,
|
|
38
|
+
* results, errors, and execution history.
|
|
39
|
+
*
|
|
40
|
+
* @template TOutput - The type of data returned by the tool implementation
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
interface ToolExecutionState<TOutput = unknown> {
|
|
44
|
+
/**
|
|
45
|
+
* Indicates whether the tool is currently executing.
|
|
46
|
+
* Use this to show loading states in your UI.
|
|
47
|
+
*/
|
|
48
|
+
isExecuting: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The result from the most recent successful execution.
|
|
51
|
+
* Will be `null` if the tool hasn't been executed or last execution failed.
|
|
52
|
+
*/
|
|
53
|
+
lastResult: TOutput | null;
|
|
54
|
+
/**
|
|
55
|
+
* The error from the most recent failed execution.
|
|
56
|
+
* Will be `null` if the tool hasn't been executed or last execution succeeded.
|
|
57
|
+
*/
|
|
58
|
+
error: Error | null;
|
|
59
|
+
/**
|
|
60
|
+
* Total number of times this tool has been executed.
|
|
61
|
+
* Increments on successful executions only.
|
|
62
|
+
*/
|
|
63
|
+
executionCount: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Tool implementation function used by `useWebMCP`.
|
|
67
|
+
*
|
|
68
|
+
* Supports sync or async implementations.
|
|
69
|
+
*
|
|
70
|
+
* @template TInputSchema - Schema defining input parameters
|
|
71
|
+
* @template TOutputSchema - Optional JSON Schema object defining output structure
|
|
72
|
+
*
|
|
73
|
+
* @public
|
|
74
|
+
*/
|
|
75
|
+
type ToolExecuteFunction<TInputSchema extends ToolInputSchema = InputSchema, TOutputSchema extends JsonSchemaObject | undefined = undefined> = (input: InferToolInput<TInputSchema>) => Promise<InferOutput<TOutputSchema>> | InferOutput<TOutputSchema>;
|
|
76
|
+
/**
|
|
77
|
+
* Shared configuration fields for the `useWebMCP` hook.
|
|
78
|
+
*
|
|
79
|
+
* Defines a tool's metadata, schema, and lifecycle callbacks.
|
|
80
|
+
* Uses JSON Schema for type inference via `as const`.
|
|
81
|
+
*
|
|
82
|
+
* @template TInputSchema - JSON Schema defining input parameters
|
|
83
|
+
* @template TOutputSchema - JSON Schema object defining output structure (enables structuredContent)
|
|
84
|
+
*
|
|
85
|
+
* @public
|
|
86
|
+
*
|
|
87
|
+
* @example Basic tool without output schema:
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const config: WebMCPConfig = {
|
|
90
|
+
* name: 'posts_like',
|
|
91
|
+
* description: 'Like a post by its ID',
|
|
92
|
+
* inputSchema: {
|
|
93
|
+
* type: 'object',
|
|
94
|
+
* properties: { postId: { type: 'string' } },
|
|
95
|
+
* required: ['postId'],
|
|
96
|
+
* } as const,
|
|
97
|
+
* execute: async ({ postId }) => {
|
|
98
|
+
* await api.likePost(postId);
|
|
99
|
+
* return { success: true };
|
|
100
|
+
* },
|
|
101
|
+
* };
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @example Tool with output schema (enables MCP structuredContent):
|
|
105
|
+
* ```typescript
|
|
106
|
+
* useWebMCP({
|
|
107
|
+
* name: 'posts_like',
|
|
108
|
+
* description: 'Like a post and return updated like count',
|
|
109
|
+
* inputSchema: {
|
|
110
|
+
* type: 'object',
|
|
111
|
+
* properties: { postId: { type: 'string' } },
|
|
112
|
+
* required: ['postId'],
|
|
113
|
+
* } as const,
|
|
114
|
+
* outputSchema: {
|
|
115
|
+
* type: 'object',
|
|
116
|
+
* properties: {
|
|
117
|
+
* likes: { type: 'number', description: 'Updated like count' },
|
|
118
|
+
* likedAt: { type: 'string', description: 'ISO timestamp of the like' },
|
|
119
|
+
* },
|
|
120
|
+
* } as const,
|
|
121
|
+
* execute: async ({ postId }) => {
|
|
122
|
+
* const result = await api.likePost(postId);
|
|
123
|
+
* return { likes: result.likes, likedAt: new Date().toISOString() };
|
|
124
|
+
* },
|
|
125
|
+
* });
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
interface WebMCPConfigBase<TInputSchema extends ToolInputSchema = InputSchema, TOutputSchema extends JsonSchemaObject | undefined = undefined> {
|
|
129
|
+
/**
|
|
130
|
+
* Unique identifier for the tool (e.g., 'posts_like', 'graph_navigate').
|
|
131
|
+
* Must follow naming conventions: lowercase with underscores.
|
|
132
|
+
*/
|
|
133
|
+
name: string;
|
|
134
|
+
/**
|
|
135
|
+
* Human-readable description explaining what the tool does.
|
|
136
|
+
* This description is used by AI assistants to understand when to use the tool.
|
|
137
|
+
*/
|
|
138
|
+
description: string;
|
|
139
|
+
/**
|
|
140
|
+
* Schema defining the input parameters for the tool.
|
|
141
|
+
* Accepts JSON Schema (with `as const`) or any Standard Schema v1
|
|
142
|
+
* library (Zod v4, Valibot, ArkType, etc.).
|
|
143
|
+
*
|
|
144
|
+
* @example JSON Schema
|
|
145
|
+
* ```typescript
|
|
146
|
+
* inputSchema: {
|
|
147
|
+
* type: 'object',
|
|
148
|
+
* properties: {
|
|
149
|
+
* postId: { type: 'string', description: 'The ID of the post to like' },
|
|
150
|
+
* },
|
|
151
|
+
* required: ['postId'],
|
|
152
|
+
* } as const
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* @example Standard Schema (Zod v4)
|
|
156
|
+
* ```typescript
|
|
157
|
+
* inputSchema: z.object({ postId: z.string() })
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
inputSchema?: TInputSchema;
|
|
161
|
+
/**
|
|
162
|
+
* **Recommended:** JSON Schema object defining the expected output structure.
|
|
163
|
+
*
|
|
164
|
+
* When provided, this enables three key features:
|
|
165
|
+
* 1. **Type Safety**: The implementation return type is inferred from this schema
|
|
166
|
+
* 2. **MCP structuredContent**: The MCP response includes `structuredContent`
|
|
167
|
+
* containing the typed output per the MCP specification
|
|
168
|
+
* 3. **AI Understanding**: AI models can better understand and use the tool's output
|
|
169
|
+
*
|
|
170
|
+
* @see {@link https://spec.modelcontextprotocol.io/specification/server/tools/#output-schemas}
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* outputSchema: {
|
|
175
|
+
* type: 'object',
|
|
176
|
+
* properties: {
|
|
177
|
+
* counter: { type: 'number', description: 'The current counter value' },
|
|
178
|
+
* timestamp: { type: 'string', description: 'ISO timestamp' },
|
|
179
|
+
* },
|
|
180
|
+
* } as const
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
outputSchema?: TOutputSchema;
|
|
184
|
+
/**
|
|
185
|
+
* Optional metadata annotations providing hints about tool behavior.
|
|
186
|
+
* See {@link ToolAnnotations} for available options.
|
|
187
|
+
*/
|
|
188
|
+
annotations?: ToolAnnotations;
|
|
189
|
+
/**
|
|
190
|
+
* Custom formatter for the MCP text response.
|
|
191
|
+
*
|
|
192
|
+
* @deprecated Use `outputSchema` instead. The `outputSchema` provides type-safe
|
|
193
|
+
* structured output via MCP's `structuredContent`, which is the recommended
|
|
194
|
+
* approach for tool outputs. This property will be removed in a future version.
|
|
195
|
+
*
|
|
196
|
+
* @param output - The raw output from the tool implementation
|
|
197
|
+
* @returns Formatted string for the MCP response content
|
|
198
|
+
*/
|
|
199
|
+
formatOutput?: (output: InferOutput<TOutputSchema>) => string;
|
|
200
|
+
/**
|
|
201
|
+
* Optional callback invoked when the tool execution succeeds.
|
|
202
|
+
* Useful for triggering side effects like navigation or analytics.
|
|
203
|
+
*
|
|
204
|
+
* @param result - The successful result from the tool implementation
|
|
205
|
+
* @param input - The input that was passed to the tool implementation
|
|
206
|
+
*/
|
|
207
|
+
onSuccess?: (result: InferOutput<TOutputSchema>, input: unknown) => void;
|
|
208
|
+
/**
|
|
209
|
+
* Optional callback invoked when the tool execution fails.
|
|
210
|
+
* Useful for error handling, logging, or showing user notifications.
|
|
211
|
+
*
|
|
212
|
+
* @param error - The error that occurred during execution
|
|
213
|
+
* @param input - The input that was passed to the tool implementation
|
|
214
|
+
*/
|
|
215
|
+
onError?: (error: Error, input: unknown) => void;
|
|
216
|
+
}
|
|
217
|
+
type WebMCPConfigImplementation<TInputSchema extends ToolInputSchema = InputSchema, TOutputSchema extends JsonSchemaObject | undefined = undefined> = {
|
|
218
|
+
/**
|
|
219
|
+
* Preferred tool implementation function.
|
|
220
|
+
*/
|
|
221
|
+
execute: ToolExecuteFunction<TInputSchema, TOutputSchema>;
|
|
222
|
+
/**
|
|
223
|
+
* Backward-compatible alias for `execute`.
|
|
224
|
+
*/
|
|
225
|
+
handler?: ToolExecuteFunction<TInputSchema, TOutputSchema>;
|
|
226
|
+
} | {
|
|
227
|
+
/**
|
|
228
|
+
* Backward-compatible alias for `execute`.
|
|
229
|
+
*/
|
|
230
|
+
handler: ToolExecuteFunction<TInputSchema, TOutputSchema>;
|
|
231
|
+
/**
|
|
232
|
+
* Preferred tool implementation function.
|
|
233
|
+
*/
|
|
234
|
+
execute?: ToolExecuteFunction<TInputSchema, TOutputSchema>;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Configuration options for the `useWebMCP` hook.
|
|
238
|
+
*
|
|
239
|
+
* You can provide tool logic with either:
|
|
240
|
+
* - `execute` (preferred), or
|
|
241
|
+
* - `handler` (backward-compatible alias).
|
|
242
|
+
*
|
|
243
|
+
* If both are provided, `execute` is used.
|
|
244
|
+
*
|
|
245
|
+
* @template TInputSchema - JSON Schema defining input parameters
|
|
246
|
+
* @template TOutputSchema - JSON Schema object defining output structure (enables structuredContent)
|
|
247
|
+
*
|
|
248
|
+
* @public
|
|
249
|
+
*/
|
|
250
|
+
type WebMCPConfig<TInputSchema extends ToolInputSchema = InputSchema, TOutputSchema extends JsonSchemaObject | undefined = undefined> = WebMCPConfigBase<TInputSchema, TOutputSchema> & WebMCPConfigImplementation<TInputSchema, TOutputSchema>;
|
|
251
|
+
/**
|
|
252
|
+
* Return value from the `useWebMCP` hook.
|
|
253
|
+
* Provides access to execution state and methods for manual tool control.
|
|
254
|
+
*
|
|
255
|
+
* @template TOutputSchema - JSON Schema object defining output structure
|
|
256
|
+
* @public
|
|
257
|
+
*/
|
|
258
|
+
interface WebMCPReturn<TOutputSchema extends JsonSchemaObject | undefined = undefined> {
|
|
259
|
+
/**
|
|
260
|
+
* Current execution state including loading status, results, and errors.
|
|
261
|
+
* See {@link ToolExecutionState} for details.
|
|
262
|
+
*/
|
|
263
|
+
state: ToolExecutionState<InferOutput<TOutputSchema>>;
|
|
264
|
+
/**
|
|
265
|
+
* Manually execute the tool with the provided input.
|
|
266
|
+
* Useful for testing, debugging, or triggering execution from your UI.
|
|
267
|
+
*
|
|
268
|
+
* @param input - The input parameters to pass to the tool
|
|
269
|
+
* @returns Promise resolving to the tool's output
|
|
270
|
+
* @throws Error if validation fails or tool implementation throws
|
|
271
|
+
*/
|
|
272
|
+
execute: (input: unknown) => Promise<InferOutput<TOutputSchema>>;
|
|
273
|
+
/**
|
|
274
|
+
* Reset the execution state to its initial values.
|
|
275
|
+
* Clears results, errors, and resets the execution count.
|
|
276
|
+
*/
|
|
277
|
+
reset: () => void;
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region src/useWebMCP.d.ts
|
|
281
|
+
/**
|
|
282
|
+
* React hook for registering and managing Model Context Protocol (MCP) tools.
|
|
283
|
+
*
|
|
284
|
+
* This hook handles the complete lifecycle of an MCP tool:
|
|
285
|
+
* - Registers the tool with `window.navigator.modelContext`
|
|
286
|
+
* - Manages execution state (loading, results, errors)
|
|
287
|
+
* - Handles tool execution and lifecycle callbacks
|
|
288
|
+
* - Automatically unregisters on component unmount
|
|
289
|
+
* - Returns `structuredContent` when `outputSchema` is defined
|
|
290
|
+
*
|
|
291
|
+
* ## Output Schema (Recommended)
|
|
292
|
+
*
|
|
293
|
+
* Always define an `outputSchema` for your tools. This provides:
|
|
294
|
+
* - **Type Safety**: Handler return type is inferred from the schema
|
|
295
|
+
* - **MCP structuredContent**: AI models receive structured, typed data
|
|
296
|
+
* - **Better AI Understanding**: Models can reason about your tool's output format
|
|
297
|
+
*
|
|
298
|
+
* ```tsx
|
|
299
|
+
* useWebMCP({
|
|
300
|
+
* name: 'get_user',
|
|
301
|
+
* description: 'Get user by ID',
|
|
302
|
+
* inputSchema: {
|
|
303
|
+
* type: 'object',
|
|
304
|
+
* properties: { userId: { type: 'string' } },
|
|
305
|
+
* required: ['userId'],
|
|
306
|
+
* } as const,
|
|
307
|
+
* outputSchema: {
|
|
308
|
+
* type: 'object',
|
|
309
|
+
* properties: {
|
|
310
|
+
* id: { type: 'string' },
|
|
311
|
+
* name: { type: 'string' },
|
|
312
|
+
* email: { type: 'string' },
|
|
313
|
+
* },
|
|
314
|
+
* } as const,
|
|
315
|
+
* execute: async ({ userId }) => {
|
|
316
|
+
* const user = await fetchUser(userId);
|
|
317
|
+
* return { id: user.id, name: user.name, email: user.email };
|
|
318
|
+
* },
|
|
319
|
+
* });
|
|
320
|
+
* ```
|
|
321
|
+
*
|
|
322
|
+
* ## Re-render Optimization
|
|
323
|
+
*
|
|
324
|
+
* This hook is optimized to minimize unnecessary tool re-registrations:
|
|
325
|
+
*
|
|
326
|
+
* - **Ref-based callbacks**: `execute`/`handler`, `onSuccess`, `onError`, and `formatOutput`
|
|
327
|
+
* are stored in refs, so changing these functions won't trigger re-registration.
|
|
328
|
+
*
|
|
329
|
+
* **IMPORTANT**: If `inputSchema`, `outputSchema`, or `annotations` are defined inline
|
|
330
|
+
* or change on every render, the tool will re-register unnecessarily. To avoid this,
|
|
331
|
+
* define them outside your component with `as const`:
|
|
332
|
+
*
|
|
333
|
+
* ```tsx
|
|
334
|
+
* // Good: Static schema defined outside component
|
|
335
|
+
* const OUTPUT_SCHEMA = {
|
|
336
|
+
* type: 'object',
|
|
337
|
+
* properties: { count: { type: 'number' } },
|
|
338
|
+
* } as const;
|
|
339
|
+
*
|
|
340
|
+
* // Bad: Inline schema (creates new object every render)
|
|
341
|
+
* useWebMCP({
|
|
342
|
+
* outputSchema: { type: 'object', properties: { count: { type: 'number' } } } as const,
|
|
343
|
+
* });
|
|
344
|
+
* ```
|
|
345
|
+
*
|
|
346
|
+
* @template TInputSchema - JSON Schema defining input parameter types (use `as const` for inference)
|
|
347
|
+
* @template TOutputSchema - JSON Schema object defining output structure (enables structuredContent)
|
|
348
|
+
*
|
|
349
|
+
* @param config - Configuration object for the tool
|
|
350
|
+
* @param deps - Optional dependency array that triggers tool re-registration when values change.
|
|
351
|
+
*
|
|
352
|
+
* @returns Object containing execution state and control methods
|
|
353
|
+
*
|
|
354
|
+
* @public
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* Basic tool with outputSchema (recommended):
|
|
358
|
+
* ```tsx
|
|
359
|
+
* function PostActions() {
|
|
360
|
+
* const likeTool = useWebMCP({
|
|
361
|
+
* name: 'posts_like',
|
|
362
|
+
* description: 'Like a post by ID',
|
|
363
|
+
* inputSchema: {
|
|
364
|
+
* type: 'object',
|
|
365
|
+
* properties: { postId: { type: 'string', description: 'The post ID' } },
|
|
366
|
+
* required: ['postId'],
|
|
367
|
+
* } as const,
|
|
368
|
+
* outputSchema: {
|
|
369
|
+
* type: 'object',
|
|
370
|
+
* properties: {
|
|
371
|
+
* success: { type: 'boolean' },
|
|
372
|
+
* likeCount: { type: 'number' },
|
|
373
|
+
* },
|
|
374
|
+
* } as const,
|
|
375
|
+
* execute: async ({ postId }) => {
|
|
376
|
+
* const result = await api.posts.like(postId);
|
|
377
|
+
* return { success: true, likeCount: result.likes };
|
|
378
|
+
* },
|
|
379
|
+
* });
|
|
380
|
+
*
|
|
381
|
+
* return <div>Likes: {likeTool.state.lastResult?.likeCount ?? 0}</div>;
|
|
382
|
+
* }
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
declare function useWebMCP<TInputSchema extends ToolInputSchema = InputSchema, TOutputSchema extends JsonSchemaObject | undefined = undefined>(config: WebMCPConfig<TInputSchema, TOutputSchema>, deps?: DependencyList): WebMCPReturn<TOutputSchema>;
|
|
386
|
+
//#endregion
|
|
387
|
+
export { type InferOutput, type InferToolInput, type ToolExecuteFunction, type ToolExecutionState, type WebMCPConfig, type WebMCPReturn, useWebMCP };
|
|
388
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/useWebMCP.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAmBA;;;;;;;AAK+B,KALnB,cAKmB,CAAA,CAAA,CAAA,GALC,CAKD,SAAA;EAAzB,SAAA,WAAA,EAAA;IACA,SAAA,KAAA,CAAA,EAAA,KAAA,MAAA;EAAM,CAAA;AAaZ,CAAA,GAlBI,KAkBQ,SAAW;EACC,SAAA,KAAA,EAAA,KAAA,EAAA;CAEpB,GApBE,CAoBF,GAnBE,MAmBF,CAAA,MAAA,EAAA,OAAA,CAAA,GAlBA,CAkBA,SAlBU,WAkBV,GAjBE,wBAiBF,CAjB2B,CAiB3B,CAAA,GAhBE,MAgBF,CAAA,MAAA,EAAA,OAAA,CAAA;;;;;;AASJ;AAoCA;;;;;AAIS,KApDG,WAoDH,CAAA,sBAnDe,gBAmDf,GAAA,SAAA,GAAA,SAAA,EAAA,YAAA,OAAA,CAAA,GAjDL,aAiDK,SAjDiB,gBAiDjB,GAjDoC,eAiDpC,CAjDoD,aAiDpD,CAAA,GAjDqE,SAiDrE;;;;;;;AAC6D;AAuD/C,UAhGN,kBAgGM,CAAA,UAAA,OAAA,CAAA,CAAA;EAAkB;;;;EAkEzB,WAAA,EAAA,OAAA;EAYsB;;;;EAkBlB,UAAA,EArLN,OAqLM,GAAA,IAAA;EAAK;AAAA;;;EAKD,KAAA,EApLf,KAoLe,GAAA,IAAA;EAMW;;;;EAIe,cAAA,EAAA,MAAA;;;;;;;;;AA2BlD;;;AAEwB,KAxMZ,mBAwMY,CAAA,qBAvMD,eAuMC,GAvMiB,WAuMjB,EAAA,sBAtMA,gBAsMA,GAAA,SAAA,GAAA,SAAA,CAAA,GAAA,CAAA,KAAA,EApMf,cAoMe,CApMA,YAoMA,CAAA,EAAA,GAnMnB,OAmMmB,CAnMX,WAmMW,CAnMC,aAmMD,CAAA,CAAA,GAnMmB,WAmMnB,CAnM+B,aAmM/B,CAAA;;;;;;;;AAWxB;;;;;;;;;;;;ACvIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UDjBU,sCACa,kBAAkB,mCACjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBAmCR;;;;;;;;;;;;;;;;;;;;;;;iBAwBC;;;;;gBAMD;;;;;;;;;;;0BAYU,YAAY;;;;;;;;uBASf,YAAY;;;;;;;;oBASf;;KAGf,gDACkB,kBAAkB,mCACjB;;;;WAMT,oBAAoB,cAAc;;;;YAIjC,oBAAoB,cAAc;;;;;WAMnC,oBAAoB,cAAc;;;;YAIjC,oBAAoB,cAAc;;;;;;;;;;;;;;;;KAiBtC,kCACW,kBAAkB,mCACjB,4CACpB,iBAAiB,cAAc,iBACjC,2BAA2B,cAAc;;;;;;;;UAS1B,mCAAmC;;;;;SAK3C,mBAAmB,YAAY;;;;;;;;;+BAUT,QAAQ,YAAY;;;;;;;;;;AArSnD;;;;;;;;;;;AAmBA;;;;;;;;AAYA;AAoCA;;;;;;;;;;;;AAKsE;;;;;;;;;;;;;AAuJ7C;;;;;;;;;;;;;;;;;AA0CzB;;;;;;;;;;;AAaA;;;;;;;;;;;;ACvIA;;;;;;;;;;;;;;;;;;iBAAgB,+BACO,kBAAkB,mCACjB,kDAEd,aAAa,cAAc,uBAC5B,iBACN,aAAa"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";import{useCallback as e,useEffect as t,useLayoutEffect as n,useRef as r,useState as i}from"react";function a(e){return typeof e==`string`?e:JSON.stringify(e,null,2)}const o=new Map;function s(e){if(!e||typeof e!=`object`||Array.isArray(e))return null;try{let t=JSON.parse(JSON.stringify(e));return!t||typeof t!=`object`||Array.isArray(t)?null:t}catch{return null}}const c=typeof window<`u`?n:t;function l(){let e=globalThis.process?.env?.NODE_ENV;return e===void 0?!1:e!==`production`}function u(n,u){let{name:d,description:f,inputSchema:p,outputSchema:m,annotations:h,execute:g,handler:_,formatOutput:v=a,onSuccess:y,onError:b}=n,x=g??_;if(!x)throw TypeError(`[useWebMCP] Tool "${d}" must provide an implementation via config.execute or config.handler`);let[S,C]=i({isExecuting:!1,lastResult:null,error:null,executionCount:0}),w=r(x),T=r(y),E=r(b),D=r(v),O=r(!0),k=r(new Set),A=r({inputSchema:p,outputSchema:m,annotations:h,description:f,deps:u});c(()=>{w.current=x,T.current=y,E.current=b,D.current=v},[x,y,b,v]),t(()=>(O.current=!0,()=>{O.current=!1}),[]),t(()=>{if(!l()){A.current={inputSchema:p,outputSchema:m,annotations:h,description:f,deps:u};return}let e=(e,t)=>{k.current.has(e)||(console.warn(`[useWebMCP] ${t}`),k.current.add(e))},t=A.current;p&&t.inputSchema&&t.inputSchema!==p&&e(`inputSchema`,`Tool "${d}" inputSchema reference changed; memoize or define it outside the component to avoid re-registration.`),m&&t.outputSchema&&t.outputSchema!==m&&e(`outputSchema`,`Tool "${d}" outputSchema reference changed; memoize or define it outside the component to avoid re-registration.`),h&&t.annotations&&t.annotations!==h&&e(`annotations`,`Tool "${d}" annotations reference changed; memoize or define it outside the component to avoid re-registration.`),f!==t.description&&e(`description`,`Tool "${d}" description changed; this re-registers the tool. Memoize the description if it does not need to update.`),u?.some(e=>typeof e==`object`&&!!e||typeof e==`function`)&&e(`deps`,`Tool "${d}" deps contains non-primitive values; prefer primitives or memoize objects/functions to reduce re-registration.`),A.current={inputSchema:p,outputSchema:m,annotations:h,description:f,deps:u}},[h,u,f,p,d,m]);let j=e(async e=>{C(e=>({...e,isExecuting:!0,error:null}));try{let t=await w.current(e);return O.current&&C(e=>({isExecuting:!1,lastResult:t,error:null,executionCount:e.executionCount+1})),T.current&&T.current(t,e),t}catch(t){let n=t instanceof Error?t:Error(String(t));throw O.current&&C(e=>({...e,isExecuting:!1,error:n})),E.current&&E.current(n,e),n}},[]),M=r(j);t(()=>{M.current=j},[j]);let N=e(e=>M.current(e),[]),P=e(()=>{C({isExecuting:!1,lastResult:null,error:null,executionCount:0})},[]);return t(()=>{if(typeof window>`u`||!window.navigator?.modelContext){console.warn(`[useWebMCP] window.navigator.modelContext is not available. Tool "${d}" will not be registered.`);return}let e=async e=>{try{let t=await M.current(e),n={content:[{type:`text`,text:D.current(t)}]};if(m){let e=s(t);if(!e)throw Error(`Tool "${d}" outputSchema requires the tool implementation to return a JSON object result`);n.structuredContent=e}return n}catch(e){return{content:[{type:`text`,text:`Error: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}},t=Symbol(d),n=window.navigator.modelContext;return n.registerTool({name:d,description:f,...p&&{inputSchema:p},...m&&{outputSchema:m},...h&&{annotations:h},execute:e}),o.set(d,t),()=>{if(o.get(d)===t){o.delete(d);try{n.unregisterTool(d)}catch(e){l()&&console.warn(`[useWebMCP] Failed to unregister tool "${d}" during cleanup:`,e)}}}},[d,f,p,m,h,...u??[]]),{state:S,execute:N,reset:P}}export{u as useWebMCP};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["response: CallToolResult"],"sources":["../src/useWebMCP.ts"],"sourcesContent":["import type { ToolInputSchema } from '@mcp-b/webmcp-polyfill';\nimport type {\n CallToolResult,\n InputSchema,\n JsonSchemaObject,\n ToolDescriptor,\n} from '@mcp-b/webmcp-types';\nimport type { DependencyList } from 'react';\nimport { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport type {\n InferOutput,\n InferToolInput,\n ToolExecutionState,\n WebMCPConfig,\n WebMCPReturn,\n} from './types.js';\n\n/**\n * Default output formatter that converts values to formatted JSON strings.\n *\n * String values are returned as-is; all other types are serialized to\n * indented JSON for readability.\n *\n * @internal\n */\nfunction defaultFormatOutput(output: unknown): string {\n if (typeof output === 'string') {\n return output;\n }\n return JSON.stringify(output, null, 2);\n}\n\nconst TOOL_OWNER_BY_NAME = new Map<string, symbol>();\ntype StructuredContent = Exclude<CallToolResult['structuredContent'], undefined>;\n\nfunction toStructuredContent(value: unknown): StructuredContent | null {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return null;\n }\n\n try {\n const normalized = JSON.parse(JSON.stringify(value)) as unknown;\n if (!normalized || typeof normalized !== 'object' || Array.isArray(normalized)) {\n return null;\n }\n return normalized as StructuredContent;\n } catch {\n return null;\n }\n}\n\nconst useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;\n\nfunction isDev(): boolean {\n const env = (globalThis as { process?: { env?: { NODE_ENV?: string } } }).process?.env?.NODE_ENV;\n return env !== undefined ? env !== 'production' : false;\n}\n\n/**\n * React hook for registering and managing Model Context Protocol (MCP) tools.\n *\n * This hook handles the complete lifecycle of an MCP tool:\n * - Registers the tool with `window.navigator.modelContext`\n * - Manages execution state (loading, results, errors)\n * - Handles tool execution and lifecycle callbacks\n * - Automatically unregisters on component unmount\n * - Returns `structuredContent` when `outputSchema` is defined\n *\n * ## Output Schema (Recommended)\n *\n * Always define an `outputSchema` for your tools. This provides:\n * - **Type Safety**: Handler return type is inferred from the schema\n * - **MCP structuredContent**: AI models receive structured, typed data\n * - **Better AI Understanding**: Models can reason about your tool's output format\n *\n * ```tsx\n * useWebMCP({\n * name: 'get_user',\n * description: 'Get user by ID',\n * inputSchema: {\n * type: 'object',\n * properties: { userId: { type: 'string' } },\n * required: ['userId'],\n * } as const,\n * outputSchema: {\n * type: 'object',\n * properties: {\n * id: { type: 'string' },\n * name: { type: 'string' },\n * email: { type: 'string' },\n * },\n * } as const,\n * execute: async ({ userId }) => {\n * const user = await fetchUser(userId);\n * return { id: user.id, name: user.name, email: user.email };\n * },\n * });\n * ```\n *\n * ## Re-render Optimization\n *\n * This hook is optimized to minimize unnecessary tool re-registrations:\n *\n * - **Ref-based callbacks**: `execute`/`handler`, `onSuccess`, `onError`, and `formatOutput`\n * are stored in refs, so changing these functions won't trigger re-registration.\n *\n * **IMPORTANT**: If `inputSchema`, `outputSchema`, or `annotations` are defined inline\n * or change on every render, the tool will re-register unnecessarily. To avoid this,\n * define them outside your component with `as const`:\n *\n * ```tsx\n * // Good: Static schema defined outside component\n * const OUTPUT_SCHEMA = {\n * type: 'object',\n * properties: { count: { type: 'number' } },\n * } as const;\n *\n * // Bad: Inline schema (creates new object every render)\n * useWebMCP({\n * outputSchema: { type: 'object', properties: { count: { type: 'number' } } } as const,\n * });\n * ```\n *\n * @template TInputSchema - JSON Schema defining input parameter types (use `as const` for inference)\n * @template TOutputSchema - JSON Schema object defining output structure (enables structuredContent)\n *\n * @param config - Configuration object for the tool\n * @param deps - Optional dependency array that triggers tool re-registration when values change.\n *\n * @returns Object containing execution state and control methods\n *\n * @public\n *\n * @example\n * Basic tool with outputSchema (recommended):\n * ```tsx\n * function PostActions() {\n * const likeTool = useWebMCP({\n * name: 'posts_like',\n * description: 'Like a post by ID',\n * inputSchema: {\n * type: 'object',\n * properties: { postId: { type: 'string', description: 'The post ID' } },\n * required: ['postId'],\n * } as const,\n * outputSchema: {\n * type: 'object',\n * properties: {\n * success: { type: 'boolean' },\n * likeCount: { type: 'number' },\n * },\n * } as const,\n * execute: async ({ postId }) => {\n * const result = await api.posts.like(postId);\n * return { success: true, likeCount: result.likes };\n * },\n * });\n *\n * return <div>Likes: {likeTool.state.lastResult?.likeCount ?? 0}</div>;\n * }\n * ```\n */\nexport function useWebMCP<\n TInputSchema extends ToolInputSchema = InputSchema,\n TOutputSchema extends JsonSchemaObject | undefined = undefined,\n>(\n config: WebMCPConfig<TInputSchema, TOutputSchema>,\n deps?: DependencyList\n): WebMCPReturn<TOutputSchema> {\n type TOutput = InferOutput<TOutputSchema>;\n type TInput = InferToolInput<TInputSchema>;\n const {\n name,\n description,\n inputSchema,\n outputSchema,\n annotations,\n execute: configExecute,\n handler: legacyHandler,\n formatOutput = defaultFormatOutput,\n onSuccess,\n onError,\n } = config;\n const toolExecute = configExecute ?? legacyHandler;\n\n if (!toolExecute) {\n throw new TypeError(\n `[useWebMCP] Tool \"${name}\" must provide an implementation via config.execute or config.handler`\n );\n }\n\n const [state, setState] = useState<ToolExecutionState<TOutput>>({\n isExecuting: false,\n lastResult: null,\n error: null,\n executionCount: 0,\n });\n\n const toolExecuteRef = useRef(toolExecute);\n const onSuccessRef = useRef(onSuccess);\n const onErrorRef = useRef(onError);\n const formatOutputRef = useRef(formatOutput);\n const isMountedRef = useRef(true);\n const warnedRef = useRef(new Set<string>());\n const prevConfigRef = useRef({\n inputSchema,\n outputSchema,\n annotations,\n description,\n deps,\n });\n // Update refs when callbacks change (doesn't trigger re-registration)\n useIsomorphicLayoutEffect(() => {\n toolExecuteRef.current = toolExecute;\n onSuccessRef.current = onSuccess;\n onErrorRef.current = onError;\n formatOutputRef.current = formatOutput;\n }, [toolExecute, onSuccess, onError, formatOutput]);\n\n // Cleanup: mark component as unmounted\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (!isDev()) {\n prevConfigRef.current = { inputSchema, outputSchema, annotations, description, deps };\n return;\n }\n\n const warnOnce = (key: string, message: string) => {\n if (warnedRef.current.has(key)) {\n return;\n }\n console.warn(`[useWebMCP] ${message}`);\n warnedRef.current.add(key);\n };\n\n const prev = prevConfigRef.current;\n\n if (inputSchema && prev.inputSchema && prev.inputSchema !== inputSchema) {\n warnOnce(\n 'inputSchema',\n `Tool \"${name}\" inputSchema reference changed; memoize or define it outside the component to avoid re-registration.`\n );\n }\n\n if (outputSchema && prev.outputSchema && prev.outputSchema !== outputSchema) {\n warnOnce(\n 'outputSchema',\n `Tool \"${name}\" outputSchema reference changed; memoize or define it outside the component to avoid re-registration.`\n );\n }\n\n if (annotations && prev.annotations && prev.annotations !== annotations) {\n warnOnce(\n 'annotations',\n `Tool \"${name}\" annotations reference changed; memoize or define it outside the component to avoid re-registration.`\n );\n }\n\n if (description !== prev.description) {\n warnOnce(\n 'description',\n `Tool \"${name}\" description changed; this re-registers the tool. Memoize the description if it does not need to update.`\n );\n }\n\n if (\n deps?.some(\n (value) => (typeof value === 'object' && value !== null) || typeof value === 'function'\n )\n ) {\n warnOnce(\n 'deps',\n `Tool \"${name}\" deps contains non-primitive values; prefer primitives or memoize objects/functions to reduce re-registration.`\n );\n }\n\n prevConfigRef.current = { inputSchema, outputSchema, annotations, description, deps };\n }, [annotations, deps, description, inputSchema, name, outputSchema]);\n\n /**\n * Executes the configured tool implementation with input validation and state management.\n *\n * @param input - The input parameters to validate and pass to the tool implementation\n * @returns Promise resolving to the tool output\n * @throws Error if validation fails or the tool implementation throws\n */\n const execute = useCallback(async (input: unknown): Promise<TOutput> => {\n setState((prev) => ({\n ...prev,\n isExecuting: true,\n error: null,\n }));\n\n try {\n const result = await toolExecuteRef.current(input as TInput);\n\n // Only update state if component is still mounted\n if (isMountedRef.current) {\n setState((prev) => ({\n isExecuting: false,\n lastResult: result,\n error: null,\n executionCount: prev.executionCount + 1,\n }));\n }\n\n if (onSuccessRef.current) {\n onSuccessRef.current(result, input);\n }\n\n return result;\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n\n // Only update state if component is still mounted\n if (isMountedRef.current) {\n setState((prev) => ({\n ...prev,\n isExecuting: false,\n error: err,\n }));\n }\n\n if (onErrorRef.current) {\n onErrorRef.current(err, input);\n }\n\n throw err;\n }\n }, []);\n const executeRef = useRef(execute);\n\n useEffect(() => {\n executeRef.current = execute;\n }, [execute]);\n\n const stableExecute = useCallback(\n (input: unknown): Promise<TOutput> => executeRef.current(input),\n []\n );\n\n /**\n * Resets the execution state to initial values.\n */\n const reset = useCallback(() => {\n setState({\n isExecuting: false,\n lastResult: null,\n error: null,\n executionCount: 0,\n });\n }, []);\n\n useEffect(() => {\n if (typeof window === 'undefined' || !window.navigator?.modelContext) {\n console.warn(\n `[useWebMCP] window.navigator.modelContext is not available. Tool \"${name}\" will not be registered.`\n );\n return;\n }\n\n /**\n * Handles MCP tool execution by running the tool implementation and formatting the response.\n *\n * @param input - The input parameters from the MCP client\n * @returns CallToolResult with text content and optional structuredContent\n */\n const mcpHandler = async (input: unknown): Promise<CallToolResult> => {\n try {\n const result = await executeRef.current(input);\n const formattedOutput = formatOutputRef.current(result);\n\n const response: CallToolResult = {\n content: [\n {\n type: 'text',\n text: formattedOutput,\n },\n ],\n };\n\n if (outputSchema) {\n const structuredContent = toStructuredContent(result);\n if (!structuredContent) {\n throw new Error(\n `Tool \"${name}\" outputSchema requires the tool implementation to return a JSON object result`\n );\n }\n response.structuredContent = structuredContent;\n }\n\n return response;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n return {\n content: [\n {\n type: 'text',\n text: `Error: ${errorMessage}`,\n },\n ],\n isError: true,\n };\n }\n };\n\n const ownerToken = Symbol(name);\n const modelContext = window.navigator.modelContext;\n\n (modelContext.registerTool as (tool: ToolDescriptor) => void)({\n name,\n description,\n ...(inputSchema && { inputSchema: inputSchema as InputSchema }),\n ...(outputSchema && { outputSchema: outputSchema as InputSchema }),\n ...(annotations && { annotations }),\n execute: mcpHandler,\n });\n TOOL_OWNER_BY_NAME.set(name, ownerToken);\n\n return () => {\n const currentOwner = TOOL_OWNER_BY_NAME.get(name);\n if (currentOwner !== ownerToken) {\n return;\n }\n\n TOOL_OWNER_BY_NAME.delete(name);\n try {\n modelContext.unregisterTool(name);\n } catch (error) {\n if (isDev()) {\n console.warn(`[useWebMCP] Failed to unregister tool \"${name}\" during cleanup:`, error);\n }\n }\n };\n // Spread operator in dependencies: Allows users to provide additional dependencies\n // via the `deps` parameter. While unconventional, this pattern is intentional to support\n // dynamic dependency injection. The spread is safe because deps is validated and warned\n // about non-primitive values earlier in this hook.\n }, [name, description, inputSchema, outputSchema, annotations, ...(deps ?? [])]);\n\n return {\n state,\n execute: stableExecute,\n reset,\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usewebmcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "React hooks for
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Standalone React hooks for strict core WebMCP tool registration with navigator.modelContext",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
7
7
|
"model-context-protocol",
|
|
@@ -12,16 +12,15 @@
|
|
|
12
12
|
"ai",
|
|
13
13
|
"assistant",
|
|
14
14
|
"tools",
|
|
15
|
-
"zod",
|
|
16
15
|
"usewebmcp"
|
|
17
16
|
],
|
|
18
|
-
"homepage": "https://github.com/WebMCP-org/
|
|
17
|
+
"homepage": "https://github.com/WebMCP-org/npm-packages#readme",
|
|
19
18
|
"bugs": {
|
|
20
|
-
"url": "https://github.com/WebMCP-org/
|
|
19
|
+
"url": "https://github.com/WebMCP-org/npm-packages/issues"
|
|
21
20
|
},
|
|
22
21
|
"repository": {
|
|
23
22
|
"type": "git",
|
|
24
|
-
"url": "git+https://github.com/WebMCP-org/
|
|
23
|
+
"url": "git+https://github.com/WebMCP-org/npm-packages.git",
|
|
25
24
|
"directory": "packages/usewebmcp"
|
|
26
25
|
},
|
|
27
26
|
"license": "MIT",
|
|
@@ -38,17 +37,26 @@
|
|
|
38
37
|
"dist"
|
|
39
38
|
],
|
|
40
39
|
"dependencies": {
|
|
41
|
-
"@mcp-b/
|
|
40
|
+
"@mcp-b/webmcp-polyfill": "0.2.0",
|
|
41
|
+
"@mcp-b/webmcp-types": "0.2.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "22.17.2",
|
|
45
|
-
"@types/react": "^19.
|
|
45
|
+
"@types/react": "^19.2.9",
|
|
46
|
+
"@vitest/browser": "^4.0.18",
|
|
47
|
+
"@vitest/browser-playwright": "^4.0.18",
|
|
48
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
49
|
+
"playwright": "^1.58.0",
|
|
50
|
+
"react": "^19.1.0",
|
|
51
|
+
"react-dom": "^19.1.0",
|
|
46
52
|
"tsdown": "^0.15.10",
|
|
47
|
-
"typescript": "^5.8.3"
|
|
53
|
+
"typescript": "^5.8.3",
|
|
54
|
+
"vitest": "^4.0.18",
|
|
55
|
+
"vitest-browser-react": "^2.0.4",
|
|
56
|
+
"@mcp-b/global": "0.0.0-beta-20260217171247"
|
|
48
57
|
},
|
|
49
58
|
"peerDependencies": {
|
|
50
|
-
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
51
|
-
"zod": "^3.25.0"
|
|
59
|
+
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
52
60
|
},
|
|
53
61
|
"publishConfig": {
|
|
54
62
|
"access": "public",
|
|
@@ -63,6 +71,8 @@
|
|
|
63
71
|
"lint": "biome lint --write .",
|
|
64
72
|
"publish:dry": "pnpm publish --access public --dry-run",
|
|
65
73
|
"publish:npm": "pnpm publish --access public",
|
|
66
|
-
"
|
|
74
|
+
"test": "vitest run",
|
|
75
|
+
"test:watch": "vitest",
|
|
76
|
+
"typecheck": "tsc --noEmit && vitest run --typecheck --silent"
|
|
67
77
|
}
|
|
68
78
|
}
|