carto-md 2.0.6 → 2.0.7

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 CHANGED
@@ -7,93 +7,104 @@
7
7
 
8
8
  **The structural intelligence layer for AI coding tools.**
9
9
 
10
+ Your AI coding tool sees files. Carto sees architecture — domains, blast radius, import graph, routes. Every AI tool you use stops guessing about your codebase and starts knowing it.
11
+
10
12
  ```bash
11
13
  npm install -g carto-md
12
14
  cd your-project
13
15
  carto init
14
16
  ```
15
17
 
16
- Carto indexes your codebase routes, models, import graph, blast radius, domain clusters and keeps it live via SQLite. Every AI tool you use gets accurate structural facts about your project instead of guessing.
18
+ That's it. Carto auto-wires itself into every AI tool you have installed. Restart your AI tool and it now knows your codebase architecturally.
19
+
20
+ **Works with:** Cursor · Claude Code · Kiro · Claude Desktop · Windsurf · VS Code Copilot · JetBrains · Zed
17
21
 
18
22
  ---
19
23
 
20
- ## Benchmarks
24
+ ## Use it with your AI tool
21
25
 
22
- Measured on real open-source repos. Apple M-series, 8 CPUs, 8GB RAM.
26
+ `carto init` auto-wires the MCP server into every AI tool it detects. If yours wasn't detected, here's the manual config — **one block, copy-paste, done.**
23
27
 
