multica-ai/multica · error

config must be a JSON object

Error message

config must be a JSON object

What it means

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.

Source

Thrown at server/internal/handler/workspace_mcp.go:164

		servers, err := unmarshalServerMap(doc[container])
		if err != nil {
			return nil, nil, fmt.Errorf("%s: %w", container, err)
		}
		for name := range servers {
			names[name] = struct{}{}
		}
	}
	return doc, names, nil
}

// validateWorkspaceMcpServerEntry checks the shape of ONE server entry before
// it is stored. Deliberately shallow — shape only, never the contents, which
// are runtime-specific and carry secrets we do not want to inspect or echo
// back in an error.
func validateWorkspaceMcpServerEntry(raw json.RawMessage) error {
	trimmed := bytes.TrimSpace(raw)
	if len(trimmed) == 0 {
		return errors.New("config must be a JSON object")
	}
	var entry map[string]json.RawMessage
	if err := json.Unmarshal(trimmed, &entry); err != nil {
		// Never wrap: the underlying error can echo fragments of an entry that
		// routinely embeds API tokens.
		return errors.New("config must be a JSON object")
	}
	if len(entry) == 0 {
		return errors.New("config must not be empty")
	}
	return nil
}

// validateWorkspaceMcpServerName checks a server name. The name is what the
// runtime mounts the server under and what an agent's own config collides
// with, so it follows the same rule the agent settings dialog enforces.
func validateWorkspaceMcpServerName(name string) error {
	if name == "" {

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Remove empty server entries before submitting: filter entries whose value trims to empty
  2. Make the client form require at least a minimal object ({"command": "..."}) per row before submit
  3. If a server should be disabled, delete its entry rather than blanking it

Example fix

// before
servers: { github: "" }
// after
servers: { github: { command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] } }
Defensive patterns

Strategy: validation

Validate before calling

for (const [name, entry] of Object.entries(servers)) {
  const raw = typeof entry === 'string' ? entry.trim() : entry;
  if (!raw || (typeof raw === 'string' && raw === '')) delete servers[name];
}

Type guard

function isNonEmptyJson(entry: unknown): boolean {
  return entry !== null && entry !== undefined
    && !(typeof entry === 'string' && entry.trim() === '');
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/033cf8ff8effcc06. Report an issue: GitHub.