@farm.js/webmcp 0.1.0-beta.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Farm.js Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @farm.js/webmcp
2
+
3
+ Explicit, typed browser tools for Farm applications using the experimental WebMCP API.
4
+
5
+ Farm.js and WebMCP are currently in beta. WebMCP is a Community Group draft, not a W3C Standard.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @farm.js/webmcp
11
+ ```
12
+
13
+ ## Configure
14
+
15
+ ```ts
16
+ import { defineConfig } from "@farm.js/core";
17
+ import { webmcp } from "@farm.js/webmcp";
18
+
19
+ export default defineConfig({
20
+ plugins: [webmcp()],
21
+ });
22
+ ```
23
+
24
+ Define tools in client code and register them only while they are relevant:
25
+
26
+ ```ts
27
+ import { defineWebMCPTool, registerWebMCPTool } from "@farm.js/webmcp/client";
28
+
29
+ const searchProducts = defineWebMCPTool({
30
+ name: "search_products",
31
+ description: "Search products currently available in this store.",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: { query: { type: "string" } },
35
+ required: ["query"],
36
+ additionalProperties: false,
37
+ },
38
+ annotations: { readOnlyHint: true },
39
+ async execute({ query }: { query: string }, { signal }) {
40
+ const response = await fetch(`/api/products?q=${encodeURIComponent(query)}`, { signal });
41
+ return response.json();
42
+ },
43
+ });
44
+
45
+ const unregister = registerWebMCPTool(searchProducts);
46
+ // Call unregister() when this tool is no longer valid for the current page.
47
+ ```
48
+
49
+ Pass a Zod, Valibot, ArkType, or other Standard Schema validator through `validate` when the input
50
+ must also be checked at runtime. Keep `inputSchema` because that JSON Schema is what the browser
51
+ agent sees.
52
+
53
+ ## Security boundary
54
+
55
+ The plugin never scans or exposes Farm server functions. Each tool is an explicit browser-side
56
+ registration. JSON Schema and `validate` protect the handler's input shape, but they are not
57
+ authorization. Any API, query, or action called by a tool must still verify the server session and
58
+ permissions exactly like a normal browser request.
59
+
60
+ Tools stay same-origin by default. The plugin intentionally does not expose the draft `exposedTo`
61
+ option in its first version.
@@ -0,0 +1,37 @@
1
+ import type { FarmWebMCPRuntime, InferWebMCPValidatorOutput, WebMCPJSONValue, WebMCPToolDefinition, WebMCPValidator } from "./types.js";
2
+ export type { FarmWebMCPRuntime, InferWebMCPValidatorOutput, WebMCPInputSchema, WebMCPJSONPrimitive, WebMCPJSONValue, WebMCPSafeParseFailure, WebMCPSafeParseResult, WebMCPSafeParseSuccess, WebMCPStandardSchema, WebMCPToolAnnotations, WebMCPToolDefinition, WebMCPToolExecutionContext, WebMCPToolMetadata, WebMCPValidator, } from "./types.js";
3
+ export type WebMCPUnsupportedBehavior = "ignore" | "warn" | "error";
4
+ export interface StartWebMCPRuntimeOptions {
5
+ unsupported?: WebMCPUnsupportedBehavior;
6
+ /** Expose `window.__FARM_WEBMCP__` and log registration failures. */
7
+ debug?: boolean;
8
+ }
9
+ declare global {
10
+ interface Window {
11
+ /** Development inspector enabled by `webmcp({ debug: true })`. */
12
+ __FARM_WEBMCP__?: FarmWebMCPRuntime;
13
+ }
14
+ }
15
+ /** An actionable validation failure returned to the invoking browser agent. */
16
+ export declare class WebMCPInputError extends TypeError {
17
+ readonly toolName: string;
18
+ readonly issues: unknown;
19
+ constructor(toolName: string, issues: unknown);
20
+ }
21
+ export declare function defineWebMCPTool<TValidator extends WebMCPValidator, TResult = WebMCPJSONValue>(definition: Omit<WebMCPToolDefinition<Extract<InferWebMCPValidatorOutput<TValidator>, object>, TResult>, "validate"> & {
22
+ validate: TValidator;
23
+ }): WebMCPToolDefinition<Extract<InferWebMCPValidatorOutput<TValidator>, object>, TResult>;
24
+ export declare function defineWebMCPTool<TInput extends object = Record<string, unknown>, TResult = WebMCPJSONValue>(definition: WebMCPToolDefinition<TInput, TResult>): WebMCPToolDefinition<TInput, TResult>;
25
+ /**
26
+ * Make one explicit tool available to the active WebMCP runtime.
27
+ * Call the returned function when the tool is no longer valid for the page.
28
+ */
29
+ export declare function registerWebMCPTool<TInput extends object, TResult>(definition: WebMCPToolDefinition<TInput, TResult>): () => void;
30
+ /** Validate and register several tools, then return one cleanup function. */
31
+ export declare function registerWebMCPTools(definitions: readonly WebMCPToolDefinition<any, any>[]): () => void;
32
+ /**
33
+ * Start the browser adapter used by the Farm plugin. Applications normally use
34
+ * `webmcp()` in `farm.config.ts` instead of calling this directly.
35
+ */
36
+ export declare function startWebMCPRuntime(options?: StartWebMCPRuntimeOptions): Promise<FarmWebMCPRuntime>;
37
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,0BAA0B,EAE1B,eAAe,EAIf,oBAAoB,EAEpB,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,iBAAiB,EACjB,0BAA0B,EAC1B,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,0BAA0B,EAC1B,kBAAkB,EAClB,eAAe,GAChB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,yBAAyB,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpE,MAAM,WAAW,yBAAyB;IACxC,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,qEAAqE;IACrE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAyCD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,kEAAkE;QAClE,eAAe,CAAC,EAAE,iBAAiB,CAAC;KACrC;CACF;AAED,+EAA+E;AAC/E,qBAAa,gBAAiB,SAAQ,SAAS;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;gBAEb,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO;CAM9C;AAED,wBAAgB,gBAAgB,CAAC,UAAU,SAAS,eAAe,EAAE,OAAO,GAAG,eAAe,EAC5F,UAAU,EAAE,IAAI,CACd,oBAAoB,CAAC,OAAO,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,EACtF,UAAU,CACX,GAAG;IAAE,QAAQ,EAAE,UAAU,CAAA;CAAE,GAC3B,oBAAoB,CAAC,OAAO,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;AAC1F,wBAAgB,gBAAgB,CAC9B,MAAM,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/C,OAAO,GAAG,eAAe,EACzB,UAAU,EAAE,oBAAoB,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,oBAAoB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAO5F;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,SAAS,MAAM,EAAE,OAAO,EAC/D,UAAU,EAAE,oBAAoB,CAAC,MAAM,EAAE,OAAO,CAAC,GAChD,MAAM,IAAI,CAKZ;AAED,6EAA6E;AAC7E,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,SAAS,oBAAoB,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,GACrD,MAAM,IAAI,CAYZ;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,iBAAiB,CAAC,CA0F5B"}
package/dist/client.js ADDED
@@ -0,0 +1,410 @@
1
+ "use client";
2
+ const TOOL_NAME_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
3
+ const registrations = new Map();
4
+ const registryListeners = new Set();
5
+ let nextRegistrationId = 1;
6
+ let activeRuntime;
7
+ /** An actionable validation failure returned to the invoking browser agent. */
8
+ export class WebMCPInputError extends TypeError {
9
+ toolName;
10
+ issues;
11
+ constructor(toolName, issues) {
12
+ super(`Invalid input for WebMCP tool "${toolName}": ${formatIssues(issues)}`);
13
+ this.name = "WebMCPInputError";
14
+ this.toolName = toolName;
15
+ this.issues = issues;
16
+ }
17
+ }
18
+ export function defineWebMCPTool(definition) {
19
+ return normalizeToolDefinition(definition);
20
+ }
21
+ /**
22
+ * Make one explicit tool available to the active WebMCP runtime.
23
+ * Call the returned function when the tool is no longer valid for the page.
24
+ */
25
+ export function registerWebMCPTool(definition) {
26
+ const normalized = normalizeToolDefinition(definition);
27
+ return registerNormalizedTool(normalized);
28
+ }
29
+ /** Validate and register several tools, then return one cleanup function. */
30
+ export function registerWebMCPTools(definitions) {
31
+ const normalized = definitions.map((definition) => normalizeToolDefinition(definition));
32
+ const cleanups = normalized.map(registerNormalizedTool);
33
+ let active = true;
34
+ return () => {
35
+ if (!active)
36
+ return;
37
+ active = false;
38
+ for (let index = cleanups.length - 1; index >= 0; index--)
39
+ cleanups[index]();
40
+ };
41
+ }
42
+ /**
43
+ * Start the browser adapter used by the Farm plugin. Applications normally use
44
+ * `webmcp()` in `farm.config.ts` instead of calling this directly.
45
+ */
46
+ export async function startWebMCPRuntime(options = {}) {
47
+ activeRuntime?.close();
48
+ const currentDocument = typeof document === "undefined" ? undefined : document;
49
+ const modelContext = currentDocument?.modelContext;
50
+ const behavior = options.unsupported ?? "ignore";
51
+ if (!modelContext) {
52
+ const message = "[farm:webmcp] This browser does not expose document.modelContext. " +
53
+ "The registered tools will remain inactive.";
54
+ if (behavior === "error")
55
+ throw new Error(message);
56
+ if (behavior === "warn")
57
+ console.warn(message);
58
+ const unsupportedRuntime = createUnsupportedRuntime();
59
+ activeRuntime = unsupportedRuntime;
60
+ exposeDebugRuntime(unsupportedRuntime, options.debug === true);
61
+ return unsupportedRuntime;
62
+ }
63
+ const supportedModelContext = modelContext;
64
+ const nativeTools = new Map();
65
+ let closed = false;
66
+ let syncQueue = Promise.resolve();
67
+ let unsubscribe = () => { };
68
+ const runtime = {
69
+ supported: true,
70
+ getTools: currentToolMetadata,
71
+ sync() {
72
+ const run = syncQueue.then(reconcile, reconcile);
73
+ syncQueue = run.catch(() => { });
74
+ return run;
75
+ },
76
+ close() {
77
+ if (closed)
78
+ return;
79
+ closed = true;
80
+ unsubscribe();
81
+ for (const active of nativeTools.values())
82
+ active.controller.abort();
83
+ nativeTools.clear();
84
+ clearDebugRuntime(runtime);
85
+ if (activeRuntime === runtime)
86
+ activeRuntime = undefined;
87
+ },
88
+ };
89
+ async function reconcile() {
90
+ if (closed)
91
+ return;
92
+ const desired = currentToolsByName();
93
+ for (const [name, active] of nativeTools) {
94
+ if (desired.get(name)?.id === active.id)
95
+ continue;
96
+ active.controller.abort();
97
+ nativeTools.delete(name);
98
+ }
99
+ const failures = [];
100
+ for (const [name, registration] of desired) {
101
+ if (closed || nativeTools.has(name))
102
+ continue;
103
+ const controller = new AbortController();
104
+ nativeTools.set(name, { id: registration.id, controller });
105
+ try {
106
+ await supportedModelContext.registerTool(toNativeTool(registration.tool, options.debug === true), { signal: controller.signal });
107
+ }
108
+ catch (error) {
109
+ if (nativeTools.get(name)?.id === registration.id)
110
+ nativeTools.delete(name);
111
+ controller.abort();
112
+ if (!closed) {
113
+ failures.push(error);
114
+ reportError(`Could not register WebMCP tool "${name}"`, error, true);
115
+ }
116
+ }
117
+ }
118
+ if (failures.length === 1)
119
+ throw failures[0];
120
+ if (failures.length > 1) {
121
+ throw new AggregateError(failures, "Could not register one or more WebMCP tools");
122
+ }
123
+ }
124
+ unsubscribe = subscribeToRegistry(() => {
125
+ void runtime.sync().catch(() => { });
126
+ });
127
+ activeRuntime = runtime;
128
+ exposeDebugRuntime(runtime, options.debug === true);
129
+ await runtime.sync();
130
+ return runtime;
131
+ }
132
+ function createUnsupportedRuntime() {
133
+ let closed = false;
134
+ const runtime = {
135
+ supported: false,
136
+ getTools: currentToolMetadata,
137
+ async sync() { },
138
+ close() {
139
+ if (closed)
140
+ return;
141
+ closed = true;
142
+ clearDebugRuntime(runtime);
143
+ if (activeRuntime === runtime)
144
+ activeRuntime = undefined;
145
+ },
146
+ };
147
+ return runtime;
148
+ }
149
+ function registerNormalizedTool(tool) {
150
+ const id = nextRegistrationId++;
151
+ registrations.set(id, { id, tool });
152
+ notifyRegistryListeners();
153
+ let active = true;
154
+ return () => {
155
+ if (!active)
156
+ return;
157
+ active = false;
158
+ registrations.delete(id);
159
+ notifyRegistryListeners();
160
+ };
161
+ }
162
+ function currentToolsByName() {
163
+ const current = new Map();
164
+ for (const registration of registrations.values())
165
+ current.set(registration.tool.name, registration);
166
+ return current;
167
+ }
168
+ function currentToolMetadata() {
169
+ return [...currentToolsByName().values()].map(({ tool }) => ({
170
+ name: tool.name,
171
+ ...(tool.title === undefined ? {} : { title: tool.title }),
172
+ description: tool.description,
173
+ inputSchema: tool.inputSchema,
174
+ ...(tool.annotations === undefined ? {} : { annotations: tool.annotations }),
175
+ }));
176
+ }
177
+ function subscribeToRegistry(listener) {
178
+ registryListeners.add(listener);
179
+ return () => registryListeners.delete(listener);
180
+ }
181
+ function notifyRegistryListeners() {
182
+ for (const listener of registryListeners)
183
+ listener();
184
+ }
185
+ function toNativeTool(tool, debug) {
186
+ return {
187
+ name: tool.name,
188
+ ...(tool.title === undefined ? {} : { title: tool.title }),
189
+ description: tool.description,
190
+ inputSchema: tool.inputSchema,
191
+ ...(tool.annotations === undefined ? {} : { annotations: tool.annotations }),
192
+ async execute(input, { signal }) {
193
+ try {
194
+ const parsed = tool.validate
195
+ ? await validateInput(tool.name, tool.validate, input)
196
+ : assertInputObject(tool.name, input);
197
+ const result = await tool.execute(parsed, { signal });
198
+ assertJSONValue(result, `Result from WebMCP tool "${tool.name}"`);
199
+ return result;
200
+ }
201
+ catch (error) {
202
+ reportError(`WebMCP tool "${tool.name}" failed`, error, debug);
203
+ throw error;
204
+ }
205
+ },
206
+ };
207
+ }
208
+ async function validateInput(toolName, validator, input) {
209
+ try {
210
+ let result;
211
+ if (hasStandardSchema(validator)) {
212
+ const validated = await validator["~standard"].validate(input);
213
+ if (validated.issues !== undefined)
214
+ throw new WebMCPInputError(toolName, validated.issues);
215
+ result = validated.value;
216
+ }
217
+ else if (hasMethod(validator, "safeParseAsync")) {
218
+ result = unwrapSafeParse(toolName, await validator.safeParseAsync(input));
219
+ }
220
+ else if (hasMethod(validator, "safeParse")) {
221
+ result = unwrapSafeParse(toolName, validator.safeParse(input));
222
+ }
223
+ else if (hasMethod(validator, "parseAsync")) {
224
+ result = await validator.parseAsync(input);
225
+ }
226
+ else if (hasMethod(validator, "parse")) {
227
+ result = validator.parse(input);
228
+ }
229
+ else if (typeof validator === "function") {
230
+ result = await validator(input);
231
+ }
232
+ else {
233
+ throw new TypeError("Unsupported validator");
234
+ }
235
+ return assertInputObject(toolName, result);
236
+ }
237
+ catch (error) {
238
+ if (error instanceof WebMCPInputError)
239
+ throw error;
240
+ throw new WebMCPInputError(toolName, extractIssues(error));
241
+ }
242
+ }
243
+ function unwrapSafeParse(toolName, result) {
244
+ if (result.success)
245
+ return result.data;
246
+ throw new WebMCPInputError(toolName, extractIssues(result.error));
247
+ }
248
+ function hasStandardSchema(value) {
249
+ if (!value || typeof value !== "object" || !("~standard" in value))
250
+ return false;
251
+ const standard = value["~standard"];
252
+ return Boolean(standard &&
253
+ typeof standard === "object" &&
254
+ "validate" in standard &&
255
+ typeof standard.validate === "function");
256
+ }
257
+ function hasMethod(value, name) {
258
+ return Boolean(value &&
259
+ (typeof value === "object" || typeof value === "function") &&
260
+ name in value &&
261
+ typeof value[name] === "function");
262
+ }
263
+ function assertInputObject(toolName, value) {
264
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
265
+ throw new WebMCPInputError(toolName, "the validated input must be an object");
266
+ }
267
+ return value;
268
+ }
269
+ function normalizeToolDefinition(definition) {
270
+ if (!definition || typeof definition !== "object" || Array.isArray(definition)) {
271
+ throw new TypeError("A WebMCP tool definition must be an object");
272
+ }
273
+ if (!TOOL_NAME_PATTERN.test(definition.name)) {
274
+ throw new TypeError("A WebMCP tool name must be 1 to 128 ASCII letters, digits, `_`, `-`, or `.`");
275
+ }
276
+ if (typeof definition.description !== "string" || !definition.description.trim()) {
277
+ throw new TypeError(`WebMCP tool "${definition.name}" needs a description`);
278
+ }
279
+ if (definition.title !== undefined &&
280
+ (typeof definition.title !== "string" || !definition.title.trim())) {
281
+ throw new TypeError(`WebMCP tool "${definition.name}" title must be a non-empty string`);
282
+ }
283
+ if (typeof definition.execute !== "function") {
284
+ throw new TypeError(`WebMCP tool "${definition.name}" needs an execute function`);
285
+ }
286
+ if (!definition.inputSchema ||
287
+ typeof definition.inputSchema !== "object" ||
288
+ Array.isArray(definition.inputSchema)) {
289
+ throw new TypeError(`WebMCP tool "${definition.name}" inputSchema must be a JSON object`);
290
+ }
291
+ assertJSONValue(definition.inputSchema, `WebMCP tool "${definition.name}" inputSchema`);
292
+ assertValidator(definition.name, definition.validate);
293
+ const annotations = normalizeAnnotations(definition.name, definition.annotations);
294
+ return Object.freeze({
295
+ name: definition.name,
296
+ ...(definition.title === undefined ? {} : { title: definition.title }),
297
+ description: definition.description,
298
+ inputSchema: cloneJSON(definition.inputSchema),
299
+ ...(definition.validate === undefined ? {} : { validate: definition.validate }),
300
+ ...(annotations === undefined ? {} : { annotations }),
301
+ execute: definition.execute,
302
+ });
303
+ }
304
+ function assertValidator(name, validator) {
305
+ if (validator === undefined || typeof validator === "function")
306
+ return;
307
+ if (hasStandardSchema(validator) ||
308
+ hasMethod(validator, "safeParseAsync") ||
309
+ hasMethod(validator, "safeParse") ||
310
+ hasMethod(validator, "parseAsync") ||
311
+ hasMethod(validator, "parse")) {
312
+ return;
313
+ }
314
+ throw new TypeError(`WebMCP tool "${name}" validate must be a supported schema or function`);
315
+ }
316
+ function normalizeAnnotations(name, annotations) {
317
+ if (annotations === undefined)
318
+ return undefined;
319
+ if (!annotations || typeof annotations !== "object" || Array.isArray(annotations)) {
320
+ throw new TypeError(`WebMCP tool "${name}" annotations must be an object`);
321
+ }
322
+ const allowed = new Set(["readOnlyHint", "untrustedContentHint", "consequentialHint"]);
323
+ for (const [key, value] of Object.entries(annotations)) {
324
+ if (!allowed.has(key)) {
325
+ throw new TypeError(`WebMCP tool "${name}" has an unknown annotation "${key}"`);
326
+ }
327
+ if (typeof value !== "boolean") {
328
+ throw new TypeError(`WebMCP tool "${name}" annotation "${key}" must be boolean`);
329
+ }
330
+ }
331
+ return Object.freeze({ ...annotations });
332
+ }
333
+ function assertJSONValue(value, location, seen = new Set()) {
334
+ if (value === null || typeof value === "string" || typeof value === "boolean")
335
+ return;
336
+ if (typeof value === "number") {
337
+ if (!Number.isFinite(value))
338
+ throw new TypeError(`${location} contains a non-finite number`);
339
+ return;
340
+ }
341
+ if (!value || typeof value !== "object") {
342
+ throw new TypeError(`${location} must be JSON-serializable`);
343
+ }
344
+ if (seen.has(value))
345
+ throw new TypeError(`${location} contains a circular reference`);
346
+ seen.add(value);
347
+ if (Array.isArray(value)) {
348
+ for (let index = 0; index < value.length; index++) {
349
+ if (!(index in value))
350
+ throw new TypeError(`${location} contains a sparse array`);
351
+ assertJSONValue(value[index], location, seen);
352
+ }
353
+ }
354
+ else {
355
+ const prototype = Object.getPrototypeOf(value);
356
+ if (prototype !== Object.prototype && prototype !== null) {
357
+ throw new TypeError(`${location} must contain only plain JSON objects`);
358
+ }
359
+ for (const entry of Object.values(value))
360
+ assertJSONValue(entry, location, seen);
361
+ }
362
+ seen.delete(value);
363
+ }
364
+ function cloneJSON(value) {
365
+ return freezeJSON(JSON.parse(JSON.stringify(value)));
366
+ }
367
+ function freezeJSON(value) {
368
+ if (value && typeof value === "object") {
369
+ for (const child of Object.values(value))
370
+ freezeJSON(child);
371
+ Object.freeze(value);
372
+ }
373
+ return value;
374
+ }
375
+ function extractIssues(error) {
376
+ if (error && typeof error === "object" && "issues" in error) {
377
+ return error.issues;
378
+ }
379
+ return error;
380
+ }
381
+ function formatIssues(issues) {
382
+ if (Array.isArray(issues)) {
383
+ const messages = issues.slice(0, 5).map((issue) => {
384
+ if (!issue || typeof issue !== "object")
385
+ return String(issue);
386
+ const record = issue;
387
+ const path = Array.isArray(record.path) && record.path.length ? `${record.path.join(".")}: ` : "";
388
+ return `${path}${typeof record.message === "string" ? record.message : "invalid value"}`;
389
+ });
390
+ return messages.join("; ") || "input did not match the validation schema";
391
+ }
392
+ if (issues instanceof Error)
393
+ return issues.message;
394
+ if (typeof issues === "string")
395
+ return issues;
396
+ return "input did not match the validation schema";
397
+ }
398
+ function exposeDebugRuntime(runtime, enabled) {
399
+ if (enabled && typeof window !== "undefined")
400
+ window.__FARM_WEBMCP__ = runtime;
401
+ }
402
+ function clearDebugRuntime(runtime) {
403
+ if (typeof window !== "undefined" && window.__FARM_WEBMCP__ === runtime) {
404
+ delete window.__FARM_WEBMCP__;
405
+ }
406
+ }
407
+ function reportError(message, error, debug) {
408
+ if (debug)
409
+ console.error(`[farm:webmcp] ${message}`, error);
410
+ }
@@ -0,0 +1,17 @@
1
+ export type { FarmWebMCPRuntime } from "./types.js";
2
+ export type { WebMCPUnsupportedBehavior } from "./client.js";
3
+ export interface WebMCPPluginOptions {
4
+ /** Set false to keep the plugin configured without starting its browser runtime. */
5
+ enabled?: boolean;
6
+ /** Behavior when `document.modelContext` is unavailable. Defaults to warn in development. */
7
+ unsupported?: "ignore" | "warn" | "error";
8
+ /** Expose `window.__FARM_WEBMCP__` for inspection. Defaults to true in development. */
9
+ debug?: boolean;
10
+ }
11
+ /** Register Farm's explicit browser tools with the experimental WebMCP API. */
12
+ export declare function webmcp(options?: WebMCPPluginOptions): import("@farm.js/core/plugin").FarmPlugin<undefined, Record<string, never>, import("./types.js").FarmWebMCPRuntime | undefined, {
13
+ enabled: boolean;
14
+ unsupported: "ignore" | "warn" | "error" | null;
15
+ debug: boolean | null;
16
+ }, unknown, false>;
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,YAAY,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAE7D,MAAM,WAAW,mBAAmB;IAClC,oFAAoF;IACpF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,6FAA6F;IAC7F,WAAW,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;IAC1C,uFAAuF;IACvF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,+EAA+E;AAC/E,wBAAgB,MAAM,CAAC,OAAO,GAAE,mBAAwB;;;;mBA6BvD"}
package/dist/index.js ADDED
@@ -0,0 +1,46 @@
1
+ import { definePlugin } from "@farm.js/core/plugin";
2
+ /** Register Farm's explicit browser tools with the experimental WebMCP API. */
3
+ export function webmcp(options = {}) {
4
+ assertOptions(options);
5
+ return definePlugin({
6
+ name: "farm:webmcp",
7
+ client: {
8
+ public: {
9
+ enabled: options.enabled ?? true,
10
+ unsupported: options.unsupported ?? null,
11
+ debug: options.debug ?? null,
12
+ },
13
+ async setup({ public: config, isDev }) {
14
+ if (!config.enabled)
15
+ return undefined;
16
+ const runtime = await import("@farm.js/webmcp/client");
17
+ return runtime.startWebMCPRuntime({
18
+ unsupported: config.unsupported ?? (isDev ? "warn" : "ignore"),
19
+ debug: config.debug ?? isDev,
20
+ });
21
+ },
22
+ navigation: {
23
+ async rendered({ state }) {
24
+ await state?.sync();
25
+ },
26
+ },
27
+ close({ state }) {
28
+ state?.close();
29
+ },
30
+ },
31
+ });
32
+ }
33
+ function assertOptions(options) {
34
+ if (options.enabled !== undefined && typeof options.enabled !== "boolean") {
35
+ throw new TypeError("webmcp enabled must be boolean");
36
+ }
37
+ if (options.debug !== undefined && typeof options.debug !== "boolean") {
38
+ throw new TypeError("webmcp debug must be boolean");
39
+ }
40
+ if (options.unsupported !== undefined &&
41
+ options.unsupported !== "ignore" &&
42
+ options.unsupported !== "warn" &&
43
+ options.unsupported !== "error") {
44
+ throw new TypeError('webmcp unsupported must be "ignore", "warn", or "error"');
45
+ }
46
+ }
@@ -0,0 +1,109 @@
1
+ export type MaybePromise<T> = T | Promise<T>;
2
+ export type WebMCPJSONPrimitive = boolean | number | string | null;
3
+ export type WebMCPJSONValue = WebMCPJSONPrimitive | WebMCPJSONValue[] | {
4
+ [key: string]: WebMCPJSONValue;
5
+ };
6
+ /** A JSON Schema object sent to the browser agent with a tool definition. */
7
+ export type WebMCPInputSchema = Readonly<Record<string, unknown>>;
8
+ export interface WebMCPToolAnnotations {
9
+ /** The tool does not change application or external state. */
10
+ readOnlyHint?: boolean;
11
+ /** A tool result can contain content supplied by an untrusted party. */
12
+ untrustedContentHint?: boolean;
13
+ /** The tool can perform a significant or difficult-to-reverse action. */
14
+ consequentialHint?: boolean;
15
+ }
16
+ export interface WebMCPToolExecutionContext {
17
+ /** Aborted when the browser agent cancels this invocation. */
18
+ signal: AbortSignal;
19
+ }
20
+ export interface WebMCPSafeParseSuccess<T> {
21
+ success: true;
22
+ data: T;
23
+ }
24
+ export interface WebMCPSafeParseFailure {
25
+ success: false;
26
+ error: unknown;
27
+ }
28
+ export type WebMCPSafeParseResult<T> = WebMCPSafeParseSuccess<T> | WebMCPSafeParseFailure;
29
+ /** Structural subset of Standard Schema used by Zod, Valibot, ArkType, and others. */
30
+ export interface WebMCPStandardSchema<TOutput = unknown> {
31
+ readonly "~standard": {
32
+ readonly version: number;
33
+ readonly vendor: string;
34
+ readonly validate: (value: unknown) => MaybePromise<{
35
+ readonly value: TOutput;
36
+ readonly issues?: undefined;
37
+ } | {
38
+ readonly issues: readonly unknown[];
39
+ }>;
40
+ readonly types?: {
41
+ readonly input: unknown;
42
+ readonly output: TOutput;
43
+ };
44
+ };
45
+ }
46
+ export type WebMCPValidator<TOutput = unknown> = WebMCPStandardSchema<TOutput> | {
47
+ safeParseAsync(value: unknown): MaybePromise<WebMCPSafeParseResult<TOutput>>;
48
+ } | {
49
+ safeParse(value: unknown): WebMCPSafeParseResult<TOutput>;
50
+ } | {
51
+ parseAsync(value: unknown): MaybePromise<TOutput>;
52
+ } | {
53
+ parse(value: unknown): TOutput;
54
+ } | ((value: unknown) => MaybePromise<TOutput>);
55
+ type StandardSchemaOutput<TValidator> = TValidator extends {
56
+ readonly "~standard": {
57
+ readonly types?: infer TTypes;
58
+ };
59
+ } ? NonNullable<TTypes> extends {
60
+ readonly output: infer TOutput;
61
+ } ? TOutput : never : never;
62
+ /** Infer the value returned by a supported validation schema. */
63
+ export type InferWebMCPValidatorOutput<TValidator> = [StandardSchemaOutput<TValidator>] extends [
64
+ never
65
+ ] ? TValidator extends {
66
+ readonly _output: infer TOutput;
67
+ } ? TOutput : TValidator extends {
68
+ parseAsync(value: unknown): MaybePromise<infer TOutput>;
69
+ } ? TOutput : TValidator extends {
70
+ parse(value: unknown): infer TOutput;
71
+ } ? TOutput : TValidator extends {
72
+ safeParseAsync(value: unknown): MaybePromise<WebMCPSafeParseResult<infer TOutput>>;
73
+ } ? TOutput : TValidator extends {
74
+ safeParse(value: unknown): WebMCPSafeParseResult<infer TOutput>;
75
+ } ? TOutput : TValidator extends (value: unknown) => MaybePromise<infer TOutput> ? TOutput : unknown : StandardSchemaOutput<TValidator>;
76
+ export interface WebMCPToolDefinition<TInput extends object = Record<string, unknown>, TResult = WebMCPJSONValue> {
77
+ /** Stable agent-facing name. Uses ASCII letters, digits, `_`, `-`, and `.`. */
78
+ name: string;
79
+ /** Optional human-readable label for browser UI. */
80
+ title?: string;
81
+ /** Explain what the tool does and when an agent should use it. */
82
+ description: string;
83
+ /** JSON Schema describing the input object presented to the agent. */
84
+ inputSchema: WebMCPInputSchema;
85
+ /** Optional runtime validation. This does not replace server authorization. */
86
+ validate?: WebMCPValidator<TInput>;
87
+ annotations?: WebMCPToolAnnotations;
88
+ /** The runtime rejects results that are not JSON-safe. */
89
+ execute(input: TInput, context: WebMCPToolExecutionContext): MaybePromise<TResult>;
90
+ }
91
+ export interface WebMCPToolMetadata {
92
+ name: string;
93
+ title?: string;
94
+ description: string;
95
+ inputSchema: WebMCPInputSchema;
96
+ annotations?: Readonly<WebMCPToolAnnotations>;
97
+ }
98
+ export interface FarmWebMCPRuntime {
99
+ /** Whether this document provides the current WebMCP API. */
100
+ readonly supported: boolean;
101
+ /** Current explicitly registered Farm tools, with execution functions omitted. */
102
+ getTools(): readonly WebMCPToolMetadata[];
103
+ /** Reconcile Farm's tool registry with `document.modelContext`. */
104
+ sync(): Promise<void>;
105
+ /** Unregister native tools and stop observing Farm registrations. */
106
+ close(): void;
107
+ }
108
+ export {};
109
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@farm.js/webmcp",
3
+ "version": "0.1.0-beta.0",
4
+ "description": "Explicit, typed WebMCP tools for Farm.js applications",
5
+ "keywords": [
6
+ "agents",
7
+ "farm.js",
8
+ "mcp",
9
+ "webmcp"
10
+ ],
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/farming-labs/farm.js",
15
+ "directory": "packages/farm-webmcp"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "type": "module",
21
+ "main": "./dist/index.js",
22
+ "module": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js"
28
+ },
29
+ "./client": {
30
+ "types": "./dist/client.d.ts",
31
+ "import": "./dist/client.js"
32
+ }
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "@farm.js/core": "0.1.0-beta.86"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.10.5",
42
+ "typescript": "^5.3.3",
43
+ "vitest": "^3.2.7"
44
+ },
45
+ "engines": {
46
+ "node": ">=22.13.0"
47
+ },
48
+ "scripts": {
49
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
50
+ "dev": "tsc --watch",
51
+ "type-check": "tsc --noEmit",
52
+ "test": "vitest run"
53
+ }
54
+ }