内容
# mcp-jadwal-dokter
一个MCP(Model Context Protocol)服务器,提供虚拟的医院医生预约工具:搜索医生、查看预约时间表、检查可用时间段和创建预约。它是为由LLM代理驱动而设计的,可以作为一个pip包运行,通过一个命令即可启动。
服务器是确定性的:数据加上工具,没有模型和API密钥。这就是重点。MCP服务器是工具层;模型位于客户端。单独的Streamlit演示(在`demo/`中)允许LLM在代理聊天中驱动这些工具,例如查找儿科医生在星期四下午是否有空并预约。
这是一个作品集项目(大型项目集中的第11个项目)。`PLAN.md`是完整的构建计划和设计记录。
## 架构
```mermaid
flowchart LR
用户 -->|聊天| Streamlit
Streamlit -->|异步运行每个回合| 代理[代理循环]
代理 <-->|提示加上工具 / 工具调用| LLM[LLM 提供者<br/>Anthropic 或 OpenRouter]
代理 <-->|stdio MCP| 服务器[jadwal-dokter MCP 服务器]
服务器 --> 工具[search_doctors / get_schedule<br/>check_slot / create_booking]
工具 --> 数据种子[(doctors.json, 只读)]
工具 --> 数据库[(SQLite 预约)]
```
从MCP服务器到右侧的所有内容都是已发布的包,不需要密钥。LLM和Streamlit客户端是演示,并且只有它们读取API密钥。每个用户回合都会打开一个到服务器的stdio会话,运行工具调用循环,然后关闭。
## 工具
八种工具,加上资源和提示:
| 工具 | 功能 |
|------|--------------|
| `get_today()` | 服务器的日期和星期几,以便代理将相对短语(“下周四”)解析为服务器时钟。 |
| `search_doctors(专业?, 名称?, 日期?)` | 按专业、名称子字符串和/或执业日期查找医生。筛选条件是可选的,并且组合使用AND。 |
| `get_schedule(医生ID)` | 医生的每周执业时间块和任何休假时间段。 |
| `check_slot(医生ID, 日期)` | 医生在ISO日期的可用30分钟时间段。如果是休息日、休假日期或全天,则返回一个带有说明的空列表,而不是错误。 |
| `create_booking(医生ID, 日期, 时间, 患者姓名)` | 创建预约。验证医生、执业日期、休假、时间段、可用性,并拒绝过去的日期,如果无法预约,则返回特定的、可操作的错误。 |
| `cancel_booking(预约ID)` | 取消预约;时间段再次空闲,历史记录保留为`cancelled`。 |
| `reschedule_booking(预约ID, 新日期, 新时间)` | 将确认的预约移动到另一个时间段,保持其ID;如果失败,则原始预约保持不变。 |
| `list_bookings(医生ID?, 日期?, 患者姓名?, 状态?)` | 预约的筛选列表,包括已取消的预约。 |
资源:`jadwal://roster`,`jadwal://specializations`,`jadwal://info`。提示:`book_appointment`。工具描述和错误消息可用印尼语(默认)或英语通过`JADWAL_LANG`。
所有数据都是虚构的。捆绑的示例是一个虚拟医院(“Rapha Hospital”):12个专业领域的32名医生,各种每周时间表和刻意的边缘情况(休假的医生、完全预约的日期、具有单个医生的专业)。预约持续在SQLite中,因此预约的时间段显示为已占用。
### 使用您自己的 Roster
将`JADWAL_DATA`指向一个JSON或YAML文件(与捆绑的示例相同的架构),服务器将使用您的医生时间表:
```bash
JADWAL_DATA=/path/to/my-roster.yaml mcp-jadwal-dokter
```
复制`jadwal_dokter/seed/doctors.example.yaml`作为起点。每位医生都有一个`id`、`name`、`specialization`、`weekly_blocks`(星期一至星期日,`start`/`end`为HH:MM;时间段为30分钟)和可选的`leave_periods`(包含ISO日期范围)。一个裸列表或一个`{doctors: [...]}`包装器都有效。YAML需要额外的 `[yaml]` (`pip install "mcp-jadwal-dokter[yaml]"`)。
## 需求
Python 3.10 或更新版本。
## 安装和运行服务器
在开发过程中,从此目录:
```bash
python -m venv .venv
.venv/Scripts/python -m pip install -e . # 在 macOS/Linux 上:.venv/bin/python
```
通过stdio运行服务器:
```bash
mcp-jadwal-dokter
```
服务器在stdio上使用JSON-RPC,并将日志记录到stderr,因此它旨在由MCP客户端启动,而不是直接在终端中使用。
它发布在 [PyPI](https://pypi.org/project/mcp-jadwal-dokter/) 上,因此也可以在无需检查的情况下运行:
```bash
uvx mcp-jadwal-dokter # 或:pipx install mcp-jadwal-dokter
```
### 从 Claude Desktop 使用
将服务器添加到 `claude_desktop_config.json`:
```json
{
"mcpServers": {
"jadwal-dokter": {
"command": "uvx",
"args": ["mcp-jadwal-dokter"]
}
}
}
```
对于本地可编辑的检出,而不是发布的包,请将命令指向venv的 `mcp-jadwal-dokter` (或 `python -m jadwal_dokter.server` )并设置 `JADWAL_DB` 如果您希望将预约文件放在特定位置。
## 运行演示(代理聊天)
演示是一个可选的额外项,具有其自己的依赖项和API密钥:
```bash
.venv/Scripts/python -m pip install -e ".[demo]"
copy .env.example .env # 然后设置您的密钥,请参见下文
.venv/Scripts/python -m streamlit run demo/streamlit_app.py
```
在 `.env` 中设置一个密钥:`OPENROUTER_API_KEY` (带有 `JADWAL_PROVIDER=openai` )或 `ANTHROPIC_API_KEY` (带有 `JADWAL_PROVIDER=anthropic` )。然后点击 “Jalankan skenario contoh” 并观察代理链 `search_doctors -> check_slot -> create_booking` ,每个调用都在工具调用日志中显示。
该场景的捕获的端到端运行记录在 [`SAMPLE_RUN.md`](SAMPLE_RUN.md) 中。
## 全栈(Vue 控制台 + 聊天)
除了 Streamlit 演示之外,还有一个更丰富的 Web 应用程序:一个 FastAPI 后端(`backend/`),它持有密钥并驱动 MCP 服务器,以及一个 Vue 前端(`frontend/`),具有聊天和 MCP 控制台(花名册、时间表、预约、工具调用面板、指标和工具调用历史)。参见 `docs/design-system.md` 和 `frontend/README.md`。
运行三个进程(MCP 服务器以 HTTP 模式运行,后端和 Vite 开发服务器):
```bash
# 1. MCP 服务器以 HTTP 模式运行
JADWAL_TRANSPORT=http JADWAL_API_KEY=dev-key mcp-jadwal-dokter
# 2. 后端(指向 MCP 服务器;持有来自 .env 的提供者密钥)
JADWAL_MCP_URL=http://127.0.0.1:8000/mcp JADWAL_API_KEY=dev-key \
.venv/Scripts/python -m backend.app
# 3. 前端开发服务器(代理 /api 到后端)
cd frontend && npm install && npm run dev # http://localhost:5173
```
在生产环境中,`npm run build` 会发出 `frontend/dist`,后端从同一个源提供服务,因此它是一个服务器。托管时,设置 `JADWAL_BACKEND_KEY` 和 `JADWAL_BACKEND_RATE_LIMIT` 以控制 `/api`,以及 `JADWAL_CORS_ORIGINS` 以适应您的域名。注意:后端没有每个用户的身份验证;它是一个单租户演示(参见 `docs/decisions/`)。
关于免费模型:OpenRouter `:free` 模型按上游限制速率并可能返回 `429`。应用程序通过重试和干净的错误消息处理此问题。为了顺利运行,请添加一小笔 OpenRouter 信用额度,或使用 Anthropic 密钥。
## 工具设计决策
工具是这里的产品,因此设计选择是刻意的:
1. **动词-名词名称,架构来自类型。** `search_doctors`、`get_schedule`、`check_slot`、`create_booking`。键入参数成为 JSON 输入架构,文档字符串成为描述,以便模型可以仅凭架构挑选和填充工具。
2. **为模型编写错误消息。** 预约失败会说明有什么问题以及下一步该怎么做(“14:00 的时间段已经预约;可用时间段:15:00,15:30”),而不是“400”。代理恢复而不是放弃。
3. **紧凑的结构化返回。** 每个工具返回一个小的键入对象,而不是一个段落。较少的令牌,模型可靠地读取字段。
4. **不可用是一个答案,而不是错误。** 休息日、休假日期或全天从 `check_slot` 返回一个带有注释的空时间段列表,以便代理可以推理并尝试另一天。只有真正糟糕的输入(未知 ID、格式错误的日期)才会引发错误。
5. **虚拟但持久的预约。** SQLite 因此预约然后显示时间段已占用,并有一些预先设定的时间段已满。没有真正的后端的状态。
6. **LLM 在客户端,不在服务器上。** 服务器是数据加上工具,因此它安装、运行和测试时无需密钥。这就是 MCP 服务器的作用,这也是使包广泛有用的原因。
## 配置
| 变量 | 目的 | 默认值 |
|----------|---------|---------|
| `JADWAL_DATA` | 自定义 Roster(JSON/YAML)的路径。 | 捆绑的示例 |
| `JADWAL_DB` | SQLite 预约文件的路径。 | `bookings.db` |
| `JADWAL_LANG` | 工具/错误语言:`id` 或 `en`。 | `id` |
| `JADWAL_TZ` | 日期和 `get_today` 的 IANA 时区。 | `Asia/Jakarta` |
| `JADWAL_TODAY` | 固定的“今天”(ISO 日期)以进行可重复运行。 | 真实的时钟 |
| `JADWAL_TRANSPORT` | `stdio` 或 `http`(Streamable HTTP 在 `/mcp`)。 | `stdio` |
| `JADWAL_HTTP_HOST` / `JADWAL_HTTP_PORT` | HTTP 模式下的绑定地址。 | `127.0.0.1` / `8000` |
| `JADWAL_API_KEY` | HTTP 模式:需要此密钥(`Bearer`/`X-API-Key`)。 | 无(无身份验证) |
| `JADWAL_RATE_LIMIT` | HTTP 模式:每分钟客户端 IP 的最大请求数。 | `0`(关闭) |
| `JADWAL_PROVIDER` | 演示专用:`openai` 或 `anthropic`。 | `openai` |
| `JADWAL_MODEL` | 演示专用:覆盖模型 ID。 | 免费的 OpenRouter 模型 |
服务器核心不需要 API 密钥。`JADWAL_*` 提供者变量和 `.env.example` 中的 API 密钥仅由演示读取。
## 开发
```bash
.venv/Scripts/python -m pip install -e ".[dev]"
.venv/Scripts/python -m pytest
```
测试套件是确定性的,不需要模型或密钥:服务器工具是纯粹的,代理循环测试驱动真正的 MCP 服务器,并使用一个存根提供者。每个测试都在一个隔离的临时预约数据库中运行。
还有一个可重放的、由模型驱动的评估(`python -m eval.run`),它运行场景并检查最终状态。它需要一个可用的模型,并报告免费层级的 `429`s 作为跳过。