simple-webmcp 0.1.0 → 0.2.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.
Files changed (49) hide show
  1. package/.agents/skills/webmcp-simple/SKILL.md +27 -12
  2. package/README.md +194 -72
  3. package/dist/dev-polyfill.cjs +68 -0
  4. package/dist/dev-polyfill.cjs.map +1 -0
  5. package/dist/dev-polyfill.d.cts +1 -0
  6. package/dist/dev-polyfill.d.ts +1 -0
  7. package/dist/dev-polyfill.js +62 -0
  8. package/dist/dev-polyfill.js.map +1 -0
  9. package/dist/devtools.cjs +452 -0
  10. package/dist/devtools.cjs.map +1 -0
  11. package/dist/devtools.d.cts +14 -0
  12. package/dist/devtools.d.ts +14 -0
  13. package/dist/devtools.js +449 -0
  14. package/dist/devtools.js.map +1 -0
  15. package/dist/index.cjs +37 -37
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +4 -21
  18. package/dist/index.d.ts +4 -21
  19. package/dist/index.js +37 -37
  20. package/dist/index.js.map +1 -1
  21. package/dist/inspect.cjs +342 -0
  22. package/dist/inspect.cjs.map +1 -0
  23. package/dist/inspect.d.cts +37 -0
  24. package/dist/inspect.d.ts +37 -0
  25. package/dist/inspect.js +334 -0
  26. package/dist/inspect.js.map +1 -0
  27. package/dist/polyfill.cjs.map +1 -1
  28. package/dist/polyfill.d.cts +15 -7
  29. package/dist/polyfill.d.ts +15 -7
  30. package/dist/polyfill.js.map +1 -1
  31. package/dist/react.cjs +850 -12
  32. package/dist/react.cjs.map +1 -1
  33. package/dist/react.d.cts +7 -10
  34. package/dist/react.d.ts +7 -10
  35. package/dist/react.js +851 -14
  36. package/dist/react.js.map +1 -1
  37. package/dist/registry-BcPw1qCf.d.cts +38 -0
  38. package/dist/registry-C6Pz_Rja.d.ts +38 -0
  39. package/dist/testing.cjs +68 -0
  40. package/dist/testing.cjs.map +1 -0
  41. package/dist/testing.d.cts +1 -0
  42. package/dist/testing.d.ts +1 -0
  43. package/dist/testing.js +62 -0
  44. package/dist/testing.js.map +1 -0
  45. package/dist/{types-D-cwSfEU.d.cts → types-DH78HDAE.d.cts} +2 -2
  46. package/dist/{types-D-cwSfEU.d.ts → types-DH78HDAE.d.ts} +2 -2
  47. package/dist/zod.d.cts +1 -1
  48. package/dist/zod.d.ts +1 -1
  49. package/package.json +30 -1
package/dist/react.js CHANGED
@@ -1,19 +1,843 @@
1
- import { useState, useMemo, useRef, useEffect } from 'react';
1
+ import { useMemo, useState, useRef, useEffect } from 'react';
2
2
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
3
3
 
4
4
  // src/internal/utils.ts
5
+ function toSnakeCase(name) {
6
+ if (!name) return "anonymous_tool";
7
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "anonymous_tool";
8
+ }
9
+ function getFunctionName(fn) {
10
+ return fn.displayName || fn.name || "";
11
+ }
5
12
  function isWebMCPSupported() {
6
13
  if (typeof document === "undefined") return false;
7
14
  const docAny = document;
8
15
  return !!docAny.modelContext && typeof docAny.modelContext.registerTool === "function";
9
16
  }
