@debugai/mcp 2.4.2 → 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.
@@ -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
+ */