Skip to content

Switch mcp-run-python server to use deno #1340

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Apr 3, 2025
Merged
40 changes: 25 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
env:
COLUMNS: 150
UV_PYTHON: 3.12
UV_FROZEN: '1'
UV_FROZEN: "1"

permissions:
contents: read
Expand All @@ -29,10 +29,9 @@ jobs:
- name: Install dependencies
run: uv sync --all-extras --all-packages --group lint

- uses: actions/setup-node@v4

- run: npm install
working-directory: mcp-run-python
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- uses: pre-commit/action@v3.0.0
with:
Expand Down Expand Up @@ -144,6 +143,10 @@ jobs:
with:
enable-cache: true

- uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- run: mkdir coverage

# run tests with just `pydantic-ai-slim` dependencies
Expand Down Expand Up @@ -233,21 +236,28 @@ jobs:
with:
enable-cache: true

- uses: actions/setup-node@v4

- run: npm install
working-directory: mcp-run-python

- run: npm run lint
working-directory: mcp-run-python
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- run: npm run prepare
- run: |
deno fmt
deno lint
deno check src
working-directory: mcp-run-python

- run: uv run --package mcp-run-python pytest mcp-run-python -v

# check npx works
- run: npx . warmup
# check warmup works
- name: warmup
run: |
deno run \
-N \
-R=node_modules \
-W=node_modules \
--node-modules-dir=auto \
src/main.ts \
warmup
working-directory: mcp-run-python

# https://github.com/marketplace/actions/alls-green#why used for branch protection checks
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ lint: ## Lint the code

.PHONY: lint-js
lint-js: ## Lint JS and TS code
cd mcp-run-python && npm run lint
cd mcp-run-python && deno task lint-format

.PHONY: typecheck-pyright
typecheck-pyright:
Expand Down
34 changes: 24 additions & 10 deletions docs/mcp/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ The name "HTTP" is used since this implemented will be adapted in future to use
Before creating the SSE client, we need to run the server (docs [here](run-python.md)):

```bash {title="terminal (run sse server)"}
npx @pydantic/mcp-run-python sse
deno run \
-N -R=node_modules -W=node_modules --node-modules-dir=auto \
jsr:@pydantic/mcp-run-python sse
```

```python {title="mcp_sse_client.py" py="3.10"}
Expand All @@ -62,12 +64,12 @@ _(This example is complete, it can be run "as is" with Python 3.10+ — you'll n

**What's happening here?**

* The model is receiving the prompt "how many days between 2000-01-01 and 2025-03-18?"
* The model decides "Oh, I've got this `run_python_code` tool, that will be a good way to answer this question", and writes some python code to calculate the answer.
* The model returns a tool call
* PydanticAI sends the tool call to the MCP server using the SSE transport
* The model is called again with the return value of running the code
* The model returns the final answer
- The model is receiving the prompt "how many days between 2000-01-01 and 2025-03-18?"
- The model decides "Oh, I've got this `run_python_code` tool, that will be a good way to answer this question", and writes some python code to calculate the answer.
- The model returns a tool call
- PydanticAI sends the tool call to the MCP server using the SSE transport
- The model is called again with the return value of running the code
- The model returns the final answer

You can visualise this clearly, and even see the code that's run by adding three lines of code to instrument the example with [logfire](https://logfire.pydantic.dev/docs):

Expand All @@ -87,14 +89,24 @@ Will display as follows:
The other transport offered by MCP is the [stdio transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio) where the server is run as a subprocess and communicates with the client over `stdin` and `stdout`. In this case, you'd use the [`MCPServerStdio`][pydantic_ai.mcp.MCPServerStdio] class.

!!! note
When using [`MCPServerStdio`][pydantic_ai.mcp.MCPServerStdio] servers, the [`agent.run_mcp_servers()`][pydantic_ai.Agent.run_mcp_servers] context manager is responsible for starting and stopping the server.

When using [`MCPServerStdio`][pydantic_ai.mcp.MCPServerStdio] servers, the [`agent.run_mcp_servers()`][pydantic_ai.Agent.run_mcp_servers] context manager is responsible for starting and stopping the server.

```python {title="mcp_stdio_client.py" py="3.10"}
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPServerStdio

server = MCPServerStdio('npx', ['-y', '@pydantic/mcp-run-python', 'stdio'])
server = MCPServerStdio( # (1)!
'deno',
args=[
'run',
'-N',
'-R=node_modules',
'-W=node_modules',
'--node-modules-dir=auto',
'jsr:@pydantic/mcp-run-python',
'stdio',
]
)
agent = Agent('openai:gpt-4o', mcp_servers=[server])


Expand All @@ -104,3 +116,5 @@ async def main():
print(result.data)
#> There are 9,208 days between January 1, 2000, and March 18, 2025.
```

1. See [MCP Run Python](run-python.md) for more information.
71 changes: 49 additions & 22 deletions docs/mcp/run-python.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,47 @@
# MCP Run Python

The **MCP Run Python** package is an MCP server that allows agents to execute Python code in a secure, sandboxed environment. It uses [Pyodide](https://pyodide.org/) to run Python code in a JavaScript environment, isolating execution from the host system.
The **MCP Run Python** package is an MCP server that allows agents to execute Python code in a secure, sandboxed environment. It uses [Pyodide](https://pyodide.org/) to run Python code in a JavaScript environment with [Deno](https://deno.com/), isolating execution from the host system.

## Features

* **Secure Execution**: Run Python code in a sandboxed WebAssembly environment
* **Package Management**: Automatically detects and installs required dependencies
* **Complete Results**: Captures standard output, standard error, and return values
* **Asynchronous Support**: Runs async code properly
* **Error Handling**: Provides detailed error reports for debugging
- **Secure Execution**: Run Python code in a sandboxed WebAssembly environment
- **Package Management**: Automatically detects and installs required dependencies
- **Complete Results**: Captures standard output, standard error, and return values
- **Asynchronous Support**: Runs async code properly
- **Error Handling**: Provides detailed error reports for debugging

## Installation

The MCP Run Python server is distributed as an [NPM package](https://www.npmjs.com/package/@pydantic/mcp-run-python) and can be run directly using [`npx`](https://docs.npmjs.com/cli/v8/commands/npx):
!!! warning "Switch from npx to deno"
We previously distributed `mcp-run-python` as an `npm` package to use via `npx`.
We now recommend using `deno` instead as it provides better sandboxing and security.

```bash
npx @pydantic/mcp-run-python [stdio|sse]
```

Where:
The MCP Run Python server is distributed as a [JSR package](https://jsr.io/@pydantic/mcp-run-python) and can be run directly using [`deno run`](https://deno.com/):

* `stdio`: Runs the server with [stdin/stdout transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio) (for subprocess usage)
* `sse`: Runs the server with [HTTP Server-Sent Events transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse) (for remote connections)
```bash {title="terminal"}
deno run \
-N -R=node_modules -W=node_modules --node-modules-dir=auto \
jsr:@pydantic/mcp-run-python [stdio|sse|warmup]
```

Usage of `@pydantic/mcp-run-python` with PydanticAI is described in the [client](client.md#mcp-stdio-server) documentation.
where:

- `-N -R=node_modules -W=node_modules` (alias of
`--allow-net --allow-read=node_modules --allow-write=node_modules`) allows
network access and read+write access to `./node_modules`. These are required
so Pyodide can download and cache the Python standard library and packages
- `--node-modules-dir=auto` tells deno to use a local `node_modules` directory
- `stdio` runs the server with the
[Stdio MCP transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio)
— suitable for running the process as a subprocess locally
- `sse` runs the server with the
[SSE MCP transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse)
— running the server as an HTTP server to connect locally or remotely
- `warmup` will run a minimal Python script to download and cache the Python
standard library. This is also useful to check the server is running
correctly.

Usage of `jsr:@pydantic/mcp-run-python` with PydanticAI is described in the [client](client.md#mcp-stdio-server) documentation.

## Direct Usage

Expand All @@ -39,12 +57,21 @@ a = numpy.array([1, 2, 3])
print(a)
a
"""
server_params = StdioServerParameters(
command='deno',
args=[
'run',
'-N',
'-R=node_modules',
'-W=node_modules',
'--node-modules-dir=auto',
'jsr:@pydantic/mcp-run-python',
'stdio',
],
)


async def main():
server_params = StdioServerParameters(
command='npx', args=['-y', '@pydantic/mcp-run-python', 'stdio']
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
Expand Down Expand Up @@ -96,9 +123,12 @@ As introduced in PEP 723, explained [here](https://packaging.python.org/en/lates
This allows use of dependencies that aren't imported in the code, and is more explicit.

```py {title="inline_script_metadata.py" py="3.10"}
from mcp import ClientSession, StdioServerParameters
from mcp import ClientSession
from mcp.client.stdio import stdio_client

