@decocms/mesh-sdk 1.2.2 → 1.2.3

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.
@@ -1,434 +1,6 @@
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
- }
1
+ export {
2
+ createBridgeTransportPair,
3
+ BridgeClientTransport,
4
+ BridgeServerTransport,
5
+ type BridgeTransportPair,
6
+ } from "@decocms/mcp-utils";
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { StudioPackAgentId, isStudioPackAgent } from "./constants";
3
+
4
+ describe("StudioPackAgentId", () => {
5
+ test("generates the Store Manager id with the org suffix", () => {
6
+ expect(StudioPackAgentId.STORE_MANAGER("org_xyz")).toBe(
7
+ "studio-store-manager_org_xyz",
8
+ );
9
+ });
10
+ });
11
+
12
+ describe("isStudioPackAgent", () => {
13
+ test("recognises every studio-pack manager id, including the new store-manager", () => {
14
+ expect(isStudioPackAgent("studio-agent-manager_org_xyz")).toBe(true);
15
+ expect(isStudioPackAgent("studio-automation-manager_org_xyz")).toBe(true);
16
+ expect(isStudioPackAgent("studio-connection-manager_org_xyz")).toBe(true);
17
+ expect(isStudioPackAgent("studio-store-manager_org_xyz")).toBe(true);
18
+ });
19
+
20
+ test("rejects unrelated ids", () => {
21
+ expect(isStudioPackAgent(null)).toBe(false);
22
+ expect(isStudioPackAgent(undefined)).toBe(false);
23
+ expect(isStudioPackAgent("vir_abc")).toBe(false);
24
+ expect(isStudioPackAgent("decopilot_org_xyz")).toBe(false);
25
+ });
26
+ });
@@ -26,6 +26,8 @@ export const WellKnownOrgMCPId = {
26
26
  COMMUNITY_REGISTRY: (org: string) => `${org}_community-registry`,
27
27
  /** Dev Assets MCP - local file storage for development */
28
28
  DEV_ASSETS: (org: string) => `${org}_dev-assets`,
29
+ /** Site Diagnostics agent (note: prefix-first format, not org-first) */
30
+ SITE_DIAGNOSTICS: (org: string) => `site-diagnostics_${org}`,
29
31
  };
30
32
 
31
33
  /**
@@ -84,7 +86,7 @@ export function getWellKnownCommunityRegistryConnection(): ConnectionCreateData
84
86
  title: "MCP Registry",
85
87
  description: "Community MCP registry with thousands of handy MCPs",
86
88
  connection_type: "HTTP",
87
- connection_url: "https://sites-registry.decocache.com/mcp",
89
+ connection_url: "https://sites-registry.deco.site/mcp",
88
90
  icon: "https://assets.decocache.com/decocms/cd7ca472-0f72-463a-b0de-6e44bdd0f9b4/mcp.png",
89
91
  app_name: "mcp-registry",
90
92
  app_id: null,
@@ -169,38 +171,6 @@ export function getWellKnownDevAssetsConnection(
169
171
  };
170
172
  }
171
173
 
172
- /**
173
- * Get well-known connection definition for OpenRouter.
174
- * Used by the chat UI to offer a one-click install when no model provider is connected.
175
- */
176
- export function getWellKnownOpenRouterConnection(
177
- opts: { id?: string } = {},
178
- ): ConnectionCreateData {
179
- return {
180
- id: opts.id,
181
- title: "OpenRouter",
182
- description: "Access hundreds of LLM models from a single API",
183
- icon: "https://assets.decocache.com/decocms/b2e2f64f-6025-45f7-9e8c-3b3ebdd073d8/openrouter_logojpg.jpg",
184
- app_name: "openrouter",
185
- app_id: "openrouter",
186
- connection_type: "HTTP",
187
- connection_url: "https://sites-openrouter.decocache.com/mcp",
188
- connection_token: null,
189
- connection_headers: null,
190
- oauth_config: null,
191
- configuration_state: null,
192
- configuration_scopes: null,
193
- metadata: {
194
- source: "chat",
195
- verified: false,
196
- scopeName: "deco",
197
- toolsCount: 0,
198
- publishedAt: null,
199
- repository: null,
200
- },
201
- };
202
- }
203
-
204
174
  /**
205
175
  * Get well-known connection definition for MCP Studio.
206
176
  * Used by agents and workflows pages to offer installation when no provider is connected.
@@ -227,57 +197,141 @@ export function getWellKnownMcpStudioConnection(): ConnectionCreateData {
227
197
  }
228
198
 
229
199
  /**
230
- * Get well-known Decopilot Virtual MCP entity.
231
- * This is the default agent that aggregates ALL org connections.
200
+ * Build a paired `{ is, get }` helper for an org-scoped well-known agent id
201
+ * of shape `${prefix}${orgId}`. Centralizes the trivial check/slice/format
202
+ * logic that every well-known agent used to hand-roll.
232
203
  *
233
- * @param organizationId - Organization ID
234
- * @returns VirtualMCPEntity representing the Decopilot agent
204
+ * `is(id)` returns the orgId when `id` matches the prefix; `null` otherwise.
205
+ * `get(orgId)` mints the well-known id.
235
206
  */
