carto-md 2.0.5 → 2.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.
@@ -1,149 +0,0 @@
1
- 'use strict';
2
-
3
- const path = require('path');
4
- const { extractImports, buildImportGraph } = require('../extractors/imports');
5
- const { clusterByDomain } = require('../agents/domains');
6
-
7
- /**
8
- * removeFileFromCache(cache, relPath)
9
- * Strips all data for a file from the cache before re-inserting updated data.
10
- */
11
- function removeFileFromCache(cache, relPath) {
12
- delete cache.fileData[relPath];
13
- delete cache.importGraph[relPath];
14
- delete cache.routesByFile[relPath];
15
-
16
- // Remove this file as a dependency target in other files' import lists
17
- for (const [file, deps] of Object.entries(cache.importGraph)) {
18
- cache.importGraph[file] = deps.filter(d => d !== relPath);
19
- }
20
- }
21
-
22
- /**
23
- * insertFileData(cache, relPath, extracted, content, projectRoot)
24
- * Inserts freshly extracted data for one file into the cache.
25
- */
26
- function insertFileData(cache, relPath, extracted, content, projectRoot) {
27
- cache.fileData[relPath] = {
28
- routes: extracted.routes || [],
29
- models: extracted.models || [],
30
- functions: extracted.functions || [],
31
- envVars: extracted.envVars || [],
32
- dbTables: extracted.dbTables || [],
33
- fetches: extracted.fetches || [],
34
- storageKeys: extracted.storageKeys || [],
35
- };
36
-
37
- if (extracted.routes && extracted.routes.length > 0) {
38
- cache.routesByFile[relPath] = extracted.routes.map(r => `${r.method} ${r.path}`);
39
- } else {
40
- delete cache.routesByFile[relPath];
41
- }
42
-
43
- // Re-extract imports for this file and update graph edges
44
- const absPath = path.join(projectRoot, relPath);
45
- const imports = extractImports(content, absPath, projectRoot);
46
- cache.importGraph[relPath] = imports;
47
- }
48
-
49
- /**
50
- * recomputeGraphMetrics(cache)
51
- * Recalculates highImpact, entryPoints, and domain clusters from current cache state.
52
- * Called after every incremental update.
53
- */
54
- function recomputeGraphMetrics(cache) {
55
- const importGraph = cache.importGraph;
56
-
57
- // Fan-in count per file (how many files import it)
58
- const depCount = {};
59
- const allValues = new Set();
60
- for (const deps of Object.values(importGraph)) {
61
- for (const dep of deps) {
62
- depCount[dep] = (depCount[dep] || 0) + 1;
63
- allValues.add(dep);
64
- }
65
- }
66
-
67
- // Entry points: files that nothing imports, but import 3+ others
68
- cache.entryPoints = Object.keys(importGraph)
69
- .filter(f => !allValues.has(f) && importGraph[f].length >= 3)
70
- .sort();
71
-
72
- // High impact: files imported by 2+ others
73
- cache.highImpact = Object.entries(depCount)
74
- .filter(([, count]) => count >= 2)
75
- .sort((a, b) => b[1] - a[1])
76
- .map(([file, dependents]) => ({ file, dependents }));
77
-
78
- // Rebuild domain clusters from all file data
79
- const allRoutes = [];
80
- const allModels = [];
81
- const allFunctions = {};
82
- const allEnvVars = [];
83
- const allDbTables = [];
84
- const fileMap = [];
85
-
86
- for (const [relPath, data] of Object.entries(cache.fileData)) {
87
- allRoutes.push(...data.routes);
88
- allModels.push(...data.models);
89
- if (data.functions && data.functions.length > 0) {
90
- allFunctions[relPath] = data.functions;
91
- }
92
- for (const v of data.envVars) {
93
- if (!allEnvVars.find(e => e.name === v)) {
94
- allEnvVars.push({ name: v, files: [] });
95
- }
96
- const entry = allEnvVars.find(e => e.name === v);
97
- if (!entry.files.includes(relPath)) entry.files.push(relPath);
98
- }
99
- allDbTables.push(...(data.dbTables || []).map(t => ({ ...t, file: relPath })));
100
- }
101
-
102
- cache.domains = clusterByDomain({
103
- routes: allRoutes,
104
- models: allModels,
105
- functions: allFunctions,
106
- envVars: allEnvVars,
107
- dbTables: allDbTables,
108
- fileMap,
109
- routesByFile: cache.routesByFile,
110
- importGraph
111
- });
112
-
113
- // Update meta
114
- cache.meta = {
115
- totalFiles: Object.keys(cache.fileData).length,
116
- totalRoutes: allRoutes.length,
117
- totalImportEdges: Object.values(importGraph).reduce((s, d) => s + d.length, 0),
118
- lastIndexed: new Date().toISOString(),
119
- indexDuration: cache.meta.indexDuration || 0
120
- };
121
- }
122
-
123
- /**
124
- * applyIncrementalUpdate(cache, relPath, extracted, content, projectRoot)
125
- *
126
- * Main entry point for incremental updates.
127
- * Call when a single file changes — updates only that file's data,
128
- * then recomputes graph metrics.
129
- *
130
- * Returns the updated cache (mutated in place).
131
- */
132
- function applyIncrementalUpdate(cache, relPath, extracted, content, projectRoot) {
133
- removeFileFromCache(cache, relPath);
134
- insertFileData(cache, relPath, extracted, content, projectRoot);
135
- recomputeGraphMetrics(cache);
136
- return cache;
137
- }
138
-
139
- /**
140
- * removeFileFromGraph(cache, relPath)
141
- * Call when a file is deleted.
142
- */
143
- function removeFileFromGraph(cache, relPath) {
144
- removeFileFromCache(cache, relPath);
145
- recomputeGraphMetrics(cache);
146
- return cache;
147
- }
148
-
149
- module.exports = { applyIncrementalUpdate, removeFileFromGraph, recomputeGraphMetrics };
package/src/mcp/server.js DELETED
@@ -1,431 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const path = require('path');
5
- const fs = require('fs');
6
- const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
7
- const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
8
- const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
9
- const { Carto } = require('../../index.js');
10
-
11
- const projectRoot = process.cwd();
12
-
13
- // Load Carto from disk cache — fast startup, no re-indexing
14
- let carto = null;
15
-
16
- async function getCarto() {
17
- if (carto) return carto;
18
- carto = new Carto();
19
- try {
20
- await carto.index(projectRoot, { useWorkers: false });
21
- } catch (err) {
22
- console.error('[CARTO MCP] Failed to load index:', err.message);
23
- }
24
- return carto;
25
- }
26
-
27
- function text(str) {
28
- return { content: [{ type: 'text', text: str }] };
29
- }
30
-
31
- function notIndexed() {
32
- return text('No .carto/graph-cache.json found. Run `carto init` first.');
33
- }
34
-
35
- // ─── Tool definitions ────────────────────────────────────────────────────────
36
-
37
- const TOOLS = [
38
- {
39
- name: 'get_routes',
40
- description: 'Get all API routes in this project including REST, tRPC, and webhooks.',
41
- inputSchema: { type: 'object', properties: {}, required: [] }
42
- },
43
- {
44
- name: 'get_blast_radius',
45
- description: 'Get all files, routes, and domains affected by changing a specific file. Includes risk level per route.',
46
- inputSchema: {
47
- type: 'object',
48
- properties: {
49
- file: { type: 'string', description: 'Relative file path from project root' }
50
- },
51
- required: ['file']
52
- }
53
- },
54
- {
55
- name: 'get_structure',
56
- description: 'Get project structure: import graph, entry points, high impact files, tech stack, and domains.',
57
- inputSchema: { type: 'object', properties: {}, required: [] }
58
- },
59
- {
60
- name: 'get_domain',
61
- description: 'Get all routes, models, functions, and context for a specific domain (AUTH, PAYMENTS, TRPC, DATABASE, EVENTS, NOTIFICATIONS, CORE).',
62
- inputSchema: {
63
- type: 'object',
64
- properties: {
65
- domain: { type: 'string', description: 'Domain name e.g. AUTH, PAYMENTS, DATABASE' }
66
- },
67
- required: ['domain']
68
- }
69
- },
70
- {
71
- name: 'get_neighbors',
72
- description: 'Get import graph neighbors of a file — files it imports and files that import it. Returns nodes and edges for visualization.',
73
- inputSchema: {
74
- type: 'object',
75
- properties: {
76
- file: { type: 'string', description: 'Relative file path from project root' },
77
- hops: { type: 'number', description: 'How many hops to traverse (default 1, max 3)' }
78
- },
79
- required: ['file']
80
- }
81
- },
82
- {
83
- name: 'get_cross_domain',
84
- description: 'Get all import edges that cross domain boundaries — e.g. AUTH importing PAYMENTS. Use to detect unexpected coupling.',
85
- inputSchema: { type: 'object', properties: {}, required: [] }
86
- },
87
- {
88
- name: 'get_context',
89
- description: 'Get full structural context for a file: domain, blast radius, import neighbors, routes, models, env vars, and cross-domain dependencies. Single call for everything.',
90
- inputSchema: {
91
- type: 'object',
92
- properties: {
93
- file: { type: 'string', description: 'Relative file path from project root' }
94
- },
95
- required: ['file']
96
- }
97
- },
98
- {
99
- name: 'search_routes',
100
- description: 'Search API routes by path or method. Case-insensitive.',
101
- inputSchema: {
102
- type: 'object',
103
- properties: {
104
- query: { type: 'string', description: 'Search query e.g. "auth", "POST", "/api/users"' }
105
- },
106
- required: ['query']
107
- }
108
- },
109
- {
110
- name: 'get_models',
111
- description: 'Get all data models (Prisma, Pydantic, TypeScript interfaces, Zod schemas) across the project, optionally filtered by domain.',
112
- inputSchema: {
113
- type: 'object',
114
- properties: {
115
- domain: { type: 'string', description: 'Optional domain filter e.g. AUTH, DATABASE' }
116
- },
117
- required: []
118
- }
119
- },
120
- {
121
- name: 'get_high_impact_files',
122
- description: 'Get the files with the highest blast radius — most other files depend on them. Changing these files is highest risk.',
123
- inputSchema: {
124
- type: 'object',
125
- properties: {
126
- limit: { type: 'number', description: 'Number of files to return (default 10)' }
127
- },
128
- required: []
129
- }
130
- },
131
- {
132
- name: 'get_env_vars',
133
- description: 'Get all environment variables used in this project, with which files use them and which domains they belong to.',
134
- inputSchema: {
135
- type: 'object',
136
- properties: {
137
- domain: { type: 'string', description: 'Optional domain filter e.g. AUTH, PAYMENTS' }
138
- },
139
- required: []
140
- }
141
- },
142
- {
143
- name: 'get_domains_list',
144
- description: 'Get all detected domains with file counts, route counts, and model counts.',
145
- inputSchema: { type: 'object', properties: {}, required: [] }
146
- }
147
- ];
148
-
149
- // ─── Server setup ─────────────────────────────────────────────────────────────
150
-
151
- const server = new Server(
152
- { name: 'carto', version: '2.0.0' },
153
- { capabilities: { tools: {} } }
154
- );
155
-
156
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
157
-
158
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
159
- const { name, arguments: args } = request.params;
160
- const c = await getCarto();
161
-
162
- if (!c._cache) return notIndexed();
163
-
164
- // ── get_routes ────────────────────────────────────────────────────────────
165
- if (name === 'get_routes') {
166
- const routes = c.getRoutes();
167
- if (routes.length === 0) return text('No routes found.');
168
- const lines = ['# All Routes\n', '| Method | Path | File |', '|--------|------|------|'];
169
- for (const r of routes) lines.push(`| ${r.method} | ${r.path} | ${r.file} |`);
170
- return text(lines.join('\n'));
171
- }
172
-
173
- // ── get_blast_radius ─────────────────────────────────────────────────────
174
- if (name === 'get_blast_radius') {
175
- const br = c.getBlastRadius(args.file);
176
- if (!br) return text(`File not found in graph: ${args.file}`);
177
-
178
- const lines = [
179
- `# Blast Radius: ${br.file}\n`,
180
- `**Risk:** ${br.risk}`,
181
- `**Directly affected:** ${br.directlyAffected.files} files, ${br.directlyAffected.domains} domains`,
182
- `**Potentially affected:** ${br.potentiallyAffected.files} files\n`,
183
- ];
184
-
185
- if (br.domainsImpacted.length > 0) {
186
- lines.push(`**Domains impacted:** ${br.domainsImpacted.join(', ')}\n`);
187
- }
188
-
189
- if (br.dependentFiles.length > 0) {
190
- lines.push(`## Files depending on this (${br.dependentFiles.length})`);
191
- for (const f of br.dependentFiles) lines.push(`- ${f}`);
192
- lines.push('');
193
- }
194
-
195
- if (br.routesImpacted.length > 0) {
196
- lines.push(`## Routes at risk (${br.routesImpacted.length})`);
197
- lines.push('| Method | Path | Risk |');
198
- lines.push('|--------|------|------|');
199
- for (const r of br.routesImpacted) lines.push(`| ${r.method} | ${r.path} | ${r.risk} |`);
200
- } else {
201
- lines.push('## Routes at risk\n_None directly traceable._');
202
- }
203
-
204
- return text(lines.join('\n'));
205
- }
206
-
207
- // ── get_structure ─────────────────────────────────────────────────────────
208
- if (name === 'get_structure') {
209
- const s = c.getStructure();
210
- const lines = ['# Project Structure\n'];
211
-
212
- if (s.stack && s.stack.length > 0) {
213
- lines.push(`**Stack:** ${s.stack.join(', ')}\n`);
214
- }
215
- lines.push(`**Meta:** ${s.meta.totalFiles} files, ${s.meta.totalRoutes} routes, ${s.meta.totalImportEdges} import edges\n`);
216
-
217
- if (s.domains.length > 0) {
218
- lines.push(`**Domains:** ${s.domains.join(', ')}\n`);
219
- }
220
-
221
- if (s.entryPoints.length > 0) {
222
- lines.push('## Entry Points');
223
- for (const e of s.entryPoints) lines.push(`- ${e}`);
224
- lines.push('');
225
- }
226
-
227
- if (s.highImpact.length > 0) {
228
- lines.push('## High Impact Files (top 15)');
229
- lines.push('| File | Dependents |');
230
- lines.push('|------|------------|');
231
- for (const h of s.highImpact.slice(0, 15)) lines.push(`| ${h.file} | ${h.dependents} |`);
232
- }
233
-
234
- return text(lines.join('\n'));
235
- }
236
-
237
- // ── get_domain ────────────────────────────────────────────────────────────
238
- if (name === 'get_domain') {
239
- const domain = c.getDomain(args.domain);
240
- if (!domain) return text(`Domain not found: ${args.domain}. Use get_domains_list to see available domains.`);
241
- if (domain.contextContent) return text(domain.contextContent);
242
-
243
- const lines = [`# Domain: ${args.domain.toUpperCase()}\n`];
244
- if ((domain.routes || []).length > 0) {
245
- lines.push('## Routes');
246
- for (const r of domain.routes) lines.push(`- ${r.method || ''} ${r.path || r}`);
247
- lines.push('');
248
- }
249
- if ((domain.models || []).length > 0) {
250
- lines.push('## Models');
251
- for (const m of domain.models) lines.push(`- ${m.name || m}`);
252
- lines.push('');
253
- }
254
- if (domain.files && domain.files.length > 0) {
255
- lines.push('## Files');
256
- for (const f of domain.files) lines.push(`- ${f}`);
257
- }
258
- return text(lines.join('\n'));
259
- }
260
-
261
- // ── get_neighbors ─────────────────────────────────────────────────────────
262
- if (name === 'get_neighbors') {
263
- const hops = Math.min(args.hops || 1, 3);
264
- const nb = c.getNeighbors(args.file, hops);
265
- if (!nb || nb.nodes.length === 0) return text(`File not found or no neighbors: ${args.file}`);
266
-
267
- const lines = [`# Import Neighbors: ${args.file} (${hops} hop${hops > 1 ? 's' : ''})\n`];
268
- lines.push(`## Nodes (${nb.nodes.length})`);
269
- lines.push('| File | Domain | Root |');
270
- lines.push('|------|--------|------|');
271
- for (const n of nb.nodes) lines.push(`| ${n.id} | ${n.domain} | ${n.isRoot ? '✓' : ''} |`);
272
- lines.push('');
273
- lines.push(`## Edges (${nb.edges.length})`);
274
- for (const e of nb.edges) lines.push(`- ${e.source} → ${e.target}`);
275
- return text(lines.join('\n'));
276
- }
277
-
278
- // ── get_cross_domain ─────────────────────────────────────────────────────
279
- if (name === 'get_cross_domain') {
280
- const xd = c.getCrossDomainDeps();
281
- if (xd.length === 0) return text('No cross-domain dependencies found. Clean architecture.');
282
-
283
- const lines = [`# Cross-Domain Dependencies (${xd.length})\n`];
284
- lines.push('| From File | From Domain | To File | To Domain |');
285
- lines.push('|-----------|------------|---------|-----------|');
286
- for (const d of xd) {
287
- lines.push(`| ${d.from} | ${d.fromDomain} | ${d.to} | ${d.toDomain} |`);
288
- }
289
- return text(lines.join('\n'));
290
- }
291
-
292
- // ── get_context ───────────────────────────────────────────────────────────
293
- if (name === 'get_context') {
294
- const ctx = c.getContextForFile(args.file);
295
- if (!ctx) return text(`File not found: ${args.file}`);
296
-
297
- const lines = [
298
- `# Context: ${ctx.file}\n`,
299
- `**Domain:** ${ctx.domain}`,
300
- `**Imports:** ${ctx.meta.importCount} files`,
301
- `**Dependents:** ${ctx.meta.dependentCount} files`,
302
- `**Blast radius risk:** ${ctx.blastRadius ? ctx.blastRadius.risk : 'SAFE'}\n`,
303
- ];
304
-
305
- if (ctx.routes.length > 0) {
306
- lines.push('## Routes served by this file');
307
- for (const r of ctx.routes) lines.push(`- ${r}`);
308
- lines.push('');
309
- }
310
-
311
- if (ctx.models.length > 0) {
312
- lines.push('## Models');
313
- for (const m of ctx.models) lines.push(`- ${m}`);
314
- lines.push('');
315
- }
316
-
317
- if (ctx.envVars.length > 0) {
318
- lines.push(`## Env vars used: ${ctx.envVars.join(', ')}\n`);
319
- }
320
-
321
- if (ctx.blastRadius && ctx.blastRadius.domainsImpacted.length > 0) {
322
- lines.push(`## Domains impacted if changed: ${ctx.blastRadius.domainsImpacted.join(', ')}\n`);
323
- }
324
-
325
- if (ctx.crossDomainDeps.length > 0) {
326
- lines.push('## Cross-domain dependencies');
327
- for (const d of ctx.crossDomainDeps) {
328
- lines.push(`- ${d.from} (${d.fromDomain}) → ${d.to} (${d.toDomain})`);
329
- }
330
- lines.push('');
331
- }
332
-
333
- if (ctx.neighbors.nodes.length > 0) {
334
- lines.push(`## Import neighbors (2 hops): ${ctx.neighbors.nodes.length} files`);
335
- for (const n of ctx.neighbors.nodes.filter(n => !n.isRoot).slice(0, 10)) {
336
- lines.push(`- ${n.id} [${n.domain}]`);
337
- }
338
- if (ctx.neighbors.nodes.length > 11) lines.push(`_...and ${ctx.neighbors.nodes.length - 11} more_`);
339
- lines.push('');
340
- }
341
-
342
- if (ctx.domainContext) {
343
- lines.push('## Domain context');
344
- lines.push(ctx.domainContext);
345
- }
346
-
347
- return text(lines.join('\n'));
348
- }
349
-
350
- // ── search_routes ─────────────────────────────────────────────────────────
351
- if (name === 'search_routes') {
352
- const results = c.searchRoutes(args.query);
353
- if (results.length === 0) return text(`No routes matching: ${args.query}`);
354
-
355
- const lines = [`# Routes matching "${args.query}" (${results.length})\n`];
356
- lines.push('| Method | Path | File |');
357
- lines.push('|--------|------|------|');
358
- for (const r of results) lines.push(`| ${r.method} | ${r.path} | ${r.file} |`);
359
- return text(lines.join('\n'));
360
- }
361
-
362
- // ── get_models ────────────────────────────────────────────────────────────
363
- if (name === 'get_models') {
364
- const models = c.getModels(args.domain);
365
- if (models.length === 0) return text(args.domain ? `No models found in domain: ${args.domain}` : 'No models found.');
366
-
367
- const lines = [`# Models${args.domain ? ` — ${args.domain.toUpperCase()}` : ''} (${models.length})\n`];
368
- lines.push('| Model | Fields | Domain | File |');
369
- lines.push('|-------|--------|--------|------|');
370
- for (const m of models) {
371
- const fields = (m.fields || []).map(f => f.name || f).join(', ') || '—';
372
- lines.push(`| ${m.name || '—'} | ${fields} | ${m.domain} | ${m.file} |`);
373
- }
374
- return text(lines.join('\n'));
375
- }
376
-
377
- // ── get_high_impact_files ─────────────────────────────────────────────────
378
- if (name === 'get_high_impact_files') {
379
- const limit = args.limit || 10;
380
- const files = c.getHighImpactFiles(limit);
381
- if (files.length === 0) return text('No high impact files found.');
382
-
383
- const lines = [`# High Impact Files (top ${limit})\n`];
384
- lines.push('Changing these files has the highest blast radius.\n');
385
- lines.push('| File | Dependents |');
386
- lines.push('|------|------------|');
387
- for (const f of files) lines.push(`| ${f.file} | ${f.dependents} |`);
388
- return text(lines.join('\n'));
389
- }
390
-
391
- // ── get_env_vars ──────────────────────────────────────────────────────────
392
- if (name === 'get_env_vars') {
393
- const vars = c.getEnvVars(args.domain);
394
- if (vars.length === 0) return text(args.domain ? `No env vars found in domain: ${args.domain}` : 'No env vars found.');
395
-
396
- const lines = [`# Environment Variables${args.domain ? ` — ${args.domain.toUpperCase()}` : ''} (${vars.length})\n`];
397
- lines.push('| Variable | Domains | Used in |');
398
- lines.push('|----------|---------|---------|');
399
- for (const v of vars) {
400
- lines.push(`| ${v.name} | ${v.domains.join(', ')} | ${v.files.length} file${v.files.length !== 1 ? 's' : ''} |`);
401
- }
402
- return text(lines.join('\n'));
403
- }
404
-
405
- // ── get_domains_list ──────────────────────────────────────────────────────
406
- if (name === 'get_domains_list') {
407
- const domains = c.getDomainsList();
408
- if (domains.length === 0) return text('No domains detected. Run `carto init` first.');
409
-
410
- const lines = [`# Domains (${domains.length})\n`];
411
- lines.push('| Domain | Files | Routes | Models |');
412
- lines.push('|--------|-------|--------|--------|');
413
- for (const d of domains) {
414
- lines.push(`| ${d.name} | ${d.fileCount} | ${d.routeCount} | ${d.modelCount} |`);
415
- }
416
- return text(lines.join('\n'));
417
- }
418
-
419
- return text(`Unknown tool: ${name}`);
420
- });
421
-
422
- async function main() {
423
- await getCarto(); // pre-load on startup
424
- const transport = new StdioServerTransport();
425
- await server.connect(transport);
426
- }
427
-
428
- main().catch(err => {
429
- console.error('[CARTO MCP] Fatal:', err.message);
430
- process.exit(1);
431
- });