@webmcp-auto-ui/sdk 2.5.27 → 2.5.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmcp-auto-ui/sdk",
3
- "version": "2.5.27",
3
+ "version": "2.5.29",
4
4
  "description": "Skills CRUD, HyperSkill format, Svelte 5 stores",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -1,3 +1,3 @@
1
1
  // Canvas store — Vanilla (framework-agnostic), no Svelte dependency
2
2
  export { canvasVanilla } from './stores/canvas.js';
3
- export type { Widget, WidgetType, Block, BlockType, Mode, LLMId, ChatMsg, McpToolInfo, CanvasSnapshot } from './stores/canvas.js';
3
+ export type { Widget, WidgetType, Block, BlockType, Mode, LLMId, ChatMsg, McpToolInfo, DataServer, CanvasSnapshot } from './stores/canvas.js';
package/src/canvas.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // Canvas store — Svelte 5 runes (browser-only, must be imported in Svelte components)
2
2
  export { canvas } from './stores/canvas.svelte.js';
3
- export type { Widget, WidgetType, Block, BlockType, Mode, LLMId, ChatMsg, McpToolInfo } from './stores/canvas.svelte.js';
3
+ export type { Widget, WidgetType, Block, BlockType, Mode, LLMId, ChatMsg, McpToolInfo, DataServer } from './stores/canvas.svelte.js';
@@ -32,7 +32,10 @@ async function compressGzip(bytes: Uint8Array): Promise<Uint8Array> {
32
32
  // @ts-ignore — CompressionStream is part of the DOM lib but may be missing in older TS targets
33
33
  const cs = new CompressionStream('gzip');
34
34
  const writer = cs.writable.getWriter();
35
- writer.write(bytes);
35
+ // Copy into a fresh Uint8Array to guarantee the underlying buffer is a plain
36
+ // ArrayBuffer (not SharedArrayBuffer/ArrayBufferLike) — required by
37
+ // WritableStreamDefaultWriter<BufferSource> under TS 5.x strict lib.
38
+ writer.write(new Uint8Array(bytes));
36
39
  writer.close();
37
40
  return new Uint8Array(await new Response(cs.readable).arrayBuffer());
38
41
  }
@@ -87,6 +90,7 @@ export function getHsParam(url?: string): string | null {
87
90
  return null;
88
91
  }
89
92
  }
93
+ if (typeof window === 'undefined') return null;
90
94
  return hs.getHsParam();
91
95
  }
92
96
 
package/src/index.ts CHANGED
@@ -44,9 +44,8 @@ import { encode, decode, hash, diff } from './hyperskills.js';
44
44
  export async function encodeHyperSkill(skill: HyperSkill, sourceUrl?: string): Promise<string> {
45
45
  const base = sourceUrl ?? (typeof window !== 'undefined' ? window.location.href.split('?')[0] : 'https://example.com');
46
46
  const json = JSON.stringify(skill);
47
- // Auto-compress with gzip when payload exceeds 6 KB to keep URLs under nginx limits
48
- const compress = json.length > 6144 ? 'gz' as const : undefined;
49
- return encode(base, json, compress ? { compress } : {});
47
+ // Skip gzip for small payloads overhead exceeds savings under ~1KB.
48
+ return encode(base, json, { compress: json.length < 1024 ? 'none' : 'gz' });
50
49
  }
51
50
 
