clinic-mcp
A reference MCP server for clinic scheduling and intake, demonstrating production patterns like tenant isolation, idempotent writes, and structured errors using synthetic data.
README
clinic-mcp
A reference Model Context Protocol server for clinic scheduling and intake. Built in TypeScript with strict typing, structured errors, and tenant isolation enforced at the data layer. The data is synthetic. This is not clinical software.
The goal is to show what a production-shaped MCP server looks like for a vertical that demands data isolation and grounded outputs: the same shape of code I write at Rentive, with mock data and a different domain so the patterns are reviewable without leaking anything proprietary.
Why MCP
LLM applications keep reinventing the same wiring: ad-hoc function definitions per provider, bespoke argument parsing, no shared transport, no consistent error model. MCP is a small open protocol that fixes the wiring layer. A server exposes a list of typed tools over stdio (or HTTP), and any MCP-aware client (Claude Desktop, IDE integrations, custom agents) can discover and call them with the same machinery.
For domain backends, that means you write tools once and they work everywhere. For agent builders, it means you stop hand-rolling tool schemas and start composing servers.
Architecture
flowchart LR
Client["MCP client<br/>(Claude Desktop, custom agent)"]
Server["clinic-mcp server"]
Tools["Tools<br/>find_available_slot<br/>book_appointment<br/>record_intake<br/>search_protocols<br/>escalate_to_oncall"]
Store["ClinicStore<br/>tenant-scoped accessors"]
Seed[("seed.json<br/>synthetic clinics, providers,<br/>patients, protocols")]
Client -->|stdio JSON-RPC| Server
Server --> Tools
Tools --> Store
Store --> Seed
Every tool takes a clinic_id and the store enforces that all reads and writes are scoped to that clinic. Cross-tenant access throws TenantMismatchError rather than silently returning the wrong row. This mirrors the row-level-security pattern a production deployment would enforce in Postgres, surfaced here in application code so the guarantee is reviewable in one file (src/store/index.ts).
Run it locally
Requires Node 20+ and pnpm.
git clone https://github.com/dominikstefanski/clinic-mcp.git
cd clinic-mcp
pnpm install
pnpm test # 29 tests
pnpm typecheck
pnpm dev # boots the server on stdio
The server reads src/store/seed.json at startup and serves two synthetic clinics: clinic_north (general practice, cardiology, dermatology) and clinic_west (pediatrics, general practice).
Wire into Claude Desktop
Add this to your Claude Desktop config (macOS: ~/Library/Application Support/Claude/claude_desktop_config.json). Replace the path with your local clone.
{
"mcpServers": {
"clinic-mcp": {
"command": "npx",
"args": ["-y", "tsx", "/absolute/path/to/clinic-mcp/src/server.ts"]
}
}
}
Restart Claude Desktop. The five tools will appear under the connections menu. Try a prompt like "Find a general practice opening at clinic_north next Monday morning."
Tool reference
All tools return { ok: true, ...result } on success or { ok: false, error: { code, message } } on failure. Inputs are validated with zod; MCP-level argument errors are returned as validation errors with field details.
find_available_slot
Find open appointment slots for a specialty in a date range, skipping conflicts.
| Field | Type | Notes |
|---|---|---|
clinic_id |
string | Required |
specialty |
enum | general_practice | pediatrics | cardiology | dermatology |
from_iso |
string | Inclusive ISO 8601 start |
to_iso |
string | Exclusive ISO 8601 end |
duration_minutes |
int | 15 to 120, default 30 |
limit |
int | 1 to 50, default 10 |
book_appointment
Create an appointment. Requires a caller-supplied idempotency_key; replays return the original appointment instead of double-booking. Voice agents will retry, so this is non-optional.
| Field | Type | Notes |
|---|---|---|
clinic_id |
string | Required |
provider_id |
string | Must belong to clinic_id |
patient_id |
string | Must belong to clinic_id |
start_iso |
string | ISO 8601 |
duration_minutes |
int | 15 to 120, default 30 |
reason |
string | 1 to 500 chars |
idempotency_key |
string | 8 to 128 chars, caller-supplied |
Returns { appointment, idempotent_replay }.
record_intake
Persist a structured intake note and assign a triage level.
| Field | Type | Notes |
|---|---|---|
clinic_id |
string | Required |
patient_id |
string | Must belong to clinic_id |
symptoms |
string[] | 1 to 20 entries |
severity |
int | 1 to 10, patient-reported |
onset_iso |
string | ISO 8601 |
notes |
string | Optional, max 2000 chars |
Triage rule: severity >= 8 is urgent, >= 5 is elevated, otherwise routine.
search_protocols
Keyword search over the clinic's protocol library. Returns ranked snippets the model can cite when answering.
| Field | Type | Notes |
|---|---|---|
clinic_id |
string | Required |
query |
string | 1 to 500 chars |
limit |
int | 1 to 20, default 5 |
The current implementation is a naive TF score with title weighting (3x). It exists to demonstrate the interface of a retrieval tool; production deployments would swap the backend for vector search (see Design notes).
escalate_to_oncall
Mark an existing appointment as urgent and reassign it to the clinic's on-call provider.
| Field | Type | Notes |
|---|---|---|
clinic_id |
string | Required |
appointment_id |
string | Must belong to clinic_id |
reason |
string | 1 to 500 chars, appended to the appointment's reason |
Returns { appointment, on_call_provider, reassigned }.
Design notes
Tenant isolation is enforced at the store, not the tool. Tools accept a clinic_id and pass it down. The store validates ownership on every accessor and throws TenantMismatchError on mismatch. If you add a new tool tomorrow, you cannot accidentally leak across clinics; the store will not let you.
Idempotency on writes. book_appointment requires an idempotency_key. Real callers (voice agents, retry loops, network blips) will repeat requests, and a healthcare system that responds to retries by creating duplicate appointments is a healthcare system that loses trust on day one.
Structured errors over thrown strings. Every domain failure is a typed DomainError subclass with a stable code. The MCP wrapper turns them into { ok: false, error: { code, message } }. Clients can branch on code instead of regexing message.
The retrieval tool is a stand-in. search_protocols uses an in-memory TF score so the repo runs without external services. In production this is the seam where you wire in Pinecone, pgvector, or your retrieval backend of choice. The tool's input/output contract stays the same.
Time handling is simplified. Provider working hours are interpreted in UTC for clarity. A real deployment would respect each clinic's timezone (already in the schema). Calling this out explicitly so reviewers know it's intentional, not an oversight.
What this isn't
- Not clinical software. The triage rule is a toy and the protocol corpus is hand-written prose. Do not use it for anything that touches real patients.
- Not HIPAA-compliant. The data is fake, the storage is in-memory, there is no audit log. Production would need all of that and then some.
- Not a complete EMR or scheduling backend. The point is to show the MCP-server shape, not to ship a clinic system.
License
MIT. See LICENSE.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。