17
+ function getModelContext() {
18
+ if (typeof document === "undefined") return null;
19
+ const docAny = document;
20
+ if (docAny.modelContext && typeof docAny.modelContext.registerTool === "function") {
21
+ return docAny.modelContext;
22
+ }
23
+ return null;
24
+ }
25
+ function looksLikeJsonSchema(value) {
26
+ if (!value || typeof value !== "object") return false;
27
+ const obj = value;
28
+ if ("~standard" in obj) return false;
29
+ return "type" in obj || "properties" in obj || "$ref" in obj || "anyOf" in obj || "oneOf" in obj || "allOf" in obj || "enum" in obj;
30
+ }
31
+ function isStandardSchema(value) {
32
+ return !!value && typeof value === "object" && "~standard" in value && value["~standard"]?.validate != null && typeof value["~standard"].validate === "function";
33
+ }
34
+ var warnedNoDesc = false;
35
+ function warnNoDescription(fn) {
36
+ if (warnedNoDesc) return;
37
+ warnedNoDesc = true;
38
+ if (typeof console !== "undefined" && typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
39
+ console.warn(
40
+ `[simple-webmcp] tool "${getFunctionName(fn) || "anonymous"}" has no description. Add \`description:"..."\` or JSDoc. See https://github.com/emingure/simple-webmcp`
41
+ );
42
+ } else if (typeof console !== "undefined") {
43
+ console.warn(`[simple-webmcp] tool "${getFunctionName(fn) || "anonymous"}" has no description.`);
44
+ }
45
+ }
46
+
47
+ // src/errors.ts
48
+ var SimpleWebMCPError = class extends Error {
49
+ constructor(message, code, cause) {
50
+ super(message);
51
+ this.code = code;
52
+ this.cause = cause;
53
+ this.name = this.constructor.name;
54
+ if (Error.captureStackTrace) {
55
+ Error.captureStackTrace(this, this.constructor);
56
+ }
57
+ }
58
+ toJSON() {
59
+ return {
60
+ name: this.name,
61
+ message: this.message,
62
+ code: this.code,
63
+ cause: this.cause
64
+ };
65
+ }
66
+ };
67
+ var NotSupportedError = class extends SimpleWebMCPError {
68
+ constructor(message = "WebMCP is not supported in this environment", opts) {
69
+ super(message, "NOT_SUPPORTED", opts?.cause);
70
+ }
71
+ };
72
+ var NotAllowedError = class extends SimpleWebMCPError {
73
+ constructor(message = "WebMCP tools are blocked by Permissions Policy", opts) {
74
+ super(message, "NOT_ALLOWED", opts?.cause);
75
+ }
76
+ };
77
+ var RegistrationError = class extends SimpleWebMCPError {
78
+ constructor(message, opts) {
79
+ super(message, "REGISTRATION_ERROR", opts?.cause);
80
+ }
81
+ };
82
+ var ConfigurationError = class extends SimpleWebMCPError {
83
+ constructor(message, opts) {
84
+ super(message, "CONFIGURATION_ERROR", opts?.cause);
85
+ }
86
+ };
87
+
88
+ // src/internal/inferRuntime.ts
89
+ function parseFunctionSource(fn) {
90
+ try {
91
+ return Function.prototype.toString.call(fn);
92
+ } catch {
93
+ return "";
94
+ }
95
+ }
96
+ function parseParamNames(fn) {
97
+ const src = parseFunctionSource(fn);
98
+ const clean = src.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "").replace(/\n/g, " ");
99
+ let paramBlock = "";
100
+ const arrowIdx = clean.indexOf("=>");
101
+ const firstParen = clean.indexOf("(");
102
+ clean.indexOf("{");
103
+ if (firstParen !== -1) {
104
+ let depth = 0;
105
+ let start = firstParen;
106
+ let end = -1;
107
+ for (let i = start; i < clean.length; i++) {
108
+ const ch = clean[i];
109
+ if (ch === "(") depth++;
110
+ else if (ch === ")") {
111
+ depth--;
112
+ if (depth === 0) {
113
+ end = i;
114
+ break;
115
+ }
116
+ }
117
+ }
118
+ if (end !== -1) {
119
+ paramBlock = clean.slice(start + 1, end);
120
+ }
121
+ } else if (arrowIdx !== -1) {
122
+ const beforeArrow = clean.slice(0, arrowIdx).trim();
123
+ const single = beforeArrow.split(/\s+/).pop() || "";
124
+ if (single && !single.includes(" ")) {
125
+ paramBlock = single.replace(/^async\s+/, "");
126
+ }
127
+ }
128
+ if (!paramBlock.trim()) return [];
129
+ const params = [];
130
+ let current = "";
131
+ let depthCurly = 0;
132
+ let depthBracket = 0;
133
+ let depthParen = 0;
134
+ for (let i = 0; i < paramBlock.length; i++) {
135
+ const ch = paramBlock[i];
136
+ if (ch === "{") depthCurly++;
137
+ else if (ch === "}") depthCurly--;
138
+ else if (ch === "[") depthBracket++;
139
+ else if (ch === "]") depthBracket--;
140
+ else if (ch === "(") depthParen++;
141
+ else if (ch === ")") depthParen--;
142
+ if (ch === "," && depthCurly === 0 && depthBracket === 0 && depthParen === 0) {
143
+ const p = current.trim();
144
+ if (p) params.push(parseSingleParam(p));
145
+ current = "";
146
+ } else {
147
+ current += ch;
148
+ }
149
+ }
150
+ const last = current.trim();
151
+ if (last) params.push(parseSingleParam(last));
152
+ return params;
153
+ }
154
+ function parseSingleParam(raw) {
155
+ let s = raw.trim();
156
+ if (s.startsWith("...")) s = s.slice(3).trim();
157
+ let hasDefault = false;
158
+ let defaultValue = void 0;
159
+ let eqIdx = -1;
160
+ let depth = 0;
161
+ for (let i = 0; i < s.length; i++) {
162
+ const ch = s[i];
163
+ if (ch === "{" || ch === "(" || ch === "[") depth++;
164
+ else if (ch === "}" || ch === ")" || ch === "]") depth--;
165
+ else if (ch === "=" && depth === 0) {
166
+ eqIdx = i;
167
+ break;
168
+ }
169
+ }
170
+ let namePart = s;
171
+ if (eqIdx !== -1) {
172
+ hasDefault = true;
173
+ namePart = s.slice(0, eqIdx).trim();
174
+ const defStr = s.slice(eqIdx + 1).trim();
175
+ defaultValue = parseDefaultLiteral(defStr);
176
+ }
177
+ let name = namePart;
178
+ if (name.startsWith("{") && name.endsWith("}")) {
179
+ name = name;
180
+ } else if (name.startsWith("[")) {
181
+ name = name;
182
+ }
183
+ return { name, hasDefault, defaultValue };
184
+ }
185
+ function parseDefaultLiteral(str) {
186
+ const t = str.trim().replace(/,$/, "");
187
+ if (t === "true") return true;
188
+ if (t === "false") return false;
189
+ if (t === "null") return null;
190
+ if (t === "undefined") return void 0;
191
+ if (/^-?\d+(\.\d+)?$/.test(t)) return Number(t);
192
+ if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'") || t.startsWith("`") && t.endsWith("`")) {
193
+ return t.slice(1, -1);
194
+ }
195
+ if (t.startsWith("{") || t.startsWith("[")) return void 0;
196
+ return void 0;
197
+ }
198
+ function inferRuntime(fn) {
199
+ const params = parseParamNames(fn);
200
+ const src = parseFunctionSource(fn);
201
+ const isAsync = /^\s*async\b/.test(src);
202
+ let isObjectParam = false;
203
+ let objectKeys = [];
204
+ if (params.length === 1 && params[0].name.startsWith("{")) {
205
+ isObjectParam = true;
206
+ const inner = params[0].name.slice(1, -1);
207
+ const keys = [];
208
+ let cur = "";
209
+ let d = 0;
210
+ for (let i = 0; i < inner.length; i++) {
211
+ const ch = inner[i];
212
+ if (ch === "{" || ch === "[" || ch === "(") d++;
213
+ else if (ch === "}" || ch === "]" || ch === ")") d--;
214
+ if (ch === "," && d === 0) {
215
+ const k = cur.trim();
216
+ if (k) keys.push(extractKey(k));
217
+ cur = "";
218
+ } else cur += ch;
219
+ }
220
+ const last = cur.trim();
221
+ if (last) keys.push(extractKey(last));
222
+ objectKeys = keys.filter(Boolean);
223
+ }
224
+ const inference = {
225
+ params,
226
+ isAsync,
227
+ isObjectParam,
228
+ objectKeys,
229
+ confidence: "low"
230
+ };
231
+ const schema = {
232
+ type: "object",
233
+ properties: {},
234
+ required: [],
235
+ additionalProperties: false
236
+ };
237
+ if (isObjectParam && objectKeys.length > 0) {
238
+ const props = {};
239
+ const required = [];
240
+ const innerRaw = params[0].name.slice(1, -1);
241
+ const entries = splitObjectEntries(innerRaw);
242
+ for (const entry of entries) {
243
+ const { key, hasDefault, defaultValue } = entry;
244
+ const prop = {};
245
+ if (typeof defaultValue === "number") prop.type = "number";
246
+ else if (typeof defaultValue === "boolean") prop.type = "boolean";
247
+ if (hasDefault && defaultValue !== void 0) prop.default = defaultValue;
248
+ props[key] = prop;
249
+ if (!hasDefault) required.push(key);
250
+ }
251
+ schema.properties = props;
252
+ schema.required = required;
253
+ } else if (params.length > 0) {
254
+ const props = {};
255
+ const required = [];
256
+ for (const p of params) {
257
+ let key = p.name;
258
+ if (key.includes(":")) key = key.split(":")[0].trim();
259
+ if (key.includes("=")) key = key.split("=")[0].trim();
260
+ if (key.startsWith("{") || key.startsWith("[")) continue;
261
+ if (!key) continue;
262
+ const prop = {};
263
+ if (typeof p.defaultValue === "number") prop.type = "number";
264
+ else if (typeof p.defaultValue === "boolean") prop.type = "boolean";
265
+ if (p.hasDefault && p.defaultValue !== void 0) prop.default = p.defaultValue;
266
+ props[key] = prop;
267
+ if (!p.hasDefault) required.push(key);
268
+ }
269
+ schema.properties = props;
270
+ schema.required = required;
271
+ }
272
+ return { inference, schema };
273
+ }
274
+ function extractKey(raw) {
275
+ let s = raw.trim();
276
+ const eq = s.indexOf("=");
277
+ if (eq !== -1) s = s.slice(0, eq).trim();
278
+ const colon = s.indexOf(":");
279
+ if (colon !== -1) s = s.slice(0, colon).trim();
280
+ if (s.startsWith("...")) s = s.slice(3).trim();
281
+ s = s.replace(/^{|}$/g, "").trim();
282
+ return s.split(/\s+/)[0] || "";
283
+ }
284
+ function splitObjectEntries(inner) {
285
+ const res = [];
286
+ let cur = "";
287
+ let d = 0;
288
+ for (let i = 0; i < inner.length; i++) {
289
+ const ch = inner[i];
290
+ if (ch === "{" || ch === "[" || ch === "(") d++;
291
+ else if (ch === "}" || ch === "]" || ch === ")") d--;
292
+ if (ch === "," && d === 0) {
293
+ if (cur.trim()) res.push(parseObjectEntry(cur.trim()));
294
+ cur = "";
295
+ } else cur += ch;
296
+ }
297
+ if (cur.trim()) res.push(parseObjectEntry(cur.trim()));
298
+ return res;
299
+ }
300
+ function parseObjectEntry(raw) {
301
+ let s = raw.trim();
302
+ let hasDefault = false;
303
+ let defaultValue;
304
+ let eqIdx = s.indexOf("=");
305
+ if (eqIdx !== -1) {
306
+ hasDefault = true;
307
+ const def = s.slice(eqIdx + 1).trim();
308
+ defaultValue = parseDefaultLiteral(def);
309
+ s = s.slice(0, eqIdx).trim();
310
+ }
311
+ const colon = s.indexOf(":");
312
+ if (colon !== -1) s = s.slice(0, colon).trim();
313
+ const key = extractKey(s);
314
+ return { key, hasDefault, defaultValue };
315
+ }
316
+
317
+ // src/internal/schema.ts
318
+ var ZOD_CONVERTER_KEY = "__simpleWebmcp_zodConverter";
319
+ function getZodConverter() {
320
+ return globalThis[ZOD_CONVERTER_KEY] ?? null;
321
+ }
322
+ function zodLikeToJsonSchema(schema) {
323
+ const conv = getZodConverter();
324
+ if (conv) {
325
+ try {
326
+ return conv(schema);
327
+ } catch {
328
+ return null;
329
+ }
330
+ }
331
+ return null;
332
+ }
333
+ function standardSchemaToJsonSchema(schema) {
334
+ const zodJson = zodLikeToJsonSchema(schema);
335
+ if (zodJson) return zodJson;
336
+ return null;
337
+ }
338
+ function normalizeSchemaInput(schema) {
339
+ if (!schema) return { json: null, standard: null };
340
+ if (looksLikeJsonSchema(schema)) {
341
+ return { json: schema, standard: null };
342
+ }
343
+ if (isStandardSchema(schema)) {
344
+ const std = schema;
345
+ const json = standardSchemaToJsonSchema(std);
346
+ return { json, standard: std };
347
+ }
348
+ const maybeZodJson = zodLikeToJsonSchema(schema);
349
+ if (maybeZodJson) return { json: maybeZodJson, standard: isStandardSchema(schema) ? schema : null };
350
+ if (typeof schema === "object" && schema !== null) {
351
+ return { json: schema, standard: null };
352
+ }
353
+ return { json: null, standard: null };
354
+ }
355
+ function applyFieldsPatch(base, fields) {
356
+ if (!fields || Object.keys(fields).length === 0) return base;
357
+ const result = {
358
+ ...base,
359
+ properties: { ...base.properties || {} },
360
+ required: [...base.required || []]
361
+ };
362
+ if (!result.properties) result.properties = {};
363
+ if (!result.required) result.required = [];
364
+ for (const [key, field] of Object.entries(fields)) {
365
+ const isStd = isStandardSchema(field);
366
+ if (isStd) {
367
+ const json = standardSchemaToJsonSchema(field);
368
+ const fragment = json ?? { type: "string" };
369
+ result.properties[key] = fragment;
370
+ const anyField = field;
371
+ const isOpt = anyField?._def?.typeName === "ZodOptional" || anyField?.isOptional?.() === true;
372
+ if (!isOpt) {
373
+ if (!result.required.includes(key)) result.required.push(key);
374
+ } else {
375
+ result.required = result.required.filter((k) => k !== key);
376
+ }
377
+ const desc = anyField?.description ?? anyField?._def?.description;
378
+ if (desc && !result.properties[key].description) result.properties[key].description = desc;
379
+ } else {
380
+ const patch = field;
381
+ const existing = result.properties[key] || {};
382
+ const merged = { ...existing, ...patch };
383
+ result.properties[key] = merged;
384
+ if ("required" in patch) {
385
+ const req = patch.required;
386
+ if (req === false) {
387
+ result.required = result.required.filter((k) => k !== key);
388
+ delete merged.required;
389
+ } else if (req === true) {
390
+ if (!result.required.includes(key)) result.required.push(key);
391
+ delete merged.required;
392
+ }
393
+ } else {
394
+ if (!(key in (base.properties || {})) && !patch.required) {
395
+ const hasDefault = "default" in patch;
396
+ if (!hasDefault && !result.required.includes(key)) result.required.push(key);
397
+ }
398
+ }
399
+ }
400
+ }
401
+ if (result.required && result.required.length === 0) delete result.required;
402
+ return result;
403
+ }
404
+ function buildFinalInputSchema(opts) {
405
+ const wholeNorm = normalizeSchemaInput(opts.wholeSchema);
406
+ let baseJson;
407
+ let standard = wholeNorm.standard;
408
+ if (wholeNorm.json) {
409
+ baseJson = wholeNorm.json;
410
+ } else if (wholeNorm.standard && !wholeNorm.json) {
411
+ baseJson = opts.inferred;
412
+ } else {
413
+ baseJson = opts.inferred;
414
+ }
415
+ if (!baseJson.type) baseJson.type = "object";
416
+ if (!baseJson.properties) baseJson.properties = {};
417
+ if (baseJson.additionalProperties == null) baseJson.additionalProperties = false;
418
+ const patched = applyFieldsPatch(baseJson, opts.fields);
419
+ return { json: patched, standard };
420
+ }
421
+ function normalizeOutputSchema(schema) {
422
+ if (!schema) return {};
423
+ const norm = normalizeSchemaInput(schema);
424
+ if (norm.json) return { json: norm.json, standard: norm.standard ?? void 0 };
425
+ if (norm.standard) return { standard: norm.standard };
426
+ return {};
427
+ }
428
+
429
+ // src/internal/registry.ts
430
+ var Registry = class {
431
+ constructor() {
432
+ this.entries = /* @__PURE__ */ new Map();
433
+ }
434
+ // expose for devtools/tests — now includes contract for inspect
435
+ list() {
436
+ return Array.from(this.entries.values()).map((e) => ({ name: e.name, status: e.status, contract: e.contract }));
437
+ }
438
+ get(name) {
439
+ return this.entries.get(name);
440
+ }
441
+ getContract(name) {
442
+ return this.entries.get(name)?.contract;
443
+ }
444
+ clear() {
445
+ for (const e of this.entries.values()) {
446
+ try {
447
+ e.controller.abort();
448
+ } catch {
449
+ }
450
+ }
451
+ this.entries.clear();
452
+ }
453
+ isRegistered(name) {
454
+ return this.entries.get(name)?.status === "registered";
455
+ }
456
+ getStatus(name) {
457
+ return this.entries.get(name)?.status ?? "unregistered";
458
+ }
459
+ async register(contract, opts) {
460
+ const name = contract.name;
461
+ const existing = this.entries.get(name);
462
+ if (existing && (existing.status === "registered" || existing.status === "registering")) {
463
+ return existing.unregister;
464
+ }
465
+ if (!isWebMCPSupported()) {
466
+ if (opts?.signal?.aborted) return () => {
467
+ };
468
+ return () => {
469
+ };
470
+ }
471
+ const modelContext = getModelContext();
472
+ if (!modelContext) throw new NotSupportedError();
473
+ const controller = new AbortController();
474
+ if (opts?.signal) {
475
+ if (opts.signal.aborted) {
476
+ controller.abort();
477
+ } else {
478
+ opts.signal.addEventListener(
479
+ "abort",
480
+ () => {
481
+ try {
482
+ controller.abort();
483
+ } catch {
484
+ }
485
+ },
486
+ { once: true }
487
+ );
488
+ }
489
+ }
490
+ const entry = {
491
+ name,
492
+ controller,
493
+ status: "registering",
494
+ promise: null,
495
+ unregister: () => {
496
+ if (entry.status === "registered" || entry.status === "registering") {
497
+ entry.status = "unregistering";
498
+ try {
499
+ controller.abort();
500
+ } catch {
501
+ }
502
+ entry.status = "unregistered";
503
+ this.entries.delete(name);
504
+ }
505
+ },
506
+ contract,
507
+ execute: opts?.execute
508
+ };
509
+ this.entries.set(name, entry);
510
+ const wrappedExecute = opts?.execute ?? (async (args) => ({ content: [{ type: "text", text: `no execute for ${name}` }] }));
511
+ try {
512
+ const regPromise = modelContext.registerTool(
513
+ {
514
+ name: contract.name,
515
+ description: contract.description,
516
+ inputSchema: contract.inputSchema,
517
+ outputSchema: contract.outputSchema,
518
+ annotations: contract.annotations,
519
+ execute: wrappedExecute
520
+ },
521
+ { signal: controller.signal }
522
+ );
523
+ entry.promise = regPromise;
524
+ await regPromise;
525
+ if (controller.signal.aborted) {
526
+ entry.status = "unregistered";
527
+ this.entries.delete(name);
528
+ } else {
529
+ entry.status = "registered";
530
+ }
531
+ controller.signal.addEventListener("abort", () => {
532
+ entry.status = "unregistered";
533
+ this.entries.delete(name);
534
+ }, { once: true });
535
+ return entry.unregister;
536
+ } catch (err) {
537
+ entry.status = "error";
538
+ this.entries.delete(name);
539
+ const msg = err?.message || String(err);
540
+ if (err?.name === "NotAllowedError" || /NotAllowedError|Permissions Policy|blocked/i.test(msg)) {
541
+ throw new NotAllowedError(msg, { cause: err });
542
+ }
543
+ throw new RegistrationError(`Failed to register tool "${name}": ${msg}`, { cause: err });
544
+ }
545
+ }
546
+ unregister(name) {
547
+ const entry = this.entries.get(name);
548
+ if (!entry) return;
549
+ try {
550
+ entry.controller.abort();
551
+ } catch {
552
+ }
553
+ this.entries.delete(name);
554
+ }
555
+ };
556
+ var REGISTRY_KEY = "__simpleWebmcpRegistry";
557
+ var globalAny = globalThis;
558
+ var registry = globalAny[REGISTRY_KEY] ?? new Registry();
559
+ if (!globalAny[REGISTRY_KEY]) {
560
+ globalAny[REGISTRY_KEY] = registry;
561
+ }
562
+
563
+ // src/internal/normalize.ts
564
+ function normalizeResult(value) {
565
+ if (value == null) {
566
+ return { content: [{ type: "text", text: "" }] };
567
+ }
568
+ if (typeof value === "string") {
569
+ return { content: [{ type: "text", text: value }] };
570
+ }
571
+ if (typeof value === "number" || typeof value === "boolean") {
572
+ return { content: [{ type: "text", text: String(value) }] };
573
+ }
574
+ if (typeof value === "object" && value !== null && "content" in value && Array.isArray(value.content)) {
575
+ return value;
576
+ }
577
+ if (typeof value === "object" && value !== null && "isError" in value) {
578
+ if ("content" in value) return value;
579
+ }
580
+ try {
581
+ const text = JSON.stringify(value, null, 2);
582
+ return { content: [{ type: "text", text }] };
583
+ } catch {
584
+ return { content: [{ type: "text", text: String(value) }] };
585
+ }
586
+ }
587
+ function normalizeError(err) {
588
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : (() => {
589
+ try {
590
+ return JSON.stringify(err);
591
+ } catch {
592
+ return String(err);
593
+ }
594
+ })();
595
+ return {
596
+ content: [{ type: "text", text: `Error: ${message}` }],
597
+ isError: true
598
+ };
599
+ }
600
+ function wrapExecute(fn, opts) {
601
+ return async (args, _ctx) => {
602
+ try {
603
+ let result;
604
+ const mode = opts?.argMode ?? "object";
605
+ if (mode === "spread" && args && typeof args === "object" && !Array.isArray(args)) ; else {
606
+ result = await fn(args);
607
+ }
608
+ return normalizeResult(result);
609
+ } catch (err) {
610
+ return normalizeError(err);
611
+ }
612
+ };
613
+ }
614
+
615
+ // src/webmcp.ts
616
+ function resolveScope(opts) {
617
+ if (opts?.scope) return opts.scope;
618
+ if (opts?.global) return "global";
619
+ return "scoped";
620
+ }
621
+ function webmcp(fn, options) {
622
+ if (typeof fn !== "function") {
623
+ throw new ConfigurationError("webmcp(fn, opts) \u2014 first argument must be a function");
624
+ }
625
+ const anyFn = fn;
626
+ if (anyFn.__webmcpBrand === true && anyFn.definition) {
627
+ if (!options || Object.keys(options).length === 0) return anyFn;
628
+ const original = anyFn.__fn ?? fn;
629
+ return webmcp(original, { ...anyFn.__webmcpOptions || {}, ...options });
630
+ }
631
+ const name = options?.name ?? toSnakeCase(getFunctionName(fn));
632
+ if (!name) throw new ConfigurationError('Tool name could not be inferred \u2014 pass {name:"my_tool"}');
633
+ let description = options?.description ?? anyFn.__webmcpDescription ?? "";
634
+ if (!description) {
635
+ warnNoDescription(fn);
636
+ description = "";
637
+ }
638
+ const hasSchema = !!options?.schema;
639
+ const hasFields = !!options?.fields && Object.keys(options.fields).length > 0;
640
+ if (options?.strict && !hasSchema && !hasFields) {
641
+ const { schema: inferred } = inferRuntime(fn);
642
+ const props = inferred.properties || {};
643
+ const propKeys = Object.keys(props);
644
+ const hasTypedProps = propKeys.length > 0 && propKeys.some((k) => !!props[k]?.type);
645
+ if (!hasTypedProps) {
646
+ throw new ConfigurationError(
647
+ `webmcp strict: could not infer schema for "${name}" \u2014 add TypeScript types, JSDoc, {schema} or {fields}`
648
+ );
649
+ }
650
+ }
651
+ const { schema: inferredSchema } = inferRuntime(fn);
652
+ const { json: finalInput, standard } = buildFinalInputSchema({
653
+ wholeSchema: options?.schema,
654
+ inferred: inferredSchema,
655
+ fields: options?.fields
656
+ });
657
+ const outNorm = normalizeOutputSchema(options?.outputSchema);
658
+ const annotations = options?.annotations;
659
+ const contract = {
660
+ name,
661
+ description,
662
+ inputSchema: finalInput,
663
+ outputSchema: outNorm.json,
664
+ annotations
665
+ };
666
+ const enabled = options?.enabled ?? true;
667
+ const wrapper = function(...args) {
668
+ return fn.apply(this, args);
669
+ };
670
+ try {
671
+ Object.defineProperty(wrapper, "name", { value: fn.name || name, configurable: true });
672
+ } catch {
673
+ }
674
+ try {
675
+ Object.defineProperty(wrapper, "length", { value: fn.length, configurable: true });
676
+ } catch {
677
+ }
678
+ let status = "unregistered";
679
+ let registrationPromise = null;
680
+ let unregisterFn = null;
681
+ let activeController = null;
682
+ const wrappedExec = wrapExecute(fn);
683
+ const toolWrapper = wrapper;
684
+ toolWrapper.__webmcpBrand = true;
685
+ toolWrapper.__fn = fn;
686
+ toolWrapper.__webmcpOptions = options;
687
+ if (standard) toolWrapper.__standardSchema = standard;
688
+ if (outNorm.standard) toolWrapper.__outputStandardSchema = outNorm.standard;
689
+ toolWrapper.tool = contract;
690
+ toolWrapper.definition = contract;
691
+ Object.defineProperty(toolWrapper, "status", {
692
+ get() {
693
+ return status;
694
+ },
695
+ enumerable: true,
696
+ configurable: true
697
+ });
698
+ Object.defineProperty(toolWrapper, "registration", {
699
+ get() {
700
+ return registrationPromise;
701
+ },
702
+ enumerable: true,
703
+ configurable: true
704
+ });
705
+ toolWrapper.isRegistered = () => status === "registered";
706
+ toolWrapper.register = async (opts) => {
707
+ if (status === "registered" || status === "registering") {
708
+ return unregisterFn ?? (() => toolWrapper.unregister());
709
+ }
710
+ if (enabled === false) {
711
+ return () => {
712
+ };
713
+ }
714
+ if (!isWebMCPSupported()) {
715
+ status = "unsupported";
716
+ return () => {
717
+ status = "unregistered";
718
+ };
719
+ }
720
+ status = "registering";
721
+ const controller = new AbortController();
722
+ activeController = controller;
723
+ if (opts?.signal) {
724
+ if (opts.signal.aborted) {
725
+ controller.abort();
726
+ status = "unregistered";
727
+ activeController = null;
728
+ return () => {
729
+ };
730
+ }
731
+ opts.signal.addEventListener("abort", () => {
732
+ try {
733
+ controller.abort();
734
+ } catch {
735
+ }
736
+ toolWrapper.unregister();
737
+ }, { once: true });
738
+ }
739
+ controller.signal.addEventListener("abort", () => {
740
+ if (status !== "unregistered") status = "unregistered";
741
+ activeController = null;
742
+ }, { once: true });
743
+ try {
744
+ const unregister = await registry.register(contract, {
745
+ signal: controller.signal,
746
+ execute: wrappedExec
747
+ });
748
+ unregisterFn = unregister;
749
+ registrationPromise = Promise.resolve();
750
+ if (controller.signal.aborted) {
751
+ status = "unregistered";
752
+ } else if (!isWebMCPSupported()) {
753
+ status = "unsupported";
754
+ } else {
755
+ status = "registered";
756
+ }
757
+ return () => {
758
+ try {
759
+ unregister();
760
+ } catch {
761
+ }
762
+ status = "unregistered";
763
+ activeController = null;
764
+ unregisterFn = null;
765
+ };
766
+ } catch (err) {
767
+ if (err instanceof NotSupportedError || err?.code === "NOT_SUPPORTED" || err?.name === "NotSupportedError") {
768
+ status = "unsupported";
769
+ } else {
770
+ status = "error";
771
+ }
772
+ registrationPromise = Promise.reject(err);
773
+ if (err instanceof SimpleWebMCPError) throw err;
774
+ throw err;
775
+ }
776
+ };
777
+ toolWrapper.unregister = () => {
778
+ if (status === "unregistered") return;
779
+ status = "unregistering";
780
+ try {
781
+ registry.unregister(contract.name);
782
+ } catch {
783
+ }
784
+ try {
785
+ activeController?.abort();
786
+ } catch {
787
+ }
788
+ status = "unregistered";
789
+ activeController = null;
790
+ unregisterFn = null;
791
+ registrationPromise = null;
792
+ };
793
+ const scope = resolveScope(options);
794
+ if (scope === "global" && enabled !== false) {
795
+ if (typeof document !== "undefined") {
796
+ queueMicrotask(() => {
797
+ toolWrapper.register().catch(() => {
798
+ });
799
+ });
800
+ }
801
+ }
802
+ toolWrapper.toString = fn.toString.bind(fn);
803
+ return toolWrapper;
804
+ }
805
+ webmcp.global = function global(fn, opts) {
806
+ return webmcp(fn, { ...opts, global: true });
807
+ };
808
+ webmcp.isWebMCPTool = function isWebMCPTool(v) {
809
+ return !!v?.__webmcpBrand;
810
+ };
10
811
 
11
812
  // src/react/useWebMCP.ts
12
813
  function isTool(v) {
13
814
  return !!v?.__webmcpBrand;
14
815
  }
15
- function useWebMCP(tool, opts) {
816
+ function useWebMCP(arg, opts) {
16
817
  const enabled = opts?.enabled ?? true;
818
+ const isRawFunction = useMemo(() => {
819
+ return typeof arg === "function" && !isTool(arg);
820
+ }, [arg]);
821
+ const tool = useMemo(() => {
822
+ if (isRawFunction) {
823
+ const { enabled: _e, ...webmcpOpts } = opts ?? {};
824
+ return webmcp(arg, webmcpOpts);
825
+ }
826
+ return arg;
827
+ }, [
828
+ arg,
829
+ isRawFunction,
830
+ opts?.name,
831
+ opts?.description,
832
+ opts?.schema,
833
+ opts?.fields,
834
+ opts?.annotations,
835
+ opts?.scope,
836
+ opts?.global,
837
+ opts?.strict,
838
+ opts?.outputSchema,
839
+ opts?.enabled
840
+ ]);
17
841
  const [registered, setRegistered] = useState(false);
18
842
  const [error, setError] = useState(null);
19
843
  const supported = useMemo(() => isWebMCPSupported(), []);
@@ -30,16 +854,7 @@ function useWebMCP(tool, opts) {
30
854
  setError(null);
31
855
  return;
32
856
  }
33
- const t = toolRef.current;
34
- if (!isTool(t)) {
35
- if (typeof console !== "undefined") {
36
- console.warn("[simple-webmcp] useWebMCP expected webmcp(fn) tool, got plain function. Wrap with webmcp() first.");
37
- }
38
- setError(new Error("useWebMCP: wrap fn with webmcp() first"));
39
- setRegistered(false);
40
- return;
41
- }
42
- const webTool = t;
857
+ const webTool = toolRef.current;
43
858
  if (webTool.status === "registered") {
44
859
  setRegistered(true);
45
860
  setError(null);
@@ -86,8 +901,30 @@ function useWebMCP(tool, opts) {
86
901
  };
87
902
  }, [enabled, supported, tool]);
88
903
  const status = !supported ? "unsupported" : error ? "error" : registered ? "registered" : enabled ? "registering" : "unregistered";
89
- return { supported: !!supported, registered, error, isPolyfilled, status };
904
+ const result = { supported: !!supported, registered, error, isPolyfilled, status };
905
+ if (isRawFunction) {
906
+ const augmented = (...args) => tool(...args);
907
+ Object.assign(augmented, tool);
908
+ Object.setPrototypeOf(augmented, Object.getPrototypeOf(tool));
909
+ for (const [k, v] of Object.entries(result)) {
910
+ Object.defineProperty(augmented, k, {
911
+ value: v,
912
+ writable: true,
913
+ configurable: true,
914
+ enumerable: true
915
+ });
916
+ }
917
+ Object.defineProperty(augmented, "__rawTool", {
918
+ value: tool,
919
+ writable: false,
920
+ configurable: true,
921
+ enumerable: false
922
+ });
923
+ return augmented;
924
+ }
925
+ return result;
90
926
  }
927
+ var useTool = useWebMCP;
91
928
  function ToolRegistrar({ tool, enabled }) {
92
929
  useWebMCP(tool, { enabled });
93
930
  return null;
@@ -101,6 +938,6 @@ function Scope({ tools, enabled = true, children }) {
101
938
  }
102
939
  var WebMCPScope = Scope;
103
940
 
104
- export { Scope, WebMCPScope, useWebMCP };
941
+ export { Scope, WebMCPScope, useTool, useWebMCP };
105
942
  //# sourceMappingURL=react.js.map
106
943
  //# sourceMappingURL=react.js.map