Skip to content

Commit b50c3f9

Browse files
authored
allow multiple sandboxes, rename methods (#3)
1 parent 477d570 commit b50c3f9

File tree

19 files changed

+389
-204
lines changed

19 files changed

+389
-204
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ env*/
1313
.coverage*
1414
/test_tmp/
1515

16-
/mcp_run_python/deno/prepareEnvCode.ts
16+
/mcp_run_python/deno/src/prepareEnvCode.ts

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ repos:
4444
language: system
4545
types: [python]
4646
pass_filenames: false
47+
- id: lint-py
48+
name: lint python
49+
entry: make
50+
args: [lint-py]
51+
language: system
52+
types: [python]
53+
pass_filenames: false
4754
- id: typecheck-py
4855
name: typecheck python
4956
entry: make

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ build: ## Build mcp_run_python/deno/prepareEnvCode.ts
2323

2424
.PHONY: format-ts
2525
format-ts: ## Format TS code
26-
cd mcp_run_python && deno task format
26+
cd mcp_run_python/deno && deno task format
2727

2828
.PHONY: format-py
2929
format-py: ## Format Python code
@@ -35,7 +35,7 @@ format: format-ts format-py ## Format all code
3535

3636
.PHONY: lint-ts
3737
lint-ts: ## Lint TS code
38-
cd mcp_run_python && deno task lint
38+
cd mcp_run_python/deno && deno task lint
3939

4040
.PHONY: lint-py
4141
lint-py: ## Lint Python code
@@ -47,7 +47,7 @@ lint: lint-ts lint-py ## Lint all code
4747

4848
.PHONY: typecheck-ts
4949
typecheck-ts: build ## Typecheck TS code
50-
cd mcp_run_python && deno task typecheck
50+
cd mcp_run_python/deno && deno task typecheck
5151

5252
.PHONY: typecheck-py
5353
typecheck-py: ## Typecheck the code

README.md

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,7 @@ where:
4949
- `example` will run a minimal Python script using `numpy`, useful for checking that the package is working, for the code
5050
to run successfully, you'll need to install `numpy` using `uvx mcp-run-python --deps numpy example`
5151

52-
## Usage in codes as an MCP server
53-
54-
```bash
55-
pip install mcp-run-python
56-
# or
57-
uv add mcp-run-python
58-
```
52+
## Usage with Pydantic AI
5953

6054
Then you can use `mcp-run-python` with Pydantic AI:
6155

@@ -70,7 +64,7 @@ logfire.configure()
7064
logfire.instrument_mcp()
7165
logfire.instrument_pydantic_ai()
7266

73-
server = MCPServerStdio('deno', args=deno_args_prepare('stdio'))
67+
server = MCPServerStdio('uvx', args=['mcp-run-python@latest', 'stdio'], timeout=10)
7468
agent = Agent('claude-3-5-haiku-latest', toolsets=[server])
7569

7670

@@ -85,9 +79,48 @@ if __name__ == '__main__':
8579
asyncio.run(main())
8680
```
8781

88-
**Note**: `deno_args_prepare` can take `deps` as a keyword argument to install dependencies.
89-
As well as returning the args needed to run `mcp_run_python`, `deno_args_prepare` installs the dependencies
90-
so they can be used by the server.
82+
## Usage in codes as an MCP server
83+
84+
First install the `mcp-run-python` package:
85+
86+
```bash
87+
pip install mcp-run-python
88+
# or
89+
uv add mcp-run-python
90+
```
91+
92+
With `mcp-run-python` installed, you can also run deno directly with `prepare_deno_env` or `async_prepare_deno_env`
93+
94+
95+
```python
96+
from pydantic_ai import Agent
97+
from pydantic_ai.mcp import MCPServerStdio
98+
from mcp_run_python import async_prepare_deno_env
99+
100+
import logfire
101+
102+
logfire.configure()
103+
logfire.instrument_mcp()
104+
logfire.instrument_pydantic_ai()
105+
106+
107+
async def main():
108+
async with async_prepare_deno_env('stdio') as deno_env:
109+
server = MCPServerStdio('deno', args=deno_env.args, cwd=deno_env.cwd, timeout=10)
110+
agent = Agent('claude-3-5-haiku-latest', toolsets=[server])
111+
async with agent:
112+
result = await agent.run('How many days between 2000-01-01 and 2025-03-18?')
113+
print(result.output)
114+
#> There are 9,208 days between January 1, 2000, and March 18, 2025.w
115+
116+
if __name__ == '__main__':
117+
import asyncio
118+
asyncio.run(main())
119+
```
120+
121+
**Note**: `prepare_deno_env` can take `deps` as a keyword argument to install dependencies.
122+
As well as returning the args needed to run `mcp_run_python`, `prepare_deno_env` creates a new deno environment
123+
and installs the dependencies so they can be used by the server.
91124

92125
## Usage in code with `code_sandbox`
93126

build/build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
# Define source and destination paths
1010
src = this_dir / 'prepare_env.py'
11-
dst = root_dir / 'mcp_run_python' / 'deno' / 'prepareEnvCode.ts'
11+
dst = root_dir / 'mcp_run_python' / 'deno' / 'src' / 'prepareEnvCode.ts'
1212

1313
python_code = src.read_text()
1414

examples/direct.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
from mcp import ClientSession, StdioServerParameters
22
from mcp.client.stdio import stdio_client
33

4-
from mcp_run_python import deno_args_prepare
4+
from mcp_run_python import async_prepare_deno_env
55

66
code = """
77
import numpy
88
a = numpy.array([1, 2, 3])
99
print(a)
1010
a
1111
"""
12-
server_params = StdioServerParameters(command='deno', args=deno_args_prepare('stdio', deps=['numpy']))
1312

1413

1514
async def main():
16-
async with stdio_client(server_params) as (read, write):
17-
async with ClientSession(read, write) as session:
18-
await session.initialize()
19-
tools = await session.list_tools()
20-
print(len(tools.tools))
21-
print(repr(tools.tools[0].name))
22-
print(repr(tools.tools[0].inputSchema))
23-
result = await session.call_tool('run_python_code', {'python_code': code})
24-
content_block = result.content[0]
25-
assert content_block.type == 'text'
26-
print(content_block.text)
15+
async with async_prepare_deno_env('stdio', dependencies=['numpy']) as deno_env:
16+
server_params = StdioServerParameters(command='deno', args=deno_env.args, cwd=deno_env.cwd)
17+
async with stdio_client(server_params) as (read, write):
18+
async with ClientSession(read, write) as session:
19+
await session.initialize()
20+
tools = await session.list_tools()
21+
print(len(tools.tools))
22+
print(repr(tools.tools[0].name))
23+
print(repr(tools.tools[0].inputSchema))
24+
result = await session.call_tool('run_python_code', {'python_code': code})
25+
content_block = result.content[0]
26+
assert content_block.type == 'text'
27+
print(content_block.text)
2728

2829

2930
if __name__ == '__main__':

examples/pydantic_ai_ex.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
from pydantic_ai import Agent
22
from pydantic_ai.mcp import MCPServerStdio
33

4-
from mcp_run_python import deno_args_prepare
5-
6-
server = MCPServerStdio('deno', args=deno_args_prepare('stdio'))
4+
server = MCPServerStdio('uv', args=['run', 'mcp-run-python', 'stdio'], timeout=10)
5+
# server = MCPServerStdio('uvx', args=['mcp-run-python@latest', 'stdio'], timeout=10)
76
agent_with_python = Agent('claude-3-5-haiku-latest', toolsets=[server])
87

98

examples/sandbox.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from mcp_run_python import code_sandbox
22

33

4-
def print_wait(level: str, message: str):
4+
def log_handler(level: str, message: str):
55
print(f'{level}: {message}')
66

77

@@ -14,7 +14,8 @@ def print_wait(level: str, message: str):
1414

1515

1616
async def main():
17-
async with code_sandbox(dependencies=['numpy'], print_handler=print_wait) as sandbox:
17+
async with code_sandbox(dependencies=['numpy'], log_handler=log_handler, logging_level='debug') as sandbox:
18+
print('running code')
1819
result = await sandbox.eval(code)
1920
print(f'{result["status"].title()}:')
2021
if result['status'] == 'success':

mcp_run_python/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from importlib.metadata import version as _metadata_version
44

55
from .code_sandbox import code_sandbox
6-
from .main import deno_args_prepare, deno_run_server
6+
from .main import async_prepare_deno_env, prepare_deno_env, run_mcp_server
77

88
__version__ = _metadata_version('mcp_run_python')
9-
__all__ = '__version__', 'deno_args_prepare', 'deno_run_server', 'code_sandbox'
9+
__all__ = '__version__', 'prepare_deno_env', 'run_mcp_server', 'code_sandbox', 'async_prepare_deno_env'

mcp_run_python/_cli.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
11
from __future__ import annotations as _annotations
22

33
import argparse
4+
import logging
5+
import sys
46
from collections.abc import Sequence
57

68
from . import __version__
7-
from .main import deno_run_server
9+
from .main import LoggingLevel, run_mcp_server
810

911

10-
def cli(args_list: Sequence[str] | None = None): # pragma: no cover
12+
def cli():
13+
sys.exit(cli_logic())
14+
15+
16+
def cli_logic(args_list: Sequence[str] | None = None) -> int:
1117
"""Run the CLI."""
1218
parser = argparse.ArgumentParser(
1319
prog='mcp-run-python',
1420
description=f'mcp-run-python CLI v{__version__}\n\nMCP server for running untrusted Python code.\n',
1521
formatter_class=argparse.RawTextHelpFormatter,
1622
)
17-
parser.add_argument('--version', action='store_true', help='Show version and exit')
1823

1924
parser.add_argument('--port', type=int, help='Port to run the server on, default 3001.')
20-
parser.add_argument('--deps', help='Comma separated list of dependencies to install')
25+
parser.add_argument('--deps', '--dependencies', help='Comma separated list of dependencies to install')
26+
parser.add_argument('--verbose', action='store_true', help='Enable verbose logging')
27+
parser.add_argument('--version', action='store_true', help='Show version and exit')
2128
parser.add_argument(
2229
'mode',
2330
choices=['stdio', 'streamable-http', 'example'],
@@ -28,8 +35,33 @@ def cli(args_list: Sequence[str] | None = None): # pragma: no cover
2835
args = parser.parse_args(args_list)
2936
if args.version:
3037
print(f'mcp-run-python {__version__}')
38+
return 0
3139
elif args.mode:
40+
logging.basicConfig(
41+
level=logging.DEBUG if args.verbose else logging.INFO,
42+
stream=sys.stderr,
43+
format='%(message)s',
44+
)
45+
3246
deps: list[str] = args.deps.split(',') if args.deps else []
33-
deno_run_server(args.mode.replace('-', '_'), port=args.port, deps=deps, prep_log_handler=print)
47+
return_code = run_mcp_server(
48+
args.mode.replace('-', '_'),
49+
http_port=args.port,
50+
dependencies=deps,
51+
deps_log_handler=deps_log_handler,
52+
)
53+
return return_code
3454
else:
3555
parser.error('Mode is required')
56+
57+
58+
logger = logging.getLogger('mcp-run-python-install')
59+
60+
61+
def deps_log_handler(level: LoggingLevel, msg: str):
62+
if level == 'debug':
63+
logger.debug('install: %s', msg)
64+
elif level == 'info':
65+
logger.info('install: %s', msg)
66+
else:
67+
logger.warning('install: %s', msg)

0 commit comments

Comments
 (0)