From b2b0cb027f8388bd71f8606a77222c778d6aaae3 Mon Sep 17 00:00:00 2001 From: luoshasha Date: Wed, 26 Nov 2025 09:58:12 +0800 Subject: [PATCH 01/12] feat: add [--host] parameter for streamable-http mode. --- mcp_run_python/_cli.py | 4 +- mcp_run_python/deno/src/main.ts | 13 ++++-- mcp_run_python/main.py | 83 +++++++++++++++++++-------------- 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/mcp_run_python/_cli.py b/mcp_run_python/_cli.py index 69bab42..7adc87b 100644 --- a/mcp_run_python/_cli.py +++ b/mcp_run_python/_cli.py @@ -20,7 +20,8 @@ def cli_logic(args_list: Sequence[str] | None = None) -> int: description=f'mcp-run-python CLI v{__version__}\n\nMCP server for running untrusted Python code.\n', formatter_class=argparse.RawTextHelpFormatter, ) - + parser.add_argument('--host', type=str, default='127.0.0.1', + help='Host to bind the HTTP server to (default: 127.0.0.1). Use 0.0.0.0 to bind to all interfaces, e.g. when using Docker.') 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( @@ -51,6 +52,7 @@ def cli_logic(args_list: Sequence[str] | None = None) -> int: args.mode.replace('-', '_'), allow_networking=not args.disable_networking, http_port=args.port, + http_host=args.host, dependencies=deps, deps_log_handler=deps_log_handler, verbose=bool(args.verbose), diff --git a/mcp_run_python/deno/src/main.ts b/mcp_run_python/deno/src/main.ts index 35920ae..843cc1d 100644 --- a/mcp_run_python/deno/src/main.ts +++ b/mcp_run_python/deno/src/main.ts @@ -30,11 +30,13 @@ export async function main() { return } else if (args[0] === 'streamable_http') { const port = parseInt(flags.port) - runStreamableHttp(port, deps, flags['return-mode'], false) + const host = flags.host + runStreamableHttp(port, host, deps, flags['return-mode'], false) return } else if (args[0] === 'streamable_http_stateless') { const port = parseInt(flags.port) - runStreamableHttp(port, deps, flags['return-mode'], true) + const host = flags.host + runStreamableHttp(port, host, deps, flags['return-mode'], true) return } else if (args[0] === 'example') { await example(deps) @@ -52,6 +54,7 @@ Usage: deno ... deno/main.ts [stdio|streamable_http|streamable_http_stateless|in options: --port Port to run the HTTP server on (default: 3001) +--host Host to run the HTTP server on (default: 127.0.0.1) --deps Comma separated list of dependencies to install --return-mode Return mode for output data (default: xml)`, ) @@ -171,9 +174,9 @@ 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 { +function runStreamableHttp(port: number, host:string, deps: string[], returnMode: string, stateless: boolean): void { const server = (stateless ? createStatelessHttpServer : createStatefulHttpServer)(deps, returnMode) - server.listen(port, () => { + server.listen(port, host, () => { console.log(`Listening on port ${port}`) }) } @@ -353,4 +356,4 @@ const LogLevels: LoggingLevel[] = [ 'emergency', ] -await main() +await main() \ No newline at end of file diff --git a/mcp_run_python/main.py b/mcp_run_python/main.py index 413f254..61894b3 100644 --- a/mcp_run_python/main.py +++ b/mcp_run_python/main.py @@ -11,7 +11,7 @@ from pathlib import Path from typing import Literal, ParamSpec, TypeVar, cast -__all__ = 'run_mcp_server', 'DenoEnv', 'prepare_deno_env', 'async_prepare_deno_env' +__all__ = "run_mcp_server", "DenoEnv", "prepare_deno_env", "async_prepare_deno_env" logger = logging.getLogger(__name__) LoggingLevel = Literal['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'] @@ -23,8 +23,9 @@ def run_mcp_server( mode: Mode, *, http_port: int | None = None, + http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal['json', 'xml'] = 'xml', + return_mode: Literal["json", "xml"] = "xml", deps_log_handler: LogHandler | None = None, allow_networking: bool = True, verbose: bool = False, @@ -34,6 +35,7 @@ def run_mcp_server( Args: mode: The mode to run the server in. http_port: The port to run the server on if mode is `streamable_http`. + http_host: The host to run the server on if mode is `streamable_http`. dependencies: The dependencies to install. return_mode: The mode to return tool results in. deps_log_handler: Optional function to receive logs emitted while installing dependencies. @@ -49,6 +51,7 @@ def run_mcp_server( mode, dependencies=dependencies, http_port=http_port, + http_host=http_host, return_mode=return_mode, deps_log_handler=deps_log_handler, allow_networking=allow_networking, @@ -56,12 +59,14 @@ def run_mcp_server( if mode in ('streamable_http', 'streamable_http_stateless'): logger.info('Running mcp-run-python via %s on port %d...', mode, http_port) else: - logger.info('Running mcp-run-python via %s...', mode) + logger.info("Running mcp-run-python via %s...", mode) try: - p = subprocess.run(('deno', *env.args), cwd=env.cwd, stdout=stdout, stderr=stderr) + p = subprocess.run( + ("deno", *env.args), cwd=env.cwd, stdout=stdout, stderr=stderr + ) except KeyboardInterrupt: # pragma: no cover - logger.warning('Server stopped.') + logger.warning("Server stopped.") return 0 else: return p.returncode @@ -78,8 +83,9 @@ def prepare_deno_env( mode: Mode, *, http_port: int | None = None, + http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal['json', 'xml'] = 'xml', + return_mode: Literal["json", "xml"] = "xml", deps_log_handler: LogHandler | None = None, allow_networking: bool = True, ) -> Iterator[DenoEnv]: @@ -92,6 +98,7 @@ def prepare_deno_env( Args: mode: The mode to run the server in. http_port: The port to run the server on if mode is `streamable_http`. + http_host: The host to run the server on if mode is `streamable_http`. dependencies: The dependencies to install. return_mode: The mode to return tool results in. deps_log_handler: Optional function to receive logs emitted while installing dependencies. @@ -101,22 +108,22 @@ def prepare_deno_env( Returns: Yields the deno environment details. """ - cwd = Path(tempfile.mkdtemp()) / 'mcp-run-python' + cwd = Path(tempfile.mkdtemp()) / "mcp-run-python" try: - src = Path(__file__).parent / 'deno' - logger.debug('Copying from %s to %s...', src, cwd) + src = Path(__file__).parent / "deno" + logger.debug("Copying from %s to %s...", src, cwd) shutil.copytree(src, cwd) - logger.info('Installing dependencies %s...', dependencies) + logger.info("Installing dependencies %s...", dependencies) - args = 'deno', *_deno_install_args(dependencies) + args = "deno", *_deno_install_args(dependencies) p = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) stdout: list[str] = [] if p.stdout is not None: for line in p.stdout: line = line.strip() if deps_log_handler: - parts = line.split('|', 1) - level, msg = parts if len(parts) == 2 else ('info', line) + parts = line.split("|", 1) + level, msg = parts if len(parts) == 2 else ("info", line) deps_log_handler(cast(LoggingLevel, level), msg) stdout.append(line) p.wait() @@ -126,6 +133,7 @@ def prepare_deno_env( args = _deno_run_args( mode, http_port=http_port, + http_host=http_host, dependencies=dependencies, return_mode=return_mode, allow_networking=allow_networking, @@ -141,8 +149,9 @@ async def async_prepare_deno_env( mode: Mode, *, http_port: int | None = None, + http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal['json', 'xml'] = 'xml', + return_mode: Literal["json", "xml"] = "xml", deps_log_handler: LogHandler | None = None, allow_networking: bool = True, ) -> AsyncIterator[DenoEnv]: @@ -151,6 +160,7 @@ async def async_prepare_deno_env( prepare_deno_env, mode, http_port=http_port, + http_host=http_host, dependencies=dependencies, return_mode=return_mode, deps_log_handler=deps_log_handler, @@ -164,13 +174,13 @@ async def async_prepare_deno_env( def _deno_install_args(dependencies: list[str] | None = None) -> list[str]: args = [ - 'run', - '--allow-net', - '--allow-read=./node_modules', - '--allow-write=./node_modules', - '--node-modules-dir=auto', - 'src/main.ts', - 'noop', + "run", + "--allow-net", + "--allow-read=./node_modules", + "--allow-write=./node_modules", + "--node-modules-dir=auto", + "src/main.ts", + "noop", ] if dependencies is not None: args.append(f'--deps={",".join(dependencies)}') @@ -181,32 +191,35 @@ def _deno_run_args( mode: Mode, *, http_port: int | None = None, + http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal['json', 'xml'] = 'xml', + return_mode: Literal["json", "xml"] = "xml", allow_networking: bool = True, ) -> list[str]: - args = ['run'] + args = ["run"] if allow_networking: - args += ['--allow-net'] + args += ["--allow-net"] args += [ - '--allow-read=./node_modules', - '--node-modules-dir=auto', - 'src/main.ts', + "--allow-read=./node_modules", + "--node-modules-dir=auto", + "src/main.ts", mode, - f'--return-mode={return_mode}', + f"--return-mode={return_mode}", ] if dependencies is not None: args.append(f'--deps={",".join(dependencies)}') - if http_port is not None: - if mode in ('streamable_http', 'streamable_http_stateless'): - args.append(f'--port={http_port}') - else: - raise ValueError('Port is only supported for `streamable_http` modes') + if mode in ('streamable_http', 'streamable_http_stateless'): + if http_port is not None: + args.append(f"--port={http_port}") + if http_host is not None: + args.append(f"--host={http_host}") + elif http_port is not None or http_host is not None: + raise ValueError("Port and host are only supported for `streamable_http` mode") return args -P = ParamSpec('P') -T = TypeVar('T') +P = ParamSpec("P") +T = TypeVar("T") async def _asyncify(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: From 27e3f0e9ac21c15c434d26413f67aec9b32c06d6 Mon Sep 17 00:00:00 2001 From: luoshasha Date: Thu, 20 Nov 2025 11:20:23 +0800 Subject: [PATCH 02/12] fix: add host to cli error info. --- mcp_run_python/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mcp_run_python/main.py b/mcp_run_python/main.py index 61894b3..e79d427 100644 --- a/mcp_run_python/main.py +++ b/mcp_run_python/main.py @@ -85,7 +85,7 @@ def prepare_deno_env( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal["json", "xml"] = "xml", + return_mode: Literal['json', 'xml'] = 'xml', deps_log_handler: LogHandler | None = None, allow_networking: bool = True, ) -> Iterator[DenoEnv]: @@ -151,7 +151,7 @@ async def async_prepare_deno_env( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal["json", "xml"] = "xml", + return_mode: Literal['json', 'xml'] = 'xml', deps_log_handler: LogHandler | None = None, allow_networking: bool = True, ) -> AsyncIterator[DenoEnv]: @@ -193,7 +193,7 @@ def _deno_run_args( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal["json", "xml"] = "xml", + return_mode: Literal['json', 'xml'] = 'xml', allow_networking: bool = True, ) -> list[str]: args = ["run"] From 43d10c4e68517acb1cd3add49748c4790affc2a9 Mon Sep 17 00:00:00 2001 From: luoshasha Date: Mon, 24 Nov 2025 13:22:49 +0800 Subject: [PATCH 03/12] fix: fix code style conflicts. --- mcp_run_python/deno/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp_run_python/deno/src/main.ts b/mcp_run_python/deno/src/main.ts index 843cc1d..774d727 100644 --- a/mcp_run_python/deno/src/main.ts +++ b/mcp_run_python/deno/src/main.ts @@ -177,7 +177,7 @@ function httpSetJsonResponse(res: http.ServerResponse, status: number, text: str function runStreamableHttp(port: number, host:string, deps: string[], returnMode: string, stateless: boolean): void { const server = (stateless ? createStatelessHttpServer : createStatefulHttpServer)(deps, returnMode) server.listen(port, host, () => { - console.log(`Listening on port ${port}`) + console.log(`Listening on host ${host} port ${port}`) }) } From fef1e65eed619b8c231380a8c4ed94326643d3ce Mon Sep 17 00:00:00 2001 From: luoshasha Date: Thu, 20 Nov 2025 11:00:40 +0800 Subject: [PATCH 04/12] feat: add [--host] parameter for streamable-http mode. --- mcp_run_python/deno/src/main.ts | 298 ++++++++++++++++++-------------- mcp_run_python/main.py | 14 +- 2 files changed, 174 insertions(+), 138 deletions(-) diff --git a/mcp_run_python/deno/src/main.ts b/mcp_run_python/deno/src/main.ts index 774d727..0558867 100644 --- a/mcp_run_python/deno/src/main.ts +++ b/mcp_run_python/deno/src/main.ts @@ -1,33 +1,36 @@ // deno-lint-ignore-file no-explicit-any /// -import './polyfill.ts' -import http from 'node:http' -import { randomUUID } from 'node:crypto' -import { parseArgs } from '@std/cli/parse-args' -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' -import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js' -import { type LoggingLevel, SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js' -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' -import { z } from 'zod' - -import { asJson, asXml, RunCode } from './runCode.ts' -import { Buffer } from 'node:buffer' - -const VERSION = '0.0.13' +import './polyfill.ts'; +import http from 'node:http'; +import { randomUUID } from 'node:crypto'; +import { parseArgs } from '@std/cli/parse-args'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; +import { + type LoggingLevel, + SetLevelRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; + +import { asJson, asXml, RunCode } from './runCode.ts'; +import { Buffer } from 'node:buffer'; + +const VERSION = '0.0.13'; export async function main() { - const { args } = Deno + const { args } = Deno; const flags = parseArgs(Deno.args, { - string: ['deps', 'return-mode', 'port'], - default: { port: '3001', 'return-mode': 'xml' }, - }) - const deps = flags.deps?.split(',') ?? [] + string: ['deps', 'return-mode', 'port', 'host'], + default: { port: '3001', 'return-mode': 'xml', host: '127.0.0.1' }, + }); + const deps = flags.deps?.split(',') ?? []; if (args.length >= 1) { if (args[0] === 'stdio') { - await runStdio(deps, flags['return-mode']) - return + await runStdio(deps, flags['return-mode']); + return; } else if (args[0] === 'streamable_http') { const port = parseInt(flags.port) const host = flags.host @@ -39,11 +42,11 @@ export async function main() { runStreamableHttp(port, host, deps, flags['return-mode'], true) return } else if (args[0] === 'example') { - await example(deps) - return + await example(deps); + return; } else if (args[0] === 'noop') { - await installDeps(deps) - return + await installDeps(deps); + return; } } console.error( @@ -56,42 +59,43 @@ options: --port Port to run the HTTP server on (default: 3001) --host Host to run the HTTP server on (default: 127.0.0.1) --deps Comma separated list of dependencies to install ---return-mode Return mode for output data (default: xml)`, - ) - Deno.exit(1) +--return-mode 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 { - const runCode = new RunCode() + const runCode = new RunCode(); const server = new McpServer( { name: 'MCP Run Python', version: VERSION, }, { - instructions: 'Call the "run_python_code" tool with the Python code to run.', + instructions: + 'Call the "run_python_code" tool with the Python code to run.', capabilities: { logging: {}, }, - }, - ) + } + ); const toolDescription = `Tool to execute Python code and return stdout, stderr, and return value. The code may be async, and the value on the last line will be returned as the return value. The code will be executed with Python 3.13. -` +`; - let setLogLevel: LoggingLevel = 'emergency' + let setLogLevel: LoggingLevel = 'emergency'; - server.server.setRequestHandler(SetLevelRequestSchema, (request) => { - setLogLevel = request.params.level - return {} - }) + server.server.setRequestHandler(SetLevelRequestSchema, request => { + setLogLevel = request.params.level; + return {}; + }); server.registerTool( 'run_python_code', @@ -100,75 +104,99 @@ The code will be executed with Python 3.13. description: toolDescription, inputSchema: { python_code: z.string().describe('Python code to run'), - global_variables: z.record(z.string(), z.any()).default({}).describe( - 'Map of global variables in context when the code is executed', - ), + global_variables: z + .record(z.string(), z.any()) + .default({}) + .describe( + 'Map of global variables in context when the code is executed' + ), }, }, - async ({ python_code, global_variables }: { python_code: string; global_variables: Record }) => { - const logPromises: Promise[] = [] + async ({ + python_code, + global_variables, + }: { + python_code: string; + global_variables: Record; + }) => { + const logPromises: Promise[] = []; const result = await runCode.run( deps, (level, data) => { if (LogLevels.indexOf(level) >= LogLevels.indexOf(setLogLevel)) { - logPromises.push(server.server.sendLoggingMessage({ level, data })) + logPromises.push(server.server.sendLoggingMessage({ level, data })); } }, { name: 'main.py', content: python_code }, global_variables, - returnMode !== 'xml', - ) - await Promise.all(logPromises) + returnMode !== 'xml' + ); + await Promise.all(logPromises); return { - content: [{ type: 'text', text: returnMode === 'xml' ? asXml(result) : asJson(result) }], - } - }, - ) - return server + content: [ + { + type: 'text', + text: returnMode === 'xml' ? asXml(result) : asJson(result), + }, + ], + }; + } + ); + return server; } /* * Define some QOL functions for both the Streamable HTTP server implementation */ function httpGetUrl(req: http.IncomingMessage): URL { - return new URL( - req.url ?? '', - `http://${req.headers.host ?? 'unknown'}`, - ) + return new URL(req.url ?? '', `http://${req.headers.host ?? 'unknown'}`); } function httpGetBody(req: http.IncomingMessage): Promise { // https://nodejs.org/en/learn/modules/anatomy-of-an-http-transaction#request-body - return new Promise((resolve) => { - const bodyParts: any[] = [] - let body - req.on('data', (chunk) => { - bodyParts.push(chunk) - }).on('end', () => { - body = Buffer.concat(bodyParts).toString() - resolve(JSON.parse(body)) - }) - }) + return new Promise(resolve => { + const bodyParts: any[] = []; + let body; + req + .on('data', chunk => { + bodyParts.push(chunk); + }) + .on('end', () => { + body = Buffer.concat(bodyParts).toString(); + resolve(JSON.parse(body)); + }); + }); } -function httpSetTextResponse(res: http.ServerResponse, status: number, text: string) { - res.setHeader('Content-Type', 'text/plain') - res.statusCode = status - res.end(`${text}\n`) +function httpSetTextResponse( + res: http.ServerResponse, + status: number, + text: string +) { + res.setHeader('Content-Type', 'text/plain'); + res.statusCode = status; + res.end(`${text}\n`); } -function httpSetJsonResponse(res: http.ServerResponse, status: number, text: string, code: number) { - res.setHeader('Content-Type', 'application/json') - res.statusCode = status - res.write(JSON.stringify({ - jsonrpc: '2.0', - error: { - code: code, - message: text, - }, - id: null, - })) - res.end() +function httpSetJsonResponse( + res: http.ServerResponse, + status: number, + text: string, + code: number +) { + res.setHeader('Content-Type', 'application/json'); + res.statusCode = status; + res.write( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: code, + message: text, + }, + id: null, + }) + ); + res.end(); } /* @@ -217,103 +245,107 @@ function createStatelessHttpServer(deps: string[], returnMode: string): http.Ser function createStatefulHttpServer(deps: 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 transports: { [sessionId: string]: StreamableHTTPServerTransport } = {} + const mcpServer = createServer(deps, returnMode); + const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; return http.createServer(async (req, res) => { - const url = httpGetUrl(req) - let pathMatch = false + const url = httpGetUrl(req); + let pathMatch = false; function match(method: string, path: string): boolean { if (url.pathname === path) { - pathMatch = true - return req.method === method + pathMatch = true; + return req.method === method; } - return false + return false; } // Reusable handler for GET and DELETE requests async function handleSessionRequest() { - const sessionId = req.headers['mcp-session-id'] as string | undefined + const sessionId = req.headers['mcp-session-id'] as string | undefined; if (!sessionId || !transports[sessionId]) { - httpSetTextResponse(res, 400, 'Invalid or missing session ID') - return + httpSetTextResponse(res, 400, 'Invalid or missing session ID'); + return; } - const transport = transports[sessionId] - await transport.handleRequest(req, res) + const transport = transports[sessionId]; + await transport.handleRequest(req, res); } // Handle different request methods and paths if (match('POST', '/mcp')) { // Check for existing session ID - const sessionId = req.headers['mcp-session-id'] as string | undefined - let transport: StreamableHTTPServerTransport + const sessionId = req.headers['mcp-session-id'] as string | undefined; + let transport: StreamableHTTPServerTransport; - const body = await httpGetBody(req) + const body = await httpGetBody(req); if (sessionId && transports[sessionId]) { // Reuse existing transport - transport = transports[sessionId] + transport = transports[sessionId]; } else if (!sessionId && isInitializeRequest(body)) { // New initialization request transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), - onsessioninitialized: (sessionId) => { + onsessioninitialized: sessionId => { // Store the transport by session ID - transports[sessionId] = transport + transports[sessionId] = transport; }, - }) + }); // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { - delete transports[transport.sessionId] + delete transports[transport.sessionId]; } - } + }; - await mcpServer.connect(transport) + await mcpServer.connect(transport); } else { - httpSetJsonResponse(res, 400, 'Bad Request: No valid session ID provided', -32000) - return + httpSetJsonResponse( + res, + 400, + 'Bad Request: No valid session ID provided', + -32000 + ); + return; } // Handle the request - await transport.handleRequest(req, res, body) + await transport.handleRequest(req, res, body); } else if (match('GET', '/mcp')) { // Handle server-to-client notifications - await handleSessionRequest() + await handleSessionRequest(); } else if (match('DELETE', '/mcp')) { // Handle requests for session termination - await handleSessionRequest() + await handleSessionRequest(); } else if (pathMatch) { - httpSetTextResponse(res, 405, 'Method not allowed') + httpSetTextResponse(res, 405, 'Method not allowed'); } else { - httpSetTextResponse(res, 404, 'Page not found') + httpSetTextResponse(res, 404, 'Page not found'); } - }) + }); } /* * Run the MCP server using the Stdio transport. */ async function runStdio(deps: string[], returnMode: string) { - const mcpServer = createServer(deps, returnMode) - const transport = new StdioServerTransport() - await mcpServer.connect(transport) + const mcpServer = createServer(deps, returnMode); + const transport = new StdioServerTransport(); + await mcpServer.connect(transport); } /* * Run pyodide to download and install dependencies. */ async function installDeps(deps: string[]) { - const runCode = new RunCode() - const result = await runCode.run( - deps, - (level, data) => console.error(`${level}|${data}`), - ) + const runCode = new RunCode(); + const result = await runCode.run(deps, (level, data) => + console.error(`${level}|${data}`) + ); if (result.status !== 'success') { - console.error('error|Failed to install dependencies') - Deno.exit(1) + console.error('error|Failed to install dependencies'); + Deno.exit(1); } } @@ -322,25 +354,25 @@ async function installDeps(deps: string[]) { */ async function example(deps: string[]) { console.error( - `Running example script for MCP Run Python version ${VERSION}...`, - ) + `Running example script for MCP Run Python version ${VERSION}...` + ); const code = ` import numpy a = numpy.array([1, 2, 3]) print('numpy array:', a) a -` - const runCode = new RunCode() +`; + const runCode = new RunCode(); const result = await runCode.run( deps, // use warn to avoid recursion since console.log is patched in runCode (level, data) => console.warn(`${level}: ${data}`), - { name: 'example.py', content: code }, - ) - console.log('Tool return value:') - console.log(asXml(result)) + { name: 'example.py', content: code } + ); + console.log('Tool return value:'); + console.log(asXml(result)); if (result.status !== 'success') { - Deno.exit(1) + Deno.exit(1); } } @@ -354,6 +386,6 @@ const LogLevels: LoggingLevel[] = [ 'critical', 'alert', 'emergency', -] +]; await main() \ No newline at end of file diff --git a/mcp_run_python/main.py b/mcp_run_python/main.py index e79d427..405f1ce 100644 --- a/mcp_run_python/main.py +++ b/mcp_run_python/main.py @@ -85,7 +85,7 @@ def prepare_deno_env( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal['json', 'xml'] = 'xml', + return_mode: Literal["json", "xml"] = "xml", deps_log_handler: LogHandler | None = None, allow_networking: bool = True, ) -> Iterator[DenoEnv]: @@ -128,7 +128,9 @@ def prepare_deno_env( stdout.append(line) p.wait() if p.returncode != 0: - raise RuntimeError(f'`deno run ...` returned a non-zero exit code {p.returncode}: {"".join(stdout)}') + raise RuntimeError( + f'`deno run ...` returned a non-zero exit code {p.returncode}: {"".join(stdout)}' + ) args = _deno_run_args( mode, @@ -151,7 +153,7 @@ async def async_prepare_deno_env( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal['json', 'xml'] = 'xml', + return_mode: Literal["json", "xml"] = "xml", deps_log_handler: LogHandler | None = None, allow_networking: bool = True, ) -> AsyncIterator[DenoEnv]: @@ -193,7 +195,7 @@ def _deno_run_args( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal['json', 'xml'] = 'xml', + return_mode: Literal["json", "xml"] = "xml", allow_networking: bool = True, ) -> list[str]: args = ["run"] @@ -223,4 +225,6 @@ def _deno_run_args( async def _asyncify(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: - return await asyncio.get_event_loop().run_in_executor(None, partial(func, *args, **kwargs)) + return await asyncio.get_event_loop().run_in_executor( + None, partial(func, *args, **kwargs) + ) From 6f69caf5f50bfea85307b787a9dbb091abe52c78 Mon Sep 17 00:00:00 2001 From: luoshasha Date: Thu, 20 Nov 2025 11:20:23 +0800 Subject: [PATCH 05/12] fix: add host to cli error info. --- mcp_run_python/main.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mcp_run_python/main.py b/mcp_run_python/main.py index 405f1ce..d40c909 100644 --- a/mcp_run_python/main.py +++ b/mcp_run_python/main.py @@ -85,7 +85,7 @@ def prepare_deno_env( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal["json", "xml"] = "xml", + return_mode: Literal['json', 'xml'] = 'xml', deps_log_handler: LogHandler | None = None, allow_networking: bool = True, ) -> Iterator[DenoEnv]: @@ -153,7 +153,7 @@ async def async_prepare_deno_env( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal["json", "xml"] = "xml", + return_mode: Literal['json', 'xml'] = 'xml', deps_log_handler: LogHandler | None = None, allow_networking: bool = True, ) -> AsyncIterator[DenoEnv]: @@ -195,7 +195,7 @@ def _deno_run_args( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal["json", "xml"] = "xml", + return_mode: Literal['json', 'xml'] = 'xml', allow_networking: bool = True, ) -> list[str]: args = ["run"] @@ -225,6 +225,4 @@ def _deno_run_args( async def _asyncify(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: - return await asyncio.get_event_loop().run_in_executor( - None, partial(func, *args, **kwargs) - ) + return await asyncio.get_event_loop().run_in_executor(None, partial(func, *args, **kwargs)) From cca87ed032ae8f2708e84b2eb8d3769d39523970 Mon Sep 17 00:00:00 2001 From: luoshasha Date: Wed, 26 Nov 2025 10:40:51 +0800 Subject: [PATCH 06/12] fix: remove cli default value to make test happy. --- mcp_run_python/_cli.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/mcp_run_python/_cli.py b/mcp_run_python/_cli.py index 7adc87b..da1c40d 100644 --- a/mcp_run_python/_cli.py +++ b/mcp_run_python/_cli.py @@ -20,13 +20,10 @@ def cli_logic(args_list: Sequence[str] | None = None) -> int: description=f'mcp-run-python CLI v{__version__}\n\nMCP server for running untrusted Python code.\n', formatter_class=argparse.RawTextHelpFormatter, ) - parser.add_argument('--host', type=str, default='127.0.0.1', - help='Host to bind the HTTP server to (default: 127.0.0.1). Use 0.0.0.0 to bind to all interfaces, e.g. when using Docker.') + parser.add_argument('--host', type=str, help='Host to bind the HTTP server to (default: 127.0.0.1). Use 0.0.0.0 to bind to all interfaces, e.g. when using Docker.') 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( - '--disable-networking', action='store_true', help='Disable networking during execution of python code' - ) + parser.add_argument( '--disable-networking', action='store_true', help='Disable networking during execution of python code' ) parser.add_argument('--verbose', action='store_true', help='Enable verbose logging') parser.add_argument('--version', action='store_true', help='Show version and exit') parser.add_argument( From 01463a237013c13ac5b59e0c95205fd94b7886a5 Mon Sep 17 00:00:00 2001 From: luoshasha Date: Fri, 28 Nov 2025 11:16:21 +0800 Subject: [PATCH 07/12] style: modify code style to mark lint happy. --- mcp_run_python/_cli.py | 10 +- mcp_run_python/deno/src/main.ts | 296 ++++++++++++++------------------ mcp_run_python/main.py | 68 ++++---- 3 files changed, 172 insertions(+), 202 deletions(-) diff --git a/mcp_run_python/_cli.py b/mcp_run_python/_cli.py index da1c40d..1c91624 100644 --- a/mcp_run_python/_cli.py +++ b/mcp_run_python/_cli.py @@ -20,10 +20,16 @@ def cli_logic(args_list: Sequence[str] | None = None) -> int: description=f'mcp-run-python CLI v{__version__}\n\nMCP server for running untrusted Python code.\n', formatter_class=argparse.RawTextHelpFormatter, ) - parser.add_argument('--host', type=str, help='Host to bind the HTTP server to (default: 127.0.0.1). Use 0.0.0.0 to bind to all interfaces, e.g. when using Docker.') + parser.add_argument( + '--host', + type=str, + help='Host to bind the HTTP server to (default: 127.0.0.1). Use 0.0.0.0 to bind to all interfaces, e.g. when using Docker.', + ) 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( '--disable-networking', action='store_true', help='Disable networking during execution of python code' ) + parser.add_argument( + '--disable-networking', action='store_true', help='Disable networking during execution of python code' + ) parser.add_argument('--verbose', action='store_true', help='Enable verbose logging') parser.add_argument('--version', action='store_true', help='Show version and exit') parser.add_argument( diff --git a/mcp_run_python/deno/src/main.ts b/mcp_run_python/deno/src/main.ts index 0558867..2a1caca 100644 --- a/mcp_run_python/deno/src/main.ts +++ b/mcp_run_python/deno/src/main.ts @@ -1,36 +1,33 @@ // deno-lint-ignore-file no-explicit-any /// -import './polyfill.ts'; -import http from 'node:http'; -import { randomUUID } from 'node:crypto'; -import { parseArgs } from '@std/cli/parse-args'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; -import { - type LoggingLevel, - SetLevelRequestSchema, -} from '@modelcontextprotocol/sdk/types.js'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { z } from 'zod'; - -import { asJson, asXml, RunCode } from './runCode.ts'; -import { Buffer } from 'node:buffer'; - -const VERSION = '0.0.13'; +import './polyfill.ts' +import http from 'node:http' +import { randomUUID } from 'node:crypto' +import { parseArgs } from '@std/cli/parse-args' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' +import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js' +import { type LoggingLevel, SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { z } from 'zod' + +import { asJson, asXml, RunCode } from './runCode.ts' +import { Buffer } from 'node:buffer' + +const VERSION = '0.0.13' export async function main() { - const { args } = Deno; + const { args } = Deno const flags = parseArgs(Deno.args, { string: ['deps', 'return-mode', 'port', 'host'], default: { port: '3001', 'return-mode': 'xml', host: '127.0.0.1' }, - }); - const deps = flags.deps?.split(',') ?? []; + }) + const deps = flags.deps?.split(',') ?? [] if (args.length >= 1) { if (args[0] === 'stdio') { - await runStdio(deps, flags['return-mode']); - return; + await runStdio(deps, flags['return-mode']) + return } else if (args[0] === 'streamable_http') { const port = parseInt(flags.port) const host = flags.host @@ -42,11 +39,11 @@ export async function main() { runStreamableHttp(port, host, deps, flags['return-mode'], true) return } else if (args[0] === 'example') { - await example(deps); - return; + await example(deps) + return } else if (args[0] === 'noop') { - await installDeps(deps); - return; + await installDeps(deps) + return } } console.error( @@ -59,43 +56,42 @@ options: --port Port to run the HTTP server on (default: 3001) --host Host to run the HTTP server on (default: 127.0.0.1) --deps Comma separated list of dependencies to install ---return-mode Return mode for output data (default: xml)` - ); - Deno.exit(1); +--return-mode 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 { - const runCode = new RunCode(); + const runCode = new RunCode() const server = new McpServer( { name: 'MCP Run Python', version: VERSION, }, { - instructions: - 'Call the "run_python_code" tool with the Python code to run.', + instructions: 'Call the "run_python_code" tool with the Python code to run.', capabilities: { logging: {}, }, - } - ); + }, + ) const toolDescription = `Tool to execute Python code and return stdout, stderr, and return value. The code may be async, and the value on the last line will be returned as the return value. The code will be executed with Python 3.13. -`; +` - let setLogLevel: LoggingLevel = 'emergency'; + let setLogLevel: LoggingLevel = 'emergency' - server.server.setRequestHandler(SetLevelRequestSchema, request => { - setLogLevel = request.params.level; - return {}; - }); + server.server.setRequestHandler(SetLevelRequestSchema, (request) => { + setLogLevel = request.params.level + return {} + }) server.registerTool( 'run_python_code', @@ -104,99 +100,75 @@ The code will be executed with Python 3.13. description: toolDescription, inputSchema: { python_code: z.string().describe('Python code to run'), - global_variables: z - .record(z.string(), z.any()) - .default({}) - .describe( - 'Map of global variables in context when the code is executed' - ), + global_variables: z.record(z.string(), z.any()).default({}).describe( + 'Map of global variables in context when the code is executed', + ), }, }, - async ({ - python_code, - global_variables, - }: { - python_code: string; - global_variables: Record; - }) => { - const logPromises: Promise[] = []; + async ({ python_code, global_variables }: { python_code: string; global_variables: Record }) => { + const logPromises: Promise[] = [] const result = await runCode.run( deps, (level, data) => { if (LogLevels.indexOf(level) >= LogLevels.indexOf(setLogLevel)) { - logPromises.push(server.server.sendLoggingMessage({ level, data })); + logPromises.push(server.server.sendLoggingMessage({ level, data })) } }, { name: 'main.py', content: python_code }, global_variables, - returnMode !== 'xml' - ); - await Promise.all(logPromises); + returnMode !== 'xml', + ) + await Promise.all(logPromises) return { - content: [ - { - type: 'text', - text: returnMode === 'xml' ? asXml(result) : asJson(result), - }, - ], - }; - } - ); - return server; + content: [{ type: 'text', text: returnMode === 'xml' ? asXml(result) : asJson(result) }], + } + }, + ) + return server } /* * Define some QOL functions for both the Streamable HTTP server implementation */ function httpGetUrl(req: http.IncomingMessage): URL { - return new URL(req.url ?? '', `http://${req.headers.host ?? 'unknown'}`); + return new URL( + req.url ?? '', + `http://${req.headers.host ?? 'unknown'}`, + ) } function httpGetBody(req: http.IncomingMessage): Promise { // https://nodejs.org/en/learn/modules/anatomy-of-an-http-transaction#request-body - return new Promise(resolve => { - const bodyParts: any[] = []; - let body; - req - .on('data', chunk => { - bodyParts.push(chunk); - }) - .on('end', () => { - body = Buffer.concat(bodyParts).toString(); - resolve(JSON.parse(body)); - }); - }); + return new Promise((resolve) => { + const bodyParts: any[] = [] + let body + req.on('data', (chunk) => { + bodyParts.push(chunk) + }).on('end', () => { + body = Buffer.concat(bodyParts).toString() + resolve(JSON.parse(body)) + }) + }) } -function httpSetTextResponse( - res: http.ServerResponse, - status: number, - text: string -) { - res.setHeader('Content-Type', 'text/plain'); - res.statusCode = status; - res.end(`${text}\n`); +function httpSetTextResponse(res: http.ServerResponse, status: number, text: string) { + res.setHeader('Content-Type', 'text/plain') + res.statusCode = status + res.end(`${text}\n`) } -function httpSetJsonResponse( - res: http.ServerResponse, - status: number, - text: string, - code: number -) { - res.setHeader('Content-Type', 'application/json'); - res.statusCode = status; - res.write( - JSON.stringify({ - jsonrpc: '2.0', - error: { - code: code, - message: text, - }, - id: null, - }) - ); - res.end(); +function httpSetJsonResponse(res: http.ServerResponse, status: number, text: string, code: number) { + res.setHeader('Content-Type', 'application/json') + res.statusCode = status + res.write(JSON.stringify({ + jsonrpc: '2.0', + error: { + code: code, + message: text, + }, + id: null, + })) + res.end() } /* @@ -205,7 +177,7 @@ function httpSetJsonResponse( function runStreamableHttp(port: number, host:string, deps: string[], returnMode: string, stateless: boolean): void { const server = (stateless ? createStatelessHttpServer : createStatefulHttpServer)(deps, returnMode) server.listen(port, host, () => { - console.log(`Listening on host ${host} port ${port}`) + console.log(`Listening on port ${port}`) }) } @@ -245,107 +217,103 @@ function createStatelessHttpServer(deps: string[], returnMode: string): http.Ser function createStatefulHttpServer(deps: 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 transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; + const mcpServer = createServer(deps, returnMode) + const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {} return http.createServer(async (req, res) => { - const url = httpGetUrl(req); - let pathMatch = false; + const url = httpGetUrl(req) + let pathMatch = false function match(method: string, path: string): boolean { if (url.pathname === path) { - pathMatch = true; - return req.method === method; + pathMatch = true + return req.method === method } - return false; + return false } // Reusable handler for GET and DELETE requests async function handleSessionRequest() { - const sessionId = req.headers['mcp-session-id'] as string | undefined; + const sessionId = req.headers['mcp-session-id'] as string | undefined if (!sessionId || !transports[sessionId]) { - httpSetTextResponse(res, 400, 'Invalid or missing session ID'); - return; + httpSetTextResponse(res, 400, 'Invalid or missing session ID') + return } - const transport = transports[sessionId]; - await transport.handleRequest(req, res); + const transport = transports[sessionId] + await transport.handleRequest(req, res) } // Handle different request methods and paths if (match('POST', '/mcp')) { // Check for existing session ID - const sessionId = req.headers['mcp-session-id'] as string | undefined; - let transport: StreamableHTTPServerTransport; + const sessionId = req.headers['mcp-session-id'] as string | undefined + let transport: StreamableHTTPServerTransport - const body = await httpGetBody(req); + const body = await httpGetBody(req) if (sessionId && transports[sessionId]) { // Reuse existing transport - transport = transports[sessionId]; + transport = transports[sessionId] } else if (!sessionId && isInitializeRequest(body)) { // New initialization request transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), - onsessioninitialized: sessionId => { + onsessioninitialized: (sessionId) => { // Store the transport by session ID - transports[sessionId] = transport; + transports[sessionId] = transport }, - }); + }) // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { - delete transports[transport.sessionId]; + delete transports[transport.sessionId] } - }; + } - await mcpServer.connect(transport); + await mcpServer.connect(transport) } else { - httpSetJsonResponse( - res, - 400, - 'Bad Request: No valid session ID provided', - -32000 - ); - return; + httpSetJsonResponse(res, 400, 'Bad Request: No valid session ID provided', -32000) + return } // Handle the request - await transport.handleRequest(req, res, body); + await transport.handleRequest(req, res, body) } else if (match('GET', '/mcp')) { // Handle server-to-client notifications - await handleSessionRequest(); + await handleSessionRequest() } else if (match('DELETE', '/mcp')) { // Handle requests for session termination - await handleSessionRequest(); + await handleSessionRequest() } else if (pathMatch) { - httpSetTextResponse(res, 405, 'Method not allowed'); + httpSetTextResponse(res, 405, 'Method not allowed') } else { - httpSetTextResponse(res, 404, 'Page not found'); + httpSetTextResponse(res, 404, 'Page not found') } - }); + }) } /* * Run the MCP server using the Stdio transport. */ async function runStdio(deps: string[], returnMode: string) { - const mcpServer = createServer(deps, returnMode); - const transport = new StdioServerTransport(); - await mcpServer.connect(transport); + const mcpServer = createServer(deps, returnMode) + const transport = new StdioServerTransport() + await mcpServer.connect(transport) } /* * Run pyodide to download and install dependencies. */ async function installDeps(deps: string[]) { - const runCode = new RunCode(); - const result = await runCode.run(deps, (level, data) => - console.error(`${level}|${data}`) - ); + const runCode = new RunCode() + const result = await runCode.run( + deps, + (level, data) => console.error(`${level}|${data}`), + ) if (result.status !== 'success') { - console.error('error|Failed to install dependencies'); - Deno.exit(1); + console.error('error|Failed to install dependencies') + Deno.exit(1) } } @@ -354,25 +322,25 @@ async function installDeps(deps: string[]) { */ async function example(deps: string[]) { console.error( - `Running example script for MCP Run Python version ${VERSION}...` - ); + `Running example script for MCP Run Python version ${VERSION}...`, + ) const code = ` import numpy a = numpy.array([1, 2, 3]) print('numpy array:', a) a -`; - const runCode = new RunCode(); +` + const runCode = new RunCode() const result = await runCode.run( deps, // use warn to avoid recursion since console.log is patched in runCode (level, data) => console.warn(`${level}: ${data}`), - { name: 'example.py', content: code } - ); - console.log('Tool return value:'); - console.log(asXml(result)); + { name: 'example.py', content: code }, + ) + console.log('Tool return value:') + console.log(asXml(result)) if (result.status !== 'success') { - Deno.exit(1); + Deno.exit(1) } } @@ -386,6 +354,6 @@ const LogLevels: LoggingLevel[] = [ 'critical', 'alert', 'emergency', -]; +] await main() \ No newline at end of file diff --git a/mcp_run_python/main.py b/mcp_run_python/main.py index d40c909..339b7f4 100644 --- a/mcp_run_python/main.py +++ b/mcp_run_python/main.py @@ -11,7 +11,7 @@ from pathlib import Path from typing import Literal, ParamSpec, TypeVar, cast -__all__ = "run_mcp_server", "DenoEnv", "prepare_deno_env", "async_prepare_deno_env" +__all__ = 'run_mcp_server', 'DenoEnv', 'prepare_deno_env', 'async_prepare_deno_env' logger = logging.getLogger(__name__) LoggingLevel = Literal['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'] @@ -25,7 +25,7 @@ def run_mcp_server( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal["json", "xml"] = "xml", + return_mode: Literal['json', 'xml'] = 'xml', deps_log_handler: LogHandler | None = None, allow_networking: bool = True, verbose: bool = False, @@ -59,14 +59,12 @@ def run_mcp_server( if mode in ('streamable_http', 'streamable_http_stateless'): logger.info('Running mcp-run-python via %s on port %d...', mode, http_port) else: - logger.info("Running mcp-run-python via %s...", mode) - + logger.info('Running mcp-run-python via %s...', mode) + try: - p = subprocess.run( - ("deno", *env.args), cwd=env.cwd, stdout=stdout, stderr=stderr - ) + p = subprocess.run(('deno', *env.args), cwd=env.cwd, stdout=stdout, stderr=stderr) except KeyboardInterrupt: # pragma: no cover - logger.warning("Server stopped.") + logger.warning('Server stopped.') return 0 else: return p.returncode @@ -108,29 +106,27 @@ def prepare_deno_env( Returns: Yields the deno environment details. """ - cwd = Path(tempfile.mkdtemp()) / "mcp-run-python" + cwd = Path(tempfile.mkdtemp()) / 'mcp-run-python' try: - src = Path(__file__).parent / "deno" - logger.debug("Copying from %s to %s...", src, cwd) + src = Path(__file__).parent / 'deno' + logger.debug('Copying from %s to %s...', src, cwd) shutil.copytree(src, cwd) - logger.info("Installing dependencies %s...", dependencies) + logger.info('Installing dependencies %s...', dependencies) - args = "deno", *_deno_install_args(dependencies) + args = 'deno', *_deno_install_args(dependencies) p = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) stdout: list[str] = [] if p.stdout is not None: for line in p.stdout: line = line.strip() if deps_log_handler: - parts = line.split("|", 1) - level, msg = parts if len(parts) == 2 else ("info", line) + parts = line.split('|', 1) + level, msg = parts if len(parts) == 2 else ('info', line) deps_log_handler(cast(LoggingLevel, level), msg) stdout.append(line) p.wait() if p.returncode != 0: - raise RuntimeError( - f'`deno run ...` returned a non-zero exit code {p.returncode}: {"".join(stdout)}' - ) + raise RuntimeError(f'`deno run ...` returned a non-zero exit code {p.returncode}: {"".join(stdout)}') args = _deno_run_args( mode, @@ -176,13 +172,13 @@ async def async_prepare_deno_env( def _deno_install_args(dependencies: list[str] | None = None) -> list[str]: args = [ - "run", - "--allow-net", - "--allow-read=./node_modules", - "--allow-write=./node_modules", - "--node-modules-dir=auto", - "src/main.ts", - "noop", + 'run', + '--allow-net', + '--allow-read=./node_modules', + '--allow-write=./node_modules', + '--node-modules-dir=auto', + 'src/main.ts', + 'noop', ] if dependencies is not None: args.append(f'--deps={",".join(dependencies)}') @@ -198,30 +194,30 @@ def _deno_run_args( return_mode: Literal['json', 'xml'] = 'xml', allow_networking: bool = True, ) -> list[str]: - args = ["run"] + args = ['run'] if allow_networking: - args += ["--allow-net"] + args += ['--allow-net'] args += [ - "--allow-read=./node_modules", - "--node-modules-dir=auto", - "src/main.ts", + '--allow-read=./node_modules', + '--node-modules-dir=auto', + 'src/main.ts', mode, - f"--return-mode={return_mode}", + f'--return-mode={return_mode}', ] if dependencies is not None: args.append(f'--deps={",".join(dependencies)}') if mode in ('streamable_http', 'streamable_http_stateless'): if http_port is not None: - args.append(f"--port={http_port}") + args.append(f'--port={http_port}') if http_host is not None: - args.append(f"--host={http_host}") + args.append(f'--host={http_host}') elif http_port is not None or http_host is not None: - raise ValueError("Port and host are only supported for `streamable_http` mode") + raise ValueError('Port and host are only supported for `streamable_http` mode') return args -P = ParamSpec("P") -T = TypeVar("T") +P = ParamSpec('P') +T = TypeVar('T') async def _asyncify(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: From 88425447028a84f5f2042aaea0699e2f3c951543 Mon Sep 17 00:00:00 2001 From: luoshasha Date: Thu, 20 Nov 2025 11:00:40 +0800 Subject: [PATCH 08/12] feat: add [--host] parameter for streamable-http mode. --- mcp_run_python/deno/src/main.ts | 290 ++++++++++++++++++-------------- mcp_run_python/main.py | 74 ++++---- 2 files changed, 202 insertions(+), 162 deletions(-) diff --git a/mcp_run_python/deno/src/main.ts b/mcp_run_python/deno/src/main.ts index 2a1caca..8d9eaea 100644 --- a/mcp_run_python/deno/src/main.ts +++ b/mcp_run_python/deno/src/main.ts @@ -1,24 +1,27 @@ // deno-lint-ignore-file no-explicit-any /// -import './polyfill.ts' -import http from 'node:http' -import { randomUUID } from 'node:crypto' -import { parseArgs } from '@std/cli/parse-args' -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' -import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js' -import { type LoggingLevel, SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js' -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' -import { z } from 'zod' - -import { asJson, asXml, RunCode } from './runCode.ts' -import { Buffer } from 'node:buffer' - -const VERSION = '0.0.13' +import './polyfill.ts'; +import http from 'node:http'; +import { randomUUID } from 'node:crypto'; +import { parseArgs } from '@std/cli/parse-args'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; +import { + type LoggingLevel, + SetLevelRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; + +import { asJson, asXml, RunCode } from './runCode.ts'; +import { Buffer } from 'node:buffer'; + +const VERSION = '0.0.13'; export async function main() { - const { args } = Deno + const { args } = Deno; const flags = parseArgs(Deno.args, { string: ['deps', 'return-mode', 'port', 'host'], default: { port: '3001', 'return-mode': 'xml', host: '127.0.0.1' }, @@ -26,8 +29,8 @@ export async function main() { const deps = flags.deps?.split(',') ?? [] if (args.length >= 1) { if (args[0] === 'stdio') { - await runStdio(deps, flags['return-mode']) - return + await runStdio(deps, flags['return-mode']); + return; } else if (args[0] === 'streamable_http') { const port = parseInt(flags.port) const host = flags.host @@ -39,11 +42,11 @@ export async function main() { runStreamableHttp(port, host, deps, flags['return-mode'], true) return } else if (args[0] === 'example') { - await example(deps) - return + await example(deps); + return; } else if (args[0] === 'noop') { - await installDeps(deps) - return + await installDeps(deps); + return; } } console.error( @@ -56,42 +59,43 @@ options: --port Port to run the HTTP server on (default: 3001) --host Host to run the HTTP server on (default: 127.0.0.1) --deps Comma separated list of dependencies to install ---return-mode Return mode for output data (default: xml)`, - ) - Deno.exit(1) +--return-mode 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 { - const runCode = new RunCode() + const runCode = new RunCode(); const server = new McpServer( { name: 'MCP Run Python', version: VERSION, }, { - instructions: 'Call the "run_python_code" tool with the Python code to run.', + instructions: + 'Call the "run_python_code" tool with the Python code to run.', capabilities: { logging: {}, }, - }, - ) + } + ); const toolDescription = `Tool to execute Python code and return stdout, stderr, and return value. The code may be async, and the value on the last line will be returned as the return value. The code will be executed with Python 3.13. -` +`; - let setLogLevel: LoggingLevel = 'emergency' + let setLogLevel: LoggingLevel = 'emergency'; - server.server.setRequestHandler(SetLevelRequestSchema, (request) => { - setLogLevel = request.params.level - return {} - }) + server.server.setRequestHandler(SetLevelRequestSchema, request => { + setLogLevel = request.params.level; + return {}; + }); server.registerTool( 'run_python_code', @@ -100,75 +104,99 @@ The code will be executed with Python 3.13. description: toolDescription, inputSchema: { python_code: z.string().describe('Python code to run'), - global_variables: z.record(z.string(), z.any()).default({}).describe( - 'Map of global variables in context when the code is executed', - ), + global_variables: z + .record(z.string(), z.any()) + .default({}) + .describe( + 'Map of global variables in context when the code is executed' + ), }, }, - async ({ python_code, global_variables }: { python_code: string; global_variables: Record }) => { - const logPromises: Promise[] = [] + async ({ + python_code, + global_variables, + }: { + python_code: string; + global_variables: Record; + }) => { + const logPromises: Promise[] = []; const result = await runCode.run( deps, (level, data) => { if (LogLevels.indexOf(level) >= LogLevels.indexOf(setLogLevel)) { - logPromises.push(server.server.sendLoggingMessage({ level, data })) + logPromises.push(server.server.sendLoggingMessage({ level, data })); } }, { name: 'main.py', content: python_code }, global_variables, - returnMode !== 'xml', - ) - await Promise.all(logPromises) + returnMode !== 'xml' + ); + await Promise.all(logPromises); return { - content: [{ type: 'text', text: returnMode === 'xml' ? asXml(result) : asJson(result) }], - } - }, - ) - return server + content: [ + { + type: 'text', + text: returnMode === 'xml' ? asXml(result) : asJson(result), + }, + ], + }; + } + ); + return server; } /* * Define some QOL functions for both the Streamable HTTP server implementation */ function httpGetUrl(req: http.IncomingMessage): URL { - return new URL( - req.url ?? '', - `http://${req.headers.host ?? 'unknown'}`, - ) + return new URL(req.url ?? '', `http://${req.headers.host ?? 'unknown'}`); } function httpGetBody(req: http.IncomingMessage): Promise { // https://nodejs.org/en/learn/modules/anatomy-of-an-http-transaction#request-body - return new Promise((resolve) => { - const bodyParts: any[] = [] - let body - req.on('data', (chunk) => { - bodyParts.push(chunk) - }).on('end', () => { - body = Buffer.concat(bodyParts).toString() - resolve(JSON.parse(body)) - }) - }) + return new Promise(resolve => { + const bodyParts: any[] = []; + let body; + req + .on('data', chunk => { + bodyParts.push(chunk); + }) + .on('end', () => { + body = Buffer.concat(bodyParts).toString(); + resolve(JSON.parse(body)); + }); + }); } -function httpSetTextResponse(res: http.ServerResponse, status: number, text: string) { - res.setHeader('Content-Type', 'text/plain') - res.statusCode = status - res.end(`${text}\n`) +function httpSetTextResponse( + res: http.ServerResponse, + status: number, + text: string +) { + res.setHeader('Content-Type', 'text/plain'); + res.statusCode = status; + res.end(`${text}\n`); } -function httpSetJsonResponse(res: http.ServerResponse, status: number, text: string, code: number) { - res.setHeader('Content-Type', 'application/json') - res.statusCode = status - res.write(JSON.stringify({ - jsonrpc: '2.0', - error: { - code: code, - message: text, - }, - id: null, - })) - res.end() +function httpSetJsonResponse( + res: http.ServerResponse, + status: number, + text: string, + code: number +) { + res.setHeader('Content-Type', 'application/json'); + res.statusCode = status; + res.write( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: code, + message: text, + }, + id: null, + }) + ); + res.end(); } /* @@ -217,103 +245,107 @@ function createStatelessHttpServer(deps: string[], returnMode: string): http.Ser function createStatefulHttpServer(deps: 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 transports: { [sessionId: string]: StreamableHTTPServerTransport } = {} + const mcpServer = createServer(deps, returnMode); + const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; return http.createServer(async (req, res) => { - const url = httpGetUrl(req) - let pathMatch = false + const url = httpGetUrl(req); + let pathMatch = false; function match(method: string, path: string): boolean { if (url.pathname === path) { - pathMatch = true - return req.method === method + pathMatch = true; + return req.method === method; } - return false + return false; } // Reusable handler for GET and DELETE requests async function handleSessionRequest() { - const sessionId = req.headers['mcp-session-id'] as string | undefined + const sessionId = req.headers['mcp-session-id'] as string | undefined; if (!sessionId || !transports[sessionId]) { - httpSetTextResponse(res, 400, 'Invalid or missing session ID') - return + httpSetTextResponse(res, 400, 'Invalid or missing session ID'); + return; } - const transport = transports[sessionId] - await transport.handleRequest(req, res) + const transport = transports[sessionId]; + await transport.handleRequest(req, res); } // Handle different request methods and paths if (match('POST', '/mcp')) { // Check for existing session ID - const sessionId = req.headers['mcp-session-id'] as string | undefined - let transport: StreamableHTTPServerTransport + const sessionId = req.headers['mcp-session-id'] as string | undefined; + let transport: StreamableHTTPServerTransport; - const body = await httpGetBody(req) + const body = await httpGetBody(req); if (sessionId && transports[sessionId]) { // Reuse existing transport - transport = transports[sessionId] + transport = transports[sessionId]; } else if (!sessionId && isInitializeRequest(body)) { // New initialization request transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), - onsessioninitialized: (sessionId) => { + onsessioninitialized: sessionId => { // Store the transport by session ID - transports[sessionId] = transport + transports[sessionId] = transport; }, - }) + }); // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { - delete transports[transport.sessionId] + delete transports[transport.sessionId]; } - } + }; - await mcpServer.connect(transport) + await mcpServer.connect(transport); } else { - httpSetJsonResponse(res, 400, 'Bad Request: No valid session ID provided', -32000) - return + httpSetJsonResponse( + res, + 400, + 'Bad Request: No valid session ID provided', + -32000 + ); + return; } // Handle the request - await transport.handleRequest(req, res, body) + await transport.handleRequest(req, res, body); } else if (match('GET', '/mcp')) { // Handle server-to-client notifications - await handleSessionRequest() + await handleSessionRequest(); } else if (match('DELETE', '/mcp')) { // Handle requests for session termination - await handleSessionRequest() + await handleSessionRequest(); } else if (pathMatch) { - httpSetTextResponse(res, 405, 'Method not allowed') + httpSetTextResponse(res, 405, 'Method not allowed'); } else { - httpSetTextResponse(res, 404, 'Page not found') + httpSetTextResponse(res, 404, 'Page not found'); } - }) + }); } /* * Run the MCP server using the Stdio transport. */ async function runStdio(deps: string[], returnMode: string) { - const mcpServer = createServer(deps, returnMode) - const transport = new StdioServerTransport() - await mcpServer.connect(transport) + const mcpServer = createServer(deps, returnMode); + const transport = new StdioServerTransport(); + await mcpServer.connect(transport); } /* * Run pyodide to download and install dependencies. */ async function installDeps(deps: string[]) { - const runCode = new RunCode() - const result = await runCode.run( - deps, - (level, data) => console.error(`${level}|${data}`), - ) + const runCode = new RunCode(); + const result = await runCode.run(deps, (level, data) => + console.error(`${level}|${data}`) + ); if (result.status !== 'success') { - console.error('error|Failed to install dependencies') - Deno.exit(1) + console.error('error|Failed to install dependencies'); + Deno.exit(1); } } @@ -322,25 +354,25 @@ async function installDeps(deps: string[]) { */ async function example(deps: string[]) { console.error( - `Running example script for MCP Run Python version ${VERSION}...`, - ) + `Running example script for MCP Run Python version ${VERSION}...` + ); const code = ` import numpy a = numpy.array([1, 2, 3]) print('numpy array:', a) a -` - const runCode = new RunCode() +`; + const runCode = new RunCode(); const result = await runCode.run( deps, // use warn to avoid recursion since console.log is patched in runCode (level, data) => console.warn(`${level}: ${data}`), - { name: 'example.py', content: code }, - ) - console.log('Tool return value:') - console.log(asXml(result)) + { name: 'example.py', content: code } + ); + console.log('Tool return value:'); + console.log(asXml(result)); if (result.status !== 'success') { - Deno.exit(1) + Deno.exit(1); } } @@ -354,6 +386,6 @@ const LogLevels: LoggingLevel[] = [ 'critical', 'alert', 'emergency', -] +]; await main() \ No newline at end of file diff --git a/mcp_run_python/main.py b/mcp_run_python/main.py index 339b7f4..e36a324 100644 --- a/mcp_run_python/main.py +++ b/mcp_run_python/main.py @@ -11,7 +11,7 @@ from pathlib import Path from typing import Literal, ParamSpec, TypeVar, cast -__all__ = 'run_mcp_server', 'DenoEnv', 'prepare_deno_env', 'async_prepare_deno_env' +__all__ = "run_mcp_server", "DenoEnv", "prepare_deno_env", "async_prepare_deno_env" logger = logging.getLogger(__name__) LoggingLevel = Literal['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'] @@ -25,7 +25,7 @@ def run_mcp_server( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal['json', 'xml'] = 'xml', + return_mode: Literal["json", "xml"] = "xml", deps_log_handler: LogHandler | None = None, allow_networking: bool = True, verbose: bool = False, @@ -59,12 +59,14 @@ def run_mcp_server( if mode in ('streamable_http', 'streamable_http_stateless'): logger.info('Running mcp-run-python via %s on port %d...', mode, http_port) else: - logger.info('Running mcp-run-python via %s...', mode) + logger.info("Running mcp-run-python via %s...", mode) try: - p = subprocess.run(('deno', *env.args), cwd=env.cwd, stdout=stdout, stderr=stderr) + p = subprocess.run( + ("deno", *env.args), cwd=env.cwd, stdout=stdout, stderr=stderr + ) except KeyboardInterrupt: # pragma: no cover - logger.warning('Server stopped.') + logger.warning("Server stopped.") return 0 else: return p.returncode @@ -83,7 +85,7 @@ def prepare_deno_env( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal['json', 'xml'] = 'xml', + return_mode: Literal["json", "xml"] = "xml", deps_log_handler: LogHandler | None = None, allow_networking: bool = True, ) -> Iterator[DenoEnv]: @@ -106,27 +108,31 @@ def prepare_deno_env( Returns: Yields the deno environment details. """ - cwd = Path(tempfile.mkdtemp()) / 'mcp-run-python' + cwd = Path(tempfile.mkdtemp()) / "mcp-run-python" try: - src = Path(__file__).parent / 'deno' - logger.debug('Copying from %s to %s...', src, cwd) + src = Path(__file__).parent / "deno" + logger.debug("Copying from %s to %s...", src, cwd) shutil.copytree(src, cwd) - logger.info('Installing dependencies %s...', dependencies) + logger.info("Installing dependencies %s...", dependencies) - args = 'deno', *_deno_install_args(dependencies) - p = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + args = "deno", *_deno_install_args(dependencies) + p = subprocess.Popen( + args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True + ) stdout: list[str] = [] if p.stdout is not None: for line in p.stdout: line = line.strip() if deps_log_handler: - parts = line.split('|', 1) - level, msg = parts if len(parts) == 2 else ('info', line) + parts = line.split("|", 1) + level, msg = parts if len(parts) == 2 else ("info", line) deps_log_handler(cast(LoggingLevel, level), msg) stdout.append(line) p.wait() if p.returncode != 0: - raise RuntimeError(f'`deno run ...` returned a non-zero exit code {p.returncode}: {"".join(stdout)}') + raise RuntimeError( + f'`deno run ...` returned a non-zero exit code {p.returncode}: {"".join(stdout)}' + ) args = _deno_run_args( mode, @@ -149,7 +155,7 @@ async def async_prepare_deno_env( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal['json', 'xml'] = 'xml', + return_mode: Literal["json", "xml"] = "xml", deps_log_handler: LogHandler | None = None, allow_networking: bool = True, ) -> AsyncIterator[DenoEnv]: @@ -172,13 +178,13 @@ async def async_prepare_deno_env( def _deno_install_args(dependencies: list[str] | None = None) -> list[str]: args = [ - 'run', - '--allow-net', - '--allow-read=./node_modules', - '--allow-write=./node_modules', - '--node-modules-dir=auto', - 'src/main.ts', - 'noop', + "run", + "--allow-net", + "--allow-read=./node_modules", + "--allow-write=./node_modules", + "--node-modules-dir=auto", + "src/main.ts", + "noop", ] if dependencies is not None: args.append(f'--deps={",".join(dependencies)}') @@ -191,18 +197,18 @@ def _deno_run_args( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal['json', 'xml'] = 'xml', + return_mode: Literal["json", "xml"] = "xml", allow_networking: bool = True, ) -> list[str]: - args = ['run'] + args = ["run"] if allow_networking: - args += ['--allow-net'] + args += ["--allow-net"] args += [ - '--allow-read=./node_modules', - '--node-modules-dir=auto', - 'src/main.ts', + "--allow-read=./node_modules", + "--node-modules-dir=auto", + "src/main.ts", mode, - f'--return-mode={return_mode}', + f"--return-mode={return_mode}", ] if dependencies is not None: args.append(f'--deps={",".join(dependencies)}') @@ -216,9 +222,11 @@ def _deno_run_args( return args -P = ParamSpec('P') -T = TypeVar('T') +P = ParamSpec("P") +T = TypeVar("T") async def _asyncify(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: - return await asyncio.get_event_loop().run_in_executor(None, partial(func, *args, **kwargs)) + return await asyncio.get_event_loop().run_in_executor( + None, partial(func, *args, **kwargs) + ) From 6dc0745a6a9e6c5e9bf181a7fd264f216c11c789 Mon Sep 17 00:00:00 2001 From: luoshasha Date: Thu, 20 Nov 2025 11:20:23 +0800 Subject: [PATCH 09/12] fix: add host to cli error info. --- mcp_run_python/main.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mcp_run_python/main.py b/mcp_run_python/main.py index e36a324..24bb24f 100644 --- a/mcp_run_python/main.py +++ b/mcp_run_python/main.py @@ -85,7 +85,7 @@ def prepare_deno_env( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal["json", "xml"] = "xml", + return_mode: Literal['json', 'xml'] = 'xml', deps_log_handler: LogHandler | None = None, allow_networking: bool = True, ) -> Iterator[DenoEnv]: @@ -155,7 +155,7 @@ async def async_prepare_deno_env( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal["json", "xml"] = "xml", + return_mode: Literal['json', 'xml'] = 'xml', deps_log_handler: LogHandler | None = None, allow_networking: bool = True, ) -> AsyncIterator[DenoEnv]: @@ -197,7 +197,7 @@ def _deno_run_args( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal["json", "xml"] = "xml", + return_mode: Literal['json', 'xml'] = 'xml', allow_networking: bool = True, ) -> list[str]: args = ["run"] @@ -227,6 +227,4 @@ def _deno_run_args( async def _asyncify(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: - return await asyncio.get_event_loop().run_in_executor( - None, partial(func, *args, **kwargs) - ) + return await asyncio.get_event_loop().run_in_executor(None, partial(func, *args, **kwargs)) From 36c73d20b0d3b833e0839b343517567ef91ddd65 Mon Sep 17 00:00:00 2001 From: luoshasha Date: Wed, 26 Nov 2025 10:40:51 +0800 Subject: [PATCH 10/12] fix: remove cli default value to make test happy. --- mcp_run_python/_cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mcp_run_python/_cli.py b/mcp_run_python/_cli.py index 1c91624..80b93bb 100644 --- a/mcp_run_python/_cli.py +++ b/mcp_run_python/_cli.py @@ -27,9 +27,7 @@ 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( - '--disable-networking', action='store_true', help='Disable networking during execution of python code' - ) + parser.add_argument( '--disable-networking', action='store_true', help='Disable networking during execution of python code' ) parser.add_argument('--verbose', action='store_true', help='Enable verbose logging') parser.add_argument('--version', action='store_true', help='Show version and exit') parser.add_argument( From dc536844595e0714447086e216b8e58cb563d033 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 28 Nov 2025 14:56:01 -0600 Subject: [PATCH 11/12] Update mcp_run_python/deno/src/main.ts --- mcp_run_python/deno/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp_run_python/deno/src/main.ts b/mcp_run_python/deno/src/main.ts index 8d9eaea..d3c7016 100644 --- a/mcp_run_python/deno/src/main.ts +++ b/mcp_run_python/deno/src/main.ts @@ -388,4 +388,4 @@ const LogLevels: LoggingLevel[] = [ 'emergency', ]; -await main() \ No newline at end of file +await main() From 88e46f8a9ee8b824b760524b83f451cb2b41b99d Mon Sep 17 00:00:00 2001 From: luoshasha Date: Thu, 4 Dec 2025 11:32:47 +0800 Subject: [PATCH 12/12] style: fix code style. --- mcp_run_python/_cli.py | 4 +- mcp_run_python/deno/src/main.ts | 292 ++++++++++++++------------------ mcp_run_python/main.py | 66 ++++---- 3 files changed, 163 insertions(+), 199 deletions(-) diff --git a/mcp_run_python/_cli.py b/mcp_run_python/_cli.py index 80b93bb..1c91624 100644 --- a/mcp_run_python/_cli.py +++ b/mcp_run_python/_cli.py @@ -27,7 +27,9 @@ 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( '--disable-networking', action='store_true', help='Disable networking during execution of python code' ) + parser.add_argument( + '--disable-networking', action='store_true', help='Disable networking during execution of python code' + ) parser.add_argument('--verbose', action='store_true', help='Enable verbose logging') parser.add_argument('--version', action='store_true', help='Show version and exit') parser.add_argument( diff --git a/mcp_run_python/deno/src/main.ts b/mcp_run_python/deno/src/main.ts index d3c7016..2a1caca 100644 --- a/mcp_run_python/deno/src/main.ts +++ b/mcp_run_python/deno/src/main.ts @@ -1,27 +1,24 @@ // deno-lint-ignore-file no-explicit-any /// -import './polyfill.ts'; -import http from 'node:http'; -import { randomUUID } from 'node:crypto'; -import { parseArgs } from '@std/cli/parse-args'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; -import { - type LoggingLevel, - SetLevelRequestSchema, -} from '@modelcontextprotocol/sdk/types.js'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { z } from 'zod'; - -import { asJson, asXml, RunCode } from './runCode.ts'; -import { Buffer } from 'node:buffer'; - -const VERSION = '0.0.13'; +import './polyfill.ts' +import http from 'node:http' +import { randomUUID } from 'node:crypto' +import { parseArgs } from '@std/cli/parse-args' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' +import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js' +import { type LoggingLevel, SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { z } from 'zod' + +import { asJson, asXml, RunCode } from './runCode.ts' +import { Buffer } from 'node:buffer' + +const VERSION = '0.0.13' export async function main() { - const { args } = Deno; + const { args } = Deno const flags = parseArgs(Deno.args, { string: ['deps', 'return-mode', 'port', 'host'], default: { port: '3001', 'return-mode': 'xml', host: '127.0.0.1' }, @@ -29,8 +26,8 @@ export async function main() { const deps = flags.deps?.split(',') ?? [] if (args.length >= 1) { if (args[0] === 'stdio') { - await runStdio(deps, flags['return-mode']); - return; + await runStdio(deps, flags['return-mode']) + return } else if (args[0] === 'streamable_http') { const port = parseInt(flags.port) const host = flags.host @@ -42,11 +39,11 @@ export async function main() { runStreamableHttp(port, host, deps, flags['return-mode'], true) return } else if (args[0] === 'example') { - await example(deps); - return; + await example(deps) + return } else if (args[0] === 'noop') { - await installDeps(deps); - return; + await installDeps(deps) + return } } console.error( @@ -59,43 +56,42 @@ options: --port Port to run the HTTP server on (default: 3001) --host Host to run the HTTP server on (default: 127.0.0.1) --deps Comma separated list of dependencies to install ---return-mode Return mode for output data (default: xml)` - ); - Deno.exit(1); +--return-mode 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 { - const runCode = new RunCode(); + const runCode = new RunCode() const server = new McpServer( { name: 'MCP Run Python', version: VERSION, }, { - instructions: - 'Call the "run_python_code" tool with the Python code to run.', + instructions: 'Call the "run_python_code" tool with the Python code to run.', capabilities: { logging: {}, }, - } - ); + }, + ) const toolDescription = `Tool to execute Python code and return stdout, stderr, and return value. The code may be async, and the value on the last line will be returned as the return value. The code will be executed with Python 3.13. -`; +` - let setLogLevel: LoggingLevel = 'emergency'; + let setLogLevel: LoggingLevel = 'emergency' - server.server.setRequestHandler(SetLevelRequestSchema, request => { - setLogLevel = request.params.level; - return {}; - }); + server.server.setRequestHandler(SetLevelRequestSchema, (request) => { + setLogLevel = request.params.level + return {} + }) server.registerTool( 'run_python_code', @@ -104,99 +100,75 @@ The code will be executed with Python 3.13. description: toolDescription, inputSchema: { python_code: z.string().describe('Python code to run'), - global_variables: z - .record(z.string(), z.any()) - .default({}) - .describe( - 'Map of global variables in context when the code is executed' - ), + global_variables: z.record(z.string(), z.any()).default({}).describe( + 'Map of global variables in context when the code is executed', + ), }, }, - async ({ - python_code, - global_variables, - }: { - python_code: string; - global_variables: Record; - }) => { - const logPromises: Promise[] = []; + async ({ python_code, global_variables }: { python_code: string; global_variables: Record }) => { + const logPromises: Promise[] = [] const result = await runCode.run( deps, (level, data) => { if (LogLevels.indexOf(level) >= LogLevels.indexOf(setLogLevel)) { - logPromises.push(server.server.sendLoggingMessage({ level, data })); + logPromises.push(server.server.sendLoggingMessage({ level, data })) } }, { name: 'main.py', content: python_code }, global_variables, - returnMode !== 'xml' - ); - await Promise.all(logPromises); + returnMode !== 'xml', + ) + await Promise.all(logPromises) return { - content: [ - { - type: 'text', - text: returnMode === 'xml' ? asXml(result) : asJson(result), - }, - ], - }; - } - ); - return server; + content: [{ type: 'text', text: returnMode === 'xml' ? asXml(result) : asJson(result) }], + } + }, + ) + return server } /* * Define some QOL functions for both the Streamable HTTP server implementation */ function httpGetUrl(req: http.IncomingMessage): URL { - return new URL(req.url ?? '', `http://${req.headers.host ?? 'unknown'}`); + return new URL( + req.url ?? '', + `http://${req.headers.host ?? 'unknown'}`, + ) } function httpGetBody(req: http.IncomingMessage): Promise { // https://nodejs.org/en/learn/modules/anatomy-of-an-http-transaction#request-body - return new Promise(resolve => { - const bodyParts: any[] = []; - let body; - req - .on('data', chunk => { - bodyParts.push(chunk); - }) - .on('end', () => { - body = Buffer.concat(bodyParts).toString(); - resolve(JSON.parse(body)); - }); - }); + return new Promise((resolve) => { + const bodyParts: any[] = [] + let body + req.on('data', (chunk) => { + bodyParts.push(chunk) + }).on('end', () => { + body = Buffer.concat(bodyParts).toString() + resolve(JSON.parse(body)) + }) + }) } -function httpSetTextResponse( - res: http.ServerResponse, - status: number, - text: string -) { - res.setHeader('Content-Type', 'text/plain'); - res.statusCode = status; - res.end(`${text}\n`); +function httpSetTextResponse(res: http.ServerResponse, status: number, text: string) { + res.setHeader('Content-Type', 'text/plain') + res.statusCode = status + res.end(`${text}\n`) } -function httpSetJsonResponse( - res: http.ServerResponse, - status: number, - text: string, - code: number -) { - res.setHeader('Content-Type', 'application/json'); - res.statusCode = status; - res.write( - JSON.stringify({ - jsonrpc: '2.0', - error: { - code: code, - message: text, - }, - id: null, - }) - ); - res.end(); +function httpSetJsonResponse(res: http.ServerResponse, status: number, text: string, code: number) { + res.setHeader('Content-Type', 'application/json') + res.statusCode = status + res.write(JSON.stringify({ + jsonrpc: '2.0', + error: { + code: code, + message: text, + }, + id: null, + })) + res.end() } /* @@ -245,107 +217,103 @@ function createStatelessHttpServer(deps: string[], returnMode: string): http.Ser function createStatefulHttpServer(deps: 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 transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; + const mcpServer = createServer(deps, returnMode) + const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {} return http.createServer(async (req, res) => { - const url = httpGetUrl(req); - let pathMatch = false; + const url = httpGetUrl(req) + let pathMatch = false function match(method: string, path: string): boolean { if (url.pathname === path) { - pathMatch = true; - return req.method === method; + pathMatch = true + return req.method === method } - return false; + return false } // Reusable handler for GET and DELETE requests async function handleSessionRequest() { - const sessionId = req.headers['mcp-session-id'] as string | undefined; + const sessionId = req.headers['mcp-session-id'] as string | undefined if (!sessionId || !transports[sessionId]) { - httpSetTextResponse(res, 400, 'Invalid or missing session ID'); - return; + httpSetTextResponse(res, 400, 'Invalid or missing session ID') + return } - const transport = transports[sessionId]; - await transport.handleRequest(req, res); + const transport = transports[sessionId] + await transport.handleRequest(req, res) } // Handle different request methods and paths if (match('POST', '/mcp')) { // Check for existing session ID - const sessionId = req.headers['mcp-session-id'] as string | undefined; - let transport: StreamableHTTPServerTransport; + const sessionId = req.headers['mcp-session-id'] as string | undefined + let transport: StreamableHTTPServerTransport - const body = await httpGetBody(req); + const body = await httpGetBody(req) if (sessionId && transports[sessionId]) { // Reuse existing transport - transport = transports[sessionId]; + transport = transports[sessionId] } else if (!sessionId && isInitializeRequest(body)) { // New initialization request transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), - onsessioninitialized: sessionId => { + onsessioninitialized: (sessionId) => { // Store the transport by session ID - transports[sessionId] = transport; + transports[sessionId] = transport }, - }); + }) // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { - delete transports[transport.sessionId]; + delete transports[transport.sessionId] } - }; + } - await mcpServer.connect(transport); + await mcpServer.connect(transport) } else { - httpSetJsonResponse( - res, - 400, - 'Bad Request: No valid session ID provided', - -32000 - ); - return; + httpSetJsonResponse(res, 400, 'Bad Request: No valid session ID provided', -32000) + return } // Handle the request - await transport.handleRequest(req, res, body); + await transport.handleRequest(req, res, body) } else if (match('GET', '/mcp')) { // Handle server-to-client notifications - await handleSessionRequest(); + await handleSessionRequest() } else if (match('DELETE', '/mcp')) { // Handle requests for session termination - await handleSessionRequest(); + await handleSessionRequest() } else if (pathMatch) { - httpSetTextResponse(res, 405, 'Method not allowed'); + httpSetTextResponse(res, 405, 'Method not allowed') } else { - httpSetTextResponse(res, 404, 'Page not found'); + httpSetTextResponse(res, 404, 'Page not found') } - }); + }) } /* * Run the MCP server using the Stdio transport. */ async function runStdio(deps: string[], returnMode: string) { - const mcpServer = createServer(deps, returnMode); - const transport = new StdioServerTransport(); - await mcpServer.connect(transport); + const mcpServer = createServer(deps, returnMode) + const transport = new StdioServerTransport() + await mcpServer.connect(transport) } /* * Run pyodide to download and install dependencies. */ async function installDeps(deps: string[]) { - const runCode = new RunCode(); - const result = await runCode.run(deps, (level, data) => - console.error(`${level}|${data}`) - ); + const runCode = new RunCode() + const result = await runCode.run( + deps, + (level, data) => console.error(`${level}|${data}`), + ) if (result.status !== 'success') { - console.error('error|Failed to install dependencies'); - Deno.exit(1); + console.error('error|Failed to install dependencies') + Deno.exit(1) } } @@ -354,25 +322,25 @@ async function installDeps(deps: string[]) { */ async function example(deps: string[]) { console.error( - `Running example script for MCP Run Python version ${VERSION}...` - ); + `Running example script for MCP Run Python version ${VERSION}...`, + ) const code = ` import numpy a = numpy.array([1, 2, 3]) print('numpy array:', a) a -`; - const runCode = new RunCode(); +` + const runCode = new RunCode() const result = await runCode.run( deps, // use warn to avoid recursion since console.log is patched in runCode (level, data) => console.warn(`${level}: ${data}`), - { name: 'example.py', content: code } - ); - console.log('Tool return value:'); - console.log(asXml(result)); + { name: 'example.py', content: code }, + ) + console.log('Tool return value:') + console.log(asXml(result)) if (result.status !== 'success') { - Deno.exit(1); + Deno.exit(1) } } @@ -386,6 +354,6 @@ const LogLevels: LoggingLevel[] = [ 'critical', 'alert', 'emergency', -]; +] -await main() +await main() \ No newline at end of file diff --git a/mcp_run_python/main.py b/mcp_run_python/main.py index 24bb24f..22e5389 100644 --- a/mcp_run_python/main.py +++ b/mcp_run_python/main.py @@ -11,7 +11,7 @@ from pathlib import Path from typing import Literal, ParamSpec, TypeVar, cast -__all__ = "run_mcp_server", "DenoEnv", "prepare_deno_env", "async_prepare_deno_env" +__all__ = 'run_mcp_server', 'DenoEnv', 'prepare_deno_env', 'async_prepare_deno_env' logger = logging.getLogger(__name__) LoggingLevel = Literal['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'] @@ -25,7 +25,7 @@ def run_mcp_server( http_port: int | None = None, http_host: str | None = None, dependencies: list[str] | None = None, - return_mode: Literal["json", "xml"] = "xml", + return_mode: Literal['json', 'xml'] = 'xml', deps_log_handler: LogHandler | None = None, allow_networking: bool = True, verbose: bool = False, @@ -59,14 +59,12 @@ def run_mcp_server( if mode in ('streamable_http', 'streamable_http_stateless'): logger.info('Running mcp-run-python via %s on port %d...', mode, http_port) else: - logger.info("Running mcp-run-python via %s...", mode) - + logger.info('Running mcp-run-python via %s...', mode) + try: - p = subprocess.run( - ("deno", *env.args), cwd=env.cwd, stdout=stdout, stderr=stderr - ) + p = subprocess.run(('deno', *env.args), cwd=env.cwd, stdout=stdout, stderr=stderr) except KeyboardInterrupt: # pragma: no cover - logger.warning("Server stopped.") + logger.warning('Server stopped.') return 0 else: return p.returncode @@ -108,31 +106,27 @@ def prepare_deno_env( Returns: Yields the deno environment details. """ - cwd = Path(tempfile.mkdtemp()) / "mcp-run-python" + cwd = Path(tempfile.mkdtemp()) / 'mcp-run-python' try: - src = Path(__file__).parent / "deno" - logger.debug("Copying from %s to %s...", src, cwd) + src = Path(__file__).parent / 'deno' + logger.debug('Copying from %s to %s...', src, cwd) shutil.copytree(src, cwd) - logger.info("Installing dependencies %s...", dependencies) + logger.info('Installing dependencies %s...', dependencies) - args = "deno", *_deno_install_args(dependencies) - p = subprocess.Popen( - args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True - ) + args = 'deno', *_deno_install_args(dependencies) + p = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) stdout: list[str] = [] if p.stdout is not None: for line in p.stdout: line = line.strip() if deps_log_handler: - parts = line.split("|", 1) - level, msg = parts if len(parts) == 2 else ("info", line) + parts = line.split('|', 1) + level, msg = parts if len(parts) == 2 else ('info', line) deps_log_handler(cast(LoggingLevel, level), msg) stdout.append(line) p.wait() if p.returncode != 0: - raise RuntimeError( - f'`deno run ...` returned a non-zero exit code {p.returncode}: {"".join(stdout)}' - ) + raise RuntimeError(f'`deno run ...` returned a non-zero exit code {p.returncode}: {"".join(stdout)}') args = _deno_run_args( mode, @@ -178,13 +172,13 @@ async def async_prepare_deno_env( def _deno_install_args(dependencies: list[str] | None = None) -> list[str]: args = [ - "run", - "--allow-net", - "--allow-read=./node_modules", - "--allow-write=./node_modules", - "--node-modules-dir=auto", - "src/main.ts", - "noop", + 'run', + '--allow-net', + '--allow-read=./node_modules', + '--allow-write=./node_modules', + '--node-modules-dir=auto', + 'src/main.ts', + 'noop', ] if dependencies is not None: args.append(f'--deps={",".join(dependencies)}') @@ -200,15 +194,15 @@ def _deno_run_args( return_mode: Literal['json', 'xml'] = 'xml', allow_networking: bool = True, ) -> list[str]: - args = ["run"] + args = ['run'] if allow_networking: - args += ["--allow-net"] + args += ['--allow-net'] args += [ - "--allow-read=./node_modules", - "--node-modules-dir=auto", - "src/main.ts", + '--allow-read=./node_modules', + '--node-modules-dir=auto', + 'src/main.ts', mode, - f"--return-mode={return_mode}", + f'--return-mode={return_mode}', ] if dependencies is not None: args.append(f'--deps={",".join(dependencies)}') @@ -222,8 +216,8 @@ def _deno_run_args( return args -P = ParamSpec("P") -T = TypeVar("T") +P = ParamSpec('P') +T = TypeVar('T') async def _asyncify(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: