@debugai/mcp 2.4.3 → 2.5.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/README.md +7 -0
- package/dist/backend.d.ts +11 -0
- package/dist/gatherWorkspaceFacts.d.ts +64 -0
- package/dist/gatherWorkspaceFacts.js +369 -0
- package/dist/tools/debugError.js +12 -0
- package/dist/workspaceFacts.d.ts +336 -0
- package/dist/workspaceFacts.js +634 -0
- package/package.json +1 -1
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the editor can SEE about this workspace, sent as facts the engine reads.
|
|
3
|
+
*
|
|
4
|
+
* ## The hole this fills
|
|
5
|
+
*
|
|
6
|
+
* Every wrong answer in the 2026-09-10 production read needed one fact that is
|
|
7
|
+
* not in the traceback and never can be, because it is a property of the
|
|
8
|
+
* machine rather than of the error:
|
|
9
|
+
*
|
|
10
|
+
* No module named 'ip_connection_monitor' -> "pip install ip_connection_monitor"
|
|
11
|
+
* the fact: that name is a .py file sitting in their own project
|
|
12
|
+
*
|
|
13
|
+
* No module named 'numpy' x3, one user, three days -> "pip install numpy"
|
|
14
|
+
* the fact: numpy was installed the whole time, under another interpreter
|
|
15
|
+
*
|
|
16
|
+
* import Flask, jsonify, ... -> "pip install Flask"
|
|
17
|
+
* the fact: flask is declared in their own requirements.txt
|
|
18
|
+
*
|
|
19
|
+
* The engine is server-side and will never touch a user's disk. The extension
|
|
20
|
+
* is already ON that disk — and has been reading `package.json`,
|
|
21
|
+
* `requirements.txt` and `pyproject.toml` since v2.1, at
|
|
22
|
+
* `detectWorkspaceFramework()`, purely to guess a one-word framework label. It
|
|
23
|
+
* has been reading the right files for the smallest possible purpose.
|
|
24
|
+
*
|
|
25
|
+
* Nothing here executes anything. Every field is a file read or a read-only
|
|
26
|
+
* VS Code API call, which is what an editor extension already is, so there is
|
|
27
|
+
* no new permission, no consent prompt and no new execution path. Running a
|
|
28
|
+
* command is a separate decision with a separate threat model (see the note on
|
|
29
|
+
* Tier 2 at the bottom) and deliberately not this module.
|
|
30
|
+
*
|
|
31
|
+
* ## The one law: observations, never classifications
|
|
32
|
+
*
|
|
33
|
+
* This module reports what is on disk. It never concludes what it means.
|
|
34
|
+
* "`ip_connection_monitor` is one of your files" and "install it" are both
|
|
35
|
+
* decisions, and decisions live in the engine — beside the error pattern that
|
|
36
|
+
* raised the question, in one place, where they already are.
|
|
37
|
+
*
|
|
38
|
+
* That is not style. It is the same law as Critical Rule 10 (the waitlist never
|
|
39
|
+
* trusts a client-computed privilege field) and the attribution rule (the client
|
|
40
|
+
* reports src and referrer, the SERVER computes `medium`). Applied here it also
|
|
41
|
+
* buys something concrete: the name normalisation that decides whether
|
|
42
|
+
* `python-dotenv` matches `python_dotenv` exists in exactly one file, so there
|
|
43
|
+
* is no cross-boundary copy to drift and no parity test to keep (Rule 15b).
|
|
44
|
+
*
|
|
45
|
+
* ## The other law: a capped list cannot prove an absence
|
|
46
|
+
*
|
|
47
|
+
* `local_modules` and `declared_deps` are capped, because a monorepo has
|
|
48
|
+
* thousands of files and this rides on a request a user is waiting for. A cap
|
|
49
|
+
* is the difference between "numpy is not one of your files" and "numpy is not
|
|
50
|
+
* in the first 600 of your files", and answering the first when you only know
|
|
51
|
+
* the second is how a confident wrong answer gets built.
|
|
52
|
+
*
|
|
53
|
+
* So every list that might be short names itself in `incomplete`, and the
|
|
54
|
+
* engine may draw a POSITIVE conclusion from any list (the name is there, so it
|
|
55
|
+
* exists) but a NEGATIVE one only from a list that is not listed there. Same
|
|
56
|
+
* judgement as MIN_SAMPLE in scanFindings and MIN_COHORT in attribution: an
|
|
57
|
+
* instrument that cannot say "I do not know" will say something false instead.
|
|
58
|
+
*
|
|
59
|
+
* Pure: no `vscode` import, no I/O. The caller supplies the file contents and
|
|
60
|
+
* the directory listings, which is what makes the whole thing testable with no
|
|
61
|
+
* editor — same split as `scanCandidates.ts` and `errorPaths.ts`.
|
|
62
|
+
*
|
|
63
|
+
* ## MIRRORED, byte for byte, in two other places
|
|
64
|
+
*
|
|
65
|
+
* packages/mcp/src/workspaceFacts.ts (published to npm, standalone)
|
|
66
|
+
* apps/extension/src/mcp/workspaceFacts.ts (bundled MCP server)
|
|
67
|
+
*
|
|
68
|
+
* This file has ZERO imports, which is what makes a byte-identical copy the
|
|
69
|
+
* whole of the solution rather than the start of a divergence. `npm publish`
|
|
70
|
+
* cannot reach into `apps/`, so the copies are not optional (Rule 15b).
|
|
71
|
+
*
|
|
72
|
+
* Do not edit a copy. Edit THIS file and re-copy:
|
|
73
|
+
*
|
|
74
|
+
* cp apps/extension/src/workspaceFacts.ts packages/mcp/src/workspaceFacts.ts
|
|
75
|
+
* cp apps/extension/src/workspaceFacts.ts apps/extension/src/mcp/workspaceFacts.ts
|
|
76
|
+
*
|
|
77
|
+
* `mcp/drift.test.ts` compares all three as text and fails on any difference,
|
|
78
|
+
* headers included - deliberately stricter than the other mirrored modules,
|
|
79
|
+
* because this one has no import line to anchor a partial comparison to.
|
|
80
|
+
*/
|
|
81
|
+
/** A list whose absence-of-a-name means nothing. See the module docstring. */
|
|
82
|
+
export type IncompleteList = 'declared_deps' | 'local_modules';
|
|
83
|
+
export interface WorkspaceFacts {
|
|
84
|
+
/**
|
|
85
|
+
* Dependency names exactly as the manifests spell them — `python-dotenv`,
|
|
86
|
+
* `@babel/core`, `Flask`. NOT normalised: normalising is comparing, and
|
|
87
|
+
* comparing is the engine's job (see the law above).
|
|
88
|
+
*/
|
|
89
|
+
declared_deps: string[];
|
|
90
|
+
/**
|
|
91
|
+
* Version as the manifest SPELLS it, keyed by the same raw name:
|
|
92
|
+
* `{"Yarp.ReverseProxy": "2.3.0", "react": "^18.2.0", "flask": ">=3"}`.
|
|
93
|
+
*
|
|
94
|
+
* Not resolved, and that is the point. `2.3.0` is a fact about what is
|
|
95
|
+
* installed; `^18.2.0` is a fact about what is ALLOWED, and the difference
|
|
96
|
+
* is information the engine needs - on 2026-09-12 we told a user to call
|
|
97
|
+
* `.AddPassiveHealthCheckPolicies()` on YARP at confidence 92 and the
|
|
98
|
+
* method does not exist in the 2.3.0 their own .csproj declares. An exact
|
|
99
|
+
* pin can settle that question; a range cannot, and the engine must be able
|
|
100
|
+
* to tell which one it has.
|
|
101
|
+
*
|
|
102
|
+
* A name declared in two manifests with two versions keeps the first seen.
|
|
103
|
+
* The alternative is a list per name, which costs the whole shape for a
|
|
104
|
+
* case (one package, two versions, one workspace) that is itself a bug.
|
|
105
|
+
*/
|
|
106
|
+
declared_versions: Record<string, string>;
|
|
107
|
+
/**
|
|
108
|
+
* Which manifest files those names came from, relative to the root. Load
|
|
109
|
+
* bearing: an empty `declared_deps` means "declares nothing" when a
|
|
110
|
+
* manifest was read and "we found no manifest" when this is empty, and
|
|
111
|
+
* those support opposite conclusions.
|
|
112
|
+
*/
|
|
113
|
+
manifests: string[];
|
|
114
|
+
/**
|
|
115
|
+
* Every name in this workspace that Python could import as a top-level
|
|
116
|
+
* module: `foo.py` -> `foo`, `foo/__init__.py` -> `foo`. Deduped across
|
|
117
|
+
* directories, because `sys.path[0]` is the running script's own directory
|
|
118
|
+
* and a sibling module is importable from there without being at the root.
|
|
119
|
+
*/
|
|
120
|
+
local_modules: string[];
|
|
121
|
+
/** Root directories that look like a virtualenv: `.venv`, `venv`, `env`. */
|
|
122
|
+
venv_dirs: string[];
|
|
123
|
+
/** A `node_modules` directory exists at the workspace root. */
|
|
124
|
+
has_node_modules: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* The interpreter VS Code currently has selected, from the Python
|
|
127
|
+
* extension's read-only environments API. `null` when the Python extension
|
|
128
|
+
* is not installed, not activated, or has no selection — three states that
|
|
129
|
+
* mean "we do not know", never "there isn't one".
|
|
130
|
+
*/
|
|
131
|
+
python_interpreter: string | null;
|
|
132
|
+
/** Lists that may be missing entries. Absence from one of these proves nothing. */
|
|
133
|
+
incomplete: IncompleteList[];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Caps. Chosen so an ordinary project is never truncated and a monorepo is
|
|
137
|
+
* truncated honestly rather than slowly. `findFiles` is given `cap + 1` so the
|
|
138
|
+
* overflow is detected exactly rather than guessed at the boundary.
|
|
139
|
+
*/
|
|
140
|
+
export declare const MAX_DECLARED_DEPS = 400;
|
|
141
|
+
/** Versions are only kept for names that survived the dep cap. */
|
|
142
|
+
export declare const MAX_VERSION_LENGTH = 60;
|
|
143
|
+
export declare const MAX_LOCAL_MODULES = 600;
|
|
144
|
+
/**
|
|
145
|
+
* Dependency names declared in a `package.json`, across all four dependency
|
|
146
|
+
* fields.
|
|
147
|
+
*
|
|
148
|
+
* `peerDependencies` and `optionalDependencies` are included on purpose: a
|
|
149
|
+
* `Cannot find module` for a peer dep is a real and common shape (the package
|
|
150
|
+
* is declared, expects you to install it, and nothing did), and leaving them
|
|
151
|
+
* out would make the engine conclude "not declared" about a name that is.
|
|
152
|
+
*
|
|
153
|
+
* Never throws. A malformed `package.json` is itself a plausible thing to be
|
|
154
|
+
* debugging, and losing the whole fact set to it would be absurd.
|
|
155
|
+
*/
|
|
156
|
+
export declare function depsFromPackageJson(text: string): string[];
|
|
157
|
+
/**
|
|
158
|
+
* Package names from a `requirements.txt`.
|
|
159
|
+
*
|
|
160
|
+
* A requirements line carries far more than a name — version specifiers,
|
|
161
|
+
* extras, environment markers, hashes, editable installs, includes, direct
|
|
162
|
+
* URLs — and every one of those forms appears in real projects. The name is
|
|
163
|
+
* whatever survives stripping them, and a line with no name left (a bare `-r
|
|
164
|
+
* base.txt`, a comment, a flag) contributes nothing rather than contributing
|
|
165
|
+
* garbage.
|
|
166
|
+
*
|
|
167
|
+
* `-r other.txt` is NOT followed. Following it would need another file read
|
|
168
|
+
* driven by content we just parsed, and the honest alternative is cheaper: the
|
|
169
|
+
* caller marks `declared_deps` incomplete, so a name missing from the list
|
|
170
|
+
* proves nothing, which is exactly the truth about a split requirements file.
|
|
171
|
+
*/
|
|
172
|
+
export declare function depsFromRequirements(text: string): {
|
|
173
|
+
deps: string[];
|
|
174
|
+
sawInclude: boolean;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Package names from a `pyproject.toml`, without a TOML parser.
|
|
178
|
+
*
|
|
179
|
+
* Bundling one would be the honest engineering answer if the names had to be
|
|
180
|
+
* exact. They do not: a name that survives this is real (a positive conclusion,
|
|
181
|
+
* always safe), and a name this misses is covered by the caller marking the
|
|
182
|
+
* list incomplete (so no negative conclusion is drawn from it). That trade buys
|
|
183
|
+
* out a dependency in a bundle that ships to 652 installs.
|
|
184
|
+
*
|
|
185
|
+
* Two shapes, which between them cover almost every real file:
|
|
186
|
+
*
|
|
187
|
+
* [project] PEP 621, the modern default
|
|
188
|
+
* dependencies = ["flask>=3", "python-dotenv"]
|
|
189
|
+
*
|
|
190
|
+
* [tool.poetry.dependencies] poetry, still very common
|
|
191
|
+
* flask = "^3.0"
|
|
192
|
+
*
|
|
193
|
+
* `found` is false when the file exists but neither shape was recognised —
|
|
194
|
+
* pdm, hatch's dynamic metadata, a dependency-groups-only file. The caller
|
|
195
|
+
* needs that to be a different state from "no pyproject at all", because one
|
|
196
|
+
* means "we could not read the declarations" and the other means "there are
|
|
197
|
+
* none to read".
|
|
198
|
+
*/
|
|
199
|
+
export declare function depsFromPyproject(text: string): {
|
|
200
|
+
deps: string[];
|
|
201
|
+
found: boolean;
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Versions from a `package.json`, keyed by dependency name.
|
|
205
|
+
*
|
|
206
|
+
* A SEPARATE pass rather than a change to `depsFromPackageJson`, on purpose.
|
|
207
|
+
* That function's contract and its tests are shipped and correct; widening a
|
|
208
|
+
* return type to carry a new field is how a working parser acquires a
|
|
209
|
+
* regression. These files are small and already capped, so the second pass
|
|
210
|
+
* costs nothing measurable.
|
|
211
|
+
*/
|
|
212
|
+
export declare function versionsFromPackageJson(text: string): Record<string, string>;
|
|
213
|
+
/**
|
|
214
|
+
* Versions from a `requirements.txt`, keyed by package name.
|
|
215
|
+
*
|
|
216
|
+
* Keeps the whole specifier as written - `==3.0.0`, `>=2,<3`, `~=1.4` - because
|
|
217
|
+
* `==` is a fact about what IS installed and `>=` is a fact about what is
|
|
218
|
+
* allowed, and collapsing them would throw away the distinction the engine
|
|
219
|
+
* needs most.
|
|
220
|
+
*
|
|
221
|
+
* A line with no specifier contributes a NAME (via `depsFromRequirements`) and
|
|
222
|
+
* no version, which is honest: `flask` on its own line pins nothing.
|
|
223
|
+
*
|
|
224
|
+
* Never reads an `--index-url`. Those carry credentials in real projects
|
|
225
|
+
* (`https://user:token@pypi.internal/simple`) and a package's version is never
|
|
226
|
+
* on that line anyway.
|
|
227
|
+
*/
|
|
228
|
+
export declare function versionsFromRequirements(text: string): Record<string, string>;
|
|
229
|
+
/**
|
|
230
|
+
* Versions from a `pyproject.toml`, keyed by package name.
|
|
231
|
+
*
|
|
232
|
+
* Same two shapes `depsFromPyproject` recognises, and the same trade: a version
|
|
233
|
+
* this finds is real, and one it misses simply is not reported. `declared_deps`
|
|
234
|
+
* already carries `incomplete` for the negative case, and a missing VERSION is
|
|
235
|
+
* never read as "no version constraint" - only as "we did not see one".
|
|
236
|
+
*/
|
|
237
|
+
export declare function versionsFromPyproject(text: string): Record<string, string>;
|
|
238
|
+
/**
|
|
239
|
+
* Package names and versions from a .NET project file (`.csproj` / `.fsproj`).
|
|
240
|
+
*
|
|
241
|
+
* The manifest that would have prevented 2026-09-12. Four consecutive answers
|
|
242
|
+
* guessed at `Elastic.Clients.Elasticsearch` constructor signatures and a fifth
|
|
243
|
+
* invented `.AddPassiveHealthCheckPolicies()` on YARP - while the user's own
|
|
244
|
+
* project file said `Version="2.3.0"` the entire time, and their error text
|
|
245
|
+
* even quoted it back to us.
|
|
246
|
+
*
|
|
247
|
+
* .NET pins are usually EXACT, which makes this the highest-value manifest in
|
|
248
|
+
* the set: `<PackageReference Include="X" Version="2.3.0" />` settles "does
|
|
249
|
+
* this method exist" in a way `^18.2.0` never can.
|
|
250
|
+
*
|
|
251
|
+
* Two spellings, both common and both emitted by `dotnet add package`:
|
|
252
|
+
*
|
|
253
|
+
* <PackageReference Include="X" Version="2.3.0" />
|
|
254
|
+
* <PackageReference Include="X"><Version>2.3.0</Version></PackageReference>
|
|
255
|
+
*
|
|
256
|
+
* Regex rather than an XML parser, for the same reason `depsFromPyproject`
|
|
257
|
+
* avoids a TOML parser: a name this finds is real, a name it misses is covered
|
|
258
|
+
* by `incomplete`, and neither justifies a dependency in a bundle that ships to
|
|
259
|
+
* every install. Attribute order is not assumed - `Version` before `Include`
|
|
260
|
+
* is legal and real.
|
|
261
|
+
*/
|
|
262
|
+
export declare function depsFromCsproj(text: string): {
|
|
263
|
+
deps: string[];
|
|
264
|
+
versions: Record<string, string>;
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* The top-level module name a Python file would be imported by, or null.
|
|
268
|
+
*
|
|
269
|
+
* `a/b/foo.py` -> `foo`, `a/b/foo/__init__.py` -> `foo`. A dunder file other
|
|
270
|
+
* than `__init__` (`__main__.py`, `__about__.py`) names no importable module
|
|
271
|
+
* and is dropped, so it cannot be mistaken for one.
|
|
272
|
+
*/
|
|
273
|
+
export declare function moduleNameForPath(relPath: string): string | null;
|
|
274
|
+
/** Raw inputs, exactly as the editor hands them over. Every field optional. */
|
|
275
|
+
export interface FactInputs {
|
|
276
|
+
packageJson?: string | null;
|
|
277
|
+
requirementsTxt?: string | null;
|
|
278
|
+
pyprojectToml?: string | null;
|
|
279
|
+
/**
|
|
280
|
+
* Contents of every .NET project file found, newest-irrelevant order. A
|
|
281
|
+
* list because a solution has one per project and the error can come from
|
|
282
|
+
* any of them - salman's repo had six.
|
|
283
|
+
*/
|
|
284
|
+
csprojFiles?: string[];
|
|
285
|
+
/** True when the caller stopped looking for .csproj files before exhausting them. */
|
|
286
|
+
csprojTruncated?: boolean;
|
|
287
|
+
/** Workspace-relative paths of every `.py` file the caller found. */
|
|
288
|
+
pythonFiles?: string[];
|
|
289
|
+
/** True when the caller's file search hit its own limit. */
|
|
290
|
+
pythonFilesTruncated?: boolean;
|
|
291
|
+
/** Root directory names the caller saw. */
|
|
292
|
+
rootDirs?: string[];
|
|
293
|
+
/** From the Python extension's environments API. */
|
|
294
|
+
pythonInterpreter?: string | null;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Assemble the facts. Total function: any subset of inputs, no throw.
|
|
298
|
+
*
|
|
299
|
+
* Every list is deduped and sorted before capping, so what survives a cap is
|
|
300
|
+
* stable across requests rather than dependent on filesystem walk order — a
|
|
301
|
+
* cache key reads this, and a set that reshuffles per request would miss the
|
|
302
|
+
* cache every time on exactly the large workspaces where a miss costs most.
|
|
303
|
+
*/
|
|
304
|
+
export declare function buildWorkspaceFacts(input: FactInputs): WorkspaceFacts;
|
|
305
|
+
/**
|
|
306
|
+
* True when these facts carry nothing the engine could act on.
|
|
307
|
+
*
|
|
308
|
+
* An empty fact set is not neutral: it costs bytes on every request, and — far
|
|
309
|
+
* worse — it takes part in the response cache key, so sending an all-empty
|
|
310
|
+
* object from a window with no folder open would split the cache between
|
|
311
|
+
* "no facts" and "no facts, spelled out" for no gain. Send nothing instead.
|
|
312
|
+
*/
|
|
313
|
+
export declare function factsAreEmpty(f: WorkspaceFacts): boolean;
|
|
314
|
+
/**
|
|
315
|
+
* NOT IN THIS MODULE, on purpose: anything that runs.
|
|
316
|
+
*
|
|
317
|
+
* `python -c "import sys; print(sys.executable)"` settles the wrong-interpreter
|
|
318
|
+
* case outright, and `python_interpreter` above only reports what VS Code has
|
|
319
|
+
* SELECTED — which is the right answer for a Run button and not necessarily the
|
|
320
|
+
* interpreter that produced a traceback pasted from an external terminal.
|
|
321
|
+
*
|
|
322
|
+
* Closing that gap means executing something, and the 2026 record of agentic
|
|
323
|
+
* tools is unambiguous about how that goes wrong: every published incident is a
|
|
324
|
+
* variant of one mistake, treating a command STRING as data. Aider ran a
|
|
325
|
+
* repo-supplied `test-cmd` at startup (#5254). Claude Code's allowlist admitted
|
|
326
|
+
* `man --html="touch ..."` (CVE-2025-66032). Cursor's approval dialog showed a
|
|
327
|
+
* workspace path while the write followed a symlink to `~/.ssh/authorized_keys`
|
|
328
|
+
* (CVE-2026-50549).
|
|
329
|
+
*
|
|
330
|
+
* So when Tier 2 lands, the server names a PROBE and its parameters from a
|
|
331
|
+
* catalog this client owns — `{probe: "interpreter"}`, never a command string —
|
|
332
|
+
* the client maps that to argv it wrote itself, and an unknown probe name is
|
|
333
|
+
* refused rather than passed through. That is the difference between MCP
|
|
334
|
+
* elicitation and every incident above, and it is a different module with a
|
|
335
|
+
* consent gate, not a quiet extension of this one.
|
|
336
|
+
*/
|