@webmcp-auto-ui/core 2.5.35 → 2.5.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/dist/index.d.ts CHANGED
@@ -9,8 +9,6 @@ export type { SchemaPatch } from './utils.js';
9
9
  export { textResult, jsonResult, } from './webmcp-helpers.js';
10
10
  export { McpMultiClient } from './multi-client.js';
11
11
  export type { ConnectedServer } from './multi-client.js';
12
- export { MultiMcpBridge, installMultiMcpBridge, parseRecipesFromToolResponse } from './multi-mcp-bridge.js';
13
- export type { MultiMcpBridgeOptions } from './multi-mcp-bridge.js';
14
12
  export { createWebMcpServer, parseFrontmatter, mountWidget } from './webmcp-server.js';
15
13
  export type { WebMcpServer, WebMcpServerOptions, WebMcpToolDef, WidgetEntry, WidgetRenderer, McpRecipeSummary } from './webmcp-server.js';
16
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,YAAY,EACV,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,cAAc,EACd,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,mBAAmB,EACnB,2BAA2B,EAC3B,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,qBAAqB,EACrB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,OAAO,EACP,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGvE,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,GACpB,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGxC,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,sBAAsB,EACtB,aAAa,GACd,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,wBAAwB,EACxB,aAAa,EACb,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9C,OAAO,EACL,UAAU,EACV,UAAU,GACX,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAC5G,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAGnE,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACvF,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,YAAY,EACV,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,cAAc,EACd,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,mBAAmB,EACnB,2BAA2B,EAC3B,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,qBAAqB,EACrB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,OAAO,EACP,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGvE,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,GACpB,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGxC,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,sBAAsB,EACtB,aAAa,GACd,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,wBAAwB,EACxB,aAAa,EACb,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9C,OAAO,EACL,UAAU,EACV,UAAU,GACX,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACvF,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -14,8 +14,6 @@ export { dispatchAndWait, signalCompletion, sanitizeSchema, sanitizeSchemaWithRe
14
14
  export { textResult, jsonResult, } from './webmcp-helpers.js';
15
15
  // Multi-MCP client
16
16
  export { McpMultiClient } from './multi-client.js';
17
- // Multi-MCP bridge (canvas store <-> McpMultiClient reconciler)
18
- export { MultiMcpBridge, installMultiMcpBridge, parseRecipesFromToolResponse } from './multi-mcp-bridge.js';
19
17
  // WebMCP Server
20
18
  export { createWebMcpServer, parseFrontmatter, mountWidget } from './webmcp-server.js';
21
19
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,+DAA+D;AAiD/D,aAAa;AACb,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGnD,WAAW;AACX,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,GACpB,MAAM,eAAe,CAAC;AAEvB,aAAa;AACb,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,qBAAqB;AACrB,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,sBAAsB,EACtB,aAAa,GACd,MAAM,aAAa,CAAC;AAErB,YAAY;AACZ,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,wBAAwB,EACxB,aAAa,EACb,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC;AAGpB,mCAAmC;AACnC,OAAO,EACL,UAAU,EACV,UAAU,GACX,MAAM,qBAAqB,CAAC;AAE7B,mBAAmB;AACnB,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGnD,gEAAgE;AAChE,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAG5G,gBAAgB;AAChB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,+DAA+D;AAiD/D,aAAa;AACb,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGnD,WAAW;AACX,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,GACpB,MAAM,eAAe,CAAC;AAEvB,aAAa;AACb,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,qBAAqB;AACrB,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,sBAAsB,EACtB,aAAa,GACd,MAAM,aAAa,CAAC;AAErB,YAAY;AACZ,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,wBAAwB,EACxB,aAAa,EACb,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC;AAGpB,mCAAmC;AACnC,OAAO,EACL,UAAU,EACV,UAAU,GACX,MAAM,qBAAqB,CAAC;AAE7B,mBAAmB;AACnB,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGnD,gBAAgB;AAChB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC"}
@@ -4,10 +4,6 @@ export interface ConnectedServer {
4
4
  name: string;
5
5
  tools: McpTool[];
6
6
  }
7
- export type AggregatedTool = McpTool & {
8
- serverUrl: string;
9
- serverName: string;
10
- };
11
7
  export declare class McpMultiClient {
12
8
  /** Ordered map — insertion order determines first-match priority */
13
9
  private servers;
@@ -29,19 +25,6 @@ export declare class McpMultiClient {
29
25
  * List all connected servers with their metadata.
30
26
  */
31
27
  listServers(): ConnectedServer[];
32
- /**
33
- * List ALL tools from ALL connected servers.
34
- * Each tool is augmented with its origin server URL and name.
35
- * Duplicate tool names across servers are prefixed with the server name
36
- * (e.g. "wikipedia__search") to satisfy the Claude API uniqueness constraint.
37
- */
38
- listAllTools(): AggregatedTool[];
39
- /**
40
- * Call a tool by name. Automatically routes to the correct server.
41
- * Supports both plain names ("search") and prefixed names ("wikipedia__search")
42
- * for disambiguated duplicates.
43
- */
44
- callTool(name: string, args?: Record<string, unknown>): Promise<McpToolResult>;
45
28
  /**
46
29
  * Call a tool on a SPECIFIC server (identified by URL).
47
30
  * Use this instead of callTool() when the same tool name may exist on multiple
@@ -52,8 +35,6 @@ export declare class McpMultiClient {
52
35
  * Disconnect from all servers.
53
36
  */
54
37
  disconnectAll(): Promise<void>;
55
- /** Convert a server name to a snake_case prefix for tool name disambiguation. */
56
- private normalizeServerName;
57
38
  /** Number of connected servers. */
58
39
  get serverCount(): number;
59
40
  /** True if at least one server is connected. */
@@ -1 +1 @@
1
- {"version":3,"file":"multi-client.d.ts","sourceRoot":"","sources":["../src/multi-client.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,OAAO,EACP,aAAa,EAEd,MAAM,YAAY,CAAC;AAMpB,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,EAAE,CAAC;CAClB;AAED,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAMjF,qBAAa,cAAc;IACzB,oEAAoE;IACpE,OAAO,CAAC,OAAO,CAA4E;IAM3F;;;OAGG;IACG,SAAS,CACb,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAC7C,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC;IAqB9C;;OAEG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9C;;OAEG;IACH,WAAW,IAAI,eAAe,EAAE;IAQhC;;;;;OAKG;IACH,YAAY,IAAI,cAAc,EAAE;IA+BhC;;;;OAIG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC;IA2BpF;;;;OAIG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC;IAMzG;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAapC,iFAAiF;IACjF,OAAO,CAAC,mBAAmB;IAQ3B,mCAAmC;IACnC,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,gDAAgD;IAChD,IAAI,cAAc,IAAI,OAAO,CAE5B;CACF"}
1
+ {"version":3,"file":"multi-client.d.ts","sourceRoot":"","sources":["../src/multi-client.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,OAAO,EACP,aAAa,EAEd,MAAM,YAAY,CAAC;AAMpB,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,EAAE,CAAC;CAClB;AAMD,qBAAa,cAAc;IACzB,oEAAoE;IACpE,OAAO,CAAC,OAAO,CAA4E;IAM3F;;;OAGG;IACG,SAAS,CACb,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAC7C,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC;IAgC9C;;OAEG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9C;;OAEG;IACH,WAAW,IAAI,eAAe,EAAE;IAQhC;;;;OAIG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC;IAMzG;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAapC,mCAAmC;IACnC,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,gDAAgD;IAChD,IAAI,cAAc,IAAI,OAAO,CAE5B;CACF"}
@@ -28,6 +28,17 @@ export class McpMultiClient {
28
28
  const client = new McpClient(url, clientOptions);
29
29
  const initResult = await client.connect();
30
30
  const tools = await client.listTools();
31
+ // -----------------------------------------------------------------------
32
+ // Cosmetic rebrand map: some MCP servers expose a `serverInfo.name` that
33
+ // doesn't match the public-facing brand we want to display in the UI. The
34
+ // Tricoteuses MCP server, for instance, declares itself as "moulineuse"
35
+ // (internal codename) but ships under the "Tricoteuses" brand. Rather
36
+ // than patch the upstream server, we rebadge the name on the client.
37
+ //
38
+ // TODO(2026-05-03): migrate this to an external config (e.g. a per-app
39
+ // `serverAliases` option passed to McpMultiClient) so that the core
40
+ // package has zero brand-specific knowledge baked in.
41
+ // -----------------------------------------------------------------------
31
42
  const SERVER_NAME_MAP = { 'moulineuse': 'Tricoteuses' };
32
43
  const name = SERVER_NAME_MAP[initResult.serverInfo.name] ?? initResult.serverInfo.name;
33
44
  this.servers.set(url, { client, name, tools });
@@ -53,71 +64,6 @@ export class McpMultiClient {
53
64
  }
54
65
  return result;
55
66
  }
56
- /**
57
- * List ALL tools from ALL connected servers.
58
- * Each tool is augmented with its origin server URL and name.
59
- * Duplicate tool names across servers are prefixed with the server name
60
- * (e.g. "wikipedia__search") to satisfy the Claude API uniqueness constraint.
61
- */
62
- listAllTools() {
63
- // First pass: count occurrences of each tool name across all servers
64
- const nameCounts = new Map();
65
- for (const [, entry] of this.servers) {
66
- for (const tool of entry.tools) {
67
- nameCounts.set(tool.name, (nameCounts.get(tool.name) ?? 0) + 1);
68
- }
69
- }
70
- // Second pass: build result, prefixing duplicates with serverName
71
- const result = [];
72
- for (const [url, entry] of this.servers) {
73
- for (const tool of entry.tools) {
74
- const isDuplicate = (nameCounts.get(tool.name) ?? 0) > 1;
75
- if (isDuplicate) {
76
- const prefix = this.normalizeServerName(entry.name);
77
- result.push({
78
- ...tool,
79
- name: `${prefix}__${tool.name}`,
80
- description: `[${entry.name}] ${tool.description ?? ''}`,
81
- serverUrl: url,
82
- serverName: entry.name,
83
- });
84
- }
85
- else {
86
- result.push({ ...tool, serverUrl: url, serverName: entry.name });
87
- }
88
- }
89
- }
90
- return result;
91
- }
92
- /**
93
- * Call a tool by name. Automatically routes to the correct server.
94
- * Supports both plain names ("search") and prefixed names ("wikipedia__search")
95
- * for disambiguated duplicates.
96
- */
97
- async callTool(name, args) {
98
- // 1. Exact match on original tool name (unprefixed)
99
- for (const [, entry] of this.servers) {
100
- const match = entry.tools.find((t) => t.name === name);
101
- if (match) {
102
- return entry.client.callTool(name, args);
103
- }
104
- }
105
- // 2. Prefixed name: "serverprefix__realToolName"
106
- const separatorIdx = name.indexOf('__');
107
- if (separatorIdx !== -1) {
108
- const prefix = name.slice(0, separatorIdx);
109
- const realName = name.slice(separatorIdx + 2);
110
- for (const [, entry] of this.servers) {
111
- if (this.normalizeServerName(entry.name) === prefix) {
112
- const match = entry.tools.find((t) => t.name === realName);
113
- if (match) {
114
- return entry.client.callTool(realName, args);
115
- }
116
- }
117
- }
118
- }
119
- throw new Error(`McpMultiClient: no server exposes tool "${name}"`);
120
- }
121
67
  /**
122
68
  * Call a tool on a SPECIFIC server (identified by URL).
123
69
  * Use this instead of callTool() when the same tool name may exist on multiple
@@ -141,13 +87,6 @@ export class McpMultiClient {
141
87
  this.servers.clear();
142
88
  }
143
89
  // -------------------------------------------------------------------------
144
- // Private helpers
145
- // -------------------------------------------------------------------------
146
- /** Convert a server name to a snake_case prefix for tool name disambiguation. */
147
- normalizeServerName(name) {
148
- return name.toLowerCase().replace(/[^a-z0-9_-]+/g, '_').replace(/_{2,}/g, '_').replace(/^_|_$/g, '');
149
- }
150
- // -------------------------------------------------------------------------
151
90
  // Getters
152
91
  // -------------------------------------------------------------------------
153
92
  /** Number of connected servers. */
@@ -1 +1 @@
1
- {"version":3,"file":"multi-client.js","sourceRoot":"","sources":["../src/multi-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"multi-client.js","sourceRoot":"","sources":["../src/multi-client.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmcp-auto-ui/core",
3
- "version": "2.5.35",
3
+ "version": "2.5.37",
4
4
  "description": "W3C WebMCP polyfill + MCP Streamable HTTP client — zero dependencies, framework-agnostic",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "author": "jb@media.mit.edu",
package/src/index.d.ts CHANGED
@@ -9,7 +9,5 @@ export type { SchemaPatch } from './utils.js';
9
9
  export { textResult, jsonResult, } from './webmcp-helpers.js';
10
10
  export { McpMultiClient } from './multi-client.js';
11
11
  export type { ConnectedServer } from './multi-client.js';
12
- export { MultiMcpBridge, installMultiMcpBridge, parseRecipesFromToolResponse } from './multi-mcp-bridge.js';
13
- export type { MultiMcpBridgeOptions } from './multi-mcp-bridge.js';
14
12
  export { createWebMcpServer, parseFrontmatter, mountWidget } from './webmcp-server.js';
15
13
  export type { WebMcpServer, WebMcpServerOptions, WebMcpToolDef, WidgetEntry, WidgetRenderer, McpRecipeSummary } from './webmcp-server.js';
package/src/index.ts CHANGED
@@ -93,10 +93,6 @@ export {
93
93
  export { McpMultiClient } from './multi-client.js';
94
94
  export type { ConnectedServer } from './multi-client.js';
95
95
 
96
- // Multi-MCP bridge (canvas store <-> McpMultiClient reconciler)
97
- export { MultiMcpBridge, installMultiMcpBridge, parseRecipesFromToolResponse } from './multi-mcp-bridge.js';
98
- export type { MultiMcpBridgeOptions } from './multi-mcp-bridge.js';
99
-
100
96
  // WebMCP Server
101
97
  export { createWebMcpServer, parseFrontmatter, mountWidget } from './webmcp-server.js';
102
98
  export type { WebMcpServer, WebMcpServerOptions, WebMcpToolDef, WidgetEntry, WidgetRenderer, McpRecipeSummary } from './webmcp-server.js';
@@ -4,10 +4,6 @@ export interface ConnectedServer {
4
4
  name: string;
5
5
  tools: McpTool[];
6
6
  }
7
- export type AggregatedTool = McpTool & {
8
- serverUrl: string;
9
- serverName: string;
10
- };
11
7
  export declare class McpMultiClient {
12
8
  /** Ordered map — insertion order determines first-match priority */
13
9
  private servers;
@@ -29,19 +25,6 @@ export declare class McpMultiClient {
29
25
  * List all connected servers with their metadata.
30
26
  */
31
27
  listServers(): ConnectedServer[];
32
- /**
33
- * List ALL tools from ALL connected servers.
34
- * Each tool is augmented with its origin server URL and name.
35
- * Duplicate tool names across servers are prefixed with the server name
36
- * (e.g. "wikipedia__search") to satisfy the Claude API uniqueness constraint.
37
- */
38
- listAllTools(): AggregatedTool[];
39
- /**
40
- * Call a tool by name. Automatically routes to the correct server.
41
- * Supports both plain names ("search") and prefixed names ("wikipedia__search")
42
- * for disambiguated duplicates.
43
- */
44
- callTool(name: string, args?: Record<string, unknown>): Promise<McpToolResult>;
45
28
  /**
46
29
  * Call a tool on a SPECIFIC server (identified by URL).
47
30
  * Use this instead of callTool() when the same tool name may exist on multiple
@@ -52,8 +35,6 @@ export declare class McpMultiClient {
52
35
  * Disconnect from all servers.
53
36
  */
54
37
  disconnectAll(): Promise<void>;
55
- /** Convert a server name to a snake_case prefix for tool name disambiguation. */
56
- private normalizeServerName;
57
38
  /** Number of connected servers. */
58
39
  get serverCount(): number;
59
40
  /** True if at least one server is connected. */
@@ -21,8 +21,6 @@ export interface ConnectedServer {
21
21
  tools: McpTool[];
22
22
  }
23
23
 
24
- export type AggregatedTool = McpTool & { serverUrl: string; serverName: string };
25
-
26
24
  // ---------------------------------------------------------------------------
27
25
  // McpMultiClient
28
26
  // ---------------------------------------------------------------------------
@@ -56,6 +54,17 @@ export class McpMultiClient {
56
54
  const initResult = await client.connect();
57
55
  const tools = await client.listTools();
58
56
 
57
+ // -----------------------------------------------------------------------
58
+ // Cosmetic rebrand map: some MCP servers expose a `serverInfo.name` that
59
+ // doesn't match the public-facing brand we want to display in the UI. The
60
+ // Tricoteuses MCP server, for instance, declares itself as "moulineuse"
61
+ // (internal codename) but ships under the "Tricoteuses" brand. Rather
62
+ // than patch the upstream server, we rebadge the name on the client.
63
+ //
64
+ // TODO(2026-05-03): migrate this to an external config (e.g. a per-app
65
+ // `serverAliases` option passed to McpMultiClient) so that the core
66
+ // package has zero brand-specific knowledge baked in.
67
+ // -----------------------------------------------------------------------
59
68
  const SERVER_NAME_MAP: Record<string, string> = { 'moulineuse': 'Tricoteuses' };
60
69
  const name = SERVER_NAME_MAP[initResult.serverInfo.name] ?? initResult.serverInfo.name;
61
70
  this.servers.set(url, { client, name, tools });
@@ -84,75 +93,6 @@ export class McpMultiClient {
84
93
  return result;
85
94
  }
86
95
 
87
- /**
88
- * List ALL tools from ALL connected servers.
89
- * Each tool is augmented with its origin server URL and name.
90
- * Duplicate tool names across servers are prefixed with the server name
91
- * (e.g. "wikipedia__search") to satisfy the Claude API uniqueness constraint.
92
- */
93
- listAllTools(): AggregatedTool[] {
94
- // First pass: count occurrences of each tool name across all servers
95
- const nameCounts = new Map<string, number>();
96
- for (const [, entry] of this.servers) {
97
- for (const tool of entry.tools) {
98
- nameCounts.set(tool.name, (nameCounts.get(tool.name) ?? 0) + 1);
99
- }
100
- }
101
-
102
- // Second pass: build result, prefixing duplicates with serverName
103
- const result: AggregatedTool[] = [];
104
- for (const [url, entry] of this.servers) {
105
- for (const tool of entry.tools) {
106
- const isDuplicate = (nameCounts.get(tool.name) ?? 0) > 1;
107
- if (isDuplicate) {
108
- const prefix = this.normalizeServerName(entry.name);
109
- result.push({
110
- ...tool,
111
- name: `${prefix}__${tool.name}`,
112
- description: `[${entry.name}] ${tool.description ?? ''}`,
113
- serverUrl: url,
114
- serverName: entry.name,
115
- });
116
- } else {
117
- result.push({ ...tool, serverUrl: url, serverName: entry.name });
118
- }
119
- }
120
- }
121
- return result;
122
- }
123
-
124
- /**
125
- * Call a tool by name. Automatically routes to the correct server.
126
- * Supports both plain names ("search") and prefixed names ("wikipedia__search")
127
- * for disambiguated duplicates.
128
- */
129
- async callTool(name: string, args?: Record<string, unknown>): Promise<McpToolResult> {
130
- // 1. Exact match on original tool name (unprefixed)
131
- for (const [, entry] of this.servers) {
132
- const match = entry.tools.find((t) => t.name === name);
133
- if (match) {
134
- return entry.client.callTool(name, args);
135
- }
136
- }
137
-
138
- // 2. Prefixed name: "serverprefix__realToolName"
139
- const separatorIdx = name.indexOf('__');
140
- if (separatorIdx !== -1) {
141
- const prefix = name.slice(0, separatorIdx);
142
- const realName = name.slice(separatorIdx + 2);
143
- for (const [, entry] of this.servers) {
144
- if (this.normalizeServerName(entry.name) === prefix) {
145
- const match = entry.tools.find((t) => t.name === realName);
146
- if (match) {
147
- return entry.client.callTool(realName, args);
148
- }
149
- }
150
- }
151
- }
152
-
153
- throw new Error(`McpMultiClient: no server exposes tool "${name}"`);
154
- }
155
-
156
96
  /**
157
97
  * Call a tool on a SPECIFIC server (identified by URL).
158
98
  * Use this instead of callTool() when the same tool name may exist on multiple
@@ -176,15 +116,6 @@ export class McpMultiClient {
176
116
  this.servers.clear();
177
117
  }
178
118
 
179
- // -------------------------------------------------------------------------
180
- // Private helpers
181
- // -------------------------------------------------------------------------
182
-
183
- /** Convert a server name to a snake_case prefix for tool name disambiguation. */
184
- private normalizeServerName(name: string): string {
185
- return name.toLowerCase().replace(/[^a-z0-9_-]+/g, '_').replace(/_{2,}/g, '_').replace(/^_|_$/g, '');
186
- }
187
-
188
119
  // -------------------------------------------------------------------------
189
120
  // Getters
190
121
  // -------------------------------------------------------------------------
@@ -1,61 +0,0 @@
1
- import { McpMultiClient } from './multi-client.js';
2
- import type { McpToolResult } from './types.js';
3
- export interface MultiMcpBridgeOptions {
4
- /** Accessor for the canvas store. Typically returns globalThis.__canvasVanilla. */
5
- getCanvas: () => any;
6
- /** Optional logger. */
7
- log?: (msg: string, data?: any) => void;
8
- }
9
- interface RecipeItem {
10
- name: string;
11
- description?: string;
12
- body?: string;
13
- }
14
- /**
15
- * Extract recipes from an MCP tool response. Expects `res.content` as the
16
- * standard MCP content array; looks for a text chunk whose payload parses as
17
- * JSON and contains an array of `{ name, description?, body? }` items (or an
18
- * object with an `items`/`recipes` array).
19
- */
20
- export declare function parseRecipesFromToolResponse(res: unknown): RecipeItem[] | null;
21
- export declare class MultiMcpBridge {
22
- private client;
23
- private unsub;
24
- /** server name -> url (for reverse lookup, since McpMultiClient keys by url) */
25
- private nameToUrl;
26
- /** server names currently connected */
27
- private connected;
28
- /** server names whose handshake is in-flight */
29
- private connecting;
30
- private options;
31
- private started;
32
- constructor(options: MultiMcpBridgeOptions);
33
- start(): void;
34
- stop(): void;
35
- /** Ensure a server is in the store with enabled=true; reconcile picks it up. */
36
- connect(name: string, url: string): Promise<void>;
37
- /** Call a tool on a named server. */
38
- callTool(serverName: string, toolName: string, args: unknown): Promise<McpToolResult>;
39
- /** Direct access to the underlying multi-client (read-only usage preferred). */
40
- get multiClient(): McpMultiClient;
41
- /** True if a server with this name has completed its handshake. */
42
- hasServer(serverName: string): boolean;
43
- /** Snapshot of currently connected server names. */
44
- connectedServers(): string[];
45
- /**
46
- * Wait until every enabled data server in the canvas is connected, or the
47
- * timeout elapses. Resolves either way (no throw) — caller inspects
48
- * `connectedServers()` to decide what's reachable.
49
- */
50
- waitForEnabledServers(timeoutMs?: number): Promise<void>;
51
- private reconcile;
52
- private handshake;
53
- private disconnect;
54
- }
55
- /**
56
- * Install a singleton bridge on globalThis.__multiMcp. If a previous bridge
57
- * exists, it is stopped first (idempotent).
58
- */
59
- export declare function installMultiMcpBridge(options: MultiMcpBridgeOptions): MultiMcpBridge;
60
- export {};
61
- //# sourceMappingURL=multi-mcp-bridge.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"multi-mcp-bridge.d.ts","sourceRoot":"","sources":["../src/multi-mcp-bridge.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAW,aAAa,EAAE,MAAM,YAAY,CAAC;AAMzD,MAAM,WAAW,qBAAqB;IACpC,mFAAmF;IACnF,SAAS,EAAE,MAAM,GAAG,CAAC;IACrB,uBAAuB;IACvB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;CACzC;AASD,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,EAAE,GAAG,IAAI,CAmB9E;AA4BD,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,KAAK,CAA6B;IAC1C,gFAAgF;IAChF,OAAO,CAAC,SAAS,CAA6B;IAC9C,uCAAuC;IACvC,OAAO,CAAC,SAAS,CAAqB;IACtC,gDAAgD;IAChD,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,qBAAqB;IAS1C,KAAK,IAAI,IAAI;IAgBb,IAAI,IAAI,IAAI;IAiBZ,gFAAgF;IAC1E,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQvD,qCAAqC;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC;IAM3F,gFAAgF;IAChF,IAAI,WAAW,IAAI,cAAc,CAEhC;IAED,mEAAmE;IACnE,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAItC,oDAAoD;IACpD,gBAAgB,IAAI,MAAM,EAAE;IAI5B;;;;OAIG;IACG,qBAAqB,CAAC,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC;YAmB9C,SAAS;YAkCT,SAAS;YA6CT,UAAU;CAWzB;AAMD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CAUpF"}
@@ -1,294 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // @webmcp-auto-ui/core — MultiMcpBridge
3
- // Observes a canvas store with a `dataServers` field and reconciles the real
4
- // MCP connection state with the user intent (`enabled`). Populates tools and
5
- // recipes metadata back into the store. Framework-agnostic.
6
- // ---------------------------------------------------------------------------
7
- import { McpMultiClient } from './multi-client.js';
8
- // ---------------------------------------------------------------------------
9
- // Helpers
10
- // ---------------------------------------------------------------------------
11
- /**
12
- * Extract recipes from an MCP tool response. Expects `res.content` as the
13
- * standard MCP content array; looks for a text chunk whose payload parses as
14
- * JSON and contains an array of `{ name, description?, body? }` items (or an
15
- * object with an `items`/`recipes` array).
16
- */
17
- export function parseRecipesFromToolResponse(res) {
18
- if (!res || typeof res !== 'object')
19
- return null;
20
- const content = res.content;
21
- if (!Array.isArray(content))
22
- return null;
23
- for (const chunk of content) {
24
- if (!chunk || typeof chunk !== 'object')
25
- continue;
26
- if (chunk.type !== 'text' || typeof chunk.text !== 'string')
27
- continue;
28
- const text = chunk.text;
29
- let parsed;
30
- try {
31
- parsed = JSON.parse(text);
32
- }
33
- catch {
34
- continue;
35
- }
36
- const items = extractItems(parsed);
37
- if (items)
38
- return items;
39
- }
40
- return null;
41
- }
42
- function extractItems(parsed) {
43
- const candidate = Array.isArray(parsed)
44
- ? parsed
45
- : Array.isArray(parsed?.items)
46
- ? parsed.items
47
- : Array.isArray(parsed?.recipes)
48
- ? parsed.recipes
49
- : null;
50
- if (!candidate)
51
- return null;
52
- const out = [];
53
- for (const it of candidate) {
54
- if (!it || typeof it !== 'object')
55
- continue;
56
- const name = typeof it.name === 'string' ? it.name : typeof it.id === 'string' ? it.id : null;
57
- if (!name)
58
- continue;
59
- const entry = { name };
60
- if (typeof it.description === 'string')
61
- entry.description = it.description;
62
- if (typeof it.body === 'string')
63
- entry.body = it.body;
64
- out.push(entry);
65
- }
66
- return out.length > 0 ? out : null;
67
- }
68
- // ---------------------------------------------------------------------------
69
- // MultiMcpBridge
70
- // ---------------------------------------------------------------------------
71
- export class MultiMcpBridge {
72
- client;
73
- unsub = null;
74
- /** server name -> url (for reverse lookup, since McpMultiClient keys by url) */
75
- nameToUrl = new Map();
76
- /** server names currently connected */
77
- connected = new Set();
78
- /** server names whose handshake is in-flight */
79
- connecting = new Set();
80
- options;
81
- started = false;
82
- constructor(options) {
83
- this.options = options;
84
- this.client = new McpMultiClient();
85
- }
86
- // -------------------------------------------------------------------------
87
- // Lifecycle
88
- // -------------------------------------------------------------------------
89
- start() {
90
- if (this.started)
91
- return;
92
- const canvas = this.options.getCanvas();
93
- if (canvas && typeof canvas.subscribe === 'function') {
94
- this.started = true;
95
- this.unsub = canvas.subscribe(() => {
96
- void this.reconcile();
97
- });
98
- void this.reconcile();
99
- return;
100
- }
101
- // Canvas not ready yet — retry shortly. Without this the bridge would
102
- // stay dead forever because no subscription was ever established.
103
- setTimeout(() => this.start(), 50);
104
- }
105
- stop() {
106
- if (!this.started)
107
- return;
108
- this.started = false;
109
- if (this.unsub) {
110
- try {
111
- this.unsub();
112
- }
113
- catch { /* ignore */ }
114
- this.unsub = null;
115
- }
116
- void this.client.disconnectAll().catch(() => { });
117
- this.connected.clear();
118
- this.connecting.clear();
119
- this.nameToUrl.clear();
120
- }
121
- // -------------------------------------------------------------------------
122
- // Imperative helpers
123
- // -------------------------------------------------------------------------
124
- /** Ensure a server is in the store with enabled=true; reconcile picks it up. */
125
- async connect(name, url) {
126
- const canvas = this.options.getCanvas();
127
- if (!canvas)
128
- return;
129
- canvas.addDataServer?.({ name, url });
130
- canvas.setDataServerEnabled?.(name, true);
131
- await this.reconcile();
132
- }
133
- /** Call a tool on a named server. */
134
- async callTool(serverName, toolName, args) {
135
- const url = this.nameToUrl.get(serverName);
136
- if (!url)
137
- throw new Error(`MultiMcpBridge: server "${serverName}" is not connected`);
138
- return this.client.callToolOn(url, toolName, (args ?? {}));
139
- }
140
- /** Direct access to the underlying multi-client (read-only usage preferred). */
141
- get multiClient() {
142
- return this.client;
143
- }
144
- /** True if a server with this name has completed its handshake. */
145
- hasServer(serverName) {
146
- return this.connected.has(serverName);
147
- }
148
- /** Snapshot of currently connected server names. */
149
- connectedServers() {
150
- return Array.from(this.connected);
151
- }
152
- /**
153
- * Wait until every enabled data server in the canvas is connected, or the
154
- * timeout elapses. Resolves either way (no throw) — caller inspects
155
- * `connectedServers()` to decide what's reachable.
156
- */
157
- async waitForEnabledServers(timeoutMs = 5000) {
158
- const canvas = this.options.getCanvas();
159
- if (!canvas)
160
- return;
161
- const deadline = Date.now() + Math.max(0, timeoutMs);
162
- while (Date.now() < deadline) {
163
- const enabled = (Array.isArray(canvas.dataServers) ? canvas.dataServers : [])
164
- .filter((s) => s?.enabled !== false)
165
- .map((s) => s.name);
166
- if (enabled.length === 0)
167
- return;
168
- const allReady = enabled.every((n) => this.connected.has(n));
169
- if (allReady)
170
- return;
171
- await new Promise((r) => setTimeout(r, 100));
172
- }
173
- }
174
- // -------------------------------------------------------------------------
175
- // Reconciliation
176
- // -------------------------------------------------------------------------
177
- async reconcile() {
178
- const canvas = this.options.getCanvas();
179
- if (!canvas)
180
- return;
181
- const servers = (canvas.dataServers ?? []);
182
- if (!Array.isArray(servers))
183
- return;
184
- const seenNames = new Set();
185
- for (const srv of servers) {
186
- if (!srv || typeof srv.name !== 'string' || typeof srv.url !== 'string')
187
- continue;
188
- // Empty URL means a legacy placeholder entry (see canvas.ensurePrimary).
189
- // Handshaking with '' resolves `fetch('')` against the current page origin,
190
- // producing a POST storm on the app root (405 loop).
191
- if (srv.url === '')
192
- continue;
193
- seenNames.add(srv.name);
194
- const key = srv.name;
195
- if (srv.enabled && !this.connected.has(key) && !this.connecting.has(key)) {
196
- // Mark as connecting synchronously before the async handshake runs,
197
- // so a concurrent reconcile() can't slip past the guard and spawn
198
- // a second handshake for the same server.
199
- this.connecting.add(key);
200
- void this.handshake(srv);
201
- }
202
- else if (!srv.enabled && this.connected.has(key)) {
203
- void this.disconnect(srv);
204
- }
205
- }
206
- // Disconnect servers that were removed from the store entirely
207
- for (const name of Array.from(this.connected)) {
208
- if (!seenNames.has(name)) {
209
- void this.disconnect({ name, url: this.nameToUrl.get(name) ?? '' });
210
- }
211
- }
212
- }
213
- async handshake(srv) {
214
- const canvas = this.options.getCanvas();
215
- // connecting.add is performed by reconcile() synchronously; don't re-add here.
216
- this.options.log?.(`[bridge] handshake start: ${srv.name}`, { url: srv.url });
217
- try {
218
- const { name: actualName, tools } = await this.client.addServer(srv.url);
219
- // The MCP server may return a different name than the one stored in the
220
- // canvas. Key the bridge by the canvas name so callers stay consistent.
221
- this.nameToUrl.set(srv.name, srv.url);
222
- this.connected.add(srv.name);
223
- // Try to fetch recipes via tool `list_recipes` if exposed.
224
- let recipes = [];
225
- const hasListRecipes = tools.some((t) => t.name === 'list_recipes');
226
- if (hasListRecipes) {
227
- try {
228
- const res = await this.client.callToolOn(srv.url, 'list_recipes', {});
229
- recipes = parseRecipesFromToolResponse(res) ?? [];
230
- }
231
- catch (err) {
232
- this.options.log?.(`[bridge] list_recipes failed for ${srv.name}`, err);
233
- }
234
- }
235
- canvas.setDataServerMeta?.(srv.name, {
236
- connected: true,
237
- tools,
238
- recipes,
239
- error: undefined,
240
- serverName: actualName,
241
- });
242
- this.options.log?.(`[bridge] connected: ${srv.name}`, { tools: tools.length, recipes: recipes.length });
243
- }
244
- catch (err) {
245
- const message = err?.message ? String(err.message) : String(err);
246
- canvas.setDataServerMeta?.(srv.name, {
247
- connected: false,
248
- tools: [],
249
- recipes: [],
250
- error: message,
251
- });
252
- this.options.log?.(`[bridge] handshake failed: ${srv.name}`, message);
253
- }
254
- finally {
255
- this.connecting.delete(srv.name);
256
- }
257
- }
258
- async disconnect(srv) {
259
- const canvas = this.options.getCanvas();
260
- const url = srv.url || this.nameToUrl.get(srv.name);
261
- if (url) {
262
- try {
263
- await this.client.removeServer(url);
264
- }
265
- catch { /* ignore */ }
266
- }
267
- this.connected.delete(srv.name);
268
- this.nameToUrl.delete(srv.name);
269
- canvas?.setDataServerMeta?.(srv.name, { connected: false });
270
- this.options.log?.(`[bridge] disconnected: ${srv.name}`);
271
- }
272
- }
273
- // ---------------------------------------------------------------------------
274
- // Singleton installer
275
- // ---------------------------------------------------------------------------
276
- /**
277
- * Install a singleton bridge on globalThis.__multiMcp. If a previous bridge
278
- * exists, it is stopped first (idempotent).
279
- */
280
- export function installMultiMcpBridge(options) {
281
- const g = globalThis;
282
- const existing = g.__multiMcp;
283
- if (existing && typeof existing.stop === 'function') {
284
- try {
285
- existing.stop();
286
- }
287
- catch { /* ignore */ }
288
- }
289
- const bridge = new MultiMcpBridge(options);
290
- g.__multiMcp = bridge;
291
- bridge.start();
292
- return bridge;
293
- }
294
- //# sourceMappingURL=multi-mcp-bridge.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"multi-mcp-bridge.js","sourceRoot":"","sources":["../src/multi-mcp-bridge.ts"],"names":[],"mappings":"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"}
@@ -1,60 +0,0 @@
1
- import { McpMultiClient } from './multi-client.js';
2
- import type { McpToolResult } from './types.js';
3
- export interface MultiMcpBridgeOptions {
4
- /** Accessor for the canvas store. Typically returns globalThis.__canvasVanilla. */
5
- getCanvas: () => any;
6
- /** Optional logger. */
7
- log?: (msg: string, data?: any) => void;
8
- }
9
- interface RecipeItem {
10
- name: string;
11
- description?: string;
12
- body?: string;
13
- }
14
- /**
15
- * Extract recipes from an MCP tool response. Expects `res.content` as the
16
- * standard MCP content array; looks for a text chunk whose payload parses as
17
- * JSON and contains an array of `{ name, description?, body? }` items (or an
18
- * object with an `items`/`recipes` array).
19
- */
20
- export declare function parseRecipesFromToolResponse(res: unknown): RecipeItem[] | null;
21
- export declare class MultiMcpBridge {
22
- private client;
23
- private unsub;
24
- /** server name -> url (for reverse lookup, since McpMultiClient keys by url) */
25
- private nameToUrl;
26
- /** server names currently connected */
27
- private connected;
28
- /** server names whose handshake is in-flight */
29
- private connecting;
30
- private options;
31
- private started;
32
- constructor(options: MultiMcpBridgeOptions);
33
- start(): void;
34
- stop(): void;
35
- /** Ensure a server is in the store with enabled=true; reconcile picks it up. */
36
- connect(name: string, url: string): Promise<void>;
37
- /** Call a tool on a named server. */
38
- callTool(serverName: string, toolName: string, args: unknown): Promise<McpToolResult>;
39
- /** Direct access to the underlying multi-client (read-only usage preferred). */
40
- get multiClient(): McpMultiClient;
41
- /** True if a server with this name has completed its handshake. */
42
- hasServer(serverName: string): boolean;
43
- /** Snapshot of currently connected server names. */
44
- connectedServers(): string[];
45
- /**
46
- * Wait until every enabled data server in the canvas is connected, or the
47
- * timeout elapses. Resolves either way (no throw) — caller inspects
48
- * `connectedServers()` to decide what's reachable.
49
- */
50
- waitForEnabledServers(timeoutMs?: number): Promise<void>;
51
- private reconcile;
52
- private handshake;
53
- private disconnect;
54
- }
55
- /**
56
- * Install a singleton bridge on globalThis.__multiMcp. If a previous bridge
57
- * exists, it is stopped first (idempotent).
58
- */
59
- export declare function installMultiMcpBridge(options: MultiMcpBridgeOptions): MultiMcpBridge;
60
- export {};
@@ -1,311 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // @webmcp-auto-ui/core — MultiMcpBridge
3
- // Observes a canvas store with a `dataServers` field and reconciles the real
4
- // MCP connection state with the user intent (`enabled`). Populates tools and
5
- // recipes metadata back into the store. Framework-agnostic.
6
- // ---------------------------------------------------------------------------
7
-
8
- import { McpMultiClient } from './multi-client.js';
9
- import type { McpTool, McpToolResult } from './types.js';
10
-
11
- // ---------------------------------------------------------------------------
12
- // Types
13
- // ---------------------------------------------------------------------------
14
-
15
- export interface MultiMcpBridgeOptions {
16
- /** Accessor for the canvas store. Typically returns globalThis.__canvasVanilla. */
17
- getCanvas: () => any;
18
- /** Optional logger. */
19
- log?: (msg: string, data?: any) => void;
20
- }
21
-
22
- interface DataServerLike {
23
- name: string;
24
- url: string;
25
- enabled?: boolean;
26
- connected?: boolean;
27
- }
28
-
29
- interface RecipeItem {
30
- name: string;
31
- description?: string;
32
- body?: string;
33
- }
34
-
35
- // ---------------------------------------------------------------------------
36
- // Helpers
37
- // ---------------------------------------------------------------------------
38
-
39
- /**
40
- * Extract recipes from an MCP tool response. Expects `res.content` as the
41
- * standard MCP content array; looks for a text chunk whose payload parses as
42
- * JSON and contains an array of `{ name, description?, body? }` items (or an
43
- * object with an `items`/`recipes` array).
44
- */
45
- export function parseRecipesFromToolResponse(res: unknown): RecipeItem[] | null {
46
- if (!res || typeof res !== 'object') return null;
47
- const content = (res as any).content;
48
- if (!Array.isArray(content)) return null;
49
-
50
- for (const chunk of content) {
51
- if (!chunk || typeof chunk !== 'object') continue;
52
- if (chunk.type !== 'text' || typeof chunk.text !== 'string') continue;
53
- const text: string = chunk.text;
54
- let parsed: any;
55
- try {
56
- parsed = JSON.parse(text);
57
- } catch {
58
- continue;
59
- }
60
- const items = extractItems(parsed);
61
- if (items) return items;
62
- }
63
- return null;
64
- }
65
-
66
- function extractItems(parsed: any): RecipeItem[] | null {
67
- const candidate = Array.isArray(parsed)
68
- ? parsed
69
- : Array.isArray(parsed?.items)
70
- ? parsed.items
71
- : Array.isArray(parsed?.recipes)
72
- ? parsed.recipes
73
- : null;
74
- if (!candidate) return null;
75
- const out: RecipeItem[] = [];
76
- for (const it of candidate) {
77
- if (!it || typeof it !== 'object') continue;
78
- const name = typeof it.name === 'string' ? it.name : typeof it.id === 'string' ? it.id : null;
79
- if (!name) continue;
80
- const entry: RecipeItem = { name };
81
- if (typeof it.description === 'string') entry.description = it.description;
82
- if (typeof it.body === 'string') entry.body = it.body;
83
- out.push(entry);
84
- }
85
- return out.length > 0 ? out : null;
86
- }
87
-
88
- // ---------------------------------------------------------------------------
89
- // MultiMcpBridge
90
- // ---------------------------------------------------------------------------
91
-
92
- export class MultiMcpBridge {
93
- private client: McpMultiClient;
94
- private unsub: (() => void) | null = null;
95
- /** server name -> url (for reverse lookup, since McpMultiClient keys by url) */
96
- private nameToUrl = new Map<string, string>();
97
- /** server names currently connected */
98
- private connected = new Set<string>();
99
- /** server names whose handshake is in-flight */
100
- private connecting = new Set<string>();
101
- private options: MultiMcpBridgeOptions;
102
- private started = false;
103
-
104
- constructor(options: MultiMcpBridgeOptions) {
105
- this.options = options;
106
- this.client = new McpMultiClient();
107
- }
108
-
109
- // -------------------------------------------------------------------------
110
- // Lifecycle
111
- // -------------------------------------------------------------------------
112
-
113
- start(): void {
114
- if (this.started) return;
115
- const canvas = this.options.getCanvas();
116
- if (canvas && typeof canvas.subscribe === 'function') {
117
- this.started = true;
118
- this.unsub = canvas.subscribe(() => {
119
- void this.reconcile();
120
- });
121
- void this.reconcile();
122
- return;
123
- }
124
- // Canvas not ready yet — retry shortly. Without this the bridge would
125
- // stay dead forever because no subscription was ever established.
126
- setTimeout(() => this.start(), 50);
127
- }
128
-
129
- stop(): void {
130
- if (!this.started) return;
131
- this.started = false;
132
- if (this.unsub) {
133
- try { this.unsub(); } catch { /* ignore */ }
134
- this.unsub = null;
135
- }
136
- void this.client.disconnectAll().catch(() => { /* ignore */ });
137
- this.connected.clear();
138
- this.connecting.clear();
139
- this.nameToUrl.clear();
140
- }
141
-
142
- // -------------------------------------------------------------------------
143
- // Imperative helpers
144
- // -------------------------------------------------------------------------
145
-
146
- /** Ensure a server is in the store with enabled=true; reconcile picks it up. */
147
- async connect(name: string, url: string): Promise<void> {
148
- const canvas = this.options.getCanvas();
149
- if (!canvas) return;
150
- canvas.addDataServer?.({ name, url });
151
- canvas.setDataServerEnabled?.(name, true);
152
- await this.reconcile();
153
- }
154
-
155
- /** Call a tool on a named server. */
156
- async callTool(serverName: string, toolName: string, args: unknown): Promise<McpToolResult> {
157
- const url = this.nameToUrl.get(serverName);
158
- if (!url) throw new Error(`MultiMcpBridge: server "${serverName}" is not connected`);
159
- return this.client.callToolOn(url, toolName, (args ?? {}) as Record<string, unknown>);
160
- }
161
-
162
- /** Direct access to the underlying multi-client (read-only usage preferred). */
163
- get multiClient(): McpMultiClient {
164
- return this.client;
165
- }
166
-
167
- /** True if a server with this name has completed its handshake. */
168
- hasServer(serverName: string): boolean {
169
- return this.connected.has(serverName);
170
- }
171
-
172
- /** Snapshot of currently connected server names. */
173
- connectedServers(): string[] {
174
- return Array.from(this.connected);
175
- }
176
-
177
- /**
178
- * Wait until every enabled data server in the canvas is connected, or the
179
- * timeout elapses. Resolves either way (no throw) — caller inspects
180
- * `connectedServers()` to decide what's reachable.
181
- */
182
- async waitForEnabledServers(timeoutMs = 5000): Promise<void> {
183
- const canvas = this.options.getCanvas();
184
- if (!canvas) return;
185
- const deadline = Date.now() + Math.max(0, timeoutMs);
186
- while (Date.now() < deadline) {
187
- const enabled = (Array.isArray(canvas.dataServers) ? canvas.dataServers : [])
188
- .filter((s: { enabled?: boolean }) => s?.enabled !== false)
189
- .map((s: { name: string }) => s.name);
190
- if (enabled.length === 0) return;
191
- const allReady = enabled.every((n: string) => this.connected.has(n));
192
- if (allReady) return;
193
- await new Promise((r) => setTimeout(r, 100));
194
- }
195
- }
196
-
197
- // -------------------------------------------------------------------------
198
- // Reconciliation
199
- // -------------------------------------------------------------------------
200
-
201
- private async reconcile(): Promise<void> {
202
- const canvas = this.options.getCanvas();
203
- if (!canvas) return;
204
- const servers = (canvas.dataServers ?? []) as DataServerLike[];
205
- if (!Array.isArray(servers)) return;
206
-
207
- const seenNames = new Set<string>();
208
- for (const srv of servers) {
209
- if (!srv || typeof srv.name !== 'string' || typeof srv.url !== 'string') continue;
210
- // Empty URL means a legacy placeholder entry (see canvas.ensurePrimary).
211
- // Handshaking with '' resolves `fetch('')` against the current page origin,
212
- // producing a POST storm on the app root (405 loop).
213
- if (srv.url === '') continue;
214
- seenNames.add(srv.name);
215
- const key = srv.name;
216
- if (srv.enabled && !this.connected.has(key) && !this.connecting.has(key)) {
217
- // Mark as connecting synchronously before the async handshake runs,
218
- // so a concurrent reconcile() can't slip past the guard and spawn
219
- // a second handshake for the same server.
220
- this.connecting.add(key);
221
- void this.handshake(srv);
222
- } else if (!srv.enabled && this.connected.has(key)) {
223
- void this.disconnect(srv);
224
- }
225
- }
226
-
227
- // Disconnect servers that were removed from the store entirely
228
- for (const name of Array.from(this.connected)) {
229
- if (!seenNames.has(name)) {
230
- void this.disconnect({ name, url: this.nameToUrl.get(name) ?? '' });
231
- }
232
- }
233
- }
234
-
235
- private async handshake(srv: DataServerLike): Promise<void> {
236
- const canvas = this.options.getCanvas();
237
- // connecting.add is performed by reconcile() synchronously; don't re-add here.
238
- this.options.log?.(`[bridge] handshake start: ${srv.name}`, { url: srv.url });
239
- try {
240
- const { name: actualName, tools } = await this.client.addServer(srv.url);
241
- // The MCP server may return a different name than the one stored in the
242
- // canvas. Key the bridge by the canvas name so callers stay consistent.
243
- this.nameToUrl.set(srv.name, srv.url);
244
- this.connected.add(srv.name);
245
-
246
- // Try to fetch recipes via tool `list_recipes` if exposed.
247
- let recipes: RecipeItem[] = [];
248
- const hasListRecipes = tools.some((t: McpTool) => t.name === 'list_recipes');
249
- if (hasListRecipes) {
250
- try {
251
- const res = await this.client.callToolOn(srv.url, 'list_recipes', {});
252
- recipes = parseRecipesFromToolResponse(res) ?? [];
253
- } catch (err) {
254
- this.options.log?.(`[bridge] list_recipes failed for ${srv.name}`, err);
255
- }
256
- }
257
-
258
- canvas.setDataServerMeta?.(srv.name, {
259
- connected: true,
260
- tools,
261
- recipes,
262
- error: undefined,
263
- serverName: actualName,
264
- });
265
- this.options.log?.(`[bridge] connected: ${srv.name}`, { tools: tools.length, recipes: recipes.length });
266
- } catch (err: any) {
267
- const message = err?.message ? String(err.message) : String(err);
268
- canvas.setDataServerMeta?.(srv.name, {
269
- connected: false,
270
- tools: [],
271
- recipes: [],
272
- error: message,
273
- });
274
- this.options.log?.(`[bridge] handshake failed: ${srv.name}`, message);
275
- } finally {
276
- this.connecting.delete(srv.name);
277
- }
278
- }
279
-
280
- private async disconnect(srv: { name: string; url: string }): Promise<void> {
281
- const canvas = this.options.getCanvas();
282
- const url = srv.url || this.nameToUrl.get(srv.name);
283
- if (url) {
284
- try { await this.client.removeServer(url); } catch { /* ignore */ }
285
- }
286
- this.connected.delete(srv.name);
287
- this.nameToUrl.delete(srv.name);
288
- canvas?.setDataServerMeta?.(srv.name, { connected: false });
289
- this.options.log?.(`[bridge] disconnected: ${srv.name}`);
290
- }
291
- }
292
-
293
- // ---------------------------------------------------------------------------
294
- // Singleton installer
295
- // ---------------------------------------------------------------------------
296
-
297
- /**
298
- * Install a singleton bridge on globalThis.__multiMcp. If a previous bridge
299
- * exists, it is stopped first (idempotent).
300
- */
301
- export function installMultiMcpBridge(options: MultiMcpBridgeOptions): MultiMcpBridge {
302
- const g = globalThis as any;
303
- const existing = g.__multiMcp;
304
- if (existing && typeof existing.stop === 'function') {
305
- try { existing.stop(); } catch { /* ignore */ }
306
- }
307
- const bridge = new MultiMcpBridge(options);
308
- g.__multiMcp = bridge;
309
- bridge.start();
310
- return bridge;
311
- }