Skip to content
This repository was archived by the owner on Jan 30, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ To use this server, you must have both Python and [Deno](https://deno.com/) inst
The server can be run with `deno` installed using `uvx`:

```bash
uvx mcp-run-python [-h] [--version] [--port PORT] [--deps DEPS] {stdio,streamable-http,streamable-http-stateless,example}
uvx mcp-run-python [-h] [--version] [--port PORT] [--dep PKG]... [--index-url URL]... {stdio,streamable-http,streamable-http-stateless,example}
```

where:
Expand All @@ -49,7 +49,7 @@ where:
- `streamable-http-stateless` runs the server with [Streamable HTTP MCP transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http) in stateless mode and does not
support server-to-client notifications
- `example` will run a minimal Python script using `numpy`, useful for checking that the package is working, for the code
to run successfully, you'll need to install `numpy` using `uvx mcp-run-python --deps numpy example`
to run successfully, you'll need to install `numpy` using `uvx mcp-run-python --dep numpy example`

## Usage with Pydantic AI

Expand Down Expand Up @@ -166,3 +166,13 @@ edit the filesystem.
* `deno` is then run with read-only permissions to the `node_modules` directory to run untrusted code.

Dependencies must be provided when initializing the server so they can be installed in the first step.

## Custom Package Indexes

Use `--index-url` to install dependencies from private registries (can be repeated, tried in order before PyPI):

```bash
uvx mcp-run-python --index-url https://private.repo.com/simple --dep mypackage stdio
```

The Python API accepts `index_urls` in `code_sandbox`, `prepare_deno_env`, and `run_mcp_server`. See [micropip documentation](https://micropip.pyodide.org/en/stable/project/api.html#micropip.install) for index URL requirements.
4 changes: 2 additions & 2 deletions build/prepare_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ class Error:
kind: Literal['error'] = 'error'


async def prepare_env(dependencies: list[str] | None) -> Success | Error:
async def prepare_env(dependencies: list[str] | None, index_urls: list[str] | None = None) -> Success | Error:
sys.setrecursionlimit(400)

if dependencies:
dependencies = _add_extra_dependencies(dependencies)

with _micropip_logging() as logs_filename:
try:
await micropip.install(dependencies, keep_going=True)
await micropip.install(dependencies, keep_going=True, index_urls=index_urls or None)
importlib.invalidate_caches()
except Exception:
with open(logs_filename) as f:
Expand Down
24 changes: 22 additions & 2 deletions mcp_run_python/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import argparse
import logging
import sys
import warnings
from collections.abc import Sequence

from . import __version__
Expand All @@ -22,7 +23,17 @@ def cli_logic(args_list: Sequence[str] | None = None) -> int:
)

parser.add_argument('--port', type=int, help='Port to run the server on, default 3001.')
parser.add_argument('--deps', '--dependencies', help='Comma separated list of dependencies to install')
parser.add_argument(
'--dep', action='append', dest='dep_list', metavar='PKG', help='Dependency to install (can be repeated)'
)
parser.add_argument('--deps', '--dependencies', help=argparse.SUPPRESS)
parser.add_argument(
'--index-url',
action='append',
dest='index_urls',
metavar='URL',
help='Package index URL for installing dependencies (can be repeated, tried in order before PyPI)',
)
parser.add_argument(
'--disable-networking', action='store_true', help='Disable networking during execution of python code'
)
Expand All @@ -46,12 +57,21 @@ def cli_logic(args_list: Sequence[str] | None = None) -> int:
format='%(message)s',
)

