Skip to content

mcpx: Python as a first-class script language, 1:1 with TypeScript #1917

Open

Description

@dezren39

Relocated from dezren39/darwin-nix#105 on 2026-10-28 (placement audit): mcpx's single source lives in developing-today/code (pkgs/mcpx); re-home tracked at #309. Original body below; issue references inside it are historical (dezren39/nix numbering).

Parent: #175

Summary

Everything mcpx offers a TypeScript author — exec, run, named scripts on a
search path, a generated typed client, ambient helpers (log, emit,
artifact), launcher phases, entry-point selection, typechecking, local and
remote execution — should exist for Python, with the same settings, the same
wire fields and the same names. This is a second codegen backend and a second
runner, not a second product.

Original notes

  • support python wherever and however typescript is used 1:1

Current state

mcpx is TypeScript-only, and the assumption is spread across the codebase
rather than parameterised:

place what assumes TypeScript
internal/codegen emits one .ts module (codegen.Module(nss, endpoint, session)) and mcpx-globals.d.ts (codegen.GlobalDeclarations)
internal/runner clientFileName = "mcpx-client.ts", GlobalsFileName = "mcpx-globals.d.ts", the launcher template is TypeScript source with await import(), Runtime.Args covers three JS runtimes
internal/settings script.runtime enum is auto|deno|bun|node; script.launcher, script.prefix/suffix phases are TS source
internal/preflight TypeCheck shells deno check
internal/cli/scripts.go ScriptExt = ".ts", ScriptExtensions = [".ts",".mts",".js",".mjs"]
internal/diagnose statically scans TypeScript for tool calls
docs/exec.md ExecOptions.runtime is "auto"|"deno"|"bun"|"node"

Python appears in the repo only as a shell helper in scripts/bench.sh and
scripts/stress.sh, and as pypi → uvx when adding an MCP server
(internal/registry/registry.go:253).

The transport is already language-neutral, which is what makes this tractable:
the generated client speaks HTTP to the daemon, over the unix socket on deno
and bun and over the loopback TCP port on node (internal/codegen/emit.go:256-300;
docs/dependencies.md:73-76 still describes only the TCP port and is stale). A
Python client is an HTTP client against the same endpoint, reading the same
MCPX_ENDPOINT/MCPX_SOCKET/MCPX_SESSION variables
(internal/execsvc/execsvc.go:573-577, emit.go:252-253). The stdlib reaches
TCP directly and a unix socket with a small http.client subclass.

What Python brings that maps cleanly

  • uv 0.9.28 and Python 3.14.7 are installed here. uv run --script x.py
    with a PEP 723 header is the direct analogue of bun's auto-install, and it is
    better than the JS equivalent because the dependency declaration lives in the
    script text
    — verified working: a file whose header is

    # /// script
    # dependencies = ["cowsay"]
    # ///

    resolved and installed cowsay into an ephemeral environment on the first
    uv run --script. Script text carrying its own pinned dependencies is exactly
    what a remote exec needs, and it solves for Python the problem the JS side has
    to invent a deps field for.

  • Typing is a real analogue, not a pretence. A generated client can be a
    .py module with TypedDict argument types and return types plus a .pyi
    stub, checked by pyright or mypy. mcpx-globals.d.ts becomes
    mcpx_globals.pyi. The check is a subprocess with a non-zero exit and
    parseable diagnostics, same shape as deno check.

  • Entry points map. TS export default ↔ a module-level main(argv);
    --export fn ↔ a named module-level callable; "ran on import" ↔ top-level
    statements. The launcher's job — import the module, call the entry, print the
    return value as JSON, run the phases — is the same job in Python.

What does not map

  • top-level await has no Python equivalent; an async entry point needs
    asyncio.run, which the launcher supplies.
  • There is no permission model at all. deno's --allow-* has no Python
    counterpart, so script.permissions is ignored — the same honest statement
    runner.Permissions already makes for bun and node.
  • npm:/jsr:/https: specifiers have no analogue; PEP 723 + uv is the
    substitute and it is a better one.
  • Deno's sandbox-free module cache story differs: uv run --script creates an
    environment per dependency set, which is slower on first use and cached
    after — needs measuring against the ~70 ms JS baseline in Update dependency @playwright/test to v1.64.1 #79.

Approaches

Approach A — least recommended: a separate mcpx py command family

mcpx py exec, mcpx py run, a python section in the config, a Python-only
/v1/pyexec.

Why not: it doubles the surface the project's own parity rule (CLI = /v1 = MCP
= plugin) already struggles to keep even — #76 exists because that rule slipped
in one direction with one language. Two command families means every future
feature is implemented twice or, more likely, once. It also contradicts the
user's own framing: "wherever and however typescript is used 1:1" is a
request for the same commands, not parallel ones.

Approach B — middle: a script.language setting and a second runner

Add script.language = auto|typescript|javascript|python (default auto,
inferred from the file extension, from a shebang, or from the exec snippet's
syntax). script.runtime keeps its current JS meaning and gains a sibling —
script.pythonRuntime = auto|uv|python3 — rather than growing Python values
into a JS enum. runner.Detect returns a Runtime for the selected language;
Runtime is already an interface-shaped struct (Name, Bin, Args func(script) []string) and needs little more than a Language field and a per-language
launcher template.

Ship without a typed client at first: Python scripts get an untyped
mcpx_client.py with tools.ns.tool(**kwargs) via __getattr__, which is a
handful of lines and works immediately.

This gets run, exec, named scripts, /v1/exec and mcpx_exec working for
Python quickly and is a defensible first cut. Its weakness is that the untyped
client gives up the thing "code mode" is for: the generated client is what lets
an agent write a correct call without a schema in its context. An untyped Python
client is a worse product than the TS one, and shipping it invites treating
Python as second-class permanently.

Approach C — recommended: codegen as a pluggable backend, the typed Python client first as a slice that stands alone, then the runner

How this differs from B: B builds the runner first and an untyped client; C
builds the typed client first and the runner second. The order matters because
the client is useful on its own, to any Python program mcpx does not run.

The addition: make language a dimension of internal/codegen rather than a
branch in the runner. One Language describes a target — file extension for
client, stub and scripts; a Module(nss, endpoint, session) emitter; a
declarations emitter; a launcher template with the same named phases
(before/prefix/onSuccess/onError/suffix, and the same @placeholder
fills from #7); a checker command; and a dependency-declaration form (PEP 723
for Python, the resolved dependency set for JS).

Why this is the right one:

  • The parity rule is enforceable at the type level rather than by review.
    Every surface that today names .ts names Language.ClientFile instead, so
    adding Ruby or Go later is a table entry and not an audit. This repo already
    has the pattern twice — one ops table generating routes, MCP tools and
    OpenAPI; one settings registry generating flags, env, config and /v1 — and
    it works.
  • A typed Python client is not much more work than an untyped one. The JSON
    Schema → TypeScript compiler already exists in internal/codegen; JSON Schema
    → TypedDict is the same traversal with different output, and .pyi
    generation is the same data again. Doing both at once avoids building the
    untyped one and then throwing it away.
  • script.check generalises instead of forking. The pipeline proposed in
    Bump build/buck/preludes/buck2-prelude from d956689 to 9afedba #100 (off|syntax|types|strict, with script.checkOnError and
    script.checkOnUnsupported) reads identically with pyright or mypy behind it. Python
    actually improves the story: unlike bun and node, there is a real checker,
    so onUnsupported never fires.
  • PEP 723 generalises the dependency design rather than being a special
    case.
    "Dependencies declared in the script text, pinned, travelling with a
    remote exec" is the shape the JS side wants too — see Bump build/buck/preludes/buck2-prelude from d956689 to 062245a #103, where rank 1 of
    the dependency chain is exactly this. Building Python against that design
    pressure-tests it.

Two slices, and a checkpoint between them. This is what makes an XL issue
decidable in pieces:

slice what ships ease usable by
1 — typed client the Language seam in internal/codegen; mcpx_client.py + .pyi (TypedDict arguments, ambient log/emit/artifact); served beside GET /v1/client.ts (op client_module, internal/api/ops.go:354) and by mcpx types with a language option M any Python program with the daemon's endpoint and a session — no runner involved
2 — runner script.language, the Python launcher with the five phases and @placeholders, .py named scripts, ExecOptions.language, PEP 723 via uv run --script, the checker behind script.check, the rest of the parity checklist below L mcpx exec/run, /v1/exec, mcpx_exec, the plugin

The decision this leaves the user: the notes ask for "1:1", which is slice 2.
Slice 1 delivers the part "code mode" exists for (a typed client an agent can
write correct calls against). It is also the natural point to confirm with
evidence that mcpx should run Python, not only serve it, before spending
the L.

Parity checklist the issue should be closed against — every one of these exists
for TypeScript today:

capability Python form
mcpx exec '<src>' mcpx exec --language python '<src>', or inferred
mcpx run file.py extension-inferred
named scripts ScriptExtensions gains .py; plumbing.allowTsJsOverlap gains the same question for foo.ts vs foo.py
generated client mcpx_client.py + mcpx_client.pyi
ambient helpers log, emit, artifact, installGlobals equivalents in the client module
launcher + 5 phases + @placeholders same names, Python template
--export fn named module-level callable
typecheck pyright/mypy behind script.check
/v1/exec, mcpx_exec, plugin ExecOptions.language; runtime stays JS-scoped
artifacts, emit, streaming frames unchanged — daemon-side, language-neutral
mcpx types Python signatures
internal/diagnose static scan a Python scanner, or a graceful "not available for this language"
Nix packaging package.nix wraps with python3/uv the way it already wraps deno/bun/node/git

