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:
- Delete all groups ->
registry.json becomes {"groups": []}.
serve starts (or restarts) -> MCP init fails on the unmarshal.
- Register a group again and index it fully — graph is queriable,
grafel status
reports it correctly.
- MCP client reconnects (twice) -> still 1 tool. The bridge reconnects, but
serve's MCP server is never re-initialised.
- 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
- 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.
- 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.
- 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
Summary
Deleting every registered group writes a valid but empty registry
(
{"version":1,"groups":[]}).Registry.UnmarshalJSONrejects it, MCP serverinit 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
Root cause
internal/mcp/state.go:93:{"groups": []}unmarshals into the CLIrawRegsuccessfully, butlen(raw.Groups) > 0is false, so the guard falls through to the legacy-mapbranch — which then tries to unmarshal a JSON array into
map[string]RegistryGroupand hard-errors (state.go:122-124).The
len > 0check conflates "not the CLI format" with "CLI format, zero groups".An empty registry is a legitimate, expected state — it is exactly what
grafel deleteof 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:registry.jsonbecomes{"groups": []}.servestarts (or restarts) -> MCP init fails on the unmarshal.grafel statusreports it correctly.
serve's MCP server is never re-initialised.launchctl kickstart -k com.grafel.daemonrecovers it.Downstream, this is severe for agent sessions: once the toolset degrades to the
sentinel mid-session, every prior
grafel_*tool reference in the conversationbecomes 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
len(raw.Groups) > 0guard. Discriminate on the JSON shape ofgroups(array vs object) rather than on element count — e.g. decodegroupsinto
json.RawMessagefirst and branch on whether it starts with[or{.An empty array must yield
r.Groups = map[string]RegistryGroup{}andnilerror.
reloadBeforeCallalready reloads theregistry lazily; init should either retry on the next call or degrade to an
empty-registry sentinel rather than latching a permanent
mcp server unavailable.{"version":1,"groups":[]}throughRegistry.UnmarshalJSONand assert no error + empty map; plus an end-to-endtest that delete-all -> re-register ->
tools/listreturns the full catalogwithout a process restart.
Environment
v0.1.9-17-g1787638c6, darwin/arm64Related
one is not fixed by a client reconnect.