Skip to content

feat(mcp)!: disable stdio MCP servers by default - #44066

Merged
yuneng-berri merged 10 commits into
mainfrom
litellm_mcp-stdio-flag
Oct 2, 2026
Merged

yuneng-berri merged 10 commits into
mainfrom
litellm_mcp-stdio-flag

Conversation

@yuneng-berri

@yuneng-berri yuneng-berri commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • stdio MCP servers run local commands on the proxy host
  • Most deployments only need remote HTTP/SSE servers
  • There was no way to switch stdio off

How it solves it:

  • New env var LITELLM_ENABLE_MCP_STDIO, off by default
  • Off: stdio servers stay listed but never start
  • Off: creating or updating a stdio server is rejected
  • UI greys out stdio and explains how to turn it on
  • Only the process env counts; config.yaml and DB-stored values are ignored
  • The UI reads the flag from the worker it is managing

Intentional product change: proxies that already run stdio MCP servers stop starting them after upgrade until the admin sets LITELLM_ENABLE_MCP_STDIO=true and restarts. Users lose those servers' tools in the meantime, and the proxy logs one warning per server naming the env var

User Flow

Before: an admin with a stdio MCP server in config.yaml upgrades and everything keeps working, with no way to opt out of stdio

  1. They open http://localhost:4000/ui/?page=mcp-servers and the stdio server card shows "Healthy"
  2. GET /mcp-rest/tools/list?server_id= returns ["echo"]
  3. POST /mcp-rest/tools/call for echo_stdio-echo returns echo: hi
  4. POST /v1/mcp/server with "transport": "stdio" creates a new stdio server

After: the same admin upgrades without setting the env var, and stdio stays off until they opt in

  1. Proxy boot logs MCP server 'echo_stdio' will not start: stdio MCP servers are disabled on this proxy. Set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart to enable them
  2. http://localhost:4000/ui/?page=mcp-servers shows a "stdio disabled" badge with that message as a tooltip
  3. GET /mcp-rest/tools/list?server_id= returns [], and POST /mcp-rest/tools/call returns 403 with the message
  4. POST /v1/mcp/server with "transport": "stdio" returns 422 with the message, and the Create/Edit forms grey out stdio
  5. They set LITELLM_ENABLE_MCP_STDIO=true, restart, and steps 1 to 4 of Before work again

Server cards with the flag off (badges and tooltip boxed):

stdio disabled badge

Edit form transport select with the flag off, stdio greyed out with the tooltip:

greyed out stdio option

Edit page of an existing stdio server with the flag off:

edit banner

Create form prefilled from a stdio catalog entry with the flag off:

create banner

Detail page of an existing stdio server with the flag off, which used to say only "No tools available":

detail banner

Same page with LITELLM_ENABLE_MCP_STDIO=true, which looks the same as before this PR (no badges, stdio server healthy):

flag on

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

Setup: a local proxy on port 4071 with Postgres and STORE_MODEL_IN_DB=True, launched under env -i so only the listed env reaches it. echo_stdio is a stdio server in config.yaml (python echo_server.py, a one-tool MCP server that echoes its input) with id ebabfe6d53267166b95d507599c04f10. new_stdio was created through the API in the Before run. KEY is the proxy master key. No LLM provider is involved, since the change is in MCP server handling

The UI screenshots for each case are embedded under User Flow above

Before (a76b59d)

stdio server works

  1. curl -s 'localhost:4071/mcp-rest/tools/list?server_id=ebabfe6d53267166b95d507599c04f10' -H "Authorization: Bearer $KEY" | jq -c '[.tools[].name]'
    ["echo"]
    
  2. curl -s localhost:4071/mcp-rest/tools/call -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"server_id":"ebabfe6d53267166b95d507599c04f10","name":"echo_stdio-echo","arguments":{"text":"hi"}}' | jq -c '.content'
    [{"type":"text","text":"echo: hi","annotations":null,"_meta":null}]
    

creating a stdio server

  1. curl -s localhost:4071/v1/mcp/server -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"server_name":"new_stdio","transport":"stdio","command":"python","args":["echo_server.py"]}' | jq -c '{server_name,transport}'
    {"server_name":"new_stdio","transport":"stdio"}
    