Open questions

  • script.language vs. inference: an exec snippet has no extension. Infer
    from syntax (fragile), require the flag, or default per script.language?
    A shebang in the snippet is a third option — see the shebang sibling issue.
  • pyright (node dependency, better inference) or mypy (pure Python, slower,
    weaker on generated stubs)? Neither is installed here; both are a new
    dependency under docs/dependencies.md's rules.
  • Start-up cost. The JS baseline is ~70 ms (Update dependency @playwright/test to v1.64.1 #79). uv run --script with a warm
    environment needs measuring before Python is offered as an equal choice, and
    environment reuse needs the same content-addressed-directory treatment the
    exec workdir already gets — as a tenant of Phase 4: joiner with missing pre-join control messages stalls silently #187's cache mechanism, not a
    fourth design (uv keeps its own cache; what mcpx would hold is the key).
  • How does the Python client authenticate/identify its session? The TS client
    reads the endpoint, socket and session from MCPX_ENDPOINT, MCPX_SOCKET and
    MCPX_SESSION (set in internal/execsvc/execsvc.go:573-577, read at
    internal/codegen/emit.go:252-253); MCPX_CLIENT, MCPX_ENTRY and
    MCPX_RUNTIME are the launcher's (internal/runner/runner.go:359-362). The
    Python one should use the identical variables. (Corrected 2026-09-30: this
    bullet named the launcher's variables as the session's.)
  • Slice 1's route: /v1/client.ts names its language in the path, so a Python
    module is either a sibling route generated by the same op or a language
    option on /v1/types. Prefer whichever keeps one op.
  • Does --typecheck's current Deno-only message become "the selected language's
    checker", and does script.permissions warn when it can do nothing?
  • Is .py allowed on the recipe and adapter paths too, or scripts only?

Ease · Impact · Related

Ease XL — a second codegen backend, a second launcher, a second checker, and
a parity checklist to hold it to (slice 1 M, slice 2 L) · Impact high;
Python is where most MCP and data tooling already lives.

Depends on / related: #100 (script.check, which slice 2's checker plugs
into), #103 (the dependency chain PEP 723 generalises), #106 (dispatch rank 3,
script.language, is this issue's hole), #187 (cache mechanism for uv
environments), #76 (parity gaps, the failure mode this must avoid), #7
(launcher placeholders, which the Python launcher must reproduce), #40 (closed,
fixed in #57 — ExecOptions, which gains language), #49 (CI — a second language
needs its own suite), #79 (startup-cost baseline), #101 (comparison doc) and
#104 (automatic imports).

