Repository navigation
Description
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
cowsayinto 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 adepsfield for. -
Typing is a real analogue, not a pretence. A generated client can be a
.pymodule withTypedDictargument types and return types plus a.pyi
stub, checked by pyright or mypy.mcpx-globals.d.tsbecomes
mcpx_globals.pyi. The check is a subprocess with a non-zero exit and
parseable diagnostics, same shape asdeno check. -
Entry points map. TS
export default↔ a module-levelmain(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 awaithas 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, soscript.permissionsis ignored — the same honest statement
runner.Permissionsalready makes for bun and node. npm:/jsr:/https:specifiers have no analogue; PEP 723 +uvis the
substitute and it is a better one.- Deno's sandbox-free module cache story differs:
uv run --scriptcreates 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.tsnamesLanguage.ClientFileinstead, 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 ininternal/codegen; JSON Schema
→TypedDictis 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.checkgeneralises instead of forking. The pipeline proposed in
Bump build/buck/preludes/buck2-prelude fromd956689to9afedba#100 (off|syntax|types|strict, withscript.checkOnErrorand
script.checkOnUnsupported) reads identically with pyright or mypy behind it. Python
actually improves the story: unlike bun and node, there is a real checker,
soonUnsupportednever 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 fromd956689to062245a#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.languagevs. inference: anexecsnippet has no extension. Infer
from syntax (fragile), require the flag, or default perscript.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 underdocs/dependencies.md's rules. - Start-up cost. The JS baseline is ~70 ms (Update dependency @playwright/test to v1.64.1 #79).
uv run --scriptwith 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 fromMCPX_ENDPOINT,MCPX_SOCKETand
MCPX_SESSION(set ininternal/execsvc/execsvc.go:573-577, read at
internal/codegen/emit.go:252-253);MCPX_CLIENT,MCPX_ENTRYand
MCPX_RUNTIMEare 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.tsnames its language in the path, so a Python
module is either a sibling route generated by the same op or alanguage
option on/v1/types. Prefer whichever keeps one op. - Does
--typecheck's current Deno-only message become "the selected language's
checker", and doesscript.permissionswarn when it can do nothing? - Is
.pyallowed 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
developing-today commentedon Oct 28, 2026
CODE-843
dezren39 commentedon Oct 28, 2026
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.
d956689to9afedba#100 Bump build/buck/preludes/buck2-prelude fromd956689to8740ce0#101 Bump build/buck/preludes/buck2-prelude fromd956689to062245a#103 Bump tracing from 0.1.38 to 0.1.40 in /sources/pavex/examples/skeleton #104 Bump h2 from 0.3.19 to 0.3.24 in /sources/pavex/examples/realworld #106 Bump checkmarx/ast-github-action from 2.0.19 to 2.0.22 #107@dezren39 · 2026-09-30T12:05:52Z
Issues pass (2026-09-30, at 05c78b2).
internal/codegen/emit.go:256-300);docs/dependencies.md:73-76is stale on this. Corrected the session-variable question: the session comes fromMCPX_ENDPOINT/MCPX_SOCKET/MCPX_SESSION(internal/execsvc/execsvc.go:573-577,emit.go:252-253), notMCPX_CLIENT/MCPX_ENTRY/MCPX_RUNTIME, which are the launcher's.d956689to062245a#103's rank 1, not the auto-import issue, and the check pipeline is Bump build/buck/preludes/buck2-prelude fromd956689to9afedba#100's (with its renamedscript.checkOnError/script.checkOnUnsupported). uv environment reuse is a tenant of Phase 4: joiner with missing pre-join control messages stalls silently #187. Re-citedinternal/registry/registry.go:253. Bump openssl from 0.10.54 to 0.10.55 in /sources/pavex/examples/realworld #40 → "closed, fixed in chore(deps): bump build/buck/preludes/buck2-prelude fromd956689to677e9eb#57". AddedParent: #175.@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:enhancement,design— no hard-stop labels. Parent tracker fix(id): make Nix E2E check reproducible #175 is OPEN.internal/codegen(one.tsmodule +mcpx-globals.d.ts) andinternal/runner(mcpx-client.ts). The ask is a second codegen backend and a second runner — a multi-part feature, not a small fix.pkgs/mcpxat HEAD (404); this repo'smcpx-nixbuild.ymlstates the source "lives only in developing-today/code now" (migration commita03494f3090bthere). A probe of developing-today/code shows only incidental Python (a few.pyfiles and docs mentions underpkgs/mcpx) — no evidence of the 1:1 Python support this issue asks for, so it is not already done.Route: no
small_fixpacket 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.dezren39 commentedon Oct 29, 2026
🤖 Agent note · picked up
dezren39 commentedon Oct 29, 2026
🤖 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.pyin script resolution.internal/codegencontains no Python target at all (code search: 0 files) — the typed client exists only as TypeScript, as the issue says.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.runtimeas a closed enum (auto|deno|bun|node). It is now a string setting that also accepts declared runtimes and paths, withscript.runtimesandscript.runtimeOrdersiblings (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:
internal/codegenand emit the typed Python client (TypedDict types,.pyiambient stub) — the slice Approach C says stands alone.script.languageinference sorun/exec/named scripts execute Python; Phase 3 — preflight checker (pyright/mypy behindscript.check),diagnosescanning 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.
195 remaining items
dezren39 commentedon Nov 3, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 3, 2026
🤖 Agent note · handed off
ses_e5e5e1482d0ffey2y3gCbge1o2repo-worker-code(bound worker uses this id for all queue helper verbs)opencode-route/workhorse, agentbuilddezren39 commentedon Nov 3, 2026
🤖 Agent note · Phase 2 implemented (issue stays open — later phases remain)
ses_e5e5e1482d0ffey2y3gCbge1o2)origin/maind3476bce7(2026-11-03).mcpx_client.py,mcpx_globals.pyi) beside a Python script, instead of writing onlymcpx-client.ts. File names come from the codegenLanguageentry, andMCPX_CLIENT/Result.Clientpoint at the installed module.runner.ScriptLanguage: shebang first, then.pyextension) and shared with the callers that render the client, so they cannot disagree./v1/client.tsand/v1/globals.d.tsaccept?language=(absent = TypeScript, byte-identical), the CLI fetches both pairs, the in-process registry source renders from codegen directly.ScriptExtensionsgains.py;mcpx run reportfindsreport.py;mcpx scriptslists it without the extension;foo.ts/foo.pygo through the existing ambiguity rule..py; no-client fallback still states the absence;ScriptLanguagetable; named.pyresolution + listing + ambiguity.go test ./internal/{runner,cli,codegen,execsvc,daemon}green.Remaining on #1917 (not this PR):
script.check—internal/preflighthas no Python check.script.language/script.pythonRuntimesettings (Approach B) — language currently comes only from shebang/extension.internal/diagnosePython scanning (or a stated "not available for this language").docs/exec.md,pkgs/mcpx/AGENTS.md,mcpx types --language python) and the Python launcher phases /@placeholdersparity.Merge pull request #4991 from developing-today/queue/8014-1917-python…
dezren39 commentedon Nov 3, 2026
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
dezren39 commentedon Nov 3, 2026
🤖 Agent note · done
awaiting_cion PR mcpx: Python scripts get the Python client; named .py scripts (#1917 Phase 2) #4991)