deps: list[str] = args.deps.split(',') if args.deps else []
deps: list[str] = args.dep_list or []
if args.deps:
warnings.warn(
'--deps is deprecated, use --dep instead (can be repeated)',
DeprecationWarning,
stacklevel=2,
)
deps.extend(args.deps.split(','))
index_urls: list[str] = args.index_urls or []
return_code = run_mcp_server(
args.mode.replace('-', '_'),
allow_networking=not args.disable_networking,
http_port=args.port,
dependencies=deps,
index_urls=index_urls,
deps_log_handler=deps_log_handler,
verbose=bool(args.verbose),
)
Expand Down
3 changes: 3 additions & 0 deletions mcp_run_python/code_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,23 @@ async def eval(
async def code_sandbox(
*,
dependencies: list[str] | None = None,
index_urls: list[str] | None = None,
log_handler: LogHandler | None = None,
allow_networking: bool = True,
) -> AsyncIterator['CodeSandbox']:
"""Create a secure sandbox.

Args:
dependencies: A list of dependencies to be installed.
index_urls: Package index URLs for installing dependencies (tried in order before PyPI).
log_handler: A callback function to handle print statements when code is running.
deps_log_handler: A callback function to run on log statements during initial install of dependencies.
allow_networking: Whether to allow networking or not while executing python code.
"""
async with async_prepare_deno_env(
'stdio',
dependencies=dependencies,
index_urls=index_urls,
deps_log_handler=log_handler,
return_mode='json',
allow_networking=allow_networking,
Expand Down
61 changes: 41 additions & 20 deletions mcp_run_python/deno/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,37 @@ export async function main() {
const { args } = Deno
const flags = parseArgs(Deno.args, {
string: ['deps', 'return-mode', 'port'],
collect: ['dep', 'index-url'],
default: { port: '3001', 'return-mode': 'xml' },
})
const deps = flags.deps?.split(',') ?? []

// Deprecation warnings for old comma-separated args
if (flags.deps) {
console.warn('Warning: --deps is deprecated, use --dep instead (can be repeated)')
}
// Support both new repeatable args and old comma-separated (backwards compat)
const deps: string[] = [
...((flags.dep as string[] | undefined) ?? []),
...(flags.deps?.split(',').filter(Boolean) ?? []),
]
const indexUrls: string[] = (flags['index-url'] as string[] | undefined) ?? []
if (args.length >= 1) {
if (args[0] === 'stdio') {
await runStdio(deps, flags['return-mode'])
await runStdio(deps, indexUrls, flags['return-mode'])
return
} else if (args[0] === 'streamable_http') {
const port = parseInt(flags.port)
runStreamableHttp(port, deps, flags['return-mode'], false)
runStreamableHttp(port, deps, indexUrls, flags['return-mode'], false)
return
} else if (args[0] === 'streamable_http_stateless') {
const port = parseInt(flags.port)
runStreamableHttp(port, deps, flags['return-mode'], true)
runStreamableHttp(port, deps, indexUrls, flags['return-mode'], true)
return
} else if (args[0] === 'example') {
await example(deps)
await example(deps, indexUrls)
return
} else if (args[0] === 'noop') {
await installDeps(deps)
await installDeps(deps, indexUrls)
return
}
}
Expand All @@ -51,17 +62,18 @@ Invalid arguments: ${args.join(' ')}
Usage: deno ... deno/main.ts [stdio|streamable_http|streamable_http_stateless|example|noop]

options:
--port <port> Port to run the HTTP server on (default: 3001)
--deps <deps> Comma separated list of dependencies to install
--return-mode <xml/json> Return mode for output data (default: xml)`,
--port <port> Port to run the HTTP server on (default: 3001)
--dep <pkg> Dependency to install (can be repeated)
--index-url <url> Package index URL (can be repeated, tried before PyPI)
--return-mode <xml/json> Return mode for output data (default: xml)`,
)
Deno.exit(1)
}

/*
* Create an MCP server with the `run_python_code` tool registered.
*/
function createServer(deps: string[], returnMode: string): McpServer {
function createServer(deps: string[], indexUrls: string[], returnMode: string): McpServer {
const runCode = new RunCode()
const server = new McpServer(
{
Expand Down Expand Up @@ -106,6 +118,7 @@ The code will be executed with Python 3.13.
const logPromises: Promise<void>[] = []
const result = await runCode.run(
deps,
indexUrls,
(level, data) => {
if (LogLevels.indexOf(level) >= LogLevels.indexOf(setLogLevel)) {
logPromises.push(server.server.sendLoggingMessage({ level, data }))
Expand Down Expand Up @@ -171,14 +184,20 @@ function httpSetJsonResponse(res: http.ServerResponse, status: number, text: str
/*
* Run the MCP server using the Streamable HTTP transport
*/
function runStreamableHttp(port: number, deps: string[], returnMode: string, stateless: boolean): void {
const server = (stateless ? createStatelessHttpServer : createStatefulHttpServer)(deps, returnMode)
function runStreamableHttp(
port: number,
deps: string[],
indexUrls: string[],
returnMode: string,
stateless: boolean,
): void {
const server = (stateless ? createStatelessHttpServer : createStatefulHttpServer)(deps, indexUrls, returnMode)
server.listen(port, () => {
console.log(`Listening on port ${port}`)
})
}

function createStatelessHttpServer(deps: string[], returnMode: string): http.Server {
function createStatelessHttpServer(deps: string[], indexUrls: string[], returnMode: string): http.Server {
return http.createServer(async (req, res) => {
const url = httpGetUrl(req)

Expand All @@ -188,7 +207,7 @@ function createStatelessHttpServer(deps: string[], returnMode: string): http.Ser
}

try {
const mcpServer = createServer(deps, returnMode)
const mcpServer = createServer(deps, indexUrls, returnMode)
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
})
Expand All @@ -211,10 +230,10 @@ function createStatelessHttpServer(deps: string[], returnMode: string): http.Ser
})
}

function createStatefulHttpServer(deps: string[], returnMode: string): http.Server {
function createStatefulHttpServer(deps: string[], indexUrls: string[], returnMode: string): http.Server {
// Stateful mode with session management
// https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#with-session-management
const mcpServer = createServer(deps, returnMode)
const mcpServer = createServer(deps, indexUrls, returnMode)
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}

return http.createServer(async (req, res) => {
Expand Down Expand Up @@ -293,19 +312,20 @@ function createStatefulHttpServer(deps: string[], returnMode: string): http.Serv
/*
* Run the MCP server using the Stdio transport.
*/
async function runStdio(deps: string[], returnMode: string) {
const mcpServer = createServer(deps, returnMode)
async function runStdio(deps: string[], indexUrls: string[], returnMode: string) {
const mcpServer = createServer(deps, indexUrls, returnMode)
const transport = new StdioServerTransport()
await mcpServer.connect(transport)
}

/*
* Run pyodide to download and install dependencies.
*/
async function installDeps(deps: string[]) {
async function installDeps(deps: string[], indexUrls: string[]) {
const runCode = new RunCode()
const result = await runCode.run(
deps,
indexUrls,
(level, data) => console.error(`${level}|${data}`),
)
if (result.status !== 'success') {
Expand All @@ -317,7 +337,7 @@ async function installDeps(deps: string[]) {
/*
* Run a short example script that requires numpy.
*/
async function example(deps: string[]) {
async function example(deps: string[], indexUrls: string[]) {
console.error(
`Running example script for MCP Run Python version ${VERSION}...`,
)
Expand All @@ -330,6 +350,7 @@ a
const runCode = new RunCode()
const result = await runCode.run(
deps,
indexUrls,
// use warn to avoid recursion since console.log is patched in runCode
(level, data) => console.warn(`${level}: ${data}`),
{ name: 'example.py', content: code },
Expand Down
11 changes: 8 additions & 3 deletions mcp_run_python/deno/src/runCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class RunCode {

async run(
dependencies: string[],
indexUrls: string[],
log: (level: LoggingLevel, data: string) => void,
file?: CodeFile,
globals?: Record<string, any>,
Expand All @@ -38,7 +39,7 @@ export class RunCode {
sys = pyodide.pyimport('sys')
} else {
if (!this.prepPromise) {
this.prepPromise = this.prepEnv(dependencies, log)
this.prepPromise = this.prepEnv(dependencies, indexUrls, log)
}
// TODO is this safe if the promise has already been accessed? it seems to work fine
const prep = await this.prepPromise
Expand Down Expand Up @@ -83,6 +84,7 @@ export class RunCode {

async prepEnv(
dependencies: string[],
indexUrls: string[],
log: (level: LoggingLevel, data: string) => void,
): Promise<PrepResult> {
const pyodide = await loadPyodide({
Expand Down Expand Up @@ -122,7 +124,10 @@ export class RunCode {

const preparePyEnv: PreparePyEnv = pyodide.pyimport(moduleName)

const prepareStatus = await preparePyEnv.prepare_env(pyodide.toPy(dependencies))
const prepareStatus = await preparePyEnv.prepare_env(
pyodide.toPy(dependencies),
pyodide.toPy(indexUrls),
)
return {
pyodide,
preparePyEnv,
Expand Down Expand Up @@ -214,6 +219,6 @@ interface PrepareError {
message: string
}
interface PreparePyEnv {
prepare_env: (files: CodeFile[]) => Promise<PrepareSuccess | PrepareError>
prepare_env: (dependencies: any, index_urls: any) => Promise<PrepareSuccess | PrepareError>
dump_json: (value: any, always_return_json: boolean) => string | null
}
Loading