Claude Code, Cursor, Codex, and more
Any client that speaks MCP over Streamable HTTP can connect. The three things every client needs are
the same β the serverβs MCP URL, an Authorization: Bearer header, and some way of declaring that
the transport is Streamable HTTP. Only the third differs between them.
These clients get memory as tools β something the agent calls, not something that happens. Passive capture is an OpenClaw and Hermes feature; see Beyond MCP: first-class memory.
For both Companion Hub and Companion Memory that URL ends in /api/mcp.
<hub-url> and <memory-url> below are your two serversβ base URLs β everything before /api/mcp.
Neither has a port you can assume, so take both from the Hub dashboard if you are unsure:
Hub,
Memory.
Claude Code
claude mcp add --scope user --transport http ci-memory "<memory-url>/api/mcp" --header "Authorization: Bearer <memory-key>"
claude mcp add --scope user --transport http ci-hub "<hub-url>/api/mcp" --header "Authorization: Bearer <hub-key>"One line each, deliberately. A trailing \ continues a command in bash but is
a literal argument in PowerShell and cmd β pasted there, the first line runs
without its URL and the rest fail as unknown commands. These fit on one line,
so they run as-is in every shell.
Then confirm. claude mcp list prints one line per server with its health, and /mcp inside a
session shows each serverβs tools:
claude mcp listTo keep a key out of your shell history and out of ~/.claude.json, register the server with a
${VAR} reference instead. Claude Code expands ${VAR} in headers when it reads the config, so
export the variable first (see Keep the keys out of the file) and
put this in a .mcp.json at your project root:
{
"mcpServers": {
"ci-memory": {
"type": "http",
"url": "<memory-url>/api/mcp",
"headers": { "Authorization": "Bearer ${CI_MEMORY_API_KEY}" }
}
}
}Hand-written JSON needs "type": "http"
The CLI sets this for you. If you write the config by hand instead, the type field is
required:
{
"mcpServers": {
"ci-memory": {
"type": "http",
"url": "<memory-url>/api/mcp",
"headers": { "Authorization": "Bearer <memory-key>" }
}
}
}An entry with a url but no type is skipped, and the server does not appear in your
session. It is not silent, though: claude mcp list reports it, naming the config file and the
fix β MCP server "β¦" has a "url" but no "type"; add "type": "http". If a server you configured by
hand is missing, run that command first.
Keep the keys out of the file
Every block below reads the key from an environment variable rather than embedding it, so the config file can be shared or committed without leaking a credential. Export both keys in the shell that launches the client:
export CI_MEMORY_API_KEY='<memory-key>'
export CI_HUB_API_KEY='<hub-key>'A client launched from a desktop icon does not inherit variables you exported in a terminal, so on macOS and Windows either start the client from that terminal or paste the key into the file and keep the file private.
Cursor
Add both servers to ~/.cursor/mcp.json, or to .cursor/mcp.json in a project. Cursor infers the
transport from the presence of url, and it expands ${env:NAME} in url and headers:
{
"mcpServers": {
"ci-memory": {
"url": "<memory-url>/api/mcp",
"headers": { "Authorization": "Bearer ${env:CI_MEMORY_API_KEY}" }
},
"ci-hub": {
"url": "<hub-url>/api/mcp",
"headers": { "Authorization": "Bearer ${env:CI_HUB_API_KEY}" }
}
}
}Reload the window after editing, then open Cursorβs MCP settings and confirm each server shows its tools.
Codex
Codex reads ~/.codex/config.toml. Its bearer_token_env_var field names the environment variable
that holds the key, and Codex sends it as Authorization: Bearer:
[mcp_servers.ci-memory]
url = "<memory-url>/api/mcp"
bearer_token_env_var = "CI_MEMORY_API_KEY"
[mcp_servers.ci-hub]
url = "<hub-url>/api/mcp"
bearer_token_env_var = "CI_HUB_API_KEY"Or add each server from the command line:
codex mcp add ci-memory --url "<memory-url>/api/mcp" --bearer-token-env-var CI_MEMORY_API_KEY
codex mcp add ci-hub --url "<hub-url>/api/mcp" --bearer-token-env-var CI_HUB_API_KEYConfirm with codex mcp list, or type /mcp in a session.
VS Code
Create .vscode/mcp.json, or run MCP: Open User Configuration for a config that follows you
across workspaces. VS Code uses servers as the top-level key, not mcpServers, and can prompt for
the key so it never lands in the file:
{
"inputs": [
{
"type": "promptString",
"id": "ci-memory-key",
"description": "Companion Memory API key",
"password": true
}
],
"servers": {
"ci-memory": {
"type": "http",
"url": "<memory-url>/api/mcp",
"headers": { "Authorization": "Bearer ${input:ci-memory-key}" }
}
}
}Gemini CLI
Gemini CLI uses httpUrl, not url, for a Streamable HTTP server, and expands ${VAR} in headers.
Add this to ~/.gemini/settings.json, or to .gemini/settings.json in a project:
{
"mcpServers": {
"ci-memory": {
"httpUrl": "<memory-url>/api/mcp",
"headers": { "Authorization": "Bearer ${CI_MEMORY_API_KEY}" }
}
}
}Or from the command line:
gemini mcp add --transport http -H "Authorization: Bearer $CI_MEMORY_API_KEY" ci-memory "<memory-url>/api/mcp"OpenCode
Add a remote server to opencode.json. OpenCode substitutes {env:NAME}:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"ci-memory": {
"type": "remote",
"url": "<memory-url>/api/mcp",
"enabled": true,
"headers": { "Authorization": "Bearer {env:CI_MEMORY_API_KEY}" }
}
}
}Claude Desktop
Claude Desktopβs Settings β Connectors β Add custom connector takes a URL and is built for
servers that use OAuth or no authentication. Companion Hub and Companion Memory authenticate with a
static bearer key and do not advertise an OAuth flow, so use a local bridge that turns the remote
server into a stdio server instead. mcp-remote is one such bridge; it is a third-party package that
Companion does not maintain or test. Put the header value in env β some clients do not escape a
space inside args:
{
"mcpServers": {
"ci-memory": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"<memory-url>/api/mcp",
"--header",
"Authorization:${AUTH_HEADER}"
],
"env": { "AUTH_HEADER": "Bearer <memory-key>" }
}
}
}The file is ~/Library/Application Support/Claude/claude_desktop_config.json on macOS and
%APPDATA%\Claude\claude_desktop_config.json on Windows. Restart Claude Desktop after editing. The
bridge needs Node.js on the machine, and the key sits in this file, so keep the file private.
Any other client
The pattern is the same. Consult your clientβs documentation for where its MCP config lives and what it calls the transport field, then fill in:
| What the client needs | Value |
|---|---|
| URL | <server-url>/api/mcp |
| Header | Authorization: Bearer <key> |
| Transport | Streamable HTTP, however that client spells it |
Confirm the tools arrived
Whichever client you use, finish with the same check: the client should list the serverβs tools, not
just report it as connected. A Hub key lists tools such as hub_list_installed_apps; a Memory key
with MCP and Intents lists tools such as memory_recall. A server that connects with an empty
list is almost always a Memory key without Intents β see
Connected, but zero tools. If the client
gives you nothing to go on, run the curl check below.
If your client runs in a browser, it almost certainly cannot reach
Companion Memory β its CORS allowlist is the native Capacitor webview origins:
capacitor://localhost, ionic://localhost, and the bare http://localhost
and https://localhost. Those last two carry no port, so a page served on
plain localhost (the default port, 80 or 443) is allowed as well β but any
other origin, including localhost on a non-default port, is refused unless
an operator adds it with CORS_ALLOWED_ORIGINS. Hub is narrower than that: it
allows its own localhost:5002 origin, the domains you configured, and
origins listed in CI_HUB_EXTRA_CORS_ORIGINS. Wire these up from a
server-side or CLI client.
Checking any client, without the client
When a client fails and gives you nothing to work with, take it out of the picture and test the server directly. If this works, the problem is in your client config; if it does not, the problem is the key or the address.
flowchart LR
T["Call the server yourself:<br/>initialize, then tools/list"] --> Q{"200, and a<br/>non-empty tools array?"}
Q -->|"yes"| A["Server and key are fine β<br/>the problem is the client config"]
Q -->|"no"| B["The problem is the key or the address β<br/>the status code names which"]macOS / Linux / WSL2
BASE="http://<host>:<port>/api/mcp"
KEY="<key>"
HDRS=$(mktemp)
curl -s -D "$HDRS" -X POST "$BASE" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "Authorization: Bearer $KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"verify","version":"1"}}}'
head -1 "$HDRS" # the status code you need for the next step
SID=$(grep -i '^mcp-session-id:' "$HDRS" | tr -d '\r' | awk '{print $2}')
echo "session: $SID"
curl -s -X POST "$BASE" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "Authorization: Bearer $KEY" \
-H "mcp-session-id: $SID" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'Read the status line first: 200 means the handshake worked, and anything else is what the
Troubleshooting page is organized by. An empty session: line
alongside a 200 is not necessarily a failure β the session header is optional in the Streamable
HTTP spec, so a stateless server can answer correctly without issuing one.
Because the request accepts text/event-stream, the body may come back as SSE frames
(event: message / data: {β¦}) rather than plain JSON. Strip the framing with
sed -n 's/^data: //p' before piping to jq β or in PowerShell, run this against the tools/list
response captured above:
($res2.Content -split '\r?\n' | Where-Object { $_ -like 'data: *' }) -replace '^data: ' | ConvertFrom-Json-split takes a regular expression, so '\r?\n' splits CRLF and LF frames alike. A bare \n leaves
a carriage return on the end of every frame β which ConvertFrom-Json itself tolerates as
whitespace, but which anything comparing or printing the raw strings will not.
Let the agent do it
If your client is itself a capable agent, point it at the connect skill and let it wire itself up:
https://docs.ci.computer/connect/SKILL.mdIt is an agent-readable version of this whole section β every endpoint here plus the OpenClaw and Hermes pages, first-class memory, and the traps on Troubleshooting β written for a model rather than a person.
Pasting the URL uses it once. To keep it available for later, install it into a skills directoryΒ your agent scans; see Or let your agent do it for the difference. It cannot mint keys for you, so have both ready first.