git:20261007.de193df to git:20261009.f9cad52

11 added, 8 removed. Audit A to A.

---
name: bifrost-debug
description: "Diagnose why a Bifrost gateway, memory injection, or skill discovery isn't working in Claude Code or Claude Desktop. Triggers on 'bifrost not working', 'mcp not connecting', 'memory not injecting', 'skill_search failing', '401/403 from bifrost', 'bifrost debug', 'gateway unreachable', 'mcp_registration_failed', 'desktop connector failing', 'oauth error', 'incompatible auth server', 'dynamic client registration', 'trusted hosts'."
---
# Bifrost Diagnostics
Work through this decision tree to isolate and fix the problem.
## 1. Check credentials are set (shell env, or plugin config)
On the CLI:
```bash
echo "URL set: ${BIFROST_URL:+yes}${BIFROST_URL:-NO — missing}"
echo "VK set: ${BIFROST_VK:+yes}${BIFROST_VK:-NO — missing}"
```
If missing: `export BIFROST_URL=https://<your-gateway-host>/mcp` and
`export BIFROST_VK=vk_<your-key>` (add both to `~/.zshrc` or `~/.bashrc`).
On Desktop and claude.ai there is no shell, so this check does not apply.
Credentials come from the plugin's install-time config values
(`gateway_url`, `virtual_key`, and the opt-in `auto_login`). Run `/plugin configure` to
view or change them.
Symptom if the key is wrong/missing: 401 or 403 from every bifrost tool call.
## 2. Check the bifrost MCP server is registered
```bash
claude mcp get bifrost 2>/dev/null || echo 'bifrost: MISSING at user scope'
```
The server can come from either the plugin's shipped `.mcp.json` (plugin
installed + enabled → shows up in `/mcp`) or a user-scope registration.
If neither: run `/bifrost-mcp-setup` for manual wiring instructions.
Then restart Claude Code for CC to pick up the change.
## 3. Check gateway reachability
```bash
curl -s -o /dev/null -w "%{http_code}" \
-H "x-bf-vk: ${BIFROST_VK}" \
"${BIFROST_URL}"
```
Expected: 200 or 405 (OPTIONS probe). 401 → bad VK. Timeout / connection refused → gateway down or `BIFROST_URL` wrong (contact the gateway operator).
## 4. Check the gateway's memory MCP tools (for agent-driven memory)
Run `/mcp` and look for a memory server under `bifrost`. If present, test it (`<prefix>` is `mcp__plugin_bifrost-plugin_bifrost__` from the plugin, `mcp__claude_ai_luca_Bifrost__` from the claude.ai connector, `mcp__bifrost__` from a hand-added server):
```
<prefix><memory-server>-search("test connection")
```
If the tool is not found, the gateway exposes no memory server — memory calls will
simply not be available. This does not affect skill discovery or gateway connectivity.
## 5. Check the plugin is installed and enabled
The hooks ship inside the plugin (via `hooks/hooks.json`), so they are managed by
the CC plugin subsystem — they do NOT appear in `~/.claude/settings.json`. Verify
the plugin itself instead:
```bash
# Is bifrost-plugin present in the CC plugins dir?
find ~/.claude/plugins -maxdepth 4 -type d -name 'bifrost-plugin*' 2>/dev/null
# Is it enabled? (enabledPlugins lives in ~/.claude.json)
node -e "
const fs=require('fs'), os=require('os');
let cfg={};
try { cfg=JSON.parse(fs.readFileSync(os.homedir()+'/.claude.json','utf8')); } catch {}
const ep=JSON.stringify(cfg.enabledPlugins||cfg.plugins||{});
console.log('bifrost-plugin enabled:', ep.includes('bifrost-plugin') ? 'yes' : 'no / unknown');
"
```
If the plugin is missing or disabled: re-install / re-enable it via `/plugin`
(or `/plugin install bifrost-plugin`), then restart Claude Code so the hooks load.
## 6. Test skill_search directly
If your gateway exposes a skill server, inside Claude Code call:
```
<prefix><skills-server>-skill_search("test connection")
```
- Returns results → MCP and skill routing are working.
- Tool not found → bifrost MCP server not loaded in this session (restart CC), or your gateway has no skill server.
- 401/403 → re-check VK (step 1).
## 7. Tool schema is stale after an upstream MCP upgrade
Suspect a stale gateway catalog when either of these happens:
- a tool call fails with `Missing required argument` even though the call matches the
schema exposed to the client; or
- the gateway advertises a schema that differs from what the upstream MCP server now
accepts.
Bifrost publishes the tool catalog it captured when the **gateway process started**.
Restarting or upgrading an upstream MCP server does not refresh that published
snapshot.
### Diagnose
Compare the two catalog views:
1. `GET /api/mcp/clients` through the admin API reads the live upstream client and
shows the upstream's current schema.
2. Send a JSON-RPC `tools/list` POST to the public MCP endpoint with the `x-bf-vk`
header. This is the schema Bifrost is actually advertising to MCP callers.
If the admin response is newer than `tools/list`, the gateway catalog is stale.
These do **not** refresh that snapshot:
- reconnecting inside Claude Code with `/mcp` — the client only reads the same stale
gateway catalog again;
- `POST /api/mcp/client/{uuid}/reconnect` — it may return
`{"message":"MCP client reconnected successfully"}`, but it only re-establishes the
upstream transport. Use the UUID form when diagnosing transport problems; the
name-based variant may return 500;
- `tool_sync_interval` — it cannot repair the published snapshot when
`config_mcp_clients.discovered_tools_json` is empty, as it was in the observed
failure even though the row had `updated_at = 2026-06-30`.
**Fix:** restart the Bifrost gateway process, then repeat the public `tools/list` call
and confirm that it matches the admin view.
During the stale window, calls can still succeed if you pass the upstream's new
arguments explicitly: the upstream server validates the call, not the gateway's
cached catalog.
## 8. The memory server is older than luca-memory v0.42
**This plugin requires luca-memory v0.42 or later.** v0.42 was a breaking release with
no compatibility aliases, and the plugin sends only the v0.42 argument shape. Against an
older server every memory call is rejected, so session-start memory injection goes
silently empty and the corpus size line disappears.
Tell the versions apart from the tool list alone — the memory server sits upstream of
the gateway and advertises no version string. v0.42 exposes exactly three memory tools
(`memory_store`, `memory_search`, `memory_call`); earlier versions expose fourteen.
These signals also mean the server predates v0.42:
- `memory_search` rejects `limit` or `filters` (it wants `k` and a flat `wing`).
- `memory_store` returns `{"status":"pending","candidate_id":"..."}` — the old
collective-review staging, removed in v0.42 along with `MEMORY_DEPLOYMENT_MODE`, the
candidate/voting ledger and the `x-bifrost-vk-id` identity header.
- `memory_store` fails demanding a trusted Bifrost VK identity.
- `memory_stats` / `memory_get_body` / `memory_list_subjects` exist as their own tools.
**Fix:** upgrade the upstream luca-memory to v0.42 or later, then restart the Bifrost
gateway process so its published tool catalog picks up the new schema (see section 7 —
a restart of the upstream alone does not refresh the catalog).
Do **not** work around a v0.40 identity-header failure by adding `x-bifrost-vk-id` to
Bifrost's `allowed_extra_headers`. That mechanism forwards a header the caller already
sent, so allowlisting it lets any caller assert any identity. Bifrost knows the
validated VK ID server-side but does not copy it into the MCP extra headers. Upgrading
removes the gate entirely and is the real fix.
## 9. Claude Desktop OAuth does not complete
A virtual key is the one auth path verified fully working on Desktop, on every
- tab, with no shell environment at all. Leaving `virtual_key` blank at install
- falls back to OAuth against the identity provider named in the plugin's OAuth
- config (`idms.nexenio.com/realms/nexenio`), and that path has two known
- identity-provider-side failures before login even starts.
+ tab, with no shell environment at all. Since 1.9.0 the plugin ships no OAuth
+ config, so a blank `virtual_key` no longer falls back to OAuth. The blank-key
+ paths are `auto_login` (browser sign-in, key cached locally) or the claude.ai
+ org connector. The OAuth failures below show up on the connector path and on a
+ hand-added server that uses OAuth against the identity provider
+ (`idms.nexenio.com/realms/nexenio`), which has two known identity-provider-side
+ failures before login even starts.
- 1. **Check the resource-server side of discovery** (this part works):
+ 1. **Check the resource-server side of discovery** (diagnostic only; this part works):
```bash
curl -s https://bifrost.culture4.life/.well-known/oauth-protected-resource
# → 200, authorization_servers: ["https://idms.nexenio.com/realms/nexenio"]
curl -si https://bifrost.culture4.life/mcp | grep -i www-authenticate
# → Bearer resource_metadata="https://bifrost.culture4.life/.well-known/oauth-protected-resource"
```
- 2. **Check the authorization-server side** (this is where it fails today):
+ 2. **Check the authorization-server side** (diagnostic only; this is where it fails today):
```bash
curl -s https://idms.nexenio.com/realms/nexenio/.well-known/openid-configuration \
| grep registration_endpoint
```
No `registration_endpoint` in the response is why dynamic client
registration fails. See the symptom map below for the exact error text.
- 3. **The plugin no longer ships an OAuth client ID option** (removed in 1.8.0:
+ 3. **The plugin no longer ships an OAuth client ID option** (removed in 1.9.0:
Claude Code never substituted it, Keycloak received the literal
placeholder). In `/mcp`, do not pick `Authenticate` for the plugin's
bifrost server; use a virtual key or `auto_login` and pick `Reconnect`.
4. **After a successful login**, the gateway maps the authenticated identity
to a personal virtual key server-side. `no_virtual_key` at that point means
the operator has not added you to that map yet.
Fix, in order of what you control. Use a virtual key: it works today and needs
no identity-provider change, or turn on `auto_login`. The plugin ships no OAuth
- config since 1.8.0, so there is no client ID to paste.
+ config since 1.9.0, so there is no client ID to paste.
## Symptom → cause map
| Symptom | Cause | Fix |
|---------|-------|-----|
| 401 on every bifrost call | `BIFROST_VK` wrong or missing (or the plugin's `virtual_key` config wrong, or a rotated key cached by auto-login) | Set env var, or `/plugin configure`, then restart. For a cached key: `rm ~/.cache/bifrost-plugin/vk` (the hooks also drop it on their next 401), restart to sign in again |
| Plugin's bifrost server shows "needs authentication" right after auto-login | Claude Code remembered the earlier 401 for 15 min | `/mcp` → `bifrost` → Reconnect (not Authenticate); restarts work again once the sign-in page said connected |
| 403 | Key valid but no permission | Contact gateway operator |
| `skill_search` tool not found | bifrost MCP not loaded or no skill server | Check `claude mcp get bifrost` / `/mcp`; restart CC |
| Memory tool not found | Gateway exposes no memory server | Check with gateway operator; memory is optional |
| Gateway timeout | Gateway offline or wrong URL | Check `BIFROST_URL`; contact gateway operator |
| Literal `${BIFROST_URL}` or `${BIFROST_VK}` shows up as a server URL or header value | Plugin version before 1.5.0 running on a surface with no shell environment (Desktop, claude.ai), so the placeholder is never resolved | Update to bifrost-plugin 1.5.0 or later |
| `Incompatible auth server: does not support dynamic client registration` | Gateway's `/.well-known/oauth-authorization-server` has no `registration_endpoint` | Identity-provider side. Use a virtual key or `auto_login` (step 9.3) |
| `Policy 'Trusted Hosts' rejected request to client-registration service. Details: Host not trusted.` | Keycloak's realm Trusted Hosts policy blocks direct client registration | Identity-provider side. Use a virtual key or `auto_login` (step 9.3) |
| Desktop: `no_virtual_key` after a successful login | Authenticated but not yet in the gateway's VK map | Operator: assign the user a virtual key |
| Permission prompt on every gateway tool | Normal Claude Code behaviour — no rule pre-approves the server | Add `"mcp__plugin_bifrost-plugin_bifrost"` (and/or `"mcp__bifrost"`) to `permissions.allow` in `~/.claude/settings.json`. The plugin cannot ship this: a plugin manifest may only carry `agent` and `subagentStatusLine` settings, and a `permissions` block there validates but is dropped at load |
| Tool exists on the gateway but not in your tool list | Server is code-mode, not flat | Call it via `executeToolCode`; `listToolFiles()` to discover. See `/bifrost-code-mode` |
For manual MCP wiring: `/bifrost-mcp-setup`.
For fresh onboarding: `/bifrost-onboard`.
## Memory check-ins are not appearing
The `Stop` hook asks about memory candidates from the third turn of a session, then
every eight. It stays silent when:
- the session is shorter than three turns (deliberate — one-shot questions are not
interrupted);
- no gateway credential resolves (same check as everything else here — see step 1);
- no memory server was discovered, so `~/.cache/bifrost-plugin/discovery.json` has a
null `memory` field;
- its marker file says this session was already asked recently. Markers live in
`~/.cache/bifrost-plugin/reflect/` and are pruned after two days.
It is registered `async`, so its text arrives on the **next** turn, not the one that
triggered it. A check-in fired by the final turn of a session is never delivered.
Findings land in `.bifrost/candidates.md` in the project root, git-ignored. Review
them with `/bifrost-candidates`. If that file is missing, nothing has been recorded
in this project yet — that is not a fault.
## `mcp__bifrost__*` tools missing, but `claude mcp list` says connected
Almost always a **server-name collision**, not a gateway fault. Both install methods
register a server named `bifrost`: the plugin's bundled `.mcp.json` (using
`${BIFROST_URL}`) and `claude mcp add`. With both present, the project entry cannot
expand and the name resolves to nothing usable — so the tools vanish in any directory
containing that `.mcp.json`, while a connected `bifrost` still appears in the list.
Confirm:
```
claude mcp list | grep -i bifrost
```
A line reading `Missing environment variables: BIFROST_URL` alongside a connected
entry at a real URL is the collision. SessionStart also emits an explicit notice.
Fix, either one:
- `export BIFROST_URL=… BIFROST_VK=…` so the project entry resolves, or
- `claude mcp remove bifrost -s user` and let the plugin's entry own the name.
Note the hooks are **unaffected** either way — they read the credential from
`~/.claude.json` directly, so skills, memory and the tool roster keep working even
while the MCP tools are missing. That asymmetry is what makes this confusing.
## Which version is actually running
Do not answer this from `~/.claude/plugins/installed_plugins.json`. That file records
the version and `installPath` captured at install time, and it does not track a
marketplace whose source is a local directory. On a machine where the marketplace
points at a working tree, the recorded entry can be several minor versions behind what
is executing, with a stale copy still sitting at the recorded path.
Check what is on disk, not what was recorded:
```
ls -la ~/.claude/plugins/cache/<marketplace>/bifrost-plugin/
```
A symlink among those version directories is the tell — it points at a working tree,
and that tree is what loads. Confirm by feature rather than by version string: if
`hooks/hooks.json` at the recorded `installPath` has no `Stop` or `PostToolUse` entry
but sessions do get memory check-ins and `~/.cache/bifrost-plugin/usage.json` is being
written, the recorded path is not the code in play.
This matters for a bug report: a plugin developer dogfooding through a directory-source
marketplace is testing uncommitted code while every diagnostic reports a released
version number.