Skip to main content
Glama
dylanbc1

clinica-mcp-server

by dylanbc1

dental-clinic-mcp-server

A production-grade MCP server for a dental clinic: appointments, affiliation checks and accounts receivable, with the security controls that 92% of the MCP ecosystem does not have.

🇪🇸 Léelo en español

cp .env.example .env && make up && make smoke

Why this exists

There are 22,000+ MCP servers listed publicly. Audited samples show 40% ship with no authentication at all, 79% handle credentials in plaintext, and only 8.5% implement OAuth. In January 2026 even Anthropic's own reference server carried three CVEs: path traversal, arbitrary file deletion and RCE.

The domain here is not the differentiator; the engineering is. This repository is a deliberate demonstration of what the other 8.5% looks like.

The failure mode it defends against is concrete. In July 2025 an AI agent deleted a production database at SaaStr during a code freeze: it held write permissions it never needed, and nobody could revoke them granularly. Its token was valid, so scopes alone would not have stopped it. What was missing was a human between intent and effect. Both are implemented here.

Related MCP server: open-dental-mcp

Domain: why a dental clinic in Colombia

Nothing in this server is invented. The appointment state machine, the affiliation check against the Colombian health regimes, the copayment rules and the waiting list are the real process a clinic or IPS runs every day. Roughly 25% of scheduled appointments go unused each month, which is why 48-hour confirmation and slot release exist. The tools implementing them attack a quantified problem, not a demo script.

There is also a regulatory line that makes the security work necessary rather than ornamental: booking an appointment is not a medical act, but recording a reason for consultation is. The moment the system stores clinical data it falls under Resolución 2654/2019, informed consent and RNBD registration with the SIC. That boundary is exactly where the clinical scope and the mandatory human approval live.

All data in this repository is synthetic, generated by Faker with a fixed seed. No real patient information is present anywhere, at any point, and a test asserts it.

Architecture

flowchart LR
    C["MCP client<br/>Claude · Cursor · Inspector"]
    subgraph mcp["MCP server · Streamable HTTP"]
      direction TB
      A1["1· OAuth 2.1 + PKCE"] --> A2["2· Scope check"] --> A3["3· Human approval"] --> T["14 tools · 3 resources · 1 prompt"]
      T --> A4["4· Structured errors"]
      T --> A5["5· Audit + transport guards"]
    end
    subgraph be["Domain backend · FastAPI"]
      API["REST API"] --> DOM["state machine · cartera<br/>afiliación · lista de espera"] --> DB[("PostgreSQL 16")]
    end
    C --> A1
    T --> API
    style mcp fill:#f6f2ff,stroke:#7c5cff
    style be fill:#f0f7ff,stroke:#3b82f6

Layer

Stack

Responsibility

Domain backend

FastAPI + PostgreSQL 16 + SQLAlchemy 2.x

Source of truth. Knows nothing about MCP.

MCP server

MCP Python SDK v2, Streamable HTTP

Translates the domain into tools/resources/prompts. Every security control lives here.

Authorization server

In-repo OAuth 2.1 (or Keycloak)

Issues tokens. Swappable without touching the resource server.

Separating the backend from the MCP server is itself the point: in production an MCP server almost never is the system, it wraps one that already exists. The LLM never touches the database directly.

Full reasoning and diagrams: docs/architecture.md.

The tool catalogue

Fourteen tools, not thirty. Model accuracy degrades past roughly 25–30 tools, so a smaller, precisely described catalogue is the design, not a limitation.

Scope

Tools

read

buscar_paciente · consultar_disponibilidad · consultar_cita · listar_citas_paciente · consultar_cartera · validar_afiliacion

write

agendar_cita · confirmar_cita · cancelar_cita · reprogramar_cita · registrar_asistencia · ofrecer_cupo_lista_espera

clinical

registrar_motivo_consulta

,

confirmar_operacion (executes an approved proposal)

Resources: clinica://info, politicas://cartera, agenda://hoy. Prompt: recepcionista_odontologia.

Every write and clinical tool returns a proposal, not a result. Calling cancelar_cita changes nothing; it hands back a plain-language summary of what would happen plus a signed token. Only confirmar_operacion, with that token, mutates anything:

{
  "requiere_confirmacion": true,
  "resumen": "Cancelar la cita 412 de Ana Gómez del 2026-09-03 09:00.",
  "esto_va_a_pasar": [
    "La cita pasará de 'confirmada' a 'cancelada'.",
    "El cupo quedará libre en la agenda.",
    "Si hay lista de espera para esa especialidad, se informará al siguiente."
  ],
  "advertencias": ["El paciente registra $180.000 COP en mora. No impide agendar."],
  "token_confirmacion": "eyJhY2Npb24iOi…",
  "vigencia_segundos": 300,
  "siguiente_paso": "Muestra este resumen a la persona responsable. …"
}

Security

Five layers, each answering a documented failure of the ecosystem. Full write-up and threat model: docs/security.md.

#

Layer

What it stops

1

OAuth 2.1 + PKCE, no API keys anywhere

Anonymous access; a stolen authorization code

2

Per-tool scopes read/write/clinical, non-nesting

The confused deputy; the SaaStr shape of over-broad tokens

3

