sipeed/picoclaw · error

config validation failed: %w

Error message

config validation failed: %w

What it means

The normalized config document is validated against the embedded MCP schema before every save (helpers.go:154-161). It fails when a server entry violates the contract: missing `enabled`, having neither `command` nor `url`, containing an unknown property (servers set additionalProperties:false), or a `type` outside stdio/http/sse. Most often reached through `picoclaw mcp edit` saving hand-made changes.

Source

Thrown at cmd/picoclaw/internal/mcp/helpers.go:160

		clone.Tools.MCP.Servers[name] = server
	}

	return &clone
}

func validateConfigDocument(data []byte) error {
	var instance map[string]any
	if err := json.Unmarshal(data, &instance); err != nil {
		return fmt.Errorf("failed to decode serialized config: %w", err)
	}

	schema, err := loadMCPConfigSchema()
	if err != nil {
		return fmt.Errorf("failed to load MCP config schema: %w", err)
	}

	if err := schema.Validate(instance); err != nil {
		return fmt.Errorf("config validation failed: %w", err)
	}

	return nil
}

func loadMCPConfigSchema() (*jsonschema.Resolved, error) {
	mcpConfigSchemaOnce.Do(func() {
		var schema jsonschema.Schema
		if err := json.Unmarshal([]byte(mcpConfigSchemaJSON), &schema); err != nil {
			errMcpConfigSchema = err
			return
		}
		mcpConfigSchema, errMcpConfigSchema = schema.Resolve(nil)
	})

	return mcpConfigSchema, errMcpConfigSchema
}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Give every server entry "enabled" plus exactly one of "command" (stdio) or "url" (http/sse)
  2. Restrict each server entry to the known keys: enabled, deferred, command, args, env, env_file, type, url, headers
  3. Set "type" only to stdio, http, or sse, or omit it

Example fix

// before
"servers": { "s": { "command": "./srv", "timeout": 30 } }
// after
"servers": { "s": { "enabled": true, "command": "./srv" } }
Defensive patterns

Strategy: validation

Validate before calling

func validServerEntry(s map[string]any) error {
	if _, ok := s["enabled"]; !ok {
		return fmt.Errorf("server entry missing 'enabled'")
	}
	_, hasCmd := s["command"]
	_, hasURL := s["url"]
	if hasCmd == hasURL { // neither or both
		return fmt.Errorf("server entry needs exactly one of 'command' or 'url'")
	}
	if t, ok := s["type"].(string); ok {
		switch t {
		case "stdio", "http", "sse":
		default:
			return fmt.Errorf("type %q not in stdio/http/sse", t)
		}
	}
	return nil
}

Try / catch

if err := saveCmd.Execute(); err != nil {
	if strings.Contains(err.Error(), "config validation failed") {
		// the wrapped jsonschema error names the offending pointer/property;
		// print it verbatim to locate the bad server entry
		fmt.Fprintf(os.Stderr, "%v\n", errors.Unwrap(err))
	}
}

Prevention

When it happens

Trigger: Editing the config to remove enabled:true from a server; deleting both command and url; adding an undocumented key like timeout or cwd inside a server entry; setting type: grpc.

Common situations: Porting entries from other tools' MCP config formats (Claude Desktop mcpServers, Claude Code .mcp.json) that allow extra keys; pruning fields too aggressively; typos in key names.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/66d4e84614c4bd5d. Report an issue: GitHub.