@happyvertical/smrt-web 0.43.2 → 0.43.4

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.
@@ -1343,6 +1343,425 @@ function persistCollection(config) {
1343
1343
  };
1344
1344
  }
1345
1345
  //#endregion
1346
+ //#region src/remote-query.ts
1347
+ function canonicalize(value) {
1348
+ if (Array.isArray(value)) return value.map(canonicalize);
1349
+ if (value && typeof value === "object") {
1350
+ const record = value;
1351
+ return Object.fromEntries(Object.keys(record).sort().map((key) => [key, canonicalize(record[key])]));
1352
+ }
1353
+ return value;
1354
+ }
1355
+ function keyFor(request) {
1356
+ const { requestId: _requestId, ...semantic } = request;
1357
+ return JSON.stringify(canonicalize(semantic));
1358
+ }
1359
+ function rebindRequestId(result, request) {
1360
+ return result.requestId === request.requestId ? result : {
1361
+ ...result,
1362
+ requestId: request.requestId
1363
+ };
1364
+ }
1365
+ function abortError() {
1366
+ if (typeof DOMException !== "undefined") return new DOMException("The operation was aborted", "AbortError");
1367
+ const error = /* @__PURE__ */ new Error("The operation was aborted");
1368
+ error.name = "AbortError";
1369
+ return error;
1370
+ }
1371
+ function isAbort(error) {
1372
+ return !!error && typeof error === "object" && error.name === "AbortError";
1373
+ }
1374
+ function pageOf(result) {
1375
+ if (!result.page) return void 0;
1376
+ return result.page.kind === "offset" ? { ...result.page } : { ...result.page };
1377
+ }
1378
+ function createSmrtWebQuery(_collection, transport, options = {}) {
1379
+ const staleTimeMs = options.staleTimeMs ?? 3e4;
1380
+ const cache = /* @__PURE__ */ new Map();
1381
+ const inFlight = /* @__PURE__ */ new Map();
1382
+ const latestSuccessfulFlight = /* @__PURE__ */ new Map();
1383
+ let flightSequence = 0;
1384
+ const listeners = /* @__PURE__ */ new Set();
1385
+ let request;
1386
+ let visibleController;
1387
+ const runControllers = /* @__PURE__ */ new Set();
1388
+ let generation = 0;
1389
+ let live;
1390
+ let liveIntent;
1391
+ let liveGeneration = 0;
1392
+ let disposed = false;
1393
+ let state = {
1394
+ rows: [],
1395
+ page: void 0,
1396
+ total: void 0,
1397
+ loading: false,
1398
+ refreshing: false,
1399
+ stale: false,
1400
+ error: null,
1401
+ lastUpdated: void 0,
1402
+ result: void 0
1403
+ };
1404
+ const publish = (next) => {
1405
+ state = next;
1406
+ for (const listener of listeners) listener(state);
1407
+ };
1408
+ const cached = (key) => cache.get(key);
1409
+ const cacheSuccess = (key, result, sequence, updatedAt = Date.now()) => {
1410
+ if (disposed) return false;
1411
+ const latest = latestSuccessfulFlight.get(key);
1412
+ if (latest === void 0 || sequence > latest) {
1413
+ latestSuccessfulFlight.set(key, sequence);
1414
+ cache.set(key, {
1415
+ result,
1416
+ updatedAt
1417
+ });
1418
+ return true;
1419
+ }
1420
+ return false;
1421
+ };
1422
+ const apply = (result, candidate, updatedAt = Date.now(), successSequence) => {
1423
+ if (successSequence !== void 0 && !cacheSuccess(keyFor(candidate), result, successSequence, updatedAt)) return;
1424
+ publish({
1425
+ rows: result.rows,
1426
+ page: pageOf(result),
1427
+ total: result.total,
1428
+ loading: false,
1429
+ refreshing: false,
1430
+ stale: result.freshness.state === "stale",
1431
+ error: null,
1432
+ lastUpdated: updatedAt,
1433
+ result
1434
+ });
1435
+ };
1436
+ const runTransport = async (candidate, controller) => {
1437
+ runControllers.add(controller);
1438
+ try {
1439
+ const result = await executeSmrtWebDataQuery(transport, candidate, { signal: controller.signal });
1440
+ if (controller.signal.aborted) throw abortError();
1441
+ return result;
1442
+ } catch (error) {
1443
+ if (controller.signal.aborted) throw abortError();
1444
+ throw error;
1445
+ } finally {
1446
+ runControllers.delete(controller);
1447
+ }
1448
+ };
1449
+ const execute = async (candidate, runOptions = {}) => {
1450
+ if (disposed) throw abortError();
1451
+ const mode = runOptions.mode ?? "visible";
1452
+ const key = keyFor(candidate);
1453
+ if (runOptions.signal?.aborted || (runOptions.deadlineMs ?? 1) <= 0) throw abortError();
1454
+ const rebindIntent = mode === "visible" && liveIntent?.active && (!live || request !== void 0 && keyFor(request) !== key) ? liveIntent : void 0;
1455
+ const entry = cached(key);
1456
+ const fresh = entry !== void 0 && Date.now() - entry.updatedAt < staleTimeMs;
1457
+ if (mode !== "visible" && !runOptions.force && fresh) return rebindRequestId(entry.result, candidate);
1458
+ if (mode === "visible" && !runOptions.force && fresh) {
1459
+ generation += 1;
1460
+ visibleController?.abort(abortError());
1461
+ if (disposed) throw abortError();
1462
+ visibleController = void 0;
1463
+ request = candidate;
1464
+ if (rebindIntent) live?.disconnect();
1465
+ const result = rebindRequestId(entry.result, candidate);
1466
+ apply(result, candidate, entry.updatedAt);
1467
+ if (disposed) throw abortError();
1468
+ if (rebindIntent?.active && !live) resumeLive(rebindIntent);
1469
+ return result;
1470
+ }
1471
+ if (mode === "visible") {
1472
+ generation += 1;
1473
+ visibleController?.abort(abortError());
1474
+ if (disposed) throw abortError();
1475
+ const invocationController = new AbortController();
1476
+ visibleController = invocationController;
1477
+ request = candidate;
1478
+ if (rebindIntent) live?.disconnect();
1479
+ const current = generation;
1480
+ const signal = runOptions.signal;
1481
+ let removeCallerAbort;
1482
+ if (signal?.aborted) invocationController.abort(signal.reason);
1483
+ else if (signal) {
1484
+ const abortCaller = () => invocationController.abort(signal.reason);
1485
+ signal.addEventListener("abort", abortCaller, { once: true });
1486
+ removeCallerAbort = () => signal.removeEventListener("abort", abortCaller);
1487
+ }
1488
+ if (entry) {
1489
+ const cachedResult = rebindRequestId(entry.result, candidate);
1490
+ publish({
1491
+ rows: cachedResult.rows,
1492
+ page: pageOf(cachedResult),
1493
+ total: cachedResult.total,
1494
+ loading: false,
1495
+ refreshing: true,
1496
+ stale: true,
1497
+ error: null,
1498
+ lastUpdated: entry.updatedAt,
1499
+ result: cachedResult
1500
+ });
1501
+ } else publish({
1502
+ ...state,
1503
+ loading: true,
1504
+ refreshing: false,
1505
+ stale: false,
1506
+ error: null
1507
+ });
1508
+ runOptions = {
1509
+ ...runOptions,
1510
+ signal: invocationController.signal,
1511
+ force: true
1512
+ };
1513
+ try {
1514
+ if (disposed) throw abortError();
1515
+ const result = await runShared(candidate, key, runOptions);
1516
+ if (current === generation) {
1517
+ if (cached(key)?.result === result) apply(result, candidate);
1518
+ else publish({
1519
+ ...state,
1520
+ loading: false,
1521
+ refreshing: false
1522
+ });
1523
+ if (rebindIntent?.active && !live) resumeLive(rebindIntent);
1524
+ }
1525
+ return result;
1526
+ } catch (error) {
1527
+ if (current === generation) if (isAbort(error)) publish({
1528
+ ...state,
1529
+ loading: false,
1530
+ refreshing: false
1531
+ });
1532
+ else publish({
1533
+ ...state,
1534
+ loading: false,
1535
+ refreshing: false,
1536
+ stale: entry !== void 0,
1537
+ error
1538
+ });
1539
+ throw error;
1540
+ } finally {
1541
+ removeCallerAbort?.();
1542
+ if (visibleController === invocationController) visibleController = void 0;
1543
+ }
1544
+ }
1545
+ return runShared(candidate, key, runOptions);
1546
+ };
1547
+ const waitForFlight = (flight, key, candidate, runOptions) => {
1548
+ if (runOptions.signal?.aborted || (runOptions.deadlineMs ?? 1) <= 0) return Promise.reject(abortError());
1549
+ return new Promise((resolve, reject) => {
1550
+ let settled = false;
1551
+ const settle = () => {
1552
+ if (settled) return;
1553
+ settled = true;
1554
+ flight.waiters -= 1;
1555
+ if (timer) clearTimeout(timer);
1556
+ runOptions.signal?.removeEventListener("abort", cancel);
1557
+ };
1558
+ const cancel = () => {
1559
+ if (settled) return;
1560
+ settle();
1561
+ if (flight.waiters === 0 && !flight.settled) {
1562
+ flight.controller.abort(abortError());
1563
+ if (inFlight.get(key) === flight) inFlight.delete(key);
1564
+ }
1565
+ reject(abortError());
1566
+ };
1567
+ const timer = runOptions.deadlineMs === void 0 ? void 0 : setTimeout(cancel, runOptions.deadlineMs);
1568
+ flight.waiters += 1;
1569
+ runOptions.signal?.addEventListener("abort", cancel, { once: true });
1570
+ flight.promise.then((result) => {
1571
+ if (settled) return;
1572
+ settle();
1573
+ resolve(rebindRequestId(result, candidate));
1574
+ }, (error) => {
1575
+ if (settled) return;
1576
+ settle();
1577
+ reject(error);
1578
+ });
1579
+ });
1580
+ };
1581
+ const runShared = async (candidate, key, runOptions) => {
1582
+ if (runOptions.signal?.aborted || (runOptions.deadlineMs ?? 1) <= 0) throw abortError();
1583
+ if (!runOptions.force) {
1584
+ const existing = inFlight.get(key);
1585
+ if (existing) return waitForFlight(existing, key, candidate, runOptions);
1586
+ }
1587
+ const flight = ++flightSequence;
1588
+ const controller = new AbortController();
1589
+ const running = runTransport(candidate, controller).then((result) => {
1590
+ if (controller.signal.aborted) throw abortError();
1591
+ cacheSuccess(key, result, flight);
1592
+ return result;
1593
+ });
1594
+ const shared = {
1595
+ controller,
1596
+ promise: running,
1597
+ waiters: 0,
1598
+ settled: false
1599
+ };
1600
+ inFlight.set(key, shared);
1601
+ running.then(() => {
1602
+ shared.settled = true;
1603
+ if (inFlight.get(key) === shared) inFlight.delete(key);
1604
+ }, () => {
1605
+ shared.settled = true;
1606
+ if (inFlight.get(key) === shared) inFlight.delete(key);
1607
+ });
1608
+ return waitForFlight(shared, key, candidate, runOptions);
1609
+ };
1610
+ const refresh = (refreshOptions = {}) => {
1611
+ if (!request) return Promise.resolve(void 0);
1612
+ return execute(request, {
1613
+ ...refreshOptions,
1614
+ mode: "visible",
1615
+ force: true
1616
+ });
1617
+ };
1618
+ const startLive = (intent) => {
1619
+ if (disposed || !intent.active || !request || !transport.subscribe) return void 0;
1620
+ const candidate = request;
1621
+ const currentGeneration = ++liveGeneration;
1622
+ const controller = new AbortController();
1623
+ let connected = true;
1624
+ let subscription;
1625
+ let handle;
1626
+ const disconnect = () => {
1627
+ if (!connected) return;
1628
+ connected = false;
1629
+ controller.abort();
1630
+ subscription.unsubscribe();
1631
+ if (live === handle) live = void 0;
1632
+ };
1633
+ const unsubscribe = () => {
1634
+ if (!intent.active) return;
1635
+ intent.active = false;
1636
+ disconnect();
1637
+ if (liveIntent === intent) liveIntent = void 0;
1638
+ if (live?.intent === intent) live.disconnect();
1639
+ };
1640
+ const reconnect = () => {
1641
+ if (!intent.active || disposed) return;
1642
+ if (!connected) {
1643
+ if (live?.intent === intent) live.reconnect();
1644
+ return;
1645
+ }
1646
+ if (currentGeneration !== liveGeneration) return;
1647
+ disconnect();
1648
+ const reconnectRequest = request;
1649
+ if (!reconnectRequest || keyFor(reconnectRequest) !== keyFor(candidate)) return;
1650
+ execute(reconnectRequest, {
1651
+ mode: "visible",
1652
+ force: true
1653
+ }).catch(() => void 0).finally(() => {
1654
+ if (intent.active && !disposed && liveIntent === intent && !live && request && keyFor(request) === keyFor(candidate)) resumeLive(intent);
1655
+ });
1656
+ };
1657
+ handle = {
1658
+ intent,
1659
+ disconnect,
1660
+ unsubscribe,
1661
+ reconnect
1662
+ };
1663
+ subscription = transport.subscribe(candidate, (raw) => {
1664
+ if (!connected || !intent.active || disposed || currentGeneration !== liveGeneration || controller.signal.aborted) return;
1665
+ const liveFlight = ++flightSequence;
1666
+ (async () => {
1667
+ const result = await executeSmrtWebDataQuery({ query: async () => raw }, candidate);
1668
+ if (connected && intent.active && !disposed && currentGeneration === liveGeneration && request && keyFor(request) === keyFor(candidate)) apply(rebindRequestId(result, request), request, Date.now(), liveFlight);
1669
+ })().catch((error) => {
1670
+ const latestSuccessful = latestSuccessfulFlight.get(keyFor(candidate));
1671
+ if (!isAbort(error) && connected && intent.active && !disposed && currentGeneration === liveGeneration && request && keyFor(request) === keyFor(candidate) && (latestSuccessful === void 0 || liveFlight > latestSuccessful)) publish({
1672
+ ...state,
1673
+ error
1674
+ });
1675
+ });
1676
+ }, { signal: controller.signal });
1677
+ if (disposed || !intent.active) {
1678
+ disconnect();
1679
+ return;
1680
+ }
1681
+ live = handle;
1682
+ return handle;
1683
+ };
1684
+ const clearLiveIntent = (intent) => {
1685
+ intent.active = false;
1686
+ if (liveIntent === intent) liveIntent = void 0;
1687
+ };
1688
+ const resumeLive = (intent) => {
1689
+ try {
1690
+ startLive(intent);
1691
+ } catch (error) {
1692
+ clearLiveIntent(intent);
1693
+ if (!disposed) publish({
1694
+ ...state,
1695
+ error
1696
+ });
1697
+ }
1698
+ };
1699
+ const subscribeLive = () => {
1700
+ if (disposed || !request || !transport.subscribe) return void 0;
1701
+ if (liveIntent) {
1702
+ liveIntent.active = false;
1703
+ live?.disconnect();
1704
+ }
1705
+ const intent = { active: true };
1706
+ liveIntent = intent;
1707
+ try {
1708
+ return startLive(intent);
1709
+ } catch (error) {
1710
+ clearLiveIntent(intent);
1711
+ throw error;
1712
+ }
1713
+ };
1714
+ return {
1715
+ get state() {
1716
+ return state;
1717
+ },
1718
+ get request() {
1719
+ return request;
1720
+ },
1721
+ execute,
1722
+ refresh,
1723
+ retry: () => refresh(),
1724
+ subscribe(listener) {
1725
+ listeners.add(listener);
1726
+ listener(state);
1727
+ return () => listeners.delete(listener);
1728
+ },
1729
+ subscribeLive,
1730
+ invalidate() {
1731
+ if (!request) return;
1732
+ const key = keyFor(request);
1733
+ const entry = cache.get(key);
1734
+ latestSuccessfulFlight.set(key, ++flightSequence);
1735
+ if (entry) cache.set(key, {
1736
+ ...entry,
1737
+ updatedAt: 0
1738
+ });
1739
+ publish({
1740
+ ...state,
1741
+ loading: false,
1742
+ refreshing: false,
1743
+ stale: true
1744
+ });
1745
+ },
1746
+ dispose() {
1747
+ disposed = true;
1748
+ generation += 1;
1749
+ liveGeneration += 1;
1750
+ visibleController?.abort(abortError());
1751
+ for (const controller of runControllers) controller.abort(abortError());
1752
+ if (liveIntent) liveIntent.active = false;
1753
+ live?.disconnect();
1754
+ live = void 0;
1755
+ liveIntent = void 0;
1756
+ request = void 0;
1757
+ listeners.clear();
1758
+ cache.clear();
1759
+ latestSuccessfulFlight.clear();
1760
+ inFlight.clear();
1761
+ }
1762
+ };
1763
+ }
1764
+ //#endregion
1346
1765
  //#region src/sse-client.ts
1347
1766
  var EVENT_SOURCE_CLOSED = 2;
1348
1767
  function defaultEventSourceFactory(url, init) {
@@ -1697,38 +2116,266 @@ function getModelContext() {
1697
2116
  if (mc && typeof mc.registerTool === "function") return mc;
1698
2117
  }
1699
2118
  function registerWebMcpTools(definitions, options = {}) {
2119
+ const exposure = validateExposurePolicy(options);
1700
2120
  const ctx = getModelContext();
1701
- if (!ctx) return () => {};
2121
+ if (!ctx) return registrationDisposer(() => {}, Promise.resolve());
1702
2122
  const basePath = options.basePath ?? "/api/v1";
2123
+ const client = options.client;
2124
+ const { tools, allowedEffects } = selectProspectiveTools(definitions, options, exposure);
2125
+ if (client && tools.some((tool) => tool.kind === "canonical" && tool.effect !== "read")) validateSmrtWebClient(client);
1703
2126
  const controller = new AbortController();
1704
2127
  const collections = /* @__PURE__ */ new Map();
1705
- for (const definition of definitions) {
1706
- const descriptors = definition.toolDescriptors;
1707
- if (!descriptors || descriptors.length === 0) continue;
1708
- const fetchers = options.resolveFetchers ? options.resolveFetchers(definition) : createDefinitionFetchers(definition, basePath, options.fetchFn);
1709
- const collection = createSmrtCollection(definition, {
1710
- fetchers,
1711
- basePath,
1712
- fetchFn: options.fetchFn,
1713
- ...options.client ? { client: options.client } : {},
1714
- ...options.scope ? { scope: options.scope } : {}
1715
- });
1716
- collections.set(definition, collection);
1717
- for (const descriptor of descriptors) {
1718
- if (options.filter && !options.filter(definition, descriptor)) continue;
1719
- ctx.registerTool({
1720
- name: descriptor.name,
1721
- description: descriptor.description,
1722
- inputSchema: descriptor.inputSchema,
1723
- annotations: { readOnlyHint: descriptor.readOnly },
1724
- execute: (args) => dispatch(fetchers, collection, definition, descriptor.action, descriptor.route, args ?? {})
1725
- }, { signal: controller.signal });
1726
- }
1727
- }
1728
- return () => {
2128
+ const collectionFetchers = /* @__PURE__ */ new Map();
2129
+ let disposed = false;
2130
+ const dispose = () => {
2131
+ if (disposed) return;
2132
+ disposed = true;
1729
2133
  controller.abort();
1730
2134
  for (const collection of collections.values()) collection.cleanup().catch(() => void 0);
1731
2135
  };
2136
+ const registrations = [];
2137
+ try {
2138
+ for (const tool of tools) {
2139
+ if (tool.kind === "legacy") {
2140
+ const { definition: definition2, descriptor } = tool;
2141
+ let fetchers2 = collectionFetchers.get(definition2);
2142
+ let collection = collections.get(definition2);
2143
+ if (!fetchers2 || !collection) {
2144
+ fetchers2 = options.resolveFetchers ? options.resolveFetchers(snapshotLegacyDefinition(definition2)) : createDefinitionFetchers(definition2, basePath, options.fetchFn);
2145
+ collection = createSmrtCollection(definition2, {
2146
+ fetchers: fetchers2,
2147
+ basePath,
2148
+ fetchFn: options.fetchFn,
2149
+ ...client ? { client } : {},
2150
+ ...options.scope ? { scope: options.scope } : {}
2151
+ });
2152
+ collectionFetchers.set(definition2, fetchers2);
2153
+ collections.set(definition2, collection);
2154
+ }
2155
+ registrations.push(Promise.resolve(ctx.registerTool({
2156
+ name: tool.name,
2157
+ description: descriptor.description,
2158
+ inputSchema: descriptor.inputSchema,
2159
+ annotations: annotationsFor(tool),
2160
+ execute: guardedExecute(tool, allowedEffects, () => disposed, (args) => dispatchCollection(fetchers2, collection, definition2, descriptor.action, descriptor.route, args))
2161
+ }, { signal: controller.signal })));
2162
+ continue;
2163
+ }
2164
+ const { definition } = tool;
2165
+ const fetchers = options.resolveToolFetchers ? options.resolveToolFetchers(snapshotCanonicalDefinition(definition, {
2166
+ effect: tool.effect,
2167
+ destructive: tool.destructive,
2168
+ idempotent: tool.idempotent,
2169
+ openWorld: tool.openWorld
2170
+ })) : createDefinitionFetchers({
2171
+ name: definition.collection,
2172
+ endpoint: definition.endpoint
2173
+ }, basePath, options.fetchFn);
2174
+ registrations.push(Promise.resolve(ctx.registerTool({
2175
+ name: tool.name,
2176
+ description: definition.description,
2177
+ inputSchema: definition.inputSchema,
2178
+ annotations: annotationsFor(tool),
2179
+ execute: guardedExecute(tool, allowedEffects, () => disposed, (args) => dispatchDirect(fetchers, definition, args, client))
2180
+ }, { signal: controller.signal })));
2181
+ }
2182
+ } catch (error) {
2183
+ Promise.all(registrations).catch(() => void 0);
2184
+ dispose();
2185
+ throw error;
2186
+ }
2187
+ const ready = Promise.all(registrations).then(() => void 0).catch((error) => {
2188
+ dispose();
2189
+ throw error;
2190
+ });
2191
+ ready.catch(() => void 0);
2192
+ return registrationDisposer(dispose, ready);
2193
+ }
2194
+ function registrationDisposer(dispose, ready) {
2195
+ return Object.assign(dispose, { ready });
2196
+ }
2197
+ var VALID_EFFECTS = [
2198
+ "read",
2199
+ "write",
2200
+ "destructive"
2201
+ ];
2202
+ var NAMESPACE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
2203
+ function validateExposurePolicy(options) {
2204
+ const effects = options.effects ?? ["read"];
2205
+ for (const effect of effects) if (!VALID_EFFECTS.includes(effect)) throw new Error(`Invalid WebMCP effect: ${String(effect)}`);
2206
+ const maxTools = options.maxTools;
2207
+ if (maxTools !== void 0 && (!Number.isSafeInteger(maxTools) || maxTools < 0)) throw new Error("WebMCP maxTools must be a non-negative safe integer");
2208
+ const namespace = options.namespace;
2209
+ if (namespace !== void 0 && !NAMESPACE_PATTERN.test(namespace)) throw new Error("WebMCP namespace must start with an alphanumeric character and contain only letters, numbers, underscores, or hyphens");
2210
+ return {
2211
+ allowedEffects: new Set(effects),
2212
+ ...maxTools !== void 0 ? { maxTools } : {},
2213
+ ...namespace !== void 0 ? { namespace } : {}
2214
+ };
2215
+ }
2216
+ function selectProspectiveTools(definitions, options, exposure) {
2217
+ const { allowedEffects, maxTools, namespace } = exposure;
2218
+ const stableDefinitions = definitions.map((definition) => snapshotValue(definition));
2219
+ const tools = [];
2220
+ for (const definition of stableDefinitions) {
2221
+ if (isCanonicalToolDefinition(definition)) {
2222
+ const semantics = actionSemantics(definition.action, definition);
2223
+ if (!allowedEffects.has(semantics.effect)) continue;
2224
+ if (options.filter && !options.filterTool) throw new Error("[smrt-web] canonical WebMCP definitions require filterTool when filter is configured");
2225
+ const stableDefinition2 = snapshotCanonicalDefinition(definition, semantics);
2226
+ if (options.filterTool && !options.filterTool(snapshotCanonicalDefinition(stableDefinition2, semantics))) continue;
2227
+ tools.push({
2228
+ kind: "canonical",
2229
+ definition: stableDefinition2,
2230
+ descriptor: stableDefinition2,
2231
+ name: qualifiedToolName(stableDefinition2.name, namespace),
2232
+ identity: `${stableDefinition2.collection}#${stableDefinition2.action}`,
2233
+ ...semantics
2234
+ });
2235
+ continue;
2236
+ }
2237
+ const stableDefinition = snapshotLegacyDefinition(definition);
2238
+ for (const descriptor of stableDefinition.toolDescriptors ?? []) {
2239
+ if (!stableDefinition.actions.includes(descriptor.action)) throw new Error(`WebMCP tool ${descriptor.name} exposes action ${descriptor.action} outside ${stableDefinition.name}'s allowed actions`);
2240
+ const semantics = actionSemantics(descriptor.action, descriptor);
2241
+ if (!allowedEffects.has(semantics.effect)) continue;
2242
+ if (options.filterTool && !options.filter) throw new Error("[smrt-web] legacy WebMCP definitions require filter when filterTool is configured");
2243
+ const stableDescriptor = snapshotLegacyDescriptor(descriptor, semantics);
2244
+ if (options.filter && !options.filter(snapshotLegacyDefinition(stableDefinition), snapshotLegacyDescriptor(stableDescriptor, semantics))) continue;
2245
+ tools.push({
2246
+ kind: "legacy",
2247
+ definition: stableDefinition,
2248
+ descriptor: stableDescriptor,
2249
+ name: qualifiedToolName(stableDescriptor.name, namespace),
2250
+ identity: `${stableDefinition.name}#${stableDescriptor.action}`,
2251
+ ...semantics
2252
+ });
2253
+ }
2254
+ }
2255
+ validateProspectiveTools(tools, maxTools);
2256
+ return {
2257
+ tools,
2258
+ allowedEffects
2259
+ };
2260
+ }
2261
+ function qualifiedToolName(name, namespace) {
2262
+ return namespace ? `${namespace}_${name}` : name;
2263
+ }
2264
+ function actionSemantics(action, declared) {
2265
+ switch (action) {
2266
+ case "list":
2267
+ case "get": return {
2268
+ effect: "read",
2269
+ destructive: false,
2270
+ idempotent: true,
2271
+ openWorld: false
2272
+ };
2273
+ case "create": return {
2274
+ effect: "write",
2275
+ destructive: true,
2276
+ idempotent: false,
2277
+ openWorld: false
2278
+ };
2279
+ case "update": return {
2280
+ effect: "write",
2281
+ destructive: true,
2282
+ idempotent: true,
2283
+ openWorld: false
2284
+ };
2285
+ case "delete": return {
2286
+ effect: "destructive",
2287
+ destructive: true,
2288
+ idempotent: true,
2289
+ openWorld: false
2290
+ };
2291
+ default: {
2292
+ const effect = VALID_EFFECTS.includes(declared.effect) ? declared.effect : "destructive";
2293
+ return {
2294
+ effect,
2295
+ destructive: effect !== "read",
2296
+ idempotent: declared.idempotent ?? false,
2297
+ openWorld: declared.openWorld ?? true
2298
+ };
2299
+ }
2300
+ }
2301
+ }
2302
+ function snapshotRoute(route) {
2303
+ return route ? snapshotValue(route) : void 0;
2304
+ }
2305
+ function snapshotValue(value, active = /* @__PURE__ */ new WeakSet(), path = "$") {
2306
+ if (value && typeof value === "object") {
2307
+ if (active.has(value)) throw new Error(`[smrt-web] WebMCP definitions must be acyclic (cycle at ${path})`);
2308
+ active.add(value);
2309
+ }
2310
+ try {
2311
+ if (Array.isArray(value)) return value.map((entry, index) => snapshotValue(entry, active, `${path}[${index}]`));
2312
+ if (value && typeof value === "object") {
2313
+ const snapshot = {};
2314
+ for (const [key, entry] of Object.entries(value)) snapshot[key] = snapshotValue(entry, active, `${path}.${key}`);
2315
+ return snapshot;
2316
+ }
2317
+ return value;
2318
+ } finally {
2319
+ if (value && typeof value === "object") active.delete(value);
2320
+ }
2321
+ }
2322
+ function snapshotLegacyDescriptor(descriptor, semantics) {
2323
+ return snapshotValue({
2324
+ ...descriptor,
2325
+ effect: semantics.effect,
2326
+ idempotent: semantics.idempotent,
2327
+ openWorld: semantics.openWorld,
2328
+ readOnly: semantics.effect === "read",
2329
+ route: snapshotRoute(descriptor.route)
2330
+ });
2331
+ }
2332
+ function snapshotLegacyDefinition(definition) {
2333
+ const snapshot = snapshotValue({
2334
+ ...definition,
2335
+ actions: [...definition.actions]
2336
+ });
2337
+ snapshot.toolDescriptors = snapshot.toolDescriptors?.map((descriptor) => snapshotLegacyDescriptor(descriptor, actionSemantics(descriptor.action, descriptor)));
2338
+ return snapshot;
2339
+ }
2340
+ function snapshotCanonicalDefinition(definition, semantics) {
2341
+ return snapshotValue({
2342
+ ...definition,
2343
+ effect: semantics.effect,
2344
+ idempotent: semantics.idempotent,
2345
+ openWorld: semantics.openWorld,
2346
+ readOnly: semantics.effect === "read",
2347
+ route: snapshotRoute(definition.route)
2348
+ });
2349
+ }
2350
+ function validateProspectiveTools(tools, maxTools) {
2351
+ if (maxTools !== void 0 && tools.length > maxTools) throw new Error(`WebMCP tool budget exceeded: ${tools.length} tools selected, maximum is ${maxTools}`);
2352
+ const names = /* @__PURE__ */ new Set();
2353
+ const identities = /* @__PURE__ */ new Set();
2354
+ for (const tool of tools) {
2355
+ if (names.has(tool.name)) throw new Error(`Duplicate WebMCP tool name: ${tool.name}`);
2356
+ names.add(tool.name);
2357
+ if (identities.has(tool.identity)) throw new Error(`Duplicate WebMCP tool identity: ${tool.identity}`);
2358
+ identities.add(tool.identity);
2359
+ }
2360
+ }
2361
+ function annotationsFor(tool) {
2362
+ return {
2363
+ readOnlyHint: tool.effect === "read",
2364
+ destructiveHint: tool.destructive,
2365
+ idempotentHint: tool.idempotent,
2366
+ openWorldHint: tool.openWorld,
2367
+ untrustedContentHint: true
2368
+ };
2369
+ }
2370
+ function guardedExecute(tool, allowedEffects, isDisposed, execute) {
2371
+ return (args) => {
2372
+ if (isDisposed()) throw new Error(`WebMCP tool ${tool.name} is no longer registered`);
2373
+ if (!allowedEffects.has(tool.effect)) throw new Error(`WebMCP policy no longer allows ${tool.effect} tool ${tool.name}`);
2374
+ return execute(args ?? {});
2375
+ };
2376
+ }
2377
+ function isCanonicalToolDefinition(definition) {
2378
+ return "collection" in definition && "readOnly" in definition;
1732
2379
  }
1733
2380
  function requireId(args, action) {
1734
2381
  const id = args.id;
@@ -1748,7 +2395,7 @@ function listParams(args) {
1748
2395
  if (args.where !== void 0) params.where = args.where;
1749
2396
  return params;
1750
2397
  }
1751
- async function dispatch(fetchers, collection, definition, action, route, args) {
2398
+ async function dispatchCollection(fetchers, collection, definition, action, route, args) {
1752
2399
  switch (action) {
1753
2400
  case "list": {
1754
2401
  const rows = unwrapListResult(await fetchers.list(listParams(args)), definition.name);
@@ -1789,6 +2436,45 @@ async function dispatch(fetchers, collection, definition, action, route, args) {
1789
2436
  return JSON.stringify(await collection.action(action, args, route));
1790
2437
  }
1791
2438
  }
2439
+ async function dispatchDirect(fetchers, definition, args, client) {
2440
+ let result;
2441
+ switch (definition.action) {
2442
+ case "list":
2443
+ if (!fetchers.list) throw new Error(`${definition.collection} has no list action`);
2444
+ result = unwrapListResult(await fetchers.list(listParams(args)), definition.collection);
2445
+ break;
2446
+ case "get":
2447
+ if (!fetchers.get) throw new Error(`${definition.collection} has no get action`);
2448
+ result = unwrapItemResult(await fetchers.get(requireIdentifier(args)), `get(${definition.collection})`);
2449
+ break;
2450
+ case "create":
2451
+ if (!fetchers.create) throw new Error(`${definition.collection} has no create action`);
2452
+ result = unwrapItemResult(await fetchers.create(args), `create(${definition.collection})`);
2453
+ break;
2454
+ case "update": {
2455
+ if (!fetchers.update) throw new Error(`${definition.collection} has no update action`);
2456
+ const id = requireId(args, "update");
2457
+ const { id: _id, ...body } = args;
2458
+ result = unwrapItemResult(await fetchers.update(id, body), `update(${definition.collection})`);
2459
+ break;
2460
+ }
2461
+ case "delete": {
2462
+ if (!fetchers.delete) throw new Error(`${definition.collection} has no delete action`);
2463
+ const id = requireId(args, "delete");
2464
+ throwIfSmrtWebError(await fetchers.delete(id), `delete(${definition.collection})`);
2465
+ result = {
2466
+ success: true,
2467
+ id
2468
+ };
2469
+ break;
2470
+ }
2471
+ default:
2472
+ if (!fetchers.custom) throw new Error(`${definition.collection} has no custom action fetcher`);
2473
+ result = throwIfSmrtWebError(await fetchers.custom(definition.action, args, definition.route), `${definition.action}(${definition.collection})`);
2474
+ }
2475
+ if (!definition.readOnly && client) invalidateSmrtWebCollections(client, [definition.collection, ...definition.relationships.map((relationship) => relationship.relatedCollection)]);
2476
+ return JSON.stringify(result);
2477
+ }
1792
2478
  async function settleTransaction(transaction, collection, key, fallback) {
1793
2479
  await transaction.isPersisted.promise;
1794
2480
  const persisted = persistedMutationResults.get(collection)?.get(key);
@@ -1837,23 +2523,34 @@ function createHttpRequestError(collectionName, status, payload) {
1837
2523
  return new SmrtWebRequestError(`[smrt-web] ${collectionName} request failed: ${detail.message ?? `HTTP ${status}`}`, payload, status, detail.code);
1838
2524
  }
1839
2525
  function unwrapListResult(result, collectionName) {
2526
+ throwIfSmrtWebError(result, `list(${collectionName})`);
1840
2527
  if (Array.isArray(result)) return result;
1841
2528
  if (result && typeof result === "object") {
1842
2529
  const record = result;
1843
- if (typeof record.error === "string") throw new SmrtWebRequestError(`[smrt-web] list(${collectionName}) failed: ${record.error}`, result);
1844
2530
  if (Array.isArray(record.data)) return record.data;
1845
2531
  }
1846
2532
  throw new SmrtWebRequestError(`[smrt-web] list(${collectionName}) returned an unexpected payload shape`, result);
1847
2533
  }
1848
2534
  function unwrapItemResult(result, context) {
2535
+ throwIfSmrtWebError(result, context);
1849
2536
  if (result && typeof result === "object" && !Array.isArray(result)) {
1850
2537
  const record = result;
1851
- if (typeof record.error === "string") throw new SmrtWebRequestError(`[smrt-web] ${context} failed: ${record.error}`, result);
1852
2538
  if (record.data && typeof record.data === "object" && !Array.isArray(record.data)) return record.data;
1853
2539
  return record;
1854
2540
  }
1855
2541
  throw new SmrtWebRequestError(`[smrt-web] ${context} returned an unexpected payload shape`, result);
1856
2542
  }
2543
+ function throwIfSmrtWebError(result, context) {
2544
+ if (!result || typeof result !== "object" || Array.isArray(result)) return result;
2545
+ const error = result.error;
2546
+ if (typeof error === "string") throw new SmrtWebRequestError(`[smrt-web] ${context} failed: ${error}`, result);
2547
+ if (!error || typeof error !== "object" || Array.isArray(error)) return result;
2548
+ const failure = error;
2549
+ if (failure.ok !== false || typeof failure.code !== "string" || typeof failure.message !== "string") return result;
2550
+ const status = typeof failure.status === "number" && Number.isInteger(failure.status) && failure.status >= 400 && failure.status <= 599 ? failure.status : void 0;
2551
+ if (status !== void 0 && status >= 500) throw new SmrtWebRequestError(`[smrt-web] ${context} failed: server error`, void 0, status);
2552
+ throw new SmrtWebRequestError(`[smrt-web] ${context} failed: ${failure.message}`, result, status, failure.code);
2553
+ }
1857
2554
  var SMRT_TO_REST_OPERATOR = {
1858
2555
  ">": "gt",
1859
2556
  ">=": "gte",
@@ -1955,12 +2652,12 @@ function createDefinitionFetchers(definition, basePath = "/api/v1", fetchFn = (.
1955
2652
  if (customRoute.method !== "GET") init.body = JSON.stringify(body);
1956
2653
  else {
1957
2654
  const queryParams = new URLSearchParams();
1958
- if (optionsBag) if (optionsValue === void 0) queryParams.set(CUSTOM_OPTIONS_QUERY_MARKER, "undefined");
1959
- else if (optionsValue === null) queryParams.set(CUSTOM_OPTIONS_QUERY_MARKER, "null");
2655
+ if (optionsBag) if (optionsValue === void 0 && pathArgs.size === 0) queryParams.set(CUSTOM_OPTIONS_QUERY_MARKER, "undefined");
2656
+ else if (optionsValue === null && pathArgs.size === 0) queryParams.set(CUSTOM_OPTIONS_QUERY_MARKER, "null");
1960
2657
  else {
1961
- const entries = Object.entries(typeof optionsValue === "object" && !Array.isArray(optionsValue) ? optionsValue : {}).filter(([, value]) => value !== void 0 && value !== null);
2658
+ const entries = Object.entries(optionsValue !== null && typeof optionsValue === "object" && !Array.isArray(optionsValue) ? optionsValue : {}).filter(([, value]) => value !== void 0 && value !== null);
1962
2659
  for (const [key, value] of entries) queryParams.set(key, typeof value === "object" ? JSON.stringify(value) : String(value));
1963
- if (entries.length === 0) queryParams.set(CUSTOM_OPTIONS_QUERY_MARKER, "object");
2660
+ if (entries.length === 0 && pathArgs.size === 0) queryParams.set(CUSTOM_OPTIONS_QUERY_MARKER, "object");
1964
2661
  }
1965
2662
  else {
1966
2663
  const queryBody = body !== null && typeof body === "object" && !Array.isArray(body) ? body : {};
@@ -1981,18 +2678,36 @@ function newLocalId() {
1981
2678
  if (cryptoRef?.randomUUID) return cryptoRef.randomUUID();
1982
2679
  return `local-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1983
2680
  }
2681
+ var smrtWebClientHandles = /* @__PURE__ */ new WeakSet();
1984
2682
  function createSmrtWebClient() {
1985
- return {
2683
+ const engine = {
1986
2684
  __smrtWebClient: "SmrtWebClient",
1987
2685
  queryClient: new QueryClient()
1988
2686
  };
2687
+ smrtWebClientHandles.add(engine);
2688
+ return engine;
1989
2689
  }
1990
2690
  function resolveQueryClient(client) {
1991
2691
  if (!client) return new QueryClient();
1992
2692
  const engine = client;
1993
- if (engine.__smrtWebClient !== "SmrtWebClient" || !engine.queryClient) throw new SmrtWebRequestError("[smrt-web] options.client must be a handle from createSmrtWebClient()");
2693
+ if (!smrtWebClientHandles.has(engine) || engine.__smrtWebClient !== "SmrtWebClient" || !engine.queryClient) throw new SmrtWebRequestError("[smrt-web] options.client must be a handle from createSmrtWebClient()");
1994
2694
  return engine.queryClient;
1995
2695
  }
2696
+ function validateSmrtWebClient(client) {
2697
+ resolveQueryClient(client);
2698
+ }
2699
+ function invalidateCollectionQueries(queryClient, collectionNames) {
2700
+ if (collectionNames.size === 0) return;
2701
+ queryClient.invalidateQueries({ predicate: (query) => {
2702
+ const key = query.queryKey;
2703
+ if (!Array.isArray(key) || key.length === 0) return false;
2704
+ const collectionSegment = key[key.length - 1];
2705
+ return typeof collectionSegment === "string" && collectionNames.has(collectionSegment);
2706
+ } });
2707
+ }
2708
+ function invalidateSmrtWebCollections(client, collectionNames) {
2709
+ invalidateCollectionQueries(resolveQueryClient(client), new Set(collectionNames));
2710
+ }
1996
2711
  function toPlainRow(row) {
1997
2712
  const plain = {};
1998
2713
  for (const [key, value] of Object.entries(row)) if (key.charCodeAt(0) !== 36) plain[key] = value;
@@ -2041,12 +2756,7 @@ function createSmrtCollection(definition, options) {
2041
2756
  const invalidationTargets = /* @__PURE__ */ new Set([definition.name]);
2042
2757
  for (const relationship of definition.relationships ?? []) invalidationTargets.add(relationship.relatedCollection);
2043
2758
  const invalidateRelated = () => {
2044
- queryClient.invalidateQueries({ predicate: (query) => {
2045
- const key = query.queryKey;
2046
- if (!Array.isArray(key) || key.length === 0) return false;
2047
- const collectionSegment = key[key.length - 1];
2048
- return typeof collectionSegment === "string" && invalidationTargets.has(collectionSegment);
2049
- } });
2759
+ invalidateCollectionQueries(queryClient, invalidationTargets);
2050
2760
  };
2051
2761
  const ctx = {
2052
2762
  definition,
@@ -2210,7 +2920,7 @@ function createSmrtCollection(definition, options) {
2210
2920
  data: {},
2211
2921
  baseUpdatedAt: getBaseUpdatedAt(mutation.original)
2212
2922
  };
2213
- const outcome = await persistMutation(envelope, async () => fetchers.delete(key));
2923
+ const outcome = await persistMutation(envelope, async () => throwIfSmrtWebError(await fetchers.delete(key), `delete(${definition.name})`));
2214
2924
  mutationResults.set(envelope.key, outcome.result);
2215
2925
  anyHandled = anyHandled || outcome.handled;
2216
2926
  }
@@ -2269,6 +2979,7 @@ function createSmrtCollection(definition, options) {
2269
2979
  async action(action, args, route) {
2270
2980
  if (!fetchers.custom) throw new Error(`${definition.name} has no custom action fetcher`);
2271
2981
  const result = route === void 0 ? await fetchers.custom(action, args) : await fetchers.custom(action, args, route);
2982
+ throwIfSmrtWebError(result, `${action}(${definition.name})`);
2272
2983
  invalidateRelated();
2273
2984
  return result;
2274
2985
  }
@@ -2287,6 +2998,6 @@ function createSmrtCollection(definition, options) {
2287
2998
  return handle;
2288
2999
  }
2289
3000
  //#endregion
2290
- export { executeSmrtWebDataQuery as A, MAX_SMRT_WEB_DATA_QUERY_FACET_VALUES as C, MAX_SMRT_WEB_DATA_QUERY_ROWS as D, MAX_SMRT_WEB_DATA_QUERY_RESULT_BYTES as E, runWrapMutation as M, MAX_SMRT_WEB_DATA_QUERY_STRING_LENGTH as O, MAX_SMRT_WEB_DATA_QUERY_FACETS as S, MAX_SMRT_WEB_DATA_QUERY_PAGE_LIMIT as T, offlineOutbox as _, createSmrtWebClient as a, wipeDurableStore as b, unwrapItemResult as c, createUpdateState as d, createSmrtWebEventSubscriber as f, getOutboxHandle as g, persistCollection as h, createSmrtCollection as i, normalizeSmrtWebDataQueryResult as j, MAX_SMRT_WEB_DATA_QUERY_WARNINGS as k, unwrapListResult as l, DEFAULT_PERSIST_DEBOUNCE_MS as m, buildListQuery as n, getEngineCollection as o, liveInvalidation as p, createDefinitionFetchers as r, newLocalId as s, SmrtWebRequestError as t, registerWebMcpTools as u, durableStoreNamespace as v, MAX_SMRT_WEB_DATA_QUERY_OFFSET as w, MAX_SMRT_WEB_DATA_QUERY_CONTAINER_ITEMS as x, registerDurableResource as y };
3001
+ export { MAX_SMRT_WEB_DATA_QUERY_RESULT_BYTES as A, registerDurableResource as C, MAX_SMRT_WEB_DATA_QUERY_FACET_VALUES as D, MAX_SMRT_WEB_DATA_QUERY_FACETS as E, normalizeSmrtWebDataQueryResult as F, runWrapMutation as I, MAX_SMRT_WEB_DATA_QUERY_STRING_LENGTH as M, MAX_SMRT_WEB_DATA_QUERY_WARNINGS as N, MAX_SMRT_WEB_DATA_QUERY_OFFSET as O, executeSmrtWebDataQuery as P, durableStoreNamespace as S, MAX_SMRT_WEB_DATA_QUERY_CONTAINER_ITEMS as T, createSmrtWebQuery as _, createSmrtWebClient as a, getOutboxHandle as b, newLocalId as c, unwrapListResult as d, validateSmrtWebClient as f, liveInvalidation as g, createSmrtWebEventSubscriber as h, createSmrtCollection as i, MAX_SMRT_WEB_DATA_QUERY_ROWS as j, MAX_SMRT_WEB_DATA_QUERY_PAGE_LIMIT as k, throwIfSmrtWebError as l, createUpdateState as m, buildListQuery as n, getEngineCollection as o, registerWebMcpTools as p, createDefinitionFetchers as r, invalidateSmrtWebCollections as s, SmrtWebRequestError as t, unwrapItemResult as u, DEFAULT_PERSIST_DEBOUNCE_MS as v, wipeDurableStore as w, offlineOutbox as x, persistCollection as y };
2291
3002
 
2292
- //# sourceMappingURL=src-CDdW9uYx.js.map
3003
+ //# sourceMappingURL=src-n14q6RHC.js.map