@siteverb/webmcp 0.1.0-rc.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 (95) hide show
  1. package/README.md +163 -0
  2. package/dist/browser.js +841 -0
  3. package/dist/client.d.ts +3 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +320 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/evals/index.d.ts +13 -0
  8. package/dist/evals/index.d.ts.map +1 -0
  9. package/dist/evals/index.js +20 -0
  10. package/dist/evals/index.js.map +1 -0
  11. package/dist/fingerprint.d.ts +3 -0
  12. package/dist/fingerprint.d.ts.map +1 -0
  13. package/dist/fingerprint.js +33 -0
  14. package/dist/fingerprint.js.map +1 -0
  15. package/dist/id.d.ts +2 -0
  16. package/dist/id.d.ts.map +1 -0
  17. package/dist/id.js +15 -0
  18. package/dist/id.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +6 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/src/client.d.ts +3 -0
  24. package/dist/src/client.d.ts.map +1 -0
  25. package/dist/src/client.js +292 -0
  26. package/dist/src/client.js.map +1 -0
  27. package/dist/src/evals/index.d.ts +13 -0
  28. package/dist/src/evals/index.d.ts.map +1 -0
  29. package/dist/src/evals/index.js +20 -0
  30. package/dist/src/evals/index.js.map +1 -0
  31. package/dist/src/fingerprint.d.ts +3 -0
  32. package/dist/src/fingerprint.d.ts.map +1 -0
  33. package/dist/src/fingerprint.js +27 -0
  34. package/dist/src/fingerprint.js.map +1 -0
  35. package/dist/src/index.d.ts +9 -0
  36. package/dist/src/index.d.ts.map +1 -0
  37. package/dist/src/index.js +6 -0
  38. package/dist/src/index.js.map +1 -0
  39. package/dist/src/telemetry.d.ts +21 -0
  40. package/dist/src/telemetry.d.ts.map +1 -0
  41. package/dist/src/telemetry.js +218 -0
  42. package/dist/src/telemetry.js.map +1 -0
  43. package/dist/src/testing/index.d.ts +3 -0
  44. package/dist/src/testing/index.d.ts.map +1 -0
  45. package/dist/src/testing/index.js +2 -0
  46. package/dist/src/testing/index.js.map +1 -0
  47. package/dist/src/testing/memory.d.ts +17 -0
  48. package/dist/src/testing/memory.d.ts.map +1 -0
  49. package/dist/src/testing/memory.js +72 -0
  50. package/dist/src/testing/memory.js.map +1 -0
  51. package/dist/src/transport.d.ts +8 -0
  52. package/dist/src/transport.d.ts.map +1 -0
  53. package/dist/src/transport.js +29 -0
  54. package/dist/src/transport.js.map +1 -0
  55. package/dist/src/types.d.ts +185 -0
  56. package/dist/src/types.d.ts.map +1 -0
  57. package/dist/src/types.js +2 -0
  58. package/dist/src/types.js.map +1 -0
  59. package/dist/src/validation.d.ts +5 -0
  60. package/dist/src/validation.d.ts.map +1 -0
  61. package/dist/src/validation.js +119 -0
  62. package/dist/src/validation.js.map +1 -0
  63. package/dist/src/version.d.ts +2 -0
  64. package/dist/src/version.d.ts.map +1 -0
  65. package/dist/src/version.js +2 -0
  66. package/dist/src/version.js.map +1 -0
  67. package/dist/telemetry.d.ts +21 -0
  68. package/dist/telemetry.d.ts.map +1 -0
  69. package/dist/telemetry.js +276 -0
  70. package/dist/telemetry.js.map +1 -0
  71. package/dist/testing/index.d.ts +3 -0
  72. package/dist/testing/index.d.ts.map +1 -0
  73. package/dist/testing/index.js +2 -0
  74. package/dist/testing/index.js.map +1 -0
  75. package/dist/testing/memory.d.ts +17 -0
  76. package/dist/testing/memory.d.ts.map +1 -0
  77. package/dist/testing/memory.js +72 -0
  78. package/dist/testing/memory.js.map +1 -0
  79. package/dist/transport.d.ts +7 -0
  80. package/dist/transport.d.ts.map +1 -0
  81. package/dist/transport.js +55 -0
  82. package/dist/transport.js.map +1 -0
  83. package/dist/types.d.ts +189 -0
  84. package/dist/types.d.ts.map +1 -0
  85. package/dist/types.js +2 -0
  86. package/dist/types.js.map +1 -0
  87. package/dist/validation.d.ts +5 -0
  88. package/dist/validation.d.ts.map +1 -0
  89. package/dist/validation.js +117 -0
  90. package/dist/validation.js.map +1 -0
  91. package/dist/version.d.ts +2 -0
  92. package/dist/version.d.ts.map +1 -0
  93. package/dist/version.js +2 -0
  94. package/dist/version.js.map +1 -0
  95. package/package.json +57 -0
