Connect MCP clients to OpenMed¶
OpenMed exposes its clinical NLP, PII extraction, and de-identification tools through the Model Context Protocol (MCP). Use the openmed-mcp command for local stdio clients or the Streamable HTTP transport for clients that connect to a URL.
All examples on this page use synthetic text. Only send real PHI to an OpenMed runtime that you operate and trust.
Install and verify¶
Install OpenMed with the optional MCP dependency:
For a checkout managed with uv:
Run in a container¶
Build the dedicated MCP image from a checkout:
The image defaults to stdio. Keep stdin attached with -i and do not allocate a TTY, because MCP protocol messages use stdin and stdout:
A command-map client can launch that container directly:
{
"mcpServers": {
"openmed-container": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-v",
"openmed-hf-cache:/root/.cache/huggingface",
"openmed-mcp:local"
]
}
}
}
For a local Streamable HTTP endpoint, override the image's default command. The server must listen on all container interfaces, while the published port remains limited to host loopback:
docker run --rm \
-p 127.0.0.1:8081:8081 \
-v openmed-hf-cache:/root/.cache/huggingface \
openmed-mcp:local \
--transport streamable-http \
--host 0.0.0.0 \
--port 8081 \
--streamable-http-path /mcp
The equivalent Compose service includes the same loopback port binding, a listener health check, and a persistent model cache:
Connect remote-style local clients to http://127.0.0.1:8081/mcp. Set OPENMED_MCP_PORT before starting Compose to select a different host port; the container endpoint remains on port 8081.
Local stdio connections¶
Stdio is the recommended transport when the MCP client and OpenMed run on the same machine. The client launches the server as a child process, so no TCP port is exposed.
JSON command-map clients¶
Use this shape for desktop and IDE clients that accept an mcpServers command map:
{
"mcpServers": {
"openmed": {
"command": "uvx",
"args": [
"--from",
"openmed[mcp]",
"openmed-mcp",
"--transport",
"stdio"
]
}
}
}
If OpenMed is already installed in a virtual environment, set command to the absolute path of that environment's openmed-mcp executable and omit the uvx, --from, and package arguments.
TOML server-table clients¶
Terminal coding clients commonly use a TOML server table:
[mcp_servers.openmed]
command = "uvx"
args = ["--from", "openmed[mcp]", "openmed-mcp", "--transport", "stdio"]
Typed local blocks¶
Clients that distinguish local and remote servers with a type field usually accept the equivalent typed block:
{
"name": "openmed",
"type": "stdio",
"command": "uvx",
"args": ["--from", "openmed[mcp]", "openmed-mcp", "--transport", "stdio"]
}
Client field names vary, but the command and argument sequence is the same.
Streamable HTTP connections¶
Start a loopback-only server for a local URL-based client:
openmed-mcp \
--transport streamable-http \
--host 127.0.0.1 \
--port 8081 \
--streamable-http-path /mcp
The MCP endpoint is http://127.0.0.1:8081/mcp. A typed remote configuration typically looks like this:
Hosted-assistant developer connectors use the same URL. They cannot reach a loopback address on your workstation; deploy through a private network or an authenticated HTTPS gateway instead of exposing the OpenMed process directly.
Journey workflow tools and generated clients¶
Six fixed-resource, read-only tools expose journey, cohort, dataset, registry, measure, and trial-review workflows. Their schemas are derived from the same versioned Journey page contract used by REST, GraphQL, and read-only SQL:
openmed_read_journeyopenmed_read_cohortopenmed_read_datasetopenmed_read_registryopenmed_read_measureopenmed_read_trial_review
Every result carries evidence identifiers, schema and immutable snapshot versions, the access-policy decision, controlled warnings, and explicit review metadata. The result contract excludes raw source text. All six tools advertise readOnlyHint=true, destructiveHint=false, and closed-world execution.
The machine-readable generation contract is available at openmed://journey-workflows. Regenerate the Python and TypeScript client surfaces after changing a Journey workflow schema:
python scripts/generate_journey_workflow_clients.py
python scripts/generate_journey_workflow_clients.py --check
Python clients expose journey(), cohort(), dataset(), registry(), measure(), and trial_review(). The TypeScript client exposes the same names, with trialReview() using normal TypeScript casing.
The registry document marks every tool as either state-changing or read-only. State-changing tools continue through the signed, single-use consent-receipt verification path; the read-only Journey tools never accept a receipt as a substitute for access policy.
Governed workflow observations and review¶
Four tools reference an action already held by a caller-injected local GovernedMCPService. They are registered from the canonical tool registry:
| Tool | Behavior | Read-only | Destructive |
|---|---|---|---|
openmed_workflow_preflight | Observe capability-grant, purpose-ticket and minimum-data projection decisions | Yes | No |
openmed_workflow_preview | Inspect proposed effect counts without clinical values or targets | Yes | No |
openmed_workflow_status | Poll run phase and action-bound receipt-verification observations | Yes | No |
openmed_workflow_request_review | Create a human reviewer handoff after server-held consent | No | No |
Each tool accepts only request, an object with exactly these five fields:
{
"schema_version": "openmed.mcp.governance_request.v1",
"run_id": "run_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"workflow_id": "workflow:org.example/synthetic@1.0.0",
"action_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"expected_state_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
The three read tools may use null for expected_state_digest. Review requests require the exact current state digest. Run IDs must be opaque, and workflow IDs must be developer-owned names; neither may encode patient information. The service resolves the existing action, policy and purpose from its own custody. MCP callers cannot upload a grant, purpose ticket, approval token, consent receipt, reviewer identity, role claim or credential. Unknown fields, including inline authority, return invalid_arguments without echoing their values. Canonical workflow identifiers use the existing 512-character bound. Raw handler operation selection is fixed when the tool is registered; caller arguments cannot select another service method. Public errors discard foreign diagnostic fields and private exception contexts.
Results use openmed.mcp.governance_result.v1 and contain only bound run/workflow references, action/state digests, phase/outcome codes, closed decisions for grant, purpose_ticket and projection, bounded proposed/committed effect counts, a receipt observation, and an optional review-request digest. Receipt observations are absent, unverified, verified, expired or conflict; a verified observation reports the service's durable check for this action and does not enable execution. The three authority decisions are allowed, denied or unavailable. No decision field transmits authority.
Configure create_mcp_server(governance_service=...) with trusted local custody. No governance adapter is loaded from environment variables or downloaded; an unconfigured tool returns governance_unavailable. Review requests additionally need governance_consent_policy (a receipt-required ConsentReceiptPolicy) and governance_consent_receipt_provider, a trusted local callback retrieving a human-issued, single-use consent receipt bound to the exact review tool and request. These are server configuration, not MCP arguments. The default policy has no keys and requires a receipt, so review is denied by default. A permissive legacy policy cannot weaken this boundary. Registration rejects missing or disabled governance consent enforcement, incorrect behavioral annotations and substituted governance schema versions or contracts.
A review request first checks the run/action/state and terminal or denied outcomes, then verifies human-channel consent, then delegates one atomic handoff creation. The service must repeat the state comparison at dispatch. Its ReviewerHandoffPacket must bind the same run/workflow and contain an evidence reference with schema openmed.agent.action.v1 and the exact action SHA-256. MCP returns only the packet digest. No tool approves, resumes, cancels or executes an action, or mutates a FHIR/OMOP target. The service retains the packet for the human channel; clinical values, receipt signatures and reviewer identities never enter an MCP result.
Stale state or mismatched run/action responses return governance_conflict; refused state returns governance_denied. Missing consent returns consent_required, failed verification returns the existing consent error, untyped/malformed acknowledgements return governance_invalid_result, and unexpected adapter failures return governance_failed. Diagnostic messages are fixed. Adapter standard output/error is suppressed, but adapters are trusted code and must keep their own logging value-free; this is not a sandbox.
A failed, expired or malformed acknowledgement after handoff creation can have unknown commit state. Reconcile with status and service custody; never retry automatically or treat the failure as proof that nothing was created. Consent is consumed before handoff dispatch. Services own durable idempotency and action authority checks, independently of MCP transport authentication.
The offline authorization matrix includes these tools. Synthetic real SDK sessions verify preflight, preview, review and polling without network access. The default adversarial corpus attempts inline instructions, hostile tool results, catalog substitution, delegation scope amplification and other forbidden fields through the actual dispatcher, under both strict and allow injection-guard modes; malformed authority still fails schema validation in allow mode. Its benign proposal is registered in service custody and referenced only by digest. These bounded controls do not establish universal prompt-injection resistance or clinical validation.
Fixed-option decisions¶
openmed_decide scores bounded caller-supplied options, preserves caller ordering, applies the selected calibration profile, and returns typed abstention, denial, conflict, unsupported, timeout, and failure states. It is a read-only, non-destructive, idempotent, closed-world tool. The result always requires human review and never authorizes a clinical action. Its request and result schemas are identical to the Python and REST contracts described in Fixed-option decision API.
Canonical clinical agent workflow¶
MCP clients can discover the openmed-clinical-workflow prompt, the openmed://clinical-workflow guidance resource, and the openmed://clinical-workflow/golden-agent-run synthetic fixture through the standard MCP prompt and resource listings. The openmed://tool-registry document also publishes a workflows entry with the prompt, resources, tool names, stage order, and registered artifact schema identifiers, so clients do not need to hardcode the workflow contract.
The workflow de-identifies first, then executes the canonical pipeline order: detect, context, sections, relations, ground, export, and risk. Each stage produces a registered artifact:
| Artifact | Contents allowed after the de-identification boundary |
|---|---|
deidentify | De-identified text and a source-text hash |
detect | Canonical text-free spans, model identifier, and count |
context | Canonical spans with clinical context metadata |
sections | Canonical spans and surface-free section offsets |
relations | Canonical spans and surface-free relation endpoints |
ground | Terminology codes, scores, hashes, and provenance |
export | Identifier-free FHIR Bundle |
risk | Aggregate residual-risk summary |
Execution is local and has zero network egress by default. Keep source text, direct identifiers, entity surfaces, and reversible mappings inside the trusted local runtime. Anything after the de-identification boundary may carry only de-identified text, offsets, hashes, canonical spans, terminology provenance, identifier-free FHIR resources, and aggregate risk values. Do not write source text or identifiers to logs or agent traces.
External-LLM-capable stages remain disabled unless an operator explicitly opts in, and they must route through the OpenMed privacy gateway. Raw source text must never cross that boundary. Grounded codes and exported resources require human review and must not automatically trigger clinical, treatment, billing, or medical-device decisions.
Remote authentication and protocol headers¶
The built-in MCP server does not validate API keys or bearer tokens. Never bind it to a public interface without a TLS-terminating reverse proxy or gateway that authenticates every request. Configure the client to send the gateway's bearer token, for example:
{
"name": "openmed",
"type": "http",
"url": "https://openmed.example.org/mcp",
"headers": {
"Authorization": "Bearer ${OPENMED_MCP_TOKEN}"
}
}
Keep the token in the client's secret or environment-variable store rather than committing it to a configuration file.
Streamable HTTP clients also send the MCP-Protocol-Version header on requests after initialization. SDK-based clients set it automatically to the version negotiated during the initialize exchange. If a gateway allowlists headers, forward MCP-Protocol-Version, Mcp-Session-Id, Content-Type, Accept, and Authorization; do not replace the negotiated protocol version with a fixed value at the proxy.
Structured tool errors¶
Expected OpenMed failures return structured content with is_error: true and the same stable code exposed by the Python API and REST service:
{
"error": {
"code": "input_error",
"message": "The request input is malformed. Correct the documented field and retry.",
"details": {"argument": "text"}
},
"is_error": true
}
Branch on error.code, not the human-readable message. Error payloads never echo tool arguments, clinical text, mappings, credentials, or upstream exception text. Security, authorization, consent, and tool-schema errors retain their existing specialized codes. See Structured public errors for the complete Python/REST/MCP mapping.
Environment-variable defaults¶
The command-line flags can also be configured with environment variables:
| Variable | Default | Purpose |
|---|---|---|
OPENMED_MCP_TRANSPORT | stdio | stdio or streamable-http |
OPENMED_MCP_HOST | 127.0.0.1 | HTTP bind address |
OPENMED_MCP_PORT | 8081 | HTTP port |
OPENMED_MCP_PATH | /mcp | Streamable HTTP endpoint path |
Command-line flags override these values. Keep OPENMED_MCP_HOST=127.0.0.1 unless an authenticated network boundary is already in place.
Troubleshooting¶
- Command not found: install the
mcpextra and ensure the selected Python environment's executable directory is onPATH. - Client starts and immediately disconnects: keep stdio reserved for MCP protocol messages; avoid wrapper scripts that print banners to stdout.
- HTTP client receives 404: confirm the URL includes the configured
--streamable-http-path, which defaults to/mcp. - Remote client cannot connect: loopback is intentionally local-only. Use a private route or authenticated HTTPS gateway rather than changing the bind address without access controls.