odysseus-dev/odysseus · error · HTTPException
disabled must be a list of tool names
Error message
disabled must be a list of tool names
What it means
On PATCH /servers/{server_id}/tools, after the server row is found, the JSON body's 'disabled' field (defaulting to []) is type-checked; anything that is not a Python list — a string, object, number, null with wrong shape — raises 400 with this message. Note the route reads the body via await request.json(), so a non-JSON body fails earlier at the framework level.
Source
Thrown at routes/mcp/mcp_routes.py:417
return server_tools
@router.patch("/servers/{server_id}/tools")
async def update_disabled_tools(server_id: str, request: Request):
"""Bulk update disabled tools list for a server.
Expects JSON body: {"disabled": ["tool_name_1", "tool_name_2"]}
"""
require_admin(request)
db = SessionLocal()
try:
srv = db.query(McpServer).filter(McpServer.id == server_id).first()
if not srv:
raise HTTPException(404, "Server not found")
body = await request.json()
disabled = body.get("disabled", [])
if not isinstance(disabled, list):
raise HTTPException(400, "disabled must be a list of tool names")
srv.disabled_tools = json.dumps(disabled) if disabled else None
db.commit()
return {"id": server_id, "disabled_count": len(disabled)}
finally:
db.close()
# ── OAuth flow for Google MCP servers ──────────────────────────
@router.get("/oauth/authorize/{server_id}")
def oauth_authorize(server_id: str, request: Request):
"""Show OAuth authorization page with Google sign-in link."""
require_admin(request)
db = SessionLocal()
try:
srv = db.query(McpServer).filter(McpServer.id == server_id).first()
if not srv:View on GitHub (pinned to f9235ebbf1)
Solutions
- Send disabled as a JSON array of tool-name strings: {"disabled": ["search", "fetch"]}.
- Ensure the client posts with Content-Type: application/json and the body is valid JSON.
- Pull tool names from GET /servers/{id}/tools (the 'name' field) so entries match exactly.
Example fix
# before
requests.patch(url, json={"disabled": "search"})
# after
requests.patch(url, json={"disabled": ["search"]}) Defensive patterns
Strategy: type-guard
Validate before calling
def is_tool_name_list(v) -> bool:
return isinstance(v, list) and all(isinstance(x, str) for x in v) Type guard
def is_disabled_body(body: unknown) -> bool:
# TypeScript
def isDisabledBody(body: unknown): body is { disabled: string[] } {
return typeof body === "object" && body !== null &&
Array.isArray((body as any).disabled) &&
(body as any).disabled.every((x: unknown) => typeof x === "string");
} Prevention
- Serialize arrays as JSON arrays, never join to strings.
- Run is_tool_name_list on the payload before sending.
When it happens
Trigger: Sending {"disabled": "search,fetch"} (comma string instead of array); {"disabled": {"search": true}}; {"disabled": null} handled by default [] but explicit wrong types fail; missing Content-Type: application/json.
Common situations: Frontend serializing an array as a string; form-style clients posting urlencoded data to a JSON route; hand-crafted curl without quotes producing a string.
Related errors
- Email account not found for selector {account!r}. Available
- Invalid JSON
- Expected a JSON object
- Invalid JSON: {e}
- Invalid JSON
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/8906aae4d2296a99.
Report an issue: GitHub.