@decocms/mesh-sdk 1.2.1 → 1.2.2

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.
@@ -0,0 +1,434 @@
1
+ import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
3
+
4
+ /**
5
+ * Bridge MCP Transport
6
+ *
7
+ * High-performance bridge transport for MCP communication within the same process.
8
+ * Uses direct callbacks with microtask scheduling to avoid serialization overhead
9
+ * and minimize event loop impact.
10
+ *
11
+ * ## Design
12
+ *
13
+ * - **Zero serialization**: Messages are passed as JavaScript objects by reference
14
+ * - **Microtask scheduling**: Uses `queueMicrotask` to avoid deep recursion while
15
+ * maintaining message ordering
16
+ * - **Direct callbacks**: No Web API overhead (EventTarget, MessageChannel, etc.)
17
+ *
18
+ * ## Usage
19
+ *
20
+ * ```ts
21
+ * import { createBridgeTransportPair } from "@decocms/mesh-sdk";
22
+ * import { Client } from "@modelcontextprotocol/sdk/client/index.js";
23
+ * import { Server } from "@modelcontextprotocol/sdk/server/index.js";
24
+ *
25
+ * const { client: clientTransport, server: serverTransport } =
26
+ * createBridgeTransportPair();
27
+ *
28
+ * const client = new Client({ name: "test-client", version: "1.0.0" });
29
+ * const server = new Server({ name: "test-server", version: "1.0.0" });
30
+ *
31
+ * await server.connect(serverTransport);
32
+ * await client.connect(clientTransport);
33
+ *
34
+ * // Now client and server can communicate via bridge
35
+ * ```
36
+ */
37
+
38
+ type TransportSide = "client" | "server";
39
+
40
+ /**
41
+ * Maximum number of messages that can be queued before throwing an error.
42
+ * This prevents unbounded memory growth if one side sends faster than the other processes.
43
+ */
44
+ const MAX_QUEUE_SIZE = 10_000;
45
+
46
+ /**
47
+ * Internal channel that manages bidirectional message queues between client and server.
48
+ */
49
+ class BridgeChannel {
50
+ private clientQueue: JSONRPCMessage[] = [];
51
+ private serverQueue: JSONRPCMessage[] = [];
52
+ private clientClosed = false;
53
+ private serverClosed = false;
54
+ private clientFlushScheduled = false;
55
+ private serverFlushScheduled = false;
56
+
57
+ // Use type-only forward reference to avoid circular dependency
58
+ private clientTransport?: { deliverMessage(message: JSONRPCMessage): void };
59
+ private serverTransport?: { deliverMessage(message: JSONRPCMessage): void };
60
+
61
+ /**
62
+ * Register transports with the channel and link them to each other.
63
+ * This sets up both message delivery and close notifications.
64
+ */
65
+ registerTransports(
66
+ client: BridgeClientTransport,
67
+ server: BridgeServerTransport,
68
+ ): void {
69
+ this.clientTransport = client;
70
+ this.serverTransport = server;
71
+ // Link transports to each other for close notifications
72
+ client.setOppositeTransport(server);
73
+ server.setOppositeTransport(client);
74
+ }
75
+
76
+ /**
77
+ * Get the queue for a specific side (opposite of sender)
78
+ */
79
+ private getQueue(side: TransportSide): JSONRPCMessage[] {
80
+ return side === "client" ? this.clientQueue : this.serverQueue;
81
+ }
82
+
83
+ /**
84
+ * Check if the target side is closed
85
+ */
86
+ isClosed(side: TransportSide): boolean {
87
+ return side === "client" ? this.clientClosed : this.serverClosed;
88
+ }
89
+
90
+ /**
91
+ * Mark a side as closed
92
+ */
93
+ close(side: TransportSide): void {
94
+ if (side === "client") {
95
+ this.clientClosed = true;
96
+ this.clientQueue = [];
97
+ } else {
98
+ this.serverClosed = true;
99
+ this.serverQueue = [];
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Enqueue a message to the target side's queue
105
+ * @throws Error if queue size exceeds MAX_QUEUE_SIZE
106
+ */
107
+ enqueue(side: TransportSide, message: JSONRPCMessage): void {
108
+ if (this.isClosed(side)) {
109
+ // Silent no-op when target is closed (matches stdio transport behavior)
110
+ return;
111
+ }
112
+
113
+ const queue = this.getQueue(side);
114
+
115
+ // Prevent unbounded memory growth
116
+ if (queue.length >= MAX_QUEUE_SIZE) {
117
+ throw new Error(
118
+ `BridgeTransport: ${side} queue overflow (max ${MAX_QUEUE_SIZE} messages). ` +
119
+ "The receiver may not be processing messages fast enough.",
120
+ );
121
+ }
122
+
123
+ queue.push(message);
124
+
125
+ // Schedule flush if not already scheduled
126
+ if (side === "client" && !this.clientFlushScheduled) {
127
+ this.scheduleFlush("client");
128
+ } else if (side === "server" && !this.serverFlushScheduled) {
129
+ this.scheduleFlush("server");
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Schedule a flush operation for the given side using microtask scheduling.
135
+ * This frees the event loop and prevents deep recursion.
136
+ */
137
+ private scheduleFlush(side: TransportSide): void {
138
+ if (side === "client") {
139
+ this.clientFlushScheduled = true;
140
+ } else {
141
+ this.serverFlushScheduled = true;
142
+ }
143
+
144
+ queueMicrotask(() => {
145
+ this.flush(side);
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Flush all messages from the queue for a specific side
151
+ * and deliver them to the appropriate transport
152
+ */
153
+ flush(side: TransportSide): void {
154
+ const queue = this.getQueue(side);
155
+
156
+ // Reset scheduled flag
157
+ if (side === "client") {
158
+ this.clientFlushScheduled = false;
159
+ } else {
160
+ this.serverFlushScheduled = false;
161
+ }
162
+
163
+ // If closed, clear queue and return
164
+ if (this.isClosed(side)) {
165
+ queue.length = 0;
166
+ return;
167
+ }
168
+
169
+ // Get the transport for this side
170
+ const transport =
171
+ side === "client" ? this.clientTransport : this.serverTransport;
172
+
173
+ if (!transport) {
174
+ // Transport not registered yet, messages will be processed when it starts
175
+ return;
176
+ }
177
+
178
+ // Drain queue in FIFO order and deliver to transport
179
+ // Continue draining even if transport isn't ready yet - deliverMessage will check
180
+ while (queue.length > 0) {
181
+ const message = queue.shift()!;
182
+ transport.deliverMessage(message);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Close both sides of the channel
188
+ */
189
+ closeBoth(): void {
190
+ this.close("client");
191
+ this.close("server");
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Base transport implementation shared by client and server transports
197
+ */
198
+ abstract class BaseBridgeTransport implements Transport {
199
+ protected channel: BridgeChannel;
200
+ protected side: TransportSide;
201
+ protected started = false;
202
+ protected closed = false;
203
+ private _onmessage?: (message: JSONRPCMessage) => void;
204
+ private _onerror?: (error: Error) => void;
205
+ private _onclose?: () => void;
206
+
207
+ constructor(channel: BridgeChannel, side: TransportSide) {
208
+ this.channel = channel;
209
+ this.side = side;
210
+ }
211
+
212
+ get onmessage(): ((message: JSONRPCMessage) => void) | undefined {
213
+ return this._onmessage;
214
+ }
215
+
216
+ set onmessage(fn: ((message: JSONRPCMessage) => void) | undefined) {
217
+ this._onmessage = fn;
218
+ // If transport is started and onmessage is set, flush any queued messages
219
+ if (fn && this.started && !this.closed) {
220
+ this.channel.flush(this.side);
221
+ }
222
+ }
223
+
224
+ get onerror(): ((error: Error) => void) | undefined {
225
+ return this._onerror;
226
+ }
227
+
228
+ set onerror(fn: ((error: Error) => void) | undefined) {
229
+ this._onerror = fn;
230
+ }
231
+
232
+ get onclose(): (() => void) | undefined {
233
+ return this._onclose;
234
+ }
235
+
236
+ set onclose(fn: (() => void) | undefined) {
237
+ this._onclose = fn;
238
+ }
239
+
240
+ /**
241
+ * Start the transport. For bridge transports, this is a no-op
242
+ * but required by the Transport interface.
243
+ */
244
+ async start(): Promise<void> {
245
+ if (this.started) {
246
+ throw new Error(
247
+ `${this.side === "client" ? "BridgeClientTransport" : "BridgeServerTransport"} already started! If using Client/Server class, note that connect() calls start() automatically.`,
248
+ );
249
+ }
250
+ this.started = true;
251
+ // Process any messages that were queued before start
252
+ // If onmessage is already set, flush immediately; otherwise it will flush when onmessage is set
253
+ if (this._onmessage && !this.closed) {
254
+ this.channel.flush(this.side);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Send a message to the opposite side
260
+ */
261
+ async send(message: JSONRPCMessage): Promise<void> {
262
+ if (this.closed) {
263
+ // Silent no-op when transport is closed (matches stdio transport behavior)
264
+ return Promise.resolve();
265
+ }
266
+
267
+ const targetSide: TransportSide =
268
+ this.side === "client" ? "server" : "client";
269
+ this.channel.enqueue(targetSide, message);
270
+
271
+ // Resolve immediately - message delivery is async via microtask
272
+ return Promise.resolve();
273
+ }
274
+
275
+ /**
276
+ * Close the transport
277
+ */
278
+ async close(): Promise<void> {
279
+ if (!this.started || this.closed) {
280
+ return;
281
+ }
282
+
283
+ this.closed = true;
284
+ this.channel.close(this.side);
285
+
286
+ // Notify opposite side that we closed
287
+ const oppositeTransport = this.getOppositeTransport();
288
+ if (oppositeTransport && !oppositeTransport.closed) {
289
+ oppositeTransport._onclose?.();
290
+ }
291
+
292
+ this._onclose?.();
293
+ }
294
+
295
+ /**
296
+ * Get reference to the opposite transport (set by factory)
297
+ */
298
+ protected abstract getOppositeTransport(): BaseBridgeTransport | undefined;
299
+
300
+ /**
301
+ * Set reference to opposite transport (called by factory)
302
+ */
303
+ abstract setOppositeTransport(transport: BaseBridgeTransport): void;
304
+
305
+ /**
306
+ * Internal method to deliver a message to this transport
307
+ * Called by the channel during flush operations
308
+ */
309
+ deliverMessage(message: JSONRPCMessage): void {
310
+ if (!this.started || this.channel.isClosed(this.side)) {
311
+ return;
312
+ }
313
+
314
+ try {
315
+ this._onmessage?.(message);
316
+ } catch (error) {
317
+ this._onerror?.(error as Error);
318
+ }
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Client-side bridge transport
324
+ */
325
+ export class BridgeClientTransport extends BaseBridgeTransport {
326
+ private oppositeTransport?: BridgeServerTransport;
327
+
328
+ constructor(channel: BridgeChannel) {
329
+ super(channel, "client");
330
+ }
331
+
332
+ protected getOppositeTransport(): BaseBridgeTransport | undefined {
333
+ return this.oppositeTransport;
334
+ }
335
+
336
+ setOppositeTransport(transport: BaseBridgeTransport): void {
337
+ if (!(transport instanceof BridgeServerTransport)) {
338
+ throw new Error("Opposite transport must be BridgeServerTransport");
339
+ }
340
+ this.oppositeTransport = transport;
341
+ }
342
+
343
+ override async start(): Promise<void> {
344
+ await super.start();
345
+ // Callbacks will be set by MCP SDK after start()
346
+ // We use property setters to sync them with internal handlers
347
+ }
348
+
349
+ override async send(message: JSONRPCMessage): Promise<void> {
350
+ await super.send(message);
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Server-side bridge transport
356
+ */
357
+ export class BridgeServerTransport extends BaseBridgeTransport {
358
+ private oppositeTransport?: BridgeClientTransport;
359
+
360
+ constructor(channel: BridgeChannel) {
361
+ super(channel, "server");
362
+ }
363
+
364
+ protected getOppositeTransport(): BaseBridgeTransport | undefined {
365
+ return this.oppositeTransport;
366
+ }
367
+
368
+ setOppositeTransport(transport: BaseBridgeTransport): void {
369
+ if (!(transport instanceof BridgeClientTransport)) {
370
+ throw new Error("Opposite transport must be BridgeClientTransport");
371
+ }
372
+ this.oppositeTransport = transport;
373
+ }
374
+
375
+ override async start(): Promise<void> {
376
+ await super.start();
377
+ // Callbacks will be set by MCP SDK after start()
378
+ // We use property setters to sync them with internal handlers
379
+ }
380
+
381
+ override async send(message: JSONRPCMessage): Promise<void> {
382
+ await super.send(message);
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Result of creating a bridge transport pair
388
+ */
389
+ export interface BridgeTransportPair {
390
+ /**
391
+ * Client-side transport (for MCP Client)
392
+ */
393
+ client: BridgeClientTransport;
394
+ /**
395
+ * Server-side transport (for MCP Server)
396
+ */
397
+ server: BridgeServerTransport;
398
+ /**
399
+ * Internal channel (for advanced use cases)
400
+ */
401
+ channel: BridgeChannel;
402
+ }
403
+
404
+ /**
405
+ * Create a pair of bridge transports for client-server communication.
406
+ *
407
+ * Uses microtask scheduling for message delivery, which frees the event loop
408
+ * and prevents deep recursion while maintaining message ordering.
409
+ *
410
+ * @returns A pair of transports connected via a bridge channel
411
+ *
412
+ * @example
413
+ * ```ts
414
+ * const { client, server } = createBridgeTransportPair();
415
+ *
416
+ * const mcpClient = new Client({ name: "test", version: "1.0.0" });
417
+ * const mcpServer = new Server({ name: "test", version: "1.0.0" });
418
+ *
419
+ * await mcpServer.connect(server);
420
+ * await mcpClient.connect(client);
421
+ *
422
+ * // Now client and server can communicate via bridge
423
+ * ```
424
+ */
425
+ export function createBridgeTransportPair(): BridgeTransportPair {
426
+ const channel = new BridgeChannel();
427
+ const client = new BridgeClientTransport(channel);
428
+ const server = new BridgeServerTransport(channel);
429
+
430
+ // Register transports with channel (also links them for close notifications)
431
+ channel.registerTransports(client, server);
432
+
433
+ return { client, server, channel };
434
+ }
@@ -5,7 +5,10 @@
5
5
  * This module provides constants and factory functions for creating standard MCP connections.
6
6
  */
7
7
 
8
- import type { ConnectionCreateData } from "../types/connection";
8
+ import type {
9
+ ConnectionCreateData,
10
+ ConnectionEntity,
11
+ } from "../types/connection";
9
12
  import type { VirtualMCPEntity } from "../types/virtual-mcp";
10
13
 
11
14
  /**
@@ -21,6 +24,8 @@ export const WellKnownOrgMCPId = {
21
24
  REGISTRY: (org: string) => `${org}_registry`,
22
25
  /** Community MCP registry */
23
26
  COMMUNITY_REGISTRY: (org: string) => `${org}_community-registry`,
27
+ /** Dev Assets MCP - local file storage for development */
28
+ DEV_ASSETS: (org: string) => `${org}_dev-assets`,
24
29
  };
25
30
 
26
31
  /**
@@ -30,6 +35,13 @@ export const WellKnownOrgMCPId = {
30
35
  */
31
36
  export const SELF_MCP_ALIAS_ID = "self";
32
37
 
38
+ /**
39
+ * Frontend connection ID for the dev-assets MCP endpoint.
40
+ * Use this constant when calling object storage tools from the frontend in dev mode.
41
+ * The endpoint is exposed at /mcp/dev-assets.
42
+ */
43
+ export const DEV_ASSETS_MCP_ALIAS_ID = "dev-assets";
44
+
33
45
  /**
34
46
  * Get well-known connection definition for the Deco Store registry.
35
47
  * This can be used by both frontend and backend to create registry connections.
@@ -44,7 +56,7 @@ export function getWellKnownRegistryConnection(
44
56
  title: "Deco Store",
45
57
  description: "Official deco MCP registry with curated integrations",
46
58
  connection_type: "HTTP",
47
- connection_url: "https://api.decocms.com/mcp/registry",
59
+ connection_url: "https://studio.decocms.com/org/deco/registry/mcp",
48
60
  icon: "https://assets.decocache.com/decocms/00ccf6c3-9e13-4517-83b0-75ab84554bb9/596364c63320075ca58483660156b6d9de9b526e.png",
49
61
  app_name: "deco-registry",
50
62
  app_id: null,
@@ -101,8 +113,8 @@ export function getWellKnownSelfConnection(
101
113
  ): ConnectionCreateData {
102
114
  return {
103
115
  id: WellKnownOrgMCPId.SELF(orgId),
104
- title: "Mesh MCP",
105
- description: "The MCP for the mesh API",
116
+ title: "Deco CMS",
117
+ description: "The MCP for the CMS API",
106
118
  connection_type: "HTTP",
107
119
  // Custom url for targeting this mcp. It's a standalone endpoint that exposes all management tools.
108
120
  connection_url: `${baseUrl}/mcp/${SELF_MCP_ALIAS_ID}`,
@@ -120,6 +132,43 @@ export function getWellKnownSelfConnection(
120
132
  };
121
133
  }
122
134
 
135
+ /**
136
+ * Get well-known connection definition for Dev Assets MCP.
137
+ * This is a dev-only MCP that provides local file storage at /data/assets/<org_id>/.
138
+ * It implements the OBJECT_STORAGE_BINDING interface.
139
+ *
140
+ * @param baseUrl - The base URL for the MCP server (e.g., "http://localhost:3000")
141
+ * @param orgId - The organization ID
142
+ * @returns ConnectionCreateData for the Dev Assets MCP
143
+ */
144
+ export function getWellKnownDevAssetsConnection(
145
+ baseUrl: string,
146
+ orgId: string,
147
+ ): ConnectionCreateData {
148
+ return {
149
+ id: WellKnownOrgMCPId.DEV_ASSETS(orgId),
150
+ title: "Local Files",
151
+ description:
152
+ "Local file storage for development. Files are stored in /data/assets/.",
153
+ connection_type: "HTTP",
154
+ connection_url: `${baseUrl}/mcp/${DEV_ASSETS_MCP_ALIAS_ID}`,
155
+ // Folder icon
156
+ icon: "https://api.iconify.design/lucide:folder.svg?color=%23888",
157
+ app_name: "@deco/dev-assets-mcp",
158
+ app_id: null,
159
+ connection_token: null,
160
+ connection_headers: null,
161
+ oauth_config: null,
162
+ configuration_state: null,
163
+ configuration_scopes: null,
164
+ metadata: {
165
+ isFixed: true,
166
+ devOnly: true,
167
+ type: "dev-assets",
168
+ },
169
+ };
170
+ }
171
+
123
172
  /**
124
173
  * Get well-known connection definition for OpenRouter.
125
174
  * Used by the chat UI to offer a one-click install when no model provider is connected.
@@ -131,7 +180,7 @@ export function getWellKnownOpenRouterConnection(
131
180
  id: opts.id,
132
181
  title: "OpenRouter",
133
182
  description: "Access hundreds of LLM models from a single API",
134
- icon: "https://openrouter.ai/favicon.ico",
183
+ icon: "https://assets.decocache.com/decocms/b2e2f64f-6025-45f7-9e8c-3b3ebdd073d8/openrouter_logojpg.jpg",
135
184
  app_name: "openrouter",
136
185
  app_id: "openrouter",
137
186
  connection_type: "HTTP",
@@ -178,27 +227,81 @@ export function getWellKnownMcpStudioConnection(): ConnectionCreateData {
178
227
  }
179
228
 
180
229
  /**
181
- * Get well-known Decopilot Agent virtual MCP entity.
230
+ * Get well-known Decopilot Virtual MCP entity.
182
231
  * This is the default agent that aggregates ALL org connections.
183
232
  *
184
233
  * @param organizationId - Organization ID
185
234
  * @returns VirtualMCPEntity representing the Decopilot agent
186
235
  */
187
- export function getWellKnownDecopilotAgent(
236
+ export function getWellKnownDecopilotVirtualMCP(
188
237
  organizationId: string,
189
238
  ): VirtualMCPEntity {
190
239
  return {
191
- id: `decopilot-${organizationId}`,
240
+ id: getDecopilotId(organizationId),
192
241
  organization_id: organizationId,
193
242
  title: "Decopilot",
194
243
  description: "Default agent that aggregates all organization connections",
195
244
  icon: "https://assets.decocache.com/decocms/fd07a578-6b1c-40f1-bc05-88a3b981695d/f7fc4ffa81aec04e37ae670c3cd4936643a7b269.png",
196
- tool_selection_mode: "exclusion",
197
245
  status: "active",
198
246
  created_at: new Date().toISOString(),
199
247
  updated_at: new Date().toISOString(),
200
248
  created_by: "system",
201
249
  updated_by: undefined,
202
- connections: [], // Empty = no exclusions, include all connections
250
+ metadata: { instructions: null },
251
+ subtype: null,
252
+ connections: [], // Empty connections array - gateway.ts will populate with all org connections
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Decopilot ID prefix constant
258
+ */
259
+ const DECOPILOT_PREFIX = "decopilot_";
260
+
261
+ /**
262
+ * Check if a connection or virtual MCP ID is the Decopilot agent.
263
+ *
264
+ * @param id - Connection or virtual MCP ID to check
265
+ * @returns The organization ID if the ID matches the Decopilot pattern (decopilot_{orgId}), null otherwise
266
+ */
267
+ export function isDecopilot(id: string | null | undefined): string | null {
268
+ if (!id) return null;
269
+ if (!id.startsWith(DECOPILOT_PREFIX)) return null;
270
+ return id.slice(DECOPILOT_PREFIX.length) || null;
271
+ }
272
+
273
+ /**
274
+ * Get the Decopilot ID for a given organization.
275
+ *
276
+ * @param organizationId - Organization ID
277
+ * @returns The Decopilot ID in the format `decopilot_{organizationId}`
278
+ */
279
+ export function getDecopilotId(organizationId: string): string {
280
+ return `${DECOPILOT_PREFIX}${organizationId}`;
281
+ }
282
+
283
+ export function getWellKnownDecopilotConnection(
284
+ organizationId: string,
285
+ ): ConnectionEntity {
286
+ const virtual = getWellKnownDecopilotVirtualMCP(organizationId);
287
+
288
+ return {
289
+ ...virtual,
290
+ id: virtual.id!,
291
+ connection_type: "VIRTUAL",
292
+ connection_url: `virtual://${virtual.id}`,
293
+ app_name: "decopilot",
294
+ app_id: "decopilot",
295
+ connection_token: null,
296
+ connection_headers: null,
297
+ oauth_config: null,
298
+ configuration_state: null,
299
+ configuration_scopes: null,
300
+ metadata: {
301
+ isDefault: true,
302
+ type: "decopilot",
303
+ },
304
+ tools: [],
305
+ bindings: [],
203
306
  };
204
307
  }