Skip to content

Commit 27df0e7

Browse files
authored
add code context (#10)
1 parent f1233b4 commit 27df0e7

File tree

6 files changed

+85
-18
lines changed

6 files changed

+85
-18
lines changed

examples/sandbox.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def log_handler(level: str, message: str):
1010

1111
code = """
1212
import numpy, asyncio
13-
a = numpy.array([1, 2, 3])
13+
a = numpy.array(thing)
1414
print(a)
1515
await asyncio.sleep(1)
1616
a
@@ -20,7 +20,7 @@ def log_handler(level: str, message: str):
2020
async def main():
2121
async with code_sandbox(dependencies=['numpy'], log_handler=log_handler) as sandbox:
2222
print('running code')
23-
result = await sandbox.eval(code)
23+
result = await sandbox.eval(code, {'thing': [1, 2, 3]})
2424
print(f'{result["status"].title()}:')
2525
if result['status'] == 'success':
2626
print(result['return_value'])

mcp_run_python/code_sandbox.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from collections.abc import AsyncIterator, Awaitable, Callable
33
from contextlib import asynccontextmanager
44
from dataclasses import dataclass
5-
from typing import Literal, TypeAlias, TypedDict
5+
from typing import Any, Literal, TypeAlias, TypedDict
66

77
from mcp import ClientSession, StdioServerParameters, types as mcp_types
88
from mcp.client.stdio import stdio_client
@@ -28,8 +28,21 @@ class RunError(TypedDict):
2828
class CodeSandbox:
2929
_session: ClientSession
3030

31-
async def eval(self, code: str) -> RunSuccess | RunError:
32-
result = await self._session.call_tool('run_python_code', {'python_code': code})
31+
async def eval(
32+
self,
33+
code: str,
34+
globals: dict[str, Any] | None = None,
35+
) -> RunSuccess | RunError:
36+
"""Run code in the sandbox.
37+
38+
Args:
39+
code: Python code to run.
40+
globals: Dictionary of global variables in context when the code is executed
41+
"""
42+
args: dict[str, Any] = {'python_code': code}
43+
if globals is not None:
44+
args['global_variables'] = globals
45+
result = await self._session.call_tool('run_python_code', args)
3346
content_block = result.content[0]
3447
if content_block.type == 'text':
3548
return json.loads(content_block.text)
@@ -44,7 +57,7 @@ async def code_sandbox(
4457
log_handler: LogHandler | None = None,
4558
allow_networking: bool = True,
4659
) -> AsyncIterator['CodeSandbox']:
47-
"""Run code in a secure sandbox.
60+
"""Create a secure sandbox.
4861
4962
Args:
5063
dependencies: A list of dependencies to be installed.

mcp_run_python/deno/src/main.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// deno-lint-ignore-file no-explicit-any
12
/// <reference types="npm:@types/node@22.12.0" />
23

34
import './polyfill.ts'
@@ -88,17 +89,23 @@ The code will be executed with Python 3.12.
8889
server.tool(
8990
'run_python_code',
9091
toolDescription,
91-
{ python_code: z.string().describe('Python code to run') },
92-
async ({ python_code }: { python_code: string }) => {
92+
{
93+
python_code: z.string().describe('Python code to run'),
94+
global_variables: z.record(z.string(), z.any()).default({}).describe(
95+
'Map of global variables in context when the code is executed',
96+
),
97+
},
98+
async ({ python_code, global_variables }: { python_code: string; global_variables: Record<string, any> }) => {
9399
const logPromises: Promise<void>[] = []
94100
const result = await runCode.run(
95101
deps,
96-
{ name: 'main.py', content: python_code },
97102
(level, data) => {
98103
if (LogLevels.indexOf(level) >= LogLevels.indexOf(setLogLevel)) {
99104
logPromises.push(server.server.sendLoggingMessage({ level, data }))
100105
}
101106
},
107+
{ name: 'main.py', content: python_code },
108+
global_variables,
102109
)
103110
await Promise.all(logPromises)
104111
return {
@@ -122,7 +129,6 @@ function httpGetUrl(req: http.IncomingMessage): URL {
122129
function httpGetBody(req: http.IncomingMessage): Promise<JSON> {
123130
// https://nodejs.org/en/learn/modules/anatomy-of-an-http-transaction#request-body
124131
return new Promise((resolve) => {
125-
// deno-lint-ignore no-explicit-any
126132
const bodyParts: any[] = []
127133
let body
128134
req.on('data', (chunk) => {
@@ -255,7 +261,6 @@ async function installDeps(deps: string[]) {
255261
const runCode = new RunCode()
256262
const result = await runCode.run(
257263
deps,
258-
undefined,
259264
(level, data) => console.error(`${level}|${data}`),
260265
)
261266
if (result.status !== 'success') {
@@ -280,9 +285,9 @@ a
280285
const runCode = new RunCode()
281286
const result = await runCode.run(
282287
deps,
283-
{ name: 'example.py', content: code },
284288
// use warn to avoid recursion since console.log is patched in runCode
285289
(level, data) => console.warn(`${level}: ${data}`),
290+
{ name: 'example.py', content: code },
286291
)
287292
console.log('Tool return value:')
288293
console.log(asXml(result))

mcp_run_python/deno/src/runCode.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ export class RunCode {
2323

2424
async run(
2525
dependencies: string[],
26-
file: CodeFile | undefined,
2726
log: (level: LoggingLevel, data: string) => void,
27+
file?: CodeFile,
28+
globals?: Record<string, any>,
2829
): Promise<RunSuccess | RunError> {
2930
// remove once we can upgrade to pyodide 0.27.7 and console.log is no longer used.
3031
const realConsoleLog = console.log
@@ -60,7 +61,7 @@ export class RunCode {
6061
} else if (file) {
6162
try {
6263
const rawValue = await pyodide.runPythonAsync(file.content, {
63-
globals: pyodide.toPy({ __name__: '__main__' }),
64+
globals: pyodide.toPy({ ...(globals || {}), __name__: '__main__' }),
6465
filename: file.name,
6566
})
6667
runResult = {

tests/test_mcp_servers.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,15 @@ async def test_list_tools(run_mcp_session: Callable[[list[str]], AbstractAsyncCo
8282
assert tool.description
8383
assert tool.description.startswith('Tool to execute Python code and return stdout, stderr, and return value.')
8484
assert tool.inputSchema['properties'] == snapshot(
85-
{'python_code': {'type': 'string', 'description': 'Python code to run'}}
85+
{
86+
'python_code': {'type': 'string', 'description': 'Python code to run'},
87+
'global_variables': {
88+
'type': 'object',
89+
'additionalProperties': {},
90+
'default': {},
91+
'description': 'Map of global variables in context when the code is executed',
92+
},
93+
}
8694
)
8795

8896

tests/test_sandbox.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from dataclasses import dataclass
12
from typing import Any
23

34
import pytest
@@ -8,24 +9,62 @@
89
pytestmark = pytest.mark.anyio
910

1011

12+
@dataclass
13+
class Foobar:
14+
a: int
15+
b: str
16+
c: bytes
17+
18+
1119
@pytest.mark.parametrize(
12-
'deps,code,expected',
20+
'deps,code,locals,expected',
1321
[
1422
pytest.param(
1523
[],
1624
'a = 1\na + 1',
25+
{},
1726
snapshot({'status': 'success', 'output': [], 'return_value': 2}),
1827
id='return-value-success',
1928
),
2029
pytest.param(
2130
[],
2231
'print(123)',
32+
{},
2333
snapshot({'status': 'success', 'output': ['123'], 'return_value': None}),
2434
id='print-success',
2535
),
36+
pytest.param(
37+
[],
38+
'a',
39+
{'a': [1, 2, 3]},
40+
snapshot({'status': 'success', 'output': [], 'return_value': [1, 2, 3]}),
41+
id='access-local-variables',
42+
),
43+
pytest.param(
44+
[],
45+
'a + b',
46+
{'a': 4, 'b': 5},
47+
snapshot({'status': 'success', 'output': [], 'return_value': 9}),
48+
id='multiple-locals',
49+
),
50+
pytest.param(
51+
[],
52+
'print(f)',
53+
{'f': Foobar(1, '2', b'3')},
54+
snapshot({'status': 'success', 'output': ["{'a': 1, 'b': '2', 'c': '3'}"], 'return_value': None}),
55+
id='print-complex-local',
56+
),
57+
pytest.param(
58+
[],
59+
'f',
60+
{'f': Foobar(1, '2', b'3')},
61+
snapshot({'status': 'success', 'output': [], 'return_value': {'a': 1, 'b': '2', 'c': '3'}}),
62+
id='return-complex-local',
63+
),
2664
pytest.param(
2765
[],
2866
'print(unknown)',
67+
{},
2968
snapshot(
3069
{
3170
'status': 'run-error',
@@ -44,14 +83,15 @@
4483
pytest.param(
4584
['numpy'],
4685
'import numpy\nnumpy.array([1, 2, 3])',
86+
{},
4787
snapshot({'status': 'success', 'output': [], 'return_value': [1, 2, 3]}),
4888
id='return-numpy-success',
4989
),
5090
],
5191
)
52-
async def test_sandbox(deps: list[str], code: str, expected: Any):
92+
async def test_sandbox(deps: list[str], code: str, locals: dict[str, Any], expected: Any):
5393
async with code_sandbox(dependencies=deps) as sandbox:
54-
result = await sandbox.eval(code)
94+
result = await sandbox.eval(code, locals)
5595
assert result == expected
5696

5797

0 commit comments

Comments
 (0)