unslothai/unsloth · warning · HTTPException
No fields to update
Error message
No fields to update
What it means
400 from the update endpoint when _changes_from_payload returns an empty dict — the PUT body contained none of the recognized mutable fields (display_name, url, headers, is_enabled, use_oauth) or contained them only as absent keys. The route treats a no-op update as a client error rather than writing nothing and returning 200.
Source
Thrown at studio/backend/routes/mcp_servers.py:216
# stdio is OAuth-less: drop a stale OAuth flag when switching to a command.
if "url" in changes and is_stdio(changes["url"]):
changes["use_oauth"] = False
return changes
@router.put("/{server_id}", response_model = McpServerResponse)
async def update_mcp_server(
server_id: str,
payload: McpServerUpdate,
current_subject: str = Depends(get_current_subject),
via_api_key: ViaApiKey = False,
):
old = mcp_servers_db.get_server(server_id)
if not old:
raise HTTPException(status_code = 404, detail = "MCP server not found")
changes = _changes_from_payload(payload)
if not changes:
raise HTTPException(status_code = 400, detail = "No fields to update")
# Both directions, so an API key can neither repoint an http row at a command
# nor edit a stdio row's env/name/enabled flag. Before every side effect, so a
# refusal leaves the row, its OAuth tokens, cache and sessions untouched.
if is_stdio(old["url"]) or is_stdio(changes.get("url", old["url"])):
require_ui_session_for_local_commands(via_api_key)
# headers == HTTP headers (remote) or env vars (stdio). On a transport-type
# switch with no new headers, drop the old ones so env secrets aren't
# re-sent as HTTP headers (or vice versa).
if (
"url" in changes
and is_stdio(changes["url"]) != is_stdio(old["url"])
and "headers_json" not in changes
):
changes["headers_json"] = None
# Clear persisted OAuth tokens when the URL changes or OAuth is disabled;
# fastmcp keys tokens by URL and would otherwise let a re-pointed server
# silently inherit the old account's credentials.
if bool(old.get("use_oauth")) and (View on GitHub (pinned to 203007d190)
Solutions
- Include at least one valid field: display_name, url, headers, is_enabled, or use_oauth.
- Check for field-name typos — the API uses snake_case exactly as documented.
- If you only wanted to verify the row, use GET instead of an empty PUT.
Example fix
// before
await api.put(`/api/mcp-servers/${id}`, {}); // 400 'No fields to update'
// after
await api.put(`/api/mcp-servers/${id}`, {is_enabled: row.is_enabled}); // real field
// or just read: await api.get(`/api/mcp-servers/${id}`); Defensive patterns
Strategy: validation
Validate before calling
const EDITABLE = new Set(['display_name','url','headers','is_enabled','use_oauth']);
const keys = Object.keys(body).filter(k => EDITABLE.has(k));
if (keys.length === 0) throw new Error('PUT needs at least one editable field'); Type guard
function hasEditableField(body) { return ['display_name','url','headers','is_enabled','use_oauth'].some(k => k in body); } Prevention
- Use exactly the snake_case field names the API documents.
- Use GET when you only want to read; PUT requires a real change.
When it happens
Trigger: PUT /{server_id} with an empty JSON object {}, with only unknown/typo'd field names like {"displayName": ...}, or with a fully-undefined payload after client-side key stripping.
Common situations: Field-name mismatches between client and API (camelCase vs snake_case), PATCH semantics expected but the endpoint requires at least one field, or forms that strip unchanged values and end up sending {}.
Related errors
- url must not be empty
- command must not be empty
- Enter an http(s):// URL, or a local command whose first toke
- MCP server address must start with http:// or https:// (for
- url is missing a host
AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15).
Data as JSON: /api/errors/97d6da23a1872663.
Report an issue: GitHub.