52
51
  export async function decodeHyperSkill(urlOrParam: string): Promise<HyperSkill> {
@@ -94,3 +93,6 @@ export type { McpDemoServer } from './mcp-demo-servers.js';
94
93
  // Recipe runner — markdown-fence parser + JS/TS/SQL/etc executor over MCP
95
94
  export { parseBody, runCode, estimateTokens, safeStringify } from './recipes/index.js';
96
95
  export type { ParsedSegment, RunResult, RunLog, RunTab, RecipeData } from './recipes/index.js';
96
+
97
+ // Short URL — domain-dependent compact token
98
+ export { buildShortUrl, getShortToken } from './short-url.js';
@@ -0,0 +1,33 @@
1
+ // Domain-dependent short URL — compact token served from the skill's own domain.
2
+ // Not a dedicated subdomain: the skill host resolves `?n=<token>` to the full state.
3
+
4
+ import { hash } from './hyperskills.js';
5
+
6
+ /**
7
+ * Build a short URL from a source URL and the content to share.
8
+ * The short token is a prefix of the content hash, resolved server-side.
9
+ */
10
+ export async function buildShortUrl(sourceUrl: string, content: string): Promise<string> {
11
+ const h = await hash(sourceUrl, content);
12
+ const token = h.slice(0, 10);
13
+ const u = new URL(sourceUrl);
14
+ u.search = '';
15
+ u.searchParams.set('n', token);
16
+ return u.toString();
17
+ }
18
+
19
+ /**
20
+ * Read the short token from a URL or param string. Returns null if absent.
21
+ */
22
+ export function getShortToken(urlOrParam: string): string | null {
23
+ try {
24
+ if (urlOrParam.startsWith('?') || urlOrParam.includes('=')) {
25
+ const sp = new URLSearchParams(urlOrParam.replace(/^\?/, ''));
26
+ return sp.get('n');
27
+ }
28
+ const u = new URL(urlOrParam);
29
+ return u.searchParams.get('n');
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
@@ -7,10 +7,10 @@
7
7
  */
8
8
 
9
9
  import { canvasVanilla } from './canvas.js';
10
- import type { Widget, WidgetType, Mode, LLMId, ChatMsg, McpToolInfo } from './canvas.js';
10
+ import type { Widget, WidgetType, Mode, LLMId, ChatMsg, McpToolInfo, DataServer } from './canvas.js';
11
11
 
12
12
  // Re-export types (including deprecated aliases)
13
- export type { Widget, WidgetType, Mode, LLMId, ChatMsg, McpToolInfo };
13
+ export type { Widget, WidgetType, Mode, LLMId, ChatMsg, McpToolInfo, DataServer };
14
14
  export type { Block, BlockType, CanvasSnapshot } from './canvas.js';
15
15
 
16
16
  function createCanvas() {
@@ -29,6 +29,7 @@ function createCanvas() {
29
29
  let statusColor = $state(canvasVanilla.statusColor);
30
30
  let themeOverrides = $state<Record<string, string>>(canvasVanilla.themeOverrides);
31
31
  let enabledServerIds = $state<string[]>(canvasVanilla.enabledServerIds);
32
+ let dataServers = $state<DataServer[]>(canvasVanilla.dataServers);
32
33
 
33
34
  // ── Derived ─────────────────────────────────────────────────────────────
34
35
  const blockCount = $derived(blocks.length);
@@ -51,6 +52,7 @@ function createCanvas() {
51
52
  statusColor = s.statusColor;
52
53
  themeOverrides = s.themeOverrides;
53
54
  enabledServerIds = canvasVanilla.enabledServerIds;
55
+ dataServers = canvasVanilla.dataServers;
54
56
  });
55
57
 
56
58
  // ── Return public API ───────────────────────────────────────────────────
@@ -112,6 +114,16 @@ function createCanvas() {
112
114
  get enabledServerIds() { return enabledServerIds; },
113
115
  setEnabledServers: canvasVanilla.setEnabledServers.bind(canvasVanilla),
114
116
 
117
+ // Data servers (multi-MCP) — additive, coexists with mcp* primary fields
118
+ get dataServers() { return dataServers; },
119
+ set dataServers(v: DataServer[]) { canvasVanilla.dataServers = v; },
120
+ addDataServer: canvasVanilla.addDataServer.bind(canvasVanilla),
121
+ removeDataServer: canvasVanilla.removeDataServer.bind(canvasVanilla),
122
+ getDataServer: canvasVanilla.getDataServer.bind(canvasVanilla),
123
+ setDataServerMeta: canvasVanilla.setDataServerMeta.bind(canvasVanilla),
124
+ setDataServerEnabled: canvasVanilla.setDataServerEnabled.bind(canvasVanilla),
125
+ toggleDataServer: canvasVanilla.toggleDataServer.bind(canvasVanilla),
126
+
115
127
  // HyperSkill
116
128
  buildSkillJSON: canvasVanilla.buildSkillJSON.bind(canvasVanilla),
117
129
  buildHyperskillParam: canvasVanilla.buildHyperskillParam.bind(canvasVanilla),
@@ -1,12 +1,26 @@
1
1
  /**
2
2
  * Canvas state store — Vanilla (framework-agnostic)
3
- * Manages widgets on the canvas, mode, MCP connection, chat history
4
- *
5
- * This is the canonical, framework-agnostic version of the canvas store.
6
- * For Svelte 5 reactivity, use the Svelte wrapper (canvas.svelte.ts)
7
- * which re-exports this store with $state/$derived reactivity.
3
+ * Manages widgets on the canvas, mode, MCP connections, chat history.
8
4
  *
9
5
  * Reactivity: subscribe(fn) / getSnapshot() pattern (useSyncExternalStore compatible).
6
+ *
7
+ * ---------------------------------------------------------------------------
8
+ * Unified server model (2026-04-23 debloat)
9
+ * ---------------------------------------------------------------------------
10
+ *
11
+ * Historically this store had TWO parallel surfaces for MCP servers:
12
+ * - `mcpUrl` / `mcpName` / `mcpConnected` / `mcpConnecting` / `mcpTools`
13
+ * (flat, single-server or comma-joined multi)
14
+ * - `dataServers: DataServer[]` (list, managed by MultiMcpBridge)
15
+ *
16
+ * They were actually the same concept. This file now stores a single list
17
+ * (`_servers`) and derives the flat `mcp*` fields from it. All writes (via
18
+ * `setMcpConnected`, `addDataServer`, etc.) mutate the same underlying list,
19
+ * so tools populated by the agent-MCP path are visible to the notebook / data
20
+ * server consumers and vice-versa.
21
+ *
22
+ * The public API shape is preserved (both `mcp*` and `dataServers` / `addDataServer`)
23
+ * so existing apps keep working without modification.
10
24
  */
11
25
 
12
26
  import { encode, decode } from '../hyperskills.js';
@@ -16,7 +30,7 @@ export type WidgetType =
16
30
  | 'stat-card' | 'data-table' | 'timeline' | 'profile' | 'trombinoscope' | 'json-viewer'
17
31
  | 'hemicycle' | 'chart-rich' | 'cards' | 'grid-data' | 'sankey' | 'map' | 'log'
18
32
  | 'gallery' | 'carousel' | 'd3' | 'js-sandbox'
19
- | (string & {}); // accept arbitrary widget types from widget packs while keeping autocompletion
33
+ | (string & {});
20
34
 
21
35
  /** @deprecated Use WidgetType */
22
36
  export type BlockType = WidgetType;
@@ -46,6 +60,25 @@ export interface McpToolInfo {
46
60
  inputSchema?: Record<string, unknown>;
47
61
  }
48
62
 
63
+ /**
64
+ * Single MCP server entry — the one true shape.
65
+ * `primary: true` marks the "agent MCP" (used by the chat/tool-call path) —
66
+ * at most one server may be primary. Additional servers (data-only) are
67
+ * non-primary but otherwise identical.
68
+ */
69
+ export interface DataServer {
70
+ name: string; // user-chosen label
71
+ url: string;
72
+ kind: 'data'; // legacy field, kept for schema stability
73
+ enabled: boolean; // user intent
74
+ connected: boolean; // handshake completed
75
+ connecting?: boolean;
76
+ primary?: boolean; // agent MCP when true
77
+ tools?: McpToolInfo[];
78
+ recipes?: { name: string; description?: string; body?: string }[];
79
+ error?: string;
80
+ }
81
+
49
82
  export interface CanvasSnapshot {
50
83
  blocks: Widget[];
51
84
  mode: Mode;
@@ -61,22 +94,20 @@ export interface CanvasSnapshot {
61
94
  statusColor: string;
62
95
  themeOverrides: Record<string, string>;
63
96
  enabledServerIds: string[];
97
+ dataServers: DataServer[];
64
98
  blockCount: number;
65
99
  isEmpty: boolean;
66
100
  }
67
101
 
68
102
  type Listener = () => void;
69
103
 
70
- function uuid() {
71
- return 'w_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
72
- }
104
+ function uuid() { return 'w_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); }
105
+ function msgId() { return 'm_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); }
73
106
 
74
- function msgId() {
75
- return 'm_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
76
- }
107
+ const NAME_ALIAS: Record<string, string> = { 'moulineuse': 'Tricoteuses' };
108
+ function aliasName(n: string): string { return NAME_ALIAS[n] ?? n; }
77
109
 
78
110
  function createCanvasVanilla() {
79
- // ── Subscribers ────────────────────────────────────────────────────────
80
111
  const listeners = new Set<Listener>();
81
112
  function notify() { listeners.forEach(fn => fn()); }
82
113
 
@@ -84,18 +115,174 @@ function createCanvasVanilla() {
84
115
  let _blocks: Widget[] = [];
85
116
  let _mode: Mode = 'drag';
86
117
  let _llm: LLMId = 'haiku';
87
- let _mcpUrl = '';
88
- let _mcpConnected = false;
89
- let _mcpConnecting = false;
90
- let _mcpName = '';
91
- const MCP_NAME_MAP: Record<string, string> = { 'moulineuse': 'Tricoteuses' };
92
- let _mcpTools: McpToolInfo[] = [];
93
118
  let _messages: ChatMsg[] = [];
94
119
  let _generating = false;
95
- let _statusText = '● no MCP connected';
96
- let _statusColor = 'text-zinc-600';
97
120
  let _themeOverrides: Record<string, string> = {};
98
121
  let _enabledServerIds: string[] = ['autoui'];
122
+ // Single source of truth for MCP servers — agent MCP entries and data-only
123
+ // entries all live here. `primary: true` marks the agent MCP.
124
+ let _servers: DataServer[] = [];
125
+
126
+ // ── Helpers over _servers ──────────────────────────────────────────────
127
+ function primaryServer(): DataServer | undefined {
128
+ return _servers.find((s) => s.primary);
129
+ }
130
+ function connectedServers(): DataServer[] {
131
+ return _servers.filter((s) => s.connected);
132
+ }
133
+ function anyConnecting(): boolean {
134
+ return _servers.some((s) => s.connecting);
135
+ }
136
+ function unionTools(): McpToolInfo[] {
137
+ const out: McpToolInfo[] = [];
138
+ for (const s of _servers) if (s.connected && Array.isArray(s.tools)) out.push(...s.tools);
139
+ return out;
140
+ }
141
+ function displayName(): string {
142
+ const connected = connectedServers();
143
+ if (connected.length === 0) return '';
144
+ if (connected.length === 1) return aliasName(connected[0].name);
145
+ return connected.map((s) => aliasName(s.name)).join(', ');
146
+ }
147
+
148
+ // ── Derived status strings (used by the header / toast) ────────────────
149
+ function statusText(): string {
150
+ if (anyConnecting()) return '● connexion…';
151
+ const errored = _servers.find((s) => s.error && !s.connected);
152
+ if (errored) return `● erreur: ${errored.error}`;
153
+ const connected = connectedServers();
154
+ if (connected.length === 0) return '● no MCP connected';
155
+ return `● ${displayName()} · ${unionTools().length} tools`;
156
+ }
157
+ function statusColor(): string {
158
+ if (anyConnecting()) return 'text-amber-400';
159
+ const errored = _servers.find((s) => s.error && !s.connected);
160
+ if (errored) return 'text-red-400';
161
+ return connectedServers().length > 0 ? 'text-teal-400' : 'text-zinc-600';
162
+ }
163
+
164
+ // ── Server list actions (public, stable) ───────────────────────────────
165
+ function addDataServer(desc: { name: string; url: string; primary?: boolean }): DataServer {
166
+ const existing = _servers.find((s) => s.name === desc.name);
167
+ if (existing) {
168
+ if (desc.primary && !existing.primary) {
169
+ // Promote: there's only one primary. Demote others.
170
+ _servers = _servers.map((s) => ({ ...s, primary: s.name === desc.name }));
171
+ notify();
172
+ }
173
+ return existing;
174
+ }
175
+ const srv: DataServer = {
176
+ name: desc.name,
177
+ url: desc.url,
178
+ kind: 'data',
179
+ enabled: true,
180
+ connected: false,
181
+ primary: !!desc.primary,
182
+ };
183
+ if (srv.primary) {
184
+ // Demote any existing primary before inserting.
185
+ _servers = _servers.map((s) => ({ ...s, primary: false }));
186
+ }
187
+ _servers = [..._servers, srv];
188
+ notify();
189
+ return srv;
190
+ }
191
+
192
+ function removeDataServer(name: string): boolean {
193
+ const before = _servers.length;
194
+ _servers = _servers.filter((s) => s.name !== name);
195
+ if (_servers.length !== before) { notify(); return true; }
196
+ return false;
197
+ }
198
+
199
+ function getDataServer(name: string): DataServer | undefined {
200
+ return _servers.find((s) => s.name === name);
201
+ }
202
+
203
+ function setDataServerMeta(name: string, patch: Partial<Omit<DataServer, 'name' | 'url' | 'kind'>>): void {
204
+ const idx = _servers.findIndex((s) => s.name === name);
205
+ if (idx < 0) return;
206
+ _servers = _servers.map((s, i) => i === idx ? { ...s, ...patch } : s);
207
+ notify();
208
+ }
209
+
210
+ function setDataServerEnabled(name: string, enabled: boolean): boolean {
211
+ const s = _servers.find((x) => x.name === name);
212
+ if (!s) return false;
213
+ if (s.enabled === enabled) return true;
214
+ _servers = _servers.map((x) => x.name === name ? { ...x, enabled } : x);
215
+ notify();
216
+ return true;
217
+ }
218
+
219
+ function toggleDataServer(name: string): boolean {
220
+ const s = _servers.find((x) => x.name === name);
221
+ if (!s) return false;
222
+ return setDataServerEnabled(name, !s.enabled);
223
+ }
224
+
225
+ // ── Agent-MCP compatibility layer ──────────────────────────────────────
226
+ // All these mutate the SAME _servers list; `mcpUrl` targets the primary
227
+ // entry, creating one if none exists. Apps can equivalently call
228
+ // addDataServer({primary: true}) + setDataServerMeta(name, ...) directly.
229
+ function ensurePrimary(url?: string): DataServer {
230
+ let p = primaryServer();
231
+ if (p) {
232
+ if (url && p.url !== url) {
233
+ _servers = _servers.map((s) => s.name === p!.name ? { ...s, url } : s);
234
+ }
235
+ return _servers.find((s) => s.primary)!;
236
+ }
237
+ // Create a placeholder primary with a stable name derived from the URL.
238
+ const nm = url ? new URL(url, 'http://local').host || url : 'primary';
239
+ _servers = [..._servers, {
240
+ name: nm, url: url ?? '', kind: 'data', enabled: true, connected: false, primary: true,
241
+ }];
242
+ return _servers[_servers.length - 1]!;
243
+ }
244
+
245
+ function setMcpUrl(u: string): void {
246
+ // Update the primary server's URL (create one if none).
247
+ ensurePrimary(u);
248
+ notify();
249
+ }
250
+
251
+ function setMcpConnecting(connecting: boolean): void {
252
+ const p = ensurePrimary();
253
+ _servers = _servers.map((s) => s.name === p.name ? { ...s, connecting } : s);
254
+ notify();
255
+ }
256
+
257
+ function setMcpConnected(connected: boolean, name?: string, tools?: McpToolInfo[]): void {
258
+ if (!connected) {
259
+ // Disconnect all — agent-level disconnect affects the primary and
260
+ // traditionally cleared the flat tools. Mirror that by disconnecting
261
+ // all primary-flagged servers.
262
+ const p = primaryServer();
263
+ if (p) {
264
+ _servers = _servers.map((s) => s.primary
265
+ ? { ...s, connected: false, connecting: false, tools: [], error: undefined }
266
+ : s);
267
+ }
268
+ notify();
269
+ return;
270
+ }
271
+ const p = ensurePrimary();
272
+ const newName = name && name.length > 0 ? name : p.name;
273
+ _servers = _servers.map((s) => s.name === p.name
274
+ ? { ...s, name: newName, connected: true, connecting: false, tools: tools ?? s.tools ?? [], error: undefined }
275
+ : s);
276
+ notify();
277
+ }
278
+
279
+ function setMcpError(err: string): void {
280
+ const p = ensurePrimary();
281
+ _servers = _servers.map((s) => s.name === p.name
282
+ ? { ...s, connected: false, connecting: false, error: err }
283
+ : s);
284
+ notify();
285
+ }
99
286
 
100
287
  // ── Widget actions ─────────────────────────────────────────────────────
101
288
  function addWidget(type: WidgetType, data: Record<string, unknown> = {}): Widget {
@@ -104,20 +291,15 @@ function createCanvasVanilla() {
104
291
  notify();
105
292
  return widget;
106
293
  }
107
-
108
- /** @deprecated Use addWidget */
109
294
  const addBlock = addWidget;
110
-
111
295
  function removeBlock(id: string) {
112
296
  _blocks = _blocks.filter((b) => b.id !== id);
113
297
  notify();
114
298
  }
115
-
116
299
  function updateBlock(id: string, data: Partial<Record<string, unknown>>) {
117
300
  _blocks = _blocks.map((b) => b.id === id ? { ...b, data: { ...b.data, ...data } } : b);
118
301
  notify();
119
302
  }
120
-
121
303
  function moveBlock(fromId: string, toId: string) {
122
304
  const fi = _blocks.findIndex((b) => b.id === fromId);
123
305
  const ti = _blocks.findIndex((b) => b.id === toId);
@@ -128,16 +310,8 @@ function createCanvasVanilla() {
128
310
  _blocks = next;
129
311
  notify();
130
312
  }
131
-
132
- function clearBlocks() {
133
- _blocks = [];
134
- notify();
135
- }
136
-
137
- function setBlocks(newBlocks: Widget[]) {
138
- _blocks = newBlocks;
139
- notify();
140
- }
313
+ function clearBlocks() { _blocks = []; notify(); }
314
+ function setBlocks(newBlocks: Widget[]) { _blocks = newBlocks; notify(); }
141
315
 
142
316
  // ── Chat ───────────────────────────────────────────────────────────────
143
317
  function addMsg(role: ChatMsg['role'], content: string, thinking = false): ChatMsg {
@@ -146,52 +320,11 @@ function createCanvasVanilla() {
146
320
  notify();
147
321
  return msg;
148
322
  }
149
-
150
323
  function updateMsg(id: string, content: string, thinking = false) {
151
324
  _messages = _messages.map((m) => m.id === id ? { ...m, content, thinking } : m);
152
325
  notify();
153
326
  }
154
-
155
- function clearMessages() {
156
- _messages = [];
157
- notify();
158
- }
159
-
160
- // ── MCP ────────────────────────────────────────────────────────────────
161
- function setMcpConnecting(connecting: boolean) {
162
- _mcpConnecting = connecting;
163
- if (connecting) {
164
- _statusText = '● connexion…';
165
- _statusColor = 'text-amber-400';
166
- }
167
- notify();
168
- }
169
-
170
- function setMcpConnected(
171
- connected: boolean,
172
- name?: string,
173
- tools?: McpToolInfo[]
174
- ) {
175
- _mcpConnected = connected;
176
- if (name) _mcpName = name;
177
- if (tools) _mcpTools = tools;
178
- if (connected) {
179
- _statusText = `● ${name} · ${tools?.length ?? 0} tools`;
180
- _statusColor = 'text-teal-400';
181
- } else {
182
- _statusText = '● no MCP connected';
183
- _statusColor = 'text-zinc-600';
184
- }
185
- notify();
186
- }
187
-
188
- function setMcpError(err: string) {
189
- _mcpConnected = false;
190
- _mcpConnecting = false;
191
- _statusText = `● erreur: ${err}`;
192
- _statusColor = 'text-red-400';
193
- notify();
194
- }
327
+ function clearMessages() { _messages = []; notify(); }
195
328
 
196
329
  // ── Theme ──────────────────────────────────────────────────────────────
197
330
  function setThemeOverrides(overrides: Record<string, string>) {
@@ -199,7 +332,7 @@ function createCanvasVanilla() {
199
332
  notify();
200
333
  }
201
334
 
202
- // ── Enabled servers ───────────────────────────────────────────────────
335
+ // ── Enabled servers (kept for UI server catalogue) ─────────────────────
203
336
  function setEnabledServers(ids: string[]) {
204
337
  _enabledServerIds = ids;
205
338
  notify();
@@ -207,11 +340,12 @@ function createCanvasVanilla() {
207
340
 
208
341
  // ── HyperSkill ─────────────────────────────────────────────────────────
209
342
  function buildSkillJSON() {
343
+ const p = primaryServer();
210
344
  const skill: Record<string, unknown> = {
211
345
  version: '1.0',
212
346
  name: 'skill-' + Date.now(),
213
347
  created: new Date().toISOString(),
214
- mcp: _mcpUrl,
348
+ mcp: p?.url ?? '',
215
349
  llm: _llm,
216
350
  blocks: _blocks.map((b) => ({ type: b.type, data: JSON.parse(JSON.stringify(b.data)) })),
217
351
  };
@@ -222,8 +356,8 @@ function createCanvasVanilla() {
222
356
 
223
357
  async function buildHyperskillParam(): Promise<string> {
224
358
  const json = JSON.stringify(buildSkillJSON());
225
- const compress = json.length > 6144 ? 'gz' as const : undefined;
226
- const url = await encode('https://x.local', json, compress ? { compress } : {});
359
+ // Skip gzip for small payloads overhead exceeds savings under ~1KB.
360
+ const url = await encode('https://x.local', json, { compress: json.length < 1024 ? 'none' : 'gz' });
227
361
  return new URL(url).searchParams.get('hs')!;
228
362
  }
229
363
 
@@ -234,7 +368,7 @@ function createCanvasVanilla() {
234
368
  servers?: string[];
235
369
  blocks?: { type: WidgetType; data: Record<string, unknown> }[];
236
370
  }) {
237
- if (skill.mcp) _mcpUrl = skill.mcp;
371
+ if (skill.mcp) ensurePrimary(skill.mcp);
238
372
  if (skill.llm) _llm = skill.llm;
239
373
  if (skill.theme) _themeOverrides = skill.theme;
240
374
  if (skill.servers) _enabledServerIds = skill.servers;
@@ -243,15 +377,11 @@ function createCanvasVanilla() {
243
377
  }
244
378
  notify();
245
379
  }
246
-
247
- // Try hyperskills NPM decode (handles gz., br., and plain base64)
248
380
  try {
249
381
  const { content: json } = await decode(param);
250
382
  applySkill(JSON.parse(json));
251
383
  return true;
252
- } catch { /* fall through to legacy */ }
253
-
254
- // Fallback: legacy format (plain base64 with escape/unescape)
384
+ } catch { /* fall through */ }
255
385
  try {
256
386
  let b64 = param.replace(/ /g, '+').replace(/-/g, '+').replace(/_/g, '/');
257
387
  while (b64.length % 4) b64 += '=';
@@ -263,14 +393,19 @@ function createCanvasVanilla() {
263
393
  }
264
394
  }
265
395
 
266
- // ── loadFromUrl ────────────────────────────────────────────────────────
267
396
  async function loadFromUrl(url: string): Promise<boolean> {
268
397
  try {
269
398
  const { content: raw } = await decode(url);
270
- const decoded = JSON.parse(raw) as { meta?: Record<string, unknown>; content?: { blocks?: { type: WidgetType; data: Record<string, unknown> }[] } };
271
- if (decoded.meta?.mcp) _mcpUrl = decoded.meta.mcp as string;
399
+ const decoded = JSON.parse(raw) as {
400
+ meta?: Record<string, unknown>;
401
+ content?: { blocks?: { type: WidgetType; data: Record<string, unknown> }[] };
402
+ servers?: string[];
403
+ };
404
+ if (decoded.meta?.mcp) ensurePrimary(decoded.meta.mcp as string);
272
405
  if (decoded.meta?.llm) _llm = decoded.meta.llm as LLMId;
273
406
  if (decoded.meta?.theme) _themeOverrides = decoded.meta.theme as Record<string, string>;
407
+ const servers = (decoded.servers ?? (decoded.meta?.servers as string[] | undefined));
408
+ if (Array.isArray(servers)) _enabledServerIds = servers;
274
409
  if (decoded.content?.blocks) _blocks = decoded.content.blocks.map((b) => ({ id: uuid(), type: b.type, data: b.data }));
275
410
  notify();
276
411
  return true;
@@ -279,89 +414,86 @@ function createCanvasVanilla() {
279
414
  }
280
415
  }
281
416
 
282
- // ── Snapshot ────────────────────────────────────────────────────────────
417
+ // ── Snapshot (fields kept for API stability) ───────────────────────────
283
418
  function getSnapshot(): CanvasSnapshot {
419
+ const p = primaryServer();
284
420
  return {
285
421
  blocks: _blocks,
286
422
  mode: _mode,
287
423
  llm: _llm,
288
- mcpUrl: _mcpUrl,
289
- mcpConnected: _mcpConnected,
290
- mcpConnecting: _mcpConnecting,
291
- mcpName: _mcpName,
292
- mcpTools: _mcpTools,
424
+ mcpUrl: p?.url ?? '',
425
+ mcpConnected: p?.connected ?? false,
426
+ mcpConnecting: anyConnecting(),
427
+ mcpName: displayName(),
428
+ mcpTools: unionTools(),
293
429
  messages: _messages,
294
430
  generating: _generating,
295
- statusText: _statusText,
296
- statusColor: _statusColor,
431
+ statusText: statusText(),
432
+ statusColor: statusColor(),
297
433
  themeOverrides: _themeOverrides,
298
434
  enabledServerIds: _enabledServerIds,
435
+ dataServers: _servers,
299
436
  blockCount: _blocks.length,
300
437
  isEmpty: _blocks.length === 0,
301
438
  };
302
439
  }
303
440
 
304
- // ── Subscribe ──────────────────────────────────────────────────────────
305
441
  function subscribe(fn: Listener): () => void {
306
442
  listeners.add(fn);
307
443
  return () => { listeners.delete(fn); };
308
444
  }
309
445
 
310
- // ── Return public API ──────────────────────────────────────────────────
311
446
  return {
312
- // State getters + setters
447
+ // Reactive getters (read-side)
313
448
  get blocks() { return _blocks; },
314
449
  get mode() { return _mode; },
315
450
  set mode(v: Mode) { _mode = v; notify(); },
316
451
  get llm() { return _llm; },
317
452
  set llm(v: LLMId) { _llm = v; notify(); },
318
- get mcpUrl() { return _mcpUrl; },
319
- set mcpUrl(v: string) { _mcpUrl = v; notify(); },
320
- get mcpConnected() { return _mcpConnected; },
321
- get mcpConnecting() { return _mcpConnecting; },
322
- get mcpName() { return MCP_NAME_MAP[_mcpName] ?? _mcpName; },
323
- get mcpTools() { return _mcpTools; },
453
+ get mcpUrl() { return primaryServer()?.url ?? ''; },
454
+ set mcpUrl(v: string) { setMcpUrl(v); },
455
+ get mcpConnected() { return primaryServer()?.connected ?? false; },
456
+ get mcpConnecting() { return anyConnecting(); },
457
+ get mcpName() { return displayName(); },
458
+ get mcpTools() { return unionTools(); },
324
459
  get messages() { return _messages; },
325
460
  get generating() { return _generating; },
326
461
  set generating(v: boolean) { _generating = v; notify(); },
327
- get statusText() { return _statusText; },
328
- get statusColor() { return _statusColor; },
462
+ get statusText() { return statusText(); },
463
+ get statusColor() { return statusColor(); },
329
464
  get blockCount() { return _blocks.length; },
330
465
  get isEmpty() { return _blocks.length === 0; },
331
466
 
332
- // Setters (kept for backward compat)
333
467
  setMode(m: Mode) { _mode = m; notify(); },
334
468
  setLlm(l: LLMId) { _llm = l; notify(); },
335
- setMcpUrl(u: string) { _mcpUrl = u; notify(); },
469
+ setMcpUrl,
336
470
  setGenerating(g: boolean) { _generating = g; notify(); },
337
471
 
338
- // Widget actions (primary name)
339
- addWidget,
340
-
341
- // Backward compat alias
342
- addBlock,
343
-
344
- // Block actions (kept as-is)
472
+ addWidget, addBlock,
345
473
  removeBlock, updateBlock, moveBlock, clearBlocks, setBlocks,
346
-
347
- // Chat
348
474
  addMsg, updateMsg, clearMessages,
349
475
 
350
- // MCP
351
476
  setMcpConnecting, setMcpConnected, setMcpError,
352
477
 
353
- // Theme
354
478
  get themeOverrides() { return _themeOverrides; },
355
479
  setThemeOverrides,
356
480
 
357
- // Enabled servers
358
481
  get enabledServerIds() { return _enabledServerIds; },
359
482
  setEnabledServers,
360
483
 
361
- // HyperSkill
484
+ // Server list — unified store. `dataServers` kept as accessor name for
485
+ // schema/API stability; it returns ALL servers (primary + data-only).
486
+ get dataServers() { return _servers; },
487
+ set dataServers(v: DataServer[]) { _servers = Array.isArray(v) ? v : []; notify(); },
488
+ addDataServer,
489
+ removeDataServer,
490
+ getDataServer,
491
+ setDataServerMeta,
492
+ setDataServerEnabled,
493
+ toggleDataServer,
494
+
362
495
  buildSkillJSON, buildHyperskillParam, loadFromParam, loadFromUrl,
363
496
 
364
- // Framework-agnostic reactivity
365
497
  subscribe,
366
498
  getSnapshot,
367
499
  };