Skip to content

Empty registry {"groups":[]} fails Registry.UnmarshalJSON — bricks MCP to 1-tool sentinel until daemon restart #5932

Description

@cajasmota

Summary

Deleting every registered group writes a valid but empty registry
({"version":1,"groups":[]}). Registry.UnmarshalJSON rejects it, MCP server
init fails, and the MCP stays stuck exposing only the 1-tool sentinel — for the
entire life of the serve process
. Reconnecting the MCP client does not recover
it; only a daemon restart does.

Error

mcp server unavailable: mcp server init: load registry: registry: unmarshal registry:
invalid format (neither CLI array nor legacy map):
json: cannot unmarshal array into Go struct field legacyReg.groups of type map[string]mcp.RegistryGroup

Root cause

internal/mcp/state.go:93:

if err := json.Unmarshal(data, &raw); err == nil && len(raw.Groups) > 0 {
    // ... CLI array format
    return nil
}
// falls through to legacy map format

{"groups": []} unmarshals into the CLI rawReg successfully, but
len(raw.Groups) > 0 is false, so the guard falls through to the legacy-map
branch — which then tries to unmarshal a JSON array into
map[string]RegistryGroup and hard-errors (state.go:122-124).

The len > 0 check conflates "not the CLI format" with "CLI format, zero groups".
An empty registry is a legitimate, expected state — it is exactly what
grafel delete of the last group produces.

Why it is worse than a transient error

The failure happens at MCP server init, and the resulting error state is sticky
for the lifetime of serve. Observed sequence:

  1. Delete all groups -> registry.json becomes {"groups": []}.
  2. serve starts (or restarts) -> MCP init fails on the unmarshal.
  3. Register a group again and index it fully — graph is queriable, grafel status
    reports it correctly.
  4. MCP client reconnects (twice) -> still 1 tool. The bridge reconnects, but
    serve's MCP server is never re-initialised.
  5. Only launchctl kickstart -k com.grafel.daemon recovers it.

Downstream, this is severe for agent sessions: once the toolset degrades to the
sentinel mid-session, every prior grafel_* tool reference in the conversation
becomes invalid and the API rejects the whole request with
400 Tool reference 'mcp__grafel__grafel_cross_links' not found in available tools.
That renders the session unrecoverable — it cannot be continued at all, only
abandoned. This actually happened and cost a long-running session.

Suggested fix

  1. Drop the len(raw.Groups) > 0 guard. Discriminate on the JSON shape of
    groups (array vs object) rather than on element count — e.g. decode groups
    into json.RawMessage first and branch on whether it starts with [ or {.
    An empty array must yield r.Groups = map[string]RegistryGroup{} and nil
    error.
  2. Make MCP init failure non-sticky. reloadBeforeCall already reloads the
    registry lazily; init should either retry on the next call or degrade to an
    empty-registry sentinel rather than latching a permanent
    mcp server unavailable.
  3. Regression test: round-trip {"version":1,"groups":[]} through
    Registry.UnmarshalJSON and assert no error + empty map; plus an end-to-end
    test that delete-all -> re-register -> tools/list returns the full catalog
    without a process restart.

Environment

  • v0.1.9-17-g1787638c6, darwin/arm64

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions