@adia-ai/a2ui 0.8.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1073 -0
- package/README.md +99 -0
- package/a2ui.schema.d.ts +192 -0
- package/controllers/accordion.js +73 -0
- package/controllers/base.js +68 -0
- package/controllers/data-stream.js +281 -0
- package/controllers/form.js +81 -0
- package/controllers/index.js +6 -0
- package/controllers/selection.js +82 -0
- package/controllers/state-machine.js +135 -0
- package/controllers/toggle.js +40 -0
- package/dockables/action.d.ts +55 -0
- package/dockables/action.js +152 -0
- package/dockables/base.d.ts +26 -0
- package/dockables/base.js +30 -0
- package/dockables/controller.d.ts +35 -0
- package/dockables/controller.js +97 -0
- package/dockables/data-source.d.ts +35 -0
- package/dockables/data-source.js +103 -0
- package/dockables/index.d.ts +21 -0
- package/dockables/index.js +6 -0
- package/dockables/lifecycle.d.ts +38 -0
- package/dockables/lifecycle.js +84 -0
- package/dockables/provider.d.ts +28 -0
- package/dockables/provider.js +59 -0
- package/index.d.ts +64 -0
- package/index.js +54 -0
- package/package.json +89 -0
- package/prop-apply.d.ts +13 -0
- package/prop-apply.js +113 -0
- package/registry.d.ts +17 -0
- package/registry.js +418 -0
- package/renderer.d.ts +67 -0
- package/renderer.js +715 -0
- package/stream.d.ts +62 -0
- package/stream.js +521 -0
- package/surface-manifest.d.ts +73 -0
- package/surface-manifest.js +294 -0
- package/surface.d.ts +72 -0
- package/surface.js +222 -0
- package/types.d.ts +26 -0
- package/validate/CHANGELOG.md +1005 -0
- package/validate/README.md +146 -0
- package/validate/index.d.ts +4 -0
- package/validate/index.js +12 -0
- package/validate/validator.d.ts +4 -0
- package/validate/validator.js +1232 -0
- package/wire-factory.d.ts +15 -0
- package/wire-factory.js +134 -0
- package/wiring-engine.d.ts +61 -0
- package/wiring-engine.js +209 -0
- package/wiring-registry.d.ts +80 -0
- package/wiring-registry.js +342 -0
package/stream.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2UI Stream Adapters — connect to various transports and yield A2UI messages.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Options shared by stream adapters that accept a catalog and/or AbortSignal. */
|
|
6
|
+
export interface StreamOptions {
|
|
7
|
+
/** Component type catalog — limits types advertised to the server. */
|
|
8
|
+
catalog?: Map<string, unknown> | Record<string, unknown>;
|
|
9
|
+
/** AbortSignal to cancel the stream. */
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Options for mcpStream — also accepts an action callback. */
|
|
14
|
+
export interface McpStreamOptions extends StreamOptions {
|
|
15
|
+
/** Called when the MCP server emits an a2ui/action RPC method. */
|
|
16
|
+
onAction?: (name: string, args: unknown) => Promise<unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* SSE (Server-Sent Events) stream adapter.
|
|
21
|
+
* Returns an AsyncIterable that yields parsed A2UI messages.
|
|
22
|
+
*/
|
|
23
|
+
export declare function sseStream(
|
|
24
|
+
url: string,
|
|
25
|
+
options?: StreamOptions,
|
|
26
|
+
): AsyncIterable<unknown>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* WebSocket stream adapter.
|
|
30
|
+
* Returns an AsyncIterable that yields parsed A2UI messages.
|
|
31
|
+
*/
|
|
32
|
+
export declare function wsStream(
|
|
33
|
+
url: string,
|
|
34
|
+
options?: StreamOptions,
|
|
35
|
+
): AsyncIterable<unknown>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Array / mock stream adapter — for testing.
|
|
39
|
+
* Yields each message in the array, optionally with a delay between items.
|
|
40
|
+
*/
|
|
41
|
+
export declare function mockStream(
|
|
42
|
+
messages: readonly unknown[],
|
|
43
|
+
delay?: number,
|
|
44
|
+
): AsyncIterable<unknown>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* MCP (Model Context Protocol) stream adapter over WebSocket.
|
|
48
|
+
* Yields A2UI messages extracted from MCP JSON-RPC tool results.
|
|
49
|
+
*/
|
|
50
|
+
export declare function mcpStream(
|
|
51
|
+
url: string,
|
|
52
|
+
options?: McpStreamOptions,
|
|
53
|
+
): AsyncIterable<unknown>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* JSONL (newline-delimited JSON) stream adapter via fetch.
|
|
57
|
+
* Yields each parsed line from the response body.
|
|
58
|
+
*/
|
|
59
|
+
export declare function jsonlStream(
|
|
60
|
+
url: string,
|
|
61
|
+
options?: StreamOptions,
|
|
62
|
+
): AsyncIterable<unknown>;
|
package/stream.js
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2UI Stream Adapters — connect to various transports and yield A2UI messages.
|
|
3
|
+
*
|
|
4
|
+
* Each adapter returns an AsyncIterable<A2UIMessage>.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const stream = sseStream('/api/agent');
|
|
8
|
+
* for await (const message of stream) { renderer.process(message); }
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* SSE (Server-Sent Events) stream adapter.
|
|
13
|
+
*/
|
|
14
|
+
export function sseStream(url, options = {}) {
|
|
15
|
+
let finalUrl = url;
|
|
16
|
+
if (options.catalog) {
|
|
17
|
+
const types = options.catalog instanceof Map ? [...options.catalog.keys()] : Object.keys(options.catalog);
|
|
18
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
19
|
+
finalUrl = `${url}${sep}a2ui_catalog=${encodeURIComponent(types.join(','))}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
[Symbol.asyncIterator]() {
|
|
24
|
+
const eventSource = new EventSource(finalUrl);
|
|
25
|
+
const queue = [];
|
|
26
|
+
let resolve = null;
|
|
27
|
+
let done = false;
|
|
28
|
+
|
|
29
|
+
eventSource.onmessage = (e) => {
|
|
30
|
+
try {
|
|
31
|
+
const message = JSON.parse(e.data);
|
|
32
|
+
if (resolve) { const r = resolve; resolve = null; r({ value: message, done: false }); }
|
|
33
|
+
else queue.push(message);
|
|
34
|
+
} catch { console.warn('A2UI SSE: invalid JSON', e.data); }
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
eventSource.onerror = () => {
|
|
38
|
+
done = true;
|
|
39
|
+
eventSource.close();
|
|
40
|
+
if (resolve) { const r = resolve; resolve = null; r({ value: undefined, done: true }); }
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (options.signal) {
|
|
44
|
+
options.signal.addEventListener('abort', () => {
|
|
45
|
+
done = true;
|
|
46
|
+
eventSource.close();
|
|
47
|
+
if (resolve) { const r = resolve; resolve = null; r({ value: undefined, done: true }); }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
next() {
|
|
53
|
+
if (queue.length > 0) return Promise.resolve({ value: queue.shift(), done: false });
|
|
54
|
+
if (done) return Promise.resolve({ value: undefined, done: true });
|
|
55
|
+
return new Promise(r => { resolve = r; });
|
|
56
|
+
},
|
|
57
|
+
return() {
|
|
58
|
+
done = true;
|
|
59
|
+
eventSource.close();
|
|
60
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* WebSocket stream adapter.
|
|
69
|
+
*/
|
|
70
|
+
export function wsStream(url, options = {}) {
|
|
71
|
+
return {
|
|
72
|
+
[Symbol.asyncIterator]() {
|
|
73
|
+
const ws = new WebSocket(url);
|
|
74
|
+
const queue = [];
|
|
75
|
+
let resolve = null;
|
|
76
|
+
let done = false;
|
|
77
|
+
|
|
78
|
+
ws.onopen = () => {
|
|
79
|
+
if (options.catalog) {
|
|
80
|
+
const types = options.catalog instanceof Map ? [...options.catalog.keys()] : Object.keys(options.catalog);
|
|
81
|
+
ws.send(JSON.stringify({ type: 'a2ui:catalog', supportedTypes: types }));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
ws.onmessage = (e) => {
|
|
86
|
+
try {
|
|
87
|
+
const message = JSON.parse(e.data);
|
|
88
|
+
if (resolve) { const r = resolve; resolve = null; r({ value: message, done: false }); }
|
|
89
|
+
else queue.push(message);
|
|
90
|
+
} catch { console.warn('A2UI WS: invalid JSON', e.data); }
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
ws.onclose = () => {
|
|
94
|
+
done = true;
|
|
95
|
+
if (resolve) { const r = resolve; resolve = null; r({ value: undefined, done: true }); }
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
ws.onerror = () => { done = true; ws.close(); };
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
next() {
|
|
102
|
+
if (queue.length > 0) return Promise.resolve({ value: queue.shift(), done: false });
|
|
103
|
+
if (done) return Promise.resolve({ value: undefined, done: true });
|
|
104
|
+
return new Promise(r => { resolve = r; });
|
|
105
|
+
},
|
|
106
|
+
return() { done = true; ws.close(); return Promise.resolve({ value: undefined, done: true }); },
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Array/mock stream adapter — for testing.
|
|
114
|
+
*/
|
|
115
|
+
export function mockStream(messages, delay = 0) {
|
|
116
|
+
return {
|
|
117
|
+
async *[Symbol.asyncIterator]() {
|
|
118
|
+
for (const msg of messages) {
|
|
119
|
+
if (delay > 0) await new Promise(r => setTimeout(r, delay));
|
|
120
|
+
yield msg;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* MCP stream adapter — connects to MCP server, yields A2UI messages.
|
|
128
|
+
*
|
|
129
|
+
* Dispatches by URL scheme:
|
|
130
|
+
* ws://, wss:// -> WebSocket transport (legacy / custom MCP-over-WS)
|
|
131
|
+
* http://, https:// -> MCP Streamable HTTP (spec 2025-11-25)
|
|
132
|
+
* relative path -> assume same-origin Streamable HTTP
|
|
133
|
+
*/
|
|
134
|
+
export function mcpStream(url, options = {}) {
|
|
135
|
+
const scheme = (url.split(':')[0] || '').toLowerCase();
|
|
136
|
+
if (scheme === 'ws' || scheme === 'wss') {
|
|
137
|
+
return mcpStreamWebSocket(url, options);
|
|
138
|
+
}
|
|
139
|
+
if (scheme === 'http' || scheme === 'https') {
|
|
140
|
+
return mcpStreamHttp(url, options);
|
|
141
|
+
}
|
|
142
|
+
// Default: relative path -> same-origin HTTP
|
|
143
|
+
return mcpStreamHttp(url, options);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Helper: extract A2UI messages from a JSON-RPC `result.content` array
|
|
148
|
+
* and push them into a queue/resolver pair.
|
|
149
|
+
*/
|
|
150
|
+
function _extractA2UIFromRpc(rpc, push) {
|
|
151
|
+
if (rpc.result?.content) {
|
|
152
|
+
for (const block of rpc.result.content) {
|
|
153
|
+
if (block.type === 'resource' && block.resource?.mimeType === 'application/json+a2ui') {
|
|
154
|
+
try {
|
|
155
|
+
const messages = JSON.parse(block.resource.text);
|
|
156
|
+
for (const msg of (Array.isArray(messages) ? messages : [messages])) push(msg);
|
|
157
|
+
} catch { /* malformed payload */ }
|
|
158
|
+
} else if (block.type === 'text') {
|
|
159
|
+
try {
|
|
160
|
+
const msg = JSON.parse(block.text);
|
|
161
|
+
if (msg.type && (msg.type.startsWith('create') || msg.type.startsWith('update') || msg.type.startsWith('delete'))) {
|
|
162
|
+
push(msg);
|
|
163
|
+
}
|
|
164
|
+
} catch { /* not A2UI */ }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* MCP over WebSocket — original implementation, unchanged behavior.
|
|
172
|
+
*/
|
|
173
|
+
function mcpStreamWebSocket(url, options) {
|
|
174
|
+
return {
|
|
175
|
+
async *[Symbol.asyncIterator]() {
|
|
176
|
+
const { signal, catalog, onAction } = options;
|
|
177
|
+
|
|
178
|
+
const ws = new WebSocket(url);
|
|
179
|
+
await new Promise((resolve, reject) => {
|
|
180
|
+
ws.onopen = resolve;
|
|
181
|
+
ws.onerror = reject;
|
|
182
|
+
if (signal) signal.addEventListener('abort', () => {
|
|
183
|
+
ws.close();
|
|
184
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
185
|
+
}, { once: true });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (catalog) {
|
|
189
|
+
const types = catalog instanceof Map ? [...catalog.keys()] : Object.keys(catalog);
|
|
190
|
+
ws.send(JSON.stringify({
|
|
191
|
+
jsonrpc: '2.0',
|
|
192
|
+
method: 'a2ui/catalog',
|
|
193
|
+
params: { supportedTypes: types },
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const queue = [];
|
|
198
|
+
let resolve = null;
|
|
199
|
+
let done = false;
|
|
200
|
+
const push = (msg) => {
|
|
201
|
+
if (resolve) { const r = resolve; resolve = null; r({ value: msg, done: false }); }
|
|
202
|
+
else queue.push(msg);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
ws.onmessage = (e) => {
|
|
206
|
+
try {
|
|
207
|
+
const rpc = JSON.parse(e.data);
|
|
208
|
+
_extractA2UIFromRpc(rpc, push);
|
|
209
|
+
|
|
210
|
+
if (rpc.method === 'a2ui/action' && onAction) {
|
|
211
|
+
onAction(rpc.params?.name, rpc.params?.arguments).then(result => {
|
|
212
|
+
ws.send(JSON.stringify({ jsonrpc: '2.0', id: rpc.id, result }));
|
|
213
|
+
}).catch(err => {
|
|
214
|
+
ws.send(JSON.stringify({ jsonrpc: '2.0', id: rpc.id, error: { code: -1, message: err.message } }));
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
} catch { console.warn('A2UI MCP: invalid message', e.data); }
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
ws.onclose = () => { done = true; if (resolve) { const r = resolve; resolve = null; r({ value: undefined, done: true }); } };
|
|
221
|
+
ws.onerror = () => { done = true; ws.close(); };
|
|
222
|
+
if (signal) signal.addEventListener('abort', () => { done = true; ws.close(); if (resolve) { const r = resolve; resolve = null; r({ value: undefined, done: true }); } });
|
|
223
|
+
|
|
224
|
+
while (!done || queue.length > 0) {
|
|
225
|
+
if (queue.length > 0) { yield queue.shift(); }
|
|
226
|
+
else if (done) { break; }
|
|
227
|
+
else {
|
|
228
|
+
const v = await new Promise(r => { resolve = r; });
|
|
229
|
+
if (v.done) break;
|
|
230
|
+
yield v.value;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* MCP over Streamable HTTP — per MCP spec 2025-11-25.
|
|
239
|
+
*
|
|
240
|
+
* Protocol:
|
|
241
|
+
* 1. POST <url> with JSON-RPC `initialize` request. Server replies with
|
|
242
|
+
* `mcp-session-id` header and either JSON or an SSE stream.
|
|
243
|
+
* 2. POST <url> with `notifications/initialized` (header set) to complete handshake.
|
|
244
|
+
* 3. GET <url> with `mcp-session-id` header (Accept: text/event-stream) opens
|
|
245
|
+
* a long-lived SSE stream of server->client JSON-RPC messages.
|
|
246
|
+
* 4. Each SSE `data:` event is a JSON-RPC message. We extract A2UI messages
|
|
247
|
+
* from `result.content` blocks the same way as the WS path.
|
|
248
|
+
* 5. If the server emits an `a2ui/action` request method, we POST a JSON-RPC
|
|
249
|
+
* response back to <url> with the session header.
|
|
250
|
+
*
|
|
251
|
+
* Note: this is a minimal client. Tools must be invoked separately by the
|
|
252
|
+
* consumer (this adapter is the *transport*, not a tool-call orchestrator).
|
|
253
|
+
* If the consumer needs feature-complete MCP semantics (resumability, request
|
|
254
|
+
* batching, cancellation tokens), use `@modelcontextprotocol/sdk`'s
|
|
255
|
+
* `StreamableHTTPClientTransport` directly.
|
|
256
|
+
*/
|
|
257
|
+
function mcpStreamHttp(url, options) {
|
|
258
|
+
return {
|
|
259
|
+
async *[Symbol.asyncIterator]() {
|
|
260
|
+
const { signal, catalog, onAction } = options;
|
|
261
|
+
|
|
262
|
+
const queue = [];
|
|
263
|
+
let resolve = null;
|
|
264
|
+
let done = false;
|
|
265
|
+
const push = (msg) => {
|
|
266
|
+
if (resolve) { const r = resolve; resolve = null; r({ value: msg, done: false }); }
|
|
267
|
+
else queue.push(msg);
|
|
268
|
+
};
|
|
269
|
+
const finish = () => {
|
|
270
|
+
done = true;
|
|
271
|
+
if (resolve) { const r = resolve; resolve = null; r({ value: undefined, done: true }); }
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// --- 1. initialize handshake ---
|
|
275
|
+
const initBody = {
|
|
276
|
+
jsonrpc: '2.0',
|
|
277
|
+
id: 1,
|
|
278
|
+
method: 'initialize',
|
|
279
|
+
params: {
|
|
280
|
+
protocolVersion: '2025-11-25',
|
|
281
|
+
capabilities: {},
|
|
282
|
+
clientInfo: { name: 'a2ui', version: '0.0.0' },
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
let initResp;
|
|
287
|
+
try {
|
|
288
|
+
initResp = await fetch(url, {
|
|
289
|
+
method: 'POST',
|
|
290
|
+
headers: {
|
|
291
|
+
'Content-Type': 'application/json',
|
|
292
|
+
'Accept': 'application/json, text/event-stream',
|
|
293
|
+
},
|
|
294
|
+
body: JSON.stringify(initBody),
|
|
295
|
+
signal,
|
|
296
|
+
});
|
|
297
|
+
} catch (err) {
|
|
298
|
+
console.warn('A2UI MCP HTTP: initialize failed', err);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!initResp.ok) {
|
|
303
|
+
console.warn(`A2UI MCP HTTP: initialize returned ${initResp.status}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const sessionId = initResp.headers.get('mcp-session-id');
|
|
308
|
+
if (!sessionId) {
|
|
309
|
+
console.warn('A2UI MCP HTTP: server did not return mcp-session-id header');
|
|
310
|
+
// Try to drain body so the connection can be reused
|
|
311
|
+
try { await initResp.text(); } catch { /* ignore */ }
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Drain initialize response body (we don't need its result for streaming)
|
|
316
|
+
try {
|
|
317
|
+
const ct = initResp.headers.get('content-type') || '';
|
|
318
|
+
if (ct.includes('text/event-stream')) {
|
|
319
|
+
// Read & discard
|
|
320
|
+
const reader = initResp.body.getReader();
|
|
321
|
+
// Read just enough to consume the initialize response then move on
|
|
322
|
+
const decoder = new TextDecoder();
|
|
323
|
+
let buf = '';
|
|
324
|
+
// eslint-disable-next-line no-constant-condition
|
|
325
|
+
while (true) {
|
|
326
|
+
const { done: rd, value } = await reader.read();
|
|
327
|
+
if (rd) break;
|
|
328
|
+
buf += decoder.decode(value, { stream: true });
|
|
329
|
+
if (buf.includes('\n\n')) break; // got at least one SSE event
|
|
330
|
+
}
|
|
331
|
+
try { reader.cancel(); } catch { /* ignore */ }
|
|
332
|
+
} else {
|
|
333
|
+
await initResp.json().catch(() => null);
|
|
334
|
+
}
|
|
335
|
+
} catch { /* ignore */ }
|
|
336
|
+
|
|
337
|
+
// --- 2. send `notifications/initialized` ---
|
|
338
|
+
try {
|
|
339
|
+
await fetch(url, {
|
|
340
|
+
method: 'POST',
|
|
341
|
+
headers: {
|
|
342
|
+
'Content-Type': 'application/json',
|
|
343
|
+
'Accept': 'application/json, text/event-stream',
|
|
344
|
+
'mcp-session-id': sessionId,
|
|
345
|
+
},
|
|
346
|
+
body: JSON.stringify({
|
|
347
|
+
jsonrpc: '2.0',
|
|
348
|
+
method: 'notifications/initialized',
|
|
349
|
+
}),
|
|
350
|
+
signal,
|
|
351
|
+
}).then(r => r.text()).catch(() => null);
|
|
352
|
+
} catch { /* ignore */ }
|
|
353
|
+
|
|
354
|
+
// --- 3. optional: advertise catalog as a custom notification ---
|
|
355
|
+
if (catalog) {
|
|
356
|
+
const types = catalog instanceof Map ? [...catalog.keys()] : Object.keys(catalog);
|
|
357
|
+
try {
|
|
358
|
+
await fetch(url, {
|
|
359
|
+
method: 'POST',
|
|
360
|
+
headers: {
|
|
361
|
+
'Content-Type': 'application/json',
|
|
362
|
+
'Accept': 'application/json, text/event-stream',
|
|
363
|
+
'mcp-session-id': sessionId,
|
|
364
|
+
},
|
|
365
|
+
body: JSON.stringify({
|
|
366
|
+
jsonrpc: '2.0',
|
|
367
|
+
method: 'a2ui/catalog',
|
|
368
|
+
params: { supportedTypes: types },
|
|
369
|
+
}),
|
|
370
|
+
signal,
|
|
371
|
+
}).then(r => r.text()).catch(() => null);
|
|
372
|
+
} catch { /* ignore */ }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Helper to POST a JSON-RPC response (for server->client requests)
|
|
376
|
+
const postRpc = (msg) => {
|
|
377
|
+
return fetch(url, {
|
|
378
|
+
method: 'POST',
|
|
379
|
+
headers: {
|
|
380
|
+
'Content-Type': 'application/json',
|
|
381
|
+
'Accept': 'application/json, text/event-stream',
|
|
382
|
+
'mcp-session-id': sessionId,
|
|
383
|
+
},
|
|
384
|
+
body: JSON.stringify(msg),
|
|
385
|
+
signal,
|
|
386
|
+
}).then(r => r.text()).catch(() => null);
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// Handle an incoming JSON-RPC message
|
|
390
|
+
const handleRpc = (rpc) => {
|
|
391
|
+
_extractA2UIFromRpc(rpc, push);
|
|
392
|
+
if (rpc.method === 'a2ui/action' && onAction) {
|
|
393
|
+
Promise.resolve()
|
|
394
|
+
.then(() => onAction(rpc.params?.name, rpc.params?.arguments))
|
|
395
|
+
.then(result => postRpc({ jsonrpc: '2.0', id: rpc.id, result }))
|
|
396
|
+
.catch(err => postRpc({ jsonrpc: '2.0', id: rpc.id, error: { code: -1, message: err.message } }));
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// --- 4. open long-lived GET SSE stream ---
|
|
401
|
+
let getResp;
|
|
402
|
+
try {
|
|
403
|
+
getResp = await fetch(url, {
|
|
404
|
+
method: 'GET',
|
|
405
|
+
headers: {
|
|
406
|
+
'Accept': 'text/event-stream',
|
|
407
|
+
'mcp-session-id': sessionId,
|
|
408
|
+
},
|
|
409
|
+
signal,
|
|
410
|
+
});
|
|
411
|
+
} catch (err) {
|
|
412
|
+
if (err?.name !== 'AbortError') console.warn('A2UI MCP HTTP: GET stream failed', err);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!getResp.ok || !getResp.body) {
|
|
417
|
+
console.warn(`A2UI MCP HTTP: GET stream returned ${getResp.status}`);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Wire abort
|
|
422
|
+
if (signal) {
|
|
423
|
+
signal.addEventListener('abort', finish, { once: true });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Best-effort: DELETE session on stream close to free server-side state
|
|
427
|
+
const closeSession = () => {
|
|
428
|
+
try {
|
|
429
|
+
fetch(url, {
|
|
430
|
+
method: 'DELETE',
|
|
431
|
+
headers: { 'mcp-session-id': sessionId },
|
|
432
|
+
}).catch(() => null);
|
|
433
|
+
} catch { /* ignore */ }
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// SSE parser loop (running concurrently with the yield loop)
|
|
437
|
+
(async () => {
|
|
438
|
+
const reader = getResp.body.getReader();
|
|
439
|
+
const decoder = new TextDecoder();
|
|
440
|
+
let buffer = '';
|
|
441
|
+
try {
|
|
442
|
+
while (!done) {
|
|
443
|
+
const { done: rd, value } = await reader.read();
|
|
444
|
+
if (rd) break;
|
|
445
|
+
buffer += decoder.decode(value, { stream: true });
|
|
446
|
+
let idx;
|
|
447
|
+
// SSE events separated by blank lines
|
|
448
|
+
while ((idx = buffer.indexOf('\n\n')) !== -1) {
|
|
449
|
+
const rawEvent = buffer.slice(0, idx);
|
|
450
|
+
buffer = buffer.slice(idx + 2);
|
|
451
|
+
const dataLines = [];
|
|
452
|
+
for (const line of rawEvent.split('\n')) {
|
|
453
|
+
if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart());
|
|
454
|
+
}
|
|
455
|
+
if (dataLines.length === 0) continue;
|
|
456
|
+
const data = dataLines.join('\n');
|
|
457
|
+
try {
|
|
458
|
+
const rpc = JSON.parse(data);
|
|
459
|
+
handleRpc(rpc);
|
|
460
|
+
} catch {
|
|
461
|
+
console.warn('A2UI MCP HTTP: invalid JSON-RPC SSE payload', data);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} catch (err) {
|
|
466
|
+
if (err?.name !== 'AbortError') console.warn('A2UI MCP HTTP: stream read error', err);
|
|
467
|
+
} finally {
|
|
468
|
+
finish();
|
|
469
|
+
closeSession();
|
|
470
|
+
}
|
|
471
|
+
})();
|
|
472
|
+
|
|
473
|
+
while (!done || queue.length > 0) {
|
|
474
|
+
if (queue.length > 0) { yield queue.shift(); }
|
|
475
|
+
else if (done) { break; }
|
|
476
|
+
else {
|
|
477
|
+
const v = await new Promise(r => { resolve = r; });
|
|
478
|
+
if (v.done) break;
|
|
479
|
+
yield v.value;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* JSONL (newline-delimited JSON) stream adapter via fetch.
|
|
488
|
+
*/
|
|
489
|
+
export function jsonlStream(url, options = {}) {
|
|
490
|
+
return {
|
|
491
|
+
async *[Symbol.asyncIterator]() {
|
|
492
|
+
const headers = {};
|
|
493
|
+
if (options.catalog) {
|
|
494
|
+
const types = options.catalog instanceof Map ? [...options.catalog.keys()] : Object.keys(options.catalog);
|
|
495
|
+
headers['X-A2UI-Catalog'] = types.join(',');
|
|
496
|
+
}
|
|
497
|
+
const response = await fetch(url, { signal: options.signal, headers });
|
|
498
|
+
const reader = response.body.getReader();
|
|
499
|
+
const decoder = new TextDecoder();
|
|
500
|
+
let buffer = '';
|
|
501
|
+
|
|
502
|
+
while (true) {
|
|
503
|
+
const { done, value } = await reader.read();
|
|
504
|
+
if (done) break;
|
|
505
|
+
buffer += decoder.decode(value, { stream: true });
|
|
506
|
+
const lines = buffer.split('\n');
|
|
507
|
+
buffer = lines.pop();
|
|
508
|
+
for (const line of lines) {
|
|
509
|
+
const trimmed = line.trim();
|
|
510
|
+
if (!trimmed) continue;
|
|
511
|
+
try { yield JSON.parse(trimmed); }
|
|
512
|
+
catch { console.warn('A2UI JSONL: invalid JSON', trimmed); }
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (buffer.trim()) {
|
|
516
|
+
try { yield JSON.parse(buffer.trim()); }
|
|
517
|
+
catch { /* ignore */ }
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface Manifest — Multi-surface relationship document (A008).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface SurfaceDescriptor {
|
|
6
|
+
name: string;
|
|
7
|
+
route?: string;
|
|
8
|
+
entryPoint?: boolean;
|
|
9
|
+
requiredParams?: string[];
|
|
10
|
+
produces?: Record<string, object>;
|
|
11
|
+
consumes?: Record<string, { keys: string[] }>;
|
|
12
|
+
status?: 'generated' | 'manual' | 'template' | 'placeholder';
|
|
13
|
+
generatedBy?: string;
|
|
14
|
+
tags?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type AssociationType =
|
|
18
|
+
| 'routes-to'
|
|
19
|
+
| 'feeds'
|
|
20
|
+
| 'shares-context'
|
|
21
|
+
| 'depends-on'
|
|
22
|
+
| 'triggers'
|
|
23
|
+
| 'contains'
|
|
24
|
+
| 'slots-into';
|
|
25
|
+
|
|
26
|
+
export interface Association {
|
|
27
|
+
type: AssociationType;
|
|
28
|
+
from: string;
|
|
29
|
+
to: string;
|
|
30
|
+
trigger?: string;
|
|
31
|
+
params?: Record<string, object>;
|
|
32
|
+
mapping?: object;
|
|
33
|
+
context?: string;
|
|
34
|
+
condition?: string;
|
|
35
|
+
fallback?: string;
|
|
36
|
+
effect?: string;
|
|
37
|
+
slot?: string;
|
|
38
|
+
position?: number;
|
|
39
|
+
meta?: object;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ManifestValidationResult {
|
|
43
|
+
valid: boolean;
|
|
44
|
+
issues: Array<{ severity: 'error' | 'warning'; message: string }>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ManifestOptions {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
version?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export declare class SurfaceManifest {
|
|
54
|
+
constructor(opts: ManifestOptions);
|
|
55
|
+
|
|
56
|
+
readonly surfaceIds: string[];
|
|
57
|
+
|
|
58
|
+
addSurface(surfaceId: string, descriptor: SurfaceDescriptor): void;
|
|
59
|
+
removeSurface(surfaceId: string): void;
|
|
60
|
+
getSurface(surfaceId: string): SurfaceDescriptor | null;
|
|
61
|
+
|
|
62
|
+
addAssociation(association: Association): void;
|
|
63
|
+
getAssociationsFrom(surfaceId: string, type?: AssociationType): Association[];
|
|
64
|
+
getAssociationsTo(surfaceId: string, type?: AssociationType): Association[];
|
|
65
|
+
getSharedContextPeers(surfaceId: string): Array<{ surfaceId: string; context: string | undefined }>;
|
|
66
|
+
|
|
67
|
+
defineSharedContext(name: string, config: object): void;
|
|
68
|
+
getSharedContext(name: string): object | null;
|
|
69
|
+
|
|
70
|
+
validate(): ManifestValidationResult;
|
|
71
|
+
toJSON(): object;
|
|
72
|
+
static fromJSON(json: object): SurfaceManifest;
|
|
73
|
+
}
|