Skip to content

Commit 583ac1a

Browse files
Switch mcp-run-python server to use deno (#1340)
Co-authored-by: Petyo Ivanov <[email protected]>
1 parent 14eced9 commit 583ac1a

24 files changed

+1384
-3090
lines changed

.github/workflows/ci.yml

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
env:
1212
COLUMNS: 150
1313
UV_PYTHON: 3.12
14-
UV_FROZEN: '1'
14+
UV_FROZEN: "1"
1515

1616
permissions:
1717
contents: read
@@ -29,10 +29,9 @@ jobs:
2929
- name: Install dependencies
3030
run: uv sync --all-extras --all-packages --group lint
3131

32-
- uses: actions/setup-node@v4
33-
34-
- run: npm install
35-
working-directory: mcp-run-python
32+
- uses: denoland/setup-deno@v2
33+
with:
34+
deno-version: v2.x
3635

3736
- uses: pre-commit/[email protected]
3837
with:
@@ -144,6 +143,10 @@ jobs:
144143
with:
145144
enable-cache: true
146145

146+
- uses: denoland/setup-deno@v2
147+
with:
148+
deno-version: v2.x
149+
147150
- run: mkdir coverage
148151

149152
# run tests with just `pydantic-ai-slim` dependencies
@@ -233,21 +236,28 @@ jobs:
233236
with:
234237
enable-cache: true
235238

236-
- uses: actions/setup-node@v4
237-
238-
- run: npm install
239-
working-directory: mcp-run-python
240-
241-
- run: npm run lint
242-
working-directory: mcp-run-python
239+
- uses: denoland/setup-deno@v2
240+
with:
241+
deno-version: v2.x
243242

244-
- run: npm run prepare
243+
- run: |
244+
deno fmt
245+
deno lint
246+
deno check src
245247
working-directory: mcp-run-python
246248
247249
- run: uv run --package mcp-run-python pytest mcp-run-python -v
248250

249-
# check npx works
250-
- run: npx . warmup
251+
# check warmup works
252+
- name: warmup
253+
run: |
254+
deno run \
255+
-N \
256+
-R=node_modules \
257+
-W=node_modules \
258+
--node-modules-dir=auto \
259+
src/main.ts \
260+
warmup
251261
working-directory: mcp-run-python
252262

253263
# https://github.com/marketplace/actions/alls-green#why used for branch protection checks

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ lint: ## Lint the code
3737

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

4242
.PHONY: typecheck-pyright
4343
typecheck-pyright:

docs/mcp/client.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ The name "HTTP" is used since this implemented will be adapted in future to use
3636
Before creating the SSE client, we need to run the server (docs [here](run-python.md)):
3737

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

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

6365
**What's happening here?**
6466

65-
* The model is receiving the prompt "how many days between 2000-01-01 and 2025-03-18?"
66-
* 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.
67-
* The model returns a tool call
68-
* PydanticAI sends the tool call to the MCP server using the SSE transport
69-
* The model is called again with the return value of running the code
70-
* The model returns the final answer
67+
- The model is receiving the prompt "how many days between 2000-01-01 and 2025-03-18?"
68+
- 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.
69+
- The model returns a tool call
70+
- PydanticAI sends the tool call to the MCP server using the SSE transport
71+
- The model is called again with the return value of running the code
72+
- The model returns the final answer
7173

7274
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):
7375

@@ -87,14 +89,24 @@ Will display as follows:
8789
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.
8890

8991
!!! note
90-
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.
91-
92+
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.
9293

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

97-
server = MCPServerStdio('npx', ['-y', '@pydantic/mcp-run-python', 'stdio'])
98+
server = MCPServerStdio( # (1)!
99+
'deno',
100+
args=[
101+
'run',
102+
'-N',
103+
'-R=node_modules',
104+
'-W=node_modules',
105+
'--node-modules-dir=auto',
106+
'jsr:@pydantic/mcp-run-python',
107+
'stdio',
108+
]
109+
)
98110
agent = Agent('openai:gpt-4o', mcp_servers=[server])
99111

100112

@@ -104,3 +116,5 @@ async def main():
104116
print(result.data)
105117
#> There are 9,208 days between January 1, 2000, and March 18, 2025.
106118
```
119+
120+
1. See [MCP Run Python](run-python.md) for more information.

docs/mcp/run-python.md

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,47 @@
11
# MCP Run Python
22

3-
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.
3+
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.
44

55
## Features
66

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

1313
## Installation
1414

15-
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):
15+
!!! warning "Switch from npx to deno"
16+
We previously distributed `mcp-run-python` as an `npm` package to use via `npx`.
17+
We now recommend using `deno` instead as it provides better sandboxing and security.
1618

17-
```bash
18-
npx @pydantic/mcp-run-python [stdio|sse]
19-
```
20-
21-
Where:
19+
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/):
2220

23-
* `stdio`: Runs the server with [stdin/stdout transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio) (for subprocess usage)
24-
* `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)
21+
```bash {title="terminal"}
22+
deno run \
23+
-N -R=node_modules -W=node_modules --node-modules-dir=auto \
24+
jsr:@pydantic/mcp-run-python [stdio|sse|warmup]
25+
```
2526

