Prototype for cohort-level temporal queries over cBioPortal clinical timeline events (msk_chord_2024). Users define cohorts by relationships between events — "progression within 180 days of starting immunotherapy", "first chemo NOT followed by progression in a year" — and the server compiles that DSL to ClickHouse SQL.
app/page.js— query-builder UI (selectors, temporal relations, recipes, treatment-lines explorer)app/api/*— API routes; the browser sends DSL JSON only, never SQLlib/compiler.js— DSL → ClickHouse SQL compiler (selectors, BEFORE/DURING/NOT_FOLLOWED_BY relations, occurrence pinning, observability guard, derived treatment-line events)lib/ch.js— server-side ClickHouse HTTP client; credentials come from env and never reach the client
The query language is documented in docs/dsl.md — selectors, temporal
relations, guards, derived treatment-line events, semantics, and worked examples with real
msk_chord_2024 counts. A machine-readable contract is in
docs/cohort-query.schema.json (JSON Schema 2020-12).
Every setting is in docs/ENVIRONMENT.md, grouped by what it controls:
data, federation, sign-in and saved data, the site, the assistant, performance, building, and
the test and check scripts. To run locally you need only CLICKHOUSE_URL, CLICKHOUSE_USER,
CLICKHOUSE_PASSWORD and CLICKHOUSE_DB in .env.local; on Vercel they go in Project
Settings → Environment Variables.
One analysis can draw on studies held by more than one cBioPortal database, and the prototype
offers that over MCP: the database that holds a study answers aggregate queries (and, opted
in, some patient-level ones) through its /api/mcp, and this app never queries it directly.
docs/DATA-BOUNDARY.md is the design and its rules.
There are two ways to run it:
- A simulated peer (the public portal, dev servers, the gate): one deployment holds both
databases and serves the second one to itself over MCP (
LOOPBACK_PEER). Every query on that database runs only while the app is answering an MCP call as that peer; a direct query on it, or one naming its database on the other connection, is refused (lib/ch.ts). It exercises the real protocol with no second deployment, but it is not separation: one process, one login. - A separate peer: a second cbio-lab instance whose own database is the other one, reached at
its
/api/mcp(PEER_MCP). This instance then holds no connection to that database at all.
Its settings, and how to set it up for the public portal, dev servers and a separate peer, are
in docs/ENVIRONMENT.md.
Every view either draws over every patient of its cohorts or is not drawn and says why
(lib/view-coverage.ts); the add-view buttons and cohort pickers are disabled with the reason.
| over MCP, aggregates | over MCP, patient-level (timeline, alterations shared) |
|
|---|---|---|
| drawn | compare, survival, cooccurrence, coexpression, clinical, hotspots, attrition, course, sequence; Explore, study list, catalog | the same, plus timeline and alterations |
| not drawn over the peer's studies | oncoprint, clinical data table, mutation table, cox, logistic, risk model, plot, genome, pathways, enrichment, overlap, incidence, cancer types, clonality, paired, diffexpr, correlates, assays, labs, composition | the same |
Measured cost (docs/DATA-BOUNDARY.md, "Federation: where it stands"): about +0.3–0.4 s per
analysis for aggregate views on a small study, about +2 s for the patient-level views; on a large
study the database's own time dominates.
- No caller authentication yet. Anyone who can reach a peer's
/api/mcpcan call it. On a simulated peer the patient-level tools answer only the app itself (the token); a separate peer answers anyone, so it must not be reachable from outside. - The boundary is enforced by the coordinator: a peer's own
run_analysis,query_cohortand web API answer patient-level for its own studies to any caller. Fine for public data; a peer serving restricted data needs caller authentication and those closed first. - There is no small-cell suppression: counts are exact.
The MCP endpoint ships inside the Next.js app at /api/mcp (app/api/[transport]/route.js
via mcp-handler), so a Vercel deploy serves it automatically. npm run mcp runs the same tools
as a standalone Streamable HTTP server (default 127.0.0.1:3201/mcp). Both expose four
tools to LLM clients: describe_study, query_cohort (the DSL), derive_treatment_lines, and
render_oncoprint (returns a PNG image block rendered server-side from the OncoprintSpec
contract via lib/oncoprint-svg.js). Tool schemas mirror docs/cohort-query.schema.json, and
results carry the semantic caveats (observability guard, prior-medication guard, panel-aware
denominators) for the model to surface. Register in Claude Code:
claude mcp add --transport http cohort-lab https://<your-deployment>/api/mcpmcp/test-client.mjs exercises all four tools end to end.
npm install
npm run dev # or: npm run build && npm starte2e.js is a Playwright suite that exercises the full flow against the live backend
(requires a running server on :3100 and a local Chromium).