Skip to content

fix(sdk): honor manifest python command, ABI-validate before spawn#106

Open
mgoldsborough wants to merge 4 commits intomainfrom
fix/issue-90-python-resolver-and-uv-gaps
Open

fix(sdk): honor manifest python command, ABI-validate before spawn#106
mgoldsborough wants to merge 4 commits intomainfrom
fix/issue-90-python-resolver-and-uv-gaps

Conversation

@mgoldsborough
Copy link
Copy Markdown
Contributor

Closes #90.

Summary

  • Replace the silent pythonpython3 rewrite with a single ABI-validated probe. Resolution order: MPAK_PYTHON env override → mcp_config.command from the manifest → literal python3.
  • Walk deps/**/*.{so,pyd,dylib} to extract the cpython ABI tag; probe the chosen interpreter once via sys.implementation.cache_tag and require it to match. abi3 wheels read the floor from the extension filename and accept any cpython ≥ floor.
  • Cross-check compatibility.runtimes.python (semver) when declared.
  • Failure path emits an actionable message — including how to set MPAK_PYTHON or install the right interpreter — instead of letting the bundle crash mid-import with ModuleNotFoundError: No module named 'rpds.rpds'.
  • Log the resolved interpreter to stderr at startup: [mpak] using python = /Users/.../python (3.13.5) — removes "what's it actually running?" entirely.

Plus two adjacent fixes the same investigation surfaced:

type: uv resolver gaps

  • Probe uv --version first; throw with platform-specific install commands when missing instead of crashing mid-spawn with raw ENOENT.
  • Default args for type: uv now match the upstream examples/hello-world-uv shape exactly: ["run", "--directory", cacheDir, entry_point] — CWD-independent. The previous default only worked because the CLI happened to set cwd: cacheDir; embedders that don't honor server.cwd couldn't find pyproject.toml.
  • Log resolved uv version to stderr, mirroring the python resolver.

Schema alignment with MCPB v0.4

  • Add compatibility block (runtimes.python, runtimes.node, platforms, plus catchall for client version constraints).
  • Make mcp_config optional on ManifestServerSchema — MCPB v0.4 lets type: "uv" bundles omit it for host-managed execution.
  • Make mcp_config.command and mcp_config.args optional within McpConfigSchema; resolver supplies sensible defaults per server type.
  • Surfaces compatibility.runtimes.python to the resolver and unblocks parsing of spec-compliant uv bundles.
  • 16 new schema tests including the verbatim hello-world-uv manifest from the MCPB spec, pinning conformance.

Test plan

  • pnpm typecheck — green
  • pnpm lint — green
  • pnpm test — green
    • @nimblebrain/mpak-schemas: 115 tests (was 99 — +16 manifest cases)
    • @nimblebrain/mpak-sdk: 277 tests (was 244 — +33: 18 python-resolver, 5 uv-resolver, 10 mpak end-to-end)
    • @nimblebrain/mpak-registry: 130 tests (unchanged)
    • @nimblebrain/mpak-web: 61 tests (unchanged)
    • @nimblebrain/mpak: 174 tests (unchanged)

Repro from the issue

  1. Have any Python ≠ 3.13 on PATH as python3 (e.g. macOS system /usr/bin/python3 is 3.9; Homebrew is 3.11).
  2. uv python install 3.13 --default --preview — installs 3.13 and creates ~/.local/bin/python but not python3.
  3. mpak bundle run @nimblebraininc/synapse-todo-board

Before this PR: ModuleNotFoundError: No module named 'rpds.rpds' deep in an import chain. ~2 hours to debug to root cause.

After this PR:

Bundle requires cpython-313 (Python 3.13.x), but $(which python3) is cpython-311.
Install with: uv python install 3.13 --default
(or set MPAK_PYTHON to override.)

Rebased

Rebased onto current main (which includes the just-merged #100). One conflict in packages/schemas/src/manifest.ts + tests/manifest.test.ts — kept main's SafeRelativePathSchema security check on entry_point and adopted this branch's optional-mcp_config change for v0.4 compliance. Both test suites union'd cleanly.

- Add `compatibility` block (`runtimes.python`, `runtimes.node`,
  `platforms`, plus catchall for client version constraints).
- Make `mcp_config` optional on `ManifestServerSchema` — MCPB v0.4
  lets `type: "uv"` bundles omit it for host-managed execution.
- Make `mcp_config.command` and `mcp_config.args` optional within
  `McpConfigSchema`; resolver supplies sensible defaults per server type.
- Normalize SDK resolver to read `mcp_config` via nullish coalescing
  so the now-optional shape is a no-op for existing bundles.
- Add manifest.test.ts with 16 cases including the verbatim
  hello-world-uv manifest from the MCPB spec, pinning conformance.

Pre-step for issue #90 work: surfaces `compatibility.runtimes.python`
to the resolver and unblocks parsing of spec-compliant uv bundles.

Refs #90
Replaces the candidate-name parade in `findPythonCommand` with a single
ABI-validated probe.

Resolution order:
  1. MPAK_PYTHON env var (explicit override)
  2. mcp_config.command from the manifest
  3. literal "python3"

Validation:
  - Walks deps/**/*.{so,pyd,dylib} to extract the cpython ABI tag
    (e.g. "cpython-313"). For abi3 wheels, reads the floor from the
    extension filename and accepts any cpython >= floor.
  - Probes the chosen interpreter once via
    `python -c "sys.implementation.cache_tag"` and requires it to match.
  - Cross-checks `compatibility.runtimes.python` (semver) when declared.

Failure path emits an actionable message — including how to set
MPAK_PYTHON or install the right interpreter — instead of letting the
bundle crash mid-import with `ModuleNotFoundError: No module named 'rpds.rpds'`.

On success, writes one stderr line at startup so users never have to
ask "which Python is mpak actually running?":

    [mpak] python: python (3.13.0, cpython-313) via manifest

Test seam: optional `pythonProbe` on `MpakOptions` lets tests stub the
interpreter probe so the suite doesn't depend on which Python is on the
runner's PATH.

Closes #90
Two gaps in the existing `case 'uv'` resolver:

1. Bundles requesting type:uv on a host without uv installed crashed
   mid-spawn with a raw ENOENT — the user had no signal that "uv is
   the missing piece." Now the resolver probes `uv --version` first
   and throws an error with platform-specific install commands.

2. Default args (`["run", join(cacheDir, entry_point)]`) only worked
   because the CLI happens to set `cwd: cacheDir` — embedders that
   don't honor `server.cwd` couldn't find pyproject.toml. Default now
   matches the upstream `examples/hello-world-uv` shape exactly:
   `["run", "--directory", cacheDir, entry_point]`. CWD-independent.

Adds optional `uvProbe` test seam on `MpakOptions` (mirrors `pythonProbe`).
Logs resolved uv version to stderr at startup, mirroring the python
resolver pattern.

7 new tests:
  - uv-resolver.test.ts (5): default args, manifest args passthrough,
    absolute uv path, missing-uv error, version surfacing
  - mpak.test.ts (2): end-to-end resolution with spec-canonical default,
    missing-uv propagates through prepareServer
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.

Python interpreter resolution is silent and version-blind

1 participant