Skip to main content
Glama
aystzh
by aystzh
README.md
# Business Data MCP

**Secure, Auditable Read-only Search** · [English](README.en.md)

一个可接入 **Codex / Claude Code** 的 Python MCP 服务:查询 PostgreSQL 客户与订单、JSON Mock CRM、Markdown 业务文档,并统一执行权限检查、租户隔离、脱敏和审计。

所有内置业务数据都是虚构数据。项目定位是可复现的本地工程展示,生产边界见 [安全说明](docs/security.md)。

## 架构

```mermaid
flowchart TD
    A[Codex / Claude Code / SDK Client] -->|Streamable HTTP + Bearer Token| B[HTTP 认证]
    B --> C[MCP 策略层:参数校验、Scope、限流、审计]
    C --> D[业务查询服务]
    D --> P[PostgreSQL 只读账号]
    D --> R[Mock CRM JSON]
    D --> F[受控 Markdown 文档]
    P --> E[脱敏 + 来源引用 + 结果大小限制]
    R --> E
    F --> E
    E --> C
    C --> L[JSON Lines 审计]
    C --> A
```

- Python 3.12,uv 管理 `.venv` 和锁定依赖。
- MCP Python SDK 2.2,支持现代协议及需要初始化握手的客户端。
- Pydantic 校验,Psycopg 异步连接池,PostgreSQL 17。
- 四个有类型定义的只读工具,不提供 SQL 执行、任意文件读取、写操作或导出。

## 快速启动

