Skip to main content
Glama
tgdn

healthcare

by tgdn
README.md
# healthcare-mcp

An MCP server for finding doctors in Romania: by county, speciality, language, name and weekly availability.

## Add it to Claude Code

Pick one.

### Hosted: a URL and a token

```sh
claude mcp add --transport http healthcare https://wonderful.xxxx0.top/mcp \
  --header "Authorization: Bearer <token>"
```

Other MCP clients need the same two things: the URL `https://wonderful.xxxx0.top/mcp` and the header `Authorization: Bearer <token>`.

### Local: stdio, with only Docker installed

```sh
git clone https://github.com/tgdn/healthcare-mcp.git && cd healthcare-mcp
docker compose up --no-start    # downloads and builds once, starts nothing
claude mcp add healthcare -- docker compose -f "$PWD/docker-compose.yml" run --rm -T stdio
```

Claude Code starts the server when it needs it, along with a Postgres container that imports the doctors on first use. `docker compose down` stops Postgres; `docker compose down -v` also deletes the data.

Then ask Claude something like *"I live in Mangalia and need a cardiologist who speaks German on Saturday"*.

## Tools

- **ListSpecialities** and **ListLanguages** return what the doctors offer, with counts, optionally for one county.
- **SearchDoctor** takes `county`, `speciality`, `first_name`, `last_name`, `language`, `availability` (`day`, `time`) and `limit`, and returns the best rated doctors first.
  - A county is required unless searching by name. Without one, the tool tells the model to ask the user rather than guess.
  - Speciality and language match case-insensitively; an unknown value returns the known ones.
  - Names ignore case and accents. A misspelt name ("Dumitresku") returns the closest spellings, with a note.
  - Availability is regular weekly working hours, not free appointment slots.
  - With no match in the county it searches the neighbouring counties and says so. With no match at all, `matches_without` counts the results if each filter were dropped.

## Design decisions

### Why Postgres?

The prompt specifies a production-scale dataset with millions of records, where Postgres is standard practice:

- **Trigram search:** GIN trigram indexes make fuzzy `ILIKE` searches fast and avoid full table scans.
- **Simple ops:** Using Postgres for both data storage and search avoids managing extra sync pipelines.
- **Future scale:** B-tree indexes handle simple filters, while table partitioning and read replicas scale globally if needed.

### Alternatives

- **Elasticsearch:** Overkill for basic text matching. Only worth the operational overhead for complex relevance ranking or heavy faceting.
- **SQLite:** Fine for the small sample, but fails under concurrent writes in production.

### Why MCP over REST?

A REST API forces you to predict every endpoint in advance. MCP gives the model typed tools to compose on the fly, leveraging the existing chat surface instead of building a new UI.

### Trade-offs

REST is still easier to cache, version, and rate-limit for non-agent software. If programmatic tools needed access, I would expose the search logic via a light REST API alongside `/mcp`. Same underlying engine, two different interfaces.

## Local HTTP server

```sh
docker compose up -d
```

Serves `http://127.0.0.1:3000/mcp` with the token `local-dev-token`. Put `MCP_AUTH_TOKEN` or `MCP_PORT` in a `.env` file to change them (see `.env.example`).

## Deploy

On a hangar host, which runs Traefik and Postgres with a proxied wildcard for `*.xxxx0.top`, one command from your laptop deploys the committed code:

```sh
./deploy/deploy.sh deploy@<host> wonderful.xxxx0.top
```

The first run creates the database with hangar's `new-project` and generates the bearer token into `/opt/healthcare/.env`; later runs keep both and redeploy. The script prints how to read the token. `SSH_KEY=<path>` picks the SSH key.

Every request to `/mcp` is logged as one JSON line with the client IP, user agent, MCP client, method and tool (not the arguments):

```sh
ssh deploy@<host> docker logs -f healthcare-mcp-mcp-1
```

Hangar's Dozzle shows the same logs in the browser. `deploy/docker-compose.prod.yml` works on any host with Traefik on a shared Docker network; its header lists the variables.

## Development

Needs Node 22.9 or later.

```sh
npm install
cp .env.example .env
npm run db:up      # Postgres on 127.0.0.1:5432, seeded
npm test
npm run dev        # http://127.0.0.1:3000/mcp, reloads on change; stop the mcp container first if it runs
```

To mount the endpoint in an existing Hono API:

```ts
import { createApp } from "./src/app.js";
import { createDb } from "./src/db.js";

api.route("/", createApp({ db: createDb(process.env.DATABASE_URL!), authToken: process.env.MCP_AUTH_TOKEN! }));
```