# using `server_params` from the above example.
from mcp_run_python import server_params

code = """\
# /// script
# dependencies = ["pydantic", "email-validator"]
Expand All @@ -113,9 +143,6 @@ print(Model(email='hello@pydantic.dev'))


async def main():
server_params = StdioServerParameters(
command='npx', args=['-y', '@pydantic/mcp-run-python', 'stdio']
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
Expand Down
1 change: 1 addition & 0 deletions mcp-run-python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
2 changes: 0 additions & 2 deletions mcp-run-python/.prettierignore

This file was deleted.

64 changes: 58 additions & 6 deletions mcp-run-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,68 @@

[Model Context Protocol](https://modelcontextprotocol.io/) server to run Python code in a sandbox.

The code is executed using [pyodide](https://pyodide.org) in node and is therefore isolated from
the rest of the operating system.
The code is executed using [Pyodide](https://pyodide.org) in [Deno](https://deno.com/) and is therefore
isolated from the rest of the operating system.

The server can be run with just npx thus:
**See <https://ai.pydantic.dev/mcp/run-python/> for complete documentation.**

The server can be run with `deno` installed using:

```bash
npx @pydantic/mcp-run-python [stdio|sse]
deno run \
-N -R=node_modules -W=node_modules --node-modules-dir=auto \
jsr:@pydantic/mcp-run-python [stdio|sse|warmup]
```

where:

- `stdio` runs the server with the [Stdio MCP transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio) — suitable for running the process as a subprocess locally
- and `sse` runs the server with the [SSE MCP transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse) — running the server as an HTTP server to connect locally or remotely
- `-N -R=node_modules -W=node_modules` (alias of
`--allow-net --allow-read=node_modules --allow-write=node_modules`) allows
network access and read+write access to `./node_modules`. These are required
so pyodide can download and cache the Python standard library and packages
- `--node-modules-dir=auto` tells deno to use a local `node_modules` directory
- `stdio` runs the server with the
[Stdio MCP transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio)
— suitable for running the process as a subprocess locally
- `sse` runs the server with the
[SSE MCP transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse)
— running the server as an HTTP server to connect locally or remotely
- `warmup` will run a minimal Python script to download and cache the Python
standard library. This is also useful to check the server is running
correctly.

Here's an example of using `@pydantic/mcp-run-python` with PydanticAI:

```python
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPServerStdio

import logfire

logfire.configure()
logfire.instrument_mcp()
logfire.instrument_pydantic_ai()

server = MCPServerStdio('deno',
args=[
'run',
'-N',
'-R=node_modules',
'-W=node_modules',
'--node-modules-dir=auto',
'jsr:@pydantic/mcp-run-python',
'stdio',
])
agent = Agent('claude-3-5-haiku-latest', mcp_servers=[server])


async def main():
async with agent.run_mcp_servers():
result = await agent.run('How many days between 2000-01-01 and 2025-03-18?')
print(result.data)
#> There are 9,208 days between January 1, 2000, and March 18, 2025.w

if __name__ == '__main__':
import asyncio
asyncio.run(main())
```
5 changes: 0 additions & 5 deletions mcp-run-python/cli.js

This file was deleted.

36 changes: 36 additions & 0 deletions mcp-run-python/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@pydantic/mcp-run-python",
"version": "0.0.11",
"license": "MIT",
"nodeModulesDir": "auto",
"exports": {
".": "./src/main.ts"
},
"tasks": {
"lint-format": "deno fmt && deno lint && deno check src"
},
"imports": {
"@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.8.0",
"@std/cli": "jsr:@std/cli@^1.0.15",
"pyodide": "npm:pyodide@^0.27.4",
"zod": "npm:zod@^3.24.2"
},
"fmt": {
"lineWidth": 120,
"semiColons": false,
"singleQuote": true,
"include": [
"src/"
]
},
"compilerOptions": {
"types": [
"node"
],
"lib": [
"ESNext",
"deno.ns",
"dom" // console needs this
]
}
}
Loading