Streamable HTTP transport¶
Engrama's MCP server speaks two transports:
| Transport | When | How it's selected |
|---|---|---|
| stdio (default) | Local desktop clients that launch the server as a subprocess (Claude Desktop's standard config). | ENGRAMA_TRANSPORT=stdio (or unset). |
| Streamable HTTP | Running Engrama as a long-lived local HTTP server you connect to over the network. | ENGRAMA_TRANSPORT=http. |
The HTTP transport is built on the MCP SDK's MCPServer
(mcp.server.mcpserver, SDK v2) — no extra dependency. The default stays stdio
so existing Claude Desktop setups are untouched.
Bind to loopback only — there is no authentication yet
The HTTP transport ships without auth. Run it bound to
127.0.0.1 (the default) and never expose it on a public or
LAN-reachable interface until the OAuth phase lands. See
Security model.
Security model¶
Local HTTP on loopback has the same attack surface as stdio. With the
default bind (127.0.0.1), the only processes that can reach /mcp are
those already running on your machine — exactly the trust boundary stdio
relies on (a local client launching and talking to a local server).
Switching a local Claude Desktop / SDK client from stdio to loopback HTTP
does not widen your exposure.
The surface only grows if you change the deployment:
- Binding off-loopback —
ENGRAMA_HTTP_HOST=0.0.0.0or a LAN IP, a reverse proxy, or an SSH /ngrok-style tunnel — turns the server into an unauthenticated remote endpoint. Anyone who can reach the port can read and write the entire memory graph. Don't, not in this phase. - A malicious local web page could try to script a browser into
POSTing to
http://127.0.0.1:8000/mcp(a DNS-rebinding / CSRF-style attack). The built-in Origin/Host validation is the guard: cross-origin requests are rejected with 403, and only loopbackHostvalues are accepted.
Rules of thumb for this phase:
- ✅ Loopback bind + local client → same trust as stdio. Fine.
- ✅ Keep the default
ENGRAMA_ALLOWED_ORIGINS(loopback only). - ❌ No off-loopback bind, no public / LAN exposure, no tunnel — unless you put your own authenticated gateway and TLS in front.
- ⏭ Real authentication (OAuth 2.1) is the next phase; the
/.well-known/oauth-protected-resourcestub is its hook.
Configuration¶
All HTTP settings are environment variables (CLI flags override them):
| Env var | CLI flag | Default | Purpose |
|---|---|---|---|
ENGRAMA_TRANSPORT |
--transport |
stdio |
stdio or http. |
ENGRAMA_HTTP_HOST |
--host |
127.0.0.1 |
Bind address (HTTP mode). |
ENGRAMA_HTTP_PORT |
--port |
8000 |
TCP port (HTTP mode). |
ENGRAMA_ALLOWED_ORIGINS |
--allowed-origins |
loopback only | CSV of allowed Origin headers. |
ENGRAMA_AUTH_ISSUER |
--auth-issuer |
(unset) | OAuth issuer for the RFC 9728 stub. Unset → endpoint 404s. |
The MCP endpoint is served at /mcp.
Starting in HTTP mode (local)¶
$env:ENGRAMA_TRANSPORT = "http"
engrama-mcp
# or, equivalently:
engrama-mcp --transport http --host 127.0.0.1 --port 8000
ENGRAMA_TRANSPORT=http engrama-mcp
# or:
engrama-mcp --transport http --host 127.0.0.1 --port 8000
The backend selection is unchanged from stdio mode (--backend sqlite
default, --backend neo4j plus the NEO4J_* vars to opt in).
Endpoints¶
| Path | Method | Purpose |
|---|---|---|
/mcp |
POST/GET | The MCP Streamable HTTP endpoint. |
/health |
GET | Liveness/readiness probe — 200 if the backend answers, 503 otherwise. |
/.well-known/oauth-protected-resource |
GET | RFC 9728 metadata stub (404 until ENGRAMA_AUTH_ISSUER is set). |
/health¶
Returns 200 with {"status": "ok", "backend": ..., "node_count": ...}
when the configured backend responds, 503 with
{"status": "error", ...} when it does not. Useful for Kubernetes
liveness/readiness probes in a future deployment phase.
curl -i http://127.0.0.1:8000/health
It is intentionally not guarded by the Origin check (probes send no
Origin header). It owns a small cached connection of its own — see
Protocol versions and sessions for why custom routes
can't reuse the MCP server's store.
/.well-known/oauth-protected-resource¶
A stub for the upcoming OAuth phase:
# No issuer configured → 404
curl -i http://127.0.0.1:8000/.well-known/oauth-protected-resource
# With an issuer → RFC 9728 document
ENGRAMA_AUTH_ISSUER=https://auth.example.com engrama-mcp --transport http
curl -s http://127.0.0.1:8000/.well-known/oauth-protected-resource
# {"resource": "http://127.0.0.1:8000/mcp",
# "authorization_servers": ["https://auth.example.com"]}
The next phase only has to set ENGRAMA_AUTH_ISSUER (and wire a token
verifier) — no change to this endpoint's code.
Origin validation (anti DNS-rebinding)¶
The HTTP transport uses the MCP SDK's built-in DNS-rebinding protection.
On every request to /mcp it validates:
OriginagainstENGRAMA_ALLOWED_ORIGINS— a disallowed Origin is rejected with 403. A missingOrigin(same-origin / non-browser client likecurl) is allowed.Hostagainst the loopback allow-list derived from--host/--port— a mismatched Host is rejected with 421.
The default Origin allow-list is loopback only, including port wildcards
so browser-style clients connecting to http://localhost:8000 work
without extra configuration:
http://localhost, http://127.0.0.1, http://localhost:*, http://127.0.0.1:*
Override it for a specific client:
ENGRAMA_ALLOWED_ORIGINS="http://localhost:8000,https://my-client.example" \
engrama-mcp --transport http
Quick check:
# Disallowed Origin → 403
curl -i -H "Origin: http://evil.com" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-X POST http://127.0.0.1:8000/mcp
# No Origin (curl) → passes the security check
curl -i -H "Accept: application/json, text/event-stream" \
http://127.0.0.1:8000/mcp
Protocol versions and sessions¶
The server speaks both eras of the MCP protocol on the same /mcp
endpoint and picks per request — there is nothing to configure:
| Client protocol | Handshake | Session |
|---|---|---|
2026-07-28 |
None. Each request carries its protocol version, client info and capabilities in _meta; server/discover is optional. |
None — the revision removed Mcp-Session-Id. |
2025-11-25 and earlier |
initialize / notifications/initialized. |
None by default (see below). |
Sessionless by default. Handshake-era clients are also served
statelessly (stateless_http=True): initialize returns no
Mcp-Session-Id and every request stands on its own, so any replica can
answer any request and a restart drops nothing a client has to recover.
Engrama's tools are plain request/response calls — no MCP sampling,
elicitation or server push — so nothing needs a long-lived channel.
Embedders that call create_engrama_mcp() directly can pass
stateless_http=False to give handshake-era clients a per-client session
again; 2026-07-28 clients stay sessionless either way.
Tenant identity is per request. The X-Engrama-Org-Id /
X-Engrama-User-Id headers a gateway injects are read on every request
(Spec 001), so isolation does not depend on a session.
Lifespan. The server lifespan — opening the graph store, vault and
embedder — runs once at startup and is shared by every request. A
backend that is down at boot does not stop the server: /health answers
503 and tools return errors until it comes back.
Custom routes (/health) are plain Starlette routes that run outside any
MCP request, so they never see the server's lifespan context. That is why
/health maintains its own lazily-created, cached backend connection
rather than reaching into the MCP request state.
Connecting clients¶
mcp CLI / Inspector (manual testing)¶
The MCP Inspector or any MCP HTTP client points at the /mcp URL:
npx @modelcontextprotocol/inspector
# Transport: "Streamable HTTP"
# URL: http://127.0.0.1:8000/mcp
From the Inspector you can list tools (engrama_status, engrama_search,
…) and call them to confirm the server responds end to end.
Claude Desktop (custom integration)¶
Some Claude Desktop builds accept a custom HTTP MCP server; others restrict custom integrations to HTTPS. To try it:
- Start Engrama in HTTP mode (above).
- In Claude Desktop, add a custom MCP server / integration pointing at
http://localhost:8000/mcp. - Confirm Engrama's tools appear and
engrama_statusreturns the expected backend/vault.
Known limitations (this phase):
- No auth. Claude Desktop may warn about or refuse an unauthenticated custom integration.
- HTTPS requirement. If your build requires HTTPS for custom
integrations, front the server with a locally-trusted TLS cert
(
mkcert) and point the client at thehttps://URL — or defer Claude Desktop integration to the OAuth/TLS phase and validate with the MCP Inspector for now.
The goal of this phase is that the server responds correctly over HTTP; full Claude Desktop acceptance may have to wait for the auth phase depending on your build.
Operational differences vs stdio¶
| stdio | Streamable HTTP | |
|---|---|---|
| Process model | Launched as a subprocess by the client. | Long-lived server you start and connect to. |
| Lifecycle | One process per client session. | One process, many requests. |
| Store lifespan | Opened once, reused for the session. | Opened once at startup, shared by all requests. |
| Network exposure | None (pipes). | Binds a TCP port; Origin/Host validated. |
| Health probe | N/A. | GET /health. |
| Auth | N/A (local trust). | None yet — loopback + Origin check only. |