UI config

  1. curl -s localhost:4071/.well-known/litellm-ui-config
    {"server_root_path":"","proxy_base_url":"http://localhost:4000","auto_redirect_to_sso":false,"admin_ui_disabled":false,"sso_configured":false,"hide_default_credentials_hint":false,"is_control_plane":false,"workers":[]}
    

flag stored in the database

  1. Not applicable before this PR, since the flag did not exist

flag set in config.yaml

  1. Not applicable before this PR, since the flag did not exist

After (ac4a835)

The cases below ran at ac4a835. The full end-to-end run at 4351da2 is at the end of this section and covers them again. Later commits: 94712b1 names the detail-page banner condition for a lint budget, and 077e1df logs the "stored in the database" warning once per process instead of on every DB config sync (found by Devin QA)

stdio server works

Flag off (env var unset):

  1. Proxy boot log
    LiteLLM:WARNING: stdio_gate.py:28 - MCP server 'echo_stdio' will not start: stdio MCP servers are disabled on this proxy. Set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart to enable them
    
  2. curl -s 'localhost:4071/mcp-rest/tools/list?server_id=ebabfe6d53267166b95d507599c04f10' -H "Authorization: Bearer $KEY" | jq -c '[.tools[].name]'
    []
    
  3. curl -s -w ' HTTP %{http_code}' localhost:4071/mcp-rest/tools/call -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"server_id":"ebabfe6d53267166b95d507599c04f10","name":"echo_stdio-echo","arguments":{"text":"hi"}}'
    {"detail":"stdio MCP servers are disabled on this proxy. Set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart to enable them"} HTTP 403
    
  4. curl -s localhost:4071/v1/mcp/server/ebabfe6d53267166b95d507599c04f10 -H "Authorization: Bearer $KEY" | jq -c '{server_name,status,health_check_error}'
    {"server_name":"echo_stdio","status":"unhealthy","health_check_error":"403: stdio MCP servers are disabled on this proxy. Set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart to enable them"}
    

Flag on (LITELLM_ENABLE_MCP_STDIO=true), no "will not start" lines in the boot log:

  1. Same tools/list call
    ["echo"]
    
  2. Same tools/call
    {"_meta":null,"content":[{"type":"text","text":"echo: hi","annotations":null,"_meta":null}],"structuredContent":{"result":"echo: hi"},"isError":false,"resultType":"complete"} HTTP 200
    
  3. Same server lookup
    {"server_name":"echo_stdio","status":"healthy","health_check_error":null}
    

creating a stdio server

  1. Flag off: curl -s -w ' HTTP %{http_code}' localhost:4071/v1/mcp/server -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"server_name":"stdio_off","transport":"stdio","command":"python","args":["echo_server.py"]}'
    {"detail":[{"type":"value_error","loc":["body"],"msg":"Value error, stdio MCP servers are disabled on this proxy. Set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart to enable them"}]} HTTP 422
    
  2. Flag on, same call with "server_name":"stdio_on"
    {"server_id":"80a3e89a-4cb6-4f87-ad87-d4d7ec90e47d","is_config":false,"server_name":"stdio_on","alias":"stdio_on",...,"transport":"stdio",...} HTTP 200
    

UI config

  1. Flag off: curl -s localhost:4071/.well-known/litellm-ui-config | jq -c '{mcp_stdio_enabled}'
    {"mcp_stdio_enabled":false}
    
  2. Flag on, same call
    {"mcp_stdio_enabled":true}
    

flag stored in the database

  1. Flag off in the process env, try to turn it on through the API: curl -s -w ' HTTP %{http_code}' localhost:4071/config/update -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"environment_variables":{"LITELLM_ENABLE_MCP_STDIO":"true"}}'
    {"message":"Config updated successfully"} HTTP 200
    
  2. curl -s localhost:4071/.well-known/litellm-ui-config | jq -c '{mcp_stdio_enabled}', and again after a restart
    {"mcp_stdio_enabled":false}
    
  3. The same tools/call still returns HTTP 403 with the message, and the proxy log shows
    LiteLLM Proxy:WARNING: proxy_server.py:7570 - Ignoring LITELLM_ENABLE_MCP_STDIO stored in the database. Set it in the proxy's environment instead
    

Without the database guard, the restart in step 2 returned {"mcp_stdio_enabled":true}, which is why it is part of this PR

