Skip to content

Commit 5f193ee

Browse files
authored
fix: preserve VS Code JSONC configurations through MCP lifecycle (#260)
* fix: preserve realistic VS Code JSONC configurations * fix: preserve JSONC removal and bind candidate dist files * test: retain Claude post-removal native evidence * test: synchronize native cancellation with actual wire call * docs: record independently verified realistic native validation
1 parent f665f71 commit 5f193ee

21 files changed

Lines changed: 555 additions & 49 deletions

‎CHANGELOG.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@ _Add entries here, never under a stamped version_ — a release commit renames t
88
heading, and a branch that wrote beneath it merges without conflict straight into a
99
published section (it happened to #170).
1010

11+
### Fixed
12+
13+
- VS Code user-global `mcp.json` accepts comments and trailing commas. Install,
14+
guard, update, toggle and remove edit only the selected server, preserving
15+
unrelated configuration/comments and the original raw backup. Invalid JSONC
16+
and ambiguous duplicate server paths refuse before writes. Other clients keep
17+
strict JSON parsing; managed configuration locations are unchanged.
18+
19+
### Added
20+
21+
- Explicit candidate-artifact native validation, with archive integrity and
22+
complete executable file-set hashes, plus realistic JSONC/input/environment-variable fixtures.
23+
The default published baseline remains 0.47.1; live model and normal approval
24+
behavior remain outside these tests.
25+
1126
## [0.47.2] - 2026-10-12
1227

1328
PATCH by content: native-client test tooling and documentation only; production

‎CLAUDE.md‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,21 @@ repinned to actual 0.47.2 passes 40 comparisons, with zero failures, two expecte
3434
unsupported combinations and seven self-checks; its locked integrity matches
3535
the verified npm tarball. The committed harness baselines remain unchanged.
3636

37-
**Native validation tooling:** `scripts/native-client/` retains published mcpm
37+
**Unreleased Phase 2B:** VS Code user-global configuration reads JSONC and edits
38+
only the selected server while preserving unrelated bytes/comments and the raw
39+
backup. Invalid/duplicate server paths fail before writes; other clients remain
40+
strict JSON. Microsoft `jsonc-parser` 3.3.1 is the sole new production dependency.
41+
The explicit native candidate mode binds archive/lock/package/complete dist-file
42+
identity and exercises actual client input/environment expansion. Independent
43+
final candidate and default published-baseline runs both pass 8/8 VS Code and
44+
7/7 Claude Code, with matching hashes, raw preservation and cleanup. Source
45+
checks: 3,588 tests, typecheck/build and 13 harness self-checks pass. Astra fixed
46+
inherited-key removal, incomplete chunk binding and a pre-wire cancellation
47+
fixture race; cancellation requires the current matching request before firing.
48+
User-global paths are unchanged; portable/Agent Host routes and normal approval,
49+
provider/model/usage-skill behavior remain unverified.
50+
51+
**Released native validation baseline (v0.47.2):** `scripts/native-client/` retains published mcpm
3852
0.47.1 as its fixed baseline. VS Code 1.141.0 passed 8/8 and Claude Code 2.1.296
3953
passed 7/7 in separate disposable macOS 26.6.2/arm64 VMs, independently repeated
4054
against the final reviewed harness with matching host/guest hashes and cleanup.

‎CONTRIBUTING.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ For installed VS Code/Claude Code checks inside disposable macOS VMs, see
6868
[native client validation](docs/NATIVE-CLIENT-VALIDATION.md). Its fixture/evidence
6969
self-checks run with `node --test scripts/native-client/self-check.mjs`; native
7070
VM runs require the separately pinned public client/runtime payload.
71+
Candidate changes must use the explicit candidate mode and a fresh packed CLI;
72+
a passing published-baseline run does not validate changed production code.
7173

7274
## Ground rules
7375

‎README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,13 @@ permission grants, model subscription or external decision service.
594594
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
595595
| Gemini CLI | `~/.gemini/settings.json` (user-global `mcpServers`) |
596596

597+
VS Code's managed `mcp.json` supports JSONC comments and trailing commas. Changes
598+
edit the selected server while preserving unrelated settings/comments and the
599+
original backup. Other clients retain strict JSON parsing. Variable references
600+
are retained for the client to resolve. Portable/workspace configurations and
601+
Copilot Agent Host configuration paths are not managed by these adapters; see
602+
the [native validation scope](docs/NATIVE-CLIENT-VALIDATION.md).
603+
597604
Linux and Windows paths are also supported. See `mcpm doctor` to verify which clients are detected on your system.
598605

599606
## How it works

‎docs/GUARD.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ Use during debugging when guard is in the way and you want to turn it off withou
253253
### `mcpm guard cleanup [--yes]`
254254
Prunes pin entries for servers no longer installed in any client config. Dry-run by default; pass `--yes` to apply.
255255

256-
A server whose config entry is malformed still counts as installed, so its pin is kept. A detected client's config that **cannot be read** (unparseable — mcpm reads configs as strict JSON, so comments and trailing commas count — or a permission error, including on the directory that holds it) holds servers cleanup cannot see. So if any pin is held by no readable config, cleanup refuses — in the dry run and with `--yes` alike — names each unreadable client, its config path and the error, prunes nothing, and exits `1`: that pin cannot be told apart from one belonging to a live server there, and pruning it would erase the server's rug-pull baseline. Fix the config and re-run. If every pin is held by a readable config, the unreadable one cannot change the result: cleanup reports nothing to prune, notes the unreadable config, and exits `0`. A client with no config file at all is not an error. A refusal because `pins.json` fails its integrity check also exits `1`.
256+
A server whose config entry is malformed still counts as installed, so its pin is kept. A detected client's config that **cannot be read** (unparseable — VS Code permits JSONC comments/trailing commas, while the other clients use strict JSON — or a permission error, including on the directory that holds it) holds servers cleanup cannot see. So if any pin is held by no readable config, cleanup refuses — in the dry run and with `--yes` alike — names each unreadable client, its config path and the error, prunes nothing, and exits `1`: that pin cannot be told apart from one belonging to a live server there, and pruning it would erase the server's rug-pull baseline. Fix the config and re-run. If every pin is held by a readable config, the unreadable one cannot change the result: cleanup reports nothing to prune, notes the unreadable config, and exits `0`. A client with no config file at all is not an error. A refusal because `pins.json` fails its integrity check also exits `1`.
257257

258258
### `mcpm guard inspect [file] [--json]`
259259

‎docs/NATIVE-CLIENT-VALIDATION.md‎

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,63 @@ archive hashes and installs npm dependencies with lifecycle scripts disabled.
3838
Claude Code's reviewed, pinned native-binary installer runs inside the offline
3939
guest to connect its preloaded platform binary to the CLI entry point.
4040

41+
### Test a candidate with realistic configuration
42+
43+
Build and pack the current checkout, then explicitly prepare/run that artifact:
44+
45+
```sh
46+
mkdir -p /tmp/mcpm-candidate
47+
pnpm build
48+
pnpm pack --pack-destination /tmp/mcpm-candidate
49+
node scripts/native-client/prepare.mjs /tmp/mcpm-candidate-payload /tmp/mcpm-candidate/getmcpm-cli-0.15.0.tgz
50+
node scripts/native-client/run-vm.mjs /path/to/tart \
51+
/tmp/mcpm-native-vms /tmp/mcpm-candidate-payload /tmp/mcpm-candidate-evidence \
52+
--candidate --realistic-config
53+
```
54+
55+
The source package uses placeholder version 0.15.0; that candidate is not the
56+
published 0.15.0 release. `cliArtifact.kind`, archive SHA-256/SHA-512, lock integrity
57+
and installed file hashes identify it. Candidate mode verifies that the package
58+
manifest and complete `dist` file set/bytes match the archive (including lazy
59+
chunks, missing files and extra files), and that guest/host identities agree. It
60+
does not establish source provenance; record the source revision/diff used for
61+
the fresh build separately. The default published baseline and pins are unchanged.
62+
Candidate preparation freezes its own dependency lock with scripts disabled.
63+
64+
The realistic suite adds comments/trailing commas and input declarations to VS
65+
Code's actual user-global file. Ordinary declared environment values use client
66+
variable references, including quotes/backslashes; VS Code also resolves a known
67+
synthetic input prompt. Claude Code exercises environment/default references in
68+
its user-global file. Echo replies must contain the exact resolved values before
69+
and after wrapping/update. Raw backup bytes, input declarations and unrelated
70+
comments/settings must survive the lifecycle. All eight/seven baseline case
71+
requirements still apply. The helper accepts only the named fixture prompt with
72+
its exact prefilled synthetic value; account/sign-in prompts remain excluded.
73+
74+
VS Code parsing uses Microsoft's `jsonc-parser`, checks every parse error and
75+
rejects duplicate server containers/names before edits. Only the managed entry
76+
is edited; removal can retain a legal trailing comma. Other clients continue
77+
to use strict JSON. This does not migrate configuration scopes or resolve
78+
variables inside mcpm.
79+
80+
Current [VS Code documentation](https://code.visualstudio.com/docs/agent-customization/mcp-servers)
81+
prefers portable `.mcp.json`/Copilot global locations and distinguishes Agent Host
82+
routing. This harness still measures the adapter's user-profile `mcp.json` through
83+
the native Tool API. Portable/workspace paths, alternate profiles and Agent Host
84+
chat routing remain separate, unverified integration paths.
85+
86+
Reviewed and independently repeated local measurements on 2026-10-12 passed
87+
**8/8 VS Code and 7/7 Claude Code** in both the realistic candidate suite and the
88+
unchanged published-baseline mode. These use macOS 26.6.2/arm64 and the named
89+
client versions below. Host/guest hashes, all 134 candidate distribution files,
90+
raw configuration/backups, actual variable bindings, correlated cancellation,
91+
post-removal receipts and cleanup were checked. All 3,588 source tests,
92+
typecheck/build and 13 harness self-checks passed. Astra fixed inherited-key
93+
server removal, incomplete candidate chunk identity and a pre-wire cancellation
94+
fixture race; cancellation now waits for the current slow request before firing.
95+
The managed server entry can be reformatted; comments/settings outside it and
96+
the original raw backup are preserved.
97+
4198
## Boundary and fixture scope
4299

43100
The runner clones the immutable image independently for **VS Code first, then
@@ -102,7 +159,7 @@ VS Code completion for drift. A separate interrupted VM run verifies cleanup.
102159
| Install and echo | mcpm writes the actual global file; native opening identifies the client and a real tool call returns the exact nonce/version/marker. |
103160
| Enable, block and recovery | The app loads the wrapped launcher; attack content produces the guard block message, then an echo succeeds. Declaration/tool pins contain real hashes. |
104161
| Warn and forward | The existing Unicode-warning fixture arrives unchanged and the guard records its expected warning signature. |
105-
| Cancel and recover | VS Code forwards correlated cancellation and continues. Claude Code is deliberately interrupted during a real slow call; a new invocation succeeds. A deadline alone cannot count as cancellation. |
162+
| Cancel and recover | VS Code waits up to 10 seconds for its current slow call to reach the peer, then forwards correlated cancellation and continues. Claude Code is deliberately interrupted during a real slow call; a new invocation succeeds. A deadline or cancellation before the wire call cannot count as success. |
106163
| VS Code reload | A real window reload reestablishes MCP; the repeated echo has a different peer PID. |
107164
| Update | The public CLI updates the fixture package coordinate while retaining the guard, extra arguments, environment and unrelated configuration. The native tool reports 2.0.0. |
108165
| Schema drift | Changed tool schema is blocked, no call reaches the peer, pins remain unchanged, and the attempt finishes without a deadline kill. Reverting the fixture restores a successful native call. |
@@ -114,7 +171,9 @@ hashes, and rejects empty/incomplete success reports or missing/reused
114171
observations. Per-case JSON records include execution status/deadlines, client receipts, synthetic wire
115172
traffic, configuration, pins and events from that invocation. Each observation
116173
has a unique filename linked from its case; recovery calls cannot overwrite
117-
earlier evidence. `manifest.json` identifies pins, flags, VM configuration,
174+
earlier evidence. The final removal observation includes raw configuration and
175+
backup bytes; Claude Code also exports its `mcp get` not-found receipt.
176+
`manifest.json` identifies pins, flags, VM configuration,
118177
all guest fixture/extension hashes, host harness hashes and cleanup.
119178
Read failures and unexpected client behavior are
120179
failures; they are not silently classified as unsupported.
@@ -133,7 +192,7 @@ evidence local; publish scoped results rather than entire client transcripts.
133192
## Remaining verification
134193

135194
Live providers and actual model/usage-skill behavior, interactive chat/default
136-
approval workflows, other clients/platforms, JSONC/input-variable configuration,
195+
approval workflows, other clients/platforms, additional configuration variants,
137196
and complete protocol conformance remain unverified. Native client versions
138197
and observed wire protocol must be recorded rather than inferred from package
139198
versions. Updating pins requires repeating the native matrix. SIGKILL, host

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"chalk": "^6.0.0",
8181
"cli-table3": "^0.6.5",
8282
"commander": "^15.0.0",
83+
"jsonc-parser": "^3.3.1",
8384
"ora": "^9.4.0",
8485
"proper-lockfile": "^4.1.2",
8586
"semver": "^7.8.1",

‎pnpm-lock.yaml‎

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎scripts/native-client/approval.mjs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
export const fixtureInputPrompt = 'native-fixture synthetic input';
2+
export const fixtureInputValue = 'synthetic input "quoted" \\ path';
3+
export function fixtureInput(widget) {
4+
return !/sign.?in|account|oauth|authentication/i.test(widget.text) && widget.text.includes(fixtureInputPrompt) && widget.value === fixtureInputValue;
5+
}
6+
17
export function fixtureApproval(dialog) {
28
if (/sign.?in|account|oauth|authentication/i.test(dialog.text)) return undefined;
39
if (!/Enable global auto approve\?/.test(dialog.text) && !/native-fixtur/.test(dialog.text)) return undefined;
@@ -32,6 +38,11 @@ export async function approveFixtureDialogs(port, log, signal) {
3238
const clicked = await evaluate(`(()=>{const d=Array.from(document.querySelectorAll('.monaco-dialog-box')).find(d=>d.innerText===${JSON.stringify(dialog.text)});const b=d&&Array.from(d.querySelectorAll('.monaco-button')).find(b=>b.innerText===${JSON.stringify(button)});if(b&&b.getAttribute('aria-disabled')!=='true'){b.click();return true;}return false;})()`);
3339
if (clicked) log.push({ text: dialog.text, button });
3440
}
41+
const widgets = await evaluate("Array.from(document.querySelectorAll('.quick-input-widget')).filter(w=>w.offsetWidth>0).map(w=>({text:w.innerText,value:w.querySelector('input')?.value}))");
42+
if ((widgets || []).some(fixtureInput)) {
43+
for (const type of ['keyDown', 'keyUp']) ws.send(JSON.stringify({ id: Math.floor(Math.random() * 1000000), method: 'Input.dispatchKeyEvent', params: { type, key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 } }));
44+
log.push({ inputPrompt: fixtureInputPrompt, value: fixtureInputValue });
45+
}
3546
}
3647
} catch (e) { if (signal.aborted) return; }
3748
finally { ws?.close(); }

0 commit comments

Comments
 (0)