Skip to content

feat(mcp): add run_python tool - #440

Merged
jahvon merged 1 commit into
feat/exec-interpreterfrom
feat/mcp-run-python
Aug 27, 2026
Merged

feat(mcp): add run_python tool#440
jahvon merged 1 commit into
feat/exec-interpreterfrom
feat/mcp-run-python

Conversation

@jahvon

@jahvon jahvon commented Aug 27, 2026

Copy link
Copy Markdown
Member

Part 2/5. Stacked on #439 — review that first; this PR's diff is only its own change.

Summary

Exposes Python execution to agents as a first-class MCP tool, alongside run_command. This is the point of the stack: flow already acts as an assistant's shell, and now it acts as their Python runtime too — same workspace env and secrets, same captured logs, same attributable history entry.

run_python({ code: "import pandas as pd\nprint(df.describe())", label: "summarize csv", dir: "/repo" })

Also adds the --interpreter flag on flow exec that backs it.

Notable Changes

  • run_python is its own tool rather than a parameter on run_command. Agents select tools by name, so a tool called "run_command" is not what gets reached for when the task is Python. It also sidesteps a real constraint: run_command's multi-command form builds serial/parallel steps, which carry no interpreter of their own.
  • For that same reason the CLI rejects --interpreter with multiple --cmd values, with a usage error. Part 4 of this stack lifts that restriction once step configs gain the field — it is a deliberate interim guard, not an oversight.
  • The tool is a thin argv layer like its siblings, reusing runTransientTool, ExecutionOutput, and the existing provenance/progress plumbing.
  • Updates the embedded MCP server instructions and docs/guides/ai-tools.md so the tool ladder names it.
  • Allowlists mcp__flow__run_python in this repo's own .claude/settings.json and adds it to the flow-context skill, so the agents working in this repo reach for it instead of shelling out to python -c.

