Skip to content

Commit 22eb651

Browse files
committed
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')
1 parent 5ee2eea commit 22eb651

File tree

7 files changed

+78
-28
lines changed

7 files changed

+78
-28
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ To use this server, you must have both Python and [Deno](https://deno.com/) inst
3434
The server can be run with `deno` installed using `uvx`:
3535

3636
```bash
37-
uvx mcp-run-python [-h] [--version] [--port PORT] [--deps DEPS] {stdio,streamable-http,streamable-http-stateless,example}
37+
uvx mcp-run-python [-h] [--version] [--port PORT] [--deps DEPS] [--index-url URL] {stdio,streamable-http,streamable-http-stateless,example}
3838
```
3939

4040
where:
@@ -166,3 +166,13 @@ edit the filesystem.
166166
* `deno` is then run with read-only permissions to the `node_modules` directory to run untrusted code.
167167

168168
Dependencies must be provided when initializing the server so they can be installed in the first step.
169+
170+
## Custom Package Indexes
171+
172+
Use `--index-url` to install dependencies from private registries (can be repeated, tried in order before PyPI):
173+
174+
```bash
175+
uvx mcp-run-python --index-url https://private.repo.com/simple --deps mypackage stdio
176+
```
177+
178+
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.

build/prepare_env.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@ class Error:
3131
kind: Literal['error'] = 'error'
3232

3333

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

3737
if dependencies:
3838
dependencies = _add_extra_dependencies(dependencies)
3939

4040
with _micropip_logging() as logs_filename:
4141
try:
42-
await micropip.install(dependencies, keep_going=True)
42+
await micropip.install(dependencies, keep_going=True, index_urls=index_urls)
4343
importlib.invalidate_caches()
4444
except Exception:
4545
with open(logs_filename) as f:

mcp_run_python/_cli.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ def cli_logic(args_list: Sequence[str] | None = None) -> int:
2323

2424
parser.add_argument('--port', type=int, help='Port to run the server on, default 3001.')
2525
parser.add_argument('--deps', '--dependencies', help='Comma separated list of dependencies to install')
26+
parser.add_argument(
27+
'--index-url',
28+
action='append',
29+
dest='index_urls',
30+
metavar='URL',
31+
help='Package index URL for installing dependencies (can be repeated, tried in order before PyPI)',
32+
)
2633
parser.add_argument(
2734
'--disable-networking', action='store_true', help='Disable networking during execution of python code'
2835
)
@@ -47,11 +54,13 @@ def cli_logic(args_list: Sequence[str] | None = None) -> int:
4754
)
4855

4956
deps: list[str] = args.deps.split(',') if args.deps else []
57+
index_urls: list[str] = args.index_urls or []
5058
return_code = run_mcp_server(
5159
args.mode.replace('-', '_'),
5260
allow_networking=not args.disable_networking,
5361
http_port=args.port,
5462
dependencies=deps,
63+
index_urls=index_urls,
5564
deps_log_handler=deps_log_handler,
5665
verbose=bool(args.verbose),
5766
)

