Security-first implementation of the first-party ModelForge MCP boundary. The project uses one shared Rust core and thin transport adapters. Domain data remains behind ModelForge services; this repository deliberately contains no JSON, SQLite, or PostgreSQL readers.
The initial M1 slice contains:
- a deterministic, versioned read-only tool catalog;
- bounded request and result validation;
- subject, organization, policy-snapshot, and field-grant contracts;
- fail-closed policy, grant-resolution, domain-adapter, and PHI-free audit interfaces;
- immutable tenant policy and context-grant snapshots with bounded startup validation;
- role, scope, organization, destination, purpose, tool, field, and kill-switch enforcement;
- operation digests for approval and replay binding;
- a stdio MCP companion exposing capability discovery;
- a stateless managed
/mcpadapter with RS256 OIDC validation; - OAuth protected-resource metadata and standards-compatible challenges;
- explicit Host, Origin, audience, issuer, scope, and body-size enforcement;
- bounded HTTPS JWKS refresh with strict
kid, RS256, signing-use, no-redirect, atomic replacement, and stale-key fail-closed behavior; - a narrow medication-conflict adapter that injects case and organization identity from trusted context rather than accepting either value from model-controlled arguments;
- a deterministic response-contract check,
clinical.response_contract_check, that mirrors the desktop app's eight-sectionRESPONSE_CONTRACT_SECTION_HEADINGScheck byte-for-byte so the two can never silently drift apart; - a
runtime.diagnosticstool backed by a narrow, single-purpose adapter that returns only a bounded, non-secret per-backend summary (state, whether a model is loaded, uptime, active requests) and never the upstreamLocalRuntimeStatusfields that can carry local file paths or other operational detail (logs,startupError, pid, port,currentConfig,installCommand); - a
DomainRouterthat composes multiple narrow, single-family domain adapters (clinical, runtime, and future evidence/compute adapters) behind the oneDomainAdapterportGatewayaccepts, dispatching by catalog tool name and failing closed on anything unregistered; - the V1 prompt surface named in the system design doc —
clinical.response_contract,clinical.soap_draft,clinical.differential_support,clinical.medication_review,clinical.evidence_appraisal, andclinical.compute_incident_triage— the first four sourced byte-for-byte from the desktop app'sCLINICAL_RESPONSE_CONTRACT/CLINICAL_MODES, the last two authored fresh for tool families (evidence, compute) that have no desktop-app equivalent. Prompts carry no PHI and need no context grant, so they are served directly by the bootstrap handler; - the
modelforge://capabilitiesresource named first among the V1 resources in the design doc, serving the same deterministic manifest as themodelforge.capabilitiestool throughresources/list/resources/readinstead oftools/call; - terminal admitted, denied, succeeded, and failed audit outcomes without arguments or results;
- real, non-test implementations of every trusted port
Gatewayneeds:BuiltInMedicationConflictService(ports the desktop app's own deterministic seed-list checker),FileAuditSink(durable, fsynced JSON-lines audit log),InMemoryIdempotencyStore(race-safe reservation, not just a presence check),HmacApprovalVerifier(signed, single-use approval tickets bound to the full operation digest), andHttpGrantResolver(calls out to an existing ModelForge grant-issuing service rather than storing grants itself); - both binaries construct a fully wired
Gatewayfrom those ports when configured (see "Clinical gateway" below) and fall back to the unchanged bootstrap-only default otherwise — live-verified end to end: real JWT → real tenant-policy authorization → real audit trail → real domain dispatch, including a PHI tool correctly rejected without a resolvable grant; clinical.record_review_decision, the firstRiskClass::ControlledWritetool in the catalog: recording a clinician's decision on a prior AI-assisted operation, gated by a single-use approval ticket and deduplicated by an idempotency key so a retry replays the original result instead of recording a second decision;clinical.submit_compute_request, the design doc's second named controlled write: a typed, organization-bound forward to ModelForge's own compute-control-plane scheduler (packages/contracts/src/compute.ts,server/src/compute/control-plane.tsin the main app), never a local reimplementation of bin-packing or node scheduling. Carries no PHI and needs no context grant — a compute job is organization-scoped infrastructure, not case data — but is stillRiskClass::ControlledWriteand idempotency-required like the review-decision tool;- tests for catalog determinism, grant binding, tenant isolation, kill switches, digest stability, payload limits, trusted-context injection, domain routing, prompt rendering, resource reads, idempotency reservation races, approval-ticket binding/expiry/replay, and audit privacy, plus end-to-end scenario tests covering both controlled-write tools' approval-then-idempotent-replay path through their real catalog entries.
cargo fmt --check
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo build --release -p modelforge-clinical-mcp-stdio
cargo build --release -p modelforge-clinical-mcp-httpRun the local companion:
cargo run -p modelforge-clinical-mcp-stdioLogs go to stderr. Stdout is reserved exclusively for MCP JSON-RPC frames.
Run the managed adapter behind a TLS-terminating reverse proxy:
export MODELFORGE_MCP_BIND=127.0.0.1:8080
export MODELFORGE_MCP_RESOURCE=https://mcp.example.com/mcp
export MODELFORGE_MCP_PROTECTED_RESOURCE_METADATA_URI=https://mcp.example.com/.well-known/oauth-protected-resource
export MODELFORGE_MCP_OIDC_ISSUER=https://identity.example.com
export MODELFORGE_MCP_OIDC_AUDIENCE=https://mcp.example.com/mcp
export MODELFORGE_MCP_OIDC_JWKS_URI=https://identity.example.com/.well-known/jwks.json
export MODELFORGE_MCP_OIDC_JWKS_REFRESH_SECONDS=300
export MODELFORGE_MCP_OIDC_JWKS_MAX_STALE_SECONDS=3600
export MODELFORGE_MCP_ALLOWED_HOSTS=mcp.example.com
export MODELFORGE_MCP_ALLOWED_ORIGINS=https://app.example.com
export MODELFORGE_MCP_REQUIRED_SCOPES=mcp:read
cargo run -p modelforge-clinical-mcp-httpSet MODELFORGE_MCP_OIDC_PUBLIC_KEY_PEM instead of MODELFORGE_MCP_OIDC_JWKS_URI for a controlled
static-key deployment; setting both is rejected. The managed binary refuses to start when any
security-critical setting is missing or invalid. It
expects TLS to terminate at a trusted local proxy and validates the external Host and Origin values
forwarded unchanged to the application. Private keys are neither required nor accepted.
Both binaries default to the bootstrap-only surface above (capability discovery, prompts, the
capabilities resource — no PHI). Setting all four of the following env vars switches either
binary to the full ClinicalServer, wired to real ports; setting only some of them is a startup
error rather than a silent partial configuration:
export MODELFORGE_MCP_POLICY_PATH=/etc/modelforge/clinical-policy.json
export MODELFORGE_MCP_GRANT_SERVICE_URL=https://grants.example.com
export MODELFORGE_MCP_AUDIT_LOG_PATH=/var/log/modelforge/clinical-audit.jsonl
export MODELFORGE_MCP_APPROVAL_SECRET=<at least 32 random bytes>MODELFORGE_MCP_POLICY_PATH points to a JSON tenant-policy file, loaded once at startup and
validated the same way PolicySet::new validates it in tests:
{
"policies": [
{
"organizationId": "org-3",
"tools": {
"clinical.medication_conflict_check": {
"allowedRoles": ["clinician"],
"requiredScopes": ["clinical:read"],
"allowedDestinations": ["local_model_forge"],
"allowedAuthenticationStrengths": []
}
}
}
],
"snapshot": {
"registryVersion": "registry-1",
"rbacVersion": "rbac-1",
"egressPolicyVersion": "egress-1",
"killSwitchVersion": "kill-1",
"toolPolicyVersion": "tools-1"
},
"killSwitchActive": false
}MODELFORGE_MCP_GRANT_SERVICE_URL must be an https:// origin; grants are looked up as
GET {url}/{grantId} rather than stored by this repository. MODELFORGE_MCP_APPROVAL_SECRET signs
and verifies approval tickets for both RiskClass::ControlledWrite tools in the catalog (see
below). runtime.diagnostics stays reachable but always returns domain_unavailable: no IPC
bridge to a running desktop process exists in this repository, and fabricating numbers would be
worse than failing closed.
Two more variables are each independently optional — set either, both, or neither, unrelated to the four above and to each other:
export MODELFORGE_MCP_REVIEW_SERVICE_URL=https://reviews.example.com
export MODELFORGE_MCP_COMPUTE_SERVICE_URL=https://compute.example.comMODELFORGE_MCP_REVIEW_SERVICE_URL enables clinical.record_review_decision, which records a
clinician's accept/override/reject decision on a prior AI-assisted operation.
MODELFORGE_MCP_COMPUTE_SERVICE_URL enables clinical.submit_compute_request, which forwards a
compute job to ModelForge's compute-control-plane scheduler. Every call to either tool must carry
a single-use approvalTicket — signed with MODELFORGE_MCP_APPROVAL_SECRET and bound to the
exact operation, subject, client, policy version, and expiry — and an idempotencyKey scoped to
organization, subject, tool, and normalized arguments; a retry with the same key and arguments
replays the stored terminal result instead of re-executing. Left unset, either tool stays listed
and reachable but fails closed with domain_unavailable — the same "reachable but honest" pattern
as runtime.diagnostics — rather than silently discarding a decision or fabricating a placement.
Known gap: the managed HTTP adapter derives SubjectContext from a verified OIDC access
token, so the chain above is complete for it. The stdio companion has no equivalent identity
source yet — the design doc's "inherited, ACL-restricted channel" that the desktop app is meant to
authenticate over doesn't exist on either side of this repository. Enabling the clinical gateway
over stdio today means every tool call fails with "verified identity is unavailable" until the
desktop integration supplies that channel.
Dockerfile is a multi-stage cargo chef build producing a ~63 MB distroless, non-root runtime
image with both binaries; it defaults to running the managed HTTP adapter and fails closed exactly
like the bare cargo run invocation above if required env vars are missing:
docker build -t modelforge-clinical-mcp .
docker run --rm -p 8080:8080 \
-e MODELFORGE_MCP_BIND=0.0.0.0:8080 \
-e MODELFORGE_MCP_RESOURCE=https://mcp.example.com/mcp \
# ...remaining vars from the managed-adapter example above...
modelforge-clinical-mcpMODELFORGE_MCP_BIND must be 0.0.0.0:<port> in a container — 127.0.0.1:<port> (correct for the
bare-metal example above) would be unreachable from outside the container.
Dockerfile.dev is a toolchain-only image (rustup + clippy + rustfmt + cargo-watch) for iterating
with the source bind-mounted from the host; its default command re-runs this file's exact fmt/
test/clippy pipeline on every change:
docker build -f Dockerfile.dev -t modelforge-clinical-mcp:dev .
docker run --rm -it -v "$(pwd)":/workspace -v modelforge-clinical-mcp-target:/workspace/target \
modelforge-clinical-mcp:devBoth base images are rust:1.89-slim-bookworm rather than the full bookworm variant, which ships
hundreds of packages (docs, extra locales, unused CLI tools) this project never uses and that
otherwise show up as avoidable CVEs in image scans.
- No inbound subject, organization, role, or scope is accepted from tool arguments.
- PHI-bearing operations require a short-lived grant bound to subject, client, organization, tool, fields, purpose, destination class, and expiry.
- Policy and domain dependencies fail closed.
- Tool arguments and results never enter audit events or tracing fields.
- The result guard rejects excessive nesting, strings, arrays, object width, and encoded size.
- No shell, filesystem, secret, raw-image, registry-administration, or direct-database tool exists.