Testing

  • MCP tests assert the exact argv, workspace/sync forwarding, that multi-line code survives as a single argument (line structure has to stay intact or tracebacks point at the wrong line), and both empty/missing code error paths. run_python added to the registered-tool and output-schema assertions.
  • E2E coverage for the flag, the unknown-interpreter rejection, and the multi-command guard.
  • flow validate passes; generate produces no diff. (Completion scripts are unaffected — cobra generates them to query the binary at runtime rather than embedding flag lists.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01R328pa3FUUfga4gYah1iQi

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@jahvon
jahvon force-pushed the feat/mcp-run-python branch from c77faef to 312d54b Compare August 27, 2026 04:28
@jahvon
jahvon force-pushed the feat/mcp-run-python branch from 312d54b to d1225f7 Compare August 27, 2026 05:48
@jahvon
jahvon force-pushed the feat/mcp-run-python branch from d1225f7 to 4ba4089 Compare August 27, 2026 06:24
@jahvon
jahvon force-pushed the feat/mcp-run-python branch from 4ba4089 to 80034f3 Compare August 27, 2026 06:44
Exposes Python execution to agents as a first-class MCP tool alongside
run_command, plus the `--interpreter` flag on `flow exec` that backs it.

run_python is its own tool rather than a parameter on run_command because
agents select tools by name, and because run_command's multi-command form
builds serial/parallel steps, which carry no interpreter of their own. The CLI
rejects `--interpreter` with multiple `--cmd` values for the same reason; a
later change can relax that once step configs gain the field.

Also allowlists the tool in this repo's own .claude config and adds it to the
flow-context skill, so the agents working in this repo reach for it instead of
shelling out to `python -c`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R328pa3FUUfga4gYah1iQi
@jahvon
jahvon force-pushed the feat/mcp-run-python branch from 80034f3 to 14151e6 Compare August 27, 2026 06:49
@jahvon
jahvon merged commit 3b5ebd0 into main Aug 27, 2026
23 checks passed
@jahvon
jahvon deleted the feat/mcp-run-python branch August 27, 2026 13:52
jahvon added a commit that referenced this pull request Aug 27, 2026
**Part 3/5.** Stacked on #440.

# Summary

Combines `interpreter: python` with the existing `container:` block, so
a pinned Python toolchain needs no local install.

```yaml
exec:
  interpreter: python
  cmd: |
    import sys
    print(sys.version)
  container:
    image: python:3.13-alpine
```

**Notable Changes**

- **Inline code is bind-mounted read-only as a script and passed via
`spec.Script`, never `spec.Cmd`.** `buildRunArgs` maps `Cmd` to
`<entrypoint> -c <code>`, which would put user code in the process table
and cost traceback line numbers — the same trade-off Part 1 avoided on
the host. The existing "script lives outside every mount" branch already
did exactly this for files, so the generated temp script reuses it and
`internal/services/run/container.go` needed no change at all.
- **The default entrypoint follows the interpreter** — `sh` for a shell
command, `python3` for a Python one. Resolved in the runner rather than
on `ExecContainer`, keeping the types package unaware of how flow
launches containers. An explicit `container.entrypoint` still wins,
including the empty form (which, with Python, only works if the image's
own `ENTRYPOINT` is an interpreter — documented).
- **Host interpreter discovery stops at the container boundary.**
`VIRTUAL_ENV`, `PYTHONPATH`, `PYTHONHOME`, and `FLOW_PYTHON_BIN` are
dropped from the container environment: those host paths either do not
exist inside it or, worse, resolve to an unrelated mounted directory.
`PYTHONUNBUFFERED` is set there too.
- The temp script cleanup is registered as a context callback next to
`ForceRemoveContainer`, so a run abandoned by the runner's timeout
goroutine does not leak it.

# Testing

- Unit tests assert the built spec: `Cmd` empty, `Script` set to a
`.py`, the script mount present and read-only, the `python3` entrypoint
default, explicit-entrypoint override, empty-entrypoint passthrough,
that a shell command still gets `sh`, and that host python env vars are
dropped while unrelated ones survive.
- An e2e case runs Python in a `python:3.13-alpine` image, behind the
suite's existing runtime guard.
- `flow validate` passes; `generate` produces no diff.

**Verification note:** I have no container runtime on this machine, so
the live container run was *not* executed locally — it is covered by the
e2e case, which only executes where a runtime is available (CI). The
spec and argv construction are verified by the unit tests above.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01R328pa3FUUfga4gYah1iQi

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
jahvon added a commit that referenced this pull request Aug 27, 2026
**Part 4/5.** Stacked on #441.

# Summary

Inline `cmd` steps in `serial`/`parallel` executables now take their own
`interpreter`, so one workflow can mix shell and Python without
splitting into separate executables.

```yaml
serial:
  execs:
    - cmd: ./fetch-data.sh
    - cmd: |
        import json
        print(json.load(open("data.json"))["total"])
      interpreter: python
```

**Notable Changes**

- Both step configs `$ref` the same `ExecInterpreter` definition the
`exec` type uses, so there is one enum rather than three parallel ones.
(Part 1 hoisted it to a top-level definition for exactly this.)
- A step that omits `interpreter` runs under the shell as before; a
`ref` step ignores the field — the referenced executable brings its own.
- **This lifts the interim restriction from #440**: `--interpreter` now
applies to every `--cmd` in an invocation, in serial and parallel mode
alike.
- `ExecutableForCmd` takes the interpreter as a new parameter (5 call
sites). Its unused `int` parameter is left alone to keep the diff to the
one concern.

# Testing

- E2E: a serial executable mixing a shell step and a Python step, each
running under its own interpreter; a step without an interpreter staying
on the shell (asserted via `echo`, a shell builtin, so it only passes if
the step really stayed there); and `--interpreter` applied across a
multi-command batch.
- The e2e case that previously asserted the multi-command rejection is
replaced with one asserting it now works.
- `flow validate` passes; `generate` produces no diff.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01R328pa3FUUfga4gYah1iQi

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant