Skip to content

fix(mcp): refuse stdio MCP transport unless LITELLM_ENABLE_STDIO_MCP is set - #44194

Closed
devin-ai-integration[bot] wants to merge 4 commits into
mainfrom
litellm_disable_stdio_mcp_by_default
Closed

devin-ai-integration[bot] wants to merge 4 commits into
mainfrom
litellm_disable_stdio_mcp_by_default

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Any proxy admin can register a stdio MCP server, which runs a command on the proxy host
  • The command allowlist doesn't hold, since python -c ... and npx <any package> run arbitrary code
  • So any bug that grants proxy admin becomes RCE on the proxy host

How it solves it:

  • stdio MCP transport is refused unless the operator sets LITELLM_ENABLE_STDIO_MCP=true at deploy time
  • The flag is read once from the process env at import, so the admin API can't flip it
  • environment_variables from config or the DB can no longer set it (or LITELLM_MCP_STDIO_EXTRA_COMMANDS)
  • The Admin UI hides the stdio transport option unless the proxy has it enabled
  • HTTP and SSE MCP servers are unchanged

Intentional product change: proxies that run stdio MCP servers today lose them on upgrade until the operator sets LITELLM_ENABLE_STDIO_MCP=true in the proxy's process environment. Registering a stdio server through the UI or API now returns 422 with that instruction. stdio servers that already exist in config or the DB stay listed, log a warning at startup, and return 403 when used

User Flow

Before: a proxy admin, or an attacker who got a proxy admin key, can run any command on the proxy host

  1. They send POST https://litellm-domain/v1/mcp/server with "transport": "stdio", "command": "python3", "args": ["-c", "<any code>"]
  2. The proxy accepts it, because python3 is on the allowlist
  3. Listing or calling that server's tools starts the subprocess on the proxy host and runs their code
  4. Anyone who reaches proxy admin through any other bug gets full control of the host

After: the same request is refused unless the operator opted in at deploy time

  1. They send the same POST https://litellm-domain/v1/mcp/server with a stdio payload
  2. The proxy answers 422 stdio MCP transport is disabled on this proxy ... set LITELLM_ENABLE_STDIO_MCP=true in the proxy's process environment
  3. A stdio server already in config.yaml shows no tools, and its outcome reads forbidden, 403
  4. Setting LITELLM_ENABLE_STDIO_MCP through environment_variables in config or /config/update is skipped with a warning, so proxy admin no longer means host code execution

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/unit/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

The proxy runs DB-free on 127.0.0.1:4000 with master key sk-1234 and a config that declares one stdio server, stdio_proof. It is a tiny JSON-RPC stdio server started with python3 -c and exposes one tool, proof_echo

After (77299dd)

Gate off (default)

  1. uv run --no-sync litellm --config proof_stdio_proxy.yaml --port 4000 with LITELLM_ENABLE_STDIO_MCP unset. The startup log prints MCP server 'stdio_proof' uses stdio transport, which is disabled by default. Set LITELLM_ENABLE_STDIO_MCP=true in the proxy's process environment to enable it
  2. curl -H 'Authorization: Bearer sk-1234' http://127.0.0.1:4000/mcp-rest/tools/list returns {"tools":[],"error":"partial_failure","message":"Failed to get tools from servers: stdio_proof: forbidden","server_outcomes":{"stdio_proof":{"status":"forbidden","http_status":403}}}
  3. curl -X POST http://127.0.0.1:4000/v1/mcp/server -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"server_name":"stdio_gate_test","transport":"stdio","command":"python3","args":["-m","test_server"]}' returns 422 with Value error, stdio MCP transport is disabled on this proxy. stdio servers run a command on the proxy host, so they are off by default. To allow them, the operator must set LITELLM_ENABLE_STDIO_MCP=true in the proxy's process environment. Alternatively, run the server behind a stdio-to-HTTP bridge and register it as an HTTP MCP server

Gate on

  1. Restart with LITELLM_ENABLE_STDIO_MCP=true uv run --no-sync litellm --config proof_stdio_proxy.yaml --port 4000
  2. The same curl .../mcp-rest/tools/list returns proof_echo with "server_outcomes":{"stdio_proof":{"status":"ok","tool_count":1}}
  3. curl -X POST http://127.0.0.1:4000/mcp-rest/tools/call -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"server_id":"stdio-proof","name":"proof_echo","arguments":{}}' returns 200 with "text":"stdio proof server reached"

Type

🐛 Bug Fix

Caveats (if any)

Severe

  • Existing stdio MCP deployments stop working on upgrade until the operator sets the env var
    • The startup warning and the 403/422 messages name LITELLM_ENABLE_STDIO_MCP

Medium

  • The Admin UI create and edit forms only offer stdio when /get/ui_settings reports enable_stdio_mcp: true. That value is read-only and comes from the env var. Editing a server that already uses stdio still shows the option. Covered by component tests, but not yet checked in a running dashboard
  • Unit tests cover blocking the env var via /config/update, but the live proof doesn't, because that endpoint needs a DB
  • The DB environment_variables path now skips every _BLOCKED_ENV_KEYS entry (PATH, LD_PRELOAD, ...), as the config-file path already did

Low

  • CI's MCP integration launchers and MCP test shards now set LITELLM_ENABLE_STDIO_MCP=true, because those suites run stdio servers on purpose. The new integration test that checks stdio is off by default, and the /bin/sh allowlist assertion, haven't been run against a live integration gateway yet
  • The env-key doc check needs docs(mcp): document LITELLM_ENABLE_STDIO_MCP opt-in for stdio servers litellm-docs#2017 merged first
  • No broader scan for other command-execution sources, which is deferred on purpose

Link to Devin session: https://app.devin.ai/sessions/55147bfac539478f9080942ee7f5b155
Open in Devin Desktop: https://app.devin.ai/desktop/session/55147bfac539478f9080942ee7f5b155?variant=devin

…is set

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot requested a review from a team October 2, 2026 15:48
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[Critical risk] Adds operator gate to stdio MCP transport, blocking it by default.

The PR should wait for the single-server tool-list response to preserve the stdio opt-in instruction

Findings

  1. P1 Disabled message lost during listing ▶
  2. P2 Database servers lack startup warning ▶

Summary

The PR makes stdio MCP transport opt-in through a process-environment flag, blocks the flag from proxy-managed environment settings, and adds registration and client-creation checks. Existing disabled servers remain registered. Single-server tool listing loses the operator instruction, and database-backed servers lack the new startup warning

Reviews (1) · Last reviewed commit: "fix(mcp): refuse stdio MCP transport unl..."

Comment on lines +4298 to +4299
if not litellm_constants.MCP_STDIO_TRANSPORT_ENABLED:
raise HTTPException(status_code=403, detail=litellm_constants.MCP_STDIO_DISABLED_MESSAGE)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Disabled message lost during listing When a client lists tools for one existing stdio server with the flag off, this 403 is treated as an upstream authentication error. The client receives only Forbidden, so the operator loses the instruction to set `LITELLM_ENABLE_STDIO_MCP=true


for server_name, raw_server_config in mcp_servers_config.items():
server_config: MCPServerConfig = raw_server_config
self._warn_if_stdio_transport_disabled(server_name, server_config)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Database servers lack startup warning This warning runs only for config-file servers. Existing database-backed stdio servers are also disabled after upgrade, but their reload path emits no warning, making the change harder for operators to diagnose

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

from typing_extensions import ReadOnly

import litellm
import litellm.constants as litellm_constants
Comment thread litellm/proxy/_types.py
)
from typing_extensions import NotRequired, ReadOnly, Required, TypedDict

import litellm.constants as litellm_constants
@codspeed

codspeed Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_disable_stdio_mcp_by_default (d68d545) with main (276fc9c)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (a7eb4ba) during the generation of this report, so 276fc9c was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Closing in favor of #44066, which ships the same stdio gate with a fuller Admin UI. Yuneng owns this change going forward

This branch is waiting to be deployed

1 waiting deployment
e2e-changed — d68d5451 Waiting Oct 2, 2026 by devin-ai-integration[bot] via oauth #2612
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants