feat(mcp): serve the read-only tools over remote Streamable HTTP at /mcp

Bridge the existing transport-agnostic MCP server factory to a Next.js
route at /mcp using the SDK's Web-standard Streamable HTTP transport, so a
remote client can connect with a manually-minted hlk_ Bearer token. The
same tool/resource layer the local stdio transport serves is reused
verbatim — no duplicated tool logic.

The route owns four gates in front of the transport:

- Auth: Authorization: Bearer hlk_… resolved by the same validator the
  HTTP edge uses, so revoked/expired/scope semantics never drift. A cookie
  is never consulted, so the cookie-only admin boundary stays unreachable
  here regardless of token scope.
- Rate limit: the Postgres limiter keyed by the <userId>:<tokenId> binding
  (mcp:<binding>), so a single shared credential cannot drain the account.
- Module gate: a new opt-in mcp module, OFF by default. With it off the
  endpoint answers 404 and reveals nothing — a self-hoster who never opts
  in gets zero new exposure. The Modules settings hub renders the toggle.
- Transport: a fresh stateless server + transport per request.

Read-only by construction (only read tools are registered) and same-origin
by construction (no CORS headers). The proxy lets /mcp skip the page cookie
redirect since it authenticates by header, mirroring the webhook surfaces.
This commit is contained in:
Marc-André Bombeck committed 2026-06-27 13:47:24 +02:00
1 parent d947bb1104
commit 03d41bc5c0
17 files changed
+599 -2

No files matched your search

+52
View File
@@ -21,6 +21,7 @@ import {
import {
isCoreDomain,
isModuleKey,
isOptInModule,
moduleDelegatesTo,
MODULE_KEYS,
CORE_DOMAIN_KEYS,
@@ -309,6 +310,57 @@ describe("resolveModuleEnabled — operator layer (two-layer AND)", () => {
});
});
describe("resolveModuleEnabled — mcp module (opt-in, default-OFF)", () => {
it("is OFF when no preference is recorded (inverse of the default-on siblings)", () => {
expect(resolveModuleEnabled("mcp", inputs(), false, ALL_AVAILABLE)).toBe(
false,
);
});
it("stays OFF on an explicit false", () => {
expect(
resolveModuleEnabled(
"mcp",
inputs({ modulePreferences: { mcp: false } }),
false,
ALL_AVAILABLE,
),
).toBe(false);
});
it("turns ON only on an explicit true (opt-in)", () => {
expect(
resolveModuleEnabled(
"mcp",
inputs({ modulePreferences: { mcp: true } }),
false,
ALL_AVAILABLE,
),
).toBe(true);
});
it("operator-off short-circuits even when the user opted in", () => {
expect(
resolveModuleEnabled(
"mcp",
inputs({ modulePreferences: { mcp: true } }),
false,
operator({ mcp: false }),
),
).toBe(false);
});
});
describe("registry — opt-in marker", () => {
it("marks only the mcp module opt-in (every other module is default-on)", () => {
expect(isOptInModule("mcp")).toBe(true);
for (const key of MODULE_KEYS) {
if (key === "mcp") continue;
expect(isOptInModule(key)).toBe(false);
}
});
});
describe("resolveOperatorAvailability — disabled allowlist (default-available)", () => {
it("treats null / non-object / array as all-available", () => {
for (const raw of [null, undefined, "nope", ["mood"]]) {
+13 -1
View File
@@ -53,7 +53,12 @@ import {
getOperatorModuleAvailability,
type OperatorModuleAvailability,
} from "./operator-availability";
import { MODULE_KEYS, moduleDelegatesTo, type ModuleKey } from "./registry";
import {
MODULE_KEYS,
isOptInModule,
moduleDelegatesTo,
type ModuleKey,
} from "./registry";
export {
getOperatorModuleAvailability,
@@ -142,6 +147,13 @@ export function resolveModuleEnabled(
return assistantCoach && !inputs.disableCoach;
}
// Opt-in module (e.g. the remote MCP endpoint): OFF until the user
// records an explicit `true`. The inverse of the default-on posture so a
// new external surface ships dark (ADR-007 / REQ-OPS-1).
if (isOptInModule(key)) {
return inputs.modulePreferences[key] === true;
}
// Disabled allowlist: only an explicit `false` turns the module off.
return inputs.modulePreferences[key] !== false;
}
+39 -1
View File
@@ -49,7 +49,8 @@ export type ModuleCategory =
| "device" // device / passive-sync domains (sleep, workouts, recovery)
| "engagement" // gamification (achievements)
| "intelligence" // AI-driven surfaces (coach, insights)
| "export"; // outbound reporting (doctor report / FHIR)
| "export" // outbound reporting (doctor report / FHIR)
| "integration"; // external connectivity (the remote MCP endpoint)
export interface ModuleDefinition {
/** Stable key — the literal used in `modulePreferencesJson` + the API. */
@@ -66,6 +67,17 @@ export interface ModuleDefinition {
* there is exactly one source of truth. See `ModuleDelegation`.
*/
delegatesTo?: ModuleDelegation;
/**
* Inverts the per-user default for this key. Ordinary toggleable modules
* are a DISABLED allowlist (default-on; only an explicit `false` turns
* them off). An `optIn` module is the opposite: OFF until the user
* records an explicit `true`. Used for surfaces that expose a new
* external attack surface and must ship dark — the remote MCP endpoint
* (ADR-007 / REQ-OPS-1). The operator-availability layer still applies
* (an operator can disable it server-wide); this flag only flips the
* per-user default.
*/
optIn?: boolean;
/**
* For delegated modules only: where the real on/off control lives, so the
* Modules hub can render a read-only "managed in X" row that deep-links to
@@ -103,6 +115,14 @@ export const MODULE_KEYS = [
// re-enabling finds the rows intact.
"medications",
"doctorReport",
// v1.22.0 — the remote Model Context Protocol endpoint (`/mcp`). Unlike
// every other module this is OPT-IN (off by default): it exposes a new
// external-assistant attack surface, so it ships dark and the operator /
// user turns it on deliberately (ADR-007 / REQ-OPS-1). The `optIn` marker
// on its registry entry inverts the per-user default; everything else
// (operator-availability layer, PATCH plumbing, the Modules hub toggle)
// reuses the existing module machinery unchanged.
"mcp",
] as const;
export type ModuleKey = (typeof MODULE_KEYS)[number];
@@ -236,6 +256,15 @@ export const MODULE_REGISTRY: Readonly<Record<ModuleKey, ModuleDefinition>> =
descriptionKey: "modules.doctorReport.description",
category: "export",
},
mcp: {
key: "mcp",
labelKey: "modules.mcp.label",
descriptionKey: "modules.mcp.description",
category: "integration",
// Off by default: exposes a remote external-assistant surface, so it
// must be turned on deliberately (ADR-007 / REQ-OPS-1).
optIn: true,
},
});
const MODULE_KEY_SET: ReadonlySet<string> = new Set(MODULE_KEYS);
@@ -251,3 +280,12 @@ export function moduleDelegatesTo(
): ModuleDelegation | undefined {
return MODULE_REGISTRY[key].delegatesTo;
}
/**
* True for a module whose per-user default is OFF (opt-in). The gate reads
* such a key as enabled only on an explicit `true`, the inverse of the
* disabled-allowlist default-on posture. See `ModuleDefinition.optIn`.
*/
export function isOptInModule(key: ModuleKey): boolean {
return MODULE_REGISTRY[key].optIn === true;
}