Inspiration
Anyone who has managed a parent's care knows the portal. Appointments in one tab, medications in another, lab results in a third, messages in a fourth, consent forms somewhere in a fifth. One question — is Mom okay this week? — takes twenty minutes and a lot of clicking to answer, and most caregivers give up halfway.
Hospitals will never fix this by issuing API keys to your agent. Liability, HIPAA, bot defenses. But you are already signed in. That gap — a site that will never expose an agent API, and a user who is already authenticated — is exactly what WebMCP fills.
What it does
CarePortal is a patient portal with proxy access, built for caregivers and the agents they bring with them.
You sign in, switch to the family member you're managing care for, and say one sentence to your agent. The agent works across four modules in your signed-in session — appointments, medications, results, messages — and the page follows it: every tool call navigates to the relevant screen and highlights what the agent is looking at. A rail on the right logs each step in plain English.
Writes are split into three tiers, decided by the portal, not the agent:
- Read anytime. Appointments, medications, results, unread messages, who you're acting for.
- Confirm in-page. Refill requests, replies to a provider, rescheduling. The agent prepares the action; a confirmation card opens in the portal; nothing is submitted until you approve. Message drafts are editable before sending.
- Not exposed. Signing consent forms, opening full records, changing proxy permissions. There is no tool for these. The agent can only tell you they exist and bring you to them.
The demo runs on realistic seed data. The data layer is an interface with a documented SMART on FHIR mapping for the production path.
Why this is a strong fit for WebMCP
The session is the trust boundary. A remote MCP server would require the hospital to build agent authentication, a new API surface, and still couldn't ask for consent inside the page. WebMCP reuses the login the portal already has and the frontend logic it already runs.
The portal decides what agents may do. DOM-driving agents can't distinguish "help with this" from "this needs a human." A tool set can. The not-exposed tier is a first-class design decision, and it's only possible when the site defines the tools.
Patient-access FHIR is read-only by law. The 21st Century Cures Act forces hospitals to expose reads to patient-authorized apps. It does not — and likely never will — expose act-as-the-patient writes. That half exists only inside the portal, which is precisely where WebMCP lives.
How it creates a better experience
Before: five tabs, ~40 clicks, no way to ask "what's affected this week."
After: one sentence. The agent traverses the modules; you make two decisions and one signature, each in the place where the action actually lives. You never leave the portal, never paste anything into a chat box, and never wonder what the agent did — the page moved with it.
What people and agents can now do together
- Cross-module reasoning. "Her blood pressure med is low, the cardiology follow-up is next week, and the lab result the PCP flagged needs a reply" — portals are organized by department, not by situation. Agents aren't.
- Consent where the action is. The confirmation card is the portal's own UI, with the portal's own copy, not a generic "allow?" in the agent's chat window.
- A visible line the agent can't cross.
what_requires_mereturns the consent form and says, in its own description, there is no tool to sign documents. The agent hands off; the human finishes. - Context switching that agents understand. Switch from your own account to your mother's and every tool re-registers with her name in its description. The agent knows whose records it's touching before it decides which tool to call.
How we built it
Vite + React + TypeScript, Zustand for state, deployed on Vercel. WebMCP via document.modelContext, with a status badge that tells judges how to enable it if the browser doesn't have it.
Ten tools in three scopes, each scope an AbortController:
| Scope | Registered when | Tools |
|---|---|---|
auth |
before sign-in | get_login_status |
patient |
after sign-in; rebuilt on every patient switch | get_patient_context, get_upcoming_appointments, get_medications, get_recent_results, get_unread_messages, request_refill, send_message_to_provider, what_requires_me |
route |
only on /appointments/:id |
reschedule_appointment |
Page-follow. Every execute begins by navigating and highlighting, so the human and the agent are always looking at the same thing.
Confirmation protocol. Confirm-tier tools return a Promise that resolves when the user clicks. Agent harnesses have tool-call timeouts and humans don't, so after 60 seconds the tool returns pending_user_confirmation, the card stays open, and a later approval is applied directly. Every confirm-tier tool shares one status vocabulary: submitted | declined_by_user | pending_user_confirmation | busy | requires_provider_approval.
document.modelContext.registerTool({
name: "request_refill",
description: "[Acting for Margaret Lee via proxy access] Requests a refill for one medication. Opens a confirmation card the user must approve; nothing is submitted until they do. ...",
inputSchema: { type: "object", properties: { medication_id: { type: "string" } }, required: ["medication_id"] },
execute: async ({ medication_id }) => {
follow("/medications", { kind: "medication", id: medication_id });
const r = await showConfirm({ title: "Request refill", detail: "...", onLateApprove: () => ds.requestRefill(medication_id) });
if ("timeout" in r) return text({ status: "pending_user_confirmation" });
if (!r.approved) return text({ status: "declined_by_user" });
ds.requestRefill(medication_id);
return text({ status: "submitted" });
}
}, { signal: patientScope.signal });
Challenges
Deciding what not to expose was harder than deciding what to expose. The temptation is to make everything a tool. The portal's credibility comes from the line it draws.
Timeouts. A confirmation card is a human-speed operation inside a machine-speed protocol. The 60-second pending state and onLateApprove came out of that mismatch.
Descriptions are the product. An agent that picks the wrong tool or skips get_patient_context isn't a model problem; it's a description problem. We rewrote every description three times, always answering: what does this do, when should you call it, what does it return, when should you not call it.
Hot reload. Re-registering a tool name throws InvalidStateError. Scopes are torn down in import.meta.hot.dispose.
What we learned
WebMCP isn't a way to make agents autonomous on the web. It's a way for a site to say exactly how far an agent may go — and for the human to stay in the page for the part that's theirs. For healthcare, that part is small, non-negotiable, and legally required. Designing around it turned out to be the whole project.
What's next
Implement FhirDataSource against a SMART on FHIR sandbox for the read tier. Add more human-only patterns (identity verification, payment authorization). Talk to a hospital.
Built With
- chatgpt
- chrome
- fhir
- react
- react-router
- tailwindcss
- typescript
- vercel
- vite
- webmcp
- zustand
Log in or sign up for Devpost to join the conversation.