ctxpkg 0.0.1
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/LICENSE +661 -0
- package/README.md +282 -0
- package/bin/cli.js +8 -0
- package/bin/daemon.js +7 -0
- package/package.json +70 -0
- package/src/agent/AGENTS.md +249 -0
- package/src/agent/agent.prompts.ts +66 -0
- package/src/agent/agent.test-runner.schemas.ts +158 -0
- package/src/agent/agent.test-runner.ts +436 -0
- package/src/agent/agent.ts +371 -0
- package/src/agent/agent.types.ts +94 -0
- package/src/backend/AGENTS.md +112 -0
- package/src/backend/backend.protocol.ts +95 -0
- package/src/backend/backend.schemas.ts +123 -0
- package/src/backend/backend.services.ts +151 -0
- package/src/backend/backend.ts +111 -0
- package/src/backend/backend.types.ts +34 -0
- package/src/cli/AGENTS.md +213 -0
- package/src/cli/cli.agent.ts +197 -0
- package/src/cli/cli.chat.ts +369 -0
- package/src/cli/cli.client.ts +55 -0
- package/src/cli/cli.collections.ts +491 -0
- package/src/cli/cli.config.ts +252 -0
- package/src/cli/cli.daemon.ts +160 -0
- package/src/cli/cli.documents.ts +413 -0
- package/src/cli/cli.mcp.ts +177 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/cli.utils.ts +122 -0
- package/src/client/AGENTS.md +135 -0
- package/src/client/client.adapters.ts +279 -0
- package/src/client/client.ts +86 -0
- package/src/client/client.types.ts +17 -0
- package/src/collections/AGENTS.md +185 -0
- package/src/collections/collections.schemas.ts +195 -0
- package/src/collections/collections.ts +1160 -0
- package/src/config/config.ts +118 -0
- package/src/daemon/AGENTS.md +168 -0
- package/src/daemon/daemon.config.ts +23 -0
- package/src/daemon/daemon.manager.ts +215 -0
- package/src/daemon/daemon.schemas.ts +22 -0
- package/src/daemon/daemon.ts +205 -0
- package/src/database/AGENTS.md +211 -0
- package/src/database/database.ts +64 -0
- package/src/database/migrations/migrations.001-init.ts +56 -0
- package/src/database/migrations/migrations.002-fts5.ts +32 -0
- package/src/database/migrations/migrations.ts +20 -0
- package/src/database/migrations/migrations.types.ts +9 -0
- package/src/documents/AGENTS.md +301 -0
- package/src/documents/documents.schemas.ts +190 -0
- package/src/documents/documents.ts +734 -0
- package/src/embedder/embedder.ts +53 -0
- package/src/exports.ts +0 -0
- package/src/mcp/AGENTS.md +264 -0
- package/src/mcp/mcp.ts +105 -0
- package/src/tools/AGENTS.md +228 -0
- package/src/tools/agent/agent.ts +45 -0
- package/src/tools/documents/documents.ts +401 -0
- package/src/tools/tools.langchain.ts +37 -0
- package/src/tools/tools.mcp.ts +46 -0
- package/src/tools/tools.types.ts +35 -0
- package/src/utils/utils.services.ts +46 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
|
|
3
|
+
import type { BackendClient } from '#root/client/client.ts';
|
|
4
|
+
import { defineTool, type ToolDefinitions } from '#root/tools/tools.types.ts';
|
|
5
|
+
import { toLangchainTools } from '#root/tools/tools.langchain.ts';
|
|
6
|
+
|
|
7
|
+
type DocumentToolOptions = {
|
|
8
|
+
/** Backend client for API calls */
|
|
9
|
+
client: BackendClient;
|
|
10
|
+
/** Optional map of alias names to collection IDs for resolving project-local aliases */
|
|
11
|
+
aliasMap?: Map<string, string>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve collection names to IDs, supporting both direct IDs and aliases.
|
|
16
|
+
*/
|
|
17
|
+
const resolveCollections = (
|
|
18
|
+
collections: string[] | undefined,
|
|
19
|
+
aliasMap: Map<string, string> | undefined,
|
|
20
|
+
): string[] | undefined => {
|
|
21
|
+
if (!collections || collections.length === 0) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
if (!aliasMap || aliasMap.size === 0) {
|
|
25
|
+
return collections;
|
|
26
|
+
}
|
|
27
|
+
return collections.map((c) => aliasMap.get(c) ?? c);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a single collection name to ID, supporting both direct IDs and aliases.
|
|
32
|
+
*/
|
|
33
|
+
const resolveCollection = (collection: string, aliasMap: Map<string, string> | undefined): string => {
|
|
34
|
+
if (!aliasMap) {
|
|
35
|
+
return collection;
|
|
36
|
+
}
|
|
37
|
+
return aliasMap.get(collection) ?? collection;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates document tool definitions that use the provided BackendClient.
|
|
42
|
+
* These tools provide read-only access to the semantic index.
|
|
43
|
+
*
|
|
44
|
+
* Returns common tool definitions that can be converted to Langchain or MCP tools.
|
|
45
|
+
*
|
|
46
|
+
* @param options - Configuration options
|
|
47
|
+
* @param options.client - BackendClient for API calls
|
|
48
|
+
* @param options.aliasMap - Optional map of project aliases to collection IDs
|
|
49
|
+
*/
|
|
50
|
+
const createDocumentToolDefinitions = (options: DocumentToolOptions): ToolDefinitions => {
|
|
51
|
+
const { client, aliasMap } = options;
|
|
52
|
+
// Build reverse map for showing aliases in results
|
|
53
|
+
const idToAlias = new Map<string, string>();
|
|
54
|
+
if (aliasMap) {
|
|
55
|
+
for (const [alias, id] of aliasMap.entries()) {
|
|
56
|
+
idToAlias.set(id, alias);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const listCollections = defineTool({
|
|
61
|
+
name: 'documents_list_collections',
|
|
62
|
+
description:
|
|
63
|
+
'List all available document collections. Returns collection names/aliases, document counts, descriptions, and versions. Use this to discover what documentation is available before searching.',
|
|
64
|
+
schema: z.object({}),
|
|
65
|
+
handler: async () => {
|
|
66
|
+
const collections = await client.documents.listCollections();
|
|
67
|
+
|
|
68
|
+
if (collections.length === 0) {
|
|
69
|
+
return 'No document collections found.';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return collections.map((c) => {
|
|
73
|
+
const alias = idToAlias.get(c.collection);
|
|
74
|
+
return {
|
|
75
|
+
collection: alias ?? c.collection,
|
|
76
|
+
collectionId: alias ? c.collection : undefined,
|
|
77
|
+
documentCount: c.document_count,
|
|
78
|
+
description: c.description ?? undefined,
|
|
79
|
+
version: c.version ?? undefined,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const searchDocuments = defineTool({
|
|
86
|
+
name: 'documents_search',
|
|
87
|
+
description:
|
|
88
|
+
'Search documents using semantic similarity and keyword matching (hybrid search). Returns the most relevant document chunks for the given query. Use this to find information in documentation, guides, or other indexed materials.',
|
|
89
|
+
schema: z.object({
|
|
90
|
+
query: z.string().describe('The search query - describe what information you are looking for'),
|
|
91
|
+
collections: z
|
|
92
|
+
.array(z.string())
|
|
93
|
+
.optional()
|
|
94
|
+
.describe(
|
|
95
|
+
'Optional list of collection names or aliases to search in. If not provided, searches all collections.',
|
|
96
|
+
),
|
|
97
|
+
limit: z.number().optional().describe('Maximum number of results to return (default: 10)'),
|
|
98
|
+
maxDistance: z
|
|
99
|
+
.number()
|
|
100
|
+
.optional()
|
|
101
|
+
.describe(
|
|
102
|
+
'Maximum distance threshold (0-2 for cosine). Results with distance greater than this are filtered out. Lower values = stricter matching.',
|
|
103
|
+
),
|
|
104
|
+
hybridSearch: z
|
|
105
|
+
.boolean()
|
|
106
|
+
.optional()
|
|
107
|
+
.describe(
|
|
108
|
+
'Whether to combine vector similarity with keyword matching (default: true). Disable for pure semantic search.',
|
|
109
|
+
),
|
|
110
|
+
rerank: z
|
|
111
|
+
.boolean()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe(
|
|
114
|
+
'Whether to re-rank results using a secondary model for higher precision (default: false). Slower but more accurate.',
|
|
115
|
+
),
|
|
116
|
+
}),
|
|
117
|
+
handler: async ({ query, collections, limit, maxDistance, hybridSearch, rerank }) => {
|
|
118
|
+
// Resolve any aliases to collection IDs
|
|
119
|
+
const resolvedCollections = resolveCollections(collections, aliasMap);
|
|
120
|
+
|
|
121
|
+
const results = await client.documents.search({
|
|
122
|
+
query,
|
|
123
|
+
collections: resolvedCollections,
|
|
124
|
+
limit: limit ?? 10,
|
|
125
|
+
maxDistance,
|
|
126
|
+
hybridSearch,
|
|
127
|
+
rerank,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (results.length === 0) {
|
|
131
|
+
return 'No results found for the given query.';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return results.map((r) => {
|
|
135
|
+
const alias = idToAlias.get(r.collection);
|
|
136
|
+
return {
|
|
137
|
+
collection: alias ?? r.collection,
|
|
138
|
+
collectionId: alias ? r.collection : undefined,
|
|
139
|
+
documentId: r.document,
|
|
140
|
+
content: r.content,
|
|
141
|
+
relevanceScore: r.score ?? 1 - r.distance, // Use score if available, else convert distance
|
|
142
|
+
distance: r.distance,
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const getDocument = defineTool({
|
|
149
|
+
name: 'documents_get_document',
|
|
150
|
+
description:
|
|
151
|
+
'Get the full content of a specific document. Use this after searching to retrieve the complete document when you need more context than the search chunks provide.',
|
|
152
|
+
schema: z.object({
|
|
153
|
+
collection: z.string().describe('The collection name or alias containing the document'),
|
|
154
|
+
document: z.string().describe('The document ID (typically the file path used when indexing)'),
|
|
155
|
+
}),
|
|
156
|
+
handler: async ({ collection, document }) => {
|
|
157
|
+
// Resolve alias to collection ID
|
|
158
|
+
const resolvedCollection = resolveCollection(collection, aliasMap);
|
|
159
|
+
|
|
160
|
+
const result = await client.documents.getDocument({ collection: resolvedCollection, id: document });
|
|
161
|
+
|
|
162
|
+
if (!result) {
|
|
163
|
+
return `Document "${document}" not found in collection "${collection}".`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const alias = idToAlias.get(result.collection);
|
|
167
|
+
return {
|
|
168
|
+
collection: alias ?? result.collection,
|
|
169
|
+
collectionId: alias ? result.collection : undefined,
|
|
170
|
+
document: result.id,
|
|
171
|
+
content: result.content,
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// === New tools for MCP v2 ===
|
|
177
|
+
|
|
178
|
+
const listDocuments = defineTool({
|
|
179
|
+
name: 'documents_list_documents',
|
|
180
|
+
description:
|
|
181
|
+
'List all documents in a collection. Returns document IDs, titles, and sizes. ' +
|
|
182
|
+
'Use this to browse what documentation is available before searching. ' +
|
|
183
|
+
'Supports pagination for large collections.',
|
|
184
|
+
schema: z.object({
|
|
185
|
+
collection: z.string().describe('The collection name or alias'),
|
|
186
|
+
limit: z.number().optional().describe('Maximum documents to return (default: 100)'),
|
|
187
|
+
offset: z.number().optional().describe('Offset for pagination (default: 0)'),
|
|
188
|
+
}),
|
|
189
|
+
handler: async ({ collection, limit, offset }) => {
|
|
190
|
+
const resolvedCollection = resolveCollection(collection, aliasMap);
|
|
191
|
+
|
|
192
|
+
const result = await client.documents.listDocuments({
|
|
193
|
+
collection: resolvedCollection,
|
|
194
|
+
limit: limit ?? 100,
|
|
195
|
+
offset: offset ?? 0,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
collection: idToAlias.get(resolvedCollection) ?? resolvedCollection,
|
|
200
|
+
collectionId: idToAlias.has(resolvedCollection) ? resolvedCollection : undefined,
|
|
201
|
+
documents: result.documents,
|
|
202
|
+
total: result.total,
|
|
203
|
+
hasMore: result.hasMore,
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const getOutline = defineTool({
|
|
209
|
+
name: 'documents_get_outline',
|
|
210
|
+
description:
|
|
211
|
+
'Get the heading structure of a document. Returns section headings with their levels ' +
|
|
212
|
+
'and line numbers. Use this to understand document organization before reading specific sections.',
|
|
213
|
+
schema: z.object({
|
|
214
|
+
collection: z.string().describe('The collection name or alias'),
|
|
215
|
+
document: z.string().describe('The document ID'),
|
|
216
|
+
maxDepth: z.number().optional().describe('Maximum heading depth 1-6 (default: 3)'),
|
|
217
|
+
}),
|
|
218
|
+
handler: async ({ collection, document, maxDepth }) => {
|
|
219
|
+
const resolvedCollection = resolveCollection(collection, aliasMap);
|
|
220
|
+
|
|
221
|
+
const result = await client.documents.getOutline({
|
|
222
|
+
collection: resolvedCollection,
|
|
223
|
+
document,
|
|
224
|
+
maxDepth: maxDepth ?? 3,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (!result) {
|
|
228
|
+
return `Document "${document}" not found in collection "${collection}".`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
collection: idToAlias.get(resolvedCollection) ?? resolvedCollection,
|
|
233
|
+
collectionId: idToAlias.has(resolvedCollection) ? resolvedCollection : undefined,
|
|
234
|
+
document,
|
|
235
|
+
title: result.title,
|
|
236
|
+
outline: result.outline,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const getSection = defineTool({
|
|
242
|
+
name: 'documents_get_section',
|
|
243
|
+
description:
|
|
244
|
+
'Get a specific section of a document by heading. Returns the section content without ' +
|
|
245
|
+
'fetching the entire document. Use this when you know which section you need.',
|
|
246
|
+
schema: z.object({
|
|
247
|
+
collection: z.string().describe('The collection name or alias'),
|
|
248
|
+
document: z.string().describe('The document ID'),
|
|
249
|
+
section: z.string().describe('Section heading text to match (case-insensitive substring match)'),
|
|
250
|
+
includeSubsections: z.boolean().optional().describe('Include nested subsections in the result (default: true)'),
|
|
251
|
+
}),
|
|
252
|
+
handler: async ({ collection, document, section, includeSubsections }) => {
|
|
253
|
+
const resolvedCollection = resolveCollection(collection, aliasMap);
|
|
254
|
+
|
|
255
|
+
const result = await client.documents.getSection({
|
|
256
|
+
collection: resolvedCollection,
|
|
257
|
+
document,
|
|
258
|
+
section,
|
|
259
|
+
includeSubsections: includeSubsections ?? true,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (!result) {
|
|
263
|
+
return `Section "${section}" not found in document "${document}".`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
collection: idToAlias.get(resolvedCollection) ?? resolvedCollection,
|
|
268
|
+
collectionId: idToAlias.has(resolvedCollection) ? resolvedCollection : undefined,
|
|
269
|
+
document,
|
|
270
|
+
section: result.section,
|
|
271
|
+
level: result.level,
|
|
272
|
+
content: result.content,
|
|
273
|
+
startLine: result.startLine,
|
|
274
|
+
endLine: result.endLine,
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const searchBatch = defineTool({
|
|
280
|
+
name: 'documents_search_batch',
|
|
281
|
+
description:
|
|
282
|
+
'Execute multiple search queries in a single call. More efficient than making ' +
|
|
283
|
+
'separate search calls when researching multiple topics. Limited to 10 queries.',
|
|
284
|
+
schema: z.object({
|
|
285
|
+
queries: z
|
|
286
|
+
.array(
|
|
287
|
+
z.object({
|
|
288
|
+
query: z.string().describe('Search query'),
|
|
289
|
+
collections: z.array(z.string()).optional().describe('Limit to specific collections'),
|
|
290
|
+
}),
|
|
291
|
+
)
|
|
292
|
+
.min(1)
|
|
293
|
+
.max(10)
|
|
294
|
+
.describe('Array of search queries (max 10)'),
|
|
295
|
+
limit: z.number().optional().describe('Results per query (default: 5)'),
|
|
296
|
+
maxDistance: z.number().optional().describe('Maximum distance threshold per query'),
|
|
297
|
+
hybridSearch: z.boolean().optional().describe('Use hybrid search (default: true)'),
|
|
298
|
+
}),
|
|
299
|
+
handler: async ({ queries, limit, maxDistance, hybridSearch }) => {
|
|
300
|
+
// Resolve collection aliases in each query
|
|
301
|
+
const resolvedQueries = queries.map((q) => ({
|
|
302
|
+
query: q.query,
|
|
303
|
+
collections: resolveCollections(q.collections, aliasMap),
|
|
304
|
+
}));
|
|
305
|
+
|
|
306
|
+
const result = await client.documents.searchBatch({
|
|
307
|
+
queries: resolvedQueries,
|
|
308
|
+
limit: limit ?? 5,
|
|
309
|
+
maxDistance,
|
|
310
|
+
hybridSearch: hybridSearch ?? true,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Map collection IDs back to aliases in results
|
|
314
|
+
return {
|
|
315
|
+
results: result.results.map((r) => ({
|
|
316
|
+
query: r.query,
|
|
317
|
+
results: r.results.map((item) => {
|
|
318
|
+
const alias = idToAlias.get(item.collection);
|
|
319
|
+
return {
|
|
320
|
+
collection: alias ?? item.collection,
|
|
321
|
+
collectionId: alias ? item.collection : undefined,
|
|
322
|
+
documentId: item.document,
|
|
323
|
+
content: item.content,
|
|
324
|
+
relevanceScore: item.score ?? 1 - item.distance,
|
|
325
|
+
distance: item.distance,
|
|
326
|
+
};
|
|
327
|
+
}),
|
|
328
|
+
})),
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const findRelated = defineTool({
|
|
334
|
+
name: 'documents_find_related',
|
|
335
|
+
description:
|
|
336
|
+
'Find content semantically related to a document or chunk. Use this to expand context ' +
|
|
337
|
+
'or discover related documentation on a topic.',
|
|
338
|
+
schema: z.object({
|
|
339
|
+
collection: z.string().describe('Collection containing the source document'),
|
|
340
|
+
document: z.string().describe('Document ID to find related content for'),
|
|
341
|
+
chunk: z
|
|
342
|
+
.string()
|
|
343
|
+
.optional()
|
|
344
|
+
.describe('Specific chunk content to find related items for (uses document centroid if not provided)'),
|
|
345
|
+
limit: z.number().optional().describe('Maximum related items (default: 5)'),
|
|
346
|
+
sameDocument: z.boolean().optional().describe('Include chunks from the same document (default: false)'),
|
|
347
|
+
}),
|
|
348
|
+
handler: async ({ collection, document, chunk, limit, sameDocument }) => {
|
|
349
|
+
const resolvedCollection = resolveCollection(collection, aliasMap);
|
|
350
|
+
|
|
351
|
+
const results = await client.documents.findRelated({
|
|
352
|
+
collection: resolvedCollection,
|
|
353
|
+
document,
|
|
354
|
+
chunk,
|
|
355
|
+
limit: limit ?? 5,
|
|
356
|
+
sameDocument: sameDocument ?? false,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
source: {
|
|
361
|
+
collection: idToAlias.get(resolvedCollection) ?? resolvedCollection,
|
|
362
|
+
collectionId: idToAlias.has(resolvedCollection) ? resolvedCollection : undefined,
|
|
363
|
+
document,
|
|
364
|
+
},
|
|
365
|
+
related: results.map((r) => {
|
|
366
|
+
const alias = idToAlias.get(r.collection);
|
|
367
|
+
return {
|
|
368
|
+
collection: alias ?? r.collection,
|
|
369
|
+
collectionId: alias ? r.collection : undefined,
|
|
370
|
+
documentId: r.document,
|
|
371
|
+
content: r.content,
|
|
372
|
+
relevanceScore: r.score ?? 1 - r.distance,
|
|
373
|
+
};
|
|
374
|
+
}),
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
listCollections,
|
|
381
|
+
searchDocuments,
|
|
382
|
+
getDocument,
|
|
383
|
+
listDocuments,
|
|
384
|
+
getOutline,
|
|
385
|
+
getSection,
|
|
386
|
+
searchBatch,
|
|
387
|
+
findRelated,
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Creates Langchain document tools for backward compatibility.
|
|
393
|
+
* @deprecated Use createDocumentToolDefinitions with toLangchainTools instead
|
|
394
|
+
*/
|
|
395
|
+
const createDocumentTools = (client: BackendClient, aliasMap?: Map<string, string>) => {
|
|
396
|
+
const definitions = createDocumentToolDefinitions({ client, aliasMap });
|
|
397
|
+
return toLangchainTools(definitions);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
export { createDocumentToolDefinitions, createDocumentTools };
|
|
401
|
+
export type { DocumentToolOptions };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { tool } from 'langchain';
|
|
2
|
+
|
|
3
|
+
import type { ToolDefinition, ToolDefinitions } from './tools.types.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert a common tool definition to a Langchain tool
|
|
7
|
+
*/
|
|
8
|
+
const toLangchainTool = <T extends ToolDefinition>(definition: T) => {
|
|
9
|
+
return tool(
|
|
10
|
+
async (input) => {
|
|
11
|
+
const result = await definition.handler(input);
|
|
12
|
+
// Langchain tools expect string output
|
|
13
|
+
if (typeof result === 'string') {
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
return JSON.stringify(result, null, 2);
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: definition.name,
|
|
20
|
+
description: definition.description,
|
|
21
|
+
schema: definition.schema,
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Convert a collection of tool definitions to Langchain tools
|
|
28
|
+
*/
|
|
29
|
+
const toLangchainTools = <T extends ToolDefinitions>(definitions: T) => {
|
|
30
|
+
const result: Record<string, ReturnType<typeof toLangchainTool>> = {};
|
|
31
|
+
for (const [key, definition] of Object.entries(definitions)) {
|
|
32
|
+
result[key] = toLangchainTool(definition);
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { toLangchainTool, toLangchainTools };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { ZodObject, ZodRawShape } from 'zod';
|
|
3
|
+
|
|
4
|
+
import type { ToolDefinition, ToolDefinitions } from './tools.types.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register a common tool definition on an MCP server
|
|
8
|
+
*/
|
|
9
|
+
const registerMcpTool = <T extends ToolDefinition>(server: McpServer, definition: T) => {
|
|
10
|
+
// MCP SDK expects the Zod schema shape directly (the inner part of z.object())
|
|
11
|
+
// Extract the shape from ZodObject if available
|
|
12
|
+
const schema = definition.schema;
|
|
13
|
+
const shape = 'shape' in schema ? (schema as ZodObject<ZodRawShape>).shape : {};
|
|
14
|
+
|
|
15
|
+
server.tool(definition.name, definition.description, shape, async (args) => {
|
|
16
|
+
try {
|
|
17
|
+
// Parse and validate input through Zod schema
|
|
18
|
+
const validatedInput = definition.schema.parse(args);
|
|
19
|
+
const result = await definition.handler(validatedInput);
|
|
20
|
+
|
|
21
|
+
// Format result for MCP
|
|
22
|
+
const content = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: 'text' as const, text: content }],
|
|
26
|
+
};
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: 'text' as const, text: `Error: ${message}` }],
|
|
31
|
+
isError: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Register a collection of tool definitions on an MCP server
|
|
39
|
+
*/
|
|
40
|
+
const registerMcpTools = <T extends ToolDefinitions>(server: McpServer, definitions: T) => {
|
|
41
|
+
for (const definition of Object.values(definitions)) {
|
|
42
|
+
registerMcpTool(server, definition);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export { registerMcpTool, registerMcpTools };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Common tool definition format that can be converted to Langchain or MCP tools.
|
|
5
|
+
*
|
|
6
|
+
* This provides a framework-agnostic way to define tools that can then
|
|
7
|
+
* be adapted to different tool runtime environments.
|
|
8
|
+
*/
|
|
9
|
+
type ToolDefinition<TInput extends z.ZodType = z.ZodType, TOutput = unknown> = {
|
|
10
|
+
/** Unique name for the tool */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Description of what the tool does */
|
|
13
|
+
description: string;
|
|
14
|
+
/** Zod schema for input validation */
|
|
15
|
+
schema: TInput;
|
|
16
|
+
/** The handler function that executes the tool */
|
|
17
|
+
handler: (input: z.infer<TInput>) => Promise<TOutput>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A collection of tool definitions - using eslint-disable for any here since
|
|
22
|
+
* the specific tool types are erased when collecting into a record
|
|
23
|
+
*/
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
type ToolDefinitions = Record<string, ToolDefinition<any, any>>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Helper to create a type-safe tool definition
|
|
29
|
+
*/
|
|
30
|
+
const defineTool = <TInput extends z.ZodType, TOutput>(
|
|
31
|
+
definition: ToolDefinition<TInput, TOutput>,
|
|
32
|
+
): ToolDefinition<TInput, TOutput> => definition;
|
|
33
|
+
|
|
34
|
+
export { defineTool };
|
|
35
|
+
export type { ToolDefinition, ToolDefinitions };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const destroy = Symbol('destroy');
|
|
2
|
+
const instanceKey = Symbol('instances');
|
|
3
|
+
|
|
4
|
+
type ServiceDependency<T> = new (services: Services) => T & {
|
|
5
|
+
[destroy]?: () => Promise<void> | void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
class Services {
|
|
9
|
+
[instanceKey]: Map<ServiceDependency<unknown>, unknown>;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this[instanceKey] = new Map();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public get = <T>(service: ServiceDependency<T>) => {
|
|
16
|
+
if (!this[instanceKey].has(service)) {
|
|
17
|
+
this[instanceKey].set(service, new service(this));
|
|
18
|
+
}
|
|
19
|
+
const instance = this[instanceKey].get(service);
|
|
20
|
+
if (!instance) {
|
|
21
|
+
throw new Error('Could not generate instance');
|
|
22
|
+
}
|
|
23
|
+
return instance as T;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
public set = <T>(service: ServiceDependency<T>, instance: Partial<T>) => {
|
|
27
|
+
this[instanceKey].set(service, instance);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
public destroy = async () => {
|
|
31
|
+
await Promise.all(
|
|
32
|
+
Array.from(this[instanceKey].values()).map(async (instance) => {
|
|
33
|
+
if (
|
|
34
|
+
typeof instance === 'object' &&
|
|
35
|
+
instance &&
|
|
36
|
+
destroy in instance &&
|
|
37
|
+
typeof instance[destroy] === 'function'
|
|
38
|
+
) {
|
|
39
|
+
await instance[destroy]();
|
|
40
|
+
}
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { Services, destroy };
|