Run the MCP gateway
Point the Streamable HTTP gateway at an executor and choose catalog or search discovery.
pnpm --filter @eyeball/mcp-gateway build
EYEBALL_API_KEYS="mcp_host_key:proj_local:demo_user" \
EYEBALL_EXECUTOR_API_KEY="eyeball_local_key" \
EYEBALL_EXECUTOR_URL="http://127.0.0.1:3000" \
EYEBALL_DATABASE_URL="postgresql://eyeball:eyeball_dev@127.0.0.1:5432/eyeball" \
EYEBALL_USER_ID="demo_user" \
EYEBALL_MCP_DISCOVERY="search" \
EYEBALL_MCP_ALLOWED_ORIGINS="https://agent.example.com" \
EYEBALL_MCP_SESSION_TTL_MS=86400000 \
EYEBALL_MCP_TASK_TTL_MS=3600000 \
EYEBALL_MCP_TASK_POLL_MS=1000 \
HOST="127.0.0.1" \
PORT=3001 \
pnpm --filter @eyeball/mcp-gateway start| Variable | Meaning |
|---|---|
EYEBALL_API_KEYS | Inbound key:projectId[:userId] policy; pin keys delegated to less-trusted hosts |
EYEBALL_EXECUTOR_API_KEY | Separate server-side key forwarded to the executor after inbound authentication |
EYEBALL_EXECUTOR_URL | Executor origin; defaults to http://127.0.0.1:3000 |
EYEBALL_DATABASE_URL | Optional Postgres connection for durable negotiated sessions and task records; blank or absent selects memory |
EYEBALL_USER_ID | Optional default external user |
EYEBALL_MCP_DISCOVERY | catalog or search |
EYEBALL_MCP_ALLOWED_ORIGINS | Comma-separated browser-origin allowlist; requests carrying Origin are rejected when this is unset |
EYEBALL_MCP_SESSION_TTL_MS | Session lifetime; defaults to 24 hours |
EYEBALL_MCP_TASK_TTL_MS | Retention used when a task request omits ttl; defaults to one hour |
EYEBALL_MCP_TASK_POLL_MS | Executor status poll interval and client hint; defaults to 1000 ms |
HOST / PORT | Listener; defaults to 127.0.0.1:3001 |
search discovery lists eyeball.search_tools plus eyeball.execute_tool, allowing standard MCP hosts to execute a returned canonical tool without receiving a new descriptor. catalog lists the full eligible catalog. Async-by-nature tools are omitted from ordinary sessions and appear only after a client opts into the experimental Tasks profile during initialization.
The gateway supports JSON responses and SSE responses on POST /mcp, a notification stream on GET /mcp, and authenticated session teardown on DELETE /mcp. It rejects unknown or expired session IDs with 404 and hashes the inbound credential before binding it to stored state. Browser requests with an untrusted Origin are rejected before authentication.
The stock server selects PostgresSessionStore when EYEBALL_DATABASE_URL is nonblank, runs the gateway's committed migrations before listening, and closes active SSE listeners and process-local timers before draining HTTP connections and closing its pool during graceful shutdown. Blank or absent configuration selects InMemorySessionStore; direct createMcpGatewayApp() composition remains synchronous and in-memory unless a store is injected.
The gateway owns an independent Drizzle migration history. Its migration ledger lives in eyeball_mcp_gateway.__drizzle_migrations, so sharing one physical database with the executor cannot cause either application's 0000 migration to suppress the other. Each service applies only its own migrations at boot; root pnpm db:migrate applies both histories before a coordinated deployment.
Postgres stores the negotiated session and its task map atomically in one row. Sessions and non-terminal task records survive gateway reconstruction, but polling is not attempted at boot because the downstream executor key is deliberately not persisted. The next correctly authenticated GET or POST supplies that credential and rearms durable working or input_required tasks. A different inbound credential or configured project/pinned-user authority hashes to a different binding and cannot attach to the session.
Only the SHA-256/base64url authBinding is persisted. Its v2 input commits to the inbound credential and configured project/pinned-user authority, so neither bearer nor plaintext authority is stored and a remapped principal cannot restore the session. The downstream executor key, authorization header, request context, timer handles, SSE listeners, and event bodies are also not stored. Logical expiry is still enforced by the protocol on access. An untouched expired row may remain physically present until operational cleanup; index cleanup work by mcp_sessions.expires_at and preserve the protocol's lazy deletion behavior.
SSE subscribers and timer handles remain process-local, and no replay log is provided.
The stock HttpMcpExecutor allocates async work, reads execution status, and forwards session-bound tasks/cancel through the SDK. Pre-dispatch cancellation fences provider invocation. After the durable dispatch marker, cancellation is best effort: same-process HTTP work is aborted where possible, but upstream work or external side effects may still complete. The executor record remains authoritatively cancelled, and a lost gateway response is reconstructed on retry through a same-credential status read.
Put the gateway behind TLS for remote clients. The inbound bearer key is always required and is distinct from the executor key. If you accept per-request user headers, use a user-pinned inbound key or otherwise authenticate the caller before trusting that identity.