From 81e9179220cf7c95c73f47b11cb50cef49c62468 Mon Sep 17 00:00:00 2001 From: Juan Abia Date: Sat, 20 Dec 2025 22:23:45 +0100 Subject: [PATCH 1/7] feat: add --index-url support for private package registries Add support for custom package index URLs to allow installing dependencies from private registries. The --index-url CLI option can be repeated multiple times to specify multiple indexes, which are tried in order before PyPI. Changes: - Add --index-url CLI argument with action='append' for multiple indexes - Add index_urls parameter to run_mcp_server(), prepare_deno_env(), async_prepare_deno_env(), and code_sandbox() - Pass index_urls through Deno/TypeScript layer to micropip.install() - Update README with usage examples Usage: uvx mcp-run-python --index-url https://private.repo.com/simple --deps pkg stdio Python API: async with code_sandbox( dependencies=['pkg'], index_urls=['https://private.repo.com/simple'] ) as sandbox: await sandbox.eval('import pkg') --- README.md | 12 ++++++- build/prepare_env.py | 4 +-- mcp_run_python/_cli.py | 9 ++++++ mcp_run_python/code_sandbox.py | 3 ++ mcp_run_python/deno/src/main.ts | 51 ++++++++++++++++++------------ mcp_run_python/deno/src/runCode.ts | 10 ++++-- mcp_run_python/main.py | 17 ++++++++-- 7 files changed, 78 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 46f0a6d..570b582 100644 --- a/README.md +++ b/README.md @@ -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] [--deps DEPS] [--index-url URL] {stdio,streamable-http,streamable-http-stateless,example} ``` where: @@ -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 --deps 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. diff --git a/build/prepare_env.py b/build/prepare_env.py index 492101d..57c9f95 100644 --- a/build/prepare_env.py +++ b/build/prepare_env.py @@ -31,7 +31,7 @@ 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: @@ -39,7 +39,7 @@ async def prepare_env(dependencies: list[str] | None) -> Success | Error: 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) importlib.invalidate_caches() except Exception: with open(logs_filename) as f: diff --git a/mcp_run_python/_cli.py b/mcp_run_python/_cli.py index 69bab42..a15a275 100644 --- a/mcp_run_python/_cli.py +++ b/mcp_run_python/_cli.py @@ -23,6 +23,13 @@ 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( + '--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' ) @@ -47,11 +54,13 @@ def cli_logic(args_list: Sequence[str] | None = None) -> int: ) deps: list[str] = args.deps.split(',') if args.deps else [] + 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), ) diff --git a/mcp_run_python/code_sandbox.py b/mcp_run_python/code_sandbox.py index f552861..1f09030 100644 --- a/mcp_run_python/code_sandbox.py +++ b/mcp_run_python/code_sandbox.py @@ -54,6 +54,7 @@ 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']: @@ -61,6 +62,7 @@ async def code_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. @@ -68,6 +70,7 @@ async def code_sandbox( 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, diff --git a/mcp_run_python/deno/src/main.ts b/mcp_run_python/deno/src/main.ts index f71ccbd..eb9fa85 100644 --- a/mcp_run_python/deno/src/main.ts +++ b/mcp_run_python/deno/src/main.ts @@ -20,27 +20,28 @@ const VERSION = '0.0.13' export async function main() { const { args } = Deno const flags = parseArgs(Deno.args, { - string: ['deps', 'return-mode', 'port'], + string: ['deps', 'return-mode', 'port', 'index-urls'], default: { port: '3001', 'return-mode': 'xml' }, }) const deps = flags.deps?.split(',') ?? [] + const indexUrls = flags['index-urls']?.split(',') ?? [] 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 } } @@ -51,9 +52,10 @@ Invalid arguments: ${args.join(' ')} Usage: deno ... deno/main.ts [stdio|streamable_http|streamable_http_stateless|example|noop] options: ---port Port to run the HTTP server on (default: 3001) ---deps Comma separated list of dependencies to install ---return-mode Return mode for output data (default: xml)`, +--port Port to run the HTTP server on (default: 3001) +--deps Comma separated list of dependencies to install +--index-urls Comma separated list of package index URLs (tried in order before PyPI) +--return-mode Return mode for output data (default: xml)`, ) Deno.exit(1) } @@ -61,7 +63,7 @@ options: /* * 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( { @@ -106,6 +108,7 @@ The code will be executed with Python 3.13. const logPromises: Promise[] = [] const result = await runCode.run( deps, + indexUrls, (level, data) => { if (LogLevels.indexOf(level) >= LogLevels.indexOf(setLogLevel)) { logPromises.push(server.server.sendLoggingMessage({ level, data })) @@ -171,14 +174,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) @@ -188,7 +197,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, }) @@ -211,10 +220,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) => { @@ -293,8 +302,8 @@ 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) } @@ -302,10 +311,11 @@ async function runStdio(deps: string[], returnMode: string) { /* * 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') { @@ -317,7 +327,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}...`, ) @@ -330,6 +340,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 }, diff --git a/mcp_run_python/deno/src/runCode.ts b/mcp_run_python/deno/src/runCode.ts index 1be8681..6ce3691 100644 --- a/mcp_run_python/deno/src/runCode.ts +++ b/mcp_run_python/deno/src/runCode.ts @@ -23,6 +23,7 @@ export class RunCode { async run( dependencies: string[], + indexUrls: string[], log: (level: LoggingLevel, data: string) => void, file?: CodeFile, globals?: Record, @@ -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 @@ -83,6 +84,7 @@ export class RunCode { async prepEnv( dependencies: string[], + indexUrls: string[], log: (level: LoggingLevel, data: string) => void, ): Promise { const pyodide = await loadPyodide({ @@ -122,7 +124,9 @@ export class RunCode { const preparePyEnv: PreparePyEnv = pyodide.pyimport(moduleName) - const prepareStatus = await preparePyEnv.prepare_env(pyodide.toPy(dependencies)) + const prepareStatus = indexUrls.length > 0 + ? await preparePyEnv.prepare_env(pyodide.toPy(dependencies), pyodide.toPy(indexUrls)) + : await preparePyEnv.prepare_env(pyodide.toPy(dependencies)) return { pyodide, preparePyEnv, @@ -214,6 +218,6 @@ interface PrepareError { message: string } interface PreparePyEnv { - prepare_env: (files: CodeFile[]) => Promise + prepare_env: (dependencies: any, index_urls?: any) => Promise dump_json: (value: any, always_return_json: boolean) => string | null } diff --git a/mcp_run_python/main.py b/mcp_run_python/main.py index 1b95224..af9c0de 100644 --- a/mcp_run_python/main.py +++ b/mcp_run_python/main.py @@ -24,6 +24,7 @@ def run_mcp_server( *, http_port: int | None = None, dependencies: list[str] | None = None, + index_urls: list[str] | None = None, return_mode: Literal['json', 'xml'] = 'xml', deps_log_handler: LogHandler | None = None, allow_networking: bool = True, @@ -35,6 +36,7 @@ def run_mcp_server( mode: The mode to run the server in. http_port: The port to run the server on if mode is `streamable_http`. dependencies: The dependencies to install. + index_urls: Package index URLs for installing dependencies (tried in order before PyPI). return_mode: The mode to return tool results in. deps_log_handler: Optional function to receive logs emitted while installing dependencies. allow_networking: Whether to allow networking when running provided python code. @@ -48,6 +50,7 @@ def run_mcp_server( with prepare_deno_env( mode, dependencies=dependencies, + index_urls=index_urls, http_port=http_port, return_mode=return_mode, deps_log_handler=deps_log_handler, @@ -79,6 +82,7 @@ def prepare_deno_env( *, http_port: int | None = None, dependencies: list[str] | None = None, + index_urls: list[str] | None = None, return_mode: Literal['json', 'xml'] = 'xml', deps_log_handler: LogHandler | None = None, allow_networking: bool = True, @@ -93,6 +97,7 @@ def prepare_deno_env( mode: The mode to run the server in. http_port: The port to run the server on if mode is `streamable_http`. dependencies: The dependencies to install. + index_urls: Package index URLs for installing dependencies (tried in order before PyPI). return_mode: The mode to return tool results in. deps_log_handler: Optional function to receive logs emitted while installing dependencies. allow_networking: Whether the prepared DenoEnv should allow networking when running code. @@ -108,7 +113,7 @@ def prepare_deno_env( shutil.copytree(src, cwd, ignore=shutil.ignore_patterns('node_modules')) logger.info('Installing dependencies %s...', dependencies) - args = 'deno', *_deno_install_args(dependencies) + args = 'deno', *_deno_install_args(dependencies, index_urls) p = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) stdout: list[str] = [] if p.stdout is not None: @@ -127,6 +132,7 @@ def prepare_deno_env( mode, http_port=http_port, dependencies=dependencies, + index_urls=index_urls, return_mode=return_mode, allow_networking=allow_networking, ) @@ -142,6 +148,7 @@ async def async_prepare_deno_env( *, http_port: int | None = None, dependencies: list[str] | None = None, + index_urls: list[str] | None = None, return_mode: Literal['json', 'xml'] = 'xml', deps_log_handler: LogHandler | None = None, allow_networking: bool = True, @@ -152,6 +159,7 @@ async def async_prepare_deno_env( mode, http_port=http_port, dependencies=dependencies, + index_urls=index_urls, return_mode=return_mode, deps_log_handler=deps_log_handler, allow_networking=allow_networking, @@ -162,7 +170,7 @@ async def async_prepare_deno_env( await _asyncify(ct.__exit__, None, None, None) -def _deno_install_args(dependencies: list[str] | None = None) -> list[str]: +def _deno_install_args(dependencies: list[str] | None = None, index_urls: list[str] | None = None) -> list[str]: args = [ 'run', '--allow-net', @@ -174,6 +182,8 @@ def _deno_install_args(dependencies: list[str] | None = None) -> list[str]: ] if dependencies is not None: args.append(f'--deps={",".join(dependencies)}') + if index_urls: + args.append(f'--index-urls={",".join(index_urls)}') return args @@ -182,6 +192,7 @@ def _deno_run_args( *, http_port: int | None = None, dependencies: list[str] | None = None, + index_urls: list[str] | None = None, return_mode: Literal['json', 'xml'] = 'xml', allow_networking: bool = True, ) -> list[str]: @@ -197,6 +208,8 @@ def _deno_run_args( ] if dependencies is not None: args.append(f'--deps={",".join(dependencies)}') + if index_urls: + args.append(f'--index-urls={",".join(index_urls)}') if http_port is not None: if mode in ('streamable_http', 'streamable_http_stateless'): args.append(f'--port={http_port}') From 229bf334764cd96f60f0e5db0c445dff7395ac68 Mon Sep 17 00:00:00 2001 From: Juan Abia Date: Tue, 13 Jan 2026 15:33:21 +0100 Subject: [PATCH 2/7] refactor: simplify indexUrls passing and add --dep flag for consistency - Always pass indexUrls to prepare_env (even if empty), matching how dependencies are handled - Add repeatable --dep flag alongside existing --deps for CLI consistency with --index-url - Mark --deps as deprecated but keep it working for backwards compatibility --- mcp_run_python/_cli.py | 9 +++++++-- mcp_run_python/deno/src/runCode.ts | 9 +++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/mcp_run_python/_cli.py b/mcp_run_python/_cli.py index a15a275..b307a1e 100644 --- a/mcp_run_python/_cli.py +++ b/mcp_run_python/_cli.py @@ -22,7 +22,10 @@ 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='(Deprecated) Comma separated list of dependencies to install') parser.add_argument( '--index-url', action='append', @@ -53,7 +56,9 @@ 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: + deps.extend(args.deps.split(',')) index_urls: list[str] = args.index_urls or [] return_code = run_mcp_server( args.mode.replace('-', '_'), diff --git a/mcp_run_python/deno/src/runCode.ts b/mcp_run_python/deno/src/runCode.ts index 6ce3691..2871cf4 100644 --- a/mcp_run_python/deno/src/runCode.ts +++ b/mcp_run_python/deno/src/runCode.ts @@ -124,9 +124,10 @@ export class RunCode { const preparePyEnv: PreparePyEnv = pyodide.pyimport(moduleName) - const prepareStatus = indexUrls.length > 0 - ? await preparePyEnv.prepare_env(pyodide.toPy(dependencies), pyodide.toPy(indexUrls)) - : await preparePyEnv.prepare_env(pyodide.toPy(dependencies)) + const prepareStatus = await preparePyEnv.prepare_env( + pyodide.toPy(dependencies), + pyodide.toPy(indexUrls), + ) return { pyodide, preparePyEnv, @@ -218,6 +219,6 @@ interface PrepareError { message: string } interface PreparePyEnv { - prepare_env: (dependencies: any, index_urls?: any) => Promise + prepare_env: (dependencies: any, index_urls: any) => Promise dump_json: (value: any, always_return_json: boolean) => string | null } From 87a9ff8a5e33976852361b0ef89168d7518deea3 Mon Sep 17 00:00:00 2001 From: Juan Abia Date: Tue, 13 Jan 2026 15:42:25 +0100 Subject: [PATCH 3/7] fix: convert empty index_urls list to None for micropip micropip treats empty list [] differently from None - with [] it doesn't fall back to PyPI. Using 'index_urls or None' ensures empty lists are converted to None so PyPI fallback works correctly. --- build/prepare_env.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/prepare_env.py b/build/prepare_env.py index 57c9f95..7b5901e 100644 --- a/build/prepare_env.py +++ b/build/prepare_env.py @@ -39,7 +39,7 @@ async def prepare_env(dependencies: list[str] | None, index_urls: list[str] | No with _micropip_logging() as logs_filename: try: - await micropip.install(dependencies, keep_going=True, index_urls=index_urls) + await micropip.install(dependencies, keep_going=True, index_urls=index_urls or None) importlib.invalidate_caches() except Exception: with open(logs_filename) as f: From b972ef4c4c7d6b3f38b482fce6dabe4e16d14d3c Mon Sep 17 00:00:00 2001 From: Juan Abia Date: Wed, 14 Jan 2026 10:22:21 +0100 Subject: [PATCH 4/7] docs: update CLI to use --dep instead of --deps - Update README.md examples to use --dep (repeatable) instead of --deps - Hide --deps from CLI help (argparse.SUPPRESS) - Add DeprecationWarning when --deps is used - Keep --deps working for backwards compatibility --- README.md | 6 +++--- mcp_run_python/_cli.py | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 570b582..08c1e09 100644 --- a/README.md +++ b/README.md @@ -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] [--index-url URL] {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: @@ -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 @@ -172,7 +172,7 @@ Dependencies must be provided when initializing the server so they can be instal 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 --deps mypackage stdio +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. diff --git a/mcp_run_python/_cli.py b/mcp_run_python/_cli.py index b307a1e..40f8744 100644 --- a/mcp_run_python/_cli.py +++ b/mcp_run_python/_cli.py @@ -3,6 +3,7 @@ import argparse import logging import sys +import warnings from collections.abc import Sequence from . import __version__ @@ -25,7 +26,7 @@ def cli_logic(args_list: Sequence[str] | None = None) -> int: parser.add_argument( '--dep', action='append', dest='dep_list', metavar='PKG', help='Dependency to install (can be repeated)' ) - parser.add_argument('--deps', '--dependencies', help='(Deprecated) Comma separated list of dependencies to install') + parser.add_argument('--deps', '--dependencies', help=argparse.SUPPRESS) parser.add_argument( '--index-url', action='append', @@ -58,6 +59,11 @@ def cli_logic(args_list: Sequence[str] | None = None) -> int: 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( From afd0d05b5ea574a9d57d9ea30c4184fd4bdf60e7 Mon Sep 17 00:00:00 2001 From: Juan Abia Date: Wed, 14 Jan 2026 11:03:02 +0100 Subject: [PATCH 5/7] refactor: make Deno CLI use repeatable args for consistency - Add --dep and --index-url as repeatable args in Deno CLI (using collect) - Update Python main.py to pass --dep= and --index-url= instead of comma-separated - Add deprecation warnings when --deps or --index-urls are used - Update Deno help text to show new args - Keep backwards compatibility for old comma-separated format - Add test for repeatable --dep flag --- mcp_run_python/deno/src/main.ts | 25 +++++++++++++++++++++---- mcp_run_python/main.py | 12 ++++++++---- tests/test_cli.py | 5 +++++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/mcp_run_python/deno/src/main.ts b/mcp_run_python/deno/src/main.ts index eb9fa85..6291d4d 100644 --- a/mcp_run_python/deno/src/main.ts +++ b/mcp_run_python/deno/src/main.ts @@ -21,10 +21,27 @@ export async function main() { const { args } = Deno const flags = parseArgs(Deno.args, { string: ['deps', 'return-mode', 'port', 'index-urls'], + collect: ['dep', 'index-url'], default: { port: '3001', 'return-mode': 'xml' }, }) - const deps = flags.deps?.split(',') ?? [] - const indexUrls = flags['index-urls']?.split(',') ?? [] + + // Deprecation warnings for old comma-separated args + if (flags.deps) { + console.warn('Warning: --deps is deprecated, use --dep instead (can be repeated)') + } + if (flags['index-urls']) { + console.warn('Warning: --index-urls is deprecated, use --index-url 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) ?? []), + ...(flags['index-urls']?.split(',').filter(Boolean) ?? []), + ] if (args.length >= 1) { if (args[0] === 'stdio') { await runStdio(deps, indexUrls, flags['return-mode']) @@ -53,8 +70,8 @@ Usage: deno ... deno/main.ts [stdio|streamable_http|streamable_http_stateless|ex options: --port Port to run the HTTP server on (default: 3001) ---deps Comma separated list of dependencies to install ---index-urls Comma separated list of package index URLs (tried in order before PyPI) +--dep Dependency to install (can be repeated) +--index-url Package index URL (can be repeated, tried before PyPI) --return-mode Return mode for output data (default: xml)`, ) Deno.exit(1) diff --git a/mcp_run_python/main.py b/mcp_run_python/main.py index af9c0de..5b6e93d 100644 --- a/mcp_run_python/main.py +++ b/mcp_run_python/main.py @@ -181,9 +181,11 @@ def _deno_install_args(dependencies: list[str] | None = None, index_urls: list[s 'noop', ] if dependencies is not None: - args.append(f'--deps={",".join(dependencies)}') + for dep in dependencies: + args.append(f'--dep={dep}') if index_urls: - args.append(f'--index-urls={",".join(index_urls)}') + for url in index_urls: + args.append(f'--index-url={url}') return args @@ -207,9 +209,11 @@ def _deno_run_args( f'--return-mode={return_mode}', ] if dependencies is not None: - args.append(f'--deps={",".join(dependencies)}') + for dep in dependencies: + args.append(f'--dep={dep}') if index_urls: - args.append(f'--index-urls={",".join(index_urls)}') + for url in index_urls: + args.append(f'--index-url={url}') if http_port is not None: if mode in ('streamable_http', 'streamable_http_stateless'): args.append(f'--port={http_port}') diff --git a/tests/test_cli.py b/tests/test_cli.py index b86bd4c..e35d09d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -15,5 +15,10 @@ def test_cli_example_success(): assert cli_logic(['--deps', 'numpy', 'example']) == 0 +def test_cli_dep_repeatable(): + """Test new --dep repeatable flag""" + assert cli_logic(['--dep', 'numpy', 'example']) == 0 + + def test_cli_example_fail(): assert cli_logic(['example']) == 1 From 30b061506793f3fd85ba16bf4e1cdb214702835f Mon Sep 17 00:00:00 2001 From: Juan Abia Date: Wed, 14 Jan 2026 11:15:15 +0100 Subject: [PATCH 6/7] test: add tests for multiple --dep flags and deprecation warning - test_cli_dep_multiple: test multiple --dep flags (--dep numpy --dep pydantic) - test_cli_dep_and_deps_combined: test combining --dep and --deps - test_cli_deps_deprecation_warning: verify deprecation warning is emitted --- tests/test_cli.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index e35d09d..170f94b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,7 @@ from __future__ import annotations as _annotations +import warnings + import pytest from mcp_run_python._cli import cli_logic @@ -20,5 +22,25 @@ def test_cli_dep_repeatable(): assert cli_logic(['--dep', 'numpy', 'example']) == 0 +def test_cli_dep_multiple(): + """Test multiple --dep flags""" + assert cli_logic(['--dep', 'numpy', '--dep', 'pydantic', 'example']) == 0 + + +def test_cli_dep_and_deps_combined(): + """Test combining --dep and --deps (backwards compat)""" + assert cli_logic(['--dep', 'numpy', '--deps', 'pydantic', 'example']) == 0 + + +def test_cli_deps_deprecation_warning(): + """Test that --deps emits a deprecation warning""" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + cli_logic(['--deps', 'numpy', 'example']) + deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)] + assert len(deprecation_warnings) >= 1 + assert any('--deps is deprecated' in str(x.message) for x in deprecation_warnings) + + def test_cli_example_fail(): assert cli_logic(['example']) == 1 From 30a7c22e160a7bc14dc38e6e0bb6482253f8886d Mon Sep 17 00:00:00 2001 From: Juan Abia Date: Wed, 21 Jan 2026 08:48:23 +0100 Subject: [PATCH 7/7] fix: remove unnecessary --index-urls deprecation warning --index-urls was never a public option, so there's nothing to deprecate. --- mcp_run_python/deno/src/main.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/mcp_run_python/deno/src/main.ts b/mcp_run_python/deno/src/main.ts index 6291d4d..7b131eb 100644 --- a/mcp_run_python/deno/src/main.ts +++ b/mcp_run_python/deno/src/main.ts @@ -20,7 +20,7 @@ const VERSION = '0.0.13' export async function main() { const { args } = Deno const flags = parseArgs(Deno.args, { - string: ['deps', 'return-mode', 'port', 'index-urls'], + string: ['deps', 'return-mode', 'port'], collect: ['dep', 'index-url'], default: { port: '3001', 'return-mode': 'xml' }, }) @@ -29,19 +29,12 @@ export async function main() { if (flags.deps) { console.warn('Warning: --deps is deprecated, use --dep instead (can be repeated)') } - if (flags['index-urls']) { - console.warn('Warning: --index-urls is deprecated, use --index-url 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) ?? []), - ...(flags['index-urls']?.split(',').filter(Boolean) ?? []), - ] + const indexUrls: string[] = (flags['index-url'] as string[] | undefined) ?? [] if (args.length >= 1) { if (args[0] === 'stdio') { await runStdio(deps, indexUrls, flags['return-mode'])