Inspiration

I started this hackathon by reading clinical pain threads across r/HealthIT, r/medicine, r/nursing, and r/healthcare, looking for an angle. One pattern kept coming back: outpatient sample medication documentation. The clearest articulation came from r/HealthIT:

"Right now I'm getting feedback the current way is too cumbersome. Is there a way to get the documentation to auto populate when the provider puts in the order?"

u/MemoryWorking, r/HealthIT, 2026

That's the sample medication workflow: clinician orders a sample, nurse documents that the sample was given to the patient, and somehow those two steps live in disconnected screens that nobody can wire together. The same data ends up entered twice. It's also a federal compliance issue (the Prescription Drug Marketing Act of 1987 requires lot numbers, expiration dates, and patient acknowledgments for every sample dispensed), and a workflow that gets frustrating fast in a busy clinic.

The data already exists in the order. The work is in re-entering it as a different FHIR resource. That's a tooling problem. MCP plus FHIR plus AI agents looked like the right shape to close it: two agents, one for each role, cooperating through the chart.

What it does

Two Model Context Protocol servers install separately into a Prompt Opinion workspace. OrderPad runs in the clinician's chat. OrderBridge runs in the staff's chat. The role separation is mechanical: SMART scope grants are disjoint, so OrderPad cannot dispense and OrderBridge cannot prescribe. The two agents never share state in chat. They cooperate through the chart.

A typical session:

Clinician chat: "Place an Atorvastatin sample for this patient. Indication: hypercholesterolemia."

Clinician agent: "To confirm: Atorvastatin 20mg, indication 'hypercholesterolemia.' This will land in the chart immediately. Shall I proceed?" → "yes" → calls place_sample_medication_order. Order created.

Staff chat (different time, same patient): "What's pending?" → calls get_pending_sample_orders. Atorvastatin appears.

Staff: "Dispense it. Patient education was given." → confirms → calls record_sample_dispense. Dispense linked back to the order via authorizingPrescription.

Clinician chat (back later): "Did the patient get their Atorvastatin?" → calls list_sample_orders. Status is now dispensed.

Every write is a real FHIR resource on the operator's FHIR server. The two MCPs never talk directly. The chart is their shared source of truth.

How I built it

  • TypeScript + Node 20 + Express, with @modelcontextprotocol/sdk v1.x and StreamableHTTPServerTransport in stateless mode (one server + transport per request, matching Prompt Opinion's handshake).
  • Prompt Opinion's ai.promptopinion/fhir-context extension declares scopes at initialize time; the platform surfaces them to the operator at install.
  • One repo, two server entry points (src/server.ts, src/server-orderpad.ts), two fly apps off one Dockerfile. The [processes] block in fly.orderpad.toml overrides the default CMD.
  • src/inventory/samples.ts is the load-bearing shared contract: SAMPLE_CATEGORY (the FHIR coding both servers must agree on), SAMPLE_TEMPLATES (RxNorm + dosing per medication), and a hasSampleCategory() filter. Both servers consume it; if it drifts, sample orders go invisible.
  • Mock inventories for samples (RxNorm-keyed) and vaccines (CVX-keyed). Production replaces these with real pharmacy and vaccine inventory backends.
  • Both deployed on fly.io with min_machines_running = 1 so the Prompt Opinion handshake doesn't pay a cold start.

Challenges I ran into

StreamableHTTP transport stateless mode. First version used the default stateful transport. Initialize succeeded, but the next request returned "Bad Request: Server not initialized" because each per-request transport hadn't seen its own initialize. Setting sessionIdGenerator to undefined fixed it.

Strict FHIR validation and connection resets. First MedicationDispense payload included custom-URL extensions for lot, expiry, manufacturer, and patient education. The FHIR server reset the TCP connection on POST instead of returning a clean 400. Undici surfaced it as a generic "fetch failed"; I had to dig into err.cause to see ECONNRESET. The fix: drop to FHIR R4 minimum payload, put structured detail in the human-readable note text, leave custom extensions for after a proper StructureDefinition registration.

The two-MCP install gotcha. First plan was one MCP and a programmatic seed step for the demo. I pivoted to two MCPs after realizing the seed was a hand-wave: real workflows have role separation between clinicians and staff. Building OrderPad as a separate MCP made the role split mechanical. A 30-minute spike with a stub OrderPad caught a platform behavior I'd missed: a per-MCP "FHIR-context extension" toggle is required in addition to the install.

Cross-MCP discoverability. Two MCPs that only make sense together is a real DX problem. I solved it three ways: paired serverInfo descriptions (each cross-references the other), a combined SMART scope table in the README, and a 7-step install runbook covering both installs and the per-MCP extension toggle.

Accomplishments that I'm proud of

  • Closed-loop demo across two MCPs, two BYO agents, two chats, one chart. Real state changes, not scripted.
  • The role split is enforced by SMART scope, not by tool naming. Even if the LLM tried to dispense from OrderPad or prescribe from OrderBridge, the FHIR server returns 403.
  • Confirm-before-write encoded entirely in tool descriptions. The LLM reads "always confirm with the user" and behaves. No special hardcoding in the agent platform.
  • Errors return operator-actionable remediation text ("re-grant the X scope," "retry," "escalate"), not stack traces.
  • The dispense tool captures lot number, expiration date, manufacturer, and patient-education flag: the four fields the Prescription Drug Marketing Act requires for sample tracking. Compliance is built in, not bolted on.

What I learned

  • Two MCPs with disjoint scope grants beat one MCP with all the tools for clinical role separation. Mechanical scope enforcement turns the role split into a safety property.
  • Tool descriptions are agent instructions in disguise. Words in the description propagate into agent behavior more reliably than out-of-band system prompts.
  • For FHIR writes against unfamiliar servers, start with R4 minimum and add fields back incrementally. Strict validators that reset connections instead of returning OperationOutcome are undebuggable without explicit transport-level error handling.

What's next for OrderBridge

  • Replace the inventory mocks with a third MCP server connecting to a real pharmacy or sample-tracking backend. That's the wedge that turns this from a documentation-loop demo into a PDMA-compliant samples-tracking system small clinics could actually use.
  • Add procedure orders and procedure-completion documentation (in-office wound care, joint injections, point-of-care labs) on the same pattern.
  • Register proper StructureDefinitions for the OrderBridge custom extensions so structured detail can move out of note text and into typed fields.
  • Add a small admin UI for clinic operators to configure the sample inventory and category coding without touching code.

Built With

Share this project:

Updates