Skip to content

fix(chroma): spawn uvx directly on Windows instead of via cmd.exe (#2716, #2667)#2816

Closed
YOMXXX wants to merge 1 commit into
thedotmack:mainfrom
YOMXXX:fix/chroma-windows-direct-spawn
Closed

fix(chroma): spawn uvx directly on Windows instead of via cmd.exe (#2716, #2667)#2816
YOMXXX wants to merge 1 commit into
thedotmack:mainfrom
YOMXXX:fix/chroma-windows-direct-spawn

Conversation

@YOMXXX

@YOMXXX YOMXXX commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Problem

On Windows the chroma-mcp subprocess is launched as cmd.exe /c uvx .... cmd.exe re-parses the dep-override version specifiers (onnxruntime>=1.20, protobuf<7) as I/O redirection (protobuf < 7), so chroma-mcp never starts — every semantic-search tool call closes in ~30 ms with MCP error -32000: Connection closed.

This is the defect tracked in #2716 / #2667 (closed as not-planned, but still reproducing on latest main, as flagged by @tomeyerman in #2699).

Why the existing quoteForCmdExe fix (#2701) doesn't work

quoteForCmdExe wraps protobuf<7"protobuf<7" as a string, and its unit test is green. But the quotes don't survive the actual spawn:

  1. The MCP SDK's StdioClientTransport launches through cross-spawn with shell: false.
  2. The command is cmd.exe (a .exe, not a .bat/.cmd), so cross-spawn does not set windowsVerbatimArguments.
  3. Node's own argv→command-line quoting then re-escapes the quotes: "protobuf<7""\"protobuf<7\"".
  4. cmd.exe treats \" as a literal backslash + a quote toggle (not an escape), so it closes the quote early and sees < outside quotes again → redirection → "The system cannot find the file specified".

So the hand-quoting is defeated by the very re-quoting layer it tries to anticipate. The cmd.exe wrapper is the whole problem.

Fix

Spawn uvx directly on every platform (buildChromaSpawnConfig). Node's child_process.spawn resolves uvxuvx.exe/uvx.cmd on PATH on Windows exactly as on POSIX, so no shell wrap is needed: the argv array is passed through without any cmd.exe re-parsing and the </> dep specs reach uvx intact. This matches the approach #2502 / #2668 converged on in the #2699 canary.

  • Remove the cmd.exe /c Windows branch and the defeated quoteForCmdExe helper.
  • Replace its string-only unit test with a spawn-contract test asserting the launch is direct uvx (no cmd.exe, no /c, dep specs verbatim) — guarding against the wrapper being reintroduced.

Testing

  • bun test tests/services/integrations/spawn-contract-windows.test.ts tests/services/sync/chroma-mcp-manager-singleton.test.ts7 pass / 0 fail
  • tsc --noEmit: 0 errors introduced by this change
  • Windows real-machine validation still welcome — I don't have a Windows box; @tomeyerman offered in 🪟 Windows Canary: combined build/test of all open Windows fixes #2699 to confirm Chroma connects end-to-end on Windows 11.

refs #2716, #2667, #2696, #2699

@greptile-apps

greptile-apps Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes the Windows chroma-mcp connection failure (#2716 / #2667) by removing the cmd.exe /c uvx wrapper in favour of spawning uvx directly on every platform. The metacharacter-quoting helper (quoteForCmdExe) and its Windows branch are deleted; ensureUvOnPath / uvBinDirs — which fix the related ENOENT regression (#2790) — are preserved unchanged.

  • buildChromaSpawnConfig is extracted into an exported, platform-agnostic function that always returns { command: 'uvx', args: [...commandArgs] }, letting Node (or cross-spawn inside the MCP SDK) resolve uvx.exe directly on Windows without cmd.exe metacharacter re-parsing.
  • New spawn-contract test asserts the command is uvx, args contain no cmd.exe//c prefix, and dep-version specifiers pass through verbatim.
  • plugin/scripts/worker-service.cjs is the rebuilt compiled bundle reflecting the source change.

Confidence Score: 5/5

Safe to merge; the change is narrowly scoped, removes known-broken Windows code, and is backed by regression tests.

The core change eliminates the cmd.exe /c wrapper and spawns uvx directly. PATH augmentation via ensureUvOnPath was not removed, so the previously flagged regression path is not re-opened. The only residual uncertainty is the uvx.cmd shim edge case on non-standard Windows uv installations, which is unlikely with the official installer.

The test file mixes an unrelated codex smoke-test with the chroma spawn-contract suite. plugin/scripts/worker-service.cjs is a large compiled artefact checked into the repo worth confirming was regenerated from current source.

Important Files Changed

Filename Overview
src/services/sync/ChromaMcpManager.ts Replaces the Windows-only cmd.exe /c uvx spawn branch (and quoteForCmdExe) with a single cross-platform buildChromaSpawnConfig that always spawns uvx directly; ensureUvOnPath / uvBinDirs are preserved so PATH augmentation for #2790 is unaffected.
tests/services/integrations/spawn-contract-windows.test.ts New test file asserting the chroma-mcp spawn contract: command is uvx, args contain no cmd.exe//c/uvx prefix, and dep-version specifiers reach uvx verbatim. Also bundles an unrelated codex spawn smoke-test (#2695) in the same file.
plugin/scripts/worker-service.cjs Compiled/bundled artifact (~4.3 MB diff) rebuilt to incorporate the buildChromaSpawnConfig change; checked in directly, which makes diffs noisy and creates potential for source/artifact drift.

Sequence Diagram

sequenceDiagram
    participant W as Worker
    participant CM as ChromaMcpManager
    participant SDK as StdioClientTransport
    participant UVX as uvx (direct)

    Note over W,UVX: Before this PR (Windows)
    W->>CM: connectInternal()
    CM->>SDK: "command=cmd.exe, args=[/c, uvx, protobuf<7, ...]"
    SDK-->>W: "cmd.exe re-parses < as redirect, connection closed"

    Note over W,UVX: After this PR (all platforms)
    W->>CM: connectInternal()
    CM->>CM: "buildChromaSpawnConfig() returns {command:uvx, args verbatim}"
    CM->>CM: getSpawnEnv() calls ensureUvOnPath()
    CM->>SDK: "command=uvx, args=[protobuf<7, ...]"
    SDK->>UVX: uvx.exe spawned directly
    UVX-->>W: chroma-mcp starts successfully
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
tests/services/integrations/spawn-contract-windows.test.ts:48-61
**Unrelated codex test bundled into chroma spawn-contract file**

The second `describe` block for `codexSpawn` / #2695 is unrelated to the chroma-mcp metacharacter fix this file is named and scoped for. Mixing the two makes the file title misleading and means a failure in the codex smoke-test silences the chroma contract suite with no obvious connection. Consider moving it to a dedicated `codex-spawn.test.ts` or `spawn-contract-codex.test.ts`.

### Issue 2 of 2
src/services/sync/ChromaMcpManager.ts:70-75
**`uvx.cmd` shim on Windows re-exposes cmd.exe via cross-spawn**

When `uvx` is installed as `uvx.cmd` (possible on Windows if uv was installed via pip/pipx or certain package managers), cross-spawn detects the `.cmd` extension and wraps the invocation with `cmd.exe /c uvx.cmd …` internally. This reintroduces the exact metacharacter issue this PR removes for users in that scenario.

The standard uv Windows installer places `uvx.exe`, so the common path is safe. Worth a code comment or a CI note documenting that the fix assumes `uvx.exe` (not `.cmd`) is on PATH, so the team knows to keep this assumption in mind if uv's Windows packaging ever changes.

Reviews (2): Last reviewed commit: "fix(chroma): spawn uvx directly instead ..." | Re-trigger Greptile

The Windows path wrapped the chroma-mcp launch as `cmd.exe /c uvx ...`,
which makes cmd.exe re-parse the dep-override version specifiers
(onnxruntime>=1.20, protobuf<7) as I/O redirection, so chroma-mcp never
started and every tool call closed in ~30ms with MCP error -32000.

Hand-quoting the args (quoteForCmdExe, thedotmack#2701) did not survive the spawn:
the MCP SDK launches through cross-spawn with shell:false, and because
the command is cmd.exe (a .exe, not a .bat/.cmd) cross-spawn leaves
windowsVerbatimArguments unset. Node then re-escapes the quotes
("protobuf<7" -> "\"protobuf<7\""), and cmd.exe, which treats \" as a
literal backslash plus a quote toggle rather than an escape, closes the
quote early and sees < outside quotes again.

Spawn uvx directly on every platform (buildChromaSpawnConfig): Node
resolves uvx on PATH on Windows the same way as POSIX, so no shell wrap
is needed and the dep specs reach uvx intact. Replace the failed
quoteForCmdExe helper and its unit test with a spawn-contract test that
guards against the wrapper being reintroduced.

refs thedotmack#2716, thedotmack#2667, thedotmack#2696
@YOMXXX YOMXXX force-pushed the fix/chroma-windows-direct-spawn branch from 74a01ae to eef2dd0 Compare June 7, 2026 03:30
@YOMXXX

YOMXXX commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

Status for reviewers

CI — the one red check (typecheck · build · test · bundle-size) is a pre-existing baseline failure, not introduced here:

src/server/runtime/create-server-beta-service.ts(5,10): error TS2300: Duplicate identifier 'ModeManager'.
src/server/runtime/create-server-beta-service.ts(17,10): error TS2300: Duplicate identifier 'ModeManager'.

That file isn't touched by this PR — it's the same tsc baseline #2699 documents (fixed separately by #2511). tsc --noEmit reports 0 errors introduced by this change, and the other three checks (build, clean-room dependency closure, server-runtime e2e) all pass.

Greptile — re-reviewed the latest commit (eef2dd0e) and cleared its earlier P1: ensureUvOnPath / uvBinDirs (the #2790 PATH fix) are preserved unchanged, so the ENOENT regression path is not re-opened. The augmented PATH flows through the spawn env and applies to the direct uvx spawn exactly as it did to the old cmd.exe wrapper — #2790 (PATH) and #2716/#2667 (cmd.exe metachar re-parse) are independent root causes, both fixed.

On Greptile's one residual note (the uvx.cmd shim on non-standard installs): it's safe either way — the official uv installer ships ~/.local/bin/uvx.exe (cross-spawn spawns the .exe directly), and if uv is a .cmd shim, cross-spawn applies its own correct cmd.exe escaping rather than the broken hand-quoting this PR removes.

Verified locally: bun test spawn-contract + chroma singleton suites 7 pass / 0 fail.

@teyerman-st teyerman-st left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted from the wrong account by mistake — please disregard this review; see my validation under @tomeyerman instead. As a non-maintainer I can't delete a submitted review — @thedotmack, feel free to dismiss this one.

@tomeyerman tomeyerman left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated on Windows 11 Pro (10.0.26200) — the direct-uvx spawn this PR introduces connects Chroma end to end. 🎉

Disclosure on what I ran: I'm running a self-host fork that carries the functionally identical change, not a checkout of this exact commit. I diffed my ChromaMcpManager.ts against this PR — the only differences are that you extracted the logic into buildChromaSpawnConfig() and added richer comments; the runtime behavior (drop the cmd.exe /c wrapper, spawn uvx directly on every platform) is the same. So this validates the approach, on a real Windows box.

Evidence

1. Process tree — worker spawns uvx directly, no cmd.exe in the chain, dep specs verbatim:

bun.exe  (worker, PID 24764)   worker-service.cjs --daemon
  └─ uvx.exe  (PID 36612)      uvx --python 3.13 --with onnxruntime>=1.20 --with protobuf<7 chroma-mcp==0.2.6 --client-type persistent --data-dir .../chroma
       └─ uv.exe  (PID 36700)
            └─ chroma-mcp.exe  (PID 36344)
                 └─ python.exe → python.exe

The worker (bun.exe) is the direct parent of uvx.exe — no intermediate cmd.exe. Critically, onnxruntime>=1.20 and protobuf<7 reach uvx intact; these are the exact >/< specifiers the old cmd.exe /c wrapper re-parsed as I/O redirection, which is what killed chroma-mcp in ~30ms with MCP error -32000: Connection closed.

2. Semantic round-trip succeeds (not the FTS5 keyword fallback):

// GET /api/chroma/status?deep=1
{
  "status": "healthy",
  "connected": true,
  "details": "chroma-mcp semantic search round-trip succeeded",
  "deep": true,
  "probe": { "ok": true, "stage": "done", "queryLatencyMs": 762 }
}
// GET /api/health
{ "status": "ok", "platform": "win32", "initialized": true, "mcpReady": true, "version": "13.4.0" }

Conclusion

The cmd.exe wrapper was the whole problem, and spawning uvx directly resolves it on Windows 11 with no regression — uvx.exe/uv.exe/chroma-mcp.exe resolve on PATH and the --with pins survive the spawn. The added spawn-contract test is a good guard against the wrapper being reintroduced. LGTM for the Windows path. 👍

@thedotmack

Copy link
Copy Markdown
Owner

Closing as duplicate: the Windows Chroma fix landed on main via #2847 — chroma-mcp now spawns a directly-resolved uvx/uvx.exe (no cmd.exe /c), so the >=/< pip specifiers never hit cmd.exe redirection parsing. Same approach as this PR; #2847 was the most current against main. Thanks — reopen if you hit a case the merged fix misses.

@thedotmack thedotmack closed this Jun 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants