@decocms/mesh-sdk 1.1.0
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 +368 -0
- package/package.json +51 -0
- package/src/context/index.ts +9 -0
- package/src/context/project-context.tsx +89 -0
- package/src/hooks/index.ts +73 -0
- package/src/hooks/use-collections.ts +357 -0
- package/src/hooks/use-connection.ts +82 -0
- package/src/hooks/use-mcp-client.ts +127 -0
- package/src/hooks/use-mcp-prompts.ts +126 -0
- package/src/hooks/use-mcp-resources.ts +128 -0
- package/src/hooks/use-mcp-tools.ts +184 -0
- package/src/hooks/use-virtual-mcp.ts +91 -0
- package/src/index.ts +128 -0
- package/src/lib/constants.ts +204 -0
- package/src/lib/mcp-oauth.ts +742 -0
- package/src/lib/query-keys.ts +178 -0
- package/src/lib/streamable-http-client-transport.ts +79 -0
- package/src/types/connection.ts +204 -0
- package/src/types/index.ts +27 -0
- package/src/types/virtual-mcp.ts +218 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection Hooks using React Query
|
|
3
|
+
*
|
|
4
|
+
* Provides React hooks for working with collection-binding-compliant tools.
|
|
5
|
+
* Uses TanStack React Query for caching, loading states, and mutations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type BaseCollectionEntity,
|
|
10
|
+
type CollectionDeleteInput,
|
|
11
|
+
type CollectionDeleteOutput,
|
|
12
|
+
type CollectionGetInput,
|
|
13
|
+
type CollectionGetOutput,
|
|
14
|
+
type CollectionInsertInput,
|
|
15
|
+
type CollectionInsertOutput,
|
|
16
|
+
type CollectionListInput,
|
|
17
|
+
type CollectionListOutput,
|
|
18
|
+
type CollectionUpdateInput,
|
|
19
|
+
type CollectionUpdateOutput,
|
|
20
|
+
type OrderByExpression,
|
|
21
|
+
type WhereExpression,
|
|
22
|
+
} from "@decocms/bindings/collections";
|
|
23
|
+
import {
|
|
24
|
+
useMutation,
|
|
25
|
+
useQueryClient,
|
|
26
|
+
useSuspenseQuery,
|
|
27
|
+
} from "@tanstack/react-query";
|
|
28
|
+
import { toast } from "sonner";
|
|
29
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
30
|
+
import { useMCPToolCall } from "./use-mcp-tools";
|
|
31
|
+
import { KEYS } from "../lib/query-keys";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Collection entity base type that matches the collection binding pattern
|
|
35
|
+
*/
|
|
36
|
+
export type CollectionEntity = BaseCollectionEntity;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Filter definition for collection queries (matches @deco/ui Filter shape)
|
|
40
|
+
*/
|
|
41
|
+
export interface CollectionFilter {
|
|
42
|
+
/** Field to filter on (must match an entity property) */
|
|
43
|
+
column: string;
|
|
44
|
+
/** Value to match */
|
|
45
|
+
value: string | boolean | number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Options for useCollectionList hook
|
|
50
|
+
*/
|
|
51
|
+
export interface UseCollectionListOptions<T extends CollectionEntity> {
|
|
52
|
+
/** Text search term (searches configured searchable fields) */
|
|
53
|
+
searchTerm?: string;
|
|
54
|
+
/** Field filters */
|
|
55
|
+
filters?: CollectionFilter[];
|
|
56
|
+
/** Sort key (field to sort by) */
|
|
57
|
+
sortKey?: keyof T;
|
|
58
|
+
/** Sort direction */
|
|
59
|
+
sortDirection?: "asc" | "desc" | null;
|
|
60
|
+
/** Fields to search when searchTerm is provided (default: ["title", "description"]) */
|
|
61
|
+
searchFields?: (keyof T)[];
|
|
62
|
+
/** Default sort key when none provided */
|
|
63
|
+
defaultSortKey?: keyof T;
|
|
64
|
+
/** Page size for pagination (default: 100) */
|
|
65
|
+
pageSize?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build a where expression from search term and filters
|
|
70
|
+
*/
|
|
71
|
+
function buildWhereExpression<T extends CollectionEntity>(
|
|
72
|
+
searchTerm: string | undefined,
|
|
73
|
+
filters: CollectionFilter[] | undefined,
|
|
74
|
+
searchFields: (keyof T)[],
|
|
75
|
+
): WhereExpression | undefined {
|
|
76
|
+
const conditions: WhereExpression[] = [];
|
|
77
|
+
|
|
78
|
+
// Add search conditions (OR)
|
|
79
|
+
if (searchTerm?.trim()) {
|
|
80
|
+
const trimmedSearchTerm = searchTerm.trim();
|
|
81
|
+
const searchConditions = searchFields.map((field) => ({
|
|
82
|
+
field: [String(field)],
|
|
83
|
+
operator: "contains" as const,
|
|
84
|
+
value: trimmedSearchTerm,
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
if (searchConditions.length === 1 && searchConditions[0]) {
|
|
88
|
+
conditions.push(searchConditions[0]);
|
|
89
|
+
} else if (searchConditions.length > 1) {
|
|
90
|
+
conditions.push({
|
|
91
|
+
operator: "or",
|
|
92
|
+
conditions: searchConditions,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Add filter conditions (AND)
|
|
98
|
+
if (filters && filters.length > 0) {
|
|
99
|
+
for (const filter of filters) {
|
|
100
|
+
conditions.push({
|
|
101
|
+
field: [filter.column],
|
|
102
|
+
operator: "eq" as const,
|
|
103
|
+
value: filter.value,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (conditions.length === 0) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (conditions.length === 1) {
|
|
113
|
+
return conditions[0];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Combine all conditions with AND
|
|
117
|
+
return {
|
|
118
|
+
operator: "and",
|
|
119
|
+
conditions,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build orderBy expression from sort key and direction
|
|
125
|
+
*/
|
|
126
|
+
function buildOrderByExpression<T extends CollectionEntity>(
|
|
127
|
+
sortKey: keyof T | undefined,
|
|
128
|
+
sortDirection: "asc" | "desc" | null | undefined,
|
|
129
|
+
defaultSortKey: keyof T,
|
|
130
|
+
): OrderByExpression[] | undefined {
|
|
131
|
+
const key = sortKey ?? defaultSortKey;
|
|
132
|
+
const direction = sortDirection ?? "asc";
|
|
133
|
+
|
|
134
|
+
return [
|
|
135
|
+
{
|
|
136
|
+
field: [String(key)],
|
|
137
|
+
direction,
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Extract payload from MCP tool result (handles structuredContent wrapper)
|
|
144
|
+
*/
|
|
145
|
+
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;
|
|
149
|
+
}
|
|
150
|
+
return r as T;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
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
|
|
161
|
+
*/
|
|
162
|
+
export function useCollectionItem<T extends CollectionEntity>(
|
|
163
|
+
scopeKey: string,
|
|
164
|
+
collectionName: string,
|
|
165
|
+
itemId: string | undefined,
|
|
166
|
+
client: Client,
|
|
167
|
+
) {
|
|
168
|
+
void scopeKey; // Reserved for future use (e.g., cache scoping)
|
|
169
|
+
const upperName = collectionName.toUpperCase();
|
|
170
|
+
const getToolName = `COLLECTION_${upperName}_GET`;
|
|
171
|
+
|
|
172
|
+
const { data } = useSuspenseQuery({
|
|
173
|
+
queryKey: KEYS.mcpToolCall(client, getToolName, itemId ?? ""),
|
|
174
|
+
queryFn: async () => {
|
|
175
|
+
if (!itemId) {
|
|
176
|
+
return { item: null } as CollectionGetOutput<T>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const result = (await client.callTool({
|
|
180
|
+
name: getToolName,
|
|
181
|
+
arguments: {
|
|
182
|
+
id: itemId,
|
|
183
|
+
} as CollectionGetInput,
|
|
184
|
+
})) as { structuredContent?: unknown };
|
|
185
|
+
|
|
186
|
+
return extractPayload<CollectionGetOutput<T>>(result);
|
|
187
|
+
},
|
|
188
|
+
staleTime: 60_000,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return data?.item ?? null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get a paginated list of items from a collection
|
|
196
|
+
*
|
|
197
|
+
* @param scopeKey - The scope key (connectionId for connection-scoped, virtualMcpId for virtual-mcp-scoped, etc.)
|
|
198
|
+
* @param collectionName - The name of the collection (e.g., "CONNECTIONS", "AGENT")
|
|
199
|
+
* @param client - The MCP client used to call collection tools
|
|
200
|
+
* @param options - Filter and configuration options
|
|
201
|
+
* @returns Suspense query result with items array
|
|
202
|
+
*/
|
|
203
|
+
export function useCollectionList<T extends CollectionEntity>(
|
|
204
|
+
scopeKey: string,
|
|
205
|
+
collectionName: string,
|
|
206
|
+
client: Client,
|
|
207
|
+
options: UseCollectionListOptions<T> = {},
|
|
208
|
+
) {
|
|
209
|
+
void scopeKey; // Reserved for future use (e.g., cache scoping)
|
|
210
|
+
const {
|
|
211
|
+
searchTerm,
|
|
212
|
+
filters,
|
|
213
|
+
sortKey,
|
|
214
|
+
sortDirection,
|
|
215
|
+
searchFields = ["title", "description"] as (keyof T)[],
|
|
216
|
+
defaultSortKey = "updated_at" as keyof T,
|
|
217
|
+
pageSize = 100,
|
|
218
|
+
} = options;
|
|
219
|
+
|
|
220
|
+
const upperName = collectionName.toUpperCase();
|
|
221
|
+
const listToolName = `COLLECTION_${upperName}_LIST`;
|
|
222
|
+
|
|
223
|
+
const where = buildWhereExpression(searchTerm, filters, searchFields);
|
|
224
|
+
const orderBy = buildOrderByExpression(
|
|
225
|
+
sortKey,
|
|
226
|
+
sortDirection,
|
|
227
|
+
defaultSortKey,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const toolArguments: CollectionListInput = {
|
|
231
|
+
...(where && { where }),
|
|
232
|
+
...(orderBy && { orderBy }),
|
|
233
|
+
limit: pageSize,
|
|
234
|
+
offset: 0,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const { data } = useMCPToolCall({
|
|
238
|
+
client,
|
|
239
|
+
toolName: listToolName,
|
|
240
|
+
toolArguments,
|
|
241
|
+
select: (result) => {
|
|
242
|
+
const payload = extractPayload<CollectionListOutput<T>>(result);
|
|
243
|
+
return payload?.items ?? [];
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return data;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get mutation actions for create, update, and delete operations
|
|
252
|
+
*
|
|
253
|
+
* @param scopeKey - The scope key (connectionId for connection-scoped, virtualMcpId for virtual-mcp-scoped, etc.)
|
|
254
|
+
* @param collectionName - The name of the collection (e.g., "CONNECTIONS", "AGENT")
|
|
255
|
+
* @param client - The MCP client used to call collection tools
|
|
256
|
+
* @returns Object with create, update, and delete mutation hooks
|
|
257
|
+
*/
|
|
258
|
+
export function useCollectionActions<T extends CollectionEntity>(
|
|
259
|
+
scopeKey: string,
|
|
260
|
+
collectionName: string,
|
|
261
|
+
client: Client,
|
|
262
|
+
) {
|
|
263
|
+
void scopeKey; // Reserved for future use (e.g., cache scoping)
|
|
264
|
+
const queryClient = useQueryClient();
|
|
265
|
+
const upperName = collectionName.toUpperCase();
|
|
266
|
+
const createToolName = `COLLECTION_${upperName}_CREATE`;
|
|
267
|
+
const updateToolName = `COLLECTION_${upperName}_UPDATE`;
|
|
268
|
+
const deleteToolName = `COLLECTION_${upperName}_DELETE`;
|
|
269
|
+
|
|
270
|
+
// Invalidate all tool call queries for this collection
|
|
271
|
+
const invalidateCollection = () => {
|
|
272
|
+
queryClient.invalidateQueries({
|
|
273
|
+
predicate: (query) => {
|
|
274
|
+
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}_`);
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const create = useMutation({
|
|
286
|
+
mutationFn: async (data: Partial<T>) => {
|
|
287
|
+
const result = (await client.callTool({
|
|
288
|
+
name: createToolName,
|
|
289
|
+
arguments: {
|
|
290
|
+
data,
|
|
291
|
+
} as CollectionInsertInput<T>,
|
|
292
|
+
})) as { structuredContent?: unknown };
|
|
293
|
+
const payload = extractPayload<CollectionInsertOutput<T>>(result);
|
|
294
|
+
|
|
295
|
+
return payload.item;
|
|
296
|
+
},
|
|
297
|
+
onSuccess: () => {
|
|
298
|
+
invalidateCollection();
|
|
299
|
+
toast.success("Item created successfully");
|
|
300
|
+
},
|
|
301
|
+
onError: (error: unknown) => {
|
|
302
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
303
|
+
toast.error(`Failed to create item: ${message}`);
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const update = useMutation({
|
|
308
|
+
mutationFn: async ({ id, data }: { id: string; data: Partial<T> }) => {
|
|
309
|
+
const result = (await client.callTool({
|
|
310
|
+
name: updateToolName,
|
|
311
|
+
arguments: {
|
|
312
|
+
id,
|
|
313
|
+
data,
|
|
314
|
+
} as CollectionUpdateInput<T>,
|
|
315
|
+
})) as { structuredContent?: unknown };
|
|
316
|
+
const payload = extractPayload<CollectionUpdateOutput<T>>(result);
|
|
317
|
+
|
|
318
|
+
return payload.item;
|
|
319
|
+
},
|
|
320
|
+
onSuccess: () => {
|
|
321
|
+
invalidateCollection();
|
|
322
|
+
toast.success("Item updated successfully");
|
|
323
|
+
},
|
|
324
|
+
onError: (error: unknown) => {
|
|
325
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
326
|
+
toast.error(`Failed to update item: ${message}`);
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const remove = useMutation({
|
|
331
|
+
mutationFn: async (id: string) => {
|
|
332
|
+
const result = (await client.callTool({
|
|
333
|
+
name: deleteToolName,
|
|
334
|
+
arguments: {
|
|
335
|
+
id,
|
|
336
|
+
} as CollectionDeleteInput,
|
|
337
|
+
})) as { structuredContent?: unknown };
|
|
338
|
+
const payload = extractPayload<CollectionDeleteOutput<T>>(result);
|
|
339
|
+
|
|
340
|
+
return payload.item.id;
|
|
341
|
+
},
|
|
342
|
+
onSuccess: () => {
|
|
343
|
+
invalidateCollection();
|
|
344
|
+
toast.success("Item deleted successfully");
|
|
345
|
+
},
|
|
346
|
+
onError: (error: unknown) => {
|
|
347
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
348
|
+
toast.error(`Failed to delete item: ${message}`);
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
create,
|
|
354
|
+
update,
|
|
355
|
+
delete: remove,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Collection Hooks
|
|
3
|
+
*
|
|
4
|
+
* Provides React hooks for working with connections using React Query.
|
|
5
|
+
* These hooks offer a reactive interface for accessing and manipulating connections.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ConnectionEntity } from "../types/connection";
|
|
9
|
+
import { useProjectContext } from "../context/project-context";
|
|
10
|
+
import {
|
|
11
|
+
type CollectionFilter,
|
|
12
|
+
useCollectionActions,
|
|
13
|
+
useCollectionItem,
|
|
14
|
+
useCollectionList,
|
|
15
|
+
type UseCollectionListOptions,
|
|
16
|
+
} from "./use-collections";
|
|
17
|
+
import { useMCPClient } from "./use-mcp-client";
|
|
18
|
+
import { SELF_MCP_ALIAS_ID } from "../lib/constants";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Filter definition for connections (matches @deco/ui Filter shape)
|
|
22
|
+
*/
|
|
23
|
+
export type ConnectionFilter = CollectionFilter;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for useConnections hook
|
|
27
|
+
*/
|
|
28
|
+
export type UseConnectionsOptions = UseCollectionListOptions<ConnectionEntity>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Hook to get all connections
|
|
32
|
+
*
|
|
33
|
+
* @param options - Filter and configuration options
|
|
34
|
+
* @returns Suspense query result with connections as ConnectionEntity[]
|
|
35
|
+
*/
|
|
36
|
+
export function useConnections(options: UseConnectionsOptions = {}) {
|
|
37
|
+
const { org } = useProjectContext();
|
|
38
|
+
const client = useMCPClient({
|
|
39
|
+
connectionId: SELF_MCP_ALIAS_ID,
|
|
40
|
+
orgId: org.id,
|
|
41
|
+
});
|
|
42
|
+
return useCollectionList<ConnectionEntity>(
|
|
43
|
+
org.id,
|
|
44
|
+
"CONNECTIONS",
|
|
45
|
+
client,
|
|
46
|
+
options,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Hook to get a single connection by ID
|
|
52
|
+
*
|
|
53
|
+
* @param connectionId - The ID of the connection to fetch (undefined returns null without making an API call)
|
|
54
|
+
* @returns Suspense query result with the connection as ConnectionEntity | null
|
|
55
|
+
*/
|
|
56
|
+
export function useConnection(connectionId: string | undefined) {
|
|
57
|
+
const { org } = useProjectContext();
|
|
58
|
+
const client = useMCPClient({
|
|
59
|
+
connectionId: SELF_MCP_ALIAS_ID,
|
|
60
|
+
orgId: org.id,
|
|
61
|
+
});
|
|
62
|
+
return useCollectionItem<ConnectionEntity>(
|
|
63
|
+
org.id,
|
|
64
|
+
"CONNECTIONS",
|
|
65
|
+
connectionId,
|
|
66
|
+
client,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Hook to get connection mutation actions (create, update, delete)
|
|
72
|
+
*
|
|
73
|
+
* @returns Object with create, update, and delete mutation hooks
|
|
74
|
+
*/
|
|
75
|
+
export function useConnectionActions() {
|
|
76
|
+
const { org } = useProjectContext();
|
|
77
|
+
const client = useMCPClient({
|
|
78
|
+
connectionId: SELF_MCP_ALIAS_ID,
|
|
79
|
+
orgId: org.id,
|
|
80
|
+
});
|
|
81
|
+
return useCollectionActions<ConnectionEntity>(org.id, "CONNECTIONS", client);
|
|
82
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
3
|
+
import { KEYS } from "../lib/query-keys";
|
|
4
|
+
import { StreamableHTTPClientTransport } from "../lib/streamable-http-client-transport";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CLIENT_INFO = {
|
|
7
|
+
name: "mesh-sdk",
|
|
8
|
+
version: "1.0.0",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface CreateMcpClientOptions {
|
|
12
|
+
/** Connection ID - use SELF_MCP_ALIAS_ID for the self/management MCP (ALL_TOOLS), or any connectionId for other MCPs */
|
|
13
|
+
connectionId: string | null;
|
|
14
|
+
/** Organization ID - required, transforms to x-org-id header */
|
|
15
|
+
orgId: string;
|
|
16
|
+
/** Authorization token - optional */
|
|
17
|
+
token?: string | null;
|
|
18
|
+
/** Mesh server URL - optional, defaults to window.location.origin (for external apps, provide your Mesh server URL) */
|
|
19
|
+
meshUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type UseMcpClientOptions = CreateMcpClientOptions;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the MCP URL from connectionId and optional meshUrl
|
|
26
|
+
* Uses /mcp/:connectionId for all servers
|
|
27
|
+
*/
|
|
28
|
+
function buildMcpUrl(connectionId: string | null, meshUrl?: string): string {
|
|
29
|
+
const baseUrl =
|
|
30
|
+
meshUrl ??
|
|
31
|
+
(typeof window !== "undefined" ? window.location.origin : undefined);
|
|
32
|
+
if (!baseUrl) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"MCP client requires either meshUrl option or a browser environment.",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const path = connectionId ? `/mcp/${connectionId}` : "/mcp";
|
|
39
|
+
return new URL(path, baseUrl).href;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create and connect an MCP client with Streamable HTTP transport.
|
|
44
|
+
* This is the low-level function for creating clients outside of React hooks.
|
|
45
|
+
*
|
|
46
|
+
* @param options - Configuration for the MCP client
|
|
47
|
+
* @returns Promise resolving to the connected MCP client
|
|
48
|
+
*/
|
|
49
|
+
export async function createMCPClient({
|
|
50
|
+
connectionId,
|
|
51
|
+
orgId,
|
|
52
|
+
token,
|
|
53
|
+
meshUrl,
|
|
54
|
+
}: CreateMcpClientOptions): Promise<Client> {
|
|
55
|
+
const url = buildMcpUrl(connectionId, meshUrl);
|
|
56
|
+
|
|
57
|
+
const client = new Client(DEFAULT_CLIENT_INFO, {
|
|
58
|
+
capabilities: {
|
|
59
|
+
tasks: {
|
|
60
|
+
list: {},
|
|
61
|
+
cancel: {},
|
|
62
|
+
requests: {
|
|
63
|
+
tool: {
|
|
64
|
+
call: {},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const transport = new StreamableHTTPClientTransport(new URL(url), {
|
|
72
|
+
requestInit: {
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
Accept: "application/json, text/event-stream",
|
|
76
|
+
"x-org-id": orgId,
|
|
77
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await client.connect(transport);
|
|
83
|
+
|
|
84
|
+
// Add toJSON method for query key serialization
|
|
85
|
+
// This allows the client to be used directly in query keys
|
|
86
|
+
const queryKey = KEYS.mcpClient(
|
|
87
|
+
orgId,
|
|
88
|
+
connectionId ?? "self",
|
|
89
|
+
token ?? "",
|
|
90
|
+
meshUrl ?? "",
|
|
91
|
+
);
|
|
92
|
+
(client as Client & { toJSON: () => string }).toJSON = () =>
|
|
93
|
+
`mcp-client:${queryKey.join(":")}`;
|
|
94
|
+
|
|
95
|
+
return client;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Hook to create and manage an MCP client with Streamable HTTP transport.
|
|
100
|
+
* Uses Suspense - must be used within a Suspense boundary.
|
|
101
|
+
*
|
|
102
|
+
* @param options - Configuration for the MCP client
|
|
103
|
+
* @returns The MCP client instance (never null - suspends until ready)
|
|
104
|
+
*/
|
|
105
|
+
export function useMCPClient({
|
|
106
|
+
connectionId,
|
|
107
|
+
orgId,
|
|
108
|
+
token,
|
|
109
|
+
meshUrl,
|
|
110
|
+
}: UseMcpClientOptions): Client {
|
|
111
|
+
const queryKey = KEYS.mcpClient(
|
|
112
|
+
orgId,
|
|
113
|
+
connectionId ?? "",
|
|
114
|
+
token ?? "",
|
|
115
|
+
meshUrl ?? "",
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const { data: client } = useSuspenseQuery({
|
|
119
|
+
queryKey,
|
|
120
|
+
queryFn: () => createMCPClient({ connectionId, orgId, token, meshUrl }),
|
|
121
|
+
staleTime: Infinity, // Keep client alive while query is active
|
|
122
|
+
gcTime: 0, // Clean up immediately when query is inactive
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// useSuspenseQuery guarantees data is available (suspends until ready)
|
|
126
|
+
return client!;
|
|
127
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import {
|
|
3
|
+
useQuery,
|
|
4
|
+
UseQueryResult,
|
|
5
|
+
useSuspenseQuery,
|
|
6
|
+
UseSuspenseQueryResult,
|
|
7
|
+
type UseQueryOptions,
|
|
8
|
+
type UseSuspenseQueryOptions,
|
|
9
|
+
} from "@tanstack/react-query";
|
|
10
|
+
import type {
|
|
11
|
+
GetPromptRequest,
|
|
12
|
+
GetPromptResult,
|
|
13
|
+
ListPromptsResult,
|
|
14
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
+
import { KEYS } from "../lib/query-keys";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* List prompts from an MCP client.
|
|
19
|
+
* This is the raw async function that can be used outside of React hooks.
|
|
20
|
+
*/
|
|
21
|
+
export async function listPrompts(client: Client): Promise<ListPromptsResult> {
|
|
22
|
+
const capabilities = client.getServerCapabilities();
|
|
23
|
+
if (!capabilities?.prompts) {
|
|
24
|
+
return { prompts: [] };
|
|
25
|
+
}
|
|
26
|
+
return await client.listPrompts();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get a specific prompt from an MCP client.
|
|
31
|
+
* This is the raw async function that can be used outside of React hooks.
|
|
32
|
+
*/
|
|
33
|
+
export async function getPrompt(
|
|
34
|
+
client: Client,
|
|
35
|
+
name: string,
|
|
36
|
+
args?: GetPromptRequest["params"]["arguments"],
|
|
37
|
+
): Promise<GetPromptResult> {
|
|
38
|
+
const capabilities = client.getServerCapabilities();
|
|
39
|
+
if (!capabilities?.prompts) {
|
|
40
|
+
throw new Error("Prompts capability not supported");
|
|
41
|
+
}
|
|
42
|
+
return await client.getPrompt({ name, arguments: args ?? {} });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface UseMcpPromptsListOptions
|
|
46
|
+
extends Omit<
|
|
47
|
+
UseSuspenseQueryOptions<ListPromptsResult, Error>,
|
|
48
|
+
"queryKey" | "queryFn"
|
|
49
|
+
> {
|
|
50
|
+
/** The MCP client from useMCPClient */
|
|
51
|
+
client: Client;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Suspense hook to list prompts from an MCP client.
|
|
56
|
+
* Must be used within a Suspense boundary.
|
|
57
|
+
*/
|
|
58
|
+
export function useMCPPromptsList({
|
|
59
|
+
client,
|
|
60
|
+
...queryOptions
|
|
61
|
+
}: UseMcpPromptsListOptions): UseSuspenseQueryResult<ListPromptsResult, Error> {
|
|
62
|
+
return useSuspenseQuery<ListPromptsResult, Error>({
|
|
63
|
+
...queryOptions,
|
|
64
|
+
queryKey: KEYS.mcpPromptsList(client),
|
|
65
|
+
queryFn: () => listPrompts(client),
|
|
66
|
+
staleTime: queryOptions.staleTime ?? 30000,
|
|
67
|
+
retry: false,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface UseMcpPromptsListQueryOptions
|
|
72
|
+
extends Omit<
|
|
73
|
+
UseQueryOptions<ListPromptsResult, Error>,
|
|
74
|
+
"queryKey" | "queryFn"
|
|
75
|
+
> {
|
|
76
|
+
/** The MCP client from useMCPClient */
|
|
77
|
+
client: Client;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Non-suspense hook to list prompts from an MCP client.
|
|
82
|
+
*/
|
|
83
|
+
export function useMCPPromptsListQuery({
|
|
84
|
+
client,
|
|
85
|
+
...queryOptions
|
|
86
|
+
}: UseMcpPromptsListQueryOptions): UseQueryResult<ListPromptsResult, Error> {
|
|
87
|
+
return useQuery<ListPromptsResult, Error>({
|
|
88
|
+
...queryOptions,
|
|
89
|
+
queryKey: KEYS.mcpPromptsList(client),
|
|
90
|
+
queryFn: () => listPrompts(client),
|
|
91
|
+
staleTime: queryOptions.staleTime ?? 30000,
|
|
92
|
+
retry: false,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface UseMcpGetPromptOptions
|
|
97
|
+
extends Omit<
|
|
98
|
+
UseSuspenseQueryOptions<GetPromptResult, Error>,
|
|
99
|
+
"queryKey" | "queryFn"
|
|
100
|
+
> {
|
|
101
|
+
/** The MCP client from useMCPClient */
|
|
102
|
+
client: Client;
|
|
103
|
+
/** Prompt name */
|
|
104
|
+
name: string;
|
|
105
|
+
/** Optional prompt arguments */
|
|
106
|
+
arguments?: GetPromptRequest["params"]["arguments"];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Suspense hook to get a specific prompt from an MCP client.
|
|
111
|
+
* Must be used within a Suspense boundary.
|
|
112
|
+
*/
|
|
113
|
+
export function useMCPGetPrompt({
|
|
114
|
+
client,
|
|
115
|
+
name,
|
|
116
|
+
arguments: args,
|
|
117
|
+
...queryOptions
|
|
118
|
+
}: UseMcpGetPromptOptions): UseSuspenseQueryResult<GetPromptResult, Error> {
|
|
119
|
+
return useSuspenseQuery<GetPromptResult, Error>({
|
|
120
|
+
...queryOptions,
|
|
121
|
+
queryKey: KEYS.mcpGetPrompt(client, name, JSON.stringify(args ?? {})),
|
|
122
|
+
queryFn: () => getPrompt(client, name, args),
|
|
123
|
+
staleTime: queryOptions.staleTime ?? 30000,
|
|
124
|
+
retry: false,
|
|
125
|
+
});
|
|
126
|
+
}
|