@@ -0,0 +1,841 @@
1
+ // src/fingerprint.ts
2
+ function canonicalize(value) {
3
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
4
+ if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
5
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`).join(",")}}`;
6
+ }
7
+ function fallbackHash(value) {
8
+ let hash = 0xcbf29ce484222325n;
9
+ for (const byte of new TextEncoder().encode(value)) {
10
+ hash ^= BigInt(byte);
11
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n);
12
+ }
13
+ return `fnv1a64:${hash.toString(16).padStart(16, "0")}`;
14
+ }
15
+ async function schemaFingerprint(schema) {
16
+ const canonical = canonicalize(schema ?? {});
17
+ const fallback = fallbackHash(canonical);
18
+ const subtle = globalThis.crypto?.subtle;
19
+ if (!subtle) return fallback;
20
+ try {
21
+ const digest = await subtle.digest("SHA-256", new TextEncoder().encode(canonical));
22
+ return `sha256:${Array.from(
23
+ new Uint8Array(digest),
24
+ (byte) => byte.toString(16).padStart(2, "0")
25
+ ).join("")}`;
26
+ } catch {
27
+ return fallback;
28
+ }
29
+ }
30
+
31
+ // src/id.ts
32
+ var fallbackSequence = 0;
33
+ function randomId() {
34
+ if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
35
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
36
+ const bytes = new Uint8Array(16);
37
+ globalThis.crypto.getRandomValues(bytes);
38
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
39
+ }
40
+ fallbackSequence += 1;
41
+ return `fallback-${Date.now().toString(36)}-${fallbackSequence.toString(36)}-${Math.random().toString(36).slice(2)}`;
42
+ }
43
+
44
+ // src/transport.ts
45
+ function validateEndpoint(value) {
46
+ if (value.length === 0 || value.length > 2e3 || value !== value.trim() || /[\u0000-\u001f\u007f]/.test(value)) {
47
+ throw new TypeError("Telemetry endpoint must be a bounded URL without controls.");
48
+ }
49
+ if (value.startsWith("/") && !value.startsWith("//")) {
50
+ const base = "https://siteverb.invalid";
51
+ const resolved = new URL(value, base);
52
+ if (resolved.origin !== base || value.includes("?") || value.includes("#")) {
53
+ throw new TypeError("Telemetry endpoint must not contain query or fragment data.");
54
+ }
55
+ return value;
56
+ }
57
+ let url;
58
+ try {
59
+ url = new URL(value);
60
+ } catch {
61
+ throw new TypeError("Telemetry endpoint must be a relative, HTTPS, or loopback HTTP URL.");
62
+ }
63
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
64
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.search || url.hash) {
65
+ throw new TypeError(
66
+ "Telemetry endpoint must use HTTPS or loopback HTTP without credentials, query, or fragment data."
67
+ );
68
+ }
69
+ return url.href;
70
+ }
71
+ function createFetchTelemetryTransport(options) {
72
+ if (typeof options.endpoint !== "string")
73
+ throw new TypeError("A telemetry endpoint is required.");
74
+ const endpoint = validateEndpoint(options.endpoint);
75
+ const fetcher = options.fetch ?? globalThis.fetch?.bind(globalThis);
76
+ return Object.freeze({
77
+ async send(batch, _reason) {
78
+ const body = JSON.stringify(batch);
79
+ if (!fetcher) throw new Error("fetch is unavailable for the telemetry transport.");
80
+ const response = await fetcher(endpoint, {
81
+ body,
82
+ credentials: "omit",
83
+ headers: { "content-type": "application/json" },
84
+ keepalive: true,
85
+ method: "POST"
86
+ });
87
+ if (!response.ok) throw new Error(`Telemetry endpoint returned HTTP ${response.status}.`);
88
+ }
89
+ });
90
+ }
91
+
92
+ // src/version.ts
93
+ var SITEVERB_WEBMCP_VERSION = "0.1.0-rc.0";
94
+
95
+ // src/telemetry.ts
96
+ function resolveSessionId(siteId) {
97
+ const key = `__siteverb_session:${siteId}`;
98
+ try {
99
+ const existing = globalThis.sessionStorage?.getItem(key);
100
+ if (existing) return existing;
101
+ const created = randomId();
102
+ globalThis.sessionStorage?.setItem(key, created);
103
+ return created;
104
+ } catch {
105
+ return randomId();
106
+ }
107
+ }
108
+ function resolveRouteTemplate(route) {
109
+ try {
110
+ return typeof route === "function" ? route() : route;
111
+ } catch {
112
+ return void 0;
113
+ }
114
+ }
115
+ function boundedLabel(value, name, maxLength) {
116
+ if (value === void 0) return void 0;
117
+ if (value.length === 0 || value.length > maxLength || /[\u0000-\u001f\u007f]/.test(value)) {
118
+ throw new TypeError(`${name} must contain 1-${maxLength} characters without controls.`);
119
+ }
120
+ return value;
121
+ }
122
+ function safeRouteTemplate(value) {
123
+ if (typeof value !== "string" || value.length === 0 || value.length > 500 || !value.startsWith("/") || value.startsWith("//") || /[?#\u0000-\u001f\u007f]/.test(value)) {
124
+ return void 0;
125
+ }
126
+ return value;
127
+ }
128
+ function positiveInteger(value, fallback, name) {
129
+ if (value === void 0) return fallback;
130
+ if (!Number.isSafeInteger(value) || value <= 0) {
131
+ throw new TypeError(`${name} must be a positive integer.`);
132
+ }
133
+ return value;
134
+ }
135
+ function report(options, diagnostic2) {
136
+ try {
137
+ options.onDiagnostic?.(diagnostic2);
138
+ } catch {
139
+ }
140
+ }
141
+ function disabledManager() {
142
+ return Object.freeze({
143
+ enabled: false,
144
+ emit: () => void 0,
145
+ flush: async () => void 0,
146
+ nextSequence: () => 0,
147
+ status: () => ({
148
+ autoRetryPaused: false,
149
+ consecutiveFailures: 0,
150
+ droppedEvents: 0,
151
+ enabled: false,
152
+ pendingEvents: 0
153
+ }),
154
+ dispose: () => void 0
155
+ });
156
+ }
157
+ function createTelemetryManager(options, webmcpSupported) {
158
+ if (!options.telemetry) return disabledManager();
159
+ const siteId = boundedLabel(options.siteId, "siteId", 128);
160
+ if (!siteId) throw new TypeError("siteId is required when telemetry is enabled.");
161
+ const environment = boundedLabel(options.environment, "environment", 64);
162
+ const release = boundedLabel(options.release, "release", 200);
163
+ const telemetry = options.telemetry;
164
+ const transport = telemetry.transport ?? (telemetry.endpoint ? createFetchTelemetryTransport({ endpoint: telemetry.endpoint }) : (() => {
165
+ throw new TypeError("telemetry requires either a transport or endpoint.");
166
+ })());
167
+ const flushIntervalMs = positiveInteger(
168
+ telemetry.flushIntervalMs,
169
+ 5e3,
170
+ "telemetry.flushIntervalMs"
171
+ );
172
+ const maxBatchSize = positiveInteger(telemetry.maxBatchSize, 20, "telemetry.maxBatchSize");
173
+ const maxQueueSize = positiveInteger(telemetry.maxQueueSize, 1e3, "telemetry.maxQueueSize");
174
+ const maxRetryAttempts = positiveInteger(
175
+ telemetry.maxRetryAttempts,
176
+ 5,
177
+ "telemetry.maxRetryAttempts"
178
+ );
179
+ const retryMaxDelayMs = positiveInteger(
180
+ telemetry.retryMaxDelayMs,
181
+ 6e4,
182
+ "telemetry.retryMaxDelayMs"
183
+ );
184
+ if (maxBatchSize > maxQueueSize) {
185
+ throw new TypeError("telemetry.maxBatchSize cannot exceed telemetry.maxQueueSize.");
186
+ }
187
+ const pageSessionId = resolveSessionId(siteId);
188
+ let queue = [];
189
+ let pendingBatch;
190
+ let inFlight;
191
+ let timer;
192
+ let disposed = false;
193
+ let autoRetryPaused = false;
194
+ let consecutiveFailures = 0;
195
+ let droppedEvents = 0;
196
+ let eventIndex = 0;
197
+ let sequenceIndex = 0;
198
+ let invalidRouteTemplateReported = false;
199
+ const schedule = (delayMs = flushIntervalMs) => {
200
+ if (disposed || autoRetryPaused || timer || queue.length === 0 && !pendingBatch) return;
201
+ timer = setTimeout(() => {
202
+ timer = void 0;
203
+ void flush("interval").catch(() => void 0);
204
+ }, delayMs);
205
+ };
206
+ const emit = (event, fields = {}) => {
207
+ if (disposed) return;
208
+ if (queue.length + (pendingBatch?.events.length ?? 0) >= maxQueueSize) {
209
+ droppedEvents += 1;
210
+ report(options, {
211
+ code: "telemetry-dropped",
212
+ message: "A Siteverb telemetry event was dropped because the bounded queue is full.",
213
+ severity: "warning"
214
+ });
215
+ return;
216
+ }
217
+ const candidateRouteTemplate = resolveRouteTemplate(options.routeTemplate);
218
+ const routeTemplate = safeRouteTemplate(candidateRouteTemplate);
219
+ if (candidateRouteTemplate !== void 0 && routeTemplate === void 0 && !invalidRouteTemplateReported) {
220
+ invalidRouteTemplateReported = true;
221
+ report(options, {
222
+ code: "telemetry-route-template-invalid",
223
+ message: "Siteverb omitted an invalid route template; use a bounded pathname template without query or fragment data.",
224
+ severity: "warning"
225
+ });
226
+ }
227
+ queue.push(
228
+ Object.freeze({
229
+ dataClass: "production",
230
+ event,
231
+ eventId: randomId(),
232
+ eventIndex: eventIndex++,
233
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
234
+ pageSessionId,
235
+ ...environment === void 0 ? {} : { environment },
236
+ ...release === void 0 ? {} : { release },
237
+ ...routeTemplate === void 0 ? {} : { routeTemplate },
238
+ ...fields
239
+ })
240
+ );
241
+ if (queue.length >= maxBatchSize) void flush("size").catch(() => void 0);
242
+ else schedule();
243
+ };
244
+ const flush = async (reason = "manual") => {
245
+ if (timer) {
246
+ clearTimeout(timer);
247
+ timer = void 0;
248
+ }
249
+ if (inFlight) {
250
+ await inFlight;
251
+ if ((reason === "manual" || reason === "lifecycle") && queue.length > 0) {
252
+ return flush(reason);
253
+ }
254
+ return;
255
+ }
256
+ if (!pendingBatch && queue.length === 0) return;
257
+ if (reason === "manual" || reason === "lifecycle") autoRetryPaused = false;
258
+ const batch = pendingBatch ?? Object.freeze({
259
+ batchId: randomId(),
260
+ events: Object.freeze(queue.splice(0, maxBatchSize)),
261
+ protocolVersion: 1,
262
+ sdkVersion: SITEVERB_WEBMCP_VERSION,
263
+ sentAt: (/* @__PURE__ */ new Date()).toISOString(),
264
+ siteId
265
+ });
266
+ pendingBatch = batch;
267
+ const attempt = (async () => {
268
+ try {
269
+ await transport.send(batch, reason);
270
+ if (pendingBatch?.batchId === batch.batchId) pendingBatch = void 0;
271
+ consecutiveFailures = 0;
272
+ autoRetryPaused = false;
273
+ } catch (cause) {
274
+ consecutiveFailures += 1;
275
+ autoRetryPaused = consecutiveFailures >= maxRetryAttempts;
276
+ report(options, {
277
+ cause,
278
+ code: "telemetry-send-failed",
279
+ message: autoRetryPaused ? "Siteverb paused automatic telemetry retries; call flush() to retry the retained batch." : "Siteverb could not deliver a telemetry batch; the same batch will be retried.",
280
+ severity: "warning"
281
+ });
282
+ if (!autoRetryPaused) {
283
+ schedule(
284
+ Math.min(flushIntervalMs * 2 ** Math.max(0, consecutiveFailures - 1), retryMaxDelayMs)
285
+ );
286
+ }
287
+ throw cause;
288
+ }
289
+ })();
290
+ let trackedAttempt;
291
+ trackedAttempt = attempt.finally(() => {
292
+ if (inFlight === trackedAttempt) inFlight = void 0;
293
+ });
294
+ inFlight = trackedAttempt;
295
+ await trackedAttempt;
296
+ if ((reason === "manual" || reason === "lifecycle") && queue.length > 0) {
297
+ return flush(reason);
298
+ }
299
+ schedule();
300
+ };
301
+ const lifecycleFlush = () => {
302
+ void flush("lifecycle").catch(() => void 0);
303
+ };
304
+ const visibilityFlush = () => {
305
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") lifecycleFlush();
306
+ };
307
+ globalThis.addEventListener?.("pagehide", lifecycleFlush);
308
+ globalThis.document?.addEventListener?.("visibilitychange", visibilityFlush);
309
+ const manager = {
310
+ enabled: true,
311
+ emit,
312
+ flush,
313
+ nextSequence: () => sequenceIndex++,
314
+ status: () => ({
315
+ autoRetryPaused,
316
+ consecutiveFailures,
317
+ droppedEvents,
318
+ enabled: true,
319
+ pendingEvents: queue.length + (pendingBatch?.events.length ?? 0)
320
+ }),
321
+ dispose: () => {
322
+ if (disposed) return;
323
+ disposed = true;
324
+ if (timer) clearTimeout(timer);
325
+ timer = void 0;
326
+ globalThis.removeEventListener?.("pagehide", lifecycleFlush);
327
+ globalThis.document?.removeEventListener?.("visibilitychange", visibilityFlush);
328
+ lifecycleFlush();
329
+ }
330
+ };
331
+ emit("surface_loaded", { webmcpSupported });
332
+ return Object.freeze(manager);
333
+ }
334
+ function classifyTelemetryError(error, signal) {
335
+ if (signal.aborted || error instanceof DOMException && error.name === "AbortError")
336
+ return "abort";
337
+ const name = error instanceof Error ? error.name.toLowerCase().replaceAll(/[-_]/g, "") : "";
338
+ if (name === "timeouterror") return "timeout";
339
+ if (name === "networkerror") return "network";
340
+ if (name === "notfounderror") return "not-found";
341
+ if (name === "notallowederror" || name === "securityerror") return "permission";
342
+ if (name === "validationerror" || name === "invalidstateerror") return "validation";
343
+ return "unknown";
344
+ }
345
+
346
+ // src/validation.ts
347
+ var DEFINED_TOOL = /* @__PURE__ */ Symbol("siteverb.defined-tool");
348
+ var STABLE_ID_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)+$/;
349
+ var TOOL_NAME_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
350
+ function diagnostic(code, message, path, severity) {
351
+ return { code, message, path, severity };
352
+ }
353
+ function inspectParameterDescriptions(value, path, diagnostics) {
354
+ if (value === null || typeof value !== "object") return;
355
+ if (Array.isArray(value)) {
356
+ value.forEach(
357
+ (entry, index) => inspectParameterDescriptions(entry, `${path}[${index}]`, diagnostics)
358
+ );
359
+ return;
360
+ }
361
+ const record = value;
362
+ if (typeof record.description === "string" && record.description.length > 150) {
363
+ diagnostics.push(
364
+ diagnostic(
365
+ "parameter-description-budget",
366
+ "Parameter descriptions should stay within 150 characters for current agent clients.",
367
+ `${path}.description`,
368
+ "warning"
369
+ )
370
+ );
371
+ }
372
+ for (const [key, entry] of Object.entries(record)) {
373
+ inspectParameterDescriptions(entry, `${path}.${key}`, diagnostics);
374
+ }
375
+ }
376
+ function inspectAnnotations(annotations, diagnostics) {
377
+ if (annotations === void 0) return;
378
+ if (annotations.readOnlyHint !== void 0 && typeof annotations.readOnlyHint !== "boolean" || annotations.untrustedContentHint !== void 0 && typeof annotations.untrustedContentHint !== "boolean") {
379
+ diagnostics.push(
380
+ diagnostic(
381
+ "invalid-annotations",
382
+ "WebMCP annotation hints must be booleans.",
383
+ "annotations",
384
+ "error"
385
+ )
386
+ );
387
+ }
388
+ }
389
+ function cloneSchema(schema) {
390
+ if (schema === void 0) return void 0;
391
+ const serialized = JSON.stringify(schema);
392
+ if (serialized === void 0) {
393
+ throw new TypeError("inputSchema must be JSON serializable.");
394
+ }
395
+ return JSON.parse(serialized);
396
+ }
397
+ function deepFreeze(value) {
398
+ if (value === null || typeof value !== "object" || Object.isFrozen(value)) return value;
399
+ Object.freeze(value);
400
+ for (const nested of Object.values(value)) deepFreeze(nested);
401
+ return value;
402
+ }
403
+ function inspectToolDefinition(definition) {
404
+ const diagnostics = [];
405
+ if (typeof definition.id !== "string" || definition.id.length > 128 || !STABLE_ID_PATTERN.test(definition.id)) {
406
+ diagnostics.push(
407
+ diagnostic(
408
+ "invalid-stable-id",
409
+ 'id must be a dot-namespaced stable key such as "catalog.search-products".',
410
+ "id",
411
+ "error"
412
+ )
413
+ );
414
+ }
415
+ if (typeof definition.name !== "string" || !TOOL_NAME_PATTERN.test(definition.name)) {
416
+ diagnostics.push(
417
+ diagnostic(
418
+ "invalid-tool-name",
419
+ "name must contain 1-128 ASCII letters, digits, underscores, hyphens, or periods.",
420
+ "name",
421
+ "error"
422
+ )
423
+ );
424
+ } else if (definition.name.length > 30) {
425
+ diagnostics.push(
426
+ diagnostic(
427
+ "tool-name-budget",
428
+ "Tool names should stay within 30 characters for current agent clients.",
429
+ "name",
430
+ "warning"
431
+ )
432
+ );
433
+ }
434
+ if (typeof definition.description !== "string" || definition.description.trim() === "") {
435
+ diagnostics.push(
436
+ diagnostic(
437
+ "invalid-description",
438
+ "description must be a non-empty string.",
439
+ "description",
440
+ "error"
441
+ )
442
+ );
443
+ } else if (definition.description.length > 500) {
444
+ diagnostics.push(
445
+ diagnostic(
446
+ "description-budget",
447
+ "Tool descriptions should stay within 500 characters for current agent clients.",
448
+ "description",
449
+ "warning"
450
+ )
451
+ );
452
+ }
453
+ if (definition.title !== void 0 && typeof definition.title !== "string") {
454
+ diagnostics.push(
455
+ diagnostic("invalid-title", "title must be a string when provided.", "title", "error")
456
+ );
457
+ }
458
+ if (typeof definition.execute !== "function") {
459
+ diagnostics.push(
460
+ diagnostic("invalid-execute", "execute must be a function.", "execute", "error")
461
+ );
462
+ }
463
+ if (definition.inputSchema !== void 0 && (definition.inputSchema === null || typeof definition.inputSchema !== "object" || Array.isArray(definition.inputSchema))) {
464
+ diagnostics.push(
465
+ diagnostic(
466
+ "invalid-input-schema",
467
+ "inputSchema must be a JSON Schema object when provided.",
468
+ "inputSchema",
469
+ "error"
470
+ )
471
+ );
472
+ } else if (definition.inputSchema !== void 0) {
473
+ try {
474
+ const serialized = JSON.stringify(definition.inputSchema);
475
+ if (serialized === void 0) throw new TypeError("Schema serialized to undefined.");
476
+ inspectParameterDescriptions(definition.inputSchema, "inputSchema", diagnostics);
477
+ } catch {
478
+ diagnostics.push(
479
+ diagnostic(
480
+ "invalid-input-schema",
481
+ "inputSchema must be JSON serializable.",
482
+ "inputSchema",
483
+ "error"
484
+ )
485
+ );
486
+ }
487
+ }
488
+ inspectAnnotations(definition.annotations, diagnostics);
489
+ return Object.freeze(diagnostics);
490
+ }
491
+ function defineTool(definition) {
492
+ const diagnostics = inspectToolDefinition(definition);
493
+ const errors = diagnostics.filter((entry) => entry.severity === "error");
494
+ if (errors.length > 0) {
495
+ throw new TypeError(errors.map((entry) => `${entry.path}: ${entry.message}`).join("\n"));
496
+ }
497
+ const tool = {
498
+ ...definition,
499
+ ...definition.inputSchema === void 0 ? {} : { inputSchema: deepFreeze(cloneSchema(definition.inputSchema)) },
500
+ ...definition.annotations === void 0 ? {} : { annotations: Object.freeze({ ...definition.annotations }) }
501
+ };
502
+ Object.defineProperty(tool, DEFINED_TOOL, {
503
+ enumerable: false,
504
+ value: true
505
+ });
506
+ return Object.freeze(tool);
507
+ }
508
+ function isDefinedTool(value) {
509
+ return Boolean(
510
+ value && typeof value === "object" && value[DEFINED_TOOL] === true
511
+ );
512
+ }
513
+
514
+ // src/client.ts
515
+ function defaultModelContext() {
516
+ if (typeof document === "undefined") return void 0;
517
+ return document.modelContext;
518
+ }
519
+ function resolveModelContext(options) {
520
+ if (typeof options.modelContext === "function") return options.modelContext();
521
+ return options.modelContext ?? defaultModelContext();
522
+ }
523
+ function nativeTool(tool, telemetry, identity) {
524
+ return {
525
+ name: tool.name,
526
+ ...tool.title === void 0 ? {} : { title: tool.title },
527
+ description: tool.description,
528
+ ...tool.inputSchema === void 0 ? {} : { inputSchema: tool.inputSchema },
529
+ ...tool.annotations === void 0 ? {} : { annotations: tool.annotations },
530
+ execute: async (input, executionOptions) => {
531
+ const signal = executionOptions?.signal ?? new AbortController().signal;
532
+ const invocationId = randomId();
533
+ const sequenceIndex = telemetry.nextSequence();
534
+ const telemetryIdentity = await identity;
535
+ const startedAt = globalThis.performance?.now() ?? Date.now();
536
+ telemetry.emit("tool_call_started", {
537
+ invocationId,
538
+ sequenceIndex,
539
+ tool: telemetryIdentity
540
+ });
541
+ try {
542
+ if (signal.aborted) throw abortReason(signal.reason);
543
+ const result = await tool.execute(input, {
544
+ signal,
545
+ invocationId
546
+ });
547
+ telemetry.emit("tool_call_completed", {
548
+ durationMs: Math.max(
549
+ 0,
550
+ Math.round((globalThis.performance?.now() ?? Date.now()) - startedAt)
551
+ ),
552
+ invocationId,
553
+ sequenceIndex,
554
+ tool: telemetryIdentity
555
+ });
556
+ return result;
557
+ } catch (error) {
558
+ const errorClass = classifyTelemetryError(error, signal);
559
+ telemetry.emit(errorClass === "abort" ? "tool_call_cancelled" : "tool_call_failed", {
560
+ durationMs: Math.max(
561
+ 0,
562
+ Math.round((globalThis.performance?.now() ?? Date.now()) - startedAt)
563
+ ),
564
+ errorClass,
565
+ invocationId,
566
+ sequenceIndex,
567
+ tool: telemetryIdentity
568
+ });
569
+ throw error;
570
+ }
571
+ }
572
+ };
573
+ }
574
+ function abortReason(reason) {
575
+ if (reason !== void 0) return reason;
576
+ return new DOMException("The Siteverb registration was removed.", "AbortError");
577
+ }
578
+ function createSiteverb(options = {}) {
579
+ const registrationsById = /* @__PURE__ */ new Map();
580
+ const registrationsByName = /* @__PURE__ */ new Map();
581
+ const telemetry = createTelemetryManager(options, Boolean(resolveModelContext(options)));
582
+ const registerTool = (definition, registerOptions = {}) => {
583
+ const tool = isDefinedTool(definition) ? definition : defineTool(definition);
584
+ if (registrationsById.has(tool.id)) {
585
+ throw new TypeError(`A Siteverb tool with id "${tool.id}" is already registered.`);
586
+ }
587
+ if (registrationsByName.has(tool.name)) {
588
+ throw new TypeError(`A WebMCP tool with name "${tool.name}" is already registered.`);
589
+ }
590
+ const controller = new AbortController();
591
+ const identity = schemaFingerprint(tool.inputSchema).then((schemaHash) => ({
592
+ schemaHash,
593
+ stableKey: tool.id,
594
+ wireName: tool.name
595
+ }));
596
+ let resolvedIdentity;
597
+ void identity.then((value) => {
598
+ resolvedIdentity = value;
599
+ });
600
+ let externalAbort;
601
+ let emittedRegistration = false;
602
+ const mutable = {};
603
+ const unregister = (reason) => {
604
+ if (mutable.status === "unregistered") return;
605
+ controller.abort(abortReason(reason));
606
+ if (emittedRegistration) {
607
+ if (resolvedIdentity) telemetry.emit("tool_unregistered", { tool: resolvedIdentity });
608
+ else {
609
+ void identity.then(
610
+ (toolIdentity) => telemetry.emit("tool_unregistered", { tool: toolIdentity })
611
+ );
612
+ }
613
+ emittedRegistration = false;
614
+ }
615
+ mutable.status = "unregistered";
616
+ registrationsById.delete(tool.id);
617
+ registrationsByName.delete(tool.name);
618
+ if (externalAbort) registerOptions.signal?.removeEventListener("abort", externalAbort);
619
+ };
620
+ const handle = {
621
+ id: tool.id,
622
+ name: tool.name,
623
+ get status() {
624
+ return mutable.status;
625
+ },
626
+ signal: controller.signal,
627
+ unregister
628
+ };
629
+ mutable.handle = handle;
630
+ mutable.status = "pending";
631
+ registrationsById.set(tool.id, mutable);
632
+ registrationsByName.set(tool.name, mutable);
633
+ if (registerOptions.signal) {
634
+ externalAbort = () => unregister(registerOptions.signal?.reason);
635
+ if (registerOptions.signal.aborted) externalAbort();
636
+ else registerOptions.signal.addEventListener("abort", externalAbort, { once: true });
637
+ }
638
+ const ready = (async () => {
639
+ if (controller.signal.aborted) throw controller.signal.reason;
640
+ const context = resolveModelContext(options);
641
+ if (controller.signal.aborted) throw controller.signal.reason;
642
+ if (!context) {
643
+ mutable.status = "unsupported";
644
+ return { id: tool.id, name: tool.name, status: "unsupported" };
645
+ }
646
+ try {
647
+ await context.registerTool(nativeTool(tool, telemetry, identity), {
648
+ signal: controller.signal,
649
+ ...registerOptions.exposedTo === void 0 ? {} : { exposedTo: [...registerOptions.exposedTo] }
650
+ });
651
+ if (controller.signal.aborted) throw controller.signal.reason;
652
+ resolvedIdentity = await identity;
653
+ if (controller.signal.aborted) throw controller.signal.reason;
654
+ mutable.status = "registered";
655
+ emittedRegistration = true;
656
+ telemetry.emit("tool_registered", { tool: resolvedIdentity });
657
+ return { id: tool.id, name: tool.name, status: "registered" };
658
+ } catch (error) {
659
+ if (mutable.status !== "unregistered") mutable.status = "failed";
660
+ registrationsById.delete(tool.id);
661
+ registrationsByName.delete(tool.name);
662
+ const errorClass = classifyTelemetryError(error, controller.signal);
663
+ telemetry.emit(
664
+ errorClass === "abort" ? "tool_registration_cancelled" : "tool_registration_failed",
665
+ {
666
+ errorClass,
667
+ tool: await identity
668
+ }
669
+ );
670
+ throw error;
671
+ }
672
+ })();
673
+ handle.ready = ready;
674
+ void ready.catch(() => void 0);
675
+ return handle;
676
+ };
677
+ const registerTools = (definitions, registerOptions = {}) => {
678
+ const definitionsById = /* @__PURE__ */ new Set();
679
+ const definitionsByName = /* @__PURE__ */ new Set();
680
+ const tools = definitions.map(
681
+ (definition) => isDefinedTool(definition) ? definition : defineTool(definition)
682
+ );
683
+ for (const tool of tools) {
684
+ if (definitionsById.has(tool.id) || registrationsById.has(tool.id)) {
685
+ throw new TypeError(`A Siteverb tool with id "${tool.id}" is already registered.`);
686
+ }
687
+ if (definitionsByName.has(tool.name) || registrationsByName.has(tool.name)) {
688
+ throw new TypeError(`A WebMCP tool with name "${tool.name}" is already registered.`);
689
+ }
690
+ definitionsById.add(tool.id);
691
+ definitionsByName.add(tool.name);
692
+ }
693
+ const controller = new AbortController();
694
+ const handles = [];
695
+ const atomic = registerOptions.atomic ?? true;
696
+ let externalAbort;
697
+ const unregister = (reason) => {
698
+ controller.abort(abortReason(reason));
699
+ for (const handle of handles) handle.unregister(reason);
700
+ if (externalAbort) registerOptions.signal?.removeEventListener("abort", externalAbort);
701
+ };
702
+ if (registerOptions.signal) {
703
+ externalAbort = () => unregister(registerOptions.signal?.reason);
704
+ if (registerOptions.signal.aborted) externalAbort();
705
+ else registerOptions.signal.addEventListener("abort", externalAbort, { once: true });
706
+ }
707
+ try {
708
+ for (const definition of tools) {
709
+ handles.push(
710
+ registerTool(definition, {
711
+ signal: controller.signal,
712
+ ...registerOptions.exposedTo === void 0 ? {} : { exposedTo: registerOptions.exposedTo }
713
+ })
714
+ );
715
+ }
716
+ } catch (error) {
717
+ if (atomic) unregister(error);
718
+ throw error;
719
+ }
720
+ const ready = Promise.all(handles.map((handle) => handle.ready)).catch((error) => {
721
+ if (atomic) unregister(error);
722
+ throw error;
723
+ });
724
+ void ready.catch(() => void 0);
725
+ return Object.freeze({
726
+ handles: Object.freeze(handles),
727
+ ready,
728
+ signal: controller.signal,
729
+ unregister
730
+ });
731
+ };
732
+ const snapshot = () => {
733
+ const tools = Array.from(registrationsById.values(), ({ handle, status }) => ({
734
+ id: handle.id,
735
+ name: handle.name,
736
+ status
737
+ })).sort((left, right) => left.id.localeCompare(right.id));
738
+ return {
739
+ ...options.environment === void 0 ? {} : { environment: options.environment },
740
+ ...options.release === void 0 ? {} : { release: options.release },
741
+ ...options.siteId === void 0 ? {} : { siteId: options.siteId },
742
+ supported: Boolean(resolveModelContext(options)),
743
+ telemetry: telemetry.status(),
744
+ tools
745
+ };
746
+ };
747
+ const coverage = async () => {
748
+ const context = resolveModelContext(options);
749
+ const instrumented = Array.from(registrationsByName.keys()).sort();
750
+ if (!context?.getTools) {
751
+ return {
752
+ completeness: null,
753
+ discovered: [],
754
+ instrumented,
755
+ inventoryOnly: [],
756
+ missing: [],
757
+ status: "unavailable"
758
+ };
759
+ }
760
+ let discovered;
761
+ try {
762
+ discovered = Array.from(new Set((await context.getTools()).map((tool) => tool.name))).sort();
763
+ } catch (cause) {
764
+ options.onDiagnostic?.({
765
+ cause,
766
+ code: "coverage-unavailable",
767
+ message: "The native WebMCP inventory could not be read.",
768
+ severity: "warning"
769
+ });
770
+ return {
771
+ completeness: null,
772
+ discovered: [],
773
+ instrumented,
774
+ inventoryOnly: [],
775
+ missing: [],
776
+ status: "unavailable"
777
+ };
778
+ }
779
+ const discoveredSet = new Set(discovered);
780
+ const instrumentedSet = new Set(instrumented);
781
+ const inventoryOnly = discovered.filter((name) => !instrumentedSet.has(name));
782
+ const missing = instrumented.filter((name) => !discoveredSet.has(name));
783
+ return {
784
+ completeness: discovered.length === 0 ? instrumented.length === 0 ? null : 0 : (discovered.length - inventoryOnly.length) / discovered.length,
785
+ discovered,
786
+ instrumented,
787
+ inventoryOnly,
788
+ missing,
789
+ status: "available"
790
+ };
791
+ };
792
+ const dispose = (reason) => {
793
+ for (const registration of Array.from(registrationsById.values())) {
794
+ registration.handle.unregister(reason);
795
+ }
796
+ telemetry.dispose();
797
+ };
798
+ return Object.freeze({
799
+ get supported() {
800
+ return Boolean(resolveModelContext(options));
801
+ },
802
+ get telemetry() {
803
+ return telemetry.status();
804
+ },
805
+ registerTool,
806
+ registerTools,
807
+ coverage,
808
+ dispose,
809
+ flush: () => telemetry.flush("manual"),
810
+ snapshot
811
+ });
812
+ }
813
+
814
+ // src/evals/index.ts
815
+ function toWebMcpEvalsSchema(tools) {
816
+ const names = /* @__PURE__ */ new Set();
817
+ const exported = tools.map((candidate) => {
818
+ const tool = isDefinedTool(candidate) ? candidate : defineTool(candidate);
819
+ if (names.has(tool.name)) {
820
+ throw new TypeError(`Cannot export duplicate WebMCP tool name "${tool.name}".`);
821
+ }
822
+ names.add(tool.name);
823
+ return Object.freeze({
824
+ name: tool.name,
825
+ description: tool.description,
826
+ inputSchema: tool.inputSchema ?? null,
827
+ outputSchema: null,
828
+ ...tool.annotations === void 0 ? {} : { annotations: tool.annotations }
829
+ });
830
+ });
831
+ return Object.freeze({ tools: Object.freeze(exported) });
832
+ }
833
+ export {
834
+ SITEVERB_WEBMCP_VERSION,
835
+ createFetchTelemetryTransport,
836
+ createSiteverb,
837
+ defineTool,
838
+ inspectToolDefinition,
839
+ isDefinedTool,
840
+ toWebMcpEvalsSchema
841
+ };