{"record":{"id":"554c983aaba40e5a","repo":"unslothai/unsloth","slug":"local-stdio-mcp-servers-can-only-be-configured-f","errorCode":null,"errorMessage":"Local (stdio) MCP servers can only be configured from the Unsloth UI, not with an API key. Use an http:// or https:// MCP server instead.","messagePattern":"Local \\(stdio\\) MCP servers can only be configured from the Unsloth UI, not with an API key\\. Use an http:// or https:// MCP server instead\\.","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"studio/backend/auth/authentication.py","lineNumber":197,"sourceCode":"    credentials: HTTPAuthorizationCredentials = Depends(security),\n) -> bool:\n    \"\"\"True when the caller used an sk-unsloth API key, not a UI session JWT.\n\n    Lets routes treat programmatic API callers differently from the Unsloth UI\n    (e.g. refuse a teardown the UI would allow).\n    \"\"\"\n    return bool(credentials and credentials.credentials.startswith(API_KEY_PREFIX))\n\n\ndef require_ui_session_for_local_commands(via_api_key: bool) -> None:\n    \"\"\"Refuse an sk-unsloth API key that asks to define a local (stdio) MCP command.\n\n    stdio MCP runs a command on this host as the backend user, outside the\n    python/terminal sandbox, so only a UI session may choose what runs. API keys\n    keep http(s) MCP, and stdio servers the owner already configured.\n    \"\"\"\n    if via_api_key:\n        raise HTTPException(\n            status_code = status.HTTP_403_FORBIDDEN,\n            detail = \"Local (stdio) MCP servers can only be configured from the Unsloth UI, \"\n            \"not with an API key. Use an http:// or https:// MCP server instead.\",\n        )\n\n\nasync def allow_ambient_hf_token(via_api_key: bool = Depends(authenticated_via_api_key)) -> bool:\n    \"\"\"Whether a download this caller starts may fall back to the backend's own HF_TOKEN.\n\n    A UI session already gets the saved token from Settings, so the ambient one grants it\n    nothing new. ``require_ui_session`` refuses an sk-unsloth API key that same token, so it\n    must not reach private repos by naming one in a download instead; it sends its own token\n    in ``X-Unsloth-HF-Token``.\n    \"\"\"\n    return not via_api_key\n\n\nasync def authenticated_via_desktop_jwt(","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/auth/authentication.py#L179-L215","documentation":"HTTP 403 raised by require_ui_session_for_local_commands when a request authenticated with an sk-unsloth API key tries to configure a local (stdio) MCP server. stdio MCP executes an arbitrary command on the host as the backend user, outside the python/terminal sandbox, so the backend restricts that capability to interactive UI sessions; API-key callers may only register http:// or https:// MCP endpoints.","triggerScenarios":"POST/PUT to an MCP-provider configuration endpoint with an sk-unsloth-... bearer token whose payload contains a provider of type 'stdio' (a command + args); automating studio configuration via API key and including a local command MCP server.","commonSituations":"Scripts or CI that provision studio settings with an API key and attempt to register a local MCP wrapper binary; users copying a stdio MCP config (meant for the UI) into an API-driven workflow; misunderstanding that API keys are deliberately scoped away from host command execution.","solutions":["Replace the stdio MCP server with an http:// or https:// MCP endpoint when authenticating via API key (e.g. run the local server yourself and expose it over HTTP).","Or perform the stdio MCP configuration once interactively from the Unsloth UI in a browser session.","If you own the local tool, front it with a small HTTP MCP adapter and point the API-key config at that URL."],"exampleFix":"# before\nmcp_providers=[{\"provider_type\": \"stdio\", \"command\": \"my-mcp\", \"args\": []}]  # via sk-unsloth key\n\n# after\nmcp_providers=[{\"provider_type\": \"http\", \"url\": \"http://127.0.0.1:8000/mcp\"}]  # or configure stdio in the UI","handlingStrategy":"validation","validationCode":"def assert_mcp_config_allowed(providers, via_api_key):\n    if via_api_key and any(p.get('provider_type') == 'stdio' for p in providers):\n        raise ValueError('configure stdio MCP from the UI, or use http(s) MCP with API keys')","typeGuard":"def is_remote_mcp_only(providers: list[dict]) -> bool:\n    return all(p.get('provider_type') in ('http', 'https', 'sse', 'streamable_http') for p in providers)","tryCatchPattern":"try:\n    save_mcp_providers(providers)\nexcept HTTPException as e:\n    if e.status_code == 403 and 'stdio' in e.detail:\n        switch_to_http_mcp_or_use_ui()","preventionTips":["Never put stdio MCP definitions in API-key-driven configuration payloads.","Gate MCP config UIs on the auth method before submit.","Run local MCP tools behind an HTTP endpoint if automation needs them."],"tags":["authorization","mcp","http-403","api-key","studio"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}