{"record":{"id":"033cf8ff8effcc06","repo":"multica-ai/multica","slug":"config-must-be-a-json-object","errorCode":null,"errorMessage":"config must be a JSON object","messagePattern":"config must be a JSON object","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"error","filePath":"server/internal/handler/workspace_mcp.go","lineNumber":164,"sourceCode":"\t\tservers, err := unmarshalServerMap(doc[container])\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"%s: %w\", container, err)\n\t\t}\n\t\tfor name := range servers {\n\t\t\tnames[name] = struct{}{}\n\t\t}\n\t}\n\treturn doc, names, nil\n}\n\n// validateWorkspaceMcpServerEntry checks the shape of ONE server entry before\n// it is stored. Deliberately shallow — shape only, never the contents, which\n// are runtime-specific and carry secrets we do not want to inspect or echo\n// back in an error.\nfunc validateWorkspaceMcpServerEntry(raw json.RawMessage) error {\n\ttrimmed := bytes.TrimSpace(raw)\n\tif len(trimmed) == 0 {\n\t\treturn errors.New(\"config must be a JSON object\")\n\t}\n\tvar entry map[string]json.RawMessage\n\tif err := json.Unmarshal(trimmed, &entry); err != nil {\n\t\t// Never wrap: the underlying error can echo fragments of an entry that\n\t\t// routinely embeds API tokens.\n\t\treturn errors.New(\"config must be a JSON object\")\n\t}\n\tif len(entry) == 0 {\n\t\treturn errors.New(\"config must not be empty\")\n\t}\n\treturn nil\n}\n\n// validateWorkspaceMcpServerName checks a server name. The name is what the\n// runtime mounts the server under and what an agent's own config collides\n// with, so it follows the same rule the agent settings dialog enforces.\nfunc validateWorkspaceMcpServerName(name string) error {\n\tif name == \"\" {","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/handler/workspace_mcp.go#L146-L182","documentation":"validateWorkspaceMcpServerEntry checks the shape of one MCP server config entry before storage and rejects input that is empty after trimming (empty bytes, whitespace-only raw JSON). The validation is deliberately shallow — shape only — because entries carry runtime secrets the server refuses to echo in errors. This is the empty-raw branch, distinct from the unparseable branch at line 170.","triggerScenarios":"PUT/POST workspace MCP servers with a servers map where one entry is \"\", \" \", or null-ish raw bytes — commonly a client sending {\"servers\": {\"foo\": \"\"}} because the UI added an empty card that was never filled.","commonSituations":"Dynamic form rows that submit placeholder empty entries; template-driven configs where an optional server variable interpolates to nothing; JSON built with conditional spreads that leave empty strings.","solutions":["Remove empty server entries before submitting: filter entries whose value trims to empty","Make the client form require at least a minimal object ({\"command\": \"...\"}) per row before submit","If a server should be disabled, delete its entry rather than blanking it"],"exampleFix":"// before\nservers: { github: \"\" }\n// after\nservers: { github: { command: \"npx\", args: [\"-y\", \"@modelcontextprotocol/server-github\"] } }","handlingStrategy":"validation","validationCode":"for (const [name, entry] of Object.entries(servers)) {\n  const raw = typeof entry === 'string' ? entry.trim() : entry;\n  if (!raw || (typeof raw === 'string' && raw === '')) delete servers[name];\n}","typeGuard":"function isNonEmptyJson(entry: unknown): boolean {\n  return entry !== null && entry !== undefined\n    && !(typeof entry === 'string' && entry.trim() === '');\n}","tryCatchPattern":null,"preventionTips":["Filter empty server rows before submit","Make empty form rows local-only until minimally filled","Blank an entry by removing the key, not by emptying it"],"tags":["validation","mcp","workspace","config"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}