flag set in config.yaml

  1. Flag off in the process env, config.yaml carries it under environment_variables
    environment_variables:
      LITELLM_ENABLE_MCP_STDIO: "true"
    
  2. Proxy boot log
    LiteLLM Proxy:WARNING: proxy_server.py:6163 - Ignoring LITELLM_ENABLE_MCP_STDIO set in the config file. Set it in the proxy's environment instead
    
  3. curl -s localhost:4071/.well-known/litellm-ui-config | jq -c '{mcp_stdio_enabled}'
    {"mcp_stdio_enabled":false}
    
  4. The same tools/call returns
    {"detail":"stdio MCP servers are disabled on this proxy. Set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart to enable them"} HTTP 403
    

gateway call to a blocked stdio tool (flag off, 4351da2)

  1. MCP tools/call on /mcp/ naming canary_cfg-echo, after initialize
    {"isError":true,"text":"Error: stdio MCP servers are disabled on this proxy. Set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart to enable them"}
    
    Before 315fe64 this said Error: Tool 'echo' not found

DB reload with a row whose updated_at is NULL (flag off, 4 workers, 4351da2)

  1. psql -c "update \"LiteLLM_MCPServerTable\" set updated_at=NULL where server_name='canary_db'", boot with --num_workers 4, wait 3 minutes
  2. grep "will not start" proxy.log | grep -oE "server '[a-z_]+'" | sort | uniq -c
    4 server 'canary_cfg'
    4 server 'canary_db'
    4 server 'http_db'
    4 server 'late_stdio'
    
    That is one per server per worker. Before 315fe64, canary_db logged 70 times in 9.5 minutes

full end-to-end run (4351da2)

What ran and what came back

The stdio test server appends a line to a log file every time it starts, so a spawn count per case shows whether stdio ever ran. A script of 47 customer cases (list, get, create, update, import, test connection, health, REST tools list and call, MCP gateway tools, prompts and resources on /mcp/ and per-server paths, /v1/responses, /v1/chat/completions and /v1/messages with litellm_proxy MCP tools against gpt-5.4-mini and claude-haiku-4-5) ran against three proxies, each on a fresh Postgres database: main at the merge base, this branch with the flag on, and this branch with the flag off

Flag on vs main: every response is identical except mcp_stdio_enabled in ui-config (null on main, true here). Spawn counts match except two adjacent cases where a background health check moved 3 spawns from one case to the next (6 in total on both). The ${X-CANARY-TOKEN} header reaches the stdio env the same way on both

Flag off: 0 spawns across all 47 cases. Every stdio create, update, import and test connection is a 422 with the message, REST calls are a 403, gateway listings leave stdio out, gateway calls return the message, and HTTP servers and the three LLM endpoints behave the same as with the flag on

Transitions on one database: restarting on, off, then on again leaves the MCP rows byte-identical and stdio works again with no repair. With an on and an off proxy sharing the database, the off proxy picks up a stdio server created on the on proxy within 48 seconds, refuses calls to it with 0 spawns and warns once. Booting main on that database afterwards runs both stdio servers. With 4 workers and the flag off, 40 of 40 ui-config reads say false, 60 of 60 calls are 403 and nothing spawns

Access: a key allowed only a stdio server sees no tools and gets the message on a gateway call. A key allowed a stdio and an HTTP server sees only the HTTP tools

Flag sources: only the process env (including a .env file next to the proxy) turns stdio on. config.yaml (direct, lowercase key, os.environ/ reference) and /config/update (at runtime and after a restart, either case) do not. Values true, TRUE, true and true\n turn it on; unset, empty, false, 0, 1, yes, on and True1 do not

The SDK litellm.experimental_mcp_client still starts stdio servers with the flag unset. With the flag on, the MCP Servers page was sampled every 25 ms for the first 10 seconds of load (352 samples) and never showed any disabled text

Unit suites for the MCP directories on main and this branch, with the flag on and off, show no outcome differences (0 failures in all four runs)

Type

🆕 New Feature

Caveats (if any)

Severe

  • Breaking: existing stdio MCP servers stop starting after upgrade
    • Fix: set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart
    • The proxy logs one warning per affected server at boot
    • Docs: BerriAI/litellm-docs PR linked in a comment below

Low

  • Only the exact value true (any case, trimmed) enables stdio; 1 and yes do not
  • Tool listing skips stdio servers quietly, including the single-server REST list
  • The /v1/mcp/server/health endpoint returns status only, the reason is on /v1/mcp/server/{id}
  • The UI treats a missing or still-loading ui-config as stdio enabled, so a proxy with the flag on never flashes the disabled state
  • A .env file next to the proxy counts as the process env and can turn stdio on
  • /v1/responses, /v1/chat/completions and /v1/messages requests that point at a blocked stdio server succeed with no tools offered, so the client sees the model say it has no tool rather than an error
  • Editing an existing stdio server while the flag is off is a 422 unless the edit also switches it to a remote transport
  • A stdio submission to /v1/mcp/server/register is now refused with the flag message instead of "use POST /v1/mcp/server"
  • The toolset picker lists stdio servers with "No tools found", and server_outcomes in the gateway's list meta reports them as ok
  • The stdio tooltip inside the select is mouse only; the banners cover keyboard users
  • Submissions tab cards do not show the "stdio disabled" badge
  • litellm.experimental_mcp_client (SDK) is not gated, only the proxy is
  • tests/mcp_tests and the integration suite now enable the flag; only tests/mcp_tests was run locally (30 passed)

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Note

High Risk
Breaking change: upgrades disable local subprocess MCP unless admins set LITELLM_ENABLE_MCP_STDIO=true and restart; touches security-sensitive command execution and broad MCP/API/UI surfaces.

Overview
stdio MCP servers are now opt-in via process env LITELLM_ENABLE_MCP_STDIO=true (default off). When disabled, stdio servers remain registered but do not spawn processes: tool/prompt/resource listings return empty, tool calls return 403, create/update validation fails, and boot/reload logs a one-time warning per server.

Proxy hardening: LITELLM_ENABLE_MCP_STDIO cannot be turned on through config.yaml environment_variables or DB-stored env vars—only the proxy process environment counts. UI discovery (/.well-known/litellm-ui-config) adds mcp_stdio_enabled so the dashboard can grey out stdio transport, show banners/badges, and explain how to enable it.

CI/tests: Integration and MCP e2e runs set the flag to true so existing stdio coverage keeps working; new unit/UI tests cover the off path and ignored flag sources.

Reviewed by Cursor Bugbot for commit 077e1df. Bugbot is set up for automated code reviews on this repo. Configure here.

stdio MCP servers now only run when the proxy is started with
LITELLM_ENABLE_MCP_STDIO=true. While it is off, existing stdio servers stay
registered but never start: tool listings skip them quietly, direct tool
calls and health checks return a 403 naming the env var, and creating or
updating a stdio server is rejected. The flag is read from the process
environment only, so DB-stored environment_variables cannot turn it on.

The UI reads mcp_stdio_enabled from /.well-known/litellm-ui-config to grey
out the stdio transport, show a banner on stdio forms, and badge stdio
server cards.

BREAKING CHANGE: stdio MCP servers are off by default. Set
LITELLM_ENABLE_MCP_STDIO=true in the proxy environment and restart to keep
using them.
@yuneng-berri

Copy link
Copy Markdown
Contributor Author

Docs for this change, covering the new LITELLM_ENABLE_MCP_STDIO env var and the upgrade note: BerriAI/litellm-docs#1991

@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Disables stdio MCP servers by default, requires explicit environment flag.

The MCP changes appear safe to merge, with no outstanding findings identified.

Summary

The PR disables proxy-hosted stdio MCP servers by default, adds a process-environment opt-in, and updates the management API and dashboard to explain the disabled state. The latest MCP changes route blocked tool calls to the flag guidance and avoid repeated database-reload warnings. Both previous findings are manually resolved; the current code also blocks YAML-sourced enablement and reads availability from the selected worker.

Reviews (3) · Last reviewed commit: "fix(ui): explain on the server detail pa..."

Comment thread litellm/proxy/_experimental/mcp_server/stdio_gate.py
@codspeed

codspeed Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_mcp-stdio-flag (077e1df) with main (5ccb1a1)

Open in CodSpeed

@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…e selected worker

LITELLM_ENABLE_MCP_STDIO set under environment_variables in config.yaml is now skipped like the DB-stored value, so only the process environment can enable stdio. The dashboard reads mcp_stdio_enabled from the proxy it is managing, so a control plane shows each worker's own setting.
@yuneng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review: the config.yaml path and the worker-aware UI flag are fixed in the latest commits

…p the UI unchanged until the flag loads