mcp_run_python/code_sandbox.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,23 @@ async def eval(
5454
async def code_sandbox(
5555
*,
5656
dependencies: list[str] | None = None,
57+
index_urls: list[str] | None = None,
5758
log_handler: LogHandler | None = None,
5859
allow_networking: bool = True,
5960
) -> AsyncIterator['CodeSandbox']:
6061
"""Create a secure sandbox.
6162
6263
Args:
6364
dependencies: A list of dependencies to be installed.
65+
index_urls: Package index URLs for installing dependencies (tried in order before PyPI).
6466
log_handler: A callback function to handle print statements when code is running.
6567
deps_log_handler: A callback function to run on log statements during initial install of dependencies.
6668
allow_networking: Whether to allow networking or not while executing python code.
6769
"""
6870
async with async_prepare_deno_env(
6971
'stdio',
7072
dependencies=dependencies,
73+
index_urls=index_urls,
7174
deps_log_handler=log_handler,
7275
return_mode='json',
7376
allow_networking=allow_networking,

mcp_run_python/deno/src/main.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,28 @@ const VERSION = '0.0.13'
2020
export async function main() {
2121
const { args } = Deno
2222
const flags = parseArgs(Deno.args, {
23-
string: ['deps', 'return-mode', 'port'],
23+
string: ['deps', 'return-mode', 'port', 'index-urls'],
2424
default: { port: '3001', 'return-mode': 'xml' },
2525
})
2626
const deps = flags.deps?.split(',') ?? []
27+
const indexUrls = flags['index-urls']?.split(',') ?? []
2728
if (args.length >= 1) {
2829
if (args[0] === 'stdio') {
29-
await runStdio(deps, flags['return-mode'])
30+
await runStdio(deps, indexUrls, flags['return-mode'])
3031
return
3132
} else if (args[0] === 'streamable_http') {
3233
const port = parseInt(flags.port)
33-
runStreamableHttp(port, deps, flags['return-mode'], false)
34+
runStreamableHttp(port, deps, indexUrls, flags['return-mode'], false)
3435
return
3536
} else if (args[0] === 'streamable_http_stateless') {
3637
const port = parseInt(flags.port)
37-
runStreamableHttp(port, deps, flags['return-mode'], true)
38+
runStreamableHttp(port, deps, indexUrls, flags['return-mode'], true)
3839
return
3940
} else if (args[0] === 'example') {
40-
await example(deps)
41+
await example(deps, indexUrls)
4142
return
4243
} else if (args[0] === 'noop') {
43-
await installDeps(deps)
44+
await installDeps(deps, indexUrls)
4445
return
4546
}
4647
}
@@ -51,17 +52,18 @@ Invalid arguments: ${args.join(' ')}
5152
Usage: deno ... deno/main.ts [stdio|streamable_http|streamable_http_stateless|example|noop]
5253
5354
options:
54-
--port <port> Port to run the HTTP server on (default: 3001)
55-
--deps <deps> Comma separated list of dependencies to install
56-
--return-mode <xml/json> Return mode for output data (default: xml)`,
55+
--port <port> Port to run the HTTP server on (default: 3001)
56+
--deps <deps> Comma separated list of dependencies to install
57+
--index-urls <urls> Comma separated list of package index URLs (tried in order before PyPI)
58+
--return-mode <xml/json> Return mode for output data (default: xml)`,
5759
)
5860
Deno.exit(1)
5961
}
6062

6163
/*
6264
* Create an MCP server with the `run_python_code` tool registered.
6365
*/
64-
function createServer(deps: string[], returnMode: string): McpServer {
66+
function createServer(deps: string[], indexUrls: string[], returnMode: string): McpServer {
6567
const runCode = new RunCode()
6668
const server = new McpServer(
6769
{
@@ -106,6 +108,7 @@ The code will be executed with Python 3.13.
106108
const logPromises: Promise<void>[] = []
107109
const result = await runCode.run(
108110
deps,
111+
indexUrls,
109112
(level, data) => {
110113
if (LogLevels.indexOf(level) >= LogLevels.indexOf(setLogLevel)) {
111114
logPromises.push(server.server.sendLoggingMessage({ level, data }))
@@ -171,14 +174,20 @@ function httpSetJsonResponse(res: http.ServerResponse, status: number, text: str
171174
/*
172175
* Run the MCP server using the Streamable HTTP transport
173176
*/
174-
function runStreamableHttp(port: number, deps: string[], returnMode: string, stateless: boolean): void {
175-
const server = (stateless ? createStatelessHttpServer : createStatefulHttpServer)(deps, returnMode)
177+
function runStreamableHttp(
178+
port: number,
179+
deps: string[],
180+
indexUrls: string[],
181+
returnMode: string,
182+
stateless: boolean,
183+
): void {
184+
const server = (stateless ? createStatelessHttpServer : createStatefulHttpServer)(deps, indexUrls, returnMode)
176185
server.listen(port, () => {
177186
console.log(`Listening on port ${port}`)
178187
})
179188
}
180189

181-
function createStatelessHttpServer(deps: string[], returnMode: string): http.Server {
190+
function createStatelessHttpServer(deps: string[], indexUrls: string[], returnMode: string): http.Server {
182191
return http.createServer(async (req, res) => {
183192
const url = httpGetUrl(req)
184193

@@ -188,7 +197,7 @@ function createStatelessHttpServer(deps: string[], returnMode: string): http.Ser
188197
}
189198

190199
try {
191-
const mcpServer = createServer(deps, returnMode)
200+
const mcpServer = createServer(deps, indexUrls, returnMode)
192201
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
193202
sessionIdGenerator: undefined,
194203
})
@@ -211,10 +220,10 @@ function createStatelessHttpServer(deps: string[], returnMode: string): http.Ser
211220
})
212221
}
213222

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

220229
return http.createServer(async (req, res) => {
@@ -293,19 +302,20 @@ function createStatefulHttpServer(deps: string[], returnMode: string): http.Serv
293302
/*
294303
* Run the MCP server using the Stdio transport.
295304
*/
296-
async function runStdio(deps: string[], returnMode: string) {
297-
const mcpServer = createServer(deps, returnMode)
305+
async function runStdio(deps: string[], indexUrls: string[], returnMode: string) {
306+
const mcpServer = createServer(deps, indexUrls, returnMode)
298307
const transport = new StdioServerTransport()
299308
await mcpServer.connect(transport)
300309
}
301310

302311
/*
303312
* Run pyodide to download and install dependencies.
304313
*/
305-
async function installDeps(deps: string[]) {
314+
async function installDeps(deps: string[], indexUrls: string[]) {
306315
const runCode = new RunCode()
307316
const result = await runCode.run(
308317
deps,
318+
indexUrls,
309319
(level, data) => console.error(`${level}|${data}`),
310320
)
311321
if (result.status !== 'success') {
@@ -317,7 +327,7 @@ async function installDeps(deps: string[]) {
317327
/*
318328
* Run a short example script that requires numpy.
319329
*/
320-
async function example(deps: string[]) {
330+
async function example(deps: string[], indexUrls: string[]) {
321331
console.error(
322332
`Running example script for MCP Run Python version ${VERSION}...`,
323333
)
@@ -330,6 +340,7 @@ a
330340
const runCode = new RunCode()
331341
const result = await runCode.run(
332342
deps,
343+
indexUrls,
333344
// use warn to avoid recursion since console.log is patched in runCode
334345
(level, data) => console.warn(`${level}: ${data}`),
335346
{ name: 'example.py', content: code },

mcp_run_python/deno/src/runCode.ts

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

2424
async run(
2525
dependencies: string[],
26+
indexUrls: string[],
2627
log: (level: LoggingLevel, data: string) => void,
2728
file?: CodeFile,
2829
globals?: Record<string, any>,
@@ -38,7 +39,7 @@ export class RunCode {
3839
sys = pyodide.pyimport('sys')
3940
} else {
4041
if (!this.prepPromise) {
41-
this.prepPromise = this.prepEnv(dependencies, log)
42+
this.prepPromise = this.prepEnv(dependencies, indexUrls, log)
4243
}
4344
// TODO is this safe if the promise has already been accessed? it seems to work fine
4445
const prep = await this.prepPromise
@@ -83,6 +84,7 @@ export class RunCode {
8384

8485
async prepEnv(
8586
dependencies: string[],
87+
indexUrls: string[],
8688
log: (level: LoggingLevel, data: string) => void,
8789
): Promise<PrepResult> {
8890
const pyodide = await loadPyodide({
@@ -122,7 +124,9 @@ export class RunCode {
122124

123125
const preparePyEnv: PreparePyEnv = pyodide.pyimport(moduleName)
124126

125-
const prepareStatus = await preparePyEnv.prepare_env(pyodide.toPy(dependencies))
127+
const prepareStatus = indexUrls.length > 0
128+
? await preparePyEnv.prepare_env(pyodide.toPy(dependencies), pyodide.toPy(indexUrls))
129+
: await preparePyEnv.prepare_env(pyodide.toPy(dependencies))
126130
return {
127131
pyodide,
128132
preparePyEnv,
@@ -214,6 +218,6 @@ interface PrepareError {
214218
message: string
215219
}
216220
interface PreparePyEnv {
217-
prepare_env: (files: CodeFile[]) => Promise<PrepareSuccess | PrepareError>
221+
prepare_env: (dependencies: any, index_urls?: any) => Promise<PrepareSuccess | PrepareError>
218222
dump_json: (value: any, always_return_json: boolean) => string | null
219223
}

mcp_run_python/main.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def run_mcp_server(
2424
*,
2525
http_port: int | None = None,
2626
dependencies: list[str] | None = None,
27+
index_urls: list[str] | None = None,
2728
return_mode: Literal['json', 'xml'] = 'xml',
2829
deps_log_handler: LogHandler | None = None,
2930
allow_networking: bool = True,
@@ -35,6 +36,7 @@ def run_mcp_server(
3536
mode: The mode to run the server in.
3637
http_port: The port to run the server on if mode is `streamable_http`.
3738
dependencies: The dependencies to install.
39+
index_urls: Package index URLs for installing dependencies (tried in order before PyPI).
3840
return_mode: The mode to return tool results in.
3941
deps_log_handler: Optional function to receive logs emitted while installing dependencies.
4042
allow_networking: Whether to allow networking when running provided python code.
@@ -48,6 +50,7 @@ def run_mcp_server(
4850
with prepare_deno_env(
4951
mode,
5052
dependencies=dependencies,
53+
index_urls=index_urls,
5154
http_port=http_port,
5255
return_mode=return_mode,
5356
deps_log_handler=deps_log_handler,
@@ -79,6 +82,7 @@ def prepare_deno_env(
7982
*,
8083
http_port: int | None = None,
8184
dependencies: list[str] | None = None,
85+
index_urls: list[str] | None = None,
8286
return_mode: Literal['json', 'xml'] = 'xml',
8387
deps_log_handler: LogHandler | None = None,
8488
allow_networking: bool = True,
@@ -93,6 +97,7 @@ def prepare_deno_env(
9397
mode: The mode to run the server in.
9498
http_port: The port to run the server on if mode is `streamable_http`.
9599
dependencies: The dependencies to install.
100+
index_urls: Package index URLs for installing dependencies (tried in order before PyPI).
96101
return_mode: The mode to return tool results in.
97102
deps_log_handler: Optional function to receive logs emitted while installing dependencies.
98103
allow_networking: Whether the prepared DenoEnv should allow networking when running code.
@@ -108,7 +113,7 @@ def prepare_deno_env(
108113
shutil.copytree(src, cwd, ignore=shutil.ignore_patterns('node_modules'))
109114
logger.info('Installing dependencies %s...', dependencies)
110115

111-
args = 'deno', *_deno_install_args(dependencies)
116+
args = 'deno', *_deno_install_args(dependencies, index_urls)
112117
p = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
113118
stdout: list[str] = []
114119
if p.stdout is not None:
@@ -127,6 +132,7 @@ def prepare_deno_env(
127132
mode,
128133
http_port=http_port,
129134
dependencies=dependencies,
135+
index_urls=index_urls,
130136
return_mode=return_mode,
131137
allow_networking=allow_networking,
132138
)
@@ -142,6 +148,7 @@ async def async_prepare_deno_env(
142148
*,
143149
http_port: int | None = None,
144150
dependencies: list[str] | None = None,
151+
index_urls: list[str] | None = None,
145152
return_mode: Literal['json', 'xml'] = 'xml',
146153
deps_log_handler: LogHandler | None = None,
147154
allow_networking: bool = True,
@@ -152,6 +159,7 @@ async def async_prepare_deno_env(
152159
mode,
153160
http_port=http_port,
154161
dependencies=dependencies,
162+
index_urls=index_urls,
155163
return_mode=return_mode,
156164
deps_log_handler=deps_log_handler,
157165
allow_networking=allow_networking,
@@ -162,7 +170,7 @@ async def async_prepare_deno_env(
162170
await _asyncify(ct.__exit__, None, None, None)
163171

164172

165-
def _deno_install_args(dependencies: list[str] | None = None) -> list[str]:
173+
def _deno_install_args(dependencies: list[str] | None = None, index_urls: list[str] | None = None) -> list[str]:
166174
args = [
167175
'run',
168176
'--allow-net',
@@ -174,6 +182,8 @@ def _deno_install_args(dependencies: list[str] | None = None) -> list[str]:
174182
]
175183
if dependencies is not None:
176184
args.append(f'--deps={",".join(dependencies)}')
185+
if index_urls:
186+
args.append(f'--index-urls={",".join(index_urls)}')
177187
return args
178188

179189

@@ -182,6 +192,7 @@ def _deno_run_args(
182192
*,
183193
http_port: int | None = None,
184194
dependencies: list[str] | None = None,
195+
index_urls: list[str] | None = None,
185196
return_mode: Literal['json', 'xml'] = 'xml',
186197
allow_networking: bool = True,
187198
) -> list[str]:
@@ -197,6 +208,8 @@ def _deno_run_args(
197208
]
198209
if dependencies is not None:
199210
args.append(f'--deps={",".join(dependencies)}')
211+
if index_urls:
212+
args.append(f'--index-urls={",".join(index_urls)}')
200213
if http_port is not None:
201214
if mode in ('streamable_http', 'streamable_http_stateless'):
202215
args.append(f'--port={http_port}')

0 commit comments

Comments
 (0)