{"record":{"id":"ac94769e53291604","repo":"multica-ai/multica","slug":"config-must-not-be-empty","errorCode":null,"errorMessage":"config must not be empty","messagePattern":"config must not be empty","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"error","filePath":"server/internal/handler/workspace_mcp.go","lineNumber":173,"sourceCode":"}\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 == \"\" {\n\t\treturn errors.New(\"name is required\")\n\t}\n\tfor _, r := range name {\n\t\tswitch {\n\t\tcase r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_':\n\t\tdefault:\n\t\t\treturn errors.New(\"name may only contain letters, digits, hyphens, and underscores\")\n\t\t}\n\t}","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/handler/workspace_mcp.go#L155-L191","documentation":"validateWorkspaceMcpServerEntry's third branch: the entry parsed as a JSON object but the object has zero keys ({}). An empty object gives the runtime nothing to mount — no command, URL, or transport — so it is rejected rather than stored as dead config. Same shallow-shape philosophy as the sibling checks.","triggerScenarios":"PUT/POST workspace MCP servers with an entry of {}. Typical when a form row was created but all its fields left blank, or a client sends {servers: {name: {}}} as a placeholder to 'reserve' a name.","commonSituations":"Add-server dialogs that submit the row on Enter before fields are filled; programmatic upserts initializing entries as {} to fill later; template interpolation producing no keys.","solutions":["Omit the server entry until it has at least its transport fields (command or url)","Require a minimal valid object in the form before enabling submit","If deleting, remove the key from the servers map rather than leaving {}"],"exampleFix":"// before\nservers: { github: {} }\n// after\n// omit until ready:\nservers: { } // or\nservers: { github: { command: \"npx\", args: [\"-y\", \"@mcp/github\"] } }","handlingStrategy":"validation","validationCode":"const cleaned = Object.fromEntries(\n  Object.entries(servers).filter(([, e]) => isMcpEntryObject(e) && Object.keys(e).length > 0)\n);","typeGuard":"function isNonEmptyMcpEntry(v: unknown): v is Record<string, unknown> {\n  return typeof v === 'object' && v !== null && !Array.isArray(v) && Object.keys(v).length > 0;\n}","tryCatchPattern":null,"preventionTips":["Don't submit placeholder {} rows from add-server dialogs","Upsert real config in one step instead of reserving names with empty objects","Require at least a command or url field before the row is submittable"],"tags":["validation","mcp","workspace","config"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}