Security/auth deferred; see tracker.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    History carried over from dezren39/darwin-nix#105 (verbatim, oldest first):


    @dezren39 · 2026-09-30T07:21:43Z

    Integration pass. Placed in the DAG rooted at #181.


    @dezren39 · 2026-09-30T12:05:52Z

    Issues pass (2026-09-30, at 05c78b2).


    @dezren39 · 2026-10-28T21:47:30Z

    Triage verdict (task 2130, repo-health-nix-pilot): RELOCATED — confirmed as a real, large, unimplemented feature whose code surface moved to developing-today/code. No fix packet in this repo.

    Verified against current main at 224c7b738e88c7a8e5d406987a7d88257e229db2:

    • Bump h2 from 0.3.18 to 0.3.24 in /sources/pavex/examples/skeleton #105 is OPEN with labels enhancement, design — no hard-stop labels. Parent tracker fix(id): make Nix E2E check reproducible #175 is OPEN.
    • The gap is as stated: mcpx is TypeScript-only, with the assumption spread across internal/codegen (one .ts module + mcpx-globals.d.ts) and internal/runner (mcpx-client.ts). The ask is a second codegen backend and a second runner — a multi-part feature, not a small fix.
    • That code no longer lives in dezren39/nix: there is no pkgs/mcpx at HEAD (404); this repo's mcpx-nixbuild.yml states the source "lives only in developing-today/code now" (migration commit a03494f3090b there). A probe of developing-today/code shows only incidental Python (a few .py files and docs mentions under pkgs/mcpx) — no evidence of the 1:1 Python support this issue asks for, so it is not already done.

    Route: no small_fix packet in dezren39/nix — no codegen/runner code exists here. The feature routes with the source in developing-today/code, referencing #105 and parent #175; per the cross-repo rule, do not re-file it there. #105 stays open on the nix side until it is implemented there or transferred.

  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  4. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  5. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    Triage verdict

    Dedupe: no duplicate. No other code issue covers Python as a script language; #1917 is the sole owner. (Its uv-environment tenancy in #1908's cache mechanism is a dependency note, not overlap.)

    Scope confirmed against current main — still TypeScript-only everywhere the issue names:

    • ScriptExtensions (internal/cli/scripts.go:41) is still [".ts", ".mts", ".js", ".mjs"] — no .py in script resolution.
    • internal/codegen contains no Python target at all (code search: 0 files) — the typed client exists only as TypeScript, as the issue says.
    • The transport-neutrality premise also holds: the generated client is an HTTP client against MCPX_ENDPOINT/MCPX_SOCKET/MCPX_SESSION, which a stdlib Python client can speak unchanged.

    One snapshot correction, in the implementer's favour: the issue describes script.runtime as a closed enum (auto|deno|bun|node). It is now a string setting that also accepts declared runtimes and paths, with script.runtimes and script.runtimeOrder siblings (internal/settings/registry.go:226/235/244 — the #1911-era generalization). The language dimension in Approach B/C should build on that machinery rather than on an enum that no longer exists.

    Routing — phased, per Approach C's own order:

    • Phase 1 (enqueued, task 3197): make language a dimension of internal/codegen and emit the typed Python client (TypedDict types, .pyi ambient stub) — the slice Approach C says stands alone.
    • Recorded remainder (not enqueued, cut as follow-ons when Phase 1 lands): Phase 2 — runner + launcher template + script.language inference so run/exec/named scripts execute Python; Phase 3 — preflight checker (pyright/mypy behind script.check), diagnose scanning for Python, docs parity (docs/exec.md). Nothing here is dropped; the phases are named so the next packets cut from this list instead of re-deriving it.

    Labels updated to mirror the queued follow-up.

  6. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed on Oct 29, 2026
  7. 195 remaining items

  8. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: opencode-claimant
    • Task: repo-health-code-pilot task 8014, issue mcpx: Python as a first-class script language, 1:1 with TypeScript #1917
    • Session / ID: ses_e5e5e1482d0ffey2y3gCbge1o2
    • Worker: repo-worker-code (bound worker uses this id for all queue helper verbs)
    • Route: opencode-route/workhorse, agent build
    • State: claimed task bound at launch; OpenCode worker prompt admitted.
  10. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · Phase 2 implemented (issue stays open — later phases remain)

    • Agent: repo-worker-code (task 8014, lane opencode, session ses_e5e5e1482d0ffey2y3gCbge1o2)
    • Revalidated: issue OPEN, no hard-stop labels, no competing PR, at origin/main d3476bce7 (2026-11-03).
    • Shipped (PR mcpx: Python scripts get the Python client; named .py scripts (#1917 Phase 2) #4991):
      • The runner installs the generated Python client + stub (mcpx_client.py, mcpx_globals.pyi) beside a Python script, instead of writing only mcpx-client.ts. File names come from the codegen Language entry, and MCPX_CLIENT / Result.Client point at the installed module.
      • Language is decided by the same evidence dispatch uses (runner.ScriptLanguage: shebang first, then .py extension) and shared with the callers that render the client, so they cannot disagree.
      • /v1/client.ts and /v1/globals.d.ts accept ?language= (absent = TypeScript, byte-identical), the CLI fetches both pairs, the in-process registry source renders from codegen directly.
      • Named scripts: ScriptExtensions gains .py; mcpx run report finds report.py; mcpx scripts lists it without the extension; foo.ts/foo.py go through the existing ambiguity rule.
    • Tests: Python file run imports the client and calls a tool end to end; Python shebang snippet harnessed and named .py; no-client fallback still states the absence; ScriptLanguage table; named .py resolution + listing + ambiguity. go test ./internal/{runner,cli,codegen,execsvc,daemon} green.

    Remaining on #1917 (not this PR):

    1. Preflight Python typecheck (pyright/mypy) behind script.check — internal/preflight has no Python check.
    2. script.language / script.pythonRuntime settings (Approach B) — language currently comes only from shebang/extension.
    3. internal/diagnose Python scanning (or a stated "not available for this language").
    4. Docs parity (docs/exec.md, pkgs/mcpx/AGENTS.md, mcpx types --language python) and the Python launcher phases / @placeholders parity.
  11. added a commit that references this issue on Nov 3, 2026
  12. added
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Nov 3, 2026
  13. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: triaged (coverage pass)
    Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 30 (priority/none). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
    Found via: coverage pass 2026-11-03

  14. removed
    queue/queuedHas a queued task in the Supabase agent queue
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 3, 2026
  15. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestsource/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions