@interop-gateway/mcp
v1.0.0
Published
MCP tool surface over interop-gateway's translate/validate, SMART on FHIR connector, MLLP send, and pipeline runtime.
Maintainers
Readme
@interop-gateway/mcp
MCP tool surface over interop-gateway —
lets an MCP client (an AI assistant, an agent framework) translate HL7v2/C-CDA, check
structural and US Core conformance, connect (backend-services or an interactive SMART
App Launch) to and read/write a live FHIR R4 server, run a Bulk Data $export, send an
HL7v2 message over MLLP, and start/stop an engine pipeline — all through tool calls,
without the client needing to know anything about the underlying formats or protocols.
Install
Not yet published to npm — see the root README for building from source, or "Running from a local build" below for running the compiled server directly.
npm install @interop-gateway/mcpRun as a standalone MCP server (stdio)
npx @interop-gateway/mcpPoint any MCP client at this command over stdio.
Running from a local build (not yet published to npm)
@interop-gateway/mcp isn't on the npm registry yet — the npx command above
will 404 until it is. Until then, build it from this repo and point a client at the
built file directly:
git clone https://github.com/heyitskundan/interop-gateway.git
cd interop-gateway
npm install
npm run build -w packages/mcpClaude Code:
claude mcp add interop-gateway -- node /absolute/path/to/interop-gateway/packages/mcp/dist/cli.jsClaude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"interop-gateway": {
"command": "node",
"args": ["/absolute/path/to/interop-gateway/packages/mcp/dist/cli.js"]
}
}
}Either way, the client sees the same tools listed below once connected.
Use programmatically
import { createInteropGatewayMcpServer } from "@interop-gateway/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// Defaults to a FileAuditLog persisted at ./mcp-audit — set
// persistence.audit.encryptPassphrase or this throws (see "Persistence" below).
const server = await createInteropGatewayMcpServer({
persistence: { audit: { encryptPassphrase: process.env.MCP_AUDIT_PASSPHRASE! } },
});
await server.connect(new StdioServerTransport());createInteropGatewayMcpServer() returns a plain McpServer from the official SDK
wrapped in a Promise (resolving the default audit sink is async) — connect it to any
Transport (stdio, an InMemoryTransport in tests, or a custom one). Every tool call
gets a correlation ID (@interop-gateway/core's createEnvelope) and writes an audit
entry to the resolved auditSink — who: "mcp", what the tool name (suffixed
:rejected on failure), resourceType when known. run_pipeline passes this same
auditSink through to every pipeline it starts, so a pipeline's own translate/
deliver events land in this log too, rather than each pipeline silently keeping its
own separate one.
Persistence
Same rules as @interop-gateway/engine's runPipeline() — persistence is the default,
encryption is required unless you explicitly opt out:
// Default: FileAuditLog at ./mcp-audit, throws without a passphrase
await createInteropGatewayMcpServer({
persistence: { audit: { encryptPassphrase: "..." } },
});
// A custom directory, and a dead-letter queue for run_pipeline's pipelines
// (deadLetterQueue stays opt-in — omit persistence.deadLetter for none, same as engine)
await createInteropGatewayMcpServer({
persistence: {
audit: { directory: "/var/mcp/audit", encryptPassphrase: "..." },
deadLetter: { directory: "/var/mcp/dead-letters", encryptPassphrase: "..." },
},
});
// Explicitly accept plaintext-on-disk instead of encrypting
await createInteropGatewayMcpServer({ allowUnencryptedPersistence: true });
// Tests/quick demos — in-memory only, the old default, no dead-letter queue either
await createInteropGatewayMcpServer({ ephemeral: true });
// Bring your own AuditSink/DeadLetterQueue directly — bypasses all of the above
import { HashChainedAuditLog } from "@interop-gateway/core";
await createInteropGatewayMcpServer({ auditSink: new HashChainedAuditLog() });Tools
Static — never leave the process:
translate—{ format: "hl7v2" | "cda", payload: string }→ the translated FHIR R4 Bundle as JSON text. On a translation failure, returnsisError: truewith the failure message as content instead of throwing.validate—{ payload: string }→ aStructuralValidationResult(from@interop-gateway/core) as JSON text: whether the input is a structurally well-formed HL7v2 message or C-CDA document, and why not if it isn't.validateUsCore—{ payload: string }(a FHIR resource or Bundle as a JSON string, typicallytranslate's own output) → aUsCoreValidationResult(single resource) orUsCoreValidationResult[](Bundle) from@interop-gateway/coreas JSON text. Required-element presence, max-cardinality shape, and fixed-code-value binding forstatus/intent/lifecycleStatusfields — not a terminology-binding validator for external code systems (LOINC/SNOMED/RxNorm). ReturnsisError: truefor non-JSON input instead of throwing.
Live — read/write a real FHIR server, send a real network message, or run a listening pipeline. A different trust boundary than the tools above.
connect_ehr—{ baseUrl: string, auth: AuthConfig, scopes: GrantedScope[] }→{ connectionId }. Opens a scope-checkedSmartClientfor backend-services auth (client_secret_postorprivate_key_jwt) forread_resource/write_resource/start_bulk_exportto reference. For the interactive, patient/clinician-facing SMART launch instead, usestart_smart_launch/complete_smart_launchbelow — the underlying@interop-gateway/connectorpackage supports both auth methods;connect_ehritself only accepts the backend-services shape. Makes no network call itself;authtravels as a tool argument, which most MCP clients display/log as part of showing what the tool was called with — a materially different exposure than usingSmartClientdirectly in your own process. Only use this with a client you trust to handle the resulting call log appropriately.start_smart_launch—{ authorizeUrl, tokenUrl, clientId, clientSecret?, redirectUri, scope, aud?, launch? }→{ url, state }. Builds the SMART App Launch authorization URL (PKCE,S256) and returns it plus astatetoken. This tool cannot perform the browser redirect and login/consent itself — that's inherently outside what any server-side tool call can do. The caller is responsible for sending the user tourland capturing thecode/statethe authorization server redirects back with, then callingcomplete_smart_launch. The PKCEcode_verifieris held server-side (keyed bystate, in memory, per server instance), never returned here.complete_smart_launch—{ state, code, baseUrl, scopes }→{ connectionId, patient?, encounter? }. Exchanges the authorizationcodefor a token (using thecode_verifierheld againststate) and opens a connection with it, same shapeconnect_ehrreturns —read_resource/write_resource/start_bulk_exportwork unchanged afterward.stateis single-use, removed from memory on this call whether it succeeds or fails.read_resource—{ connectionId, resourceType, id? , searchParams? }→ the resource (byid) or a search Bundle (searchParams, omitid) as JSON. Scope-checked before any network call; the response contains real resource content.write_resource—{ connectionId, operation: "create"|"update"|"delete", resourceType, id?, resource? }→ aWriteResult({ ok: true, status, resource }or{ ok: false, status, code, path, issues }) as JSON. Never throws for a server-side rejection.start_bulk_export—{ connectionId, level: "system"|"patient"|"group", groupId?, types?, since?, typeFilter?, outputFormat? }→{ exportId }. Kicks off a Bulk Data$exportper the Bulk Data Access IG.check_bulk_export_status—{ connectionId, exportId }→ aBulkExportStatusas JSON:{status:"in-progress",progress?,retryAfterSeconds?},{status:"completed",transactionTime,output,requiresAccessToken,...}, or{status:"error",issues}. One-shot — call it again yourself until"completed".download_bulk_export_file—{ connectionId, type, url, requiresAccessToken? }→ the raw NDJSON text of one output file from a completed export (passrequiresAccessTokenstraight from that same completed status). Files can be large — the full text comes back as this tool's result.cancel_bulk_export—{ connectionId, exportId }→{ cancelled: true }. Cancels an export the server hasn't finished yet.send_message—{ host, port, message, timeoutMs?, maxAttempts? }→ anMllpSendResultas JSON. Sends raw HL7v2 over plain, unencrypted MLLP — only send to a host reachable over a trusted network.run_pipeline—{ yamlConfig }→{ pipelineId, name, address }. Parses and starts anenginepipeline (the same YAML shape its CLI accepts), sharing this server's resolvedauditSink/deadLetterQueue(see "Persistence" above) — both follow the same persisted-and-encrypted-by-default rule the server itself does. The pipeline keeps running (a listening MLLP/HTTP server, or a file watcher) after this call returns — callstop_pipelineexplicitly or it leaks a listening port/watcher for the life of the process.stop_pipeline—{ pipelineId }→{ stopped: true }. Stops a pipeline started byrun_pipelineand releases itspipelineId.
Connections and running pipelines live in memory, per server instance — not restored
across a process restart, and a connectionId/pipelineId from one server instance is
meaningless to another.
License
Apache-2.0 — see LICENSE.
