expert-booking-mcp
by Dante-dan
README.md
# Expert Booking MCP Server
一个面向 AI Agent、支持渐进式工具发现的专家预约 MCP Server。默认端点采用 **Streamable HTTP** 和 MCP `2026-07-28` modern era 的 `server/discover`;为保证终态响应时间可验证,旧版 stateless/SSE fallback 会被明确拒绝,不做静默降级。
## 一条命令验证
```bash
npm install && npm test
```
测试会真实启动 Streamable HTTP MCP 端点,用官方 Client 完成能力发现、工具发现与工具调用,并覆盖预约状态机。完整可重复 Demo:
```bash
npm run demo
```
启动持久化服务:
```bash
cp .env.example .env
set -a; source .env; set +a
npm run dev
# MCP endpoint: http://127.0.0.1:3000/mcp
```
生产构建使用 `npm run build && npm start`。服务默认仅绑定 localhost,并启用 Host/Origin 校验;远程暴露前必须另加认证。
设计动机、现实风险和未覆盖范围见 [NOTES.md](NOTES.md)。
## 给本地 Agent 的安装提示词
把下面内容复制给需要安装 MCP 的编码 Agent:
```text
请在当前仓库安装并接入 expert-booking-mcp:
1. 确认 Node.js >= 22,运行 npm ci。
2. 运行 cp .env.example .env && npm run dev,MCP 地址是 http://127.0.0.1:3000/mcp。
3. 按当前 MCP Host 的配置方式添加这个 Streamable HTTP Server,不要改成 stdio。客户端请求需复用 src/temporal-http.ts 的 createTemporalClientFetch,以附带服务要求的动态时间字段。
4. 连接后确认 tools/list 能发现 tool_catalog.search、tool_catalog.read、tool_catalog.call,并通过目录工具搜索、读取和调用 booking 工具。
5. 运行 npm test 验证安装;需要查看完整预约流程时运行 npm run demo。付款由用户在 Agent 外完成,不要实现或代替付款。
如果失败,请保留原始错误并定位原因,不要通过关闭参数校验、时间校验或 Host/Origin 校验来绕过问题。最后告诉我 MCP 配置位置、实际运行的命令和验证结果。
```
## Agent 使用流程
默认 `tools/list` 只返回三个稳定、低上下文成本的 MCP Tool:
| MCP Tool | 用途 |
|---|---|
| `tool_catalog.search` | 检索式预筛选;返回标题、`tool://` 位置、摘要、总数和 `nextCursor` |
| `tool_catalog.read` | 通过 `offset/limit` 分片读取固定的完整 schema,明确报告截断与下一段位置 |
| `tool_catalog.call` | 以未经转换的 `arguments` 调用已加载的逻辑工具 |
目录中的四个固定逻辑工具是:
| 逻辑工具 | 关键输入 | 结果 |
|---|---|---|
| `booking.search_available_slots` | `query`、显式 `limit`、可选时间/专业过滤和 `cursor` | 结构化时段候选、总数、下一页游标 |
| `booking.hold_slot` | `slotId`、原样保存的客户信息、UUID `idempotencyKey` | 五分钟独占预约、支付页、完整快照和 Server 签发的 `confirmationIntent` |
| `booking.confirm_booking` | `bookingId`、`expectedVersion`、UUID 幂等键、两分钟内且绑定 intent 的人工批准 | intent 与支付双重校验后的完整确认快照 |
| `booking.get_booking` | `bookingId` | 当前完整预约快照 |
推荐 Host 轨迹:
1. `server/discover`:发现 MCP 能力与协议版本。
2. `tools/list`:仅加载上述三个目录工具。
3. `tool_catalog.search`:搜索相关逻辑工具;状态栏只保留返回的工具名。
4. `tool_catalog.read`:按需读 schema;读完整后将 schema 一次追加到上下文尾部,此后留在原历史位置以命中前缀缓存。
5. `tool_catalog.call`:把用户/Agent 给出的业务参数原样送入严格校验和执行。
第 3–5 步是服务提供的协议能力;“把 schema 放到对话历史的哪个位置”最终由 MCP Host 控制,Server 通过 instructions、固定 schema 和分页元数据给出可执行约定。
## 状态机与事务
```text
AVAILABLE SLOT
│ hold (SQLite IMMEDIATE transaction)
▼
HELD / payment=PENDING ───── expiresAt reached ────▶ EXPIRED
│
├── queryPayment=PENDING ─────────────────────▶ HELD
├── queryPayment=FAILED ─────────────────────▶ PAYMENT_FAILED
├── queryPayment=CLOSED ─────────────────────▶ PAYMENT_CLOSED
└── fresh human approval + exact PAID match ──▶ CONFIRMED
```
- `one_active_booking_per_slot` 部分唯一索引从存储层保证每个时段最多一个 `HELD/CONFIRMED` 预约。
- 临时预约的 `expiresAt = heldAt + 5 分钟`。每次相关读写前在同一事务中清理过期预约,因此进程重启后也不会丢失释放动作。
- 本地状态变更全部位于 SQLite 事务内;异常会回滚整个本地变更。只读支付查询在事务外执行,返回后重新读取版本并在一个事务中提交最终状态,避免持锁等待外部网络。
- 支付初始状态只能是 `PENDING`,确认只允许本地 `PENDING → PAID`。`bookingId` 同时是不可变、唯一的 `payment.queryId`,重复确认查询同一个支付对象。
- `expectedVersion` 提供乐观并发控制。Hold 与 Confirm 各自要求 UUID 幂等键;同键同参返回原结果,同键异参返回 `IDEMPOTENCY_CONFLICT`。
- `FAILED/CLOSED` 会事务性释放时段。支付已经成功但预约在外部查询完成前过期时,服务拒绝确认;退款/人工对账按题目范围留给外围系统。
## 参数保真与错误契约
逻辑工具由 AJV 严格校验:`coerceTypes=false`、`useDefaults=false`、`removeAdditional=false`、所有对象 `additionalProperties=false`。例如邮箱不会转小写,数字字符串不会转换为数字,未知字段不会被丢弃。业务参数与调用上下文分开传递,日志上下文不会注入业务参数。
所有执行结果都是机器可读 envelope。写操作返回完整的变更后预约,而非空 `200 OK`:
```json
{
"ok": false,
"error": {
"code": "PAYMENT_PENDING",
"message": "Payment has not completed; the booking remains held until expiresAt.",
"retryable": true,
"details": { "bookingId": "booking-..." },
"recovery": { "action": "Ask the user to finish payment, then retry confirmation before expiry." }
},
"meta": {
"traceId": "32 lowercase hex characters",
"spanId": "16 lowercase hex characters",
"operationId": "UUID",
"clientSentAt": "RFC 3339",
"serverReceivedAt": "RFC 3339",
"serverResultCreatedAt": "RFC 3339",
"cache": "BYPASS"
}
}
```
输入 schema 错误由 MCP SDK 或 `INVALID_ARGUMENTS` 返回;可预期业务错误不会泄露堆栈。主要错误包括 `SLOT_UNAVAILABLE`、`BOOKING_EXPIRED`、`VERSION_CONFLICT`、`PAYMENT_PENDING`、`PAYMENT_MISMATCH`、`HUMAN_APPROVAL_EXPIRED`、`APPROVAL_INTENT_HASH_MISMATCH`、`APPROVAL_INTENT_MISMATCH` 和 `IDEMPOTENCY_CONFLICT`。
## Human in the loop
临时预约是可自动重试、会自动释放的动作。最终确认必须携带:
```json
{
"humanApproval": {
"approvalId": "UUID",
"decision": "APPROVE",
"approvedAt": "2026-08-26T09:30:00.000Z",
"intent": {
"bookingId": "booking-...",
"slotId": "slot-ai-001",
"startsAt": "2030-09-02T02:00:00.000Z",
"amount": "500.00",
"currency": "CNY",
"version": 1
},
"intentHash": "sha256:<64 lowercase hex>"
}
}
```
`hold_slot` 和处于 `HELD/PENDING` 的 `get_booking` 会返回 `confirmationIntent`。Host 必须向用户展示对应的 booking、专家、时段、金额、币种和版本,并把其中的 `intent`、`intentHash` 原样复制到批准对象;不要由 Agent 重建或修改。Server 先重新计算 hash,再把六个字段与当前 booking 精确比较,任一不一致都不会查询支付。
v1 canonical intent 是按 key 升序排列的无空白 UTF-8 JSON:`amount, bookingId, currency, slotId, startsAt, version`。`intentHash` 是 `sha256:` 加该字节串的 64 位小写十六进制 SHA-256。Server 返回 hash,因此一般 Client 只需原样传递。这个无密钥 hash 绑定内容完整性,但不是对批准者身份的数字签名;需要抵御不可信 Host 时,应把 v1 binding 升级为 Server HMAC 或签名 challenge。
批准有效期为 120 秒,允许 5 秒时钟偏差;缺失、过期、未来时间、缺少 binding 或 binding 不匹配的默认行为都是 **不查询支付、不确认预约**。确认成功后 `confirmationIntent` 变为 `null`,避免把旧 intent 当成可再次批准的 challenge。
## 支付依赖 seam
预约模块只依赖下面的只读 port:
```ts
interface PaymentQueryPort {
queryPayment(bookingId: string, trace: TraceContext): Promise<PaymentResult>;
}
```
生产 adapter 对 `PAYMENT_QUERY_BASE_URL/payments/{bookingId}` 发 GET,请求超时 3 秒并传递 `traceparent`。测试和 Demo 注入 Stub;仓库没有实现支付服务或支付动作。支付结果必须精确匹配 `bookingId`、金额字符串和币种,`PAID` 还必须有 `paidAt`。
## 按需加载、分页与缓存
- Tool 搜索和时段搜索都返回结构化候选,不拼接全文。
- 游标是与原查询绑定的不透明 token;跨查询复用会返回 `INVALID_CURSOR`。
- `tool_catalog.read` 以 0-based `offset` 和显式 `limit` 读取,返回 `totalLines`、`truncated`、`omittedLines`、`next` 和人类可读继续方式。
- 固定 Tool 目录的 search/read 结果可永久安全复用;可预约时段查询缓存 5 秒,任何预约写入或过期释放都会使其失效。
- SQLite WAL 允许多个只读感知调用安全并发;写入由数据库事务和唯一索引串行化。
## 可观测性
每个目录动作和逻辑工具动作都向 stderr 输出 JSON Lines,包含 `source`、RFC 3339 `timestamp`、`caller`、动作、耗时、`trace_id`、`span_id`、`operation_id`。支付查询创建子 span 并通过 W3C `traceparent` 传播。
调用方显式传入 UUID `requestSpanId`;服务用它确定性派生 128-bit `trace_id`,每次动作生成新的 64-bit `span_id`。OpenTelemetry 标准要求 `span_id` 是 16 个十六进制字符而不是 UUID,因此另行生成 UUID `operation_id`,没有伪造不兼容的 trace context。代码同时调用 OpenTelemetry API;部署方可注册 exporter。
所有 MCP HTTP 请求必须由显式 client adapter 在真正发送边界加入 `x-mcp-client-sent-at`,并在 `params._meta` 写入同值;Server 不会替 Client 猜测或注入该时间。Server 在入口和响应边界分别加入 `x-mcp-server-received-at`、`x-mcp-server-responded-at`,JSON-RPC `result._meta` 同样携带三段时间;Tool envelope 另以 `serverResultCreatedAt` 标识业务结果生成时刻。因此 Agent 可验证:
```text
clientSentAt ≤ serverReceivedAt ≤ serverResultCreatedAt ≤ serverRespondedAt ≤ clientReceivedAt
```
缺失或非法 RFC 3339 客户端时间会得到 HTTP 400 / `MISSING_CLIENT_TIMESTAMP`,响应不会伪造客户端时间,并回显 JSON-RPC request id。支付查询这个 Server 发出的外部 GET 也携带 `x-mcp-server-request-sent-at`;依赖响应的发送时间(若对方提供)及本地接收时间进入结构化日志。当前工具响应均为 JSON;未来启用 SSE 进度/Server-initiated MCP request 前,需要为每个事件补独立发送时间,而不能只沿用建流 Header。
## 最重要的测试场景
1. **竞争与过期释放**:先验证第二位用户无法占用,再推进时钟超过五分钟并验证其可预约。这直接覆盖“同一时段仅一人”和“过期后重新开放”两个核心不变量。
2. **先查后做的支付确认**:`PENDING` 保持预约不变,随后同一支付查询 ID 返回 `PAID`,只发生一次本地 `PENDING → PAID/CONFIRMED`。这是外部付款与本地事务交界处风险最高的路径。
3. **金额/身份不匹配回滚**:支付返回 `PAID` 但金额错误,本地仍保持 `HELD/PENDING/version=1`,证明外部查询无法造成部分提交。
4. **幂等重放与参数漂移**:同键同参得到同一预约;同键异参明确冲突,防止 Agent 重试制造重复副作用。
5. **Human approval 超时**:过期批准不会调用支付依赖,验证默认拒绝行为。
6. **真实 MCP modern discovery**:官方 Client 通过 Streamable HTTP 协商到 `modern`,发现三项稳定工具,再检索和调用逻辑工具。
7. **独立进程时序黑盒**:测试先构建,再 `spawn` 生产入口,用官方 Client 验证每次 wire request/response、Tool envelope 与日志的时间顺序,并确保子进程和临时数据库被清理。
## 假设与范围
- 预置三位专家和六个 2030 年 UTC 时段;不实现专家管理。
- 所有时间通过 RFC 3339 传输,存储为 UTC ISO 字符串;服务不静默转换调用参数。
- 跨机器时序比较假设 Client、预约服务和支付服务通过 NTP 保持时钟同步;生产告警应配置允许偏差。Server 不根据偏差静默改写调用方时间。
- 金额用两位小数字符串,币种用三位大写代码,避免浮点误差。
- 最小实现没有账户认证;localhost 运行且 booking ID 不可猜。生产环境必须在 MCP handler 前验证身份,并按 caller 限制预约读取。
- 付款页 URL 仅由配置基址和 booking ID 组成;本仓库不提供页面。
- 退款、通知、后台、已付款但过期后的自动对账,以及云部署不在范围内。
## 项目结构
```text
src/
booking-service.ts 深预约模块:状态机、幂等、事务、缓存
database.ts SQLite schema、约束与预置数据
tool-catalog.ts 固定逻辑 schema、检索、分片读取、严格分发
mcp-server.ts 三个稳定 MCP Tool 与 Streamable HTTP handler
payment.ts 只读支付 HTTP adapter
observability.ts OTel hooks 与结构化日志
temporal-http.ts MCP Client/Server wire 时间契约
test/ 状态机与真实 HTTP MCP 集成测试
scripts/demo.ts 可重复端到端演示
NOTES.md 设计动机、现实风险、取舍与明确未覆盖范围
```
实现对齐官方 [MCP TypeScript SDK HTTP Guide](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/http.md)、[MCP Tools Guide](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/servers/tools.md)、[Streamable HTTP 2026-07-28 规范](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/transports/streamable-http.mdx) 与 [MCP Pagination 规范](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination)。
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing