Skip to content

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 to run Python code in a JavaScript environment, 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

Installation

The MCP Run Python server is distributed as an NPM package and can be run directly using npx:

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

Where:

Usage of @pydantic/mcp-run-python with PydanticAI is described in the client documentation.

Direct Usage

As well as using this server with PydanticAI, it can be connected to other MCP clients. For clarity, in this example we connect directly using the Python MCP client.

mcp_run_python.py
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

code = """
import numpy
a = numpy.array([1, 2, 3])
print(a)
a
"""


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()
            tools = await session.list_tools()
            print(len(tools.tools))
            #> 1
            print(repr(tools.tools[0].name))
            #> 'run_python_code'
            print(repr(tools.tools[0].inputSchema))
            """
            {'type': 'object', 'properties': {'python_code': {'type': 'string', 'description': 'Python code to run'}}, 'required': ['python_code'], 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}
            """
            result = await session.call_tool('run_python_code', {'python_code': code})
            print(result.content[0].text)
            """
            <status>success</status>
            <dependencies>["numpy"]</dependencies>
            <output>
            [1 2 3]
            </output>
            <return_value>
            [
              1,
              2,
              3
            ]
            </return_value>
            """

If an exception occurs, status will be install-error or run-error and return_value will be replaced by error which will include the traceback and exception message.

Dependencies

Dependencies are installed when code is run.

Dependencies can be defined in one of two ways:

Inferred from imports

If there's no metadata, dependencies are inferred from imports in the code, as shown in the example above.

Inline script metadata

As introduced in PEP 723, explained here, and popularized by uv — dependencies can be defined in a comment at the top of the file.

This allows use of dependencies that aren't imported in the code, and is more explicit.

inline_script_metadata.py
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

code = """\
# /// script
# dependencies = ["pydantic", "email-validator"]
# ///
import pydantic

class Model(pydantic.BaseModel):
    email: pydantic.EmailStr

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()
            result = await session.call_tool('run_python_code', {'python_code': code})
            print(result.content[0].text)
            """
            <status>success</status>
            <dependencies>["pydantic","email-validator"]</dependencies>
            <output>
            email='hello@pydantic.dev'
            </output>
            """

It also allows versions to be pinned for non-binary packages (Pyodide only supports a single version for the binary packages it supports, like pydantic and numpy).

E.g. you could set the dependencies to

# /// script
# dependencies = ["rich<13"]
# ///

Logging

MCP Run Python supports emitting stdout and stderr from the python execution as MCP logging messages.

For logs to be emitted you must set the logging level when connecting to the server. By default, the log level is set to the highest level, emergency.

Currently, it's not possible to demonstrate this due to a bug in the Python MCP Client, see modelcontextprotocol/python-sdk#201.