Skip to content

feat(server): remote authenticated MCP recall endpoint (POST /v1/mcp)#3070

Open
ChenglinWei97 wants to merge 3 commits into
thedotmack:mainfrom
ChenglinWei97:feat-remote-authenticated-mcp-recall
Open

feat(server): remote authenticated MCP recall endpoint (POST /v1/mcp)#3070
ChenglinWei97 wants to merge 3 commits into
thedotmack:mainfrom
ChenglinWei97:feat-remote-authenticated-mcp-recall

Conversation

@ChenglinWei97

Copy link
Copy Markdown

What

The secure authenticated MCP link a user pastes into Claude Code (or Cursor / Codex / any MCP client) to recall their cloud memory from Server Beta:

claude mcp add --transport http claude-mem <server-beta-base>/v1/mcp \
  --header "Authorization: Bearer cm_..."

Closes the one gap: the MCP server today is stdio-only (local worker). Server Beta already has the cloud recall data + API-key auth + REST recall — it just had no remote MCP transport. This adds it.

Design — reuse, not rebuild

  • Auth: the existing readAuth (requirePostgresServerAuth, scope memories:read). No new auth — the cm_ key's team (and project scope) bound every read, same as /v1/search.
  • Storage: the existing PostgresObservationRepository.search / .listByProject — the same code path as /v1/search and /v1/context. Identical data, identical guards.
  • Transport: MCP streamable-HTTP (SDK 1.29), stateless — one transport + server per request, so it slots into Express 5 with no session state.

src/server/mcp/recall-mcp-server.ts is a pure factory (storage injected as a RecallBackend) exposing read-only tools search / context / recent. The route in ServerV1PostgresRoutes supplies a team-scoped backend. Mutating tools are intentionally omitted — a pasted recall link is read-only.

Tests

tests/server/mcp/recall-mcp-server.test.ts drives a real MCP client over an in-memory transport (no Postgres): tool listing, arg forwarding + limit clamping, context packing, and backend failures surfacing as tool errors. 7/7 pass; full tsc --noEmit clean.

Before merge — one live check

Per the "verify against the live server" lesson: worth one integration pass hitting /v1/mcp on a real Server Beta with a cm_ key from an MCP client, to confirm the streamable-HTTP + Express 5 wiring end-to-end (the unit test covers the tool logic, not the HTTP transport handshake).

Open question

Where is Server Beta deployed (the host the link points at), and is POST /v1/keys the intended way for a user to mint a cm_ key? Happy to add a key-issuance route + a Connect snippet in a follow-up.

🤖 Generated with Claude Code

Adds the "secure authenticated MCP link" a user pastes into Claude Code (or any
MCP client) to recall their cloud memory from Server Beta:

  claude mcp add --transport http claude-mem <base>/v1/mcp \
    --header "Authorization: Bearer cm_..."

Design — reuse, not rebuild:
- Auth: the existing `readAuth` (requirePostgresServerAuth, scope memories:read).
  No new auth; the cm_ API key's team (and project scope) bound the recall.
- Storage: the existing PostgresObservationRepository.search / .listByProject,
  the same code path as /v1/search and /v1/context. Identical data, identical guards.
- Transport: MCP streamable-HTTP (SDK 1.29), stateless — one transport + server
  per request, so it slots into Express 5 with no session state.

`src/server/mcp/recall-mcp-server.ts` is a pure factory (storage injected as a
RecallBackend) exposing the read-only tools search / context / recent. The route
in ServerV1PostgresRoutes supplies a team-scoped backend. Mutating tools are
intentionally omitted — a pasted recall link is read-only.

Tests: tests/server/mcp/recall-mcp-server.test.ts drives a real MCP client over
an in-memory transport (no Postgres) — tool listing, arg forwarding/clamping,
context packing, and backend failures surfacing as tool errors. 7/7 pass; full
tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a remote authenticated MCP recall endpoint for Server Beta. The main changes are:

  • New read-only MCP server factory with search, context, and recent tools.
  • New /v1/mcp streamable-HTTP route using existing API-key auth and Postgres recall queries.
  • API documentation for connecting MCP clients with a bearer key.
  • Unit and Postgres-gated integration tests for MCP tool behavior and HTTP auth.

Confidence Score: 4/5

The endpoint is read-only and reuses existing authentication and storage paths, with no outstanding code issues identified.

Coverage includes unit tests for MCP tool behavior and route-level auth checks, while a live transport smoke test against the deployed environment remains the main remaining integration risk.

T-Rex T-Rex Logs