236
- export function getWellKnownDecopilotVirtualMCP(
237
- organizationId: string,
238
- ): VirtualMCPEntity {
207
+ function createWellKnownAgentPrefix(prefix: string): {
208
+ is: (id: string | null | undefined) => string | null;
209
+ get: (organizationId: string) => string;
210
+ } {
239
211
  return {
240
- id: getDecopilotId(organizationId),
241
- organization_id: organizationId,
242
- title: "Decopilot",
243
- description: "Default agent that aggregates all organization connections",
244
- icon: "https://assets.decocache.com/decocms/fd07a578-6b1c-40f1-bc05-88a3b981695d/f7fc4ffa81aec04e37ae670c3cd4936643a7b269.png",
212
+ is(id) {
213
+ if (!id) return null;
214
+ if (!id.startsWith(prefix)) return null;
215
+ return id.slice(prefix.length) || null;
216
+ },
217
+ get(organizationId) {
218
+ return `${prefix}${organizationId}`;
219
+ },
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Build a well-known agent VirtualMCPEntity with sensible defaults
225
+ * (status active, system creator, empty connections, etc.). Only the
226
+ * id/title/description/icon and optional instructions vary across agents.
227
+ */
228
+ function defineWellKnownAgentVMCP(opts: {
229
+ id: string;
230
+ organizationId: string;
231
+ title: string;
232
+ description: string;
233
+ icon: string;
234
+ instructions?: string | null;
235
+ }): VirtualMCPEntity {
236
+ return {
237
+ id: opts.id,
238
+ organization_id: opts.organizationId,
239
+ title: opts.title,
240
+ description: opts.description,
241
+ icon: opts.icon,
245
242
  status: "active",
246
243
  created_at: new Date().toISOString(),
247
244
  updated_at: new Date().toISOString(),
248
245
  created_by: "system",
249
246
  updated_by: undefined,
250
- metadata: { instructions: null },
251
- subtype: null,
252
- connections: [], // Empty connections array - gateway.ts will populate with all org connections
247
+ metadata: { instructions: opts.instructions ?? null },
248
+ pinned: false,
249
+ connections: [],
253
250
  };
254
251
  }
255
252
 
256
- /**
257
- * Decopilot ID prefix constant
258
- */
259
- const DECOPILOT_PREFIX = "decopilot_";
253
+ // ---- Decopilot ----
254
+ // Default agent that aggregates ALL org connections. Gateway populates
255
+ // the connections array at lookup time.
256
+ const decopilotPrefix = createWellKnownAgentPrefix("decopilot_");
257
+ export const isDecopilot = decopilotPrefix.is;
258
+ export const getDecopilotId = decopilotPrefix.get;
259
+
260
+ export function getWellKnownDecopilotVirtualMCP(
261
+ organizationId: string,
262
+ ): VirtualMCPEntity {
263
+ return defineWellKnownAgentVMCP({
264
+ id: getDecopilotId(organizationId),
265
+ organizationId,
266
+ title: "Decopilot",
267
+ description: "Default agent that aggregates all organization connections",
268
+ icon: "https://assets.decocache.com/decocms/fd07a578-6b1c-40f1-bc05-88a3b981695d/f7fc4ffa81aec04e37ae670c3cd4936643a7b269.png",
269
+ });
270
+ }
271
+
272
+ // ---- Brand-Context Setup ----
273
+ // Guided-onboarding agent for the brand-context preset task. The
274
+ // `brand_context_setup` built-in is injected by `dispatchRun` when this
275
+ // id is seen; the system prompt lives in `metadata.instructions`.
276
+ const brandContextSetupPrefix = createWellKnownAgentPrefix(
277
+ "brand-context-setup_",
278
+ );
279
+ export const isBrandContextSetup = brandContextSetupPrefix.is;
280
+ export const getBrandContextSetupId = brandContextSetupPrefix.get;
281
+
282
+ const BRAND_CONTEXT_SETUP_INSTRUCTIONS = `
283
+ You are running the brand-context onboarding for the user's organization. Your only job in this thread is to set up the organization's brand context:
284
+
285
+ 1. If the user hasn't already given you a website URL, ask for it in one short message. Accept whatever URL they give — don't quibble about format.
286
+ 2. As soon as you have a URL, call the \`brand_context_setup\` tool exactly once with that URL.
287
+ 3. After the tool returns success, briefly confirm to the user what was captured (brand name + domain) in one or two sentences. Do not list every color or font.
288
+ 4. Do NOT call any other tools in this thread. Do NOT call \`brand_context_setup\` more than once.
289
+
290
+ If the tool returns an error, surface the error message to the user and ask whether they want to try a different URL.
291
+ `.trim();
292
+
293
+ export function getWellKnownBrandContextSetupVirtualMCP(
294
+ organizationId: string,
295
+ ): VirtualMCPEntity {
296
+ return defineWellKnownAgentVMCP({
297
+ id: getBrandContextSetupId(organizationId),
298
+ organizationId,
299
+ title: "Brand context setup",
300
+ description:
301
+ "Guided onboarding agent that extracts brand context from a website URL.",
302
+ icon: "https://assets.decocache.com/decocms/fd07a578-6b1c-40f1-bc05-88a3b981695d/f7fc4ffa81aec04e37ae670c3cd4936643a7b269.png",
303
+ instructions: BRAND_CONTEXT_SETUP_INSTRUCTIONS,
304
+ });
305
+ }
306
+
307
+ // ---- Site Diagnostics ----
308
+ const siteDiagnosticsPrefix = createWellKnownAgentPrefix("site-diagnostics_");
309
+ export const isSiteDiagnostics = siteDiagnosticsPrefix.is;
310
+ export const getSiteDiagnosticsId = siteDiagnosticsPrefix.get;
260
311
 
261
312
  /**
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
313
+ * Studio Pack agent ID generators (org-scoped)
266
314
  */
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
- }
315
+ export const StudioPackAgentId = {
316
+ AGENT_MANAGER: (orgId: string) => `studio-agent-manager_${orgId}`,
317
+ AUTOMATION_MANAGER: (orgId: string) => `studio-automation-manager_${orgId}`,
318
+ CONNECTION_MANAGER: (orgId: string) => `studio-connection-manager_${orgId}`,
319
+ STORE_MANAGER: (orgId: string) => `studio-store-manager_${orgId}`,
320
+ BRAND_MANAGER: (orgId: string) => `studio-brand-manager_${orgId}`,
321
+ } as const;
272
322
 
273
323
  /**
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}`
324
+ * Check if a connection or virtual MCP ID is a Studio Pack agent.
278
325
  */
279
- export function getDecopilotId(organizationId: string): string {
280
- return `${DECOPILOT_PREFIX}${organizationId}`;
326
+ export function isStudioPackAgent(id: string | null | undefined): boolean {
327
+ if (!id) return false;
328
+ return (
329
+ id.startsWith("studio-agent-manager_") ||
330
+ id.startsWith("studio-automation-manager_") ||
331
+ id.startsWith("studio-connection-manager_") ||
332
+ id.startsWith("studio-store-manager_") ||
333
+ id.startsWith("studio-brand-manager_")
334
+ );
281
335
  }
282
336
 
283
337
  export function getWellKnownDecopilotConnection(