前置条件:Docker Desktop / Docker Engine + Compose、[uv](https://docs.astral.sh/uv/getting-started/installation/)。命令均在仓库根目录执行;uv 会使用或下载 Python 3.12。

```bash
uv sync --locked
uv run business-data-init
docker compose up -d --wait
uv run business-data-mcp
```

首次初始化会创建权限为 `0600` 的 `.env` 和 `.local` 凭据文件,不打印密码或 Token,也不覆盖已有凭据。开发 Token 有效期为 30 天。

服务地址:`http://127.0.0.1:8000/mcp`。PostgreSQL 默认监听本机 `5433`,使用本项目独立 volume。

在另一个终端进入仓库根目录:

```bash
curl http://127.0.0.1:8000/health/ready
uv run business-data-demo
```

演示脚本依次切换四个身份,调用四个工具,不需要付费模型 API。输出包含脱敏结果、来源和 `query_id`。若设置了 `BUSINESS_DATA_MCP_TOKEN`,脚本会仅使用该 Token。

```bash
uv run business-data-demo --identity support
uv run business-data-demo --identity pii
uv run business-data-demo --identity tenant-b
uv run business-data-demo --identity restricted
```

## 工具

| 工具 | 参数 | 行为 |
|---|---|---|
| `search_customers` | `query`, `limit=10` | 客户名称字面子串或精确客户 ID |
| `get_customer` | `customer_id` | 客户资料及获授权的 CRM 摘要 |
| `search_orders` | `order_no`, `phone`, `status`, `date_from`, `date_to`, `limit=10` | 至少一个条件,多个条件使用 AND |
| `search_business_docs` | `query`, `limit=10` | 当前租户文档关键词检索,返回摘录和行号 |

- 关键词去除首尾空格后为 2–100 个字符;ID 仅允许字母、数字、下划线和短横线,最长 64 个字符。
- 条数为整数 1–20;无分页。订单手机号为完整值匹配,状态为 `pending / paid / shipped / cancelled`。
- 日期使用 `YYYY-MM-DD`,按 UTC 自然日包含起止日期。
- 文档按空白分词,全部词命中才返回;按出现次数降序、文档 ID 升序排列。
- 搜索无结果和访问不到的对象均返回空记录;错误使用 MCP `isError`,不伪装成空结果。

成功结果示意(`structuredContent`;文本内容也包含相同数据):

```json
{
  "records": [{
    "customer_id": "C10001",
    "name": "星河科技",
    "phone": "138****8000",
    "source_uris": ["business://tenant-a/customers/C10001"]
  }],
  "sources": [{
    "uri": "business://tenant-a/customers/C10001",
    "source_type": "postgres",
    "label": "C10001",
    "updated_at": "2026-09-14T10:00:00Z"
  }],
  "query_id": "qry_example",
  "notices": ["PII fields are masked."],
  "truncated": false
}
```

逻辑 URI 是来源标识,不是可直接打开的网址。源信息以各演示数据源提供的更新时间为准。

## 身份与审计

| 身份 | 租户 | 权限 |
|---|---|---|
| `support` | tenant-a | 四个工具及 CRM,PII 脱敏 |
| `pii` | tenant-a | 同上,额外允许原始 PII |
| `tenant-b` | tenant-b | 仅乙租户数据,PII 脱敏 |
| `restricted` | tenant-a | 仅客户查询,省略 CRM,订单和文档拒绝 |

`.local/tokens.json` 保存 Token 的 SHA-256 摘要和身份映射;`.local/client-tokens.json` 保存仅供本地演示的原始 Token。服务启动时加载映射,修改后需要重启。

审计位于 `.local/audit.jsonl`,通过返回的 ID 查找:

```bash
rg 'qry_返回的ID' .local/audit.jsonl
```

记录开始、结束、身份、租户、工具、条件字段名、条数、耗时和状态;不保存完整查询值或响应正文。认证失败有单独的 `request_id`。审计不可写时不返回业务数据。

## 接入 Codex / Claude Code:CLI 与桌面端

以下步骤以 **macOS、本地客户端、本地 MCP 服务** 为例。在仓库根目录操作,先完成前面的快速启动。客户端的模型账号登录和本服务的 Token 认证彼此独立。

### 共同准备:确认服务并选择身份

```bash
curl -fsS http://127.0.0.1:8000/health/ready
```

应返回 `{"status":"ready"}`。若连接失败,在一个终端运行并保持服务不退出:

```bash
docker compose up -d --wait
uv run business-data-mcp
```

在另一个终端进入仓库根目录。CLI 使用下面的环境变量;桌面端使用下文的本地静态请求头,避免依赖终端环境:

```bash
export BUSINESS_DATA_MCP_TOKEN="$(uv run python -c 'import json; print(json.load(open(".local/client-tokens.json"))["support"])')"
```

这个命令不打印 Token。新终端需要重新执行,客户端必须在同一个终端启动。桌面端需要粘贴 Token 时,可在 macOS 执行:

```bash
uv run python -c 'import json; print(json.load(open(".local/client-tokens.json"))["support"], end="")' | pbcopy
```

下文的 `PASTE_SUPPORT_TOKEN_HERE` 均需替换为剪贴板中的 Token,并保留 `Bearer` 后的一个空格。真实凭据仅放在本机私有配置,不放入 README、示例文件或 Git。

### 1. Codex CLI

先确认 `codex --version` 能运行。如果提示 `zsh: command not found: codex`,且 macOS 应用包含下面的可执行文件,可执行:

```bash
# 先确认文件存在;应用安装位置可能不同。
test -x /Applications/ChatGPT.app/Contents/Resources/codex
export PATH="/Applications/ChatGPT.app/Contents/Resources:$PATH"
codex --version
```

希望新终端也可用,可将该 `export PATH=...` 行加入 `~/.zshrc`。若本机没有这个文件,按 [Codex CLI 官方说明](https://developers.openai.com/codex/cli)安装 CLI。

加载上面的 Token 环境变量后注册服务:

```bash
codex mcp add business-data \
  --url http://127.0.0.1:8000/mcp \
  --bearer-token-env-var BUSINESS_DATA_MCP_TOKEN
codex mcp get business-data
codex
```

注册只需一次。`get` 应显示 `enabled: true`、`transport: streamable_http`、正确 URL 和环境变量名称;它只验证已保存的配置。进入 Codex 后输入 `/mcp` 检查连接,再发送下文的验收提示。

### 2. Codex 桌面端

桌面端与 CLI 在同一 Codex host 上共享 `~/.codex/config.toml`,但从 Dock 打开的应用不会自动继承终端里的 `export`。本地演示采用静态请求头:

1. 用上面的 `pbcopy` 命令复制 Token。
2. 执行 `open -e ~/.codex/config.toml` 打开配置;若文件不存在,先通过桌面 **Settings → MCP servers → Add server** 添加名为 `business-data` 的 Streamable HTTP 服务。
3. 找到已有的 `business-data` 段,只替换该段,保留其他服务;不要创建重复段:

```toml
[mcp_servers.business-data]
url = "http://127.0.0.1:8000/mcp"
enabled = true
http_headers = { Authorization = "Bearer PASTE_SUPPORT_TOKEN_HERE" }
```

删除这一服务原来的 `bearer_token_env_var` 行;如有 `env_http_headers` 或 header helper 提供 Authorization,也移除冲突项。这样 CLI 也会使用相同的静态 Token,不再需要为这个服务执行 `export`。

4. 保存后在 **Settings → MCP servers** 确认已启用,点击 **Restart**;没有此按钮时完全退出应用后重开。
5. 打开新的**本地**对话,输入 `/mcp` 查看服务并发送验收提示。

参见 [Codex MCP 官方文档](https://developers.openai.com/codex/mcp)。本服务使用开发 Bearer Token,不需要点击 OAuth 登录。

### 3. Claude Code CLI

确认 `claude --version` 可用,并完成客户端登录。使用共同准备步骤中的 Token,在同一个终端执行:

```bash
claude --mcp-config examples/claude.mcp.json --strict-mcp-config
```

示例配置的内容如下,CLI 会展开环境变量:

```json
{
  "mcpServers": {
    "business-data": {
      "type": "http",
      "url": "http://127.0.0.1:8000/mcp",
      "headers": {
        "Authorization": "Bearer ${BUSINESS_DATA_MCP_TOKEN}"
      }
    }
  }
}
```

`--strict-mcp-config` 让本次演示只加载指定文件里的 MCP 服务。进入后输入 `/mcp` 查看状态,按客户端提示批准工具使用,再发送验收提示。参见 [Claude Code MCP 官方文档](https://code.claude.com/docs/en/mcp)。

### 4. Claude Code 桌面端(Claude 应用的 Code 标签页)

选择 **Code → 本地会话**,打开本仓库。Code 标签页可读取项目 `.mcp.json`。为了从 Dock 启动也能读取 Token,在仓库根目录的 `.mcp.json` 中设置静态请求头:

```json
{
  "mcpServers": {
    "business-data": {
      "type": "http",
      "url": "http://127.0.0.1:8000/mcp",
      "headers": {
        "Authorization": "Bearer PASTE_SUPPORT_TOKEN_HERE"
      }
    }
  }
}
```

如果已有 `.mcp.json`,只合并 `mcpServers.business-data`,不要覆盖其他服务。本项目已将 `.mcp.json` 加入 `.gitignore`;可运行 `chmod 600 .mcp.json` 限制本机读取权限。不要把这个含真实 Token 的配置复制回 `examples/claude.mcp.json`。

保存后重开本地 Code 会话,按提示信任项目并启用 MCP,发送验收提示。选择原始项目目录;新 worktree 不会自动包含这个被 Git 忽略的配置文件。CLI 的 `--mcp-config` 参数不会自动传给桌面应用。参见 [Claude Code Desktop 官方说明](https://code.claude.com/docs/en/desktop)。

### 可选:Claude Desktop 的普通 Chat 标签页

Chat 与 Code 的接入入口不同。不要直接把 `http://127.0.0.1:8000/mcp` 填进账户级 **Add custom connector**:该功能从 Anthropic 云端连接,无法访问你的本机回环地址。见 [远程连接器网络要求](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp)。

普通 Chat 可通过本地 stdio → HTTP 桥接器接入当前服务。此方式额外依赖 Node.js / npx 和第三方 [mcp-remote](https://github.com/punkpeye/mcp-remote),不属于本项目 Python 运行依赖:

1. 确认 `node --version` 和 `npx --version` 可用。
2. 在 Claude 的 **Settings → Developer → Edit Config** 打开本地配置。macOS 常见位置为 `~/Library/Application Support/Claude/claude_desktop_config.json`。
3. 将以下服务合并到 `mcpServers`,替换 Token:

```json
{
  "mcpServers": {
    "business-data": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote", "http://127.0.0.1:8000/mcp",
        "--transport", "http-only", "--header", "Authorization:${AUTH_HEADER}"
      ],
      "env": {
        "AUTH_HEADER": "Bearer PASTE_SUPPORT_TOKEN_HERE"
      }
    }
  }
}
```

4. 完全退出并重开 Claude,在新 Chat 对话中启用工具并发送验收提示。首次启动需下载桥接器;桌面进程必须能找到 Node/npx。如果使用 nvm,配置 `command` 为 `command -v npx` 得到的绝对路径,并在 `env.PATH` 中配置 `command -v node` 对应目录及系统路径;更新 Node 后检查路径。

以上为配置说明,尚未实际验收 Claude 桌面 Code / Chat 或该桥接器。已验证的客户端和证据见 [验收记录](docs/verification.md)。

### 验收提示、身份切换与排错

在任一客户端发送:

> 请实际调用 business-data 的 get_customer 工具查询 C10001,展示客户资料、CRM 跟进、来源 URI 和 query_id。不要通过终端或本地文件读取数据。

`support` 的预期结果:星河科技、手机号 `138****8000`、跟进“已发送产品方案,等待技术评估”,来源包含 `business://tenant-a/customers/C10001` 与 `crm://tenant-a/customers/C10001`,以及新生成的 `qry_...`。用该 ID 在 `.local/audit.jsonl` 中查找 `query_started` / `query_finished`,确认发生了真实调用。

- **切换身份**:CLI 环境变量方案将读取命令中的 `support` 改为 `pii`、`tenant-b` 或 `restricted`,退出并重启客户端;静态配置方案替换配置中的 Token 后重新加载。不要重新初始化服务端凭据。
- **权限验证**:`restricted` 查询 `ORD-10001` 应返回 `FORBIDDEN`;`pii` 查询 `C10001` 显示完整手机号;`tenant-b` 查询同一订单返回 `cancelled`、`9900.00`。
- **401 / 环境变量缺失**:检查 Token、过期时间和认证配置来源;桌面端避免依赖终端临时环境。
- **连接拒绝**:检查健康接口和服务进程,确认客户端 URL 与 `PORT` 一致。
- **找不到工具**:重新加载 MCP 或打开新会话;确认服务启用、项目目录正确、没有同名服务覆盖配置。
- **远程会话**:云端或 SSH 会话的 `127.0.0.1` 指向执行端,不是本机;本文步骤仅针对本机会话。

## 自动化验证

```bash
make check                 # Ruff + 格式检查 + mypy
make test                  # 单元测试;数据库测试明确跳过
make test-integration      # 全部测试;需要已启动的演示数据库
```

GitHub Actions 使用独立 PostgreSQL service、同一初始化脚本和锁定依赖运行全套检查。

## 配置与运行维护

默认配置见 `.env.example`。`.env` 不提交 Git;进程环境变量优先于 `.env`。

| 配置 | 默认值 / 含义 |
|---|---|
| `DATABASE_URL` | 必填,只读账号连接串 |
| `TOKEN_FILE` / `AUDIT_FILE` | `.local/tokens.json` / `.local/audit.jsonl` |
| `DATA_DIR` | `data`,必须从可信本地目录加载 |
| `HOST` / `PORT` | `127.0.0.1` / `8000` |
| `RATE_LIMIT` | 每身份每分钟 60 次工具调用 |
| `QUERY_TIMEOUT` | 工具 10 秒,数据库语句另限 5 秒 |
| `MAX_RESULT_BYTES` | MCP 工具结果最多 65536 字节,含文本和结构化内容 |

结果超限时去掉完整记录并标记 `truncated`;单条仍过大返回 `RESULT_TOO_LARGE`。请求体最多 16 KiB。`/health/live` 表示进程存活,`/health/ready` 检查数据库连接。

停止服务用 Ctrl+C,停止数据库用 `docker compose stop`。普通重启保留数据。

**重置数据库会删除本项目演示数据**,先停止 MCP 服务再执行:

```bash
bash scripts/reset-demo.sh --confirm-delete-demo-data
```

凭据不会随数据库重置。需要重新生成凭据时,先停止服务,手动备份并移走 `.env`、`.local/tokens.json`、`.local/client-tokens.json`,再运行初始化并重置数据库;新密码不会自动应用到旧 volume。

### 常见问题

- Docker 报 `docker-credential-desktop` 找不到:在 macOS 终端执行 `export PATH="/Applications/Docker.app/Contents/Resources/bin:$PATH"`,再启动 Compose。
- 5433 冲突:首次初始化使用 `POSTGRES_PORT=55433 uv run business-data-init`。已有环境需同步修改 `.env` 中端口和 `DATABASE_URL`。
- 8000 冲突:使用 `PORT=8001 uv run business-data-mcp`,并同步修改客户端地址。
- 数据库连接失败:检查 `docker compose ps`;确保应用密码与首次初始化的 volume 一致。
- 401:检查客户端是否继承 Token 环境变量、Token 是否过期;Token 变更后重启服务和客户端。
- `FORBIDDEN`:检查身份 scopes;`restricted` 的订单拒绝属于预期演示。
- 远程客户端连接不到:`127.0.0.1` 只支持同机访问;首版未配置远程 HTTPS 部署。

## 后续方向

真实 OAuth → PostgreSQL RLS → 真实 CRM → 集中审计与分布式限流 → 应用容器化和部署。

许可证:[Apache-2.0](LICENSE)。