{"record":{"id":"8906aae4d2296a99","repo":"odysseus-dev/odysseus","slug":"disabled-must-be-a-list-of-tool-names","errorCode":null,"errorMessage":"disabled must be a list of tool names","messagePattern":"disabled must be a list of tool names","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/mcp/mcp_routes.py","lineNumber":417,"sourceCode":"        return server_tools\n\n    @router.patch(\"/servers/{server_id}/tools\")\n    async def update_disabled_tools(server_id: str, request: Request):\n        \"\"\"Bulk update disabled tools list for a server.\n\n        Expects JSON body: {\"disabled\": [\"tool_name_1\", \"tool_name_2\"]}\n        \"\"\"\n        require_admin(request)\n        db = SessionLocal()\n        try:\n            srv = db.query(McpServer).filter(McpServer.id == server_id).first()\n            if not srv:\n                raise HTTPException(404, \"Server not found\")\n\n            body = await request.json()\n            disabled = body.get(\"disabled\", [])\n            if not isinstance(disabled, list):\n                raise HTTPException(400, \"disabled must be a list of tool names\")\n\n            srv.disabled_tools = json.dumps(disabled) if disabled else None\n            db.commit()\n\n            return {\"id\": server_id, \"disabled_count\": len(disabled)}\n        finally:\n            db.close()\n\n    # ── OAuth flow for Google MCP servers ──────────────────────────\n\n    @router.get(\"/oauth/authorize/{server_id}\")\n    def oauth_authorize(server_id: str, request: Request):\n        \"\"\"Show OAuth authorization page with Google sign-in link.\"\"\"\n        require_admin(request)\n        db = SessionLocal()\n        try:\n            srv = db.query(McpServer).filter(McpServer.id == server_id).first()\n            if not srv:","sourceCodeStart":399,"sourceCodeEnd":435,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/mcp/mcp_routes.py#L399-L435","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nrequests.patch(url, json={\"disabled\": \"search\"})\n\n# after\nrequests.patch(url, json={\"disabled\": [\"search\"]})","handlingStrategy":"type-guard","validationCode":"def is_tool_name_list(v) -> bool:\n    return isinstance(v, list) and all(isinstance(x, str) for x in v)","typeGuard":"def is_disabled_body(body: unknown) -> bool:\n    # TypeScript\ndef isDisabledBody(body: unknown): body is { disabled: string[] } {\n  return typeof body === \"object\" && body !== null &&\n    Array.isArray((body as any).disabled) &&\n    (body as any).disabled.every((x: unknown) => typeof x === \"string\");\n}","tryCatchPattern":null,"preventionTips":["Serialize arrays as JSON arrays, never join to strings.","Run is_tool_name_list on the payload before sending."],"tags":["mcp","validation","json","tools"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}