garrytan/gbrain

skill catalog reports every tool as unavailable over stdio MCP, and tells the agent not to call them

Open

#3,635 opened on Jul 30, 2026

 (3 comments) (0 reactions) (0 assignees)TypeScript (3,057 forks)github user discovery
fix-neededhelp wantedp1verified-real

Repository metrics

Stars
 (21,368 stars)
PR merge metrics
 (Avg merge 18d 10h) (162 merged PRs in 30d)

Description

Version: 0.42.67.0 (checkout at c6dc0adf, clean) Transport: stdio MCP, registered exactly as docs/mcp/CLAUDE_CODE.md:11 prescribes — claude mcp add gbrain -- gbrain serve

Symptom

On the stdio transport, list_skills returns every skill with usable_tools: [], every declared tool in unavailable_tools, and instructions.available_brain_tools: []. The same payload then instructs the agent (src/core/operations-descriptions.ts:208,224):

"Only call tools in this skill's usable_tools; tools in unavailable_tools are not callable by you on this server." "Do not invent tools — only the tools in usable_tools are callable by you."

An agent that obeys the catalog concludes it cannot use the brain at all.

It is not true

Same server, same session, same minute:

  • whoami{"transport":"stdio","scopes":[]}
  • search, get_health, list_skills → all succeed
  • put_page — a write-scope op the catalog listed as unavailable — succeeded and wrote through to disk: {"status":"created_or_updated","chunks":1,"write_through":{"written":true,"path":"..."}}

For contrast, the CLI path on the same skill reports the truth: gbrain skill brain-opsusable_tools: [search, query, get_page, put_page, add_link, add_timeline_entry, get_backlinks, sync_brain], unavailable_tools: [].

Mechanism

src/core/skill-catalog.ts:295-299

function opCallableByCaller(op: Operation, ctx: OperationContext): boolean {
  if (ctx.remote === false) return true;   // local CLI
  if (op.localOnly) return false;
  return hasScope(ctx.auth?.scopes ?? [], op.scope ?? 'read');
}

The stdio pipe is deliberately remote: true (src/core/operations.ts:339-344) and carries no auth, so ctx.auth?.scopes ?? [] is empty and hasScope([], 'read') is false for every op.

The catalog is the only place that consults scopes on this transport. hasScope has three call sites in the whole repo: OAuth grant issuance, request-time enforcement in src/commands/serve-http.ts:1893 (HTTP only), and skill-catalog.ts:298. There is no scope enforcement in the stdio dispatch path, which is why every op executes regardless.

So the advertisement applies an HTTP authorization model to a transport that has no authorization, and reports the exact inverse of reality.

Suggested fix

Treat the auth-less stdio transport as trusted for the purposes of this advertisement — it already is for dispatch. Something like:

if (ctx.remote === false || ctx.transport === 'stdio') return true;

(OperationContext already carries the 'stdio' transport marker per the note at src/core/operations.ts:339-344.)

Why it matters

This is upstream's own zero-config path — docs/mcp/DEPLOY.md:18-25 and docs/mcp/CLAUDE_CODE.md:11-15, "No server, no tunnel, no token needed". Any agent that follows the catalog's instructions on that path silently loses the entire brain surface.

Contributor guide