Repository navigation
Conversation
WebAPI can now serve the Model Context Protocol at POST /mcp (Spring AI 1.1.8, stateless Streamable HTTP), off by default behind spring.ai.mcp.server.enabled. It sits behind the REST API's security chain, so agents authenticate with a JWT or X-API-KEY and every tool runs under that user's existing @PreAuthorize rules. Tools cover the phenotype workflow: vocabulary search and hierarchy, concept set editing, cohort definition create/update, Circe checks plus SqlRender translation, cohort generation and status with attrition. Calls are made traceable on the server side rather than trusted to the client: - tools that change concept sets or cohorts require clinical_rationale and evidence_reference; - concept ids are re-checked against the vocabulary by id and name before anything is saved, so assumed ids are refused; - _meta.traceparent is continued with a new span per call and returned in the result's _meta; - each call is written to mcp_audit_log before it runs and completed with its outcome; a trigger makes the table append-only, and get_audit_trail reads a trace back as a decision log. The MCP server bean is excluded from lazy initialization; left lazy it is never created and /mcp cannot answer.
McpServerIT streams person, observation_period, condition_occurrence, concept and concept_ancestor from src/test/resources/eunomia.csv.zip into the embedded CDM with COPY. Through MCP it creates a cohort for the first gastrointestinal hemorrhage with an "Age 40+" inclusion rule, generates it, and polls get_cohort_generation_status until COMPLETE. The counts and attrition it reports (479 at base, 204 kept by the rule) are checked against the same cohort counted with plain SQL on the CDM, and against the subjects written to results.cohort.
watilde
force-pushed
the
feature/mcp-server
branch
from
November 4, 2026 12:00
9bae5c1 to
b3dee5f
Compare
Atlas needs to show, on a cohort definition or concept set, which agent
calls built it and why. The audit log only had the trace, so finding the
calls about one artifact meant searching result_json.
- mcp_audit_log gets entity_type and entity_id (with an index). Tools
that create, change, validate or generate a cohort definition or
concept set declare their entity; the id is taken from the arguments
before the call runs, or for a create from the result once it has.
The append-only trigger lets the entity be filled in once, never
changed. Read-only tools (get_*, counts, status polls) record none.
- GET /cohortdefinition/{id}/mcp-provenance and
GET /conceptset/{id}/mcp-provenance return those calls in order: tool,
reasoning step, rationale, evidence, outcome, user, times and trace.
They use the artifact's own read permission, leave out the argument
and result payloads, and stay available when MCP is turned off.
3 of 4 tasks
watilde
added a commit
to watilde/Atlas3
that referenced
this pull request
Nov 4, 2026
The id is always the one in WebAPI's mcp_audit_log. It matches spans in a tracing backend only when WebAPI runs with the OpenTelemetry Java agent, which OHDSI/WebAPI#2580 now records the exported span ids for.
The tool-call span ids were WebAPI's own and never exported, so an audit row's trace id could not be found in a tracing backend. Also, the OpenTelemetry Java agent traced the HTTP request separately, under the traceparent header rather than the one MCP carries in _meta. Follow the OpenTelemetry semantic conventions for MCP and SEP-414 instead: each tools/call is a SERVER span "tools/call <tool>". Its parent is the W3C context in params._meta, it links the ambient (HTTP request) span, and it is current while the tool runs, so the agent's JDBC spans nest under it. The audit row and the result's _meta.traceparent carry that span's ids, so the audit log and the tracing backend share trace ids. Only opentelemetry-api is added (version from Spring Boot). With the agent it is the agent's implementation; without it, it is a no-op and WebAPI allocates the ids as before. Span attributes are mcp.method.name, gen_ai.tool.name, gen_ai.operation.name, error.type=tool_error and WebAPI's step, outcome, audit entry id and entity. The arguments, rationale and error messages stay in the audit log.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a Model Context Protocol server to WebAPI so AI agents can run the phenotype workflow through WebAPI itself: search vocabularies, build concept sets and cohort definitions, validate the cohort SQL, generate cohorts and read back counts. Each call is traced and recorded in an audit log on the server.
It is off by default (
spring.ai.mcp.server.enabled: false). When enabled it servesPOST /mcp(Spring AI 1.1.8, stateless Streamable HTTP). The endpoint is behind the same security chain as the REST API, so clients authenticate with a JWT orX-API-KEY, and every tool runs under that user's existing@PreAuthorizerules.Tools
list_sources,search_concepts,get_concept_details,get_concept_hierarchycreate_concept_set,get_concept_set,add_concept_to_set,remove_concept_from_set,count_concept_set_conceptscreate_cohort_definition,update_cohort_definition,get_cohort_definitionvalidate_cohort_definition: Circe checks, concept verification, SqlRender translation. The SQL is not executed.generate_cohort,get_cohort_generation_status: status, counts, inclusion-rule attritionget_audit_trailRules the server enforces
clinical_rationale(at leastmcp.minRationaleLengthcharacters, default 20) andevidence_reference. A call without them is refused._meta.traceparent, the HTTP request's span is linked, and the span is current while the tool runs, so its SQL nests under it. The result's_metacarries the span's traceparent andaudit_entry_id. A call without a traceparent starts a new trace (setmcp.requireTraceparent: trueto refuse it). Onlyopentelemetry-apiis added. With the OpenTelemetry Java agent the span is exported and the audit row stores its ids, so the audit log and the tracing backend share trace ids. Without the agent the API is a no-op and WebAPI allocates the ids itself. Span attributes are the tool, step, outcome, audit entry id and entity. The arguments, rationale and error messages are not exported, because the tracing backend has no per-artifact access control and may sample.mcp_audit_logtable before it runs, then completed with its outcome:SUCCESS,ERROR,REJECTEDorDENIED. A trigger makes the table append-only.get_audit_trailreturns a trace's entries in order, as a decision log for that user.entity_type,entity_id).GET /cohortdefinition/{id}/mcp-provenanceandGET /conceptset/{id}/mcp-provenancereturn that history to anyone who can read the artifact, without the argument or result payloads, so Atlas can show why an agent built it that way. These endpoints are not switched off with the MCP server.Implementation notes
/mcpwould not respond.ObjectMapper. Spring AI'smcpServerObjectMapperis not a default candidate, andMcpServerITchecks this.Known limitation
generate_cohortqueues a batch job, and the OpenTelemetry agent carries the submitting request's context into the job's thread pool. The generation SQL therefore lands in the tool call's trace. This is the same limitation the Atlas3 observability setup documents. A separate root span per job, linked to the request, is #2557.Test plan
TraceContextTest,McpToolFactoryTest: traceparent parsing, rationale enforcement, audit-before-run, outcome mapping; OpenTelemetry span against an in-memory SDK (parent from_meta, link to the HTTP span, joining the HTTP trace without_meta,tool_erroron refusal, no rationale in attributes, audit row stores the exported ids) and the no-op fallbackMcpServerIT: JSON-RPC over HTTP against embedded Postgres. Covers tools/list, trace continuation, the audit row, refusal of mismatched concept names and of missing rationale, the append-only trigger, SQL translation, unverified concepts in cohorts, and calls without a traceparentMcpServerITprovenance: a concept set's and a cohort'smcp-provenancelist their create/change/generate calls (including a refused one) and leave out read-only calls and payloadsMcpServerITend-to-end generation on the bundled Eunomia CDM (src/test/resources/eunomia.csv.zip, loaded with COPY):generate_cohortruns the batch job for a first-GI-hemorrhage cohort with an "Age 40+" rule, andget_cohort_generation_statusreports COMPLETE with counts and attrition (479 at base, 204 kept) that match the same cohort counted with plain SQLmvn test: 176 + 46 tests, 0 failures (before the generation IT was added;McpServerITwas rerun with it: 12/12)generate_cohortagainst a full-size CDM