Prompt, resource and resource-template listings now skip a blocked stdio server at debug level like tool listing does, instead of logging a warning per server on every call. The dashboard only treats stdio as disabled once the proxy explicitly reports mcp_stdio_enabled false, so a proxy with the flag on, or an older one without the field, renders exactly as before with no flicker while loading.
…nce per server

A gateway tools/call naming a blocked stdio server's tool now returns the
LITELLM_ENABLE_MCP_STDIO message instead of "Tool not found".

The "will not start" warning moves out of build_mcp_server_from_table, which
DB reload re-runs on every cycle for rows with a NULL updated_at and which
drafts and test-connection also call. It now fires when a row first enters
the registry or changes transport.
The Overview and MCP Tools tabs showed "No tools available" with no reason
while stdio is disabled. The detail page now shows the same warning banner
as the edit form, and hands off to the form's banner once editing starts.
@yuneng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review: blocked stdio tool calls now return the flag error, and the startup warning fires once per server

Keeps local/no-long-condition-chain within its budget
Comment thread litellm/proxy/_types.py
@devin-ai-integration

Copy link
Copy Markdown
Contributor

QA PASS at tip 94712b17d6, run against merge base a14af739a4 on live proxies (2 instances x 2 workers per leg, shared Postgres) with a real stdio MCP server, an HTTP MCP control server, and real Anthropic calls

At the merge base stdio starts unconditionally. At the tip with the flag unset, stdio servers stay listed but are marked disabled, return no tools, calls get 403, create and update get 422, and HTTP MCP is unaffected. Setting LITELLM_ENABLE_MCP_STDIO=true in the process env restores everything, while the same value in config.yaml or the DB is ignored

Check Before (a14af73) After, flag unset After, LITELLM_ENABLE_MCP_STDIO=true
GET /mcp-rest/tools/list stdio ["echo"] [] ["echo"]
POST /mcp-rest/tools/call stdio 200 echo: hi 403 disabled message 200 echo: hi
HTTP MCP list and call ["shout"], 200 SHOUT: HI ["shout"], 200 SHOUT: HI ["shout"], 200 SHOUT: HI
GET /v1/mcp/server/{id} stdio healthy unhealthy, 403 disabled message healthy
POST /v1/mcp/server stdio 201 422 disabled message 201
/.well-known/litellm-ui-config no field mcp_stdio_enabled: false mcp_stdio_enabled: true
MCP SDK client over /mcp both tools listed and callable only echo_http-shout listed, stdio call isError both tools listed and callable
/v1/responses with MCP tool echo: hi model sees no tools echo: hi
/v1/chat/completions with MCP tool echo: hi model sees no tools echo: hi
/v1/messages with MCP tool not run model sees no tools echo: hi

Both instances of each leg returned identical results

After, flag unset (instance A1, A2 identical)
$ curl -s 'localhost:39987/mcp-rest/tools/list?server_id=ebabfe6d...' -H "Authorization: Bearer $KEY" | jq -c '[.tools[].name]'
[]
$ curl -s -w ' HTTP %{http_code}' localhost:39987/mcp-rest/tools/call -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"server_id":"ebabfe6d...","name":"echo_stdio-echo","arguments":{"text":"hi"}}'
{"detail":"stdio MCP servers are disabled on this proxy. Set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart to enable them"} HTTP 403
$ curl -s -w ' HTTP %{http_code}' localhost:39987/mcp-rest/tools/call ... '{"server_id":"0e5aa629...","name":"echo_http-shout","arguments":{"text":"hi"}}'
{"_meta":null,"content":[{"type":"text","text":"SHOUT: HI",...}],"isError":false,...} HTTP 200
$ curl -s localhost:39987/v1/mcp/server/ebabfe6d... -H "Authorization: Bearer $KEY" | jq -c '{server_name,transport,status,health_check_error}'
{"server_name":"echo_stdio","transport":"stdio","status":"unhealthy","health_check_error":"403: stdio MCP servers are disabled on this proxy. Set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart to enable them"}
$ curl -s -w ' HTTP %{http_code}' localhost:39987/v1/mcp/server ... -d '{"server_name":"new_stdio_after_off_A1","transport":"stdio","command":"python","args":["echo_server.py"],"allow_all_keys":true}'
{"detail":[{"type":"value_error","loc":["body"],"msg":"Value error, stdio MCP servers are disabled on this proxy. Set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart to enable them"}]} HTTP 422
$ python mcp_client.py 39987
list_tools: ['echo_http-shout']
call_tool echo_stdio-echo: isError=True ['Error: stdio MCP servers are disabled on this proxy. Set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart to enable them']
call_tool echo_http-shout: isError=False ['SHOUT: HI']
After, flag set in process env
$ curl -s 'localhost:39987/mcp-rest/tools/list?server_id=ebabfe6d...' ... | jq -c '[.tools[].name]'
["echo"]
$ curl -s -w ' HTTP %{http_code}' localhost:39987/mcp-rest/tools/call ... echo_stdio-echo
{"_meta":null,"content":[{"type":"text","text":"echo: hi",...}],"isError":false,...} HTTP 200
$ curl -s -w ' HTTP %{http_code}' localhost:39987/v1/mcp/server ... '{"server_name":"new_stdio_after_on_A1","transport":"stdio",...}'
{"server_id":"4ab0d762-...","transport":"stdio",...} HTTP 201
$ curl -s localhost:39987/v1/responses ... -d '{"model":"claude-haiku","input":"Call the echo tool with text hi, then reply with exactly what it returned.","tools":[{"type":"mcp","server_label":"litellm","server_url":"litellm_proxy/mcp/echo_stdio","require_approval":"never"}]}'
{"status":"completed","output":[{"type":"message","text":"echo: hi"},{"type":"mcp_tools_fetched",...},{"type":"tool_execution_results","text":"[{\"result\": \"echo: hi\", \"name\": \"echo_stdio-echo\"}]"}]}
$ curl -s localhost:39987/v1/chat/completions ... same MCP tool
{"finish_reason":"stop","content":"The echo tool returned: `echo: hi`"}
$ curl -s localhost:39987/v1/messages ... same MCP tool
{"stop_reason":"end_turn","content":[{"type":"text","text":"echo: hi"}]}
Before (merge base, instance B1, B2 identical)
$ curl -s 'localhost:36553/mcp-rest/tools/list?server_id=ebabfe6d...' ... | jq -c '[.tools[].name]'
["echo"]
$ curl -s -w ' HTTP %{http_code}' localhost:36553/mcp-rest/tools/call ... echo_stdio-echo
{"_meta":null,"content":[{"type":"text","text":"echo: hi",...}],"isError":false,...} HTTP 200
$ curl -s localhost:36553/v1/mcp/server/ebabfe6d... | jq -c '{status}'
{"status":"healthy"}
$ curl -s -w ' HTTP %{http_code}' localhost:36553/v1/mcp/server ... stdio create
{"server_id":"20e4d974-...","transport":"stdio",...} HTTP 201
$ curl -s localhost:36553/.well-known/litellm-ui-config | jq -c '{mcp_stdio_enabled}'
{"mcp_stdio_enabled":null}
$ curl -s localhost:36553/v1/responses ... same MCP tool
{"status":"completed","output":[{"type":"message","text":"echo: hi"},...]}
Config file and DB values are ignored

Instance started with the flag unset in env and environment_variables: {LITELLM_ENABLE_MCP_STDIO: "true"} in config.yaml

LiteLLM Proxy:WARNING: proxy_server.py:6189 - Ignoring LITELLM_ENABLE_MCP_STDIO set in the config file. Set it in the proxy's environment instead
$ curl -s localhost:33071/.well-known/litellm-ui-config | jq -c '{mcp_stdio_enabled}'
{"mcp_stdio_enabled":false}
$ curl -s -w ' HTTP %{http_code}' localhost:33071/mcp-rest/tools/call ... echo_stdio-echo
{"detail":"stdio MCP servers are disabled on this proxy. ..."} HTTP 403

Value written to the DB through the API, checked after a 70s sync on both instances