24
- | Repo | Language | Source Files | Indexed | First Run | Second Run | DB Size | Import Edges |
25
- |------|----------|-------------|---------|-----------|------------|---------|--------------|
26
- | [prisma/prisma](https://github.com/prisma/prisma) | TypeScript | 3,303 | 3,303 | **1.6s** | **178ms** | 2.2 MB | 3,590 |
27
- | [supabase/supabase](https://github.com/supabase/supabase) | TypeScript | 6,818 | 6,746 | **4.9s** | **725ms** | 4.3 MB | 5,754 |
28
- | [microsoft/vscode](https://github.com/microsoft/vscode) | TypeScript | 10,565 | 10,565 | **9.7s** | **1.2s** | 10.6 MB | 19,769 |
29
- | [zed-industries/zed](https://github.com/zed-industries/zed) | Rust | 1,837 | 1,837 | **2.7s** | **83ms** | 4.7 MB | 2,176 |
28
+ ### Cursor
30
29
 
31
- **Second run** = only changed files re-parsed. mtime+size checked before reading content — if nothing changed, nothing is re-parsed.
30
+ `carto init` writes this for you. Manual: `~/.cursor/mcp.json`
31
+ ```json
32
+ { "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
33
+ ```
32
34
 
33
- **All MCP queries** (blast radius, routes, structure, domains): **<5ms** on all repos.
35
+ ### Claude Code (CLI)
34
36
 
35
- ### Domains detected
37
+ One command from your project root:
38
+ ```bash
39
+ claude mcp add carto -- carto serve
40
+ ```
41
+ Or create `.mcp.json` at the project root:
42
+ ```json
43
+ { "mcpServers": { "carto": { "command": "carto", "args": ["serve"] } } }
44
+ ```
36
45
 
37
- | Repo | Domains |
38
- |------|---------|
39
- | prisma | DATABASE · CORE · EVENTS · AUTH |
40
- | supabase | CORE · AUTH · DATABASE · PAYMENTS · EVENTS · NOTIFICATIONS · TRPC |
41
- | vscode | CORE · AUTH · EVENTS · DATABASE · NOTIFICATIONS |
42
- | zed (Rust) | CORE · DATABASE · AUTH · EVENTS · PAYMENTS · NOTIFICATIONS |
46
+ ### Kiro
43
47
 
44
- vscode at 10,565 files in under 10 seconds. Rust import graph working on zed (2,176 edges from `mod` declarations and `use crate::` paths).
48
+ `carto init` writes this for you. Manual: `~/.kiro/settings/mcp.json`
49
+ ```json
50
+ { "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
51
+ ```
45
52
 
46
- ---
53
+ ### Claude Desktop
54
+
55
+ `carto init` writes this for you. Manual: `~/Library/Application Support/Claude/claude_desktop_config.json`
56
+ ```json
57
+ { "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
58
+ ```
47
59
 
48
- ## What it does
60
+ ### VS Code Copilot
49
61
 
62
+ `.vscode/mcp.json` in your project root, or Command Palette → `MCP: Add Server`
63
+ ```json
64
+ { "servers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
50
65
  ```
51
- carto init / carto sync
52
-
53
- Discovers all files (no cap — SQLite handles the volume)
54
- mtime+size check → skip unchanged files
55
- tree-sitter parse → imports + symbols (0.05–0.2ms/file)
56
- Babel deep parse routes + models (API handler files only)
57
- Leiden+CPM graph clustering → auto-detects domains
58
- Computes reverse deps → blast radius for every file
59
- Writes AGENTS.md + .carto/context/*.md (lazy, on-demand)
60
- Auto-wires MCP into Kiro, Cursor, Claude Desktop
61
-
62
- carto watch
63
-
64
- File saved → debounce 50ms → re-parse 1 file → SQLite write → <50ms
66
+
67
+ ### Windsurf
68
+
69
+ `~/.windsurf/mcp.json`
70
+ ```json
71
+ { "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
65
72
  ```
66
73
 
74
+ ### Zed / JetBrains / VS Code (full agent mode)
75
+
76
+ Carto also runs as a full **ACP agent** — not just a passive MCP server, but an active coding agent with architectural awareness. See [ACP Agent](#acp-agent-zed--jetbrains--vs-code) below.
77
+
67
78
  ---
68
79
 
69
- ## 16 MCP tools
80
+ ## What changes for your AI
70
81
 
71
- `carto serve` exposes a local MCP server. Kiro, Cursor, and Claude query it mid-task.
82
+ Without carto, your AI greps text and guesses. With carto, it queries structural facts:
72
83
 
73
- | Tool | What it returns |
74
- |------|----------------|
75
- | `get_architecture()` | 500-word project overview: domains, entry points, stack, key patterns. **Use this first.** |
76
- | `get_blast_radius(file)` | All files affected by changing a given file, with hop distance |
77
- | `get_context(file)` | Everything about a file: domain, blast radius, neighbors, routes, models |
78
- | `get_change_plan(intent)` | Given "add rate limiting to /api/users" → files to touch, domains affected, blast radius, similar patterns |
79
- | `get_file_summary(file)` | What a file does, its role, key deps and dependents |
80
- | `get_similar_patterns(file)` | Files with same domain, same route shape, or shared dependencies — find conventions before writing new code |
81
- | `get_routes()` | All API endpoints with file mapping |
82
- | `get_structure()` | Import graph, entry points, high-impact files, tech stack |
83
- | `get_domain(name)` | All routes, models, functions for a domain. Lazily regenerated when stale. |
84
- | `get_neighbors(file, hops)` | Import graph neighbors: nodes and edges |
85
- | `get_cross_domain()` | Import edges that cross domain boundaries |
86
- | `search_routes(query)` | Search API routes by path or method |
87
- | `get_models(domain?)` | All data models, optionally filtered by domain |
88
- | `get_high_impact_files(n)` | Top N files by blast radius |
89
- | `get_env_vars(domain?)` | All env vars with domain mapping |
90
- | `get_domains_list()` | All detected domains with file, route, model counts |
84
+ > *"Add rate limiting to /api/users"*
85
+
86
+ **Without carto:** AI grep-searches your codebase, finds 12 files mentioning "users", picks 3 at random, hopes for the best.
87
+
88
+ **With carto:** AI calls `get_change_plan("add rate limiting to /api/users")` and gets back:
89
+ - The exact route handler file
90
+ - All 7 files that import it (blast radius)
91
+ - The AUTH domain it lives in
92
+ - Similar middleware patterns already in the codebase
93
+ - Cross-domain dependencies that could break
94
+
95
+ Your AI's response goes from "here's a guess" to "here's the right change with the architectural context."
96
+
97
+ ### In the wild
98
+
99
+ ![Claude Code reviewing the supabase repo through carto's MCP server](docs/screenshots/claude-code-supabase.png)
100
+
101
+ *Claude Code analyzing the [supabase](https://github.com/supabase/supabase) repo via carto. Real session, no editing — 5,974 files indexed in ~780ms, 86 routes, 4,839 import edges, 7 domains. The agent's own verdict at the bottom: "useful, especially for a large codebase like supabase. The blast radius + cross-domain tools are the most valuable."*
91
102
 
92
103
  ---
93
104
 
94
105
  ## Languages and frameworks
95
106
 
96
- ### Import graph + symbols (tree-sitter — works on any repo)
107
+ ### Import graph + symbols (any repo)
97
108
 
98
109
  | Language | Extensions |
99
110
  |----------|-----------|
@@ -106,16 +117,16 @@ File saved → debounce 50ms → re-parse 1 file → SQLite write → <50ms
106
117
  | C# | `.cs` |
107
118
  | Ruby | `.rb` |
108
119
 
109
- ### Route extraction (framework-specific)
120
+ ### Route extraction (framework-aware)
110
121
 
111
122
  | Framework | Language |
112
123
  |-----------|---------|
113
- | Express, Next.js (App + Pages Router), tRPC, React Router | TypeScript / JavaScript |
124
+ | Express, Next.js (App + Pages), tRPC, React Router | TypeScript / JavaScript |
114
125
  | FastAPI, Flask, Django | Python |
115
126
  | Gin, Echo, Chi, net/http | Go |
116
127
  | Actix-web, Axum, Rocket | Rust |
117
128
  | Spring MVC / Boot, JAX-RS | Java |
118
- | ASP.NET Core (attribute routing + minimal API) | C# |
129
+ | ASP.NET Core | C# |
119
130
  | Rails, Sinatra | Ruby |
120
131
 
121
132
  ### Model extraction
@@ -127,52 +138,18 @@ File saved → debounce 50ms → re-parse 1 file → SQLite write → <50ms
127
138
  | Go structs | Go |
128
139
  | Rust structs | Rust |
129
140
  | JPA `@Entity`, Java records | Java |
130
- | EF Core classes, C# records | C# |
141
+ | EF Core, C# records | C# |
131
142
  | ActiveRecord | Ruby |
132
143
 
133
144
  ### TypeScript path aliases
134
145
 
135
- Reads `tsconfig.json` / `jsconfig.json` for `paths` config. `@/components/Button` resolves to the actual file in the import graph — blast radius works correctly for Next.js and Vite projects.
136
-
137
- ---
138
-
139
- ## Domain detection
140
-
141
- Carto uses **Leiden+CPM graph clustering** — files that import each other heavily cluster together. Domain names are inferred from path tokens, with keyword hints for well-known patterns (AUTH, PAYMENTS, DATABASE, etc.).
142
-
143
- Works on any repo — not just SaaS apps. vscode gets AUTH/EVENTS/DATABASE. zed (Rust) gets DATABASE/AUTH/EVENTS. A game engine would get RENDERER/PHYSICS/AUDIO.
144
-
145
- Custom domains via `carto.config.json`:
146
-
147
- ```json
148
- {
149
- "domains": {
150
- "EDITOR": ["editor", "monaco", "text"],
151
- "WORKBENCH": ["workbench", "layout", "panel"]
152
- }
153
- }
154
- ```
155
-
156
- ---
157
-
158
- ## Commands
159
-
160
- | Command | What it does |
161
- |---------|-------------|
162
- | `carto init` | Detect project, index codebase, generate AGENTS.md, wire MCP |
163
- | `carto sync` | Full re-index (skips unchanged files via mtime+size cache) |
164
- | `carto watch` | Incremental live re-index on every file save (<50ms) |
165
- | `carto serve` | Start MCP server for Kiro / Cursor / Claude |
166
- | `carto agent` | Start ACP agent mode (for Zed / JetBrains / VS Code) |
167
- | `carto impact <file>` | Blast radius: risk level, affected files, routes at risk |
168
- | `carto check` | Cross-domain violations, high-risk uncommitted changes, domain health |
169
- | `carto remove` | Remove AGENTS.md and .carto/ from project |
146
+ Reads `tsconfig.json` / `jsconfig.json` `paths` config. `@/components/Button` resolves to the actual file in the import graph — blast radius works correctly for Next.js and Vite projects.
170
147
 
171
148
  ---
172
149
 
173
150
  ## ACP Agent (Zed / JetBrains / VS Code)
174
151
 
175
- Carto works as a full **ACP agent** — not just a passive tool server, but an active coding agent with architectural awareness.
152
+ Beyond MCP, Carto runs as a full **ACP agent** — an active coding agent with built-in architectural awareness.
176
153
 
177
154
  ```
178
155
  User: "Add rate limiting to /api/users"
@@ -189,20 +166,16 @@ Sends to LLM (your API key) → streams answer + diffs back to editor
189
166
 
190
167
  ### Setup in Zed
191
168
 
192
- Add to `~/.config/zed/settings.json`:
193
-
169
+ `~/.config/zed/settings.json`:
194
170
  ```json
195
171
  {
196
172
  "agent_servers": {
197
- "Carto": {
198
- "command": "carto",
199
- "args": ["agent"]
200
- }
173
+ "Carto": { "command": "carto", "args": ["agent"] }
201
174
  }
202
175
  }
203
176
  ```
204
177
 
205
- ### BYOK (Bring Your Own Key)
178
+ ### Bring Your Own Key
206
179
 
207
180
  Carto supports any LLM provider — configure in your editor:
208
181
 
@@ -217,53 +190,123 @@ Carto supports any LLM provider — configure in your editor:
217
190
  | Together AI | Open-source models |
218
191
  | Azure OpenAI | Enterprise deployments |
219
192
 
220
- ### What makes it different
193
+ ---
194
+
195
+ ## The 16 MCP tools
196
+
197
+ Once carto is wired in, your AI tool can call any of these mid-task. You don't need to memorize them — your AI will pick the right ones.
198
+
199
+ | Tool | What it returns |
200
+ |------|----------------|
201
+ | `get_architecture()` | 500-word project overview: domains, entry points, stack, key patterns. **Use this first.** |
202
+ | `get_change_plan(intent)` | Given "add rate limiting to /api/users" → files to touch, domains affected, blast radius, similar patterns |
203
+ | `get_blast_radius(file)` | All files affected by changing a given file, with hop distance |
204
+ | `get_context(file)` | Everything about a file: domain, blast radius, neighbors, routes, models |
205
+ | `get_file_summary(file)` | What a file does, its role, key deps and dependents |
206
+ | `get_similar_patterns(file)` | Files with same domain, route shape, or shared deps — find conventions before writing new code |
207
+ | `get_routes()` | All API endpoints with file mapping |
208
+ | `get_structure()` | Import graph, entry points, high-impact files, tech stack |
209
+ | `get_domain(name)` | All routes, models, functions for a domain. Lazily regenerated when stale. |
210
+ | `get_neighbors(file, hops)` | Import graph neighbors: nodes and edges |
211
+ | `get_cross_domain()` | Import edges that cross domain boundaries |
212
+ | `search_routes(query)` | Search API routes by path or method |
213
+ | `get_models(domain?)` | All data models, optionally filtered by domain |
214
+ | `get_high_impact_files(n)` | Top N files by blast radius |
215
+ | `get_env_vars(domain?)` | All env vars with domain mapping |
216
+ | `get_domains_list()` | All detected domains with file, route, model counts |
221
217
 
222
- Other agents are smart but blind. Carto sees the architecture:
223
- - Auto-indexes your project on first session (1-10s depending on size)
224
- - Injects structural context into every LLM call (blast radius, domains, routes)
225
- - 12 internal tools the LLM can call during reasoning
226
- - Zero cost to you beyond your own API key
218
+ **All MCP queries:** **<5ms** on every benchmarked repo.
227
219
 
228
220
  ---
229
221
 
230
- ## MCP config (if auto-wire missed your IDE)
222
+ ## Domain detection
231
223
 
232
- `carto init` auto-wires into Kiro, Cursor, and Claude Desktop. If it missed yours, add manually:
224
+ Carto uses **Leiden+CPM graph clustering** files that import each other heavily cluster together. Domain names are inferred from path tokens, with keyword hints for well-known patterns (AUTH, PAYMENTS, DATABASE, etc.).
233
225
 
234
- **Kiro**`~/.kiro/settings/mcp.json`
235
- ```json
236
- { "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
237
- ```
226
+ Works on any repo not just SaaS apps. vscode gets AUTH/EVENTS/DATABASE. zed (Rust) gets DATABASE/AUTH/EVENTS. A game engine would get RENDERER/PHYSICS/AUDIO.
238
227
 
239
- **Cursor** `~/.cursor/mcp.json`
228
+ Custom domains via `carto.config.json`:
240
229
  ```json
241
- { "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
230
+ {
231
+ "domains": {
232
+ "EDITOR": ["editor", "monaco", "text"],
233
+ "WORKBENCH": ["workbench", "layout", "panel"]
234
+ }
235
+ }
242
236
  ```
243
237
 
244
- **Claude Desktop** — `~/Library/Application Support/Claude/claude_desktop_config.json`
245
- ```json
246
- { "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
247
- ```
238
+ ---
248
239
 
249
- **VS Code Copilot** — `.vscode/mcp.json` in your project root, or Command Palette → `MCP: Add Server`
250
- ```json
251
- { "servers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
252
- ```
240
+ ## CLI commands
253
241
 
254
- **Windsurf** `~/.windsurf/mcp.json`
255
- ```json
256
- { "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
242
+ | Command | What it does |
243
+ |---------|-------------|
244
+ | `carto init` | Detect project, index codebase, generate AGENTS.md, auto-wire MCP into every AI tool found |
245
+ | `carto sync` | Full re-index (skips unchanged files via mtime+size cache) |
246
+ | `carto watch` | Incremental live re-index on every file save (<50ms) |
247
+ | `carto serve` | Start MCP server (called by your AI tool — usually you don't run this directly) |
248
+ | `carto agent` | Start ACP agent mode (for Zed / JetBrains / VS Code) |
249
+ | `carto impact <file>` | Blast radius: risk level, affected files, routes at risk |
250
+ | `carto check` | Cross-domain violations, high-risk uncommitted changes, domain health |
251
+ | `carto remove` | Remove AGENTS.md and .carto/ from project |
252
+
253
+ ---
254
+
255
+ ## Benchmarks
256
+
257
+ Measured on real open-source repos. Apple M-series, 8 CPUs, 8GB RAM. SHAs pinned in `~/carto-test-repos`.
258
+
259
+ | Repo | Language | Indexed Files | First Run | Second Run | DB Size | Import Edges |
260
+ |------|----------|---------------|-----------|------------|---------|--------------|
261
+ | [prisma/prisma](https://github.com/prisma/prisma) | TypeScript | 961 | **649ms** | **110ms** | 0.7 MB | 1,387 |
262
+ | [supabase/supabase](https://github.com/supabase/supabase) | TypeScript | 6,259 | **4.8s** | **751ms** | 4.0 MB | 5,321 |
263
+ | [microsoft/vscode](https://github.com/microsoft/vscode) | TypeScript | 7,567 | **7.8s** | **387ms** | 6.7 MB | 13,420 |
264
+ | [zed-industries/zed](https://github.com/zed-industries/zed) | Rust | 1,752 | **3.0s** | **182ms** | 4.4 MB | 2,113 |
265
+
266
+ **Indexed Files** counts what Carto actually parses — `.ts/.js/.py/.go/.rs/...` after excluding `node_modules`, build output, and per-file `*.test.*` / `*.spec.*` / `*.stories.*` / `test_*.py` patterns. The on-disk file count of the repo is larger.
267
+
268
+ **Second run** = only changed files re-parsed. mtime+size checked before reading content — if nothing changed, nothing is re-parsed.
269
+
270
+ ### Domains detected
271
+
272
+ | Repo | Domains |
273
+ |------|---------|
274
+ | prisma | CORE · DATABASE · EVENTS · AUTH |
275
+ | supabase | CORE · AUTH · DATABASE · PAYMENTS · EVENTS · NOTIFICATIONS · TRPC |
276
+ | vscode | CORE · DATABASE · AUTH · EVENTS · NOTIFICATIONS |
277
+ | zed (Rust) | CORE · DATABASE · AUTH · EVENTS · PAYMENTS · NOTIFICATIONS · TRPC |
278
+
279
+ vscode at 7,567 indexed files in under 8 seconds. Rust import graph working on zed (2,113 edges from `mod` declarations and `use crate::` paths).
280
+
281
+ ---
282
+
283
+ ## How it works
284
+
285
+ ```
286
+ carto init / carto sync
287
+
288
+ Discovers all files (no cap — SQLite handles the volume)
289
+ mtime+size check → skip unchanged files
290
+ tree-sitter parse → imports + symbols (0.05–0.2ms/file)
291
+ Babel deep parse → routes + models (API handler files only)
292
+ Leiden+CPM graph clustering → auto-detects domains
293
+ Computes reverse deps → blast radius for every file
294
+ Writes AGENTS.md + .carto/context/*.md (lazy, on-demand)
295
+ Auto-wires MCP into every AI tool found
296
+
297
+ carto watch (optional)
298
+
299
+ File saved → debounce 50ms → re-parse 1 file → SQLite write → <50ms
257
300
  ```
258
301
 
259
302
  ---
260
303
 
261
304
  ## What Carto never does
262
305
 
263
- - Sends your code anywhere. Local only. SQLite on disk.
264
- - Writes secrets into AGENTS.md. `.cartoignore` blocks `.env` and credential files by default.
265
- - Touches your manual notes. Writes only between `<!-- CARTO:AUTO:START -->` and `<!-- CARTO:AUTO:END -->`.
266
- - Costs money. MIT license. Free forever.
306
+ - **Sends your code anywhere.** Local only. SQLite on disk.
307
+ - **Writes secrets into AGENTS.md.** `.cartoignore` blocks `.env` and credential files by default.
308
+ - **Touches your manual notes.** Writes only between `<!-- CARTO:AUTO:START -->` and `<!-- CARTO:AUTO:END -->`.
309
+ - **Costs money.** MIT license. Free forever.
267
310
 
268
311
  ---
269
312
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carto-md",
3
- "version": "2.0.6",
3
+ "version": "2.0.7",
4
4
  "description": "Structural intelligence layer for AI coding tools. Indexes your codebase into SQLite — routes, models, import graph, blast radius, domains — and exposes 16 MCP tools for Kiro, Cursor, and Claude.",
5
5
  "bin": {
6
6
  "carto": "src/cli/index.js"
@@ -29,7 +29,7 @@
29
29
  "tree-sitter-typescript": "0.23.2"
30
30
  },
31
31
  "engines": {
32
- "node": ">=18.0.0"
32
+ "node": ">=18.0.0 <26.0.0"
33
33
  },
34
34
  "author": {
35
35
  "name": "Ansh Sonkar",
package/src/acp/agent.js CHANGED
@@ -70,19 +70,12 @@ class CartoAgent {
70
70
  }
71
71
 
72
72
  // Provider methods
73
- async unstable_listProviders(_params) {
74
- return { providers: this.providers.list() };
75
- }
76
-
77
- async unstable_setProvider(params) {
78
- this.providers.set(params);
79
- return {};
80
- }
81
-
82
- async unstable_disableProvider(_params) {
83
- this.providers.disable();
84
- return {};
85
- }
73
+ // The custom `unstable_*` provider-management methods (list / set / disable)
74
+ // were removed in 2.0.7 (Spec 7 Bug 3). The ACP SDK only dispatches the
75
+ // methods declared in its AGENT_METHODS constant, so those custom routes
76
+ // returned -32601 "Method not found" anyway. Provider config flows
77
+ // through env vars / editor settings; ProviderRegistry remains as the
78
+ // internal configuration carrier.
86
79
 
87
80
  // Session list/load stubs
88
81
  async listSessions(_params) { return { sessions: [] }; }
@@ -30,20 +30,12 @@ class ProviderRegistry {
30
30
  this._config = null;
31
31
  }
32
32
 
33
- /**
34
- * list() Returns supported providers for ACP providers/list.
35
- */
36
- list() {
37
- return SUPPORTED_PROVIDERS.map(p => ({
38
- id: p.id,
39
- name: p.name,
40
- defaultModel: p.defaultModel,
41
- models: p.models,
42
- }));
43
- }
33
+ // list() was removed in 2.0.7 (Spec 7 Bug 3). It was only called by the
34
+ // dropped custom provider-list ACP method, which the SDK never dispatched.
35
+ // SUPPORTED_PROVIDERS is still exported for external callers / tests.
44
36
 
45
37
  /**
46
- * set(params) — Configures the active provider from ACP providers/set.
38
+ * set(params) — Configures the active provider.
47
39
  * params: { providerId, apiKey, baseUrl?, model? }
48
40
  */
49
41
  set(params) {
package/src/cli/impact.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { StoreAdapter } = require('../store/store-adapter');
4
+ const { normalizeFileArg } = require('../store/path-utils');
4
5
  const { checkForUpdate } = require('./update-check');
5
6
 
6
7
  async function run(projectRoot, fileArg) {
@@ -10,6 +11,10 @@ async function run(projectRoot, fileArg) {
10
11
  process.exit(1);
11
12
  }
12
13
 
14
+ // Normalize so `./lib/x.js`, absolute paths, and Windows separators
15
+ // all resolve to the same `files.path` value stored in SQLite.
16
+ const file = normalizeFileArg(projectRoot, fileArg);
17
+
13
18
  const carto = new StoreAdapter();
14
19
  try {
15
20
  await carto.index(projectRoot);
@@ -18,7 +23,7 @@ async function run(projectRoot, fileArg) {
18
23
  process.exit(1);
19
24
  }
20
25
 
21
- const br = carto.getBlastRadius(fileArg);
26
+ const br = carto.getBlastRadius(file);
22
27
  if (!br) {
23
28
  console.error(`[CARTO] File not found in project graph: ${fileArg}`);
24
29
  carto.close();
package/src/cli/init.js CHANGED
@@ -95,6 +95,21 @@ function wireIDEs(projectRoot) {
95
95
  const home = os.homedir();
96
96
  const wired = [];
97
97
 
98
+ // Claude Code (project-scoped). Spec 7: write .mcp.json at the project root.
99
+ // Claude Code reads this file automatically when opened in the project — no
100
+ // host-level config needed, and no detection required (it's a project-local
101
+ // file that only takes effect when the user actually uses Claude Code here).
102
+ try {
103
+ const mcpJsonPath = path.join(projectRoot, '.mcp.json');
104
+ const config = fs.existsSync(mcpJsonPath)
105
+ ? JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8'))
106
+ : { mcpServers: {} };
107
+ config.mcpServers = config.mcpServers || {};
108
+ config.mcpServers.carto = { command: 'carto', args: ['serve'] };
109
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
110
+ wired.push('Claude Code');
111
+ } catch {}
112
+
98
113
  // Kiro
99
114
  const kiroDir = path.join(home, '.kiro', 'settings');
100
115
  if (fs.existsSync(path.join(home, '.kiro'))) {
@@ -36,7 +36,11 @@ function extractImports(content, filePath, projectRoot) {
36
36
  if (['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext)) {
37
37
  rawImports = extractJSImports(content);
38
38
  } else if (ext === '.py') {
39
- rawImports = extractPythonImports(content, filePath, projectRoot);
39
+ // Python returns absolute paths from resolvePythonRelativeImport / tryResolvePythonModule.
40
+ // Relativize to project root and return early — the JS-style dedup loop below only handles
41
+ // `./` and `@/~/#` prefixed strings, so falling through silently drops every Python edge.
42
+ const abs = extractPythonImports(content, filePath, projectRoot);
43
+ return [...new Set(abs.map(p => path.relative(projectRoot, p)))].sort();
40
44
  } else if (ext === '.r') {
41
45
  return extractRImports(content, filePath, projectRoot);
42
46
  } else if (ext === '.go') {
@@ -7,9 +7,22 @@ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
7
7
  const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
8
8
  const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
9
9
  const { SQLiteStore } = require('../store/sqlite-store');
10
+ const { normalizeFileArg } = require('../store/path-utils');
10
11
 
11
12
  const projectRoot = process.cwd();
12
13
 
14
+ // Process-level safety nets — Spec 7 Bug 5c. Without these, any error that
15
+ // escapes the request handler (very rare, but possible from native bindings
16
+ // or async stack frames) takes the whole MCP server down, and Claude Code /
17
+ // Kiro surface `-32000 Failed to reconnect`. We log to stderr (which the
18
+ // host logs but never terminates the JSON-RPC channel) and stay alive.
19
+ process.on('uncaughtException', (err) => {
20
+ process.stderr.write(`[CARTO MCP] Uncaught exception: ${err && err.stack ? err.stack : err}\n`);
21
+ });
22
+ process.on('unhandledRejection', (reason) => {
23
+ process.stderr.write(`[CARTO MCP] Unhandled rejection: ${reason && reason.stack ? reason.stack : reason}\n`);
24
+ });
25
+
13
26
  // Open SQLite directly — no re-indexing, instant startup
14
27
  let store = null;
15
28
 
@@ -17,8 +30,13 @@ function getStore() {
17
30
  if (store) return store;
18
31
  const dbPath = path.join(projectRoot, '.carto', 'carto.db');
19
32
  if (!fs.existsSync(dbPath)) return null;
20
- store = new SQLiteStore(projectRoot);
21
- store.open();
33
+ // Open BEFORE assigning to the module-scoped `store` so that if open()
34
+ // throws (corrupt DB, locked file, schema mismatch) we don't poison the
35
+ // cache with a broken instance — every subsequent call would otherwise
36
+ // return the broken object and never recover.
37
+ const s = new SQLiteStore(projectRoot);
38
+ s.open({ readonly: true }); // Spec 8 — defense in depth: MCP tools never write
39
+ store = s;
22
40
  return store;
23
41
  }
24
42
 
@@ -62,8 +80,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
62
80
 
63
81
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
64
82
  const { name, arguments: args } = request.params;
65
- const s = getStore();
66
- if (!s) return notIndexed();
83
+ // Normalize file-arg tools to the canonical SQLite-stored form so
84
+ // `./lib/x.js`, absolute paths, and Windows separators all resolve.
85
+ // Tools that don't take a `file` arg are unaffected.
86
+ if (args && typeof args.file === 'string') {
87
+ args.file = normalizeFileArg(projectRoot, args.file);
88
+ }
89
+ // Wrap entire handler body so any tool error (SQLite, null deref, bad
90
+ // input) returns a structured error response instead of crashing the
91
+ // MCP transport — Spec 7 Bug 5 fix. Pre-2.0.7, an unhandled throw here
92
+ // killed the stdio connection and Claude Code/Kiro would surface
93
+ // `-32000 Failed to reconnect`.
94
+ try {
95
+ const s = getStore();
96
+ if (!s) return notIndexed();
67
97
 
68
98
  if (name === 'get_routes') {
69
99
  const routes = s.getRoutes();
@@ -105,6 +135,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
105
135
  }
106
136
 
107
137
  if (name === 'get_domain') {
138
+ // Guard: AI clients sometimes call this with no/empty `domain` arg.
139
+ // Pre-2.0.7 we'd then call args.domain.toUpperCase() on undefined and crash.
140
+ if (!args.domain || typeof args.domain !== 'string') {
141
+ return text('Missing required argument: domain. Use get_domains_list to see available domains.');
142
+ }
108
143
  const domain = s.getDomain(args.domain);
109
144
  if (!domain) return text(`Domain not found: ${args.domain}. Use get_domains_list to see available domains.`);
110
145
 
@@ -133,7 +168,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
133
168
  models: domain.models.map(m => ({
134
169
  ...m,
135
170
  className: m.name,
136
- fields: m.fields_json ? JSON.parse(m.fields_json) : []
171
+ // Wrap parse so one corrupt row doesn't take down the whole tool call.
172
+ fields: (() => {
173
+ if (!m.fields_json) return [];
174
+ try { return JSON.parse(m.fields_json); } catch { return []; }
175
+ })()
137
176
  })),
138
177
  functions: {},
139
178
  envVars: [],
@@ -446,6 +485,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
446
485
  }
447
486
 
448
487
  return text(`Unknown tool: ${name}`);
488
+ } catch (err) {
489
+ process.stderr.write(`[CARTO MCP] Tool "${name}" error: ${err.stack || err.message || err}\n`);
490
+ return {
491
+ content: [{ type: 'text', text: `Error in ${name}: ${err.message || String(err)}` }],
492
+ isError: true,
493
+ };
494
+ }
449
495
  });
450
496
 
451
497
  async function main() {
@@ -2,8 +2,11 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
4
  const DEFAULT_IGNORE_PATTERNS = [
5
+ // Env files
5
6
  '.env',
6
7
  '.env.*',
8
+
9
+ // Generic secret/credential name globs
7
10
  '*secret*',
8
11
  '*SECRET*',
9
12
  '*password*',
@@ -12,8 +15,60 @@ const DEFAULT_IGNORE_PATTERNS = [
12
15
  '*CREDENTIAL*',
13
16
  '*private_key*',
14
17
  '*PRIVATE_KEY*',
18
+
19
+ // Generic key/cert extensions
15
20
  '*.pem',
16
- '*.key'
21
+ '*.key',
22
+
23
+ // SSH keys (no extension, won't be caught by *.key)
24
+ 'id_rsa',
25
+ 'id_rsa.pub',
26
+ 'id_ed25519',
27
+ 'id_ed25519.pub',
28
+ 'id_ecdsa',
29
+ 'id_ecdsa.pub',
30
+ 'id_dsa',
31
+ 'id_dsa.pub',
32
+ 'authorized_keys',
33
+ 'known_hosts',
34
+
35
+ // Package registry credentials
36
+ '.npmrc',
37
+ '.pypirc',
38
+ '.netrc',
39
+ '.pip/pip.conf',
40
+ '.gem/credentials',
41
+ '.cargo/credentials.toml',
42
+
43
+ // Cloud / K8s / Docker
44
+ 'kubeconfig',
45
+ '*.kubeconfig',
46
+ '.docker/config.json',
47
+ '.dockercfg',
48
+ '*service-account*.json',
49
+ '*service_account*.json',
50
+ '*-credentials.json',
51
+ '.aws/credentials',
52
+ '.aws/config',
53
+ '.gcp/*',
54
+ '.gnupg/*',
55
+
56
+ // Cert / keystore formats (defense in depth — already excluded by CODE_EXTS today)
57
+ '*.crt',
58
+ '*.cer',
59
+ '*.p12',
60
+ '*.pfx',
61
+ '*.jks',
62
+ '*.keystore',
63
+ '*.pkcs12'
64
+
65
+ // NOTE: deliberately NOT including `*api_key*` / `*api-key*` globs.
66
+ // Real SaaS projects (cal.com, supabase, fastapi, zed) ship legitimate
67
+ // feature code under these filenames — `api_key.py`, `api-keys-service.ts`,
68
+ // `language_model/src/api_key.rs` — and a broad glob would silently delete
69
+ // 17+ files per repo from the import graph. Raw-credential files literally
70
+ // named `api_key.txt` are unusual; the realistic threat surface (.env,
71
+ // *credentials.json, .npmrc, id_rsa, kubeconfig, *secret*) is covered above.
17
72
  ];
18
73
 
19
74
  /**
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ /**
6
+ * normalizeFileArg(projectRoot, fileArg) → string
7
+ *
8
+ * Convert a user-provided file path into the canonical form stored in the
9
+ * SQLite `files.path` column: relative-to-project-root, forward-slashed,
10
+ * no leading `./`.
11
+ *
12
+ * Handles every form a user / IDE / shell tab-completion realistically
13
+ * produces:
14
+ * - relative form already correct ("lib/application.js" → "lib/application.js")
15
+ * - leading "./" ("./lib/application.js" → "lib/application.js")
16
+ * - leading "/" absolute under projectRoot ("/abs/.../lib/application.js" → "lib/application.js")
17
+ * - Windows backslashes ("lib\\application.js" → "lib/application.js")
18
+ * - paths containing "../" (e.g. typed from a sibling dir) — resolved.
19
+ *
20
+ * Falsy / empty input returns the input unchanged so the caller's
21
+ * "missing argument" check still fires the same way.
22
+ */
23
+ function normalizeFileArg(projectRoot, fileArg) {
24
+ if (typeof fileArg !== 'string' || fileArg.length === 0) return fileArg;
25
+
26
+ let p = fileArg;
27
+
28
+ // Normalize separators first so the absolute-path check works on Windows.
29
+ p = p.split('\\').join('/');
30
+
31
+ // Absolute → relative-to-project-root
32
+ if (path.isAbsolute(p) || /^[a-zA-Z]:\//.test(p)) {
33
+ p = path.relative(projectRoot, p);
34
+ p = p.split(path.sep).join('/');
35
+ }
36
+
37
+ // Strip leading "./" (or repeated "./././")
38
+ while (p.startsWith('./')) p = p.slice(2);
39
+
40
+ // Resolve any embedded "../" segments without going absolute.
41
+ // e.g. "src/foo/../bar.js" → "src/bar.js"
42
+ if (p.includes('/../') || p.startsWith('../')) {
43
+ const abs = path.resolve(projectRoot, p);
44
+ p = path.relative(projectRoot, abs).split(path.sep).join('/');
45
+ }
46
+
47
+ return p;
48
+ }
49
+
50
+ module.exports = { normalizeFileArg };
@@ -41,14 +41,35 @@ class SQLiteStore {
41
41
  }
42
42
 
43
43
  /**
44
- * open() — Opens or creates the database. Applies pragmas and schema.
44
+ * open(opts) — Opens or creates the database. Applies pragmas and schema.
45
+ *
46
+ * Options:
47
+ * readonly — Open the DB in read-only mode. Used by the MCP server
48
+ * (`carto serve`) so a malformed/buggy tool can never write
49
+ * through the SQLite layer. Skips mkdir, schema bootstrap, and
50
+ * WAL pragma (WAL needs write capability and would otherwise
51
+ * create `carto.db-wal`/`carto.db-shm` in repos that only run
52
+ * the MCP server). Sets `fileMustExist: true` so a missing DB
53
+ * returns a clear SQLite error instead of silently creating an
54
+ * empty file in the wrong location.
45
55
  */
46
- open() {
56
+ open(opts = {}) {
47
57
  const cartoDir = path.join(this._projectRoot, '.carto');
48
- fs.mkdirSync(cartoDir, { recursive: true });
49
-
50
58
  const dbPath = path.join(cartoDir, 'carto.db');
51
59
 
60
+ if (opts.readonly) {
61
+ // Read-only path: dir must already exist (a writer process — `carto sync`
62
+ // or `carto init` — created the DB). Don't mkdir, don't ensure schema,
63
+ // don't apply write-only pragmas.
64
+ this._db = new Database(dbPath, { readonly: true, fileMustExist: true });
65
+ // Read-only safe pragmas only:
66
+ this._db.pragma('busy_timeout = 5000');
67
+ this._db.pragma('cache_size = -64000'); // 64MB cache
68
+ return this;
69
+ }
70
+
71
+ fs.mkdirSync(cartoDir, { recursive: true });
72
+
52
73
  try {
53
74
  this._db = new Database(dbPath);
54
75
  } catch (err) {
@@ -618,7 +639,12 @@ class SQLiteStore {
618
639
  },
619
640
  entryPoints,
620
641
  highImpact,
621
- stack: stack ? JSON.parse(stack) : [],
642
+ // Defensive parse a corrupt stack_json row must never crash callers.
643
+ // Spec 7 Bug 5f.
644
+ stack: (() => {
645
+ if (!stack) return [];
646
+ try { return JSON.parse(stack); } catch { return []; }
647
+ })(),
622
648
  domains
623
649
  };
624
650
  }