What T-Rex did

  • Ran the base-commit MCP access test to verify authentication and tool access behavior, confirming POST /v1/mcp requires auth and returns 401/403 with a read-only tool list of [context, recent, search].
  • Ran the head-commit MCP access test to confirm the same auth behavior persists and the MCP tool list remains read-only, mirroring the base commit results.
  • Booted the harness server with a minimal Postgres-like fixture for api_keys and observations to exercise the ServerV1PostgresRoutes in a controlled environment.
  • Ran the MCP tools runtime tests; the before-state shows the factory/tools are absent, and the after-state reports a successful MCP transcript with assertions for tool availability, read-only tool set, backend forwarding, limit clamping, context packing, and tool-error responses.
  • Checked the MCP storage flow by examining the before-state for missing MCP recall server components and the after-state for a functional /v1/mcp route that uses readAuth, constructs a PostgresObservationRepository, enforces teamId scope, and calls repo methods without any create/update/delete mutations; runtime notes confirm tests pass but DB-env-dependent tests are skipped.
  • Reviewed the API documentation changes; the before-state lacked MCP documentation, and the after-state adds the /v1/mcp endpoint, connection command, auth header example, read-only wording, and tool documentation aligned with implemented routes.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (2): Last reviewed commit: "docs(api): document the /v1/mcp remote M..." | Re-trigger Greptile

Comment on lines +943 to +973
app.all('/v1/mcp', readAuth, this.asyncHandler(async (req, res) => {
const teamId = this.requireTeamId(req, res);
if (!teamId) return;
const projectScope = req.authContext?.projectId ?? null;
const repo = new PostgresObservationRepository(this.options.pool);
const assertProjectAllowed = (projectId: string): void => {
if (projectScope && projectScope !== projectId) {
throw new Error('API key is scoped to a different project');
}
};
const backend: RecallBackend = {
search: async ({ projectId, query, limit }) => {
assertProjectAllowed(projectId);
const rows = await repo.search({ projectId, teamId, query, limit });
return rows.map(serializeObservation);
},
recent: async ({ projectId, limit }) => {
assertProjectAllowed(projectId);
const rows = await repo.listByProject({ projectId, teamId, limit });
return rows.map(serializeObservation);
},
};
const server = createRecallMcpServer(backend, MCP_SERVER_VERSION);
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
res.on('close', () => {
void transport.close();
void server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
}));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Audit logging is absent from the MCP read path

/v1/search and /v1/context both call this.auditRead(...) immediately after a successful Postgres query, recording the actor, project, query, and returned observation IDs. The /v1/mcp handler skips this entirely. Because the backend closures (search / recent) are called deep inside the MCP SDK dispatch loop — not from this handler — there is no natural place to attach a post-query hook without threading audit context down into RecallBackend. As a result every observation read through the MCP endpoint leaves no audit trail, even though the PR description explicitly states it "reads identical data through identical guards."

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

// Same readAuth (memories:read) + team/project scoping as /v1/search, so it
// reads identical data through identical guards. Stateless streamable-HTTP:
// one transport + server per request, bound to this key's team.
app.all('/v1/mcp', readAuth, this.asyncHandler(async (req, res) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 app.all exposes the MCP endpoint to all HTTP methods

The MCP streamable-HTTP protocol only uses POST (JSON-RPC) and GET (SSE). Using app.all additionally routes DELETE, PUT, PATCH, HEAD, and OPTIONS through the full auth middleware before the transport can reject them. CORS preflight OPTIONS requests will fail with 401 because browsers omit the Authorization header on preflights — silently breaking any browser-based MCP client. Restricting to the two methods the protocol actually uses is safer.

Suggested change
app.all('/v1/mcp', readAuth, this.asyncHandler(async (req, res) => {
app.route('/v1/mcp')
.get(readAuth, this.asyncHandler(async (req, res) => { await handleMcp(req, res); }))
.post(readAuth, this.asyncHandler(async (req, res) => { await handleMcp(req, res); }));

…sport)

Covers the gap the unit test can't: the real streamable-HTTP wiring through
Express 5 + readAuth + Postgres. Boots the actual server, seeds observations,
and drives /v1/mcp with a genuine MCP HTTP client carrying a Bearer cm_ key —
asserts tool listing, team-scoped recent/search, and a 401 for an
unauthenticated connection. Postgres-gated (CLAUDE_MEM_TEST_POSTGRES_URL),
mirroring server-mcp-routes.test.ts.

Verified live: 4/4 pass against a real Postgres 17 + the MCP streamable-HTTP
client. tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ChenglinWei97

Copy link
Copy Markdown
Author

Update: the "one live check" caveat is done. Added tests/server/runtime/server-mcp-http-routes.test.ts — a Postgres-gated integration test that boots the real server and drives /v1/mcp with a genuine MCP streamable-HTTP client (Bearer cm_ key), mirroring server-mcp-routes.test.ts.

Verified live: 4/4 pass against real Postgres 17 + the HTTP transport — tool listing, team-scoped recent/search, and a 401 for unauthenticated connections. So the Express 5 + readAuth + streamable-HTTP wiring is proven end-to-end, not just the tool logic.

(CI's action_required is just the first-time-fork-contributor gate — needs a maintainer to approve the workflow run.)

Adds the connect command (claude mcp add --transport http ... /v1/mcp), the
read-only tool list (search/context/recent), auth (memories:read, team-scoped),
and the stateless-transport note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants