Skip to Content

Companion Memory

Companion Memory is reachable three ways: an MCP server for agents, a REST API for memory operations, and a GraphQL API for search and structured queries.

One key can open MCP and REST together. GraphQL is different — scope enforcement is deny-by-default, so a key that has any scope ticked is refused everywhere no scope is declared, and GraphQL declares none. Reaching GraphQL means using a key with no scopes selected at all, which is a full-access key. See Which scopes for which job.

Find your address first

Companion Memory has no fixed port. Every example in this section writes its base URL as <memory-url> because the real value depends on how your instance runs, and guessing it is the most likely reason a correct config still fails to connect.

How it runsAddress of the MCP endpoint
Installed from the Hub app store (usual)http://companion-memory.localhost/api/mcp — no port
Same, reached by LAN IPhttp://<appliance-ip>:<allocated-port>/api/mcp
Over a private VPN such as Tailscalehttp://<appliance-vpn-name>:<allocated-port>/api/mcp
Exposed through a tunnel or custom domainhttps://<your-domain>/api/mcp — no port
Standalone, from Companion Memory’s composehttp://<host>:8642/api/mcp

The .localhost suffix is Hub’s default local domain. If you set LOCAL_DOMAIN, substitute yours — see Finding Your App’s URL.

8642 is the port inside the container, not on your appliance. When Hub installs Companion Memory it publishes it on a host port it allocates itself, from a wide range — so the number differs per appliance and is not 8642. If you have seen 8642 quoted for a Hub install, that is where the connection-refused is coming from.

Take the address from the Hub dashboard instead: the Companion Memory tile shows its URL, and Open on local network gives you the LAN form with the correct port already filled in.

MCP endpoint

URL<memory-url>/api/mcp
TransportStreamable HTTP
Protocol version2025-11-25
AuthAuthorization: Bearer <key> (or x-api-key)
Required scopesMCP and Intents

The path is /api/mcp. A request to /mcp returns 405 Method Not Allowed — the gateway routes ^/api/mcp only. Older versions of these docs published the wrong path, so if you followed one of those, this is why.

A key with MCP but not Intents connects successfully and shows zero tools. This is the most common problem people hit here, and it is a scope choice rather than a fault. See the scope trap.

REST API

The memory endpoints live under /api/memory and require the Memory scope — which is not the same scope MCP needs. A key minted for MCP alone returns 403 here.

Scope is only half of it: these routes also declare a capability, and falling short of it is a 403 too.

MethodPathPurposeCapability
GET/api/memory/contextBootstrap context for a new sessionread
POST/api/memory/recallRetrieve memories relevant to a queryread
POST/api/memory/writeStore a memory explicitlywrite
POST/api/memory/turnRecord a conversation turnwrite
POST/api/memory/compactCompact accumulated contextwrite

A read key with the Memory scope reaches the first two rows and gets 403 on the other three — “This API key is “read” and cannot perform this action, which requires “write” access”. Scope and capability produce the same status code here, so check both before concluding the scope is wrong.

curl -s -X POST "<memory-url>/api/memory/recall" \ -H "Authorization: Bearer <memory-key>" \ -H 'Content-Type: application/json' \ -d '{"query":"what did I decide about the database migration"}'

GraphQL API

Search and structured queries go through GraphQL rather than a REST search endpoint.

GraphQL declares no scope, and a scope-limited key is refused on every route that declares none. So the MCP + Intents + Memory key the rest of this page tells you to mint is rejected here with “This API key is scope-limited and cannot access this endpoint”. Expect that message inside the GraphQL errors array rather than as an HTTP error status — a client that only checks the status code will read the call as having succeeded. Only a key with no scopes selected reaches GraphQL, and that key is full-access by construction.

# <unscoped-key> = a key created with no scopes ticked. A scoped key is refused here. curl -s -X POST "<memory-url>/api/graphql" \ -H "Authorization: Bearer <unscoped-key>" \ -H 'Content-Type: application/json' \ -d '{"query":"query($input: SearchInput!) { search(input: $input) { __typename } }","variables":{"input":{"query":"database migration"}}}'

There is no /api/search endpoint. Earlier documentation referenced one; it never existed.

Which scopes for which job

What you wantScopes to select
MCP tools onlyMCP + Intents
REST memory onlyMemory
MCP tools and direct REST callsMCP + Intents + Memory
GraphQL, bulk ingest, or filesnone — an unscoped, full-access key

This trips people up because MCP and REST are genuinely separate surfaces with separate scopes. If your agent connects over MCP but every /api/memory/* call returns 403, the key is missing Memory.

Scopes are not additive permissions. Only three surfaces declare a scope — /api/mcp, the intent catalog, and /api/memory/*. Ticking any box makes the key scope-limited, which denies it everywhere no scope is declared: GraphQL, bulk ingest, files, and the graph routes. Leaving every box unticked is the only way to reach those, and it produces a full-access key. There is no combination that opens all four rows above at once.

A gap worth knowing about in memory_remember

The tool’s advertised input schema describes operations as an array of open-ended objects. The server actually validates each entry as a discriminated union on a kind field, which the schema does not mention:

{ "operations": [ { "kind": "entity.create", "entityType": "Note", "name": "…", "properties": { "body": "…" } } ], "dryRun": true }

Valid kind values are entity.create, entity.update, relationship.create, and relationship.update.

An agent working only from tools/list cannot construct a valid call — it has to fail once and read the error to learn the contract. Use "dryRun": true while you work out the shape; it checks the operation’s shape and reports what would happen without writing anything.

dryRun does not lower the bar to use the tool. memory_remember is a write tool, and capability is checked before the arguments are looked at — so a read key does not see it in tools/list at all, and naming it directly returns “requires “write” access, but this API key is “read"". Experimenting with dryRun needs a write key.

dryRun validates shape, not references. It returns early before the checks that the entities a relationship.create points at actually exist and belong to you, so a dry run can report created for an operation that fails for real. Treat a clean dry run as “the payload parses”, not “the write will succeed”.

Capability

Beyond scopes, each key carries a capability — read, write, or full — that decides what it may do on the surfaces it can reach. See Choosing a capability.

Capability is enforced on routes that declare a scope. A key with no scopes selected is not scope-limited, so it reaches GraphQL and bulk ingest where nothing checks capability — meaning read does not fully hold for that one combination. Select the areas your key needs.

CORS

Companion Memory sends no CORS headers. Browser-based MCP clients and browser JavaScript cannot call these endpoints directly — this is a server-side and CLI integration.

Last updated on