@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.
- package/README.md +10 -10
- package/package.json +3 -2
- package/src/context/index.ts +0 -1
- package/src/context/project-context.tsx +0 -10
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-collections.ts +19 -12
- package/src/hooks/use-connection.ts +12 -1
- package/src/hooks/use-mcp-client.ts +27 -10
- package/src/hooks/use-virtual-mcp.ts +64 -0
- package/src/index.ts +38 -2
- package/src/lib/bridge-transport.ts +6 -434
- package/src/lib/constants.test.ts +26 -0
- package/src/lib/constants.ts +121 -67
- package/src/lib/default-model.ts +188 -3
- package/src/lib/mcp-oauth.ts +59 -8
- package/src/lib/query-keys.ts +19 -4
- package/src/lib/server-client-bridge.ts +4 -146
- package/src/lib/usage.test.ts +66 -0
- package/src/lib/usage.ts +26 -0
- package/src/types/ai-providers.ts +19 -1
- package/src/types/connection.ts +5 -0
- package/src/types/decopilot-events.test.ts +78 -0
- package/src/types/decopilot-events.ts +51 -8
- package/src/types/index.ts +18 -0
- package/src/types/virtual-mcp.test.ts +202 -0
- package/src/types/virtual-mcp.ts +416 -9
|
@@ -1,434 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
});
|
package/src/lib/constants.ts
CHANGED
|
@@ -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.
|
|
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
|
-
*
|
|
231
|
-
*
|
|
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
|
-
*
|
|
234
|
-
*
|
|
204
|
+
* `is(id)` returns the orgId when `id` matches the prefix; `null` otherwise.
|
|
205
|
+
* `get(orgId)` mints the well-known id.
|
|
235
206
|
*/
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
207
|
+
function createWellKnownAgentPrefix(prefix: string): {
|
|
208
|
+
is: (id: string | null | undefined) => string | null;
|
|
209
|
+
get: (organizationId: string) => string;
|
|
210
|
+
} {
|
|
239
211
|
return {
|
|
240
|
-
id
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
252
|
-
connections: [],
|
|
247
|
+
metadata: { instructions: opts.instructions ?? null },
|
|
248
|
+
pinned: false,
|
|
249
|
+
connections: [],
|
|
253
250
|
};
|
|
254
251
|
}
|
|
255
252
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
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
|
-
*
|
|
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
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
*
|
|
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
|
|
280
|
-
return
|
|
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(
|