regen-koi-mcp 1.0.4 → 1.0.6
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 +51 -0
- package/dist/graph_client.d.ts +204 -0
- package/dist/graph_client.d.ts.map +1 -0
- package/dist/graph_client.js +360 -0
- package/dist/graph_client.js.map +1 -0
- package/dist/graph_tool.d.ts +68 -0
- package/dist/graph_tool.d.ts.map +1 -0
- package/dist/graph_tool.js +656 -0
- package/dist/graph_tool.js.map +1 -0
- package/dist/hybrid-client.js +1 -1
- package/dist/hybrid-client.js.map +1 -1
- package/dist/index.js +695 -4
- package/dist/index.js.map +1 -1
- package/dist/query_router.d.ts +81 -0
- package/dist/query_router.d.ts.map +1 -0
- package/dist/query_router.js +205 -0
- package/dist/query_router.js.map +1 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +78 -0
- package/dist/tools.js.map +1 -1
- package/dist/unified_search.d.ts +109 -0
- package/dist/unified_search.d.ts.map +1 -0
- package/dist/unified_search.js +352 -0
- package/dist/unified_search.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph Tool - MCP Tool for Querying Regen Code Knowledge Graph
|
|
3
|
+
*
|
|
4
|
+
* Provides an MCP tool interface for querying the Apache AGE graph database
|
|
5
|
+
* containing Regen Network code entities and relationships.
|
|
6
|
+
*
|
|
7
|
+
* Supports two modes:
|
|
8
|
+
* 1. API mode: When KOI_API_ENDPOINT is set, uses HTTP API at /api/koi/graph
|
|
9
|
+
* 2. Direct mode: Connects directly to PostgreSQL (only works on server)
|
|
10
|
+
*/
|
|
11
|
+
import axios from 'axios';
|
|
12
|
+
import { createGraphClient, } from './graph_client.js';
|
|
13
|
+
// Check if we should use API mode
|
|
14
|
+
const KOI_API_ENDPOINT = process.env.KOI_API_ENDPOINT || '';
|
|
15
|
+
const USE_GRAPH_API = !!KOI_API_ENDPOINT;
|
|
16
|
+
// Tool definition following the pattern from tools.ts
|
|
17
|
+
export const GRAPH_TOOL = {
|
|
18
|
+
name: 'query_code_graph',
|
|
19
|
+
description: 'Query the Regen code knowledge graph to find code entities (Classes, Functions, Sensors, Handlers, Interfaces) and their relationships across repositories',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
query_type: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
enum: [
|
|
26
|
+
'keeper_for_msg', 'msgs_for_keeper', 'docs_mentioning', 'entities_in_doc',
|
|
27
|
+
'related_entities', 'find_by_type', 'search_entities', 'list_repos',
|
|
28
|
+
// RAPTOR module queries
|
|
29
|
+
'list_modules', 'get_module', 'search_modules', 'module_entities', 'module_for_entity'
|
|
30
|
+
],
|
|
31
|
+
description: 'Type of graph query: find_by_type (get all Sensors, Handlers, etc.), search_entities (search by name), list_repos (show indexed repositories), list_modules (show all modules), search_modules (search modules by keyword)'
|
|
32
|
+
},
|
|
33
|
+
entity_name: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Name or search term (e.g., "MsgCreateBatch", "Sensor", "Twitter")'
|
|
36
|
+
},
|
|
37
|
+
entity_type: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'Entity type for find_by_type query (e.g., "Sensor", "Handler", "Class", "Interface", "Function")'
|
|
40
|
+
},
|
|
41
|
+
doc_path: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Document path for doc-related queries'
|
|
44
|
+
},
|
|
45
|
+
repo_name: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: 'Repository name to filter by (e.g., "koi-sensors", "GAIA")'
|
|
48
|
+
},
|
|
49
|
+
module_name: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
description: 'Module name for module-related queries (e.g., "ecocredit", "cli", "sensors")'
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
required: ['query_type']
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Execute graph query via HTTP API
|
|
59
|
+
*/
|
|
60
|
+
async function executeViaApi(args) {
|
|
61
|
+
const graphApiUrl = `${KOI_API_ENDPOINT}/graph`;
|
|
62
|
+
try {
|
|
63
|
+
const response = await axios.post(graphApiUrl, args, {
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
timeout: 30000,
|
|
66
|
+
});
|
|
67
|
+
return response.data;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error.response) {
|
|
71
|
+
throw new Error(`API error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Execute the query_code_graph tool
|
|
78
|
+
*/
|
|
79
|
+
export async function executeGraphTool(args) {
|
|
80
|
+
const { query_type, entity_name, doc_path } = args;
|
|
81
|
+
// Validate required parameters
|
|
82
|
+
if (!query_type) {
|
|
83
|
+
return {
|
|
84
|
+
content: [{
|
|
85
|
+
type: 'text',
|
|
86
|
+
text: 'Error: query_type is required'
|
|
87
|
+
}]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Use API mode if KOI_API_ENDPOINT is set
|
|
91
|
+
if (USE_GRAPH_API) {
|
|
92
|
+
try {
|
|
93
|
+
const startTime = Date.now();
|
|
94
|
+
const apiResult = await executeViaApi(args);
|
|
95
|
+
const duration_ms = Date.now() - startTime;
|
|
96
|
+
// Format API results
|
|
97
|
+
const results = apiResult.results || [];
|
|
98
|
+
const total_results = results.length;
|
|
99
|
+
let markdownSummary = '';
|
|
100
|
+
let hits = [];
|
|
101
|
+
// Format based on query type
|
|
102
|
+
switch (query_type) {
|
|
103
|
+
case 'list_repos':
|
|
104
|
+
markdownSummary = `# Indexed Repositories\n\nFound **${total_results}** repositories:\n\n`;
|
|
105
|
+
markdownSummary += '| Repository | Entity Count |\n|------------|-------------|\n';
|
|
106
|
+
results.forEach((r) => {
|
|
107
|
+
const name = r.name || r.result?.name || 'unknown';
|
|
108
|
+
const count = r.entity_count || r.result?.entity_count || 0;
|
|
109
|
+
markdownSummary += `| ${name} | ${count} |\n`;
|
|
110
|
+
hits.push({ entity_type: 'Repository', entity_name: name, content_preview: `${count} entities` });
|
|
111
|
+
});
|
|
112
|
+
break;
|
|
113
|
+
case 'find_by_type':
|
|
114
|
+
markdownSummary = `# Entities of Type: ${args.entity_type}\n\nFound **${total_results}** ${args.entity_type}(s):\n\n`;
|
|
115
|
+
results.forEach((r, i) => {
|
|
116
|
+
const entity = r.result || r;
|
|
117
|
+
markdownSummary += `## ${i + 1}. ${entity.name}\n`;
|
|
118
|
+
markdownSummary += `- **File:** \`${entity.file_path || 'N/A'}\`\n`;
|
|
119
|
+
if (entity.line_number)
|
|
120
|
+
markdownSummary += `- **Line:** ${entity.line_number}\n`;
|
|
121
|
+
markdownSummary += '\n';
|
|
122
|
+
hits.push({
|
|
123
|
+
entity_type: args.entity_type,
|
|
124
|
+
entity_name: entity.name,
|
|
125
|
+
file_path: entity.file_path,
|
|
126
|
+
line_number: entity.line_number,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
break;
|
|
130
|
+
case 'search_entities':
|
|
131
|
+
markdownSummary = `# Search Results: "${entity_name}"\n\nFound **${total_results}** matching entities:\n\n`;
|
|
132
|
+
results.forEach((r, i) => {
|
|
133
|
+
const entity = r.result || r;
|
|
134
|
+
const label = entity.label || 'Entity';
|
|
135
|
+
markdownSummary += `## ${i + 1}. ${entity.name} (${label})\n`;
|
|
136
|
+
markdownSummary += `- **Repository:** ${entity.repo || 'N/A'}\n`;
|
|
137
|
+
markdownSummary += `- **File:** \`${entity.file_path || 'N/A'}\`\n`;
|
|
138
|
+
markdownSummary += '\n';
|
|
139
|
+
hits.push({
|
|
140
|
+
entity_type: label,
|
|
141
|
+
entity_name: entity.name,
|
|
142
|
+
file_path: entity.file_path,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
break;
|
|
146
|
+
default:
|
|
147
|
+
// Generic formatting for other query types
|
|
148
|
+
markdownSummary = `# ${query_type} Results\n\nFound **${total_results}** results:\n\n`;
|
|
149
|
+
results.forEach((r, i) => {
|
|
150
|
+
const entity = r.result || r;
|
|
151
|
+
markdownSummary += `${i + 1}. ${JSON.stringify(entity)}\n`;
|
|
152
|
+
hits.push({ entity_name: entity.name || 'unknown', content_preview: JSON.stringify(entity).substring(0, 100) });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const jsonData = JSON.stringify({ hits, metadata: { query_type, duration_ms, total_results, via: 'api' } }, null, 2);
|
|
156
|
+
return {
|
|
157
|
+
content: [{
|
|
158
|
+
type: 'text',
|
|
159
|
+
text: markdownSummary + '\n\n---\n\n<details>\n<summary>Raw JSON (for eval harness)</summary>\n\n```json\n' + jsonData + '\n```\n</details>'
|
|
160
|
+
}]
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.error('[query_code_graph] API Error:', error);
|
|
165
|
+
return {
|
|
166
|
+
content: [{
|
|
167
|
+
type: 'text',
|
|
168
|
+
text: `Error querying graph via API: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
169
|
+
}]
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Fall back to direct PostgreSQL connection (only works on server)
|
|
174
|
+
const client = createGraphClient();
|
|
175
|
+
try {
|
|
176
|
+
const startTime = Date.now();
|
|
177
|
+
// Execute query based on type
|
|
178
|
+
let markdownSummary = '';
|
|
179
|
+
let hits = [];
|
|
180
|
+
let total_results = 0;
|
|
181
|
+
switch (query_type) {
|
|
182
|
+
case 'keeper_for_msg':
|
|
183
|
+
if (!entity_name) {
|
|
184
|
+
return {
|
|
185
|
+
content: [{
|
|
186
|
+
type: 'text',
|
|
187
|
+
text: 'Error: entity_name is required for keeper_for_msg query'
|
|
188
|
+
}]
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const keeperResults = await client.getKeeperForMsg(entity_name);
|
|
192
|
+
total_results = keeperResults.length;
|
|
193
|
+
markdownSummary = formatKeeperForMsgResults(entity_name, keeperResults);
|
|
194
|
+
hits = keeperResults.map(r => ({
|
|
195
|
+
entity_type: 'Keeper',
|
|
196
|
+
entity_name: r.keeper_name,
|
|
197
|
+
file_path: r.keeper_file_path,
|
|
198
|
+
line_number: r.keeper_line_number,
|
|
199
|
+
content_preview: `${r.keeper_name} handles ${entity_name}`,
|
|
200
|
+
edges: [{ type: 'HANDLES', target: entity_name }]
|
|
201
|
+
}));
|
|
202
|
+
break;
|
|
203
|
+
case 'msgs_for_keeper':
|
|
204
|
+
if (!entity_name) {
|
|
205
|
+
return {
|
|
206
|
+
content: [{
|
|
207
|
+
type: 'text',
|
|
208
|
+
text: 'Error: entity_name is required for msgs_for_keeper query'
|
|
209
|
+
}]
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const msgResults = await client.getMsgsForKeeper(entity_name);
|
|
213
|
+
total_results = msgResults.length;
|
|
214
|
+
markdownSummary = formatMsgsForKeeperResults(entity_name, msgResults);
|
|
215
|
+
hits = msgResults.map(r => ({
|
|
216
|
+
entity_type: 'Msg',
|
|
217
|
+
entity_name: r.msg_name,
|
|
218
|
+
content_preview: r.msg_package ? `${r.msg_name} (${r.msg_package})` : r.msg_name,
|
|
219
|
+
edges: [{ type: 'HANDLED_BY', target: entity_name }]
|
|
220
|
+
}));
|
|
221
|
+
break;
|
|
222
|
+
case 'docs_mentioning':
|
|
223
|
+
if (!entity_name) {
|
|
224
|
+
return {
|
|
225
|
+
content: [{
|
|
226
|
+
type: 'text',
|
|
227
|
+
text: 'Error: entity_name is required for docs_mentioning query'
|
|
228
|
+
}]
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const docResults = await client.getDocsMentioning(entity_name);
|
|
232
|
+
total_results = docResults.length;
|
|
233
|
+
markdownSummary = formatDocsMentioningResults(entity_name, docResults);
|
|
234
|
+
hits = docResults.map(r => ({
|
|
235
|
+
entity_type: 'Document',
|
|
236
|
+
entity_name: r.title,
|
|
237
|
+
file_path: r.file_path,
|
|
238
|
+
content_preview: `Document mentions ${entity_name}`,
|
|
239
|
+
edges: [{ type: 'MENTIONS', target: entity_name }]
|
|
240
|
+
}));
|
|
241
|
+
break;
|
|
242
|
+
case 'entities_in_doc':
|
|
243
|
+
if (!doc_path) {
|
|
244
|
+
return {
|
|
245
|
+
content: [{
|
|
246
|
+
type: 'text',
|
|
247
|
+
text: 'Error: doc_path is required for entities_in_doc query'
|
|
248
|
+
}]
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const entityResults = await client.getEntitiesInDoc(doc_path);
|
|
252
|
+
total_results = entityResults.length;
|
|
253
|
+
markdownSummary = formatEntitiesInDocResults(doc_path, entityResults);
|
|
254
|
+
hits = entityResults.map(r => ({
|
|
255
|
+
entity_type: r.type,
|
|
256
|
+
entity_name: r.name,
|
|
257
|
+
content_preview: `${r.type}: ${r.name}`,
|
|
258
|
+
edges: [{ type: 'MENTIONED_IN', target: doc_path }]
|
|
259
|
+
}));
|
|
260
|
+
break;
|
|
261
|
+
case 'related_entities':
|
|
262
|
+
if (!entity_name) {
|
|
263
|
+
return {
|
|
264
|
+
content: [{
|
|
265
|
+
type: 'text',
|
|
266
|
+
text: 'Error: entity_name is required for related_entities query'
|
|
267
|
+
}]
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const relatedResults = await client.getRelatedEntities(entity_name);
|
|
271
|
+
total_results = relatedResults.length;
|
|
272
|
+
markdownSummary = formatRelatedEntitiesResults(entity_name, relatedResults);
|
|
273
|
+
hits = relatedResults.map(r => ({
|
|
274
|
+
entity_type: r.type,
|
|
275
|
+
entity_name: r.name,
|
|
276
|
+
score: r.shared_docs ? r.shared_docs / 10 : 0.5, // Normalize score
|
|
277
|
+
content_preview: `${r.type}: ${r.name} (${r.shared_docs || 0} shared docs)`,
|
|
278
|
+
edges: [{ type: 'RELATED_VIA_DOCS', target: entity_name }]
|
|
279
|
+
}));
|
|
280
|
+
break;
|
|
281
|
+
case 'find_by_type':
|
|
282
|
+
const entityType = args.entity_type;
|
|
283
|
+
if (!entityType) {
|
|
284
|
+
return {
|
|
285
|
+
content: [{
|
|
286
|
+
type: 'text',
|
|
287
|
+
text: 'Error: entity_type is required for find_by_type query'
|
|
288
|
+
}]
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const typeResults = await client.getEntitiesByType(entityType);
|
|
292
|
+
total_results = typeResults.length;
|
|
293
|
+
markdownSummary = `# Entities of Type: ${entityType}\n\nFound **${total_results}** ${entityType}(s):\n\n`;
|
|
294
|
+
typeResults.forEach((r, i) => {
|
|
295
|
+
markdownSummary += `## ${i + 1}. ${r.name}\n`;
|
|
296
|
+
markdownSummary += `- **File:** \`${r.file_path || 'N/A'}\`\n`;
|
|
297
|
+
if (r.line_number)
|
|
298
|
+
markdownSummary += `- **Line:** ${r.line_number}\n`;
|
|
299
|
+
markdownSummary += '\n';
|
|
300
|
+
});
|
|
301
|
+
hits = typeResults.map(r => ({
|
|
302
|
+
entity_type: r.type,
|
|
303
|
+
entity_name: r.name,
|
|
304
|
+
file_path: r.file_path,
|
|
305
|
+
line_number: r.line_number,
|
|
306
|
+
content_preview: r.docstring || `${r.type}: ${r.name}`
|
|
307
|
+
}));
|
|
308
|
+
break;
|
|
309
|
+
case 'search_entities':
|
|
310
|
+
const searchTerm = entity_name;
|
|
311
|
+
if (!searchTerm) {
|
|
312
|
+
return {
|
|
313
|
+
content: [{
|
|
314
|
+
type: 'text',
|
|
315
|
+
text: 'Error: entity_name (search term) is required for search_entities query'
|
|
316
|
+
}]
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const searchResults = await client.findEntitiesByName(searchTerm);
|
|
320
|
+
total_results = searchResults.length;
|
|
321
|
+
markdownSummary = `# Search Results: "${searchTerm}"\n\nFound **${total_results}** matching entities:\n\n`;
|
|
322
|
+
searchResults.forEach((r, i) => {
|
|
323
|
+
markdownSummary += `## ${i + 1}. ${r.name} (${r.type})\n`;
|
|
324
|
+
markdownSummary += `- **Repository:** ${r.repo || 'N/A'}\n`;
|
|
325
|
+
markdownSummary += `- **File:** \`${r.file_path || 'N/A'}\`\n`;
|
|
326
|
+
if (r.line_number)
|
|
327
|
+
markdownSummary += `- **Line:** ${r.line_number}\n`;
|
|
328
|
+
markdownSummary += '\n';
|
|
329
|
+
});
|
|
330
|
+
hits = searchResults.map((r) => ({
|
|
331
|
+
entity_type: r.type,
|
|
332
|
+
entity_name: r.name,
|
|
333
|
+
file_path: r.file_path,
|
|
334
|
+
line_number: r.line_number,
|
|
335
|
+
content_preview: r.docstring || `${r.type}: ${r.name}`
|
|
336
|
+
}));
|
|
337
|
+
break;
|
|
338
|
+
case 'list_repos':
|
|
339
|
+
const repoResults = await client.getRepositories();
|
|
340
|
+
total_results = repoResults.length;
|
|
341
|
+
markdownSummary = `# Indexed Repositories\n\nFound **${total_results}** repositories:\n\n`;
|
|
342
|
+
markdownSummary += '| Repository | Entity Count |\n|------------|-------------|\n';
|
|
343
|
+
repoResults.forEach(r => {
|
|
344
|
+
markdownSummary += `| ${r.name} | ${r.entity_count} |\n`;
|
|
345
|
+
});
|
|
346
|
+
hits = repoResults.map(r => ({
|
|
347
|
+
entity_type: 'Repository',
|
|
348
|
+
entity_name: r.name,
|
|
349
|
+
content_preview: `${r.entity_count} entities`
|
|
350
|
+
}));
|
|
351
|
+
break;
|
|
352
|
+
// ============= RAPTOR Module Queries =============
|
|
353
|
+
case 'list_modules':
|
|
354
|
+
const moduleResults = await client.getAllModules();
|
|
355
|
+
total_results = moduleResults.length;
|
|
356
|
+
markdownSummary = `# All Modules (RAPTOR)\n\nFound **${total_results}** modules:\n\n`;
|
|
357
|
+
markdownSummary += '| Module | Repository | Path | Entities |\n|--------|------------|------|----------|\n';
|
|
358
|
+
moduleResults.forEach(m => {
|
|
359
|
+
markdownSummary += `| ${m.name} | ${m.repo} | ${m.path} | ${m.entity_count} |\n`;
|
|
360
|
+
});
|
|
361
|
+
hits = moduleResults.map(m => ({
|
|
362
|
+
entity_type: 'Module',
|
|
363
|
+
entity_name: m.name,
|
|
364
|
+
file_path: m.path,
|
|
365
|
+
content_preview: m.summary ? m.summary.substring(0, 200) + '...' : `${m.entity_count} entities in ${m.repo}`
|
|
366
|
+
}));
|
|
367
|
+
break;
|
|
368
|
+
case 'get_module':
|
|
369
|
+
const moduleName = args.module_name;
|
|
370
|
+
const repoFilter = args.repo_name;
|
|
371
|
+
if (!moduleName) {
|
|
372
|
+
return {
|
|
373
|
+
content: [{
|
|
374
|
+
type: 'text',
|
|
375
|
+
text: 'Error: module_name is required for get_module query'
|
|
376
|
+
}]
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const moduleResult = await client.getModule(moduleName, repoFilter);
|
|
380
|
+
if (!moduleResult) {
|
|
381
|
+
markdownSummary = `# Module Not Found: ${moduleName}\n\nNo module named **${moduleName}** was found.\n`;
|
|
382
|
+
total_results = 0;
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
total_results = 1;
|
|
386
|
+
markdownSummary = `# Module: ${moduleResult.name}\n\n`;
|
|
387
|
+
markdownSummary += `- **Repository:** ${moduleResult.repo}\n`;
|
|
388
|
+
markdownSummary += `- **Path:** \`${moduleResult.path}\`\n`;
|
|
389
|
+
markdownSummary += `- **Entity Count:** ${moduleResult.entity_count}\n\n`;
|
|
390
|
+
if (moduleResult.summary) {
|
|
391
|
+
markdownSummary += `## Summary\n\n${moduleResult.summary}\n`;
|
|
392
|
+
}
|
|
393
|
+
hits = [{
|
|
394
|
+
entity_type: 'Module',
|
|
395
|
+
entity_name: moduleResult.name,
|
|
396
|
+
file_path: moduleResult.path,
|
|
397
|
+
content_preview: moduleResult.summary || `${moduleResult.entity_count} entities`
|
|
398
|
+
}];
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
case 'search_modules':
|
|
402
|
+
const searchPattern = entity_name;
|
|
403
|
+
if (!searchPattern) {
|
|
404
|
+
return {
|
|
405
|
+
content: [{
|
|
406
|
+
type: 'text',
|
|
407
|
+
text: 'Error: entity_name (search pattern) is required for search_modules query'
|
|
408
|
+
}]
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const moduleSearchResults = await client.searchModules(searchPattern);
|
|
412
|
+
total_results = moduleSearchResults.length;
|
|
413
|
+
markdownSummary = `# Module Search: "${searchPattern}"\n\nFound **${total_results}** matching modules:\n\n`;
|
|
414
|
+
moduleSearchResults.forEach((m, i) => {
|
|
415
|
+
markdownSummary += `## ${i + 1}. ${m.name} (${m.repo})\n`;
|
|
416
|
+
markdownSummary += `- **Path:** \`${m.path}\`\n`;
|
|
417
|
+
markdownSummary += `- **Entities:** ${m.entity_count}\n`;
|
|
418
|
+
if (m.summary) {
|
|
419
|
+
markdownSummary += `\n${m.summary.substring(0, 300)}...\n`;
|
|
420
|
+
}
|
|
421
|
+
markdownSummary += '\n';
|
|
422
|
+
});
|
|
423
|
+
hits = moduleSearchResults.map(m => ({
|
|
424
|
+
entity_type: 'Module',
|
|
425
|
+
entity_name: m.name,
|
|
426
|
+
file_path: m.path,
|
|
427
|
+
content_preview: m.summary || `${m.entity_count} entities in ${m.repo}`
|
|
428
|
+
}));
|
|
429
|
+
break;
|
|
430
|
+
case 'module_entities':
|
|
431
|
+
const targetModule = args.module_name;
|
|
432
|
+
const targetRepo = args.repo_name;
|
|
433
|
+
if (!targetModule) {
|
|
434
|
+
return {
|
|
435
|
+
content: [{
|
|
436
|
+
type: 'text',
|
|
437
|
+
text: 'Error: module_name is required for module_entities query'
|
|
438
|
+
}]
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const moduleEntities = await client.getModuleEntities(targetModule, targetRepo);
|
|
442
|
+
total_results = moduleEntities.length;
|
|
443
|
+
markdownSummary = `# Entities in Module: ${targetModule}\n\nFound **${total_results}** entities:\n\n`;
|
|
444
|
+
// Group by type
|
|
445
|
+
const byType = {};
|
|
446
|
+
moduleEntities.forEach(e => {
|
|
447
|
+
if (!byType[e.type])
|
|
448
|
+
byType[e.type] = [];
|
|
449
|
+
byType[e.type].push(e);
|
|
450
|
+
});
|
|
451
|
+
Object.keys(byType).forEach(type => {
|
|
452
|
+
markdownSummary += `## ${type}s (${byType[type].length})\n\n`;
|
|
453
|
+
byType[type].forEach(e => {
|
|
454
|
+
markdownSummary += `- **${e.name}**`;
|
|
455
|
+
if (e.file_path)
|
|
456
|
+
markdownSummary += ` (\`${e.file_path}\`)`;
|
|
457
|
+
markdownSummary += '\n';
|
|
458
|
+
});
|
|
459
|
+
markdownSummary += '\n';
|
|
460
|
+
});
|
|
461
|
+
hits = moduleEntities.map(e => ({
|
|
462
|
+
entity_type: e.type,
|
|
463
|
+
entity_name: e.name,
|
|
464
|
+
file_path: e.file_path,
|
|
465
|
+
line_number: e.line_number
|
|
466
|
+
}));
|
|
467
|
+
break;
|
|
468
|
+
case 'module_for_entity':
|
|
469
|
+
const entityToFind = entity_name;
|
|
470
|
+
if (!entityToFind) {
|
|
471
|
+
return {
|
|
472
|
+
content: [{
|
|
473
|
+
type: 'text',
|
|
474
|
+
text: 'Error: entity_name is required for module_for_entity query'
|
|
475
|
+
}]
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const containingModule = await client.getModuleForEntity(entityToFind);
|
|
479
|
+
if (!containingModule) {
|
|
480
|
+
markdownSummary = `# Module for Entity: ${entityToFind}\n\nNo module found containing **${entityToFind}**.\n`;
|
|
481
|
+
total_results = 0;
|
|
482
|
+
hits = [];
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
total_results = 1;
|
|
486
|
+
markdownSummary = `# Module for Entity: ${entityToFind}\n\n`;
|
|
487
|
+
markdownSummary += `**${entityToFind}** is part of the **${containingModule.name}** module.\n\n`;
|
|
488
|
+
markdownSummary += `- **Repository:** ${containingModule.repo}\n`;
|
|
489
|
+
markdownSummary += `- **Path:** \`${containingModule.path}\`\n`;
|
|
490
|
+
markdownSummary += `- **Entity Count:** ${containingModule.entity_count}\n\n`;
|
|
491
|
+
if (containingModule.summary) {
|
|
492
|
+
markdownSummary += `## Module Summary\n\n${containingModule.summary}\n`;
|
|
493
|
+
}
|
|
494
|
+
hits = [{
|
|
495
|
+
entity_type: 'Module',
|
|
496
|
+
entity_name: containingModule.name,
|
|
497
|
+
file_path: containingModule.path,
|
|
498
|
+
content_preview: containingModule.summary || `Contains ${entityToFind}`
|
|
499
|
+
}];
|
|
500
|
+
}
|
|
501
|
+
break;
|
|
502
|
+
default:
|
|
503
|
+
return {
|
|
504
|
+
content: [{
|
|
505
|
+
type: 'text',
|
|
506
|
+
text: `Error: Unknown query_type: ${query_type}`
|
|
507
|
+
}]
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
const duration_ms = Date.now() - startTime;
|
|
511
|
+
// Return response - MCP only supports type: 'text'
|
|
512
|
+
// Include JSON as a code block in the markdown for eval harness
|
|
513
|
+
const jsonData = JSON.stringify({
|
|
514
|
+
hits,
|
|
515
|
+
metadata: {
|
|
516
|
+
query_type,
|
|
517
|
+
entity_name: entity_name || doc_path,
|
|
518
|
+
duration_ms,
|
|
519
|
+
total_results
|
|
520
|
+
}
|
|
521
|
+
}, null, 2);
|
|
522
|
+
return {
|
|
523
|
+
content: [
|
|
524
|
+
{
|
|
525
|
+
type: 'text',
|
|
526
|
+
text: markdownSummary + '\n\n---\n\n<details>\n<summary>Raw JSON (for eval harness)</summary>\n\n```json\n' + jsonData + '\n```\n</details>'
|
|
527
|
+
}
|
|
528
|
+
]
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
console.error('[query_code_graph] Error:', error);
|
|
533
|
+
return {
|
|
534
|
+
content: [{
|
|
535
|
+
type: 'text',
|
|
536
|
+
text: `Error querying graph: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
537
|
+
}]
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
finally {
|
|
541
|
+
await client.close();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Format keeper_for_msg results as markdown
|
|
546
|
+
*/
|
|
547
|
+
function formatKeeperForMsgResults(msgName, results) {
|
|
548
|
+
if (results.length === 0) {
|
|
549
|
+
return `# Keeper for Message: ${msgName}\n\nNo Keeper found that handles **${msgName}**.\n\n` +
|
|
550
|
+
`This could mean:\n` +
|
|
551
|
+
`- The message hasn't been indexed yet\n` +
|
|
552
|
+
`- The HANDLES relationship hasn't been created\n` +
|
|
553
|
+
`- The message name is incorrect\n`;
|
|
554
|
+
}
|
|
555
|
+
let markdown = `# Keeper for Message: ${msgName}\n\n`;
|
|
556
|
+
markdown += `Found **${results.length}** Keeper(s) that handle **${msgName}**:\n\n`;
|
|
557
|
+
results.forEach((result, index) => {
|
|
558
|
+
markdown += `## ${index + 1}. ${result.keeper_name}\n\n`;
|
|
559
|
+
markdown += `- **File:** \`${result.keeper_file_path}\`\n`;
|
|
560
|
+
markdown += `- **Line:** ${result.keeper_line_number}\n`;
|
|
561
|
+
markdown += `- **Relationship:** HANDLES → ${msgName}\n\n`;
|
|
562
|
+
});
|
|
563
|
+
return markdown;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Format msgs_for_keeper results as markdown
|
|
567
|
+
*/
|
|
568
|
+
function formatMsgsForKeeperResults(keeperName, results) {
|
|
569
|
+
if (results.length === 0) {
|
|
570
|
+
return `# Messages Handled by: ${keeperName}\n\n**${keeperName}** doesn't handle any messages (or hasn't been indexed yet).\n`;
|
|
571
|
+
}
|
|
572
|
+
let markdown = `# Messages Handled by: ${keeperName}\n\n`;
|
|
573
|
+
markdown += `**${keeperName}** handles **${results.length}** message(s):\n\n`;
|
|
574
|
+
results.forEach((result, index) => {
|
|
575
|
+
markdown += `${index + 1}. **${result.msg_name}**`;
|
|
576
|
+
if (result.msg_package) {
|
|
577
|
+
markdown += ` (package: ${result.msg_package})`;
|
|
578
|
+
}
|
|
579
|
+
markdown += `\n`;
|
|
580
|
+
});
|
|
581
|
+
markdown += `\n`;
|
|
582
|
+
return markdown;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Format docs_mentioning results as markdown
|
|
586
|
+
*/
|
|
587
|
+
function formatDocsMentioningResults(entityName, results) {
|
|
588
|
+
if (results.length === 0) {
|
|
589
|
+
return `# Documents Mentioning: ${entityName}\n\nNo documents found that mention **${entityName}**.\n\n` +
|
|
590
|
+
`This could mean:\n` +
|
|
591
|
+
`- Documents haven't been indexed yet\n` +
|
|
592
|
+
`- MENTIONS relationships haven't been created\n` +
|
|
593
|
+
`- The entity name is incorrect\n`;
|
|
594
|
+
}
|
|
595
|
+
let markdown = `# Documents Mentioning: ${entityName}\n\n`;
|
|
596
|
+
markdown += `Found **${results.length}** document(s) that mention **${entityName}**:\n\n`;
|
|
597
|
+
results.forEach((result, index) => {
|
|
598
|
+
markdown += `${index + 1}. **${result.title}**\n`;
|
|
599
|
+
markdown += ` - Path: \`${result.file_path}\`\n\n`;
|
|
600
|
+
});
|
|
601
|
+
return markdown;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Format entities_in_doc results as markdown
|
|
605
|
+
*/
|
|
606
|
+
function formatEntitiesInDocResults(docPath, results) {
|
|
607
|
+
if (results.length === 0) {
|
|
608
|
+
return `# Entities in Document: ${docPath}\n\nNo entities found in this document.\n`;
|
|
609
|
+
}
|
|
610
|
+
let markdown = `# Entities in Document\n\n`;
|
|
611
|
+
markdown += `**Path:** \`${docPath}\`\n\n`;
|
|
612
|
+
markdown += `Found **${results.length}** entities mentioned in this document:\n\n`;
|
|
613
|
+
// Group by type
|
|
614
|
+
const byType = {};
|
|
615
|
+
results.forEach(result => {
|
|
616
|
+
if (!byType[result.type]) {
|
|
617
|
+
byType[result.type] = [];
|
|
618
|
+
}
|
|
619
|
+
byType[result.type].push(result.name);
|
|
620
|
+
});
|
|
621
|
+
Object.keys(byType).forEach(type => {
|
|
622
|
+
markdown += `## ${type}s (${byType[type].length})\n\n`;
|
|
623
|
+
byType[type].forEach(name => {
|
|
624
|
+
markdown += `- ${name}\n`;
|
|
625
|
+
});
|
|
626
|
+
markdown += `\n`;
|
|
627
|
+
});
|
|
628
|
+
return markdown;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Format related_entities results as markdown
|
|
632
|
+
*/
|
|
633
|
+
function formatRelatedEntitiesResults(entityName, results) {
|
|
634
|
+
if (results.length === 0) {
|
|
635
|
+
return `# Related Entities: ${entityName}\n\nNo related entities found for **${entityName}**.\n`;
|
|
636
|
+
}
|
|
637
|
+
let markdown = `# Related Entities: ${entityName}\n\n`;
|
|
638
|
+
markdown += `Found **${results.length}** entities related to **${entityName}** (via shared documentation):\n\n`;
|
|
639
|
+
results.forEach((result, index) => {
|
|
640
|
+
markdown += `${index + 1}. **${result.name}** (${result.type})`;
|
|
641
|
+
if (result.shared_docs) {
|
|
642
|
+
markdown += ` - ${result.shared_docs} shared document(s)`;
|
|
643
|
+
}
|
|
644
|
+
markdown += `\n`;
|
|
645
|
+
});
|
|
646
|
+
markdown += `\n`;
|
|
647
|
+
return markdown;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Export for easy integration into MCP server
|
|
651
|
+
*/
|
|
652
|
+
export default {
|
|
653
|
+
tool: GRAPH_TOOL,
|
|
654
|
+
execute: executeGraphTool,
|
|
655
|
+
};
|
|
656
|
+
//# sourceMappingURL=graph_tool.js.map
|