@supermodeltools/mcp-server 0.4.2 → 0.4.4
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 +27 -13
- package/dist/cache/graph-cache.js +260 -0
- package/dist/cache/graph-types.js +6 -0
- package/dist/cache/index.js +21 -0
- package/dist/queries/discovery.js +148 -0
- package/dist/queries/index.js +241 -0
- package/dist/queries/summary.js +36 -0
- package/dist/queries/traversal.js +392 -0
- package/dist/queries/types.js +38 -0
- package/dist/server.js +38 -1
- package/dist/tools/create-supermodel-graph.js +500 -29
- package/dist/utils/zip-repository.js +332 -0
- package/package.json +4 -1
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.handler = exports.tool = exports.metadata = void 0;
|
|
4
4
|
const promises_1 = require("fs/promises");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const crypto_2 = require("crypto");
|
|
8
|
+
const path_1 = require("path");
|
|
5
9
|
const types_1 = require("../types");
|
|
6
10
|
const filtering_1 = require("../filtering");
|
|
11
|
+
const queries_1 = require("../queries");
|
|
12
|
+
const zip_repository_1 = require("../utils/zip-repository");
|
|
7
13
|
exports.metadata = {
|
|
8
14
|
resource: 'graphs',
|
|
9
15
|
operation: 'write',
|
|
@@ -13,56 +19,522 @@ exports.metadata = {
|
|
|
13
19
|
operationId: 'generateSupermodelGraph',
|
|
14
20
|
};
|
|
15
21
|
exports.tool = {
|
|
16
|
-
name: '
|
|
17
|
-
description:
|
|
22
|
+
name: 'explore_codebase',
|
|
23
|
+
description: `Analyzes code within the target directory to produce a graph that can be used to navigate the codebase when solving bugs, planning or analyzing code changes.
|
|
24
|
+
|
|
25
|
+
## Example Output
|
|
26
|
+
|
|
27
|
+
This is actual output from running explore_codebase on its own repository (19 TypeScript files, ~60KB).
|
|
28
|
+
|
|
29
|
+
The graph structure shows 163 nodes (Functions, Classes, Types, Files, Domains) and 254 relationships between them. Below is an excerpt showing the data structure:
|
|
30
|
+
|
|
31
|
+
\`\`\`json
|
|
32
|
+
{
|
|
33
|
+
"repo": "1c740c9c4f5c9528e244ab144488214341f959231f73009a46a74a1f11350c3c",
|
|
34
|
+
"version": "sir-2026-01-15",
|
|
35
|
+
"schemaVersion": "1.2.0",
|
|
36
|
+
"generatedAt": "2026-01-15T18:17:10.067Z",
|
|
37
|
+
"summary": {
|
|
38
|
+
"filesProcessed": 19,
|
|
39
|
+
"types": 28,
|
|
40
|
+
"functions": 52,
|
|
41
|
+
"repoSizeBytes": 61058,
|
|
42
|
+
"classes": 2,
|
|
43
|
+
"domains": 6,
|
|
44
|
+
"primaryLanguage": "json"
|
|
45
|
+
},
|
|
46
|
+
"graph": {
|
|
47
|
+
"nodeCount": 163,
|
|
48
|
+
"relationshipCount": 254,
|
|
49
|
+
"sampleNodes": [
|
|
50
|
+
{
|
|
51
|
+
"id": "0965aff4:42ff:df74:ae01:0d17d0886720",
|
|
52
|
+
"labels": ["ExternalModule"],
|
|
53
|
+
"properties": {
|
|
54
|
+
"name": "mcp.js"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "ab32efae:3825:dada:a4b5:95e95dbb71cc",
|
|
59
|
+
"labels": ["Function"],
|
|
60
|
+
"properties": {
|
|
61
|
+
"name": "getNode",
|
|
62
|
+
"filePath": "src/queries/discovery.ts",
|
|
63
|
+
"language": "typescript",
|
|
64
|
+
"startLine": 25,
|
|
65
|
+
"endLine": 57,
|
|
66
|
+
"kind": "function"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "8648e520:0be3:b754:77ce:c19dcebf6d6f",
|
|
71
|
+
"labels": ["File"],
|
|
72
|
+
"properties": {
|
|
73
|
+
"name": "test-full-graph.js",
|
|
74
|
+
"filePath": "test-full-graph.js",
|
|
75
|
+
"path": "test-full-graph.js",
|
|
76
|
+
"language": "javascript"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"sampleRelationships": [
|
|
81
|
+
{
|
|
82
|
+
"id": "b161d717:5ee5:b827:cc26:f071f7a9648d->ff2a17f0:c2b6:f518:dcb8:91584b241c0f:CHILD_DIRECTORY",
|
|
83
|
+
"type": "CHILD_DIRECTORY",
|
|
84
|
+
"startNode": "b161d717:5ee5:b827:cc26:f071f7a9648d",
|
|
85
|
+
"endNode": "ff2a17f0:c2b6:f518:dcb8:91584b241c0f",
|
|
86
|
+
"properties": {}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "ff2a17f0:c2b6:f518:dcb8:91584b241c0f->7f19b034:9fee:67c7:7b42:c7ba738d9ceb:CONTAINS_FILE",
|
|
90
|
+
"type": "CONTAINS_FILE",
|
|
91
|
+
"startNode": "ff2a17f0:c2b6:f518:dcb8:91584b241c0f",
|
|
92
|
+
"endNode": "7f19b034:9fee:67c7:7b42:c7ba738d9ceb",
|
|
93
|
+
"properties": {}
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
\`\`\`
|
|
99
|
+
|
|
100
|
+
The graph contains nodes with properties like filePath, startLine, endLine for functions, and relationships like CHILD_DIRECTORY, CONTAINS_FILE, calls, IMPORTS that connect code entities.
|
|
101
|
+
|
|
102
|
+
Query types available: graph_status, summary, get_node, search, list_nodes, function_calls_in, function_calls_out, definitions_in_file, file_imports, domain_map, domain_membership, neighborhood, jq
|
|
103
|
+
`,
|
|
18
104
|
inputSchema: {
|
|
19
105
|
type: 'object',
|
|
20
106
|
properties: {
|
|
21
|
-
|
|
107
|
+
directory: {
|
|
22
108
|
type: 'string',
|
|
23
|
-
description: 'Path to the
|
|
109
|
+
description: 'Path to the repository directory to analyze. Can be a subdirectory for faster analysis and smaller graph size (e.g., "/repo/src/core" instead of "/repo").',
|
|
24
110
|
},
|
|
25
111
|
'Idempotency-Key': {
|
|
26
112
|
type: 'string',
|
|
27
|
-
description: '
|
|
113
|
+
description: 'Optional cache key in format {repo}:{type}:{hash}. If not provided, will be auto-generated using git commit hash or random UUID. Provide a previously used idempotency key to fetch a cached response, for example with a different filter.',
|
|
114
|
+
},
|
|
115
|
+
query: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
enum: [
|
|
118
|
+
'graph_status', 'summary', 'get_node', 'search', 'list_nodes',
|
|
119
|
+
'function_calls_in', 'function_calls_out', 'definitions_in_file',
|
|
120
|
+
'file_imports', 'domain_map', 'domain_membership', 'neighborhood', 'jq'
|
|
121
|
+
],
|
|
122
|
+
description: 'Query type to execute. Use graph_status first to check cache, then summary to load.',
|
|
123
|
+
},
|
|
124
|
+
targetId: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'Node ID for queries that operate on a specific node (get_node, function_calls_*, etc.)',
|
|
127
|
+
},
|
|
128
|
+
searchText: {
|
|
129
|
+
type: 'string',
|
|
130
|
+
description: 'Search text for name substring matching (search, domain_membership)',
|
|
131
|
+
},
|
|
132
|
+
namePattern: {
|
|
133
|
+
type: 'string',
|
|
134
|
+
description: 'Regex pattern for name matching (list_nodes)',
|
|
135
|
+
},
|
|
136
|
+
filePathPrefix: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: 'Filter by file path prefix (list_nodes, definitions_in_file, search)',
|
|
139
|
+
},
|
|
140
|
+
labels: {
|
|
141
|
+
type: 'array',
|
|
142
|
+
items: { type: 'string' },
|
|
143
|
+
description: 'Filter by node labels: Function, Class, Type, File, Domain, etc. (list_nodes, search)',
|
|
144
|
+
},
|
|
145
|
+
depth: {
|
|
146
|
+
type: 'number',
|
|
147
|
+
description: 'Traversal depth for neighborhood query (default 1, max 3)',
|
|
148
|
+
},
|
|
149
|
+
relationshipTypes: {
|
|
150
|
+
type: 'array',
|
|
151
|
+
items: { type: 'string' },
|
|
152
|
+
description: 'Relationship types to traverse (neighborhood). Options: calls, IMPORTS',
|
|
153
|
+
},
|
|
154
|
+
limit: {
|
|
155
|
+
type: 'number',
|
|
156
|
+
description: 'Max results to return (default 200)',
|
|
157
|
+
},
|
|
158
|
+
includeRaw: {
|
|
159
|
+
type: 'boolean',
|
|
160
|
+
description: 'Include full raw node data in get_node response (default false)',
|
|
28
161
|
},
|
|
29
162
|
jq_filter: {
|
|
30
163
|
type: 'string',
|
|
31
164
|
title: 'jq Filter',
|
|
32
|
-
description: '
|
|
165
|
+
description: 'Raw jq filter for escape hatch queries or legacy mode (when query param not specified)',
|
|
33
166
|
},
|
|
34
167
|
},
|
|
35
|
-
required: ['
|
|
168
|
+
required: ['directory'],
|
|
36
169
|
},
|
|
37
170
|
};
|
|
171
|
+
/**
|
|
172
|
+
* Generate an idempotency key in format {repo}:supermodel:{hash}
|
|
173
|
+
* Tries to use git commit hash, falls back to UUID-based hash
|
|
174
|
+
*/
|
|
175
|
+
function generateIdempotencyKey(directory) {
|
|
176
|
+
const repoName = (0, path_1.basename)(directory);
|
|
177
|
+
let hash;
|
|
178
|
+
let statusHash = '';
|
|
179
|
+
try {
|
|
180
|
+
// Try to get git commit hash
|
|
181
|
+
hash = (0, child_process_1.execSync)('git rev-parse --short HEAD', {
|
|
182
|
+
cwd: directory,
|
|
183
|
+
encoding: 'utf-8',
|
|
184
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
185
|
+
}).trim();
|
|
186
|
+
try {
|
|
187
|
+
// Get git status to detect uncommitted changes
|
|
188
|
+
const statusOutput = (0, child_process_1.execSync)('git status --porcelain', {
|
|
189
|
+
cwd: directory,
|
|
190
|
+
encoding: 'utf-8',
|
|
191
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
192
|
+
}).trim();
|
|
193
|
+
statusHash = (0, crypto_2.createHash)('sha1').update(statusOutput || 'clean').digest('hex').substring(0, 7);
|
|
194
|
+
console.error('[DEBUG] Generated idempotency key using git hash:', hash, 'and status hash:', statusHash);
|
|
195
|
+
}
|
|
196
|
+
catch (statusError) {
|
|
197
|
+
// If git status fails, just use commit hash
|
|
198
|
+
console.error('[DEBUG] Generated idempotency key using git hash:', hash, '(git status unavailable)');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
// Git not available or not a git repo, use UUID-based hash
|
|
203
|
+
const uuid = (0, crypto_1.randomUUID)();
|
|
204
|
+
// Hash like git does (SHA-1) and take first 7 characters
|
|
205
|
+
hash = (0, crypto_2.createHash)('sha1').update(uuid).digest('hex').substring(0, 7);
|
|
206
|
+
console.error('[DEBUG] Generated idempotency key using random UUID hash:', hash);
|
|
207
|
+
}
|
|
208
|
+
return statusHash ? `${repoName}:supermodel:${hash}-${statusHash}` : `${repoName}:supermodel:${hash}`;
|
|
209
|
+
}
|
|
38
210
|
const handler = async (client, args) => {
|
|
39
211
|
if (!args) {
|
|
40
212
|
return (0, types_1.asErrorResult)('No arguments provided');
|
|
41
213
|
}
|
|
42
|
-
const { jq_filter,
|
|
43
|
-
|
|
44
|
-
|
|
214
|
+
const { jq_filter, directory, 'Idempotency-Key': providedIdempotencyKey, query, targetId, searchText, namePattern, filePathPrefix, labels, depth, relationshipTypes, limit, includeRaw, } = args;
|
|
215
|
+
// Validate directory
|
|
216
|
+
if (!directory || typeof directory !== 'string') {
|
|
217
|
+
return (0, types_1.asErrorResult)('Directory argument is required and must be a string path');
|
|
218
|
+
}
|
|
219
|
+
// Generate or validate idempotency key
|
|
220
|
+
let idempotencyKey;
|
|
221
|
+
let keyGenerated = false;
|
|
222
|
+
if (!providedIdempotencyKey || typeof providedIdempotencyKey !== 'string') {
|
|
223
|
+
idempotencyKey = generateIdempotencyKey(directory);
|
|
224
|
+
keyGenerated = true;
|
|
225
|
+
console.error('[DEBUG] Auto-generated idempotency key:', idempotencyKey);
|
|
45
226
|
}
|
|
46
|
-
|
|
47
|
-
|
|
227
|
+
else {
|
|
228
|
+
idempotencyKey = providedIdempotencyKey;
|
|
229
|
+
console.error('[DEBUG] Using provided idempotency key:', idempotencyKey);
|
|
48
230
|
}
|
|
231
|
+
// Check if we can skip zipping (graph already cached)
|
|
232
|
+
// Use get() atomically to avoid TOCTOU race condition
|
|
233
|
+
const cachedGraph = queries_1.graphCache.get(idempotencyKey);
|
|
234
|
+
if (cachedGraph && query) {
|
|
235
|
+
console.error('[DEBUG] Graph cached, skipping ZIP creation');
|
|
236
|
+
// Execute query directly from cache using the cached graph
|
|
237
|
+
// We pass the cached graph to executeQuery so it doesn't need to look it up again
|
|
238
|
+
const result = await handleQueryModeWithCache(client, {
|
|
239
|
+
query: query,
|
|
240
|
+
idempotencyKey,
|
|
241
|
+
cachedGraph,
|
|
242
|
+
targetId,
|
|
243
|
+
searchText,
|
|
244
|
+
namePattern,
|
|
245
|
+
filePathPrefix,
|
|
246
|
+
labels,
|
|
247
|
+
depth,
|
|
248
|
+
relationshipTypes,
|
|
249
|
+
limit,
|
|
250
|
+
includeRaw,
|
|
251
|
+
jq_filter,
|
|
252
|
+
});
|
|
253
|
+
// Add metadata about cache hit
|
|
254
|
+
if (keyGenerated && result.content?.[0]?.type === 'text') {
|
|
255
|
+
const originalText = result.content[0].text;
|
|
256
|
+
let responseData;
|
|
257
|
+
try {
|
|
258
|
+
responseData = JSON.parse(originalText);
|
|
259
|
+
// Add metadata about auto-generated key
|
|
260
|
+
responseData._metadata = {
|
|
261
|
+
...responseData._metadata,
|
|
262
|
+
idempotencyKey,
|
|
263
|
+
idempotencyKeyGenerated: true
|
|
264
|
+
};
|
|
265
|
+
result.content[0].text = JSON.stringify(responseData, null, 2);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// Not JSON, prepend key info as text
|
|
269
|
+
result.content[0].text = `[Auto-generated Idempotency-Key: ${idempotencyKey}]\n\n${originalText}`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
console.error('[DEBUG] Auto-zipping directory:', directory);
|
|
275
|
+
// Handle auto-zipping
|
|
276
|
+
let zipPath;
|
|
277
|
+
let cleanup = null;
|
|
49
278
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
279
|
+
const zipResult = await (0, zip_repository_1.zipRepository)(directory);
|
|
280
|
+
zipPath = zipResult.path;
|
|
281
|
+
cleanup = zipResult.cleanup;
|
|
282
|
+
console.error('[DEBUG] Auto-zip complete:', zipResult.fileCount, 'files,', formatBytes(zipResult.sizeBytes));
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
console.error('[ERROR] Auto-zip failed:', error.message);
|
|
286
|
+
// Provide helpful error messages
|
|
287
|
+
if (error.message.includes('does not exist')) {
|
|
288
|
+
return (0, types_1.asErrorResult)(`Directory does not exist: ${directory}`);
|
|
289
|
+
}
|
|
290
|
+
if (error.message.includes('Permission denied')) {
|
|
291
|
+
return (0, types_1.asErrorResult)(`Permission denied accessing directory: ${directory}`);
|
|
292
|
+
}
|
|
293
|
+
if (error.message.includes('exceeds limit')) {
|
|
294
|
+
return (0, types_1.asErrorResult)(error.message);
|
|
295
|
+
}
|
|
296
|
+
if (error.message.includes('ENOSPC')) {
|
|
297
|
+
return (0, types_1.asErrorResult)('Insufficient disk space to create ZIP archive');
|
|
298
|
+
}
|
|
299
|
+
return (0, types_1.asErrorResult)(`Failed to create ZIP archive: ${error.message}`);
|
|
300
|
+
}
|
|
301
|
+
// Execute query with cleanup handling
|
|
302
|
+
try {
|
|
303
|
+
let result;
|
|
304
|
+
// If query param is specified, use the new query engine
|
|
305
|
+
if (query) {
|
|
306
|
+
result = await handleQueryMode(client, {
|
|
307
|
+
query: query,
|
|
308
|
+
file: zipPath,
|
|
309
|
+
idempotencyKey,
|
|
310
|
+
targetId,
|
|
311
|
+
searchText,
|
|
312
|
+
namePattern,
|
|
313
|
+
filePathPrefix,
|
|
314
|
+
labels,
|
|
315
|
+
depth,
|
|
316
|
+
relationshipTypes,
|
|
317
|
+
limit,
|
|
318
|
+
includeRaw,
|
|
319
|
+
jq_filter,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
// Legacy mode: use jq_filter directly on API response
|
|
324
|
+
result = await handleLegacyMode(client, zipPath, idempotencyKey, jq_filter);
|
|
325
|
+
}
|
|
326
|
+
// If key was auto-generated, add it to the response
|
|
327
|
+
if (keyGenerated && result.content && result.content[0]?.type === 'text') {
|
|
328
|
+
const originalText = result.content[0].text;
|
|
329
|
+
let responseData;
|
|
330
|
+
try {
|
|
331
|
+
responseData = JSON.parse(originalText);
|
|
332
|
+
// Add metadata about auto-generated key
|
|
333
|
+
responseData._metadata = {
|
|
334
|
+
...responseData._metadata,
|
|
335
|
+
idempotencyKey,
|
|
336
|
+
idempotencyKeyGenerated: true
|
|
337
|
+
};
|
|
338
|
+
result.content[0].text = JSON.stringify(responseData, null, 2);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
// Not JSON, prepend key info as text
|
|
342
|
+
result.content[0].text = `[Auto-generated Idempotency-Key: ${idempotencyKey}]\n\n${originalText}`;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
// Always cleanup temp ZIP files
|
|
349
|
+
if (cleanup) {
|
|
350
|
+
await cleanup();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
exports.handler = handler;
|
|
355
|
+
/**
|
|
356
|
+
* Format bytes as human-readable string
|
|
357
|
+
*/
|
|
358
|
+
function formatBytes(bytes) {
|
|
359
|
+
if (bytes < 1024)
|
|
360
|
+
return `${bytes} B`;
|
|
361
|
+
if (bytes < 1024 * 1024)
|
|
362
|
+
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
363
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
364
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
365
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Handle query-based requests when graph is already cached
|
|
369
|
+
* Uses the cached graph directly to avoid TOCTOU issues
|
|
370
|
+
*/
|
|
371
|
+
async function handleQueryModeWithCache(client, params) {
|
|
372
|
+
const queryParams = {
|
|
373
|
+
query: params.query,
|
|
374
|
+
file: '', // Not used when we have cached graph
|
|
375
|
+
idempotencyKey: params.idempotencyKey,
|
|
376
|
+
targetId: params.targetId,
|
|
377
|
+
searchText: params.searchText,
|
|
378
|
+
namePattern: params.namePattern,
|
|
379
|
+
filePathPrefix: params.filePathPrefix,
|
|
380
|
+
labels: params.labels,
|
|
381
|
+
depth: params.depth,
|
|
382
|
+
relationshipTypes: params.relationshipTypes,
|
|
383
|
+
limit: params.limit,
|
|
384
|
+
includeRaw: params.includeRaw,
|
|
385
|
+
jq_filter: params.jq_filter,
|
|
386
|
+
};
|
|
387
|
+
// Execute query with the cached graph's raw data
|
|
388
|
+
// This handles the edge case where cache is evicted between our check and query execution
|
|
389
|
+
// by passing the raw API response so executeQuery can rebuild indexes if needed
|
|
390
|
+
let result = await (0, queries_1.executeQuery)(queryParams, params.cachedGraph.raw);
|
|
391
|
+
// Handle query errors
|
|
392
|
+
if ((0, queries_1.isQueryError)(result)) {
|
|
393
|
+
const errorWithHints = {
|
|
394
|
+
...result,
|
|
395
|
+
hints: getErrorHints(result.error.code, params.query),
|
|
396
|
+
};
|
|
397
|
+
return (0, types_1.asTextContentResult)(errorWithHints);
|
|
398
|
+
}
|
|
399
|
+
// Add breadcrumb hints to successful results
|
|
400
|
+
const resultWithHints = addBreadcrumbHints(result, params.query);
|
|
401
|
+
return (0, types_1.asTextContentResult)(resultWithHints);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Handle query-based requests using the query engine
|
|
405
|
+
*/
|
|
406
|
+
async function handleQueryMode(client, params) {
|
|
407
|
+
const queryParams = {
|
|
408
|
+
query: params.query,
|
|
409
|
+
file: params.file,
|
|
410
|
+
idempotencyKey: params.idempotencyKey,
|
|
411
|
+
targetId: params.targetId,
|
|
412
|
+
searchText: params.searchText,
|
|
413
|
+
namePattern: params.namePattern,
|
|
414
|
+
filePathPrefix: params.filePathPrefix,
|
|
415
|
+
labels: params.labels,
|
|
416
|
+
depth: params.depth,
|
|
417
|
+
relationshipTypes: params.relationshipTypes,
|
|
418
|
+
limit: params.limit,
|
|
419
|
+
includeRaw: params.includeRaw,
|
|
420
|
+
jq_filter: params.jq_filter,
|
|
421
|
+
};
|
|
422
|
+
// First, try to execute query from cache
|
|
423
|
+
let result = await (0, queries_1.executeQuery)(queryParams);
|
|
424
|
+
// If cache miss, fetch from API and retry
|
|
425
|
+
if ((0, queries_1.isQueryError)(result) && result.error.code === 'CACHE_MISS') {
|
|
426
|
+
console.error('[DEBUG] Cache miss, fetching from API...');
|
|
427
|
+
try {
|
|
428
|
+
const apiResponse = await fetchFromApi(client, params.file, params.idempotencyKey);
|
|
429
|
+
result = await (0, queries_1.executeQuery)(queryParams, apiResponse);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
return (0, types_1.asErrorResult)(`API call failed: ${error.message || String(error)}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Handle query errors
|
|
436
|
+
if ((0, queries_1.isQueryError)(result)) {
|
|
437
|
+
// Include hints for common errors
|
|
438
|
+
const errorWithHints = {
|
|
439
|
+
...result,
|
|
440
|
+
hints: getErrorHints(result.error.code, params.query),
|
|
63
441
|
};
|
|
64
|
-
|
|
65
|
-
|
|
442
|
+
return (0, types_1.asTextContentResult)(errorWithHints);
|
|
443
|
+
}
|
|
444
|
+
// Add breadcrumb hints to successful results
|
|
445
|
+
const resultWithHints = addBreadcrumbHints(result, params.query);
|
|
446
|
+
return (0, types_1.asTextContentResult)(resultWithHints);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Add breadcrumb hints to query results for agent navigation
|
|
450
|
+
*/
|
|
451
|
+
function addBreadcrumbHints(result, queryType) {
|
|
452
|
+
const hints = [];
|
|
453
|
+
switch (queryType) {
|
|
454
|
+
case 'summary':
|
|
455
|
+
hints.push('NEXT: Use search with searchText to find specific functions/classes', 'NEXT: Use list_nodes with labels=["Function"] to browse all functions', 'NEXT: Use domain_map to see architectural domains');
|
|
456
|
+
break;
|
|
457
|
+
case 'search':
|
|
458
|
+
case 'list_nodes':
|
|
459
|
+
if (result.result?.nodes?.length > 0) {
|
|
460
|
+
hints.push('NEXT: Use get_node with targetId to get full details for any node', 'NEXT: Use function_calls_in with targetId to see who calls a function', 'NEXT: Use function_calls_out with targetId to see what a function calls');
|
|
461
|
+
}
|
|
462
|
+
break;
|
|
463
|
+
case 'get_node':
|
|
464
|
+
if (result.result?.node) {
|
|
465
|
+
const label = result.result.node.labels?.[0];
|
|
466
|
+
if (label === 'Function') {
|
|
467
|
+
hints.push('NEXT: Use function_calls_in to see callers of this function', 'NEXT: Use function_calls_out to see functions this calls', 'NEXT: Use neighborhood with depth=2 to see call graph around this function');
|
|
468
|
+
}
|
|
469
|
+
else if (label === 'File') {
|
|
470
|
+
hints.push('NEXT: Use definitions_in_file to see all definitions in this file', 'NEXT: Use file_imports to see import relationships');
|
|
471
|
+
}
|
|
472
|
+
else if (label === 'Domain') {
|
|
473
|
+
hints.push('NEXT: Use domain_membership to see all members of this domain');
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
break;
|
|
477
|
+
case 'function_calls_in':
|
|
478
|
+
case 'function_calls_out':
|
|
479
|
+
if (result.result?.nodes?.length > 0) {
|
|
480
|
+
hints.push('NEXT: Use get_node with any caller/callee ID for full details', 'NEXT: Chain function_calls_in/out to trace deeper call paths', 'NEXT: Use neighborhood for broader call graph exploration');
|
|
481
|
+
}
|
|
482
|
+
break;
|
|
483
|
+
case 'definitions_in_file':
|
|
484
|
+
hints.push('NEXT: Use function_calls_in/out on any function ID to trace calls', 'NEXT: Use get_node for full details on any definition');
|
|
485
|
+
break;
|
|
486
|
+
case 'domain_map':
|
|
487
|
+
hints.push('NEXT: Use domain_membership with domain name to see members', 'NEXT: Use search to find specific functions within a domain');
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
if (hints.length > 0) {
|
|
491
|
+
return { ...result, hints };
|
|
492
|
+
}
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Get hints for specific error conditions
|
|
497
|
+
*/
|
|
498
|
+
function getErrorHints(errorCode, queryType) {
|
|
499
|
+
switch (errorCode) {
|
|
500
|
+
case 'NOT_FOUND':
|
|
501
|
+
return [
|
|
502
|
+
'Use search with searchText to find nodes by name',
|
|
503
|
+
'Use list_nodes with labels filter to browse available nodes',
|
|
504
|
+
'Check the targetId format - it should be the full node ID from a previous query'
|
|
505
|
+
];
|
|
506
|
+
case 'INVALID_PARAMS':
|
|
507
|
+
return [
|
|
508
|
+
`Query '${queryType}' may require specific parameters`,
|
|
509
|
+
'Use graph_status to see available query types and their requirements'
|
|
510
|
+
];
|
|
511
|
+
default:
|
|
512
|
+
return [];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Fetch graph from API
|
|
517
|
+
*/
|
|
518
|
+
async function fetchFromApi(client, file, idempotencyKey) {
|
|
519
|
+
console.error('[DEBUG] Reading file:', file);
|
|
520
|
+
const fileBuffer = await (0, promises_1.readFile)(file);
|
|
521
|
+
const fileBlob = new Blob([fileBuffer], { type: 'application/zip' });
|
|
522
|
+
console.error('[DEBUG] File size:', fileBuffer.length, 'bytes');
|
|
523
|
+
console.error('[DEBUG] Making API request with idempotency key:', idempotencyKey);
|
|
524
|
+
const requestParams = {
|
|
525
|
+
file: fileBlob,
|
|
526
|
+
idempotencyKey: idempotencyKey,
|
|
527
|
+
};
|
|
528
|
+
const response = await client.graphs.generateSupermodelGraph(requestParams);
|
|
529
|
+
console.error('[DEBUG] API request successful');
|
|
530
|
+
return response;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Legacy mode: direct jq filtering on API response
|
|
534
|
+
*/
|
|
535
|
+
async function handleLegacyMode(client, file, idempotencyKey, jq_filter) {
|
|
536
|
+
try {
|
|
537
|
+
const response = await fetchFromApi(client, file, idempotencyKey);
|
|
66
538
|
return (0, types_1.asTextContentResult)(await (0, filtering_1.maybeFilter)(jq_filter, response));
|
|
67
539
|
}
|
|
68
540
|
catch (error) {
|
|
@@ -91,6 +563,5 @@ const handler = async (client, args) => {
|
|
|
91
563
|
}
|
|
92
564
|
return (0, types_1.asErrorResult)(`API call failed: ${error.message || String(error)}. Check server logs for details.`);
|
|
93
565
|
}
|
|
94
|
-
}
|
|
95
|
-
exports.handler = handler;
|
|
566
|
+
}
|
|
96
567
|
exports.default = { metadata: exports.metadata, tool: exports.tool, handler: exports.handler };
|