内容
# FreshBooks 时间表 MCP
一个 [MCP](https://modelcontextprotocol.io) 服务器,允许 AI 代理 **检查时间表状态** 和 **记录时间** 在 FreshBooks 中,具有安全的 OAuth2 刷新令牌处理。
询问你的代理 *"在上周一至周四每天记录 8 小时到 Acme 项目,星期五休假"* — 它会发现你的项目,确认哪一个,然后写入记录。或者 *"这个月我缺少哪些天?"* — 它会报告缺失的天数,从不标记尚未发生的天数。
> **👉 设置它?请参阅 [GETTING_STARTED.md](GETTING_STARTED.md)** — 一个约 10 分钟的指南。推荐的路径是单击 **Claude Desktop 扩展**(`.mcpb`);Docker 和本地 Python 作为替代方案。捆绑包的构建/开发详细信息位于 [extension/](extension/README.md)。本 README 是应用程序本身的参考:工具、配置和内部实现。
---
## 功能
- **`check_timesheet`** — 报告已记录 / 缺失 / 记录不足的天数,适用于某一天、一个星期或一个月(周一至周五)。未来工作日永远不会被标记为缺失。
- **`log_time`** — 在一个项目中记录每个工作日的 X 小时,排除 PTO/休息日,具有预演和自动跳过已记录天数的功能。
- **`list_time_entries` / `update_time_entry`** — 列出具有其 ID 的记录,然后编辑一个(例如,将其重新分配给不同的项目,或修复其备注/小时数)。
- **`list_projects` / `list_clients` / `list_services`** — 发现功能,以便代理可以询问要记录的项目,然后再写入任何内容。
- **安全身份验证** — OAuth2,具有自动访问令牌刷新和正确的旋转刷新令牌处理。令牌默认存储在操作系统的密钥链中。
- **时区正确** — 所有日期边界都在您配置的时区中计算,而不是 UTC。
---
## 架构
| 模块 | 责任 |
|---|---|
| `server.py` | MCP 接口 — 工具定义 + 可测试的 `handle_*` 函数 |
| `freshbooks_client.py` | HTTP 包装器:身份验证注入、401→刷新→重试、分页、`/me` 自动发现 |
| `auth_manager.py` | OAuth2 生命周期:刷新、**旋转**、引导 CLI |
| `token_store.py` | 可插拔的安全存储:操作系统密钥链或加密文件 |
| `transformers.py` | 纯日期数学、单位转换、报告构建 |
| `config.py` / `models.py` | 配置加载 + 类型化数据结构 |
工具逻辑存在于普通的 `handle_*` 函数中(注入客户端),因此可以在不使用 MCP 运行时环境的情况下进行完整的单元测试。
---
## 要求
- **Docker Desktop** (推荐路径),或 **Python 3.11+** 用于本地安装
- 具有私有 OAuth2 应用程序(客户端 ID + 秘密)的 FreshBooks 帐户
要完成设置 — 创建 FreshBooks 应用程序、秘密、构建、首次身份验证和与您的代理注册 — 请遵循 **[GETTING_STARTED.md](GETTING_STARTED.md)**。
本地安装的简要版本:
```bash
python -m venv .venv && source .venv/bin/activate
pip install ".[dev]" # 安装软件包、控制台脚本和开发工具
cp .env.example .env # 然后填写您的凭证(参见配置)
```
> 安装常规的 **,不是** `-e`。(可编辑安装在此处不可靠 — Python 的 `site` 模块会跳过构建后端 `.pth` 的最后一行。)更改源代码后,请重新运行 `pip install .` 以刷新控制台脚本。测试始终针对源代码树运行,因此不需要重新安装 `pytest`。
---
## 配置
在 `.env` 中设置(自动加载)。从 **my.freshbooks.com → Developer → your app** 获取客户端凭证。
| 变量 | 必需 | 备注 |
|---|---|---|
| `FRESHBOOKS_CLIENT_ID` | ✅ | OAuth 应用程序客户端 ID |
| `FRESHBOOKS_CLIENT_SECRET` | ✅ | OAuth 应用程序秘密 |
| `FRESHBOOKS_REDIRECT_URI` | ✅ | 必须 **完全** 匹配应用程序的重定向 URI(例如 `https://localhost/callback`) |
| `FRESHBOOKS_BUSINESS_ID` | — | 如果为空,则通过 `/me` 自动发现 |
| `FRESHBOOKS_IDENTITY_ID` | — | 如果为空,则通过 `/me` 自动发现 |
| `FRESHBOOKS_TOKEN_BACKEND` | — | `keyring`(默认)或 `file` |
| `FRESHBOOKS_TOKEN_PATH` | — | 加密令牌文件路径(仅限文件后端) |
| `FRESHBOOKS_TOKEN_KEY` | — | 文件后端的 Fernet 密钥(参见下文) |
| `TZ` | — | 日期/周/月边界时区(默认 `UTC`) |
| `DEFAULT_DAILY_HOURS` | — | 预期每天小时数(默认 `8`) |
| `DEFAULT_START_TIME` | — | 已记录条目的本地起始时间(默认 `09:00`) |
| `MAX_LOG_DAYS` | — | `log_time` 调用时的安全上限(默认 `31`) |
对于加密文件令牌后端,请生成一个密钥:
```bash
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
```
---
## 工具
### `check_timesheet(period, date?, expected_hours?)`
报告某一天、一个星期或一个月(周一至周五)的已记录/缺失天数。`date` 是锚点(默认为今天)。返回每天的状态(`logged` / `under` / `missing` / `future`)、`missing_days`、`under_logged_days`、总数和文本摘要。
### `log_time(period, hours, project_id, …)`
在项目中记录每个工作日的 `hours` 小时,适用于指定期间。
| 参数 | 默认值 | 备注 |
|---|---|---|
| `project_id` | — | **必需**。没有静默默认值 — 代理必须首先确认。 |
| `date` | 今天 | 期间的锚点日期 |
| `off_days` | `[]` | 要跳过的 `YYYY-MM-DD` 列表(PTO/假期) |
| `note` | `"Logged via MCP"` | 条目备注 |
| `billable` | `false` | 如果为真,则 **`client_id` 是必需的** |
| `client_id` / `service_id` | — | 对于可计费条目 / 计费率 |
| `skip_existing` | `true` | 跳过已记录天数的条目 |
| `dry_run` | `false` | 预览计划而不写入 |
周末始终被排除在外。`hours` 必须为 `0 < hours ≤ 24`。单个调用不得超过 `MAX_LOG_DAYS`。
### `list_time_entries(period, date?)`
列出某一天/一个星期/一个月的单个条目(带有 `id`、`date`、`hours`、`project_id`、`note`、`billable`)。使用它来查找要编辑的条目的 `id`。
### `update_time_entry(entry_id, project_id?, note?, hours?)`
编辑现有条目 — 例如 **将其移至不同项目**。通过 `list_time_entries` 查找 `entry_id`。提供至少一个 `project_id`、`note` 或 `hours`;仅更改传递的字段。`hours`(如果给出)必须为 `0 < hours ≤ 24`。
### `list_projects(active_only?, query?)` · `list_clients()` · `list_services()`
发现工具。代理调用 `list_projects` 并询问要使用的项目,当没有指定项目时,然后将选定的 `project_id` 传递给 `log_time`。
### 示例输出

---
## 工作原理(API 注释)
在 FreshBooks API 上实时验证:
- **两个主机**。浏览器授权位于 `https://auth.freshbooks.com/oauth/authorize`;令牌交换和所有数据调用位于 `https://api.freshbooks.com`。
- **旋转刷新令牌**。每个刷新都返回一个新的刷新令牌并使旧令牌失效。新令牌在 **使用前** 被持久化,在锁定下,因此并发刷新不会破坏链条。
- **时间条目使用 `businessId`** (不是 accountId)在路径中。`duration` 以 **秒** 为单位;`started_at` 为 UTC,带有毫秒数 + `Z`。
- **`identity_id` 陷阱**。列表端点已经限定为经过身份验证的用户。发送 `identity_id` 而不带 `team=true` 返回 422(`"team must be true in order to use identity_id filter"`),因此客户端省略了它用于自身情况,仅在 `identity_id`+`team=true` 发送给团队视图管理员查看队友的情况下发送。
- **可计费需要客户端**。`billable=true` 需要 `client_id` 在按客户端计费的帐户上。
- **日桶** 用于报告发生在您配置的 `TZ` 中,因此晚间条目落在正确的本地日期。
---
## 安全
- **令牌存储** — 默认情况下使用操作系统的密钥链;加密文件备用方案使用 Fernet 和通过环境变量提供的密钥(从不与密文放在一起),`0600` 权限和原子写入。
- **无秘密泄漏** — `TokenSet` 在 `repr` 中掩盖其值;令牌请求有效载体和响应永远不会被记录。
- **服务器端验证** — 调用代理是不可信的。小时被限制,日期有效,日期计数上限,并且 `identity_id` 被锁定到经过身份验证的用户(您不能以其他人身份记录时间)。
- **安全写入** — `log_time` 需要明确的 `project_id`,默认为 `skip_existing=true`,支持 `dry_run`,并且永远不会记录周末。
- **妥协恢复** — 在 FreshBooks UI 中撤销应用程序授权,然后重新运行 `freshbooks-mcp-auth`。
---
## 开发
```bash
pytest # 48 个测试;针对源代码树运行
# 或在 Docker 中运行测试套件:
docker build --target test -t freshbooks-mcp:test . && docker run --rm freshbooks-mcp:test
```
```
freshbooks_mcp/ 包(配置、模型、令牌存储、身份验证管理器、
FreshBooks 客户端、转换器、服务器)
scripts/smoke.py 只读实时验证
tests/ 每个模块的单元测试
```
---
## 许可证
MIT © 2026 Cristino Santiago。参见 [LICENSE](LICENSE)。