Skip to main content
Glama
README.md
# clinic-mcp

A reference [Model Context Protocol](https://modelcontextprotocol.io) 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](https://rentive.ai), 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

```mermaid
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](src/store/index.ts)).

## Run it locally

Requires Node 20+ and pnpm.

```bash
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.

```json
{
  "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](https://zod.dev); 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](LICENSE).

TDQS

A3.5/5.0

Scored across 5 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: booking appointments, escalating to on-call, finding slots, recording intake, and searching protocols. No two tools overlap in functionality.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern in snake_case, making it easy to predict the action and resource for each tool.

Tool Count5/5

With exactly 5 tools, the server is well-scoped for a clinic domain, covering core patient-facing and administrative workflows without being overly heavy or thin.

Completeness3/5

The set covers appointment creation, urgent escalation, slot availability, intake recording, and protocol search. However, it lacks read/update/delete for appointments and any patient or provider management, which are notable gaps.

Maintenance

ActivityInactive
ResponsivenessNo issues