Connection troubleshooting
Organized by what you observe, because the symptom rarely names the cause. Each entry was reproduced against a live appliance or checked against the server source.
Work down from the first symptom you hit; each branch is a section on this page:
flowchart TD
B{"Refused, or hangs?"}
B -->|"yes"| B1["A port was assumed.<br/>Take the address from<br/>the Hub dashboard."]
B -->|"no"| C{"405 or 404?"}
C -->|"yes"| C1["The path is /mcp.<br/>Use /api/mcp."]
C -->|"no"| D{"401 with a key?"}
D -->|"yes"| D1["Key unknown or revoked,<br/>wrong server, Hub key<br/>without the mcp scope,<br/>or x-api-key on Hub."]
D -->|"no"| E{"403 (Memory only)?"}
E -->|"yes"| E1["Scopes miss that surface.<br/>Mint a key with the<br/>scopes the job needs."]
E -->|"no"| F{"Zero tools, or -32601?"}
F -->|"yes"| F1["The Memory key lacks Intents.<br/>Mint one with MCP + Intents."]
F -->|"no"| G{"Remembers nothing?"}
G -->|"yes"| G1["MCP gives tools, not capture.<br/>Install the memory provider."]
B1 ~~~ C1 ~~~ D1 ~~~ E1 ~~~ F1 ~~~ G1Connection refused, or the request just hangs
Nothing is listening on that port — almost always because a port was assumed rather than looked up. It hits both servers, for different reasons.
Companion Hub, over a public URL or VPN. 5002 is a real, fixed port, but it is only in the URL
when you are talking to it directly — on the LAN or across a VPN such as Tailscale. Reached by its
public URL or tunnel, Hub answers on 443 as https://<your-hub-domain>/api/mcp, with no port
at all: that route enters through cloudflared and is served by the appliance’s Traefik on port 80.
Appending :5002 to a tunnel origin is a connection error. See
Find your address.
Companion Memory, on any install. Here the port is not fixed by the docs.
Hub publishes Companion Memory on the app’s own port, 8642, when that port is free at install time.
If something already holds 8642, or you chose another port in the install form, Hub picks a free
port in the 10000–60000 range instead. So 8642 is right on many installs and refuses the
connection on others. Hub’s own 5002, by contrast, is fixed and can be relied on.
Use whichever of these matches your install:
| How Memory runs | Address |
|---|---|
| Installed from the Hub app store | http://<appliance-ip>:<host-port>/api/mcp |
| Over Tailscale | https://<hub>.<tailnet>.ts.net:<host-port>/api/mcp |
| Public domain | https://<your-domain>/api/mcp |
| Desktop app, on the same computer | http://127.0.0.1:8642/api/mcp |
| Standalone, from Memory’s own compose | http://<host>:8642/api/mcp |
The Access points card on the Companion Memory app page in the Hub dashboard carries the right address for each route, with a copy button. See Find your address.
A refused connection is not an auth problem — if you are getting one, the key is not yet involved.
405 Method Not Allowed, or 404 Not Found
The path is wrong. Companion Hub and Companion Memory serve MCP at /api/mcp, not /mcp.
The two servers report the same mistake differently, which is worth knowing before you conclude one of them is down:
| You sent | Companion Memory | Companion Hub |
|---|---|---|
POST /mcp | 405 | 404 |
POST /api/mcp, no key | 401 | 401 |
Memory’s 405 is the more misleading of the two — it reads like the service is refusing your request
method, when in fact its gateway routes ^/api/mcp only and the request never reached the MCP server.
401 with a key attached
Read the response body first. Companion Hub names the cause, and Companion Memory does not:
| Server | Body | Means |
|---|---|---|
| Hub | Missing Authorization header | No Authorization header arrived — for example, you sent x-api-key |
| Hub | Malformed Authorization header | The header was not exactly Bearer <key> |
| Hub | Invalid API key | Unknown, revoked, or missing the mcp scope |
| Memory | Unauthorized (no detail) | Unknown, revoked, expired, or not 64 lowercase hex characters |
The cause is one of these:
- The key is not in the shape a key has. Both servers issue 64 lowercase hexadecimal characters.
A trailing space, a doubled
Bearer, or a truncated paste fails as an unknown key. - The key is unknown, revoked, or expired. Confirm it exists —
cihub api-key listfor Hub, Settings → API Keys for Memory, where an expiry date shows on the row. - The Hub key lacks the
mcpscope. Hub resolves the token and its scope in a single step, so a scope failure comes back as401 Invalid API key— the same answer as an unknown key, and not the 403 you would get from Memory.cihub api-key listshows each key’s scopes. - You sent it to the wrong server. Hub keys and Memory keys are not interchangeable, and the error is the same either way.
- You used the wrong header on Hub. Hub accepts only
Authorization: Bearer. Memory acceptsx-api-keytoo, so a config that works against Memory returns 401 against Hub if it uses that header.
403 — Companion Memory only
Only Companion Memory answers 403. The key is valid, but its scopes do not cover the surface you are calling. Enforcement is deny-by-default: there are only four scopes, only the surfaces below declare one, and a key with any scope ticked is refused on every route that declares none. (Companion Hub never returns 403 for a scope problem — see the 401 entry above.)
| Calling | Needs |
|---|---|
/api/mcp | MCP |
| the intent catalog, the pending-action routes | Intents |
/api/memory/*, /api/v1/intelligence/*, /api/v1/agent/jobs | Memory |
the /api/v1 bulk-write routes, POST /api/files/upload, PATCH /api/files/:id | Capture |
/api/graphql, POST /api/v1/files, /api/v1/ingest/*, graph routes | no scopes ticked at all |
/api/v1/* is not one scope, which is the trap in that table. Only the bulk-write routes
(/api/v1/events, /communications, /contents, /things, /inventory (a deprecated alias of /things), /browser-activity, /notes,
/positions, /receipts, /product-models, /observations) declare Capture;
/api/v1/intelligence/* needs Memory; and /api/v1/files and /api/v1/ingest/* declare nothing
at all. So a Capture-only key can POST every batch successfully and then 403 on
POST /api/v1/ingest/finish — the terminal call that runs the post-load enrichment and embed
backfill. The rows are stored, but because the bulk routes deliberately skip live indexing they
stay out of the search index until finish runs — and finish, declaring no scope, accepts only an
unscoped full-access key or a signed-in session.
A key minted for MCP connects fine and returns 403 on every memory REST call. These are separate scopes, and needing both is normal — see Get your API keys.
If a key that holds MCP + Intents + Memory still 403s, check whether the surface is one of the
undeclared ones in the last row — those are reachable only by an unscoped, full-access key.
On Companion Hub the equivalent failure is a 401, not a 403 — a key that
lacks the mcp scope is rejected by the MCP auth guard rather than by a
separate scope check.
Connected, but zero tools
The most common problem in this whole section, and it is not a fault.
A Companion Memory key with MCP but not Intents authenticates, opens a session, and
advertises nothing. Depending on your client, you see an empty tool list or tools/list
answering -32601 Method not found.
Both axes move the count, but scopes do the heavier work — they decide which families are
registered at all, and capability then filters whatever survived. Counted from Companion Memory
v2026.10.2, which has 36 native intents and a GraphQL schema of 112 queries and 138 mutations:
| Scopes on the key | Capability | Tools returned |
|---|---|---|
MCP only | any | none — -32601 on tools/list |
MCP + Intents | read | 26 |
MCP + Intents | write | 32 |
MCP + Intents | full | 36 |
| No scopes selected | full | 286 |
A key with no scopes selected and a lower capability sees a number between the intent count and 286,
because its GraphQL tools are filtered by capability too. Holding scopes fixed, full ⊇ write ⊇
read always.
Scopes do two separate things, which is why the gap between 36 and 286 is so much larger than a capability change could explain:
- No
Intents→ no tools at all. Nothing is registered, so the server never advertises atoolscapability andtools/listanswers-32601. - Any scope ticked → no graph tools, ever. A key with any scope is scope-limited and is confined to the native intent tools. The much larger family generated from the GraphQL schema is withheld from it at every capability, because those tools run against resolvers a scope-limited key is denied anyway.
Treat these as illustrative rather than exact: both families grow release to release, and these are Memory’s numbers — Companion Hub advertises its own, smaller set.
Fix: mint a key with both MCP and Intents.
A tool call is refused
Capability is checked again at call time, and the refusal names the level that would have worked.
| Server | Message | Fix |
|---|---|---|
| Memory | Tool "<name>" requires "write" access, but this API key is "read" | Raise the key to write under Settings → API Keys |
| Memory | Tool "<name>" reads the knowledge graph directly, which this scope-limited API key cannot access | Use a key with no scopes selected, or use the intent tools |
| Hub | Tool '<name>' is destructive and this API key's capability is '<level>'… | Raise the key to full under Settings → Security |
A call comes back as TOOL_QUEUED
Nothing failed. The tool deletes data or sends something outside the appliance, and
Settings → Autonomy is Balanced or Cautious, so Companion Memory asked its
owner instead of running it. The error carries the code TOOL_QUEUED and a
pendingActionId. Open Approvals in Companion Memory and approve the card; the
call runs after that. A card nobody answers expires after 24 hours. A key that Hub
minted for an installed agent app is asked every time, whatever the setting. See
A full key still waits for Approvals.
Fewer tools than you expected
Your key’s capability is filtering the list. A read key sees only tools it could actually call;
a write key sees everything except destructive ones.
This is working as intended — it stops the agent burning a turn on a tool that was always going to be refused. If you want more, see Choosing a capability.
Alternating success and empty-bodied 400
Every second request fails, with a 400 carrying no body at all:
200 400 200 400 200 400Strict alternation, unaffected by delays between requests. This is a gateway defect, not a client problem — no config change can help.
Test for it with ten requests, not one. A single successful initialize
proves nothing here, and the handshake tends to fail at the second step,
making it look like notifications/initialized or tools/list is unsupported
when the connection dropped underneath it.
Fixed in Companion Memory v2026.7.30 — upgrade. If you cannot upgrade immediately, retrying
works — the failures observed were strictly alternating, so a second attempt succeeded every time.
tools/list returns -32601 Method not found
Not a missing method, and — on Companion Memory — almost always one cause: your key lacks
Intents, so nothing was ever registered for tools/list to return. See
the scope trap.
A lost session is not the explanation, even though it looks like a candidate. Neither server
reports one this way: both answer 404 — Hub with a JSON-RPC -32001 Session not found body, and
Memory with Session not found in its standard error body. Sessions expire after 30 minutes idle on
both servers and do not survive a restart of the server, and a client that follows the MCP spec
re-runs initialize on a 404. If you are getting -32601, the session is not the problem and
re-running initialize does not help.
The agent connected but does not remember anything
Working as designed, and the most common surprise with this setup.
MCP provides tools. It does not provide passive turn capture or automatic context injection — those come from a memory-slot provider, which is a separate mechanism. Over MCP alone, your agent has to call a memory tool during a conversation for anything to be stored, and saying so in its system prompt is usually enough.
If you want capture to happen on its own, that provider is published for both agents: OpenClaw, Hermes. See also Beyond MCP: first-class memory.
If it is installed and still nothing accumulates, the cause is usually one of a short list — each
agent’s page ends with them. The one to check first, on either agent: the key’s scopes and
capability. The provider talks REST, so its key needs the Memory scope at Read & write — an
MCP-scoped or Read-only key is refused on every capture, silently, while every setup check passes.
On OpenClaw the other usual answer is hooks.allowConversationAccess missing from the plugin’s
entry: you get the tools, and nothing is captured.
A browser-based client cannot connect
Companion Memory’s CORS allowlist contains only its own native-app origins (capacitor://localhost
and the localhost forms the iOS/Android shells serve from), so a page on any other origin is
refused — no client-side change fixes that. Use a server-side or CLI client. An operator can add
origins with CORS_ALLOWED_ORIGINS (comma-separated); it is empty out of the box.
Companion Hub’s CORS layer is also narrow. It allows http://localhost:5002 and
http://127.0.0.1:5002 (its own port), the domains you configured as DOMAIN and LOCAL_DOMAIN, and
any origin an operator lists in CI_HUB_EXTRA_CORS_ORIGINS, and it refuses every other origin.
Requests with no Origin header — every non-browser client — are unaffected. Treat both servers as
server-side and CLI integrations.
Still stuck
Take the client out of the picture and test the server directly with curl. That single step separates “my config is wrong” from “my key is wrong” faster than any amount of reading logs.