@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.
@@ -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 ?? "asc";
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
- const r = result as { structuredContent?: T } | T;
147
- if (r && typeof r === "object" && "structuredContent" in r) {
148
- return r.structuredContent as T;
163
+ if (!result || typeof result !== "object") {
164
+ throw new Error("Invalid result");
149
165
  }
150
- return r as T;
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
- * Get a single item by ID from a collection
155
- *
156
- * @param scopeKey - The scope key (connectionId for connection-scoped, virtualMcpId for virtual-mcp-scoped, etc.)
157
- * @param collectionName - The name of the collection (e.g., "CONNECTIONS", "AGENT")
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 useCollectionItem<T extends CollectionEntity>(
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
- const { data } = useSuspenseQuery({
173
- queryKey: KEYS.mcpToolCall(client, getToolName, itemId ?? ""),
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 } as CollectionGetOutput<T>;
208
+ return { item: null } satisfies CollectionGetOutput<T>;
177
209
  }
178
210
 
179
- const result = (await client.callTool({
211
+ const result = await client.callTool({
180
212
  name: getToolName,
181
- arguments: {
182
- id: itemId,
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"] as (keyof T)[],
216
- defaultSortKey = "updated_at" as keyof T,
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 { data } = useMCPToolCall({
287
+ const argsKey = JSON.stringify(toolArguments);
288
+ const queryKey = KEYS.collectionList(
238
289
  client,
239
- toolName: listToolName,
240
- toolArguments,
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 tool call queries for this collection
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 mcpToolCall keys: ["mcp", "client", client, "tool-call", toolName, argsKey]
276
- if (key[0] !== "mcp" || key[1] !== "client" || key[3] !== "tool-call") {
277
- return false;
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 = (await client.callTool({
401
+ const result = await client.callTool({
288
402
  name: createToolName,
289
- arguments: {
290
- data,
291
- } as CollectionInsertInput<T>,
292
- })) as { structuredContent?: unknown };
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 = (await client.callTool({
430
+ const result = await client.callTool({
310
431
  name: updateToolName,
311
- arguments: {
312
- id,
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 = (await client.callTool({
450
+ const result = await client.callTool({
333
451
  name: deleteToolName,
334
- arguments: {
335
- id,
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 type UseConnectionsOptions = UseCollectionListOptions<ConnectionEntity>;
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 all connections
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
- options,
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, transforms to x-org-id header */
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(connectionId: string | null, meshUrl?: string): string {
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 path = connectionId ? `/mcp/${connectionId}` : "/mcp";
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: () => createMCPClient({ connectionId, orgId, token, meshUrl }),
137
+ queryFn: () =>
138
+ createMCPClient({ connectionId, orgId, orgSlug, token, meshUrl }),
121
139
  staleTime: Infinity, // Keep client alive while query is active
122
- gcTime: 0, // Clean up immediately when query is inactive
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
- return await client.listPrompts();
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
  /**