26-
Usage of `@pydantic/mcp-run-python` with PydanticAI is described in the [client](client.md#mcp-stdio-server) documentation.
27+
where:
28+
29+
- `-N -R=node_modules -W=node_modules` (alias of
30+
`--allow-net --allow-read=node_modules --allow-write=node_modules`) allows
31+
network access and read+write access to `./node_modules`. These are required
32+
so Pyodide can download and cache the Python standard library and packages
33+
- `--node-modules-dir=auto` tells deno to use a local `node_modules` directory
34+
- `stdio` runs the server with the
35+
[Stdio MCP transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio)
36+
— suitable for running the process as a subprocess locally
37+
- `sse` runs the server with the
38+
[SSE MCP transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse)
39+
— running the server as an HTTP server to connect locally or remotely
40+
- `warmup` will run a minimal Python script to download and cache the Python
41+
standard library. This is also useful to check the server is running
42+
correctly.
43+
44+
Usage of `jsr:@pydantic/mcp-run-python` with PydanticAI is described in the [client](client.md#mcp-stdio-server) documentation.
2745

2846
## Direct Usage
2947

@@ -39,12 +57,21 @@ a = numpy.array([1, 2, 3])
3957
print(a)
4058
a
4159
"""
60+
server_params = StdioServerParameters(
61+
command='deno',
62+
args=[
63+
'run',
64+
'-N',
65+
'-R=node_modules',
66+
'-W=node_modules',
67+
'--node-modules-dir=auto',
68+
'jsr:@pydantic/mcp-run-python',
69+
'stdio',
70+
],
71+
)
4272

4373

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

98125
```py {title="inline_script_metadata.py" py="3.10"}
99-
from mcp import ClientSession, StdioServerParameters
126+
from mcp import ClientSession
100127
from mcp.client.stdio import stdio_client
101128

129+
# using `server_params` from the above example.
130+
from mcp_run_python import server_params
131+
102132
code = """\
103133
# /// script
104134
# dependencies = ["pydantic", "email-validator"]
@@ -113,9 +143,6 @@ print(Model(email='[email protected]'))
113143

114144

115145
async def main():
116-
server_params = StdioServerParameters(
117-
command='npx', args=['-y', '@pydantic/mcp-run-python', 'stdio']
118-
)
119146
async with stdio_client(server_params) as (read, write):
120147
async with ClientSession(read, write) as session:
121148
await session.initialize()

mcp-run-python/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

mcp-run-python/.prettierignore

Lines changed: 0 additions & 2 deletions
This file was deleted.

mcp-run-python/README.md

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,68 @@
22

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

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

8-
The server can be run with just npx thus:
8+
**See <https://ai.pydantic.dev/mcp/run-python/> for complete documentation.**
9+
10+
The server can be run with `deno` installed using:
911

1012
```bash
11-
npx @pydantic/mcp-run-python [stdio|sse]
13+
deno run \
14+
-N -R=node_modules -W=node_modules --node-modules-dir=auto \
15+
jsr:@pydantic/mcp-run-python [stdio|sse|warmup]
1216
```
1317

1418
where:
1519

16-
- `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
17-
- 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
20+
- `-N -R=node_modules -W=node_modules` (alias of
21+
`--allow-net --allow-read=node_modules --allow-write=node_modules`) allows
22+
network access and read+write access to `./node_modules`. These are required
23+
so pyodide can download and cache the Python standard library and packages
24+
- `--node-modules-dir=auto` tells deno to use a local `node_modules` directory
25+
- `stdio` runs the server with the
26+
[Stdio MCP transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio)
27+
— suitable for running the process as a subprocess locally
28+
- `sse` runs the server with the
29+
[SSE MCP transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse)
30+
— running the server as an HTTP server to connect locally or remotely
31+
- `warmup` will run a minimal Python script to download and cache the Python
32+
standard library. This is also useful to check the server is running
33+
correctly.
34+
35+
Here's an example of using `@pydantic/mcp-run-python` with PydanticAI:
36+
37+
```python
38+
from pydantic_ai import Agent
39+
from pydantic_ai.mcp import MCPServerStdio
40+
41+
import logfire
42+
43+
logfire.configure()
44+
logfire.instrument_mcp()
45+
logfire.instrument_pydantic_ai()
46+
47+
server = MCPServerStdio('deno',
48+
args=[
49+
'run',
50+
'-N',
51+
'-R=node_modules',
52+
'-W=node_modules',
53+
'--node-modules-dir=auto',
54+
'jsr:@pydantic/mcp-run-python',
55+
'stdio',
56+
])
57+
agent = Agent('claude-3-5-haiku-latest', mcp_servers=[server])
58+
59+
60+
async def main():
61+
async with agent.run_mcp_servers():
62+
result = await agent.run('How many days between 2000-01-01 and 2025-03-18?')
63+
print(result.data)
64+
#> There are 9,208 days between January 1, 2000, and March 18, 2025.w
65+
66+
if __name__ == '__main__':
67+
import asyncio
68+
asyncio.run(main())
69+
```

mcp-run-python/cli.js

Lines changed: 0 additions & 5 deletions
This file was deleted.

mcp-run-python/deno.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "@pydantic/mcp-run-python",
3+
"version": "0.0.11",
4+
"license": "MIT",
5+
"nodeModulesDir": "auto",
6+
"exports": {
7+
".": "./src/main.ts"
8+
},
9+
"tasks": {
10+
"lint-format": "deno fmt && deno lint && deno check src"
11+
},
12+
"imports": {
13+
"@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.8.0",
14+
"@std/cli": "jsr:@std/cli@^1.0.15",
15+
"pyodide": "npm:pyodide@^0.27.4",
16+
"zod": "npm:zod@^3.24.2"
17+
},
18+
"fmt": {
19+
"lineWidth": 120,
20+
"semiColons": false,
21+
"singleQuote": true,
22+
"include": [
23+
"src/"
24+
]
25+
},
26+
"compilerOptions": {
27+
"types": [
28+
"node"
29+
],
30+
"lib": [
31+
"ESNext",
32+
"deno.ns",
33+
"dom" // console needs this
34+
]
35+
}
36+
}

0 commit comments

Comments
 (0)