usewebmcp 4.0.0 → 5.0.0

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 CHANGED
@@ -1,56 +1,21 @@
1
1
  # usewebmcp
2
2
 
3
- Standalone React hooks for strict core WebMCP tool registration via `document.modelContext`.
3
+ React hook for registering strict-core WebMCP tools with `document.modelContext`.
4
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
- ## Type Safety First
11
-
12
- `usewebmcp` is built for schema-driven types:
13
-
14
- - `config.execute`/`config.handler` input is inferred from `inputSchema`
15
- - Tool result type is inferred from `outputSchema`
16
- - `state.lastResult` and returned `execute(input)` are typed from that same inferred output
17
-
18
- ## Package Selection
19
-
20
- | Package | Use When |
21
- | ------------------------ | --------------------------------------------------------- |
22
- | `usewebmcp` | React hooks for strict core `document.modelContext` tools |
23
- | `@mcp-b/react-webmcp` | React hooks for full MCP-B runtime surface |
24
- | `@mcp-b/webmcp-polyfill` | You need a strict core runtime polyfill |
25
- | `@mcp-b/global` | You need full MCP-B runtime (core + extensions) |
5
+ Use `usewebmcp` when an app only needs standard tool registration. Use
6
+ `@mcp-b/react-webmcp` for prompts, resources, or MCP client/provider flows.
26
7
 
27
8
  ## Install
28
9
 
29
10
  ```bash
30
11
  pnpm add usewebmcp react
31
- # or
32
- npm install usewebmcp react
33
12
  ```
34
13
 
35
- Optional (only if you want Standard Schema authoring like Zod v4 input schemas):
14
+ The hook expects `document.modelContext` to exist before the component mounts. Provide it through
15
+ native browser support, `@mcp-b/webmcp-polyfill`, or `@mcp-b/global`. Older
16
+ `navigator.modelContext` runtimes remain a fallback.
36
17
 
37
- ```bash
38
- pnpm add zod
39
- ```
40
-
41
- ## Runtime Prerequisite
42
-
43
- `usewebmcp` expects `window.document.modelContext` to exist.
44
-
45
- You can provide it via:
46
-
47
- - Browser-native WebMCP implementation, or
48
- - `@mcp-b/webmcp-polyfill`, or
49
- - `@mcp-b/global`
50
-
51
- If `document.modelContext` is missing, the hook falls back to older preview `navigator.modelContext` runtimes. If both are missing, the hook logs a warning and skips registration.
52
-
53
- ## Quick Start
18
+ ## Quick start
54
19
 
55
20
  ```tsx
56
21
  import { initializeWebMCPPolyfill } from '@mcp-b/webmcp-polyfill';
@@ -58,221 +23,85 @@ import { useWebMCP } from 'usewebmcp';
58
23
 
59
24
  initializeWebMCPPolyfill();
60
25
 
61
- const COUNTER_INPUT_SCHEMA = {
62
- type: 'object',
63
- properties: {},
64
- } as const;
65
-
66
- const COUNTER_OUTPUT_SCHEMA = {
67
- type: 'object',
68
- properties: {
69
- count: { type: 'integer' },
70
- },
71
- required: ['count'],
72
- additionalProperties: false,
73
- } as const;
74
-
75
- export function CounterTool() {
76
- const counterTool = useWebMCP({
77
- name: 'counter_get',
78
- description: 'Get current count',
79
- inputSchema: COUNTER_INPUT_SCHEMA,
80
- outputSchema: COUNTER_OUTPUT_SCHEMA,
81
- execute: async () => ({ count: 42 }),
82
- });
83
-
84
- return (
85
- <div>
86
- <p>Executions: {counterTool.state.executionCount}</p>
87
- <p>Last count: {counterTool.state.lastResult?.count ?? 'none'}</p>
88
- {counterTool.state.error && <p>Error: {counterTool.state.error.message}</p>}
89
- <button
90
- onClick={async () => {
91
- await counterTool.execute({});
92
- }}
93
- >
94
- Run Tool Locally
95
- </button>
96
- </div>
97
- );
98
- }
99
- ```
100
-
101
- ## How `useWebMCP` Works
102
-
103
- - Registers a tool on mount with `document.modelContext.registerTool(tool, { signal })` and aborts the controller on unmount.
104
- - Older Chrome preview builds may not remove tools when the controller aborts. Install `@mcp-b/global` or `@mcp-b/webmcp-polyfill` when you need consistent cleanup across preview versions.
105
- - Exposes local execution state:
106
- - `state.isExecuting`
107
- - `state.lastResult`
108
- - `state.error`
109
- - `state.executionCount`
110
- - Returns `execute(input)` for manual in-app invocation and `reset()` for state reset.
111
-
112
- Your tool implementation (`config.execute` or `config.handler`) can be synchronous or asynchronous.
113
-
114
- ## `config.execute` vs returned `execute(...)`
115
-
116
- - `config.execute`: preferred config field for tool logic.
117
- - `config.handler`: backward-compatible alias for `config.execute`.
118
- - returned `execute(input)`: hook return function for local manual invocation from your UI/tests.
119
-
120
- If both `config.execute` and `config.handler` are provided, `config.execute` is used.
121
-
122
- Both paths run the same underlying tool logic and update the hook state.
123
-
124
- ## Type Inference
125
-
126
- ### Input inference
127
-
128
- `inputSchema` supports:
129
-
130
- - JSON Schema literals (`as const`) via `InferArgsFromInputSchema`
131
- - Standard Schema v1 input typing (for example Zod v4 / Valibot / ArkType) via `~standard.types.input`
132
-
133
- ```tsx
134
26
  const INPUT_SCHEMA = {
135
27
  type: 'object',
136
- properties: {
137
- query: { type: 'string' },
138
- limit: { type: 'integer' },
139
- },
28
+ properties: { query: { type: 'string' } },
140
29
  required: ['query'],
141
- additionalProperties: false,
142
30
  } as const;
143
31
 
144
- useWebMCP({
145
- name: 'search',
146
- description: 'Search docs',
147
- inputSchema: INPUT_SCHEMA,
148
- execute(input) {
149
- // input is inferred as { query: string; limit?: number }
150
- return { total: 1 };
151
- },
152
- });
153
- ```
154
-
155
- ### Output inference
156
-
157
- When `outputSchema` is provided as a literal JSON Schema:
158
-
159
- - implementation return type is inferred from `outputSchema`
160
- - `state.lastResult` is inferred to the same type
161
- - MCP response includes `structuredContent`
162
-
163
- ```tsx
164
32
  const OUTPUT_SCHEMA = {
165
33
  type: 'object',
166
- properties: {
167
- total: { type: 'integer' },
168
- },
34
+ properties: { total: { type: 'integer' } },
169
35
  required: ['total'],
170
- additionalProperties: false,
171
36
  } as const;
172
37
 
173
- const tool = useWebMCP({
174
- name: 'count_items',
175
- description: 'Count items',
176
- outputSchema: OUTPUT_SCHEMA,
177
- execute: () => ({ total: 3 }),
178
- });
179
-
180
- // tool.state.lastResult is inferred as { total: number } | null
181
- ```
182
-
183
- ## Manual `execute(...)` Calls
184
-
185
- You can call the returned `execute(...)` function directly from your component.
186
-
187
- ```tsx
188
- function SearchToolPanel() {
189
- const searchTool = useWebMCP({
190
- name: 'search_local',
191
- description: 'Run local search',
192
- inputSchema: {
193
- type: 'object',
194
- properties: { query: { type: 'string' } },
195
- required: ['query'],
196
- additionalProperties: false,
197
- } as const,
198
- execute: async ({ query }) => ({ query, total: query.length }),
38
+ export function SearchTool() {
39
+ const search = useWebMCP({
40
+ name: 'search',
41
+ description: 'Search indexed documents',
42
+ inputSchema: INPUT_SCHEMA,
43
+ outputSchema: OUTPUT_SCHEMA,
44
+ execute: async ({ query }) => ({ total: await countMatches(query) }),
199
45
  });
200
46
 
201
47
  return (
202
- <button
203
- onClick={async () => {
204
- await searchTool.execute({ query: 'webmcp' });
205
- }}
206
- >
207
- Run Search
48
+ <button disabled={search.state.isExecuting} onClick={() => search.execute({ query: 'webmcp' })}>
49
+ Run search ({search.state.executionCount})
208
50
  </button>
209
51
  );
210
52
  }
211
53
  ```
212
54
 
213
- ## Output Schema Contract
214
-
215
- If `outputSchema` is defined, your tool implementation must return a JSON-serializable value that matches that schema for MCP-B helpers that consume it. Object, array, string, number, boolean, and null schemas are supported for type inference and hook result shaping.
55
+ JSON Schema literals infer the `execute` input, output, returned `execute(input)`, and
56
+ `state.lastResult` types. Standard JSON Schema v1 inputs such as Zod 4.2+ are also supported.
216
57
 
217
- Native Chrome WebMCP does not currently define or enforce `outputSchema`; the browser standard tool dictionary defines `inputSchema`.
218
-
219
- ## Re-Registration and Performance
220
-
221
- The tool re-registers when any of these change:
222
-
223
- - `name`
224
- - `description`
225
- - `inputSchema` reference
226
- - `outputSchema` reference
227
- - `annotations` reference
228
- - values in `deps`
229
-
230
- The hook avoids re-registration when only callback references change:
58
+ ## API
231
59
 
232
- - `execute`
233
- - `handler`
234
- - `onSuccess`
235
- - `onError`
236
- - `formatOutput`
60
+ ```ts
61
+ const { state, execute, reset } = useWebMCP(config, deps?);
62
+ ```
237
63
 
238
- Latest callback versions are still used at execution time.
64
+ `config` contains:
239
65
 
240
- Recommendation:
66
+ - `name` and `description`
67
+ - optional `inputSchema`, `outputSchema`, and `annotations`
68
+ - `execute(input)`, which may return synchronously or asynchronously
241
69
 
242
- - Define schemas/annotations outside render or memoize them.
243
- - Keep `deps` primitive when possible.
70
+ `state` contains `isExecuting`, `lastResult`, `error`, and `executionCount`. The returned
71
+ `execute(input)` invokes the same implementation locally. `reset()` clears observed state; it does
72
+ not cancel work already running.
244
73
 
245
- ## API
74
+ ## Tool responses
246
75
 
247
- ### `useWebMCP(config, deps?)`
76
+ Raw return values use the same normalization as the MCP-B runtime:
248
77
 
249
- `config` fields:
78
+ - Existing MCP responses with a `content` array pass through unchanged.
79
+ - Strings become text content.
80
+ - JSON values become text content plus `structuredContent`.
81
+ - Other values receive a safe text representation.
250
82
 
251
- - `name: string`
252
- - `description: string`
253
- - `inputSchema?`
254
- - `outputSchema?`
255
- - `annotations?`
256
- - `execute(input)` (preferred)
257
- - `handler(input)` (backward-compatible alias)
258
- - `formatOutput?(output)` (deprecated)
259
- - `onSuccess?(result, input)`
260
- - `onError?(error, input)`
83
+ When `outputSchema` is present, a non-JSON-serializable result rejects local execution and becomes
84
+ an MCP error response. Native Chrome WebMCP does not advertise `outputSchema`; it is MCP-B metadata
85
+ for output inference and compatible runtimes.
261
86
 
262
- Return value:
87
+ ## Re-registration
263
88
 
264
- - `state`
265
- - `execute(input)`
266
- - `reset()`
89
+ The tool re-registers when `name`, `description`, or a value in `deps` changes. Implementations,
90
+ schemas, and annotations use their latest committed values without reference-driven churn. Include
91
+ a primitive schema or annotation revision in `deps` when registered metadata must change.
267
92
 
268
- `execute(input)` is a local direct call to your configured tool implementation for in-app control/testing.
269
- Tool calls coming from MCP clients still go through the page `document.modelContext` surface.
93
+ Registration is aborted on unmount. If the runtime is missing at mount, the hook warns and skips
94
+ registration; initialize the runtime first rather than polling once per hook.
270
95
 
271
- ## Important Notes
96
+ ## Exports
272
97
 
273
- - This is a client-side hook package (`'use client'`).
274
- - `formatOutput` is deprecated; prefer `outputSchema` + structured output.
275
- - When tool output is not a string, default text content is pretty-printed JSON.
98
+ - `useWebMCP`
99
+ - `WebMCPConfig`
100
+ - `WebMCPReturn`
101
+ - `ToolExecutionState`
102
+ - `ToolExecuteFunction`
103
+ - `InferToolInput`
104
+ - `InferOutput`
276
105
 
277
106
  ## License
278
107
 
package/dist/index.d.ts CHANGED
@@ -3,377 +3,53 @@ import { DependencyList } from "react";
3
3
  import { InferArgsFromInputSchema, InferJsonSchema, InputSchema, JsonSchemaForInference, ToolAnnotations } from "@mcp-b/webmcp-types";
4
4
 
5
5
  //#region src/types.d.ts
6
- /**
7
- * Infers tool input type from either a Standard Schema or JSON Schema.
8
- *
9
- * - **Standard Schema** (Zod v4, Valibot, ArkType): extracts `~standard.types.input`
10
- * - **JSON Schema** (`as const`): uses `InferArgsFromInputSchema` for structural inference
11
- * - **Fallback**: `Record<string, unknown>`
12
- *
13
- * @template T - The input schema type
14
- * @internal
15
- */
6
+ /** Infers tool input from a JSON Schema literal or Standard JSON Schema. */
16
7
  type InferToolInput<T> = T extends {
17
8
  readonly '~standard': {
18
9
  readonly types?: infer Types;
19
10
  };
20
- } ? Types extends {
21
- readonly input: infer I;
22
- } ? I : Record<string, unknown> : T extends InputSchema ? InferArgsFromInputSchema<T> : Record<string, unknown>;
23
- /**
24
- * Utility type to infer the output type from a JSON Schema.
25
- *
26
- * When `TOutputSchema` is a literal inferable JSON schema, this resolves to
27
- * `InferJsonSchema<TOutputSchema>`. When it's `undefined`,
28
- * it falls back to the provided `TFallback` type.
29
- *
30
- * @template TOutputSchema - JSON Schema for output inference
31
- * @template TFallback - Fallback type when no schema is provided
32
- * @internal
33
- */
34
- type InferOutput<TOutputSchema extends JsonSchemaForInference | undefined = undefined, TFallback = unknown> = TOutputSchema extends undefined ? TFallback : TOutputSchema extends JsonSchemaForInference ? InferJsonSchema<TOutputSchema> : TFallback;
35
- /**
36
- * Represents the current execution state of a tool, including loading status,
37
- * results, errors, and execution history.
38
- *
39
- * @template TOutput - The type of data returned by the tool implementation
40
- * @public
41
- */
11
+ } ? NonNullable<Types> extends {
12
+ readonly input: infer Input;
13
+ } ? Input : Record<string, unknown> : T extends InputSchema ? InferArgsFromInputSchema<T> : Record<string, unknown>;
14
+ /** Infers tool output from a JSON Schema literal. */
15
+ type InferOutput<TOutputSchema extends JsonSchemaForInference | undefined = undefined> = TOutputSchema extends undefined ? unknown : TOutputSchema extends JsonSchemaForInference ? InferJsonSchema<TOutputSchema> : unknown;
16
+ /** Current state for local and MCP-triggered tool executions. */
42
17
  interface ToolExecutionState<TOutput = unknown> {
43
- /**
44
- * Indicates whether the tool is currently executing.
45
- * Use this to show loading states in your UI.
46
- */
18
+ /** Whether at least one execution is pending. */
47
19
  isExecuting: boolean;
48
- /**
49
- * The result from the most recent successful execution.
50
- * Will be `null` if the tool hasn't been executed or last execution failed.
51
- */
20
+ /** Most recent successful result, or `null` before one exists. */
52
21
  lastResult: TOutput | null;
53
- /**
54
- * The error from the most recent failed execution.
55
- * Will be `null` if the tool hasn't been executed or last execution succeeded.
56
- */
22
+ /** Most recent execution error. */
57
23
  error: Error | null;
58
- /**
59
- * Total number of times this tool has been executed.
60
- * Increments on successful executions only.
61
- */
24
+ /** Number of successful executions since the last reset. */
62
25
  executionCount: number;
63
26
  }
64
- /**
65
- * Tool implementation function used by `useWebMCP`.
66
- *
67
- * Supports sync or async implementations.
68
- *
69
- * @template TInputSchema - Schema defining input parameters
70
- * @template TOutputSchema - Optional JSON Schema defining output structure
71
- *
72
- * @public
73
- */
27
+ /** Synchronous or asynchronous tool implementation. */
74
28
  type ToolExecuteFunction<TInputSchema extends ToolInputSchema = InputSchema, TOutputSchema extends JsonSchemaForInference | undefined = undefined> = (input: InferToolInput<TInputSchema>) => Promise<InferOutput<TOutputSchema>> | InferOutput<TOutputSchema>;
75
- /**
76
- * Shared configuration fields for the `useWebMCP` hook.
77
- *
78
- * Defines a tool's metadata, schema, and lifecycle callbacks.
79
- * Uses JSON Schema for type inference via `as const`.
80
- *
81
- * @template TInputSchema - JSON Schema defining input parameters
82
- * @template TOutputSchema - JSON Schema defining output structure
83
- *
84
- * @public
85
- *
86
- * @example Basic tool without output schema:
87
- * ```typescript
88
- * const config: WebMCPConfig = {
89
- * name: 'posts_like',
90
- * description: 'Like a post by its ID',
91
- * inputSchema: {
92
- * type: 'object',
93
- * properties: { postId: { type: 'string' } },
94
- * required: ['postId'],
95
- * } as const,
96
- * execute: async ({ postId }) => {
97
- * await api.likePost(postId);
98
- * return { success: true };
99
- * },
100
- * };
101
- * ```
102
- *
103
- * @example Tool with output schema (enables MCP structuredContent):
104
- * ```typescript
105
- * useWebMCP({
106
- * name: 'posts_like',
107
- * description: 'Like a post and return updated like count',
108
- * inputSchema: {
109
- * type: 'object',
110
- * properties: { postId: { type: 'string' } },
111
- * required: ['postId'],
112
- * } as const,
113
- * outputSchema: {
114
- * type: 'object',
115
- * properties: {
116
- * likes: { type: 'number', description: 'Updated like count' },
117
- * likedAt: { type: 'string', description: 'ISO timestamp of the like' },
118
- * },
119
- * } as const,
120
- * execute: async ({ postId }) => {
121
- * const result = await api.likePost(postId);
122
- * return { likes: result.likes, likedAt: new Date().toISOString() };
123
- * },
124
- * });
125
- * ```
126
- */
127
- interface WebMCPConfigBase<TInputSchema extends ToolInputSchema = InputSchema, TOutputSchema extends JsonSchemaForInference | undefined = undefined> {
128
- /**
129
- * Unique identifier for the tool (e.g., 'posts_like', 'graph_navigate').
130
- * Must follow naming conventions: lowercase with underscores.
131
- */
29
+ /** Configuration for a tool registered by `useWebMCP`. */
30
+ interface WebMCPConfig<TInputSchema extends ToolInputSchema = InputSchema, TOutputSchema extends JsonSchemaForInference | undefined = undefined> {
31
+ /** Unique WebMCP tool name. */
132
32
  name: string;
133
- /**
134
- * Human-readable description explaining what the tool does.
135
- * This description is used by AI assistants to understand when to use the tool.
136
- */
33
+ /** Description shown to MCP clients. */
137
34
  description: string;
138
- /**
139
- * Schema defining the input parameters for the tool.
140
- * Accepts JSON Schema (with `as const`) or any Standard Schema v1
141
- * library (Zod v4, Valibot, ArkType, etc.).
142
- *
143
- * @example JSON Schema
144
- * ```typescript
145
- * inputSchema: {
146
- * type: 'object',
147
- * properties: {
148
- * postId: { type: 'string', description: 'The ID of the post to like' },
149
- * },
150
- * required: ['postId'],
151
- * } as const
152
- * ```
153
- *
154
- * @example Standard Schema (Zod v4)
155
- * ```typescript
156
- * inputSchema: z.object({ postId: z.string() })
157
- * ```
158
- */
35
+ /** JSON Schema literal or Standard JSON Schema for the tool input. */
159
36
  inputSchema?: TInputSchema;
160
- /**
161
- * **Recommended:** JSON Schema defining the expected output structure.
162
- *
163
- * When provided, this enables three key features:
164
- * 1. **Type Safety**: The implementation return type is inferred from this schema
165
- * 2. **MCP structuredContent**: The MCP response includes `structuredContent`
166
- * containing the typed output per the MCP specification
167
- * 3. **AI Understanding**: AI models can better understand and use the tool's output
168
- *
169
- * @see {@link https://spec.modelcontextprotocol.io/specification/server/tools/#output-schemas}
170
- *
171
- * @example
172
- * ```typescript
173
- * outputSchema: {
174
- * type: 'object',
175
- * properties: {
176
- * counter: { type: 'number', description: 'The current counter value' },
177
- * timestamp: { type: 'string', description: 'ISO timestamp' },
178
- * },
179
- * } as const
180
- * ```
181
- */
37
+ /** JSON Schema used for output inference and structured content. */
182
38
  outputSchema?: TOutputSchema;
183
- /**
184
- * Optional metadata annotations providing hints about tool behavior.
185
- * See {@link ToolAnnotations} for available options.
186
- */
39
+ /** Optional WebMCP behavior hints. */
187
40
  annotations?: ToolAnnotations;
188
- /**
189
- * Custom formatter for the MCP text response.
190
- *
191
- * @deprecated Use `outputSchema` instead. The `outputSchema` provides type-safe
192
- * structured output via MCP's `structuredContent`, which is the recommended
193
- * approach for tool outputs. This property will be removed in a future version.
194
- *
195
- * @param output - The raw output from the tool implementation
196
- * @returns Formatted string for the MCP response content
197
- */
198
- formatOutput?: (output: InferOutput<TOutputSchema>) => string;
199
- /**
200
- * Optional callback invoked when the tool execution succeeds.
201
- * Useful for triggering side effects like navigation or analytics.
202
- *
203
- * @param result - The successful result from the tool implementation
204
- * @param input - The input that was passed to the tool implementation
205
- */
206
- onSuccess?: (result: InferOutput<TOutputSchema>, input: unknown) => void;
207
- /**
208
- * Optional callback invoked when the tool execution fails.
209
- * Useful for error handling, logging, or showing user notifications.
210
- *
211
- * @param error - The error that occurred during execution
212
- * @param input - The input that was passed to the tool implementation
213
- */
214
- onError?: (error: Error, input: unknown) => void;
215
- }
216
- type WebMCPConfigImplementation<TInputSchema extends ToolInputSchema = InputSchema, TOutputSchema extends JsonSchemaForInference | undefined = undefined> = {
217
- /**
218
- * Preferred tool implementation function.
219
- */
41
+ /** Tool implementation. */
220
42
  execute: ToolExecuteFunction<TInputSchema, TOutputSchema>;
221
- /**
222
- * Backward-compatible alias for `execute`.
223
- */
224
- handler?: ToolExecuteFunction<TInputSchema, TOutputSchema>;
225
- } | {
226
- /**
227
- * Backward-compatible alias for `execute`.
228
- */
229
- handler: ToolExecuteFunction<TInputSchema, TOutputSchema>;
230
- /**
231
- * Preferred tool implementation function.
232
- */
233
- execute?: ToolExecuteFunction<TInputSchema, TOutputSchema>;
234
- };
235
- /**
236
- * Configuration options for the `useWebMCP` hook.
237
- *
238
- * You can provide tool logic with either:
239
- * - `execute` (preferred), or
240
- * - `handler` (backward-compatible alias).
241
- *
242
- * If both are provided, `execute` is used.
243
- *
244
- * @template TInputSchema - JSON Schema defining input parameters
245
- * @template TOutputSchema - JSON Schema defining output structure
246
- *
247
- * @public
248
- */
249
- type WebMCPConfig<TInputSchema extends ToolInputSchema = InputSchema, TOutputSchema extends JsonSchemaForInference | undefined = undefined> = WebMCPConfigBase<TInputSchema, TOutputSchema> & WebMCPConfigImplementation<TInputSchema, TOutputSchema>;
250
- /**
251
- * Return value from the `useWebMCP` hook.
252
- * Provides access to execution state and methods for manual tool control.
253
- *
254
- * @template TOutputSchema - JSON Schema defining output structure
255
- * @public
256
- */
43
+ }
44
+ /** State and controls returned by `useWebMCP`. */
257
45
  interface WebMCPReturn<TOutputSchema extends JsonSchemaForInference | undefined = undefined, TInputSchema extends ToolInputSchema = InputSchema> {
258
- /**
259
- * Current execution state including loading status, results, and errors.
260
- * See {@link ToolExecutionState} for details.
261
- */
262
46
  state: ToolExecutionState<InferOutput<TOutputSchema>>;
263
- /**
264
- * Manually execute the tool with the provided input.
265
- * Useful for testing, debugging, or triggering execution from your UI.
266
- *
267
- * @param input - The input parameters to pass to the tool
268
- * @returns Promise resolving to the tool's output
269
- * @throws Error if validation fails or tool implementation throws
270
- */
271
47
  execute: (input: InferToolInput<TInputSchema>) => Promise<InferOutput<TOutputSchema>>;
272
- /**
273
- * Reset the execution state to its initial values.
274
- * Clears results, errors, and resets the execution count.
275
- */
276
48
  reset: () => void;
277
49
  }
278
50
  //#endregion
279
51
  //#region src/useWebMCP.d.ts
280
- /**
281
- * React hook for registering and managing Model Context Protocol (MCP) tools.
282
- *
283
- * This hook handles the complete lifecycle of an MCP tool:
284
- * - Registers the tool with `window.document.modelContext`
285
- * - Manages execution state (loading, results, errors)
286
- * - Handles tool execution and lifecycle callbacks
287
- * - Automatically unregisters on component unmount
288
- * - Returns `structuredContent` when `outputSchema` is defined
289
- *
290
- * ## Output Schema (Recommended)
291
- *
292
- * Always define an `outputSchema` for your tools. This provides:
293
- * - **Type Safety**: Handler return type is inferred from the schema
294
- * - **MCP structuredContent**: AI models receive structured, typed data
295
- * - **Better AI Understanding**: Models can reason about your tool's output format
296
- *
297
- * ```tsx
298
- * useWebMCP({
299
- * name: 'get_user',
300
- * description: 'Get user by ID',
301
- * inputSchema: {
302
- * type: 'object',
303
- * properties: { userId: { type: 'string' } },
304
- * required: ['userId'],
305
- * } as const,
306
- * outputSchema: {
307
- * type: 'object',
308
- * properties: {
309
- * id: { type: 'string' },
310
- * name: { type: 'string' },
311
- * email: { type: 'string' },
312
- * },
313
- * } as const,
314
- * execute: async ({ userId }) => {
315
- * const user = await fetchUser(userId);
316
- * return { id: user.id, name: user.name, email: user.email };
317
- * },
318
- * });
319
- * ```
320
- *
321
- * ## Re-render Optimization
322
- *
323
- * This hook is optimized to minimize unnecessary tool re-registrations:
324
- *
325
- * - **Ref-based static descriptors and callbacks**: `inputSchema`, `outputSchema`,
326
- * `annotations`, `execute`/`handler`, `onSuccess`, `onError`, and `formatOutput`
327
- * are stored in refs, so recreating them during render won't trigger re-registration.
328
- *
329
- * If a schema or annotation actually changes and the registered descriptor should change,
330
- * include the relevant primitive value in the `deps` array.
331
- *
332
- * ```tsx
333
- * useWebMCP({
334
- * outputSchema: { type: 'object', properties: { count: { type: 'number' } } } as const,
335
- * });
336
- * ```
337
- *
338
- * @template TInputSchema - JSON Schema defining input parameter types (use `as const` for inference)
339
- * @template TOutputSchema - JSON Schema defining output structure
340
- *
341
- * @param config - Configuration object for the tool
342
- * @param deps - Optional dependency array that triggers tool re-registration when values change.
343
- *
344
- * @returns Object containing execution state and control methods
345
- *
346
- * @public
347
- *
348
- * @example
349
- * Basic tool with outputSchema (recommended):
350
- * ```tsx
351
- * function PostActions() {
352
- * const likeTool = useWebMCP({
353
- * name: 'posts_like',
354
- * description: 'Like a post by ID',
355
- * inputSchema: {
356
- * type: 'object',
357
- * properties: { postId: { type: 'string', description: 'The post ID' } },
358
- * required: ['postId'],
359
- * } as const,
360
- * outputSchema: {
361
- * type: 'object',
362
- * properties: {
363
- * success: { type: 'boolean' },
364
- * likeCount: { type: 'number' },
365
- * },
366
- * } as const,
367
- * execute: async ({ postId }) => {
368
- * const result = await api.posts.like(postId);
369
- * return { success: true, likeCount: result.likes };
370
- * },
371
- * });
372
- *
373
- * return <div>Likes: {likeTool.state.lastResult?.likeCount ?? 0}</div>;
374
- * }
375
- * ```
376
- */
52
+ /** Registers a React-owned tool with the current WebMCP model context. */
377
53
  declare function useWebMCP<TInputSchema extends ToolInputSchema = InputSchema, TOutputSchema extends JsonSchemaForInference | undefined = undefined>(config: WebMCPConfig<TInputSchema, TOutputSchema>, deps?: DependencyList): WebMCPReturn<TOutputSchema, TInputSchema>;
378
54
  //#endregion
379
55
  export { type InferOutput, type InferToolInput, type ToolExecuteFunction, type ToolExecutionState, type WebMCPConfig, type WebMCPReturn, useWebMCP };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- "use client";import{normalizeInputSchema as e,toJsonValue as t}from"@mcp-b/webmcp-polyfill/schema";import{useCallback as n,useEffect as r,useLayoutEffect as i,useRef as a,useState as o}from"react";function s(){if(typeof window>`u`)return;let e=window.document.modelContext,t=window.navigator.modelContext;return e??t}function c(e){return typeof e==`string`?e:JSON.stringify(e,null,2)}const l=new Map,u=typeof window<`u`?i:r;function d(){let e=typeof process<`u`?`production`:void 0;return e===void 0?!1:e!==`production`}function f(i,f){let{name:p,description:m,inputSchema:h,outputSchema:g,annotations:_,execute:v,handler:y,formatOutput:b=c,onSuccess:x,onError:S}=i,C=v??y;if(!C)throw TypeError(`[useWebMCP] Tool "${p}" must provide an implementation via config.execute or config.handler`);let[w,T]=o({isExecuting:!1,lastResult:null,error:null,executionCount:0}),E=a(C),D=a(x),O=a(S),k=a(b),A=a(h),j=a(g),M=a(_),N=a(!0),P=a(new Set),F=a({description:m,deps:f});u(()=>{E.current=C,D.current=x,O.current=S,k.current=b,A.current=h,j.current=g,M.current=_},[_,h,x,S,g,C,b]),r(()=>(N.current=!0,()=>{N.current=!1}),[]),r(()=>{if(!d()){F.current={description:m,deps:f};return}let e=(e,t)=>{P.current.has(e)||(console.warn(`[useWebMCP] ${t}`),P.current.add(e))};m!==F.current.description&&e(`description`,`Tool "${p}" description changed; this re-registers the tool. Memoize the description if it does not need to update.`),f?.some(e=>typeof e==`object`&&!!e||typeof e==`function`)&&e(`deps`,`Tool "${p}" deps contains non-primitive values; prefer primitives or memoize objects/functions to reduce re-registration.`),F.current={description:m,deps:f}},[f,m,p]);let I=n(async e=>{T(e=>({...e,isExecuting:!0,error:null}));try{let t=await E.current(e);return N.current&&T(e=>({isExecuting:!1,lastResult:t,error:null,executionCount:e.executionCount+1})),D.current&&D.current(t,e),t}catch(t){let n=t instanceof Error?t:Error(String(t));throw N.current&&T(e=>({...e,isExecuting:!1,error:n})),O.current&&O.current(n,e),n}},[]),L=a(I);r(()=>{L.current=I},[I]);let R=n(e=>L.current(e),[]),z=n(()=>{T({isExecuting:!1,lastResult:null,error:null,executionCount:0})},[]);return r(()=>{let n=s();if(!n){console.warn(`[useWebMCP] window.document.modelContext is not available. Tool "${p}" will not be registered.`);return}let r=async e=>{try{let n=await Reflect.apply(L.current,void 0,[e]),r={content:[{type:`text`,text:k.current(n)}]};if(j.current){let e=t(n);if(e===void 0)throw Error(`Tool "${p}" outputSchema requires the tool implementation to return a JSON-serializable result`);r.structuredContent=e}return r}catch(e){return{content:[{type:`text`,text:`Error: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}},i=Symbol(p),a=e(A.current).inputSchema,o=j.current,c=M.current,u={name:p,description:m,inputSchema:a,...o&&{outputSchema:o},...c&&{annotations:c},execute:r},d=new AbortController,f=!1,h=!1;try{let e=n.registerTool(u,{signal:d.signal});Promise.resolve(e).then(()=>{!h&&!d.signal.aborted&&(f=!0,l.set(p,i))},e=>{d.signal.aborted||(d.abort(),console.warn(`[useWebMCP] registerTool("${p}") rejected:`,e))})}catch(e){d.abort(),console.warn(`[useWebMCP] registerTool("${p}") rejected:`,e);return}return()=>{if(h=!0,f&&l.get(p)===i){l.delete(p),d.abort();return}f||d.abort()}},[p,m,...f??[]]),{state:w,execute:R,reset:z}}export{f as useWebMCP};
1
+ "use client";import{normalizeInputSchema as e,normalizeToolResponse as t}from"@mcp-b/webmcp-polyfill/schema";import{useCallback as n,useEffect as r,useLayoutEffect as i,useRef as a,useState as o}from"react";const s={isExecuting:!1,lastResult:null,error:null,executionCount:0},c=typeof window>`u`?r:i;function l(i,l){let{name:u,description:d}=i,[f,p]=o(s),m=a(i),h=a(0);c(()=>{m.current=i});let g=n(async e=>{h.current+=1,p(e=>({...e,isExecuting:!0,error:null}));let n,r=m.current;try{if(n=await r.execute(e),r.outputSchema&&t(n).structuredContent===void 0)throw Error(`Tool "${r.name}" outputSchema requires execute to return a JSON-serializable result`)}catch(e){--h.current;let t=e instanceof Error?e:Error(String(e));throw p(e=>({...e,isExecuting:h.current>0,error:t})),t}return--h.current,p(e=>({isExecuting:h.current>0,lastResult:n,error:null,executionCount:e.executionCount+1})),n},[]),_=n(()=>p({...s,isExecuting:h.current>0}),[]);return r(()=>{let n=typeof document>`u`?void 0:document.modelContext??(typeof navigator>`u`?void 0:navigator.modelContext);if(!n){console.warn(`[useWebMCP] document.modelContext is not available. Tool "${u}" will not be registered.`);return}let r=m.current,i=r.outputSchema,a=new AbortController,o={name:u,description:d,inputSchema:e(r.inputSchema).inputSchema,...i&&{outputSchema:i},...r.annotations&&{annotations:r.annotations},execute:async e=>{try{let n=t(await g(e));if(i&&n.structuredContent===void 0)throw Error(`Tool "${u}" outputSchema requires the tool implementation to return a JSON-serializable result`);return n}catch(e){return{content:[{type:`text`,text:`Error: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}};try{let e=n.registerTool(o,{signal:a.signal});Promise.resolve(e).catch(e=>{a.signal.aborted||(a.abort(),console.warn(`[useWebMCP] registerTool("${u}") rejected:`,e))})}catch(e){a.abort(),console.warn(`[useWebMCP] registerTool("${u}") rejected:`,e);return}return()=>a.abort()},[u,d,...l??[]]),{state:f,execute:g,reset:_}}export{l as useWebMCP};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/model-context.ts","../src/useWebMCP.ts"],"sourcesContent":["import type { ModelContext } from '@mcp-b/webmcp-types';\n\nexport type ModelContextSurface = ModelContext;\ntype ModelContextHost = { modelContext?: ModelContext };\n\nexport function getModelContext(): ModelContextSurface | undefined {\n if (typeof window === 'undefined') {\n return undefined;\n }\n\n const documentContext = (window.document as Document & ModelContextHost).modelContext;\n const navigatorContext = (window.navigator as Navigator & ModelContextHost).modelContext;\n return documentContext ?? navigatorContext;\n}\n","import {\n normalizeInputSchema,\n toJsonValue,\n type ToolInputSchema,\n} from '@mcp-b/webmcp-polyfill/schema';\nimport type {\n CallToolResult,\n InputSchema,\n JsonSchemaForInference,\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';\nimport { getModelContext } from './model-context.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>();\n\nconst useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;\n\nfunction isDev(): boolean {\n const env = typeof process !== 'undefined' ? process.env?.NODE_ENV : undefined;\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.document.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 static descriptors and callbacks**: `inputSchema`, `outputSchema`,\n * `annotations`, `execute`/`handler`, `onSuccess`, `onError`, and `formatOutput`\n * are stored in refs, so recreating them during render won't trigger re-registration.\n *\n * If a schema or annotation actually changes and the registered descriptor should change,\n * include the relevant primitive value in the `deps` array.\n *\n * ```tsx\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 defining output structure\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 JsonSchemaForInference | undefined = undefined,\n>(\n config: WebMCPConfig<TInputSchema, TOutputSchema>,\n deps?: DependencyList\n): WebMCPReturn<TOutputSchema, TInputSchema> {\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 inputSchemaRef = useRef(inputSchema);\n const outputSchemaRef = useRef(outputSchema);\n const annotationsRef = useRef(annotations);\n const isMountedRef = useRef(true);\n const warnedRef = useRef(new Set<string>());\n const prevConfigRef = useRef({\n description,\n deps,\n });\n // Update refs when callbacks or static descriptors are recreated during render.\n useIsomorphicLayoutEffect(() => {\n toolExecuteRef.current = toolExecute;\n onSuccessRef.current = onSuccess;\n onErrorRef.current = onError;\n formatOutputRef.current = formatOutput;\n inputSchemaRef.current = inputSchema;\n outputSchemaRef.current = outputSchema;\n annotationsRef.current = annotations;\n }, [annotations, inputSchema, onSuccess, onError, outputSchema, toolExecute, 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 = { 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 (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 = { description, deps };\n }, [deps, description, name]);\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: TInput): Promise<TOutput> => {\n setState((prev) => ({\n ...prev,\n isExecuting: true,\n error: null,\n }));\n\n try {\n const result = await toolExecuteRef.current(input);\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: TInput): 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 const modelContext = getModelContext();\n if (!modelContext) {\n console.warn(\n `[useWebMCP] window.document.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 Reflect.apply(executeRef.current, undefined, [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 (outputSchemaRef.current) {\n const structuredContent = toJsonValue(result);\n if (structuredContent === undefined) {\n throw new Error(\n `Tool \"${name}\" outputSchema requires the tool implementation to return a JSON-serializable 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 resolvedInputSchema = normalizeInputSchema(inputSchemaRef.current).inputSchema;\n const resolvedOutputSchema = outputSchemaRef.current;\n const resolvedAnnotations = annotationsRef.current;\n const toolDescriptor: ToolDescriptor & { inputSchema: InputSchema } = {\n name,\n description,\n inputSchema: resolvedInputSchema,\n ...(resolvedOutputSchema && { outputSchema: resolvedOutputSchema }),\n ...(resolvedAnnotations && { annotations: resolvedAnnotations }),\n execute: mcpHandler,\n };\n\n const controller = new AbortController();\n let registered = false;\n let disposed = false;\n\n try {\n const registerResult = modelContext.registerTool(toolDescriptor, {\n signal: controller.signal,\n });\n\n void Promise.resolve(registerResult).then(\n () => {\n if (!disposed && !controller.signal.aborted) {\n registered = true;\n TOOL_OWNER_BY_NAME.set(name, ownerToken);\n }\n },\n (error: unknown) => {\n if (!controller.signal.aborted) {\n controller.abort();\n console.warn(`[useWebMCP] registerTool(\"${name}\") rejected:`, error);\n }\n }\n );\n } catch (error) {\n controller.abort();\n console.warn(`[useWebMCP] registerTool(\"${name}\") rejected:`, error);\n return;\n }\n\n return () => {\n disposed = true;\n\n if (registered && TOOL_OWNER_BY_NAME.get(name) === ownerToken) {\n TOOL_OWNER_BY_NAME.delete(name);\n controller.abort();\n return;\n }\n\n if (!registered) {\n controller.abort();\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, ...(deps ?? [])]);\n\n return {\n state,\n execute: stableExecute,\n reset,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/useWebMCP.ts"],"sourcesContent":["import {\n normalizeInputSchema,\n normalizeToolResponse,\n type ToolInputSchema,\n} from '@mcp-b/webmcp-polyfill/schema';\nimport type { InputSchema, JsonSchemaForInference } 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\nconst INITIAL_STATE = {\n isExecuting: false,\n lastResult: null,\n error: null,\n executionCount: 0,\n};\n\nconst useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;\n\n/** Registers a React-owned tool with the current WebMCP model context. */\nexport function useWebMCP<\n TInputSchema extends ToolInputSchema = InputSchema,\n TOutputSchema extends JsonSchemaForInference | undefined = undefined,\n>(\n config: WebMCPConfig<TInputSchema, TOutputSchema>,\n deps?: DependencyList\n): WebMCPReturn<TOutputSchema, TInputSchema> {\n type TOutput = InferOutput<TOutputSchema>;\n type TInput = InferToolInput<TInputSchema>;\n const { name, description } = config;\n const [state, setState] = useState<ToolExecutionState<TOutput>>(INITIAL_STATE);\n const committedConfigRef = useRef(config);\n const pendingExecutionsRef = useRef(0);\n\n // MCP calls can arrive after commit but before passive effects. A layout effect\n // publishes only committed renders; the server fallback avoids the SSR warning.\n useIsomorphicLayoutEffect(() => {\n committedConfigRef.current = config;\n });\n\n const run = useCallback(async (input: TInput): Promise<TOutput> => {\n pendingExecutionsRef.current += 1;\n setState((previous) => ({ ...previous, isExecuting: true, error: null }));\n\n let result: TOutput;\n const executionConfig = committedConfigRef.current;\n try {\n result = await executionConfig.execute(input);\n if (\n executionConfig.outputSchema &&\n normalizeToolResponse(result).structuredContent === undefined\n ) {\n throw new Error(\n `Tool \"${executionConfig.name}\" outputSchema requires execute to return a JSON-serializable result`\n );\n }\n } catch (error) {\n pendingExecutionsRef.current -= 1;\n const normalizedError = error instanceof Error ? error : new Error(String(error));\n setState((previous) => ({\n ...previous,\n isExecuting: pendingExecutionsRef.current > 0,\n error: normalizedError,\n }));\n throw normalizedError;\n }\n\n pendingExecutionsRef.current -= 1;\n setState((previous) => ({\n isExecuting: pendingExecutionsRef.current > 0,\n lastResult: result,\n error: null,\n executionCount: previous.executionCount + 1,\n }));\n return result;\n }, []);\n\n const reset = useCallback(\n () => setState({ ...INITIAL_STATE, isExecuting: pendingExecutionsRef.current > 0 }),\n []\n );\n\n useEffect(() => {\n const modelContext =\n typeof document === 'undefined'\n ? undefined\n : (document.modelContext ??\n (typeof navigator === 'undefined' ? undefined : navigator.modelContext));\n if (!modelContext) {\n console.warn(\n `[useWebMCP] document.modelContext is not available. Tool \"${name}\" will not be registered.`\n );\n return;\n }\n\n const registeredConfig = committedConfigRef.current;\n const registeredOutputSchema = registeredConfig.outputSchema;\n const controller = new AbortController();\n const tool = {\n name,\n description,\n inputSchema: normalizeInputSchema(registeredConfig.inputSchema).inputSchema,\n ...(registeredOutputSchema && { outputSchema: registeredOutputSchema }),\n ...(registeredConfig.annotations && { annotations: registeredConfig.annotations }),\n execute: async (input: TInput) => {\n try {\n const response = normalizeToolResponse(await run(input));\n if (registeredOutputSchema && response.structuredContent === undefined) {\n throw new Error(\n `Tool \"${name}\" outputSchema requires the tool implementation to return a JSON-serializable result`\n );\n }\n return response;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n content: [{ type: 'text' as const, text: `Error: ${message}` }],\n isError: true,\n };\n }\n },\n };\n\n try {\n const registration = modelContext.registerTool(tool, { signal: controller.signal });\n void Promise.resolve(registration).catch((error: unknown) => {\n if (!controller.signal.aborted) {\n controller.abort();\n console.warn(`[useWebMCP] registerTool(\"${name}\") rejected:`, error);\n }\n });\n } catch (error) {\n controller.abort();\n console.warn(`[useWebMCP] registerTool(\"${name}\") rejected:`, error);\n return;\n }\n\n return () => controller.abort();\n // `deps` lets callers explicitly opt descriptor values into re-registration.\n // oxlint-disable-next-line react-doctor/exhaustive-deps -- Public API deliberately forwards caller deps.\n }, [name, description, ...(deps ?? [])]);\n\n return { state, execute: run, reset };\n}\n"],"mappings":"+MAgBA,MAAM,EAAgB,CACpB,YAAa,GACb,WAAY,KACZ,MAAO,KACP,eAAgB,EACjB,CAEK,EAA4B,OAAO,OAAW,IAAc,EAAY,EAG9E,SAAgB,EAId,EACA,EAC2C,CAG3C,GAAM,CAAE,OAAM,eAAgB,EACxB,CAAC,EAAO,GAAY,EAAsC,EAAc,CACxE,EAAqB,EAAO,EAAO,CACnC,EAAuB,EAAO,EAAE,CAItC,MAAgC,CAC9B,EAAmB,QAAU,GAC7B,CAEF,IAAM,EAAM,EAAY,KAAO,IAAoC,CACjE,EAAqB,SAAW,EAChC,EAAU,IAAc,CAAE,GAAG,EAAU,YAAa,GAAM,MAAO,KAAM,EAAE,CAEzE,IAAI,EACE,EAAkB,EAAmB,QAC3C,GAAI,CAEF,GADA,EAAS,MAAM,EAAgB,QAAQ,EAAM,CAE3C,EAAgB,cAChB,EAAsB,EAAO,CAAC,oBAAsB,IAAA,GAEpD,MAAU,MACR,SAAS,EAAgB,KAAK,sEAC/B,OAEI,EAAO,CACd,IAAqB,QACrB,IAAM,EAAkB,aAAiB,MAAQ,EAAY,MAAM,OAAO,EAAM,CAAC,CAMjF,MALA,EAAU,IAAc,CACtB,GAAG,EACH,YAAa,EAAqB,QAAU,EAC5C,MAAO,EACR,EAAE,CACG,EAUR,MAPA,IAAqB,QACrB,EAAU,IAAc,CACtB,YAAa,EAAqB,QAAU,EAC5C,WAAY,EACZ,MAAO,KACP,eAAgB,EAAS,eAAiB,EAC3C,EAAE,CACI,GACN,EAAE,CAAC,CAEA,EAAQ,MACN,EAAS,CAAE,GAAG,EAAe,YAAa,EAAqB,QAAU,EAAG,CAAC,CACnF,EAAE,CACH,CA8DD,OA5DA,MAAgB,CACd,IAAM,EACJ,OAAO,SAAa,IAChB,IAAA,GACC,SAAS,eACT,OAAO,UAAc,IAAc,IAAA,GAAY,UAAU,cAChE,GAAI,CAAC,EAAc,CACjB,QAAQ,KACN,6DAA6D,EAAK,2BACnE,CACD,OAGF,IAAM,EAAmB,EAAmB,QACtC,EAAyB,EAAiB,aAC1C,EAAa,IAAI,gBACjB,EAAO,CACX,OACA,cACA,YAAa,EAAqB,EAAiB,YAAY,CAAC,YAChE,GAAI,GAA0B,CAAE,aAAc,EAAwB,CACtE,GAAI,EAAiB,aAAe,CAAE,YAAa,EAAiB,YAAa,CACjF,QAAS,KAAO,IAAkB,CAChC,GAAI,CACF,IAAM,EAAW,EAAsB,MAAM,EAAI,EAAM,CAAC,CACxD,GAAI,GAA0B,EAAS,oBAAsB,IAAA,GAC3D,MAAU,MACR,SAAS,EAAK,sFACf,CAEH,OAAO,QACA,EAAO,CAEd,MAAO,CACL,QAAS,CAAC,CAAE,KAAM,OAAiB,KAAM,UAF3B,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,GAEN,CAAC,CAC/D,QAAS,GACV,GAGN,CAED,GAAI,CACF,IAAM,EAAe,EAAa,aAAa,EAAM,CAAE,OAAQ,EAAW,OAAQ,CAAC,CACnF,QAAa,QAAQ,EAAa,CAAC,MAAO,GAAmB,CACtD,EAAW,OAAO,UACrB,EAAW,OAAO,CAClB,QAAQ,KAAK,6BAA6B,EAAK,cAAe,EAAM,GAEtE,OACK,EAAO,CACd,EAAW,OAAO,CAClB,QAAQ,KAAK,6BAA6B,EAAK,cAAe,EAAM,CACpE,OAGF,UAAa,EAAW,OAAO,EAG9B,CAAC,EAAM,EAAa,GAAI,GAAQ,EAAE,CAAE,CAAC,CAEjC,CAAE,QAAO,QAAS,EAAK,QAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usewebmcp",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "Standalone React hooks for strict core WebMCP tool registration with document.modelContext",
5
5
  "keywords": [
6
6
  "ai",
@@ -29,7 +29,9 @@
29
29
  "dist"
30
30
  ],
31
31
  "type": "module",
32
+ "sideEffects": false,
32
33
  "main": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
33
35
  "exports": {
34
36
  ".": {
35
37
  "types": "./dist/index.d.ts",
@@ -41,8 +43,8 @@
41
43
  "registry": "https://registry.npmjs.org/"
42
44
  },
43
45
  "dependencies": {
44
- "@mcp-b/webmcp-polyfill": "4.0.0",
45
- "@mcp-b/webmcp-types": "4.0.0"
46
+ "@mcp-b/webmcp-polyfill": "5.0.0",
47
+ "@mcp-b/webmcp-types": "5.0.0"
46
48
  },
47
49
  "devDependencies": {
48
50
  "@types/node": "22.17.2",
@@ -55,10 +57,14 @@
55
57
  "vite-plus": "^0.1.22",
56
58
  "vitest": "npm:@voidzero-dev/vite-plus-test@latest",
57
59
  "vitest-browser-react": "^2.0.4",
58
- "@mcp-b/global": "4.0.0"
60
+ "zod": "4.4.3",
61
+ "@mcp-b/global": "5.0.0"
59
62
  },
60
63
  "peerDependencies": {
61
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
64
+ "react": "^18.0.0 || ^19.0.0"
65
+ },
66
+ "engines": {
67
+ "node": ">=20"
62
68
  },
63
69
  "scripts": {
64
70
  "build": "vp pack",
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/useWebMCP.ts"],"mappings":";;;;;;;;AAmBA;;;;;;;KAAY,cAAA,MAAoB,CAAA;EAAA,SAAqB,WAAA;IAAA,SAAwB,KAAA;EAAA;AAAA,IACzE,KAAA;EAAA,SAAyB,KAAA;AAAA,IACvB,CAAA,GACA,MAAA,oBACF,CAAA,SAAU,WAAA,GACR,wBAAA,CAAyB,CAAA,IACzB,MAAA;;;;;;;;;;;;KAaM,WAAA,uBACY,sBAAA,iDAEpB,aAAA,qBACA,SAAA,GACA,aAAA,SAAsB,sBAAA,GACpB,eAAA,CAAgB,aAAA,IAChB,SAAA;;;;AAPN;;;;UAgBiB,kBAAA;EAZb;;;;EAiBF,WAAA;EAdI;;;;EAoBJ,UAAA,EAAY,OAAA;EAzBZ;;;;EA+BA,KAAA,EAAO,KAAA;EA3BH;;;;EAiCJ,cAAA;AAAA;;;;;;;;;;;KAaU,mBAAA,sBACW,eAAA,GAAkB,WAAA,wBACjB,sBAAA,6BAEtB,KAAA,EAAO,cAAA,CAAe,YAAA,MACnB,OAAA,CAAQ,WAAA,CAAY,aAAA,KAAkB,WAAA,CAAY,aAAA;;AALvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKsE;;;;;;;;;;;;;;;;;;;;;UAsD5D,gBAAA,sBACa,eAAA,GAAkB,WAAA,wBACjB,sBAAA;EAYtB;;;;EANA,IAAA;EA2DA;;;;EArDA,WAAA;EAiEgB;;;;;;;;;;;;AAkBuB;;;;;;;;;EA5DvC,WAAA,GAAc,YAAA;EA2EkC;;;;;;;;;;;;;;;;;;;;;;EAnDhD,YAAA,GAAe,aAAA;EAyDX;;;;EAnDJ,WAAA,GAAc,eAAA;EAuDA;;;;;AAiBhB;;;;;EA5DE,YAAA,IAAgB,MAAA,EAAQ,WAAA,CAAY,aAAA;EA+DjB;;;;;;;EAtDnB,SAAA,IAAa,MAAA,EAAQ,WAAA,CAAY,aAAA,GAAgB,KAAA;EAoDjD;;;;;;;EA3CA,OAAA,IAAW,KAAA,EAAO,KAAA,EAAO,KAAA;AAAA;AAAA,KAGtB,0BAAA,sBACkB,eAAA,GAAkB,WAAA,wBACjB,sBAAA;EAyCmB;;;EAnCrC,OAAA,EAAS,mBAAA,CAAoB,YAAA,EAAc,aAAA;EA4CpB;;;EAxCvB,OAAA,GAAU,mBAAA,CAAoB,YAAA,EAAc,aAAA;AAAA;EAgDV;;;EA1ClC,OAAA,EAAS,mBAAA,CAAoB,YAAA,EAAc,aAAA;EAoD9B;;;EAhDb,OAAA,GAAU,mBAAA,CAAoB,YAAA,EAAc,aAAA;AAAA;;;;;;;;;;;;;;;KAiBtC,YAAA,sBACW,eAAA,GAAkB,WAAA,wBACjB,sBAAA,4BACpB,gBAAA,CAAiB,YAAA,EAAc,aAAA,IACjC,0BAAA,CAA2B,YAAA,EAAc,aAAA;;;;;;;;UAS1B,YAAA,uBACO,sBAAA,+CACD,eAAA,GAAkB,WAAA;EChKzB;;;;EDsKd,KAAA,EAAO,kBAAA,CAAmB,WAAA,CAAY,aAAA;ECpKhB;;;;;;;;ED8KtB,OAAA,GAAU,KAAA,EAAO,cAAA,CAAe,YAAA,MAAkB,OAAA,CAAQ,WAAA,CAAY,aAAA;EC1KzD;;;;EDgLb,KAAA;AAAA;;;;AAlTF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmBA;;;;;;;;;;;;;;;;;;;;;;;;;AAgBA;;;;;;;;;;;;;AAoCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBCqDgB,SAAA,sBACO,eAAA,GAAkB,WAAA,wBACjB,sBAAA,yBAAA,CAEtB,MAAA,EAAQ,YAAA,CAAa,YAAA,EAAc,aAAA,GACnC,IAAA,GAAO,cAAA,GACN,YAAA,CAAa,aAAA,EAAe,YAAA"}