$ curl -s -w ' HTTP %{http_code}' localhost:39987/config/update ... -d '{"environment_variables":{"LITELLM_ENABLE_MCP_STDIO":"true"}}'
{"message":"Config updated successfully"} HTTP 200
$ curl -s localhost:39987/.well-known/litellm-ui-config | jq -c '{mcp_stdio_enabled}'
{"mcp_stdio_enabled":false}
$ curl -s -w ' HTTP %{http_code}' localhost:39987/mcp-rest/tools/call ... echo_stdio-echo
{"detail":"stdio MCP servers are disabled on this proxy. ..."} HTTP 403
(42245 identical)
Mixed fleet: A1 with flag, A2 without, same DB
# stdio server 4ab0d762 created through A1
$ curl -s 'localhost:42245/mcp-rest/tools/list?server_id=4ab0d762...' | jq -c '[.tools[].name]'
[]
$ curl -s -w ' HTTP %{http_code}' localhost:42245/mcp-rest/tools/call ... new_stdio_after_on_A1-echo
{"detail":"stdio MCP servers are disabled on this proxy. ..."} HTTP 403
$ curl -s -w ' HTTP %{http_code}' -X PUT localhost:42245/v1/mcp/server ... stdio update
{"detail":[{"type":"value_error",...,"msg":"Value error, stdio MCP servers are disabled on this proxy. ..."}]} HTTP 422
$ curl -s 'localhost:39987/mcp-rest/tools/list?server_id=4ab0d762...' | jq -c '[.tools[].name]'
["echo"]
$ curl -s -w ' HTTP %{http_code}' localhost:39987/mcp-rest/tools/call ... new_stdio_after_on_A1-echo
{"content":[{"type":"text","text":"echo: hi",...}],"isError":false,...} HTTP 200
$ curl -s -X PUT localhost:39987/v1/mcp/server ... stdio update | jq -c '{server_name,transport,description}'
{"server_name":"new_stdio_after_on_A1","transport":"stdio","description":"edited"}

Dashboard built from the tip. Flag unset: stdio servers show a stdio disabled chip and the stdio transport option is greyed out with the env var hint. Flag set: stdio servers render healthy with no chip

flag unset, server list
flag unset, stdio transport disabled in create form
flag set, server list

Two non-blocking observations. With the flag unset, Responses, Chat Completions and Messages requests that reference a stdio server succeed with an empty tool list, so the client only sees the model saying it has no tool rather than an explicit error. The DB ignore warning is logged on every DB config sync per worker (27 lines in about 3 minutes on one instance), which may be noisy in production logs

Written by Devin

…g once

The DB config sync re-reads environment_variables on every cycle, so a stored
flag logged the warning on each sync per worker
@yuneng-berri

Copy link
Copy Markdown
Contributor Author

Thanks. 077e1df now logs the DB warning once per process. Empty tool lists for blocked stdio servers are the intended quiet behavior

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor

bugbot run

@yuneng-berri
yuneng-berri merged commit 2b85808 into main Oct 2, 2026
106 of 107 checks passed
@yuneng-berri
yuneng-berri deleted the litellm_mcp-stdio-flag branch October 2, 2026 17:28

@cursor cursor Bot left a comment

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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 077e1df. Configure here.

yuneng-berri added a commit that referenced this pull request Oct 2, 2026
* feat(mcp)!: disable stdio MCP servers by default (#44066)

* feat(mcp)!: disable stdio MCP servers by default

stdio MCP servers now only run when the proxy is started with
LITELLM_ENABLE_MCP_STDIO=true. While it is off, existing stdio servers stay
registered but never start: tool listings skip them quietly, direct tool
calls and health checks return a 403 naming the env var, and creating or
updating a stdio server is rejected. The flag is read from the process
environment only, so DB-stored environment_variables cannot turn it on.

The UI reads mcp_stdio_enabled from /.well-known/litellm-ui-config to grey
out the stdio transport, show a banner on stdio forms, and badge stdio
server cards.

BREAKING CHANGE: stdio MCP servers are off by default. Set
LITELLM_ENABLE_MCP_STDIO=true in the proxy environment and restart to keep
using them.

* fix(mcp): ignore stdio flag from config file and read UI flag from the selected worker

LITELLM_ENABLE_MCP_STDIO set under environment_variables in config.yaml is now skipped like the DB-stored value, so only the process environment can enable stdio. The dashboard reads mcp_stdio_enabled from the proxy it is managing, so a control plane shows each worker's own setting.

* test(mcp): cover non-mapping payloads in the shared transport validator

* fix(mcp): skip blocked stdio servers quietly in every listing and keep the UI unchanged until the flag loads

Prompt, resource and resource-template listings now skip a blocked stdio server at debug level like tool listing does, instead of logging a warning per server on every call. The dashboard only treats stdio as disabled once the proxy explicitly reports mcp_stdio_enabled false, so a proxy with the flag on, or an older one without the field, renders exactly as before with no flicker while loading.

* fix(mcp): route blocked stdio tool calls to the flag error and warn once per server

A gateway tools/call naming a blocked stdio server's tool now returns the
LITELLM_ENABLE_MCP_STDIO message instead of "Tool not found".

The "will not start" warning moves out of build_mcp_server_from_table, which
DB reload re-runs on every cycle for rows with a NULL updated_at and which
drafts and test-connection also call. It now fires when a row first enters
the registry or changes transport.

* fix(ui): explain on the server detail page why a stdio server is inert

The Overview and MCP Tools tabs showed "No tools available" with no reason
while stdio is disabled. The detail page now shows the same warning banner
as the edit form, and hands off to the form's banner once editing starts.

* refactor(ui): name the stdio banner conditions on the server detail page

Keeps local/no-long-condition-chain within its budget

* fix(proxy): log the ignored DB-stored LITELLM_ENABLE_MCP_STDIO warning once

The DB config sync re-reads environment_variables on every cycle, so a stored
flag logged the warning on each sync per worker

(cherry picked from commit 2b85808)

* chore: update Next.js build artifacts (2026-10-02 17:40 UTC, node v24.19.0)
yuneng-berri added a commit that referenced this pull request Oct 2, 2026
* feat(mcp)!: disable stdio MCP servers by default (#44066)

* feat(mcp)!: disable stdio MCP servers by default

stdio MCP servers now only run when the proxy is started with
LITELLM_ENABLE_MCP_STDIO=true. While it is off, existing stdio servers stay
registered but never start: tool listings skip them quietly, direct tool
calls and health checks return a 403 naming the env var, and creating or
updating a stdio server is rejected. The flag is read from the process
environment only, so DB-stored environment_variables cannot turn it on.

The UI reads mcp_stdio_enabled from /.well-known/litellm-ui-config to grey
out the stdio transport, show a banner on stdio forms, and badge stdio
server cards.

BREAKING CHANGE: stdio MCP servers are off by default. Set
LITELLM_ENABLE_MCP_STDIO=true in the proxy environment and restart to keep
using them.

* fix(mcp): ignore stdio flag from config file and read UI flag from the selected worker

LITELLM_ENABLE_MCP_STDIO set under environment_variables in config.yaml is now skipped like the DB-stored value, so only the process environment can enable stdio. The dashboard reads mcp_stdio_enabled from the proxy it is managing, so a control plane shows each worker's own setting.

* test(mcp): cover non-mapping payloads in the shared transport validator

* fix(mcp): skip blocked stdio servers quietly in every listing and keep the UI unchanged until the flag loads

Prompt, resource and resource-template listings now skip a blocked stdio server at debug level like tool listing does, instead of logging a warning per server on every call. The dashboard only treats stdio as disabled once the proxy explicitly reports mcp_stdio_enabled false, so a proxy with the flag on, or an older one without the field, renders exactly as before with no flicker while loading.

* fix(mcp): route blocked stdio tool calls to the flag error and warn once per server

A gateway tools/call naming a blocked stdio server's tool now returns the
LITELLM_ENABLE_MCP_STDIO message instead of "Tool not found".

The "will not start" warning moves out of build_mcp_server_from_table, which
DB reload re-runs on every cycle for rows with a NULL updated_at and which
drafts and test-connection also call. It now fires when a row first enters
the registry or changes transport.

* fix(ui): explain on the server detail page why a stdio server is inert

The Overview and MCP Tools tabs showed "No tools available" with no reason
while stdio is disabled. The detail page now shows the same warning banner
as the edit form, and hands off to the form's banner once editing starts.

* refactor(ui): name the stdio banner conditions on the server detail page

Keeps local/no-long-condition-chain within its budget

* fix(proxy): log the ignored DB-stored LITELLM_ENABLE_MCP_STDIO warning once

The DB config sync re-reads environment_variables on every cycle, so a stored
flag logged the warning on each sync per worker

(cherry picked from commit 2b85808)

* chore(deps): bump pyjwt to 2.15.0

* chore(deps): bump pypdf to 6.19.0

* chore(deps): bump tornado to 6.5.9

* chore(deps): bump urllib3 to 2.8.0

* chore(deps): bump gitpython to 3.1.62

* chore: update Next.js build artifacts (2026-10-02 18:00 UTC, node v24.19.0)

This branch is waiting to be deployed

1 waiting deployment
e2e-changed — 077e1df1 Waiting Oct 2, 2026 by yuneng-berri via oauth #2545
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.

3 participants