@optave/codegraph 1.4.1 → 2.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/src/mcp.js CHANGED
@@ -9,7 +9,14 @@ import { createRequire } from 'node:module';
9
9
  import { findCycles } from './cycles.js';
10
10
  import { findDbPath } from './db.js';
11
11
 
12
- const TOOLS = [
12
+ const REPO_PROP = {
13
+ repo: {
14
+ type: 'string',
15
+ description: 'Repository name from the registry (omit for local project)',
16
+ },
17
+ };
18
+
19
+ const BASE_TOOLS = [
13
20
  {
14
21
  name: 'query_function',
15
22
  description: 'Find callers and callees of a function by name',
@@ -143,7 +150,7 @@ const TOOLS = [
143
150
  {
144
151
  name: 'list_functions',
145
152
  description:
146
- 'List functions, methods, and classes in the codebase, optionally filtered by file or name pattern',
153
+ 'List functions, methods, classes, structs, enums, traits, records, and modules in the codebase, optionally filtered by file or name pattern',
147
154
  inputSchema: {
148
155
  type: 'object',
149
156
  properties: {
@@ -153,15 +160,91 @@ const TOOLS = [
153
160
  },
154
161
  },
155
162
  },
163
+ {
164
+ name: 'structure',
165
+ description:
166
+ 'Show project structure with directory hierarchy, cohesion scores, and per-file metrics',
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: {
170
+ directory: { type: 'string', description: 'Filter to a specific directory path' },
171
+ depth: { type: 'number', description: 'Max directory depth to show' },
172
+ sort: {
173
+ type: 'string',
174
+ enum: ['cohesion', 'fan-in', 'fan-out', 'density', 'files'],
175
+ description: 'Sort directories by metric',
176
+ },
177
+ },
178
+ },
179
+ },
180
+ {
181
+ name: 'hotspots',
182
+ description:
183
+ 'Find structural hotspots: files or directories with extreme fan-in, fan-out, or symbol density',
184
+ inputSchema: {
185
+ type: 'object',
186
+ properties: {
187
+ metric: {
188
+ type: 'string',
189
+ enum: ['fan-in', 'fan-out', 'density', 'coupling'],
190
+ description: 'Metric to rank by',
191
+ },
192
+ level: {
193
+ type: 'string',
194
+ enum: ['file', 'directory'],
195
+ description: 'Rank files or directories',
196
+ },
197
+ limit: { type: 'number', description: 'Number of results to return', default: 10 },
198
+ },
199
+ },
200
+ },
156
201
  ];
157
202
 
158
- export { TOOLS };
203
+ const LIST_REPOS_TOOL = {
204
+ name: 'list_repos',
205
+ description: 'List all repositories registered in the codegraph registry',
206
+ inputSchema: {
207
+ type: 'object',
208
+ properties: {},
209
+ },
210
+ };
211
+
212
+ /**
213
+ * Build the tool list based on multi-repo mode.
214
+ * @param {boolean} multiRepo - If true, inject `repo` prop into each tool and append `list_repos`
215
+ * @returns {object[]}
216
+ */
217
+ function buildToolList(multiRepo) {
218
+ if (!multiRepo) return BASE_TOOLS;
219
+ return [
220
+ ...BASE_TOOLS.map((tool) => ({
221
+ ...tool,
222
+ inputSchema: {
223
+ ...tool.inputSchema,
224
+ properties: { ...tool.inputSchema.properties, ...REPO_PROP },
225
+ },
226
+ })),
227
+ LIST_REPOS_TOOL,
228
+ ];
229
+ }
230
+
231
+ // Backward-compatible export: full multi-repo tool list
232
+ const TOOLS = buildToolList(true);
233
+
234
+ export { TOOLS, buildToolList };
159
235
 
160
236
  /**
161
237
  * Start the MCP server.
162
238
  * This function requires @modelcontextprotocol/sdk to be installed.
239
+ *
240
+ * @param {string} [customDbPath] - Path to a specific graph.db
241
+ * @param {object} [options]
242
+ * @param {boolean} [options.multiRepo] - Enable multi-repo access (default: false)
243
+ * @param {string[]} [options.allowedRepos] - Restrict access to these repo names only
163
244
  */
164
- export async function startMCPServer(customDbPath) {
245
+ export async function startMCPServer(customDbPath, options = {}) {
246
+ const { allowedRepos } = options;
247
+ const multiRepo = options.multiRepo || !!allowedRepos;
165
248
  let Server, StdioServerTransport;
166
249
  try {
167
250
  const sdk = await import('@modelcontextprotocol/sdk/server/index.js');
@@ -196,13 +279,37 @@ export async function startMCPServer(customDbPath) {
196
279
  { capabilities: { tools: {} } },
197
280
  );
198
281
 
199
- server.setRequestHandler('tools/list', async () => ({ tools: TOOLS }));
282
+ server.setRequestHandler('tools/list', async () => ({ tools: buildToolList(multiRepo) }));
200
283
 
201
284
  server.setRequestHandler('tools/call', async (request) => {
202
285
  const { name, arguments: args } = request.params;
203
- const dbPath = customDbPath || undefined;
204
286
 
205
287
  try {
288
+ if (!multiRepo && args.repo) {
289
+ throw new Error(
290
+ 'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to access other repositories.',
291
+ );
292
+ }
293
+ if (!multiRepo && name === 'list_repos') {
294
+ throw new Error(
295
+ 'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to list repositories.',
296
+ );
297
+ }
298
+
299
+ let dbPath = customDbPath || undefined;
300
+ if (args.repo) {
301
+ if (allowedRepos && !allowedRepos.includes(args.repo)) {
302
+ throw new Error(`Repository "${args.repo}" is not in the allowed repos list.`);
303
+ }
304
+ const { resolveRepoDbPath } = await import('./registry.js');
305
+ const resolved = resolveRepoDbPath(args.repo);
306
+ if (!resolved)
307
+ throw new Error(
308
+ `Repository "${args.repo}" not found in registry or its database is missing.`,
309
+ );
310
+ dbPath = resolved;
311
+ }
312
+
206
313
  let result;
207
314
  switch (name) {
208
315
  case 'query_function':
@@ -296,6 +403,34 @@ export async function startMCPServer(customDbPath) {
296
403
  noTests: args.no_tests,
297
404
  });
298
405
  break;
406
+ case 'structure': {
407
+ const { structureData } = await import('./structure.js');
408
+ result = structureData(dbPath, {
409
+ directory: args.directory,
410
+ depth: args.depth,
411
+ sort: args.sort,
412
+ });
413
+ break;
414
+ }
415
+ case 'hotspots': {
416
+ const { hotspotsData } = await import('./structure.js');
417
+ result = hotspotsData(dbPath, {
418
+ metric: args.metric,
419
+ level: args.level,
420
+ limit: args.limit,
421
+ });
422
+ break;
423
+ }
424
+ case 'list_repos': {
425
+ const { listRepos, pruneRegistry } = await import('./registry.js');
426
+ pruneRegistry();
427
+ let repos = listRepos();
428
+ if (allowedRepos) {
429
+ repos = repos.filter((r) => allowedRepos.includes(r.name));
430
+ }
431
+ result = { repos };
432
+ break;
433
+ }
299
434
  default:
300
435
  return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
301
436
  }