@webmcp-auto-ui/sdk 2.5.28 → 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.28",
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",
package/src/index.ts CHANGED
@@ -44,7 +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
- return encode(base, json, { compress: 'gz' });
47
+ // Skip gzip for small payloads — overhead exceeds savings under ~1KB.
48
+ return encode(base, json, { compress: json.length < 1024 ? 'none' : 'gz' });
48
49
  }
49
50
 
50
51
  export async function decodeHyperSkill(urlOrParam: string): Promise<HyperSkill> {
@@ -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,15 +60,23 @@ 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
+ */
49
69
  export interface DataServer {
50
- name: string; // unique identifier (user-chosen label)
70
+ name: string; // user-chosen label
51
71
  url: string;
52
- kind: 'data';
53
- enabled: boolean; // user intent; bridge only connects to enabled servers
54
- connected: boolean; // flipped by the bridge after handshake
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
55
77
  tools?: McpToolInfo[];
56
78
  recipes?: { name: string; description?: string; body?: string }[];
57
- error?: string; // handshake error message, if any
79
+ error?: string;
58
80
  }
59
81
 
60
82
  export interface CanvasSnapshot {
@@ -79,16 +101,13 @@ export interface CanvasSnapshot {
79
101
 
80
102
  type Listener = () => void;
81
103
 
82
- function uuid() {
83
- return 'w_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
84
- }
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); }
85
106
 
86
- function msgId() {
87
- return 'm_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
88
- }
107
+ const NAME_ALIAS: Record<string, string> = { 'moulineuse': 'Tricoteuses' };
108
+ function aliasName(n: string): string { return NAME_ALIAS[n] ?? n; }
89
109
 
90
110
  function createCanvasVanilla() {
91
- // ── Subscribers ────────────────────────────────────────────────────────
92
111
  const listeners = new Set<Listener>();
93
112
  function notify() { listeners.forEach(fn => fn()); }
94
113
 
@@ -96,63 +115,175 @@ function createCanvasVanilla() {
96
115
  let _blocks: Widget[] = [];
97
116
  let _mode: Mode = 'drag';
98
117
  let _llm: LLMId = 'haiku';
99
- let _mcpUrl = '';
100
- let _mcpConnected = false;
101
- let _mcpConnecting = false;
102
- let _mcpName = '';
103
- const MCP_NAME_MAP: Record<string, string> = { 'moulineuse': 'Tricoteuses' };
104
- let _mcpTools: McpToolInfo[] = [];
105
118
  let _messages: ChatMsg[] = [];
106
119
  let _generating = false;
107
- let _statusText = '● no MCP connected';
108
- let _statusColor = 'text-zinc-600';
109
120
  let _themeOverrides: Record<string, string> = {};
110
121
  let _enabledServerIds: string[] = ['autoui'];
111
- let _dataServers: DataServer[] = [];
112
-
113
- // ── Data servers (multi-MCP) ───────────────────────────────────────────
114
- function addDataServer(desc: { name: string; url: string }): DataServer {
115
- const existing = _dataServers.find((s) => s.name === desc.name);
116
- if (existing) return existing;
117
- const srv: DataServer = { name: desc.name, url: desc.url, kind: 'data', enabled: true, connected: false };
118
- _dataServers = [..._dataServers, srv];
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];
119
188
  notify();
120
189
  return srv;
121
190
  }
122
191
 
123
192
  function removeDataServer(name: string): boolean {
124
- const before = _dataServers.length;
125
- _dataServers = _dataServers.filter((s) => s.name !== name);
126
- if (_dataServers.length !== before) { notify(); return true; }
193
+ const before = _servers.length;
194
+ _servers = _servers.filter((s) => s.name !== name);
195
+ if (_servers.length !== before) { notify(); return true; }
127
196
  return false;
128
197
  }
129
198
 
130
199
  function getDataServer(name: string): DataServer | undefined {
131
- return _dataServers.find((s) => s.name === name);
200
+ return _servers.find((s) => s.name === name);
132
201
  }
133
202
 
134
203
  function setDataServerMeta(name: string, patch: Partial<Omit<DataServer, 'name' | 'url' | 'kind'>>): void {
135
- const idx = _dataServers.findIndex((s) => s.name === name);
204
+ const idx = _servers.findIndex((s) => s.name === name);
136
205
  if (idx < 0) return;
137
- _dataServers = _dataServers.map((s, i) => i === idx ? { ...s, ...patch } : s);
206
+ _servers = _servers.map((s, i) => i === idx ? { ...s, ...patch } : s);
138
207
  notify();
139
208
  }
140
209
 
141
210
  function setDataServerEnabled(name: string, enabled: boolean): boolean {
142
- const s = _dataServers.find((x) => x.name === name);
211
+ const s = _servers.find((x) => x.name === name);
143
212
  if (!s) return false;
144
213
  if (s.enabled === enabled) return true;
145
- _dataServers = _dataServers.map((x) => x.name === name ? { ...x, enabled } : x);
214
+ _servers = _servers.map((x) => x.name === name ? { ...x, enabled } : x);
146
215
  notify();
147
216
  return true;
148
217
  }
149
218
 
150
219
  function toggleDataServer(name: string): boolean {
151
- const s = _dataServers.find((x) => x.name === name);
220
+ const s = _servers.find((x) => x.name === name);
152
221
  if (!s) return false;
153
222
  return setDataServerEnabled(name, !s.enabled);
154
223
  }
155
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
+ }
286
+
156
287
  // ── Widget actions ─────────────────────────────────────────────────────
157
288
  function addWidget(type: WidgetType, data: Record<string, unknown> = {}): Widget {
158
289
  const widget: Widget = { id: uuid(), type, data };
@@ -160,20 +291,15 @@ function createCanvasVanilla() {
160
291
  notify();
161
292
  return widget;
162
293
  }
163
-
164
- /** @deprecated Use addWidget */
165
294
  const addBlock = addWidget;
166
-
167
295
  function removeBlock(id: string) {
168
296
  _blocks = _blocks.filter((b) => b.id !== id);
169
297
  notify();
170
298
  }
171
-
172
299
  function updateBlock(id: string, data: Partial<Record<string, unknown>>) {
173
300
  _blocks = _blocks.map((b) => b.id === id ? { ...b, data: { ...b.data, ...data } } : b);
174
301
  notify();
175
302
  }
176
-
177
303
  function moveBlock(fromId: string, toId: string) {
178
304
  const fi = _blocks.findIndex((b) => b.id === fromId);
179
305
  const ti = _blocks.findIndex((b) => b.id === toId);
@@ -184,16 +310,8 @@ function createCanvasVanilla() {
184
310
  _blocks = next;
185
311
  notify();
186
312
  }
187
-
188
- function clearBlocks() {
189
- _blocks = [];
190
- notify();
191
- }
192
-
193
- function setBlocks(newBlocks: Widget[]) {
194
- _blocks = newBlocks;
195
- notify();
196
- }
313
+ function clearBlocks() { _blocks = []; notify(); }
314
+ function setBlocks(newBlocks: Widget[]) { _blocks = newBlocks; notify(); }
197
315
 
198
316
  // ── Chat ───────────────────────────────────────────────────────────────
199
317
  function addMsg(role: ChatMsg['role'], content: string, thinking = false): ChatMsg {
@@ -202,56 +320,11 @@ function createCanvasVanilla() {
202
320
  notify();
203
321
  return msg;
204
322
  }
205
-
206
323
  function updateMsg(id: string, content: string, thinking = false) {
207
324
  _messages = _messages.map((m) => m.id === id ? { ...m, content, thinking } : m);
208
325
  notify();
209
326
  }
210
-
211
- function clearMessages() {
212
- _messages = [];
213
- notify();
214
- }
215
-
216
- // ── MCP ────────────────────────────────────────────────────────────────
217
- function setMcpConnecting(connecting: boolean) {
218
- _mcpConnecting = connecting;
219
- if (connecting) {
220
- _statusText = '● connexion…';
221
- _statusColor = 'text-amber-400';
222
- }
223
- notify();
224
- }
225
-
226
- function setMcpConnected(
227
- connected: boolean,
228
- name?: string,
229
- tools?: McpToolInfo[]
230
- ) {
231
- _mcpConnected = connected;
232
- if (connected) {
233
- if (name) _mcpName = name;
234
- if (tools) _mcpTools = tools;
235
- _statusText = `● ${name} · ${tools?.length ?? 0} tools`;
236
- _statusColor = 'text-teal-400';
237
- } else {
238
- // Reset stale connection state on disconnect so the UI doesn't keep
239
- // advertising the previous server name / tool list.
240
- _mcpName = '';
241
- _mcpTools = [];
242
- _statusText = '● no MCP connected';
243
- _statusColor = 'text-zinc-600';
244
- }
245
- notify();
246
- }
247
-
248
- function setMcpError(err: string) {
249
- _mcpConnected = false;
250
- _mcpConnecting = false;
251
- _statusText = `● erreur: ${err}`;
252
- _statusColor = 'text-red-400';
253
- notify();
254
- }
327
+ function clearMessages() { _messages = []; notify(); }
255
328
 
256
329
  // ── Theme ──────────────────────────────────────────────────────────────
257
330
  function setThemeOverrides(overrides: Record<string, string>) {
@@ -259,7 +332,7 @@ function createCanvasVanilla() {
259
332
  notify();
260
333
  }
261
334
 
262
- // ── Enabled servers ───────────────────────────────────────────────────
335
+ // ── Enabled servers (kept for UI server catalogue) ─────────────────────
263
336
  function setEnabledServers(ids: string[]) {
264
337
  _enabledServerIds = ids;
265
338
  notify();
@@ -267,11 +340,12 @@ function createCanvasVanilla() {
267
340
 
268
341
  // ── HyperSkill ─────────────────────────────────────────────────────────
269
342
  function buildSkillJSON() {
343
+ const p = primaryServer();
270
344
  const skill: Record<string, unknown> = {
271
345
  version: '1.0',
272
346
  name: 'skill-' + Date.now(),
273
347
  created: new Date().toISOString(),
274
- mcp: _mcpUrl,
348
+ mcp: p?.url ?? '',
275
349
  llm: _llm,
276
350
  blocks: _blocks.map((b) => ({ type: b.type, data: JSON.parse(JSON.stringify(b.data)) })),
277
351
  };
@@ -282,7 +356,8 @@ function createCanvasVanilla() {
282
356
 
283
357
  async function buildHyperskillParam(): Promise<string> {
284
358
  const json = JSON.stringify(buildSkillJSON());
285
- const url = await encode('https://x.local', json, { compress: 'gz' });
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' });
286
361
  return new URL(url).searchParams.get('hs')!;
287
362
  }
288
363
 
@@ -293,7 +368,7 @@ function createCanvasVanilla() {
293
368
  servers?: string[];
294
369
  blocks?: { type: WidgetType; data: Record<string, unknown> }[];
295
370
  }) {
296
- if (skill.mcp) _mcpUrl = skill.mcp;
371
+ if (skill.mcp) ensurePrimary(skill.mcp);
297
372
  if (skill.llm) _llm = skill.llm;
298
373
  if (skill.theme) _themeOverrides = skill.theme;
299
374
  if (skill.servers) _enabledServerIds = skill.servers;
@@ -302,15 +377,11 @@ function createCanvasVanilla() {
302
377
  }
303
378
  notify();
304
379
  }
305
-
306
- // Try hyperskills NPM decode (handles gz., br., and plain base64)
307
380
  try {
308
381
  const { content: json } = await decode(param);
309
382
  applySkill(JSON.parse(json));
310
383
  return true;
311
- } catch { /* fall through to legacy */ }
312
-
313
- // Fallback: legacy format (plain base64 with escape/unescape)
384
+ } catch { /* fall through */ }
314
385
  try {
315
386
  let b64 = param.replace(/ /g, '+').replace(/-/g, '+').replace(/_/g, '/');
316
387
  while (b64.length % 4) b64 += '=';
@@ -322,7 +393,6 @@ function createCanvasVanilla() {
322
393
  }
323
394
  }
324
395
 
325
- // ── loadFromUrl ────────────────────────────────────────────────────────
326
396
  async function loadFromUrl(url: string): Promise<boolean> {
327
397
  try {
328
398
  const { content: raw } = await decode(url);
@@ -331,12 +401,9 @@ function createCanvasVanilla() {
331
401
  content?: { blocks?: { type: WidgetType; data: Record<string, unknown> }[] };
332
402
  servers?: string[];
333
403
  };
334
- if (decoded.meta?.mcp) _mcpUrl = decoded.meta.mcp as string;
404
+ if (decoded.meta?.mcp) ensurePrimary(decoded.meta.mcp as string);
335
405
  if (decoded.meta?.llm) _llm = decoded.meta.llm as LLMId;
336
406
  if (decoded.meta?.theme) _themeOverrides = decoded.meta.theme as Record<string, string>;
337
- // Align with loadFromParam: restore enabledServerIds when `servers` is
338
- // present. buildSkillJSON emits it at the root, but tolerate it under
339
- // `meta` as well for forward/back compatibility.
340
407
  const servers = (decoded.servers ?? (decoded.meta?.servers as string[] | undefined));
341
408
  if (Array.isArray(servers)) _enabledServerIds = servers;
342
409
  if (decoded.content?.blocks) _blocks = decoded.content.blocks.map((b) => ({ id: uuid(), type: b.type, data: b.data }));
@@ -347,89 +414,77 @@ function createCanvasVanilla() {
347
414
  }
348
415
  }
349
416
 
350
- // ── Snapshot ────────────────────────────────────────────────────────────
417
+ // ── Snapshot (fields kept for API stability) ───────────────────────────
351
418
  function getSnapshot(): CanvasSnapshot {
419
+ const p = primaryServer();
352
420
  return {
353
421
  blocks: _blocks,
354
422
  mode: _mode,
355
423
  llm: _llm,
356
- mcpUrl: _mcpUrl,
357
- mcpConnected: _mcpConnected,
358
- mcpConnecting: _mcpConnecting,
359
- mcpName: _mcpName,
360
- mcpTools: _mcpTools,
424
+ mcpUrl: p?.url ?? '',
425
+ mcpConnected: p?.connected ?? false,
426
+ mcpConnecting: anyConnecting(),
427
+ mcpName: displayName(),
428
+ mcpTools: unionTools(),
361
429
  messages: _messages,
362
430
  generating: _generating,
363
- statusText: _statusText,
364
- statusColor: _statusColor,
431
+ statusText: statusText(),
432
+ statusColor: statusColor(),
365
433
  themeOverrides: _themeOverrides,
366
434
  enabledServerIds: _enabledServerIds,
367
- dataServers: _dataServers,
435
+ dataServers: _servers,
368
436
  blockCount: _blocks.length,
369
437
  isEmpty: _blocks.length === 0,
370
438
  };
371
439
  }
372
440
 
373
- // ── Subscribe ──────────────────────────────────────────────────────────
374
441
  function subscribe(fn: Listener): () => void {
375
442
  listeners.add(fn);
376
443
  return () => { listeners.delete(fn); };
377
444
  }
378
445
 
379
- // ── Return public API ──────────────────────────────────────────────────
380
446
  return {
381
- // State getters + setters
447
+ // Reactive getters (read-side)
382
448
  get blocks() { return _blocks; },
383
449
  get mode() { return _mode; },
384
450
  set mode(v: Mode) { _mode = v; notify(); },
385
451
  get llm() { return _llm; },
386
452
  set llm(v: LLMId) { _llm = v; notify(); },
387
- get mcpUrl() { return _mcpUrl; },
388
- set mcpUrl(v: string) { _mcpUrl = v; notify(); },
389
- get mcpConnected() { return _mcpConnected; },
390
- get mcpConnecting() { return _mcpConnecting; },
391
- get mcpName() { return MCP_NAME_MAP[_mcpName] ?? _mcpName; },
392
- 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(); },
393
459
  get messages() { return _messages; },
394
460
  get generating() { return _generating; },
395
461
  set generating(v: boolean) { _generating = v; notify(); },
396
- get statusText() { return _statusText; },
397
- get statusColor() { return _statusColor; },
462
+ get statusText() { return statusText(); },
463
+ get statusColor() { return statusColor(); },
398
464
  get blockCount() { return _blocks.length; },
399
465
  get isEmpty() { return _blocks.length === 0; },
400
466
 
401
- // Setters (kept for backward compat)
402
467
  setMode(m: Mode) { _mode = m; notify(); },
403
468
  setLlm(l: LLMId) { _llm = l; notify(); },
404
- setMcpUrl(u: string) { _mcpUrl = u; notify(); },
469
+ setMcpUrl,
405
470
  setGenerating(g: boolean) { _generating = g; notify(); },
406
471
 
407
- // Widget actions (primary name)
408
- addWidget,
409
-
410
- // Backward compat alias
411
- addBlock,
412
-
413
- // Block actions (kept as-is)
472
+ addWidget, addBlock,
414
473
  removeBlock, updateBlock, moveBlock, clearBlocks, setBlocks,
415
-
416
- // Chat
417
474
  addMsg, updateMsg, clearMessages,
418
475
 
419
- // MCP
420
476
  setMcpConnecting, setMcpConnected, setMcpError,
421
477
 
422
- // Theme
423
478
  get themeOverrides() { return _themeOverrides; },
424
479
  setThemeOverrides,
425
480
 
426
- // Enabled servers
427
481
  get enabledServerIds() { return _enabledServerIds; },
428
482
  setEnabledServers,
429
483
 
430
- // Data servers (multi-MCP) additive, coexists with mcp* primary fields
431
- get dataServers() { return _dataServers; },
432
- set dataServers(v: DataServer[]) { _dataServers = Array.isArray(v) ? v : []; notify(); },
484
+ // Server listunified 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(); },
433
488
  addDataServer,
434
489
  removeDataServer,
435
490
  getDataServer,
@@ -437,10 +492,8 @@ function createCanvasVanilla() {
437
492
  setDataServerEnabled,
438
493
  toggleDataServer,
439
494
 
440
- // HyperSkill
441
495
  buildSkillJSON, buildHyperskillParam, loadFromParam, loadFromUrl,
442
496
 
443
- // Framework-agnostic reactivity
444
497
  subscribe,
445
498
  getSnapshot,
446
499
  };