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

  1. Send disabled as a JSON array of tool-name strings: {"disabled": ["search", "fetch"]}.
  2. Ensure the client posts with Content-Type: application/json and the body is valid JSON.
  3. 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

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


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/8906aae4d2296a99. Report an issue: GitHub.