doctolib-mcp
MCP server for Doctolib that enables searching practitioners, checking availability, and booking or canceling appointments across Doctolib Germany, France, and Italy.
README
doctolib-mcp
I injured myself the other day, and now we have a Doctolib MCP server.
MCP tools over Doctolib's internal JSON API. Search practitioners, read their visit motives, and find the earliest bookable slot across an entire specialty in one call. With a logged-in session it also reads your own appointment calendar, and behind an explicit confirmation step it can book and cancel.
Works against doctolib.de, doctolib.fr and doctolib.it through the country
argument. The specialty slug list is German only, but free-text keywords work everywhere.
Why this exists
Doctolib's official API is partner-only, restricted to medical software vendors and hospital information systems. There is no self-serve tier to sign up for. The only thing on offer elsewhere was an Apify scraper actor, which returns directory data with no availability and no booking, at roughly $30 per 1000 records.
What does exist is the JSON that Doctolib's own patient frontend talks to, and it turns out to be enough for the whole job.
The tool worth having is find_earliest_appointment. The website makes you open
practitioners one at a time, which is precisely the part that stops working when what you
want is the first free appointment instead of one particular doctor. This searches, fans
out over every candidate's availability in parallel, and returns them sorted by soonest
slot.
Tools
| Tool | What it does | Writes? |
|---|---|---|
find_earliest_appointment |
search plus parallel availability fan-out, sorted by soonest slot | no |
search_doctors |
practitioners only, no availability check | no |
get_practitioner |
every visit motive, agenda and place for one profile | no |
get_availabilities |
open slots for one motive/agenda/practice triple | no |
list_specialties |
common German specialty slugs | no |
booking_url |
the URL a human opens to book in a browser | no |
my_appointments |
your own confirmed, pending and past appointments | no, but authenticated |
session_status |
whether the stored login is still live | no |
abort_booking_draft |
release a lingering temporary slot hold | yes, but harmless |
book_appointment |
books a real appointment. See docs/safety.md | yes, binding |
cancel_appointment |
cancels a real appointment. See docs/safety.md | yes, binding |
Search results already carry visitMotiveId, agendaIds and practiceId, so a search
chains straight into availabilities without a per-practitioner round trip. The full
reverse-engineered endpoint contract lives in docs/endpoints.md.
Install
cd ~/dev/doctolib-mcp
uv venv
uv pip install "mcp>=2.0.0" "httpx>=0.27" "typer>=0.12"
The project sets package = false under [tool.uv], so it is never installed into the
venv. Everything runs with PYTHONPATH=src, which is why the MCP registration below sets
that environment variable. There is no installed entry point to fall back on.
For the authenticated tools, add Playwright:
uv pip install "playwright>=1.44"
./.venv/Scripts/python.exe -m playwright install chromium
Registered in ~/.claude.json as:
"doctolib": {
"command": "/absolute/path/to/doctolib-mcp/.venv/Scripts/python.exe",
"args": ["-m", "doctolib_mcp.mcp_server"],
"env": { "PYTHONPATH": "/absolute/path/to/doctolib-mcp/src" }
}
pi registers the same server lazily, with a directTools list that deliberately omits
book_appointment and cancel_appointment so the two binding writes stay behind the
proxy instead of sitting one token away in the default tool list.
Logging in
Both binding writes and my_appointments need a real logged-in session. Chrome wraps its
own cookie store in App-Bound Encryption (v20 / APPB keys), so there is no reliable way
to lift a session cookie out of it offline, and the session cookie is httpOnly so
JavaScript cannot read it either. Rather than fight that, this server owns a separate
browser profile: a persistent Playwright user-data-dir under .session/<country>/ that you
log into once.
PYTHONPATH=src ./.venv/Scripts/python.exe -m doctolib_mcp.cli login
A real browser window opens. Sign in, complete any 2FA, and the command returns once the
session goes live. The profile survives restarts, so this is a one-time step until Doctolib
actually expires the session. session_status reports when that has happened.
You do not have to run that by hand
When an authenticated tool finds no live session, it opens the login window itself and
retries the call once the login exits successfully. So my_appointments against an expired
session shows you a browser, waits while you sign in, and then answers, instead of returning
an error that tells you to go and run a command.
Three details govern how that behaves:
It runs the login as a subprocess and treats exit code 0 as the completion signal,
which doctolib login gives only after it has probed the session and found it live. That
also sidesteps a hard constraint. Chromium takes an exclusive lock on the profile directory,
so a login cannot start while another context is open on it, and every caller closes its own
context before the login is spawned.
It is opt-out through DOCTOLIB_AUTO_LOGIN=0. A blocking browser window is right at a
terminal and wrong in an unattended worker, so anything running without a human in front of
it should set that and get the immediate failure instead.
It backs off and serialises. A dismissed window is not reopened for 90 seconds, and concurrent callers produce one window between them rather than one each.
Public reads deliberately do not trigger any of this. They work anonymously, so a plain search never pops a browser at you. Only the tools that genuinely cannot proceed without authentication will.
If your MCP client's own request timeout is shorter than the login takes, the tool call will fail while the window is still open. Finish signing in anyway. The session persists, and the next call picks it up.
.session/ is gitignored and holds a live login. It must never be committed, copied into a
scratch directory, or handed to another agent.
The public reads use the session too
Once you are logged in, the read tools stop querying anonymously and go through the session.
Without it they miss whatever the backend personalises for a known patient: existing-patient
("Bestandspatient") visit motives, slots that some practices only show to logged-in known
patients, the correct insurance sector, and personalised ranking. Every read result carries
"authenticated": true or false so it is always clear which view produced the answer.
With no live session the read tools fall back to the anonymous public view silently. They remain fully functional, just not personalised.
Mechanically the reads take the fast path. The session cookie is read out of the login
profile, cached in-process for five minutes so a fan-out like find_earliest_appointment
costs one browser launch instead of twenty, and replayed over httpx. Only the write path
insists on running inside the real browser context.
Writes
book_appointment commits a real clinical slot at a real practice under a real name. If it
succeeds, a clinic has put you in their calendar and a human being expects you to physically
show up. cancel_appointment is the inverse and releases that slot irreversibly.
Three things guard them, and it is worth being precise about which is which:
confirm=Falseis the default, and on that pathbook_appointmentperforms no network call at all. It returns the payload that would be sent so an agent can read the appointment back to you in plain language and get an explicit yes.cancel_appointmentwithconfirm=Falseperforms one read, fetching the valid cancellation reasons, and issues noDELETE.- A live login session is required. There is no cookie parameter on the booking client, because the funnel is session-bound and needs real CSRF tokens and draft state rather than a replayed cookie. Without a session the call fails before it can commit anything.
- Neither tool is in pi's
directTools, so on that box they stay behind the proxy.
One thing that does not protect you: DoctolibBookingClient takes no cookie parameter
and reads no DOCTOLIB_COOKIE, so leaving that environment variable unset guards nothing.
The confirmation step and the login requirement are the whole list.
docs/safety.md records where that particular misconception came from.
The booking funnel is four stateful steps sharing a server-side draft, and it has never been executed end to end, because that cannot be tested without making a real booking. Cancelling has been executed once, on 05.08.2026, as an explicitly authorised real cancellation. Both contracts, the failure modes, and the rules an agent should follow are written up in docs/safety.md.
CLI
Every tool is reachable without an MCP client, so a misbehaving tool reproduces in one shell command:
PYTHONPATH=src ./.venv/Scripts/python.exe -m doctolib_mcp.cli earliest handchirurg --city Berlin --within-days 21
Available commands: earliest, search, practitioner, slots, login, status,
appointments, abort-draft, cancel. The cancel command is a preview unless you pass
--confirm.
Tests
uv pip install "pytest>=8.0"
PYTHONPATH=src ./.venv/Scripts/python.exe -m pytest tests -q
111 tests, about six seconds, no network. The suite runs entirely offline because a
conftest.py fixture blocks real socket connections,
blocks Playwright from launching, and repoints the session directory at a temporary path, so
a test that reaches for the network or for the real login fails loudly instead of quietly
succeeding. For a repository that can cancel real medical appointments, a test run that can
touch the live account is itself the hazard.
What the tests are there to catch, in descending order of what it costs when they go red:
- The confirmation step never writes. A regression that makes
confirm=Falsecommit something is the worst failure this codebase has available to it. _pick_patientchooses the right person. On an account with family members, the wrong branch books a clinical slot for the wrong human._build_confirm_payloadinvents nothing. Every field submitted to a medical intake form has to have come from the server's own prefill.- The booking funnel calls its four steps in the documented order, at the documented paths. This is as close to end-to-end as is reachable without making a real booking, which makes it the most useful thing the suite does.
- Failures after step one tear the draft down, so a broken run never leaves a slot held.
- Answers that would be silently wrong rather than loudly broken: the Nominatim bounding-box
transposition, the multi-chunk availability walk, and the
specialityfield that arrives as a string in one payload and a dict in another.
session.login() and session.status() are deliberately untested. They drive a visible
interactive browser, and mocking Playwright deeply enough to cover them would only test the
mock. The commit hop itself is also untested, for the obvious reason.
Etiquette
Requests are throttled per host with jitter, and the availability fan-out is capped at four workers. Keep it that way. This is a personal-use tool hitting a healthcare provider's undocumented endpoints, and the polite failure mode is being slow rather than being blocked.
推荐服务器
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 模型以安全和受控的方式获取实时的网络信息。