@decocms/mesh-sdk 1.2.1 → 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 +7 -4
- package/src/context/index.ts +5 -1
- package/src/context/project-context.tsx +68 -29
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-collections.ts +179 -63
- package/src/hooks/use-connection.ts +50 -4
- package/src/hooks/use-mcp-client.ts +81 -11
- package/src/hooks/use-mcp-prompts.ts +16 -6
- package/src/hooks/use-mcp-resources.ts +15 -5
- package/src/hooks/use-virtual-mcp.ts +64 -0
- package/src/index.ts +119 -4
- package/src/lib/bridge-transport.test.ts +368 -0
- package/src/lib/bridge-transport.ts +6 -0
- package/src/lib/constants.test.ts +26 -0
- package/src/lib/constants.ts +193 -36
- package/src/lib/default-model.ts +281 -0
- package/src/lib/mcp-oauth.ts +139 -17
- package/src/lib/query-keys.ts +20 -4
- package/src/lib/server-client-bridge.ts +4 -0
- package/src/lib/usage.test.ts +229 -0
- package/src/lib/usage.ts +187 -0
- package/src/plugins/index.ts +15 -0
- package/src/plugins/plugin-context-provider.tsx +99 -0
- package/src/plugins/topbar-portal.tsx +118 -0
- package/src/types/ai-providers.ts +86 -0
- package/src/types/connection.ts +43 -20
- package/src/types/decopilot-events.test.ts +78 -0
- package/src/types/decopilot-events.ts +171 -0
- package/src/types/index.ts +48 -1
- package/src/types/virtual-mcp.test.ts +202 -0
- package/src/types/virtual-mcp.ts +514 -109
|
@@ -20,20 +20,22 @@ import {
|
|
|
20
20
|
type OrderByExpression,
|
|
21
21
|
type WhereExpression,
|
|
22
22
|
} from "@decocms/bindings/collections";
|
|
23
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
23
24
|
import {
|
|
24
25
|
useMutation,
|
|
25
26
|
useQueryClient,
|
|
26
27
|
useSuspenseQuery,
|
|
27
28
|
} from "@tanstack/react-query";
|
|
28
29
|
import { toast } from "sonner";
|
|
29
|
-
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
30
|
-
import { useMCPToolCall } from "./use-mcp-tools";
|
|
31
30
|
import { KEYS } from "../lib/query-keys";
|
|
32
31
|
|
|
33
32
|
/**
|
|
34
33
|
* Collection entity base type that matches the collection binding pattern
|
|
34
|
+
* Note: id can be nullable for synthetic entities like Decopilot agent
|
|
35
35
|
*/
|
|
36
|
-
export type CollectionEntity = BaseCollectionEntity
|
|
36
|
+
export type CollectionEntity = Omit<BaseCollectionEntity, "id"> & {
|
|
37
|
+
id: string | null;
|
|
38
|
+
};
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
41
|
* Filter definition for collection queries (matches @deco/ui Filter shape)
|
|
@@ -63,12 +65,27 @@ export interface UseCollectionListOptions<T extends CollectionEntity> {
|
|
|
63
65
|
defaultSortKey?: keyof T;
|
|
64
66
|
/** Page size for pagination (default: 100) */
|
|
65
67
|
pageSize?: number;
|
|
68
|
+
/** Additional arguments forwarded to the collection tool call (e.g., binding, include_virtual) */
|
|
69
|
+
additionalToolArgs?: Record<string, unknown>;
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Query key type for collection list queries
|
|
74
|
+
*/
|
|
75
|
+
export type CollectionQueryKey = readonly [
|
|
76
|
+
unknown,
|
|
77
|
+
string,
|
|
78
|
+
string,
|
|
79
|
+
"collection",
|
|
80
|
+
string,
|
|
81
|
+
"list",
|
|
82
|
+
string,
|
|
83
|
+
];
|
|
84
|
+
|
|
68
85
|
/**
|
|
69
86
|
* Build a where expression from search term and filters
|
|
70
87
|
*/
|
|
71
|
-
function buildWhereExpression<T extends CollectionEntity>(
|
|
88
|
+
export function buildWhereExpression<T extends CollectionEntity>(
|
|
72
89
|
searchTerm: string | undefined,
|
|
73
90
|
filters: CollectionFilter[] | undefined,
|
|
74
91
|
searchFields: (keyof T)[],
|
|
@@ -123,13 +140,13 @@ function buildWhereExpression<T extends CollectionEntity>(
|
|
|
123
140
|
/**
|
|
124
141
|
* Build orderBy expression from sort key and direction
|
|
125
142
|
*/
|
|
126
|
-
function buildOrderByExpression<T extends CollectionEntity>(
|
|
143
|
+
export function buildOrderByExpression<T extends CollectionEntity>(
|
|
127
144
|
sortKey: keyof T | undefined,
|
|
128
145
|
sortDirection: "asc" | "desc" | null | undefined,
|
|
129
146
|
defaultSortKey: keyof T,
|
|
130
147
|
): OrderByExpression[] | undefined {
|
|
131
148
|
const key = sortKey ?? defaultSortKey;
|
|
132
|
-
const direction = sortDirection ?? "
|
|
149
|
+
const direction = sortDirection ?? "desc";
|
|
133
150
|
|
|
134
151
|
return [
|
|
135
152
|
{
|
|
@@ -143,78 +160,110 @@ function buildOrderByExpression<T extends CollectionEntity>(
|
|
|
143
160
|
* Extract payload from MCP tool result (handles structuredContent wrapper)
|
|
144
161
|
*/
|
|
145
162
|
function extractPayload<T>(result: unknown): T {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return r.structuredContent as T;
|
|
163
|
+
if (!result || typeof result !== "object") {
|
|
164
|
+
throw new Error("Invalid result");
|
|
149
165
|
}
|
|
150
|
-
|
|
166
|
+
|
|
167
|
+
if ("isError" in result && result.isError) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
"content" in result &&
|
|
170
|
+
Array.isArray(result.content) &&
|
|
171
|
+
result.content[0]?.type === "text"
|
|
172
|
+
? result.content[0].text
|
|
173
|
+
: "Unknown error",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if ("structuredContent" in result) {
|
|
178
|
+
return result.structuredContent as T;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
throw new Error("No structured content found");
|
|
151
182
|
}
|
|
152
183
|
|
|
153
184
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* @param itemId - The ID of the item to fetch (undefined returns null without making an API call)
|
|
159
|
-
* @param client - The MCP client used to call collection tools
|
|
160
|
-
* @returns Suspense query result with the item, or null if itemId is undefined
|
|
185
|
+
* Query options for a single collection item. Shared between useCollectionItem
|
|
186
|
+
* and parallel-prefetch batches (useSuspenseQueries) so both build an identical
|
|
187
|
+
* query key + queryFn — letting a prefetch warm the exact cache entry the hook
|
|
188
|
+
* later reads.
|
|
161
189
|
*/
|
|
162
|
-
export function
|
|
190
|
+
export function collectionItemQueryOptions<T extends CollectionEntity>(
|
|
163
191
|
scopeKey: string,
|
|
164
192
|
collectionName: string,
|
|
165
193
|
itemId: string | undefined,
|
|
166
194
|
client: Client,
|
|
167
195
|
) {
|
|
168
|
-
void scopeKey; // Reserved for future use (e.g., cache scoping)
|
|
169
196
|
const upperName = collectionName.toUpperCase();
|
|
170
197
|
const getToolName = `COLLECTION_${upperName}_GET`;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
198
|
+
return {
|
|
199
|
+
queryKey: KEYS.collectionItem(
|
|
200
|
+
client,
|
|
201
|
+
scopeKey,
|
|
202
|
+
"",
|
|
203
|
+
upperName,
|
|
204
|
+
itemId ?? "",
|
|
205
|
+
),
|
|
174
206
|
queryFn: async () => {
|
|
175
207
|
if (!itemId) {
|
|
176
|
-
return { item: null }
|
|
208
|
+
return { item: null } satisfies CollectionGetOutput<T>;
|
|
177
209
|
}
|
|
178
210
|
|
|
179
|
-
const result =
|
|
211
|
+
const result = await client.callTool({
|
|
180
212
|
name: getToolName,
|
|
181
|
-
arguments: {
|
|
182
|
-
|
|
183
|
-
} as CollectionGetInput,
|
|
184
|
-
})) as { structuredContent?: unknown };
|
|
213
|
+
arguments: { id: itemId } satisfies CollectionGetInput,
|
|
214
|
+
});
|
|
185
215
|
|
|
186
216
|
return extractPayload<CollectionGetOutput<T>>(result);
|
|
187
217
|
},
|
|
188
218
|
staleTime: 60_000,
|
|
189
|
-
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function useCollectionItem<T extends CollectionEntity>(
|
|
223
|
+
scopeKey: string,
|
|
224
|
+
collectionName: string,
|
|
225
|
+
itemId: string | undefined,
|
|
226
|
+
client: Client,
|
|
227
|
+
) {
|
|
228
|
+
const { data } = useSuspenseQuery(
|
|
229
|
+
collectionItemQueryOptions<T>(scopeKey, collectionName, itemId, client),
|
|
230
|
+
);
|
|
190
231
|
|
|
191
232
|
return data?.item ?? null;
|
|
192
233
|
}
|
|
193
234
|
|
|
235
|
+
/** Fake MCP result for empty collection list when client is skipped */
|
|
236
|
+
export const EMPTY_COLLECTION_LIST_RESULT = {
|
|
237
|
+
structuredContent: {
|
|
238
|
+
items: [],
|
|
239
|
+
} satisfies CollectionListOutput<CollectionEntity>,
|
|
240
|
+
isError: false,
|
|
241
|
+
} as const;
|
|
242
|
+
|
|
194
243
|
/**
|
|
195
244
|
* Get a paginated list of items from a collection
|
|
196
245
|
*
|
|
197
246
|
* @param scopeKey - The scope key (connectionId for connection-scoped, virtualMcpId for virtual-mcp-scoped, etc.)
|
|
198
247
|
* @param collectionName - The name of the collection (e.g., "CONNECTIONS", "AGENT")
|
|
199
|
-
* @param client - The MCP client used to call collection tools
|
|
248
|
+
* @param client - The MCP client used to call collection tools (null/undefined returns [] without MCP call)
|
|
200
249
|
* @param options - Filter and configuration options
|
|
201
250
|
* @returns Suspense query result with items array
|
|
202
251
|
*/
|
|
203
252
|
export function useCollectionList<T extends CollectionEntity>(
|
|
204
253
|
scopeKey: string,
|
|
205
254
|
collectionName: string,
|
|
206
|
-
client: Client,
|
|
255
|
+
client: Client | null | undefined,
|
|
207
256
|
options: UseCollectionListOptions<T> = {},
|
|
208
257
|
) {
|
|
209
|
-
void scopeKey; // Reserved for future use (e.g., cache scoping)
|
|
210
258
|
const {
|
|
211
259
|
searchTerm,
|
|
212
260
|
filters,
|
|
213
261
|
sortKey,
|
|
214
262
|
sortDirection,
|
|
215
|
-
searchFields = ["title", "description"]
|
|
216
|
-
defaultSortKey = "updated_at"
|
|
263
|
+
searchFields = ["title", "description"] satisfies (keyof T)[],
|
|
264
|
+
defaultSortKey = "updated_at" satisfies keyof T,
|
|
217
265
|
pageSize = 100,
|
|
266
|
+
additionalToolArgs,
|
|
218
267
|
} = options;
|
|
219
268
|
|
|
220
269
|
const upperName = collectionName.toUpperCase();
|
|
@@ -227,19 +276,39 @@ export function useCollectionList<T extends CollectionEntity>(
|
|
|
227
276
|
defaultSortKey,
|
|
228
277
|
);
|
|
229
278
|
|
|
230
|
-
const toolArguments: CollectionListInput = {
|
|
279
|
+
const toolArguments: CollectionListInput & Record<string, unknown> = {
|
|
231
280
|
...(where && { where }),
|
|
232
281
|
...(orderBy && { orderBy }),
|
|
233
282
|
limit: pageSize,
|
|
234
283
|
offset: 0,
|
|
284
|
+
...additionalToolArgs,
|
|
235
285
|
};
|
|
236
286
|
|
|
237
|
-
const
|
|
287
|
+
const argsKey = JSON.stringify(toolArguments);
|
|
288
|
+
const queryKey = KEYS.collectionList(
|
|
238
289
|
client,
|
|
239
|
-
|
|
240
|
-
|
|
290
|
+
scopeKey,
|
|
291
|
+
"",
|
|
292
|
+
upperName,
|
|
293
|
+
argsKey,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const { data } = useSuspenseQuery({
|
|
297
|
+
queryKey,
|
|
298
|
+
queryFn: async () => {
|
|
299
|
+
if (!client) {
|
|
300
|
+
return EMPTY_COLLECTION_LIST_RESULT;
|
|
301
|
+
}
|
|
302
|
+
const result = await client.callTool({
|
|
303
|
+
name: listToolName,
|
|
304
|
+
arguments: toolArguments,
|
|
305
|
+
});
|
|
306
|
+
return result;
|
|
307
|
+
},
|
|
308
|
+
staleTime: 30_000,
|
|
309
|
+
retry: false,
|
|
241
310
|
select: (result) => {
|
|
242
|
-
const payload = extractPayload<CollectionListOutput<T>>(result);
|
|
311
|
+
const payload = extractPayload<CollectionListOutput<T>>(result ?? {});
|
|
243
312
|
return payload?.items ?? [];
|
|
244
313
|
},
|
|
245
314
|
});
|
|
@@ -247,6 +316,54 @@ export function useCollectionList<T extends CollectionEntity>(
|
|
|
247
316
|
return data;
|
|
248
317
|
}
|
|
249
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Builds a query key for a collection list query
|
|
321
|
+
* Matches the internal logic of useCollectionList exactly
|
|
322
|
+
*
|
|
323
|
+
* @param client - The MCP client used to call collection tools (null/undefined is valid for skip queries)
|
|
324
|
+
* @param collectionName - The name of the collection (e.g., "THREAD_MESSAGES", "CONNECTIONS")
|
|
325
|
+
* @param scopeKey - The scope key (connectionId for connection-scoped, virtualMcpId for virtual-mcp-scoped, etc.)
|
|
326
|
+
* @param options - Filter and configuration options
|
|
327
|
+
* @returns Query key array
|
|
328
|
+
*/
|
|
329
|
+
export function buildCollectionQueryKey<T extends CollectionEntity>(
|
|
330
|
+
client: Client | null | undefined,
|
|
331
|
+
collectionName: string,
|
|
332
|
+
scopeKey: string,
|
|
333
|
+
options: UseCollectionListOptions<T> = {},
|
|
334
|
+
): CollectionQueryKey {
|
|
335
|
+
const {
|
|
336
|
+
searchTerm,
|
|
337
|
+
filters,
|
|
338
|
+
sortKey,
|
|
339
|
+
sortDirection,
|
|
340
|
+
searchFields = ["title", "description"] satisfies (keyof T)[],
|
|
341
|
+
defaultSortKey = "updated_at" satisfies keyof T,
|
|
342
|
+
pageSize = 100,
|
|
343
|
+
additionalToolArgs,
|
|
344
|
+
} = options;
|
|
345
|
+
|
|
346
|
+
const upperName = collectionName.toUpperCase();
|
|
347
|
+
|
|
348
|
+
const where = buildWhereExpression(searchTerm, filters, searchFields);
|
|
349
|
+
const orderBy = buildOrderByExpression(
|
|
350
|
+
sortKey,
|
|
351
|
+
sortDirection,
|
|
352
|
+
defaultSortKey,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const toolArguments: CollectionListInput & Record<string, unknown> = {
|
|
356
|
+
...(where && { where }),
|
|
357
|
+
...(orderBy && { orderBy }),
|
|
358
|
+
limit: pageSize,
|
|
359
|
+
offset: 0,
|
|
360
|
+
...additionalToolArgs,
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const argsKey = JSON.stringify(toolArguments);
|
|
364
|
+
return KEYS.collectionList(client, scopeKey, "", upperName, argsKey);
|
|
365
|
+
}
|
|
366
|
+
|
|
250
367
|
/**
|
|
251
368
|
* Get mutation actions for create, update, and delete operations
|
|
252
369
|
*
|
|
@@ -260,36 +377,40 @@ export function useCollectionActions<T extends CollectionEntity>(
|
|
|
260
377
|
collectionName: string,
|
|
261
378
|
client: Client,
|
|
262
379
|
) {
|
|
263
|
-
void scopeKey; // Reserved for future use (e.g., cache scoping)
|
|
264
380
|
const queryClient = useQueryClient();
|
|
265
381
|
const upperName = collectionName.toUpperCase();
|
|
266
382
|
const createToolName = `COLLECTION_${upperName}_CREATE`;
|
|
267
383
|
const updateToolName = `COLLECTION_${upperName}_UPDATE`;
|
|
268
384
|
const deleteToolName = `COLLECTION_${upperName}_DELETE`;
|
|
269
385
|
|
|
270
|
-
// Invalidate all
|
|
386
|
+
// Invalidate all collection queries for this scope and collection
|
|
271
387
|
const invalidateCollection = () => {
|
|
272
388
|
queryClient.invalidateQueries({
|
|
273
389
|
predicate: (query) => {
|
|
274
390
|
const key = query.queryKey;
|
|
275
|
-
// Match
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const toolName = key[4] as string;
|
|
280
|
-
return toolName?.startsWith(`COLLECTION_${upperName}_`);
|
|
391
|
+
// Match collectionList/collectionItem keys: [client, scopeKey, "", "collection", collectionName, ...]
|
|
392
|
+
return (
|
|
393
|
+
key[1] === scopeKey && key[3] === "collection" && key[4] === upperName
|
|
394
|
+
);
|
|
281
395
|
},
|
|
282
396
|
});
|
|
283
397
|
};
|
|
284
398
|
|
|
285
399
|
const create = useMutation({
|
|
286
400
|
mutationFn: async (data: Partial<T>) => {
|
|
287
|
-
const result =
|
|
401
|
+
const result = await client.callTool({
|
|
288
402
|
name: createToolName,
|
|
289
|
-
arguments: {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
403
|
+
arguments: { data } satisfies CollectionInsertInput<T>,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
if (result.isError) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
Array.isArray(result.content)
|
|
409
|
+
? result.content[0]?.text
|
|
410
|
+
: String(result.content),
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
293
414
|
const payload = extractPayload<CollectionInsertOutput<T>>(result);
|
|
294
415
|
|
|
295
416
|
return payload.item;
|
|
@@ -306,13 +427,10 @@ export function useCollectionActions<T extends CollectionEntity>(
|
|
|
306
427
|
|
|
307
428
|
const update = useMutation({
|
|
308
429
|
mutationFn: async ({ id, data }: { id: string; data: Partial<T> }) => {
|
|
309
|
-
const result =
|
|
430
|
+
const result = await client.callTool({
|
|
310
431
|
name: updateToolName,
|
|
311
|
-
arguments: {
|
|
312
|
-
|
|
313
|
-
data,
|
|
314
|
-
} as CollectionUpdateInput<T>,
|
|
315
|
-
})) as { structuredContent?: unknown };
|
|
432
|
+
arguments: { id, data } satisfies CollectionUpdateInput<T>,
|
|
433
|
+
});
|
|
316
434
|
const payload = extractPayload<CollectionUpdateOutput<T>>(result);
|
|
317
435
|
|
|
318
436
|
return payload.item;
|
|
@@ -329,12 +447,10 @@ export function useCollectionActions<T extends CollectionEntity>(
|
|
|
329
447
|
|
|
330
448
|
const remove = useMutation({
|
|
331
449
|
mutationFn: async (id: string) => {
|
|
332
|
-
const result =
|
|
450
|
+
const result = await client.callTool({
|
|
333
451
|
name: deleteToolName,
|
|
334
|
-
arguments: {
|
|
335
|
-
|
|
336
|
-
} as CollectionDeleteInput,
|
|
337
|
-
})) as { structuredContent?: unknown };
|
|
452
|
+
arguments: { id } satisfies CollectionDeleteInput,
|
|
453
|
+
});
|
|
338
454
|
const payload = extractPayload<CollectionDeleteOutput<T>>(result);
|
|
339
455
|
|
|
340
456
|
return payload.item.id;
|
|
@@ -25,25 +25,69 @@ export type ConnectionFilter = CollectionFilter;
|
|
|
25
25
|
/**
|
|
26
26
|
* Options for useConnections hook
|
|
27
27
|
*/
|
|
28
|
-
export
|
|
28
|
+
export interface UseConnectionsOptions
|
|
29
|
+
extends UseCollectionListOptions<ConnectionEntity> {
|
|
30
|
+
/**
|
|
31
|
+
* Server-side binding filter. Only returns connections whose tools satisfy the binding.
|
|
32
|
+
* Can be a well-known binding name (e.g., "LLM", "ASSISTANTS", "OBJECT_STORAGE")
|
|
33
|
+
* or a custom binding schema object.
|
|
34
|
+
*/
|
|
35
|
+
binding?: string | Record<string, unknown> | Record<string, unknown>[];
|
|
36
|
+
/**
|
|
37
|
+
* Whether to include VIRTUAL connections in results. Defaults to false (server default).
|
|
38
|
+
*/
|
|
39
|
+
includeVirtual?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Filter by computed connection slug (matches app_name, or slug derived from connection_url/title).
|
|
42
|
+
*/
|
|
43
|
+
slug?: string;
|
|
44
|
+
}
|
|
29
45
|
|
|
30
46
|
/**
|
|
31
|
-
* Hook to get
|
|
47
|
+
* Hook to get connections with server-side filtering.
|
|
32
48
|
*
|
|
33
|
-
* @param options - Filter and configuration options
|
|
49
|
+
* @param options - Filter and configuration options (binding, search, etc.)
|
|
34
50
|
* @returns Suspense query result with connections as ConnectionEntity[]
|
|
35
51
|
*/
|
|
36
52
|
export function useConnections(options: UseConnectionsOptions = {}) {
|
|
53
|
+
const { binding, includeVirtual, slug, ...collectionOptions } = options;
|
|
54
|
+
|
|
55
|
+
// Build additional tool args for the COLLECTION_CONNECTIONS_LIST tool
|
|
56
|
+
const additionalToolArgs: Record<string, unknown> = {
|
|
57
|
+
...collectionOptions.additionalToolArgs,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (binding !== undefined) {
|
|
61
|
+
additionalToolArgs.binding = binding;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (includeVirtual !== undefined) {
|
|
65
|
+
additionalToolArgs.include_virtual = includeVirtual;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (slug !== undefined) {
|
|
69
|
+
additionalToolArgs.slug = slug;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const finalOptions: UseCollectionListOptions<ConnectionEntity> = {
|
|
73
|
+
...collectionOptions,
|
|
74
|
+
additionalToolArgs:
|
|
75
|
+
Object.keys(additionalToolArgs).length > 0
|
|
76
|
+
? additionalToolArgs
|
|
77
|
+
: undefined,
|
|
78
|
+
};
|
|
79
|
+
|
|
37
80
|
const { org } = useProjectContext();
|
|
38
81
|
const client = useMCPClient({
|
|
39
82
|
connectionId: SELF_MCP_ALIAS_ID,
|
|
40
83
|
orgId: org.id,
|
|
84
|
+
orgSlug: org.slug,
|
|
41
85
|
});
|
|
42
86
|
return useCollectionList<ConnectionEntity>(
|
|
43
87
|
org.id,
|
|
44
88
|
"CONNECTIONS",
|
|
45
89
|
client,
|
|
46
|
-
|
|
90
|
+
finalOptions,
|
|
47
91
|
);
|
|
48
92
|
}
|
|
49
93
|
|
|
@@ -58,6 +102,7 @@ export function useConnection(connectionId: string | undefined) {
|
|
|
58
102
|
const client = useMCPClient({
|
|
59
103
|
connectionId: SELF_MCP_ALIAS_ID,
|
|
60
104
|
orgId: org.id,
|
|
105
|
+
orgSlug: org.slug,
|
|
61
106
|
});
|
|
62
107
|
return useCollectionItem<ConnectionEntity>(
|
|
63
108
|
org.id,
|
|
@@ -77,6 +122,7 @@ export function useConnectionActions() {
|
|
|
77
122
|
const client = useMCPClient({
|
|
78
123
|
connectionId: SELF_MCP_ALIAS_ID,
|
|
79
124
|
orgId: org.id,
|
|
125
|
+
orgSlug: org.slug,
|
|
80
126
|
});
|
|
81
127
|
return useCollectionActions<ConnectionEntity>(org.id, "CONNECTIONS", client);
|
|
82
128
|
}
|
|
@@ -11,8 +11,10 @@ const DEFAULT_CLIENT_INFO = {
|
|
|
11
11
|
export interface CreateMcpClientOptions {
|
|
12
12
|
/** Connection ID - use SELF_MCP_ALIAS_ID for the self/management MCP (ALL_TOOLS), or any connectionId for other MCPs */
|
|
13
13
|
connectionId: string | null;
|
|
14
|
-
/** Organization ID - required
|
|
14
|
+
/** Organization ID - required for query-key scoping */
|
|
15
15
|
orgId: string;
|
|
16
|
+
/** Organization slug - required, used to build the /api/:org/mcp URL */
|
|
17
|
+
orgSlug: string;
|
|
16
18
|
/** Authorization token - optional */
|
|
17
19
|
token?: string | null;
|
|
18
20
|
/** Mesh server URL - optional, defaults to window.location.origin (for external apps, provide your Mesh server URL) */
|
|
@@ -21,11 +23,21 @@ export interface CreateMcpClientOptions {
|
|
|
21
23
|
|
|
22
24
|
export type UseMcpClientOptions = CreateMcpClientOptions;
|
|
23
25
|
|
|
26
|
+
export interface UseMcpClientOptionalOptions
|
|
27
|
+
extends Omit<CreateMcpClientOptions, "connectionId"> {
|
|
28
|
+
/** Connection ID - string for connection MCP, null for default/self, undefined to skip (returns null) */
|
|
29
|
+
connectionId: string | null | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
/**
|
|
25
|
-
* Build the MCP URL from connectionId and optional meshUrl
|
|
26
|
-
* Uses /mcp/:connectionId for all servers
|
|
33
|
+
* Build the MCP URL from connectionId and optional meshUrl.
|
|
34
|
+
* Uses /api/:org/mcp/:connectionId for all servers (org-scoped routing).
|
|
27
35
|
*/
|
|
28
|
-
function buildMcpUrl(
|
|
36
|
+
function buildMcpUrl(
|
|
37
|
+
connectionId: string | null,
|
|
38
|
+
orgSlug: string,
|
|
39
|
+
meshUrl?: string,
|
|
40
|
+
): string {
|
|
29
41
|
const baseUrl =
|
|
30
42
|
meshUrl ??
|
|
31
43
|
(typeof window !== "undefined" ? window.location.origin : undefined);
|
|
@@ -35,7 +47,10 @@ function buildMcpUrl(connectionId: string | null, meshUrl?: string): string {
|
|
|
35
47
|
);
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
const
|
|
50
|
+
const orgPath = `/api/${encodeURIComponent(orgSlug)}`;
|
|
51
|
+
const path = connectionId
|
|
52
|
+
? `${orgPath}/mcp/${connectionId}`
|
|
53
|
+
: `${orgPath}/mcp`;
|
|
39
54
|
return new URL(path, baseUrl).href;
|
|
40
55
|
}
|
|
41
56
|
|
|
@@ -49,10 +64,11 @@ function buildMcpUrl(connectionId: string | null, meshUrl?: string): string {
|
|
|
49
64
|
export async function createMCPClient({
|
|
50
65
|
connectionId,
|
|
51
66
|
orgId,
|
|
67
|
+
orgSlug,
|
|
52
68
|
token,
|
|
53
69
|
meshUrl,
|
|
54
70
|
}: CreateMcpClientOptions): Promise<Client> {
|
|
55
|
-
const url = buildMcpUrl(connectionId, meshUrl);
|
|
71
|
+
const url = buildMcpUrl(connectionId, orgSlug, meshUrl);
|
|
56
72
|
|
|
57
73
|
const client = new Client(DEFAULT_CLIENT_INFO, {
|
|
58
74
|
capabilities: {
|
|
@@ -73,7 +89,6 @@ export async function createMCPClient({
|
|
|
73
89
|
headers: {
|
|
74
90
|
"Content-Type": "application/json",
|
|
75
91
|
Accept: "application/json, text/event-stream",
|
|
76
|
-
"x-org-id": orgId,
|
|
77
92
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
78
93
|
},
|
|
79
94
|
},
|
|
@@ -89,6 +104,7 @@ export async function createMCPClient({
|
|
|
89
104
|
token ?? "",
|
|
90
105
|
meshUrl ?? "",
|
|
91
106
|
);
|
|
107
|
+
|
|
92
108
|
(client as Client & { toJSON: () => string }).toJSON = () =>
|
|
93
109
|
`mcp-client:${queryKey.join(":")}`;
|
|
94
110
|
|
|
@@ -105,23 +121,77 @@ export async function createMCPClient({
|
|
|
105
121
|
export function useMCPClient({
|
|
106
122
|
connectionId,
|
|
107
123
|
orgId,
|
|
124
|
+
orgSlug,
|
|
108
125
|
token,
|
|
109
126
|
meshUrl,
|
|
110
127
|
}: UseMcpClientOptions): Client {
|
|
111
128
|
const queryKey = KEYS.mcpClient(
|
|
112
129
|
orgId,
|
|
113
|
-
connectionId ?? "",
|
|
130
|
+
connectionId ?? "self",
|
|
114
131
|
token ?? "",
|
|
115
132
|
meshUrl ?? "",
|
|
116
133
|
);
|
|
117
134
|
|
|
118
135
|
const { data: client } = useSuspenseQuery({
|
|
119
136
|
queryKey,
|
|
120
|
-
queryFn: () =>
|
|
137
|
+
queryFn: () =>
|
|
138
|
+
createMCPClient({ connectionId, orgId, orgSlug, token, meshUrl }),
|
|
121
139
|
staleTime: Infinity, // Keep client alive while query is active
|
|
122
|
-
|
|
140
|
+
// Keep the client cached for a minute after the last subscriber detaches so
|
|
141
|
+
// brief unmount/remount transitions (sidebar collapse toggle, popover open,
|
|
142
|
+
// etc.) re-use the same transport instead of re-establishing it.
|
|
143
|
+
gcTime: 60_000,
|
|
123
144
|
});
|
|
124
145
|
|
|
125
|
-
// useSuspenseQuery guarantees data is available (suspends until ready)
|
|
126
146
|
return client!;
|
|
127
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Optional MCP client - returns null when connectionId is undefined (skip creating client).
|
|
151
|
+
* Use when the connection may not be selected yet (e.g. model picker with no connections).
|
|
152
|
+
*
|
|
153
|
+
* - connectionId: string → connection-specific MCP
|
|
154
|
+
* - connectionId: null → default/self MCP
|
|
155
|
+
* - connectionId: undefined → skip (returns null, no MCP call)
|
|
156
|
+
*
|
|
157
|
+
* @param options - Configuration for the MCP client
|
|
158
|
+
* @returns The MCP client instance, or null when connectionId is undefined
|
|
159
|
+
*/
|
|
160
|
+
export function useMCPClientOptional({
|
|
161
|
+
connectionId,
|
|
162
|
+
orgId,
|
|
163
|
+
orgSlug,
|
|
164
|
+
token,
|
|
165
|
+
meshUrl,
|
|
166
|
+
}: UseMcpClientOptionalOptions): Client | null {
|
|
167
|
+
const queryKey =
|
|
168
|
+
connectionId !== undefined
|
|
169
|
+
? KEYS.mcpClient(
|
|
170
|
+
orgId,
|
|
171
|
+
connectionId ?? "self",
|
|
172
|
+
token ?? "",
|
|
173
|
+
meshUrl ?? "",
|
|
174
|
+
)
|
|
175
|
+
: (["mcp", "client", "skip", orgId] as const);
|
|
176
|
+
|
|
177
|
+
const { data: client } = useSuspenseQuery({
|
|
178
|
+
queryKey,
|
|
179
|
+
queryFn: async () => {
|
|
180
|
+
if (connectionId === undefined) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return createMCPClient({
|
|
184
|
+
connectionId: connectionId as string | null,
|
|
185
|
+
orgId,
|
|
186
|
+
orgSlug,
|
|
187
|
+
token,
|
|
188
|
+
meshUrl,
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
staleTime: Infinity,
|
|
192
|
+
// Match the non-optional variant — see useMCPClient for the rationale.
|
|
193
|
+
gcTime: 60_000,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return client ?? null;
|
|
197
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import {
|
|
3
|
+
ErrorCode,
|
|
4
|
+
McpError,
|
|
5
|
+
type GetPromptRequest,
|
|
6
|
+
type GetPromptResult,
|
|
7
|
+
type ListPromptsResult,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
2
9
|
import {
|
|
3
10
|
useQuery,
|
|
4
11
|
UseQueryResult,
|
|
@@ -7,11 +14,6 @@ import {
|
|
|
7
14
|
type UseQueryOptions,
|
|
8
15
|
type UseSuspenseQueryOptions,
|
|
9
16
|
} from "@tanstack/react-query";
|
|
10
|
-
import type {
|
|
11
|
-
GetPromptRequest,
|
|
12
|
-
GetPromptResult,
|
|
13
|
-
ListPromptsResult,
|
|
14
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
15
17
|
import { KEYS } from "../lib/query-keys";
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -23,7 +25,15 @@ export async function listPrompts(client: Client): Promise<ListPromptsResult> {
|
|
|
23
25
|
if (!capabilities?.prompts) {
|
|
24
26
|
return { prompts: [] };
|
|
25
27
|
}
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
return await client.listPrompts();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error instanceof McpError && error.code === ErrorCode.MethodNotFound) {
|
|
33
|
+
return { prompts: [] };
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
/**
|