@happyvertical/smrt-svelte 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.
Files changed (44) hide show
  1. package/AGENTS.md +23 -0
  2. package/README.md +82 -0
  3. package/dist/Provider.svelte +83 -26
  4. package/dist/Provider.svelte.d.ts +4 -12
  5. package/dist/Provider.svelte.d.ts.map +1 -1
  6. package/dist/__tests__/provider-webmcp-harness.svelte +14 -0
  7. package/dist/__tests__/provider-webmcp-harness.svelte.d.ts +8 -0
  8. package/dist/__tests__/provider-webmcp-harness.svelte.d.ts.map +1 -0
  9. package/dist/components/forms/Form.svelte +112 -35
  10. package/dist/components/forms/Form.svelte.d.ts.map +1 -1
  11. package/dist/components/forms/__tests__/Form.webmcp.test.js +5 -5
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -0
  15. package/dist/provider-webmcp.test.js +244 -0
  16. package/dist/web/__tests__/provider-ui-registry.fixture.svelte +43 -0
  17. package/dist/web/__tests__/provider-ui-registry.fixture.svelte.d.ts +13 -0
  18. package/dist/web/__tests__/provider-ui-registry.fixture.svelte.d.ts.map +1 -0
  19. package/dist/web/__tests__/provider-ui-registry.test.js +235 -0
  20. package/dist/web/__tests__/remote-query-harness.svelte +27 -0
  21. package/dist/web/__tests__/remote-query-harness.svelte.d.ts +11 -0
  22. package/dist/web/__tests__/remote-query-harness.svelte.d.ts.map +1 -0
  23. package/dist/web/__tests__/remote-query.svelte.test.js +100 -0
  24. package/dist/web/__tests__/webmcp-ui.test.js +410 -0
  25. package/dist/web/__tests__/webmcp.test.js +1 -1
  26. package/dist/web/index.d.ts +4 -0
  27. package/dist/web/index.d.ts.map +1 -1
  28. package/dist/web/index.js +3 -0
  29. package/dist/web/remote-query.svelte.d.ts +26 -0
  30. package/dist/web/remote-query.svelte.d.ts.map +1 -0
  31. package/dist/web/remote-query.svelte.js +57 -0
  32. package/dist/web/webmcp-provider.d.ts +23 -0
  33. package/dist/web/webmcp-provider.d.ts.map +1 -0
  34. package/dist/web/webmcp-provider.js +1 -0
  35. package/dist/web/webmcp-ui-context.d.ts +12 -0
  36. package/dist/web/webmcp-ui-context.d.ts.map +1 -0
  37. package/dist/web/webmcp-ui-context.js +16 -0
  38. package/dist/web/webmcp-ui.d.ts +21 -0
  39. package/dist/web/webmcp-ui.d.ts.map +1 -0
  40. package/dist/web/webmcp-ui.js +455 -0
  41. package/dist/web/webmcp.d.ts +1 -1
  42. package/dist/web/webmcp.svelte.d.ts +4 -1
  43. package/dist/web/webmcp.svelte.d.ts.map +1 -1
  44. package/package.json +5 -5
@@ -0,0 +1,410 @@
1
+ import { createDataSurfaceRegistry, } from '@happyvertical/smrt-ui/data';
2
+ import { createControlInteractionRegistry, } from '@happyvertical/smrt-ui/forms';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { registerWebMcpUiTools } from '../webmcp-ui.js';
5
+ function modelContext() {
6
+ const registered = [];
7
+ const signals = [];
8
+ const document = {
9
+ modelContext: {
10
+ async registerTool(tool, options) {
11
+ registered.push(tool);
12
+ if (options?.signal)
13
+ signals.push(options.signal);
14
+ },
15
+ },
16
+ };
17
+ return { document, registered, signals };
18
+ }
19
+ function parse(value) {
20
+ return Promise.resolve(value).then((result) => JSON.parse(result));
21
+ }
22
+ function findTool(tools, name) {
23
+ const tool = tools.find((candidate) => candidate.name === name);
24
+ if (!tool)
25
+ throw new Error(`Missing tool: ${name}`);
26
+ return tool;
27
+ }
28
+ function descriptor() {
29
+ return {
30
+ version: 1,
31
+ identity: { surfaceId: 'content', kind: 'table' },
32
+ schemaVersion: 1,
33
+ label: 'Content',
34
+ rowKey: 'id',
35
+ columns: [
36
+ {
37
+ id: 'id',
38
+ label: 'ID',
39
+ capabilities: ['read', 'project'],
40
+ },
41
+ {
42
+ id: 'internal',
43
+ label: 'Internal',
44
+ visibility: 'hidden',
45
+ capabilities: ['read', 'project'],
46
+ },
47
+ ],
48
+ query: {
49
+ modes: ['rows'],
50
+ projectableColumnIds: ['id', 'internal'],
51
+ },
52
+ controls: [{ id: 'next-page', label: 'Next page' }],
53
+ actions: [],
54
+ limits: { maxQueryRows: 50, maxQueryBytes: 10_000, maxSelectionSize: 10 },
55
+ };
56
+ }
57
+ describe('registerWebMcpUiTools', () => {
58
+ it('registers one fixed tool set and resolves mounted controls dynamically', async () => {
59
+ const browser = modelContext();
60
+ const controls = createControlInteractionRegistry();
61
+ const surfaces = createDataSurfaceRegistry();
62
+ const dispose = registerWebMcpUiTools({
63
+ controlRegistry: controls,
64
+ dataSurfaceRegistry: surfaces,
65
+ document: browser.document,
66
+ });
67
+ expect(browser.registered.map((tool) => tool.name)).toEqual([
68
+ 'smrt_ui_list_form_controls',
69
+ 'smrt_ui_inspect_form_control',
70
+ 'smrt_ui_execute_form_control',
71
+ 'smrt_ui_list_data_surfaces',
72
+ 'smrt_ui_inspect_data_surface',
73
+ 'smrt_ui_execute_data_surface_control',
74
+ ]);
75
+ expect(browser.registered).toHaveLength(6);
76
+ const list = findTool(browser.registered, 'smrt_ui_list_form_controls');
77
+ expect(await parse(list.execute({}))).toEqual({ ok: true, result: [] });
78
+ let value = 'Ada';
79
+ const unregister = controls.register({
80
+ identity: { formId: 'profile', controlId: 'name' },
81
+ metadata: { kind: 'text', label: 'Name' },
82
+ getValue: () => value,
83
+ setValue: (next) => {
84
+ value = String(next);
85
+ },
86
+ focus: vi.fn(),
87
+ });
88
+ expect((await parse(list.execute({}))).result).toHaveLength(1);
89
+ expect(browser.registered).toHaveLength(6);
90
+ unregister();
91
+ expect(await parse(list.execute({}))).toEqual({ ok: true, result: [] });
92
+ dispose();
93
+ expect(browser.signals).toHaveLength(6);
94
+ expect(browser.signals.every((signal) => signal.aborted)).toBe(true);
95
+ });
96
+ it('uses agent consent semantics and cannot be tricked into confirmation', async () => {
97
+ const browser = modelContext();
98
+ const controls = createControlInteractionRegistry();
99
+ let value = 'Ada';
100
+ controls.register({
101
+ identity: { formId: 'profile', controlId: 'name' },
102
+ metadata: { kind: 'text', label: 'Name' },
103
+ getValue: () => value,
104
+ setValue: (next) => {
105
+ value = String(next);
106
+ },
107
+ });
108
+ registerWebMcpUiTools({
109
+ controlRegistry: controls,
110
+ dataSurfaceRegistry: createDataSurfaceRegistry(),
111
+ document: browser.document,
112
+ });
113
+ const execute = findTool(browser.registered, 'smrt_ui_execute_form_control');
114
+ const identity = { formId: 'profile', controlId: 'name' };
115
+ expect((await parse(execute.execute({ action: 'stage', identity, value: 'Grace' }))).result).toMatchObject({ ok: true, action: 'stage' });
116
+ expect(value).toBe('Ada');
117
+ expect((await parse(execute.execute({ action: 'apply', identity }))).result).toMatchObject({ ok: false, reason: 'consent_required' });
118
+ expect(value).toBe('Ada');
119
+ expect(await parse(execute.execute({
120
+ action: 'apply',
121
+ identity,
122
+ confirmed: true,
123
+ }))).toEqual({ ok: false, reason: 'invalid_request', details: 'confirmed' });
124
+ expect(value).toBe('Ada');
125
+ });
126
+ it('keeps secret values redacted from list, inspect, and execute responses', async () => {
127
+ const browser = modelContext();
128
+ const controls = createControlInteractionRegistry();
129
+ controls.register({
130
+ identity: { formId: 'account', controlId: 'password' },
131
+ metadata: {
132
+ kind: 'password',
133
+ label: 'Password',
134
+ sensitivity: 'secret',
135
+ },
136
+ getValue: () => 'never-serialize-this',
137
+ setValue: () => { },
138
+ getState: () => ({
139
+ valid: false,
140
+ validationMessage: 'never-serialize-this is invalid',
141
+ }),
142
+ focus: () => {
143
+ throw new Error('never-serialize-this cannot be focused');
144
+ },
145
+ });
146
+ registerWebMcpUiTools({
147
+ controlRegistry: controls,
148
+ dataSurfaceRegistry: createDataSurfaceRegistry(),
149
+ document: browser.document,
150
+ });
151
+ const list = findTool(browser.registered, 'smrt_ui_list_form_controls');
152
+ const inspect = findTool(browser.registered, 'smrt_ui_inspect_form_control');
153
+ const execute = findTool(browser.registered, 'smrt_ui_execute_form_control');
154
+ const listed = await parse(list.execute({ formId: 'account' }));
155
+ const inspected = await parse(inspect.execute({
156
+ identity: { formId: 'account', controlId: 'password' },
157
+ }));
158
+ const executed = await parse(execute.execute({
159
+ action: 'focus',
160
+ identity: { formId: 'account', controlId: 'password' },
161
+ }));
162
+ expect(JSON.stringify([listed, inspected, executed])).not.toContain('never-serialize-this');
163
+ expect(listed.result[0].state.valueRedacted).toBe(true);
164
+ expect(inspected.result.state.valueRedacted).toBe(true);
165
+ expect(executed.result.snapshot.state.valueRedacted).toBe(true);
166
+ expect(executed.result.snapshot.state).not.toHaveProperty('validationMessage');
167
+ expect(executed.result.reason).toBe('execution_failed');
168
+ });
169
+ it('does not expose host error messages that mimic public failures', async () => {
170
+ const browser = modelContext();
171
+ const controls = createControlInteractionRegistry();
172
+ controls.register({
173
+ identity: { formId: 'profile', controlId: 'unstable' },
174
+ metadata: { kind: 'text', label: 'Unstable' },
175
+ getValue: () => {
176
+ throw new Error('not_found: private customer 4711');
177
+ },
178
+ });
179
+ registerWebMcpUiTools({
180
+ controlRegistry: controls,
181
+ dataSurfaceRegistry: createDataSurfaceRegistry(),
182
+ document: browser.document,
183
+ });
184
+ const inspect = findTool(browser.registered, 'smrt_ui_inspect_form_control');
185
+ const result = await parse(inspect.execute({
186
+ identity: { formId: 'profile', controlId: 'unstable' },
187
+ }));
188
+ expect(result).toEqual({ ok: false, reason: 'execution_failed' });
189
+ expect(JSON.stringify(result)).not.toContain('customer 4711');
190
+ });
191
+ it('redacts secret values from an injected registry even when its flags are inconsistent', async () => {
192
+ const browser = modelContext();
193
+ const snapshot = {
194
+ identity: { formId: 'injected', controlId: 'secret' },
195
+ metadata: { kind: 'password', sensitivity: 'secret' },
196
+ state: {
197
+ value: 'injected-secret-value',
198
+ valueRedacted: false,
199
+ stagedValue: 'injected-staged-secret',
200
+ stagedValueRedacted: false,
201
+ },
202
+ };
203
+ const controls = {
204
+ register: () => () => { },
205
+ unregister: () => { },
206
+ list: () => [snapshot],
207
+ get: () => snapshot,
208
+ execute: async (command) => ({
209
+ ok: true,
210
+ action: command.action,
211
+ identity: command.identity,
212
+ snapshot,
213
+ }),
214
+ subscribe: () => () => { },
215
+ };
216
+ registerWebMcpUiTools({
217
+ controlRegistry: controls,
218
+ dataSurfaceRegistry: createDataSurfaceRegistry(),
219
+ document: browser.document,
220
+ });
221
+ const list = await parse(findTool(browser.registered, 'smrt_ui_list_form_controls').execute({}));
222
+ expect(JSON.stringify(list)).not.toContain('injected-secret');
223
+ });
224
+ it('filters hidden columns and preserves surface revision and replay failures', async () => {
225
+ const browser = modelContext();
226
+ const surfaces = createDataSurfaceRegistry();
227
+ let revision = 2;
228
+ let page = 1;
229
+ surfaces.register({
230
+ descriptor: descriptor(),
231
+ getSnapshot: () => ({
232
+ revision,
233
+ state: {
234
+ page,
235
+ internal: 'never-serialize-this',
236
+ table: {
237
+ version: 3,
238
+ state: {
239
+ filters: [
240
+ { columnId: 'internal', value: 'nested-hidden-filter' },
241
+ { columnId: 'id', value: ['internal'] },
242
+ ],
243
+ sorting: [{ columnId: 'internal', direction: 'asc' }],
244
+ columnOrder: ['id', 'internal'],
245
+ },
246
+ },
247
+ },
248
+ }),
249
+ execute: (_command) => {
250
+ page += 1;
251
+ revision += 1;
252
+ },
253
+ });
254
+ registerWebMcpUiTools({
255
+ controlRegistry: createControlInteractionRegistry(),
256
+ dataSurfaceRegistry: surfaces,
257
+ document: browser.document,
258
+ });
259
+ const list = findTool(browser.registered, 'smrt_ui_list_data_surfaces');
260
+ const inspect = findTool(browser.registered, 'smrt_ui_inspect_data_surface');
261
+ const execute = findTool(browser.registered, 'smrt_ui_execute_data_surface_control');
262
+ const identity = { surfaceId: 'content', kind: 'table' };
263
+ expect((await parse(list.execute({}))).result[0].columns).toHaveLength(1);
264
+ const snapshot = (await parse(inspect.execute({ identity }))).result;
265
+ expect(snapshot.descriptor.query.projectableColumnIds).toEqual(['id']);
266
+ expect(snapshot.state).not.toHaveProperty('internal');
267
+ expect(JSON.stringify(snapshot)).not.toContain('nested-hidden-filter');
268
+ expect(snapshot.state.table.state.columnOrder).toEqual(['id']);
269
+ expect(snapshot.state.table.state.filters).toEqual([
270
+ { columnId: 'id', value: ['internal'] },
271
+ ]);
272
+ const hiddenRowKeyDescriptor = descriptor();
273
+ hiddenRowKeyDescriptor.rowKey = 'internal';
274
+ const hiddenRowKeySurfaces = createDataSurfaceRegistry();
275
+ hiddenRowKeySurfaces.register({
276
+ descriptor: hiddenRowKeyDescriptor,
277
+ getSnapshot: () => ({
278
+ revision: 0,
279
+ state: {
280
+ table: {
281
+ state: {
282
+ selection: {
283
+ scope: 'explicit',
284
+ rowIds: ['nested-private-row-id'],
285
+ },
286
+ selectedRowIds: ['nested-private-row-id'],
287
+ expandedRowIds: ['nested-private-row-id'],
288
+ },
289
+ },
290
+ },
291
+ selection: { scope: 'explicit-ids', rowIds: ['private-row-id'] },
292
+ }),
293
+ });
294
+ const hiddenRowKeyBrowser = modelContext();
295
+ registerWebMcpUiTools({
296
+ controlRegistry: createControlInteractionRegistry(),
297
+ dataSurfaceRegistry: hiddenRowKeySurfaces,
298
+ document: hiddenRowKeyBrowser.document,
299
+ });
300
+ const hiddenRowKeyList = await parse(findTool(hiddenRowKeyBrowser.registered, 'smrt_ui_list_data_surfaces').execute({}));
301
+ expect(hiddenRowKeyList.result[0]).not.toHaveProperty('rowKey');
302
+ const hiddenRowKeyInspect = await parse(findTool(hiddenRowKeyBrowser.registered, 'smrt_ui_inspect_data_surface').execute({ identity }));
303
+ expect(hiddenRowKeyInspect.result.selection).toBeNull();
304
+ expect(JSON.stringify(hiddenRowKeyInspect)).not.toContain('private-row-id');
305
+ expect(JSON.stringify(hiddenRowKeyInspect)).not.toContain('nested-private-row-id');
306
+ const command = {
307
+ version: 1,
308
+ commandId: 'page-1',
309
+ identity,
310
+ expectedRevision: 2,
311
+ controlId: 'next-page',
312
+ };
313
+ const completed = (await parse(execute.execute(command))).result;
314
+ expect(completed).toMatchObject({
315
+ ok: true,
316
+ revision: 3,
317
+ });
318
+ expect(completed.snapshot.descriptor.columns).toHaveLength(1);
319
+ expect(completed.snapshot.state).not.toHaveProperty('internal');
320
+ expect((await parse(execute.execute(command))).result).toMatchObject({
321
+ ok: true,
322
+ revision: 3,
323
+ });
324
+ expect((await parse(execute.execute({ ...command, controlId: 'different-control' }))).result).toMatchObject({ ok: false, reason: 'idempotency_conflict' });
325
+ const stale = (await parse(execute.execute({ ...command, commandId: 'page-2' }))).result;
326
+ expect(stale).toMatchObject({ ok: false, reason: 'stale_revision' });
327
+ expect(stale.snapshot.descriptor.columns).toHaveLength(1);
328
+ });
329
+ it('rejects invalid and oversized requests with distinct failures', async () => {
330
+ const browser = modelContext();
331
+ const controls = createControlInteractionRegistry();
332
+ registerWebMcpUiTools({
333
+ controlRegistry: controls,
334
+ dataSurfaceRegistry: createDataSurfaceRegistry(),
335
+ document: browser.document,
336
+ });
337
+ const inspect = findTool(browser.registered, 'smrt_ui_inspect_form_control');
338
+ expect(await parse(inspect.execute({ identity: { formId: '', controlId: 'name' } }))).toEqual({
339
+ ok: false,
340
+ reason: 'invalid_identifier',
341
+ details: 'formId',
342
+ });
343
+ expect(await parse(inspect.execute({ payload: 'x'.repeat(100_001) }))).toEqual({ ok: false, reason: 'limit_exceeded' });
344
+ controls.register({
345
+ identity: { formId: 'broken', controlId: 'value' },
346
+ metadata: { kind: 'text' },
347
+ getValue: () => {
348
+ throw new Error('private host detail');
349
+ },
350
+ });
351
+ const list = findTool(browser.registered, 'smrt_ui_list_form_controls');
352
+ const failed = await parse(list.execute({ formId: 'broken' }));
353
+ expect(failed).toEqual({ ok: false, reason: 'execution_failed' });
354
+ expect(JSON.stringify(failed)).not.toContain('private host detail');
355
+ });
356
+ it('locks a prefix atomically, permits distinct prefixes, and no-ops without WebMCP', () => {
357
+ const browser = modelContext();
358
+ const registries = {
359
+ controlRegistry: createControlInteractionRegistry(),
360
+ dataSurfaceRegistry: createDataSurfaceRegistry(),
361
+ };
362
+ const dispose = registerWebMcpUiTools({
363
+ ...registries,
364
+ document: browser.document,
365
+ });
366
+ expect(() => registerWebMcpUiTools({ ...registries, document: browser.document })).toThrow('already registered');
367
+ expect(browser.registered).toHaveLength(6);
368
+ const disposeOther = registerWebMcpUiTools({
369
+ ...registries,
370
+ prefix: 'other_',
371
+ document: browser.document,
372
+ });
373
+ expect(browser.registered).toHaveLength(12);
374
+ disposeOther();
375
+ dispose();
376
+ expect(() => registerWebMcpUiTools({
377
+ ...registries,
378
+ document: {},
379
+ })).not.toThrow();
380
+ });
381
+ it('aborts every partial registration and releases the lock when a host rejects a tool', () => {
382
+ const signals = [];
383
+ let calls = 0;
384
+ const document = {
385
+ modelContext: {
386
+ registerTool(_tool, options) {
387
+ calls += 1;
388
+ if (options?.signal)
389
+ signals.push(options.signal);
390
+ if (calls === 3)
391
+ throw new Error('host collision');
392
+ },
393
+ },
394
+ };
395
+ const registries = {
396
+ controlRegistry: createControlInteractionRegistry(),
397
+ dataSurfaceRegistry: createDataSurfaceRegistry(),
398
+ };
399
+ expect(() => registerWebMcpUiTools({ ...registries, document })).toThrow('host collision');
400
+ expect(signals.every((signal) => signal.aborted)).toBe(true);
401
+ calls = 0;
402
+ document.modelContext.registerTool = (_tool, options) => {
403
+ calls += 1;
404
+ if (options?.signal)
405
+ signals.push(options.signal);
406
+ };
407
+ expect(() => registerWebMcpUiTools({ ...registries, document })).not.toThrow();
408
+ expect(calls).toBe(6);
409
+ });
410
+ });
@@ -5,7 +5,7 @@ import Harness from './webmcp-harness.svelte';
5
5
  function installModelContext() {
6
6
  const registered = [];
7
7
  document.modelContext = {
8
- registerTool(tool, options) {
8
+ async registerTool(tool, options) {
9
9
  registered.push({ name: tool.name, signal: options?.signal });
10
10
  },
11
11
  };
@@ -16,5 +16,9 @@
16
16
  */
17
17
  export { type ActivityFeedHandle, type ActivityFeedMap, type ActivityFeedOptions, activityFeed, type ShellActivityInput, } from './activity-feed.svelte.js';
18
18
  export { type LiveCollection, type LiveCollectionMutation, type LiveCollectionOptions, type LiveCollectionStatus, liveCollection, } from './live-collection.svelte.js';
19
+ export { type RemoteQueryBinding, remoteQuery, } from './remote-query.svelte.js';
19
20
  export { type UpdateAvailableView, type UseUpdateAvailableOptions, useUpdateAvailable, } from './update-available.svelte.js';
21
+ export type { WebMcpProviderConfig, WebMcpUiProviderConfig, } from './webmcp-provider.js';
22
+ export { type RegisterWebMcpUiToolsOptions, registerWebMcpUiTools, } from './webmcp-ui.js';
23
+ export { useWebMcpUi, type WebMcpUiContext, } from './webmcp-ui-context.js';
20
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/web/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,YAAY,EACZ,KAAK,kBAAkB,GACxB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,cAAc,GACf,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,kBAAkB,GACnB,MAAM,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/web/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,YAAY,EACZ,KAAK,kBAAkB,GACxB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,cAAc,GACf,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,KAAK,kBAAkB,EACvB,WAAW,GACZ,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,kBAAkB,GACnB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EACV,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,KAAK,4BAA4B,EACjC,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,WAAW,EACX,KAAK,eAAe,GACrB,MAAM,wBAAwB,CAAC"}
package/dist/web/index.js CHANGED
@@ -16,4 +16,7 @@
16
16
  */
17
17
  export { activityFeed, } from './activity-feed.svelte.js';
18
18
  export { liveCollection, } from './live-collection.svelte.js';
19
+ export { remoteQuery, } from './remote-query.svelte.js';
19
20
  export { useUpdateAvailable, } from './update-available.svelte.js';
21
+ export { registerWebMcpUiTools, } from './webmcp-ui.js';
22
+ export { useWebMcpUi, } from './webmcp-ui-context.js';
@@ -0,0 +1,26 @@
1
+ import { type SmrtWebCollection, type SmrtWebDataQueryRequest, type SmrtWebDataQueryResult, type SmrtWebQueryLiveSubscription, type SmrtWebQueryRunOptions, type SmrtWebQueryState, type SmrtWebQueryTransport } from '@happyvertical/smrt-web';
2
+ export interface RemoteQueryBinding<TData extends object = object> {
3
+ readonly rows: ReadonlyArray<TData>;
4
+ readonly page: SmrtWebQueryState<TData>['page'];
5
+ readonly total: SmrtWebQueryState<TData>['total'];
6
+ readonly loading: boolean;
7
+ readonly refreshing: boolean;
8
+ readonly stale: boolean;
9
+ readonly error: unknown;
10
+ readonly lastUpdated: number | undefined;
11
+ readonly request: SmrtWebDataQueryRequest | undefined;
12
+ execute(request: SmrtWebDataQueryRequest, options?: SmrtWebQueryRunOptions): Promise<SmrtWebDataQueryResult>;
13
+ refresh(options?: Omit<SmrtWebQueryRunOptions, 'mode' | 'force'>): Promise<SmrtWebDataQueryResult | undefined>;
14
+ retry(): Promise<SmrtWebDataQueryResult | undefined>;
15
+ subscribeLive(): SmrtWebQueryLiveSubscription | undefined;
16
+ dispose(): void;
17
+ }
18
+ /**
19
+ * Bind a canonical remote query to Svelte 5 state. The binding is query
20
+ * shaped: rows are only the requested page and never the entire collection.
21
+ * Call during component initialization so cleanup follows the component.
22
+ */
23
+ export declare function remoteQuery<TData extends object>(collection: SmrtWebCollection<TData>, transport: SmrtWebQueryTransport, options?: {
24
+ staleTimeMs?: number;
25
+ }): RemoteQueryBinding<TData>;
26
+ //# sourceMappingURL=remote-query.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-query.svelte.d.ts","sourceRoot":"","sources":["../../src/web/remote-query.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAE3B,KAAK,4BAA4B,EACjC,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,kBAAkB,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM;IAC/D,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC;IAChD,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC;IAClD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,OAAO,EAAE,uBAAuB,GAAG,SAAS,CAAC;IACtD,OAAO,CACL,OAAO,EAAE,uBAAuB,EAChC,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACnC,OAAO,CACL,OAAO,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,MAAM,GAAG,OAAO,CAAC,GACvD,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC,CAAC;IAC/C,KAAK,IAAI,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC,CAAC;IACrD,aAAa,IAAI,4BAA4B,GAAG,SAAS,CAAC;IAC1D,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAC9C,UAAU,EAAE,iBAAiB,CAAC,KAAK,CAAC,EACpC,SAAS,EAAE,qBAAqB,EAChC,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACjC,kBAAkB,CAAC,KAAK,CAAC,CAwD3B"}
@@ -0,0 +1,57 @@
1
+ import { createSmrtWebQuery, } from '@happyvertical/smrt-web';
2
+ /**
3
+ * Bind a canonical remote query to Svelte 5 state. The binding is query
4
+ * shaped: rows are only the requested page and never the entire collection.
5
+ * Call during component initialization so cleanup follows the component.
6
+ */
7
+ export function remoteQuery(collection, transport, options) {
8
+ const query = createSmrtWebQuery(collection, transport, options);
9
+ let snapshot = $state(query.state);
10
+ let activeRequest = $state(query.request);
11
+ const unsubscribe = query.subscribe((next) => {
12
+ snapshot = next;
13
+ activeRequest = query.request;
14
+ });
15
+ $effect(() => () => {
16
+ unsubscribe();
17
+ query.dispose();
18
+ });
19
+ const binding = {
20
+ get rows() {
21
+ return snapshot.rows;
22
+ },
23
+ get page() {
24
+ return snapshot.page;
25
+ },
26
+ get total() {
27
+ return snapshot.total;
28
+ },
29
+ get loading() {
30
+ return snapshot.loading;
31
+ },
32
+ get refreshing() {
33
+ return snapshot.refreshing;
34
+ },
35
+ get stale() {
36
+ return snapshot.stale;
37
+ },
38
+ get error() {
39
+ return snapshot.error;
40
+ },
41
+ get lastUpdated() {
42
+ return snapshot.lastUpdated;
43
+ },
44
+ get request() {
45
+ return activeRequest;
46
+ },
47
+ execute: (request, runOptions) => query.execute(request, runOptions),
48
+ refresh: (runOptions) => query.refresh(runOptions),
49
+ retry: () => query.retry(),
50
+ subscribeLive: () => query.subscribeLive(),
51
+ dispose: () => {
52
+ unsubscribe();
53
+ query.dispose();
54
+ },
55
+ };
56
+ return binding;
57
+ }
@@ -0,0 +1,23 @@
1
+ import type { DataSurfaceRegistry } from '@happyvertical/smrt-ui/data';
2
+ import type { ControlInteractionRegistry } from '@happyvertical/smrt-ui/forms';
3
+ import type { RegisterWebMcpToolsOptions, SmrtWebClient, WebMcpExposurePolicy, WebMcpRegistrationDefinition } from '@happyvertical/smrt-web';
4
+ export interface WebMcpUiProviderConfig {
5
+ controlRegistry?: ControlInteractionRegistry;
6
+ dataSurfaceRegistry?: DataSurfaceRegistry;
7
+ /** Document-global namespace prefix. @default 'smrt_ui_' */
8
+ prefix?: string;
9
+ }
10
+ export interface WebMcpProviderConfig extends WebMcpExposurePolicy {
11
+ definitions?: readonly WebMcpRegistrationDefinition[];
12
+ client?: SmrtWebClient;
13
+ basePath?: string;
14
+ fetchFn?: typeof fetch;
15
+ scope?: string;
16
+ filter?: RegisterWebMcpToolsOptions['filter'];
17
+ filterTool?: RegisterWebMcpToolsOptions['filterTool'];
18
+ resolveFetchers?: RegisterWebMcpToolsOptions['resolveFetchers'];
19
+ resolveToolFetchers?: RegisterWebMcpToolsOptions['resolveToolFetchers'];
20
+ /** Fixed browser-native tools over the mounted form/data registries. */
21
+ ui?: false | WebMcpUiProviderConfig;
22
+ }
23
+ //# sourceMappingURL=webmcp-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webmcp-provider.d.ts","sourceRoot":"","sources":["../../src/web/webmcp-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC/E,OAAO,KAAK,EACV,0BAA0B,EAC1B,aAAa,EACb,oBAAoB,EACpB,4BAA4B,EAC7B,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,sBAAsB;IACrC,eAAe,CAAC,EAAE,0BAA0B,CAAC;IAC7C,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAqB,SAAQ,oBAAoB;IAChE,WAAW,CAAC,EAAE,SAAS,4BAA4B,EAAE,CAAC;IACtD,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IAC9C,UAAU,CAAC,EAAE,0BAA0B,CAAC,YAAY,CAAC,CAAC;IACtD,eAAe,CAAC,EAAE,0BAA0B,CAAC,iBAAiB,CAAC,CAAC;IAChE,mBAAmB,CAAC,EAAE,0BAA0B,CAAC,qBAAqB,CAAC,CAAC;IACxE,wEAAwE;IACxE,EAAE,CAAC,EAAE,KAAK,GAAG,sBAAsB,CAAC;CACrC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { DataSurfaceRegistry } from '@happyvertical/smrt-ui/data';
2
+ import type { ControlInteractionRegistry } from '@happyvertical/smrt-ui/forms';
3
+ export interface WebMcpUiContext {
4
+ readonly enabled: boolean;
5
+ readonly controlRegistry: ControlInteractionRegistry;
6
+ readonly dataSurfaceRegistry: DataSurfaceRegistry;
7
+ }
8
+ export declare function setWebMcpUiContext(context: WebMcpUiContext): void;
9
+ export declare function tryGetWebMcpUiContext(): WebMcpUiContext | null;
10
+ /** Return the mounted-UI registries owned by the nearest SMRT Provider. */
11
+ export declare function useWebMcpUi(): WebMcpUiContext;
12
+ //# sourceMappingURL=webmcp-ui-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webmcp-ui-context.d.ts","sourceRoot":"","sources":["../../src/web/webmcp-ui-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAK/E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,0BAA0B,CAAC;IACrD,QAAQ,CAAC,mBAAmB,EAAE,mBAAmB,CAAC;CACnD;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAEjE;AAED,wBAAgB,qBAAqB,IAAI,eAAe,GAAG,IAAI,CAE9D;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,IAAI,eAAe,CAQ7C"}
@@ -0,0 +1,16 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const WEBMCP_UI_CONTEXT_KEY = Symbol('smrt-webmcp-ui-context');
3
+ export function setWebMcpUiContext(context) {
4
+ setContext(WEBMCP_UI_CONTEXT_KEY, context);
5
+ }
6
+ export function tryGetWebMcpUiContext() {
7
+ return getContext(WEBMCP_UI_CONTEXT_KEY) ?? null;
8
+ }
9
+ /** Return the mounted-UI registries owned by the nearest SMRT Provider. */
10
+ export function useWebMcpUi() {
11
+ const context = tryGetWebMcpUiContext();
12
+ if (!context?.enabled) {
13
+ throw new Error('WebMCP UI context not found. Wrap this component with <Provider>.');
14
+ }
15
+ return context;
16
+ }
@@ -0,0 +1,21 @@
1
+ import { type DataSurfaceRegistry } from '@happyvertical/smrt-ui/data';
2
+ import type { ControlInteractionRegistry } from '@happyvertical/smrt-ui/forms';
3
+ import type { WebMcpToolSpec } from './webmcp.svelte.js';
4
+ export interface RegisterWebMcpUiToolsOptions {
5
+ controlRegistry: ControlInteractionRegistry;
6
+ dataSurfaceRegistry: DataSurfaceRegistry;
7
+ prefix?: string;
8
+ /** Injectable browser document used by tests and non-window hosts. */
9
+ document?: {
10
+ modelContext?: WebMcpModelContextLike;
11
+ };
12
+ }
13
+ interface WebMcpModelContextLike {
14
+ registerTool(tool: WebMcpToolSpec, options?: {
15
+ signal?: AbortSignal;
16
+ }): void | Promise<void>;
17
+ }
18
+ /** Register the fixed browser-native adapter over mounted UI registries. */
19
+ export declare function registerWebMcpUiTools(options: RegisterWebMcpUiToolsOptions): () => void;
20
+ export {};
21
+ //# sourceMappingURL=webmcp-ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webmcp-ui.d.ts","sourceRoot":"","sources":["../../src/web/webmcp-ui.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,mBAAmB,EAGzB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAIV,0BAA0B,EAE3B,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AA6CzD,MAAM,WAAW,4BAA4B;IAC3C,eAAe,EAAE,0BAA0B,CAAC;IAC5C,mBAAmB,EAAE,mBAAmB,CAAC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,QAAQ,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,sBAAsB,CAAA;KAAE,CAAC;CACtD;AAED,UAAU,sBAAsB;IAC9B,YAAY,CACV,IAAI,EAAE,cAAc,EACpB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACjC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AA4eD,4EAA4E;AAC5E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,GACpC,MAAM,IAAI,CA+CZ"}