Human-in-the-loop: signed, single-use, TTL-bound proposals

An agent mutating data on its own judgement

4

Structured errors with an actionable next step

Blind retry loops; leaked stack traces

5

Audit trail + transport guards

Unattributable changes; DNS rebinding; runaway agents

Three hardening measures beyond the brief, because concurrent agents find them in the first hour:

  • Double-booking is impossible at the database level, via a partial unique index over the slot plus optimistic locking. An application-level check always loses that race.

  • Idempotency keys on booking, so a retrying agent gets the same appointment back, not a duplicate.

  • Store UTC, present America/Bogota. Naive datetimes are rejected rather than guessed.

The scopes deliberately do not nest. A write token cannot read the reason for consultation and a clinical token cannot cancel an appointment, because "administrative" and "clinical" are different kinds of authority, not different amounts of it.

Quickstart

cp .env.example .env      # local placeholders only; no real secrets exist here
make up                   # postgres + backend + authorization server + mcp
make smoke                # walks the whole client path and prints each step

make up takes about ten seconds from cold and leaves you with:

MCP server

http://localhost:8080/mcp

Domain API docs

http://localhost:8000/docs

Authorization server

http://localhost:9000/.well-known/oauth-authorization-server

Connect the MCP Inspector

make inspector            # opens the Inspector, already authenticated
make inspector-cli        # or list the catalogue without a browser

make token prints an access token obtained through the real PKCE flow, for curl or for pasting into any client. Try issuing a read-only token and calling cancelar_cita, the refusal explains exactly what to do next.

Swap the authorization server for Keycloak

make keycloak             # Keycloak on :9100 + a second MCP server on :8081
make keycloak-verify      # proves the swap works, and that tokens don't cross over

--profile keycloak starts a second MCP server, same image, same code: trusting a real Keycloak realm instead of the in-repo authorization server, and runs it side by side with the original. Only OAUTH_ISSUER and OAUTH_JWKS_URL differ.

make keycloak-verify obtains a token from Keycloak, uses it against that server, and then shows the two are not interchangeable: each server returns 401 for the other's token. That refusal is the audience binding working, and it is why the realm carries an explicit audience mapper. Keycloak omits aud unless asked, and a resource server that accepts an audience-less token accepts every token that IdP ever issued, to anyone.

Development

make install     # uv sync
make lint        # ruff + mypy --strict
make test-unit   # fast tests, no docker required
make check       # everything CI runs

Testing

Every layer is tested against the real thing: real PostgreSQL (never SQLite, where partial unique indexes, native enums and timezone-aware timestamps do not exist), the real MCP server, the real authorization server.

Suite

What it proves

tests/unit

State machine (exhaustive 7×7 + property-based), affiliation, receivables, waiting-list ordering, time handling, error contracts

tests/integration

Schema constraints, migration reversibility, seed determinism, two live connections racing for one slot

tests/contract

The MCP surface: catalogue, tool schemas, descriptions, resources, prompt, and every tool executed end to end

tests/security

The full 13 × 3 scope matrix, approval replay/expiry/tampering, PKCE enforcement, JWT audience and alg=none, Host/Origin guards, rate limiting

scripts/smoke.py

The whole client path over real HTTP, run in CI

813 tests: 346 unit, 231 integration, 86 contract, 150 security. Want to check it yourself? docs/manual-testing.md is a 25-minute walkthrough of thirteen checks, each saying what to run and what you should see. docs/inspector.md covers the same ground through the MCP Inspector.

CI gates on a 95% coverage floor (currently 99%), mypy --strict, ruff, bandit, pip-audit, and a grep that fails the build if a secret-shaped literal or a private key ever lands in the source.

Repository layout

backend/            domain source of truth, knows nothing about MCP
  domain/           pure logic: estados, cartera, afiliacion, lista_espera, tiempo, errores
  models.py         SQLAlchemy 2.x schema · api.py  internal REST API
  seed.py           deterministic synthetic data (Faker, fixed seed)
mcp_server/
  tools/            read.py · write.py · clinical.py · confirmacion.py
  auth.py           OAuth verification and scopes      (layers 1-2)
  aprobacion.py     signed human-approval tokens       (layer 3)
  errores.py        structured, actionable failures    (layer 4)
  auditoria.py      audit log · limites.py rate limit  (layer 5)
  oauth/            the in-repo authorization server
tests/              unit · integration · contract · security
docs/               architecture.md · security.md (bilingual)

License

MIT. Portfolio project, Horizonte Labs.

Maintenance

ActivityMaintained
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Exposes tools from the Ecuro Light API for managing clinical appointments, patient records, and clinic availability. It enables users to perform healthcare management tasks such as scheduling, patient search, and report generation through MCP-compatible clients.
  • A
    license
    A
    quality
    B
    maintenance
    Enables interaction with Open Dental practice management software, allowing reading of patients, appointments, providers, procedures, and recalls, as well as writing communications back to patient charts via MCP clients.
    14
    4
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    An MCP server for clinical workflows with tools for patient lookup, appointment booking, prescriptions, drug interactions, symptom triage, lab results, insurance eligibility, and telehealth, enforcing role-based access control and audit logging.
    1

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dylanbc1/dental-clinic-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server