# FluxDesk AI 助手 / Agent 接入指引(MCP 技术协议)

| | |
|---|---|
| 版本 | `v1.20` |
| 最后更新 | 2026-06-03 |
| 适用 FluxDesk 版本 | `v0.25.154+`(一键添加 Phase 1 起) |
| 端点 | `POST /mcp`(Streamable HTTP transport,JSON-RPC 2.0) |
| 鉴权 | `Authorization: Bearer sk-flux-...`(用户自助 token) |
| 相关 | [对接「外部业务系统 → FluxDesk」](./INBOUND_INTEGRATION_SPEC.md) · [出站 webhook](./INTEGRATIONS.md) |

---

## 1. 这是什么

FluxDesk 可以让你把自己的 AI 助手 / Agent 接进来,让 Claude Code / Claude Desktop / Cursor / Continue / 任何 MCP-compatible 的客户端以"工具调用"的形式读写 FluxDesk 数据 —— 看 inbox、查任务、提交今日同步、关闭需求、给反馈加评论等等。

对普通用户来说,这叫 **AI 助手 / Agent 接入**。对开发者来说,底层协议是 **MCP(Model Context Protocol)** over Streamable HTTP。后面的配置示例和 tool 表会保留 MCP 术语,因为客户端配置文件、JSON-RPC 方法和工具名都按这个协议命名。

文案边界:产品页面、公告和成员操作文档优先使用「AI 助手 / Agent 接入」;只有在讲客户端配置、tool name、JSON-RPC、scope 或协议细节时才使用「MCP」。

安全边界:

- Token 跟单个用户 1:1 绑定。AI 助手只能看到 / 操作这个用户自己能看到 / 操作的数据。
- 项目隔离在服务端执行。别的项目即使存在,AI 助手也读不到。
- 写操作按你的身份进入审计日志。不要把 token 给同事共用。
- 建议先从读取场景开始,再给写权限;写入前让 AI 助手先起草 / 汇总,由人确认后再执行。

跟 outbound webhook 的区别:

| | MCP (pull) | Webhook (push) |
|---|---|---|
| **谁发起** | Agent 主动调用 | FluxDesk 主动推 |
| **何时触发** | Agent 决定 | 事件发生时 |
| **适合** | "帮我看看 inbox 有什么"、"创建一个 task" 这种 agent-driven workflow | 接 Slack bridge / Telegram bot / 自建 receiver,实时通知 |
| **典型场景** | 跟 Claude 对话时让它代你操作 FluxDesk | 让外部系统知道你这边的状态变化 |

两者**可以同时用**。

---

## 2. 拿到 token

1. 登录 FluxDesk → 左侧栏底部「AI 助手 / Agent 接入」或直接访问 `/me/agent`
2. 先走页面里的 Guided setup。它是 deterministic state machine,不是 LLM 对话;会按你选择的 AI 工具给出固定、可复核的接入步骤。
3. 选择 Claude Desktop / Cursor / Cline / Aider 等 quickstart card。支持 URL handler 的客户端会显示「一键添加」:FluxDesk 只打开 `fluxdesk://connect?session_id=...&tool=...&base_url=...`,URL 里没有原始 token。目标客户端必须先弹出确认,再调用 `/api/internal-os/agent/connect-callback` 用短期 `session_id` 换取真正 token 并写入本地配置。
4. Claude Code / Devin / Custom 等暂未稳定支持 URL handler 的客户端继续使用 copy config / download config 兜底。没有客户端响应一键链接时,也回到复制 / 下载配置,不阻塞接入。
5. 在「AI 助手 Token」里给 token 填一个容易识别的备注名,例如 `Claude Code main` 或 `Cursor backup`
6. 默认不展开权限选择器时会生成全权限 token;更安全的做法是展开「权限范围」,先选 `read:*` 或具体读取 scope,需要写入时再生成单独的写 token。全权限 token 只适合短期调试,不要长期放在多个客户端里复用。
7. 点击「+ 生成 Agent Token」
8. 复制弹出的 `sk-flux-...` token(**只显示一次**,丢了只能撤销后重新生成),或复制 / 下载页面生成的客户端配置。
9. Token 跟你 1:1 绑定。Agent 用这个 token 调 MCP 时,所有操作都在你的身份下执行,看到的数据范围 = 你自己能看到的(Charter Art 5 项目隔离自动生效)。

> 离职时 token 自动吊销(走标准 offboarding 流程)。token 12 个月未使用也会自动失效。

`/me/agent` 也会显示现有 token activity 摘要,用于确认 token 是否被使用、排查异常调用、撤销泄漏或不再使用的 token。一键添加生成的 token 会显示「通过一键添加」标记,撤销动作显示为「撤销接入」。当前 token 仍是用户绑定 token;per-agent identity schema / dashboard、AI 帮用户 mint / 改 scope / rotate token 的 AI-for-AI 流程都还是 RFC,必须等用户 UI 确认模型和安全审查完成后才能实现。

## 2.1 安全快速开始

第一次接入建议按这个顺序:

1. **先读不写**:生成一个读取型 token,让 AI 助手只做 inbox 汇总、任务查询、文档检索。
2. **先让 AI 助手解释它要做什么**:例如"先列出你会调用哪些 FluxDesk 工具,不要写入"。
3. **写操作先草稿后确认**:让 AI 助手先起草评论 / 需求描述 / 状态备注,确认后再调用写工具。
4. **定期看审计和撤销**:`/me/agent` 可以看自己的 token;发现泄漏或不再使用,立即撤销并生成新 token。

可以直接复制的第一轮提示:

```text
你已经接入 FluxDesk。先不要写入。
请调用可用工具查看我的未读 inbox 和我名下进行中的需求,
按"需要我今天处理 / 只是信息 / 建议归档"三类汇总。
如果你认为需要写操作,先告诉我你打算调用哪个工具和参数,等我确认。
```

更窄的开发者调试提示:

```text
先调用 whoami / ping 验证 FluxDesk token,再 tools/list 列出可用工具。
不要调用任何 write:* 或 admin:* 工具。
```

---

## 3. 客户端配置(MCP)

这一节是开发者 / 高级用户配置。FluxDesk UI 里叫「AI 助手 / Agent 接入」,但 Claude、Cursor 等客户端的配置项通常仍叫 `mcpServers` 或 MCP server。

### 3.1 Claude Code(终端 / VS Code)

在 `~/.config/claude/claude_code_config.json` 或项目根的 `.claude/settings.json` 里加:

```json
{
  "mcpServers": {
    "fluxdesk": {
      "type": "http",
      "url": "https://my.fluxdesk.net/mcp",
      "headers": {
        "Authorization": "Bearer sk-flux-XXXXXXXXXXXXXXXX"
      }
    }
  }
}
```

重启 Claude Code 后,`/mcp` 命令会列出 fluxdesk tools 可用。然后你可以直接说"看看我的 inbox 有什么新通知"、"把今天的状态改成专注处理中并选上 REQ-1234",Claude 会主动去调对应的工具。

### 3.2 Claude Desktop(Mac / Win)

`~/Library/Application Support/Claude/claude_desktop_config.json`(Mac)
`%APPDATA%\Claude\claude_desktop_config.json`(Win)

```json
{
  "mcpServers": {
    "fluxdesk": {
      "type": "http",
      "url": "https://my.fluxdesk.net/mcp",
      "headers": {
        "Authorization": "Bearer sk-flux-XXXXXXXXXXXXXXXX"
      }
    }
  }
}
```

完整重启 Claude Desktop(系统托盘退出后重开)生效。下方会出现 🔌 图标,展开能看到 fluxdesk 几个 tool。

### 3.3 Cursor

设置 → 「Features」 → 「MCP」 → 「+ Add server」:

| 字段 | 值 |
|---|---|
| Name | `fluxdesk` |
| Type | `HTTP` |
| URL | `https://my.fluxdesk.net/mcp` |
| Header | `Authorization: Bearer sk-flux-XXXXXXXXXXXXXXXX` |

### 3.4 自己写客户端 / curl 调试

任何能发 JSON-RPC 2.0 over Streamable HTTP 的客户端都行。最小可工作请求:

```bash
curl -X POST https://my.fluxdesk.net/mcp \
  -H "Authorization: Bearer sk-flux-XXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

返回当前 token 能调用的全部 tools 列表。

### 3.5 批量请求 (JSON-RPC 2.0 batch)

FluxDesk 完整支持 JSON-RPC 2.0 batch:在一个 HTTP POST 里发数组形式的多个请求,server 端逐个执行,返回数组形式的多个响应。底层走 `WebStandardStreamableHTTPServerTransport` (官方 MCP SDK 的实现),自动识别 `Array.isArray(body)` 并 fan-out。

**用例**:agent 一次握手前的轮询 ─ `tools/list` + `whoami` + `ping` 三连合一,省两个 RTT。

```bash
curl -X POST https://my.fluxdesk.net/mcp \
  -H "Authorization: Bearer sk-flux-XXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '[
    {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ping","arguments":{}}},
    {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"whoami","arguments":{}}}
  ]'
```

返回:

```json
[
  {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{...}"}]}},
  {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{...}"}]}}
]
```

注意:

- **建议批量大小 ≤ 20**:超过的话单个请求可能撞到 8s gateway timeout。SDK 不强制截断 ─ 客户端自己负责拆分。
- **顺序无关**:server 并发执行,响应数组的顺序跟请求顺序一致 (按 `id` 配对就行)。
- **错误是局部的**:某条 sub-request 失败只让那一条返回 error envelope,其它 sub-requests 照常返回 result。
- **`initialize` 例外**:batch 里不能塞多个 initialize,SDK 会 -32600。普通 tools/call 不受影响。

---

## 4. Tools 一览

> Tools 集合会随版本演进。运行 `tools/list` JSON-RPC 拿到当前真实清单。

### 读类(read-only)

| Tool | 输入 | 返回 | 用途 |
|---|---|---|---|
| `whoami` / `ping` | — | `{ userId, email, name, role, now }` | 验证 token 工作 |
| `list_inbox` | `unreadOnly?: boolean`, `type?: "task" \| "review" \| "meeting" \| "system"`, `limit?: number` | `{ totalCount, unreadCount, items[] }` | 看通知列表 |
| `get_notification` | `notificationId: string` | 完整一条通知 + actor / 目标 URL | 看单条详情 |
| `list_tasks` | `assignee?: "me" \| userId`, `project?: slug`, `status?`, `priority?`, `limit?` | `{ items[] }` | 查任务 |
| `get_task` | `code: "REQ-1234"` 或 `task_id: uuid`(v0.20.29 起 `code` 为 canonical 入参;`task_code` 仍接受为 deprecated alias) | 完整 task + assignees + status history | 看单个任务 |
| `list_projects` | — | `{ items[] }` | 自己能看到的项目列表 |
| `list_project_repositories` | `project_slug` | `{ project_slug, count, repositories: [{ id, provider, full_name, web_url, ssh_url, default_branch, is_monitored, archived }] }` | 列出项目绑定代码库。项目成员或 admin 可读；scope `read:projects` |
| `list_project_repository_commits` | `project_slug`, `repo_id?`, `branch?`, `limit?` | `{ project_slug, count, branch_filter, commits: [{ repo_full_name, provider, short_sha, title, url, actor, occurred_at }] }` | 读取项目已同步 commit 摘要。项目成员或 admin 可读；scope `read:projects` |
| `get_owner_intake_templates` | `owner_email?` 或 `owner_user_id?` | `{ owner, description, description_source, checklists, checklist_source }` | 给别人创建需求前读取系统固定起稿和负责人的需求模板；`description.label` / `description.body` 只是可改写起点，负责人维护的是 `checklists` 模板；负责人未维护模板时返回系统默认模板。scope `read:tasks` |
| `list_docs` | `project_slug?`, `space_slug?`, `status?`, `kind?`, `limit?` | `{ count, docs: [{ id, title, slug, space_slug, project_slug, kind, status, updated_at, url }] }` | 列出当前 token 可见的 FluxDocs 文档(含项目隔离 + 草稿可见性) |
| `get_doc` | `page_id` 或 `(space_slug + page_slug)` | `{ id, title, body, slug, space_slug, project_slug, kind, status, updated_at, published_at?, url }` | 读取单篇 FluxDocs 文档正文 |
| `search_docs` | `query`, `project_slug?`, `space_slug?`, `status?`, `kind?`, `limit?` | `{ query, count, hits: [{ id, title, slug, space_slug, project_slug, kind, status, snippet, matched_field, url }] }` | 搜索 FluxDocs 标题 / 正文 |
| `list_doc_drafts` | `project_slug?`, `space_slug?`, `limit?` | `{ count, drafts: [{ page_id, title, slug, space_slug, project_slug, kind, author_name, updated_at, url }] }` | 列出当前 token 按权限可见的文档草稿。作者看自己草稿；admin / assistant / lead 看其范围内草稿 |
| `search` | `query: string`, `types?: ("feedback"\|"task"\|"meeting"\|"notification"\|"comment")[]`, `filters?: {project_slug,author_email,since,status}`, `limit?: number` | `{ query, count, hits: [{ type, id, title, snippet, matched_field, url, score, createdAt }] }` | 跨表 full-text search (v1 = ILIKE);scope = `read:*` |
| `list_inbound_tokens` | `kind?: "personal" \| "moderation" \| "tools_claim"`, `include_revoked?: boolean`(默认 `false`) | `{ items: [{ id, label, kind, owner_email, created_at, revoked_at, linked_catalog_slug }] }` | Admin-only · scope `admin:catalog`。给 `broadcast_event.tools_token_id` 取 UUID 用,不暴露 token 原文/hash(Charter Art 7) |
| `list_approvals_for_user` | `email`, `status?: "approved"\|"revoked"\|"pending"\|"all"`(默认 `approved`) | `{ items: [{ approval_id, catalog_slug, catalog_name, status, created_at, decided_at, decided_by }] }` | Admin-only(v0.24.10,scope `admin:approvals`)。给 offboard CLI 枚举一个用户的真实接入清单用,替代 fd1 之前的硬编码 catalog 列表(新加 catalog row 后会漏)。`status='revoked'` 返回 terminal 行(withdrawn/rejected/cancelled)。Charter Art 7:admin 本来就能查同样数据,只是 aggregation 便利;不返任何 token 值 |

### 写类(state-changing)

| Tool | 输入 | 返回 | 守底 |
|---|---|---|---|
| `mark_notification_read` | `notificationId` | `{ updated: boolean }` | 只能改自己的 |
| `mark_all_notifications_read` | — | `{ updatedCount }` | 只清自己的 |
| `submit_daily_update` | `availabilityStatus`, `mainTaskIds[]`, `contactWindow?`, `blockers?`, `note?` | 提交结果 | 当天有则覆盖 |
| `create_task` | `title`, **`project_slug`(v0.20.29 起必填;`as_draft=true` 例外)**, `description?`, `parent_task_code?`, `assignee_email?`, `priority?`, `due_date?`, `kind?`, `as_draft?` | `{ task_id, code, url, assignee, priority }` | 走标准创建路径,带 audit。给别人建单时建议先调 `get_owner_intake_templates`;非 draft 缺 `project_slug` 返回 `{error: "project_slug is required"}`(422 等价) |
| `claim_task` | `code` 或 `task_id` | `{ task_id, code, status, assignee, url }` | 认领未分配的 `issue` task。调用者必须是项目成员 / admin / coordinator。成功后写 `task.claimed` audit 并通知 creator + PO/TL |
| `update_task_status` | `code: "REQ-1234"` 或 `task_id: uuid`(v0.20.29 起 `code` 为 canonical;`task_code` 仍接受为 deprecated alias),`status`, `progress_note?` | `{ task_id, code, status, new_status, comment_id }`(v0.20.29 起 `status` 为 canonical;`new_status` 仍输出为 deprecated duplicate,下个 release 移除) | 只允许 assignee / admin / reviewer(review→done/in_progress 时) |
| `add_task_comment` | `taskId`, `body` | `{ commentId }` | 评论,带 audit |
| `add_feedback_comment` | `feedbackId`, `body`(≤ 8000 字符,v0.19.5 起;原 4000),`client_message_id?` | `{ commentId }` | 同上;错误信封改用结构化 RFC 7807 形式,见下 § 8.1 |
| `create_feedback` | `title`, `body`(≤ 8000 字符,v0.19.9 起;原默默截断在 4999),`project_slug?`, `is_anonymous?`, `auto_delete_after?` | `{ feedback_id, url }` | project_slug 缺省 / "unsure" 进 反馈池 triage;超长 body 现在直接拒绝(结构化 RFC 7807 错误,见下 § 8.1)而非静默截断 |
| `bind_project_repository` | `project_slug`, `provider: "gitone" \| "gitea"`, `repo_url`, `ssh_url?`, `default_branch?` | `{ repo_id, project_slug, provider, web_url, full_name, default_branch, created }` | Admin-only。绑定自建代码库到项目；服务端解析 URL 并写 audit。scope `admin:projects` |
| `unbind_project_repository` | `project_slug`, `repo_url` | `{ repo_id, project_slug, archived: true }` | Admin-only。归档项目↔代码库绑定，保留历史动态链接。scope `admin:projects` |
| `create_doc_draft` | `project_slug?` 或 `space_slug?`(二选一至少一项), `title`, `slug?`, `body?`, `kind?`, `parent_page_id?`, `pinned?`, `change_summary?` | `{ page_id, project_slug, space_slug, slug, status: "draft", url }` | 创建 FluxDocs 草稿。`space_slug` 缺省时会自动复用 / 创建项目默认文档空间；可显式传 `slug` 避免中文标题落成泛化 URL |
| `update_doc_draft` | `page_id` 或 `(space_slug + page_slug)`, `title?`, `body?`, `kind?`, `parent_page_id?`, `pinned?`, `change_summary?` | `{ page_id, slug, space_slug, project_slug, status: "draft", url }` | 更新草稿内容。只允许作者改正文；published 文档不能走这条 |
| `publish_doc_draft` | `page_id` 或 `(space_slug + page_slug)` | `{ page_id, slug, space_slug, project_slug, status: "published", published_at, url }` | 人工确认后把草稿发布到文档中心。守同 web UI 发布权限(author/admin/assistant/lead) |
| `archive_doc` | `page_id` 或 `(space_slug + page_slug)` | `{ page_id, slug, space_slug, project_slug, status: "archived", archived_count, url }` | 下线文档；父文档归档时级联归档整棵子树 |
| `broadcast_event` | `recipients[]`(≤100), `event_type`, `severity`, `title`, `summary?`, `markdown_body?`, `actions?[]`, `event_id_prefix?`, `project_slug?`, `tools_token_id?`, `auto_claim?`(v0.20.0 起,默认 `true`) | `{ ok, delivered: [{ email, deliveryId, deduped, pushed, autoClaimed? } \| { email, error }] }` | Admin-only(非 admin 直接 `forbidden`)。v0.20.0 起:未 claim 的 recipient 默认会用 `tools_token_id` 内联 mint 一个 personal token 再投递(每个 +1 DB RTT),结果行带 `autoClaimed: true`;大批量 fan-out 建议先 `POST /api/v1/tokens/claim/batch`,再 `auto_claim: false`。Charter Art 2(no nag)+ Art 7(server-side claim) |
| `resolve_inbound_event` | `delivery_id`, `resolution_note?` | `{ ok, deliveryId, resolved, resolvedAt?, resolutionNote? }` | 只允许原发送人(token owner)或 admin |
| `supplement_inbound_event` | `delivery_id`, `markdown_body`(≤4000) | `{ ok, deliveryId, supplementId, createdAt }` | 只允许原发送人或 admin;Append-only,原 body 永不可变(Charter Art 9) |
| `issue_admin_tools_claim_token` | `label`, `source_note?`, `catalog_display_name?`, `catalog_description?` | `{ token_id, token_value, catalog_row_id, catalog_slug, daily_limit, endpoint_url }` | Admin-only(v0.22.2,scope `admin:tokens`)。包装 `createToolsClaimToken` 表单 action,mint `flux-tools-…` + 自动建对应 catalog row。`token_value` **只回一次**(Charter Art 7) |
| `issue_admin_moderation_token` | `label`, `source_note?`, `daily_limit?`(默认 200) | `{ token_id, token_value, catalog_row_id, catalog_slug }` | Admin-only(v0.22.2,scope `admin:tokens`)。包装 `createModerationInboundToken`,mint `flux-in-pers-…`(`is_moderation=true`)+ catalog row |
| `issue_personal_inbound_token` | `label`, `daily_limit?`(默认 50) | `{ token_id, token_value, endpoint_url }` | v0.22.2,scope `write:inbox`。**只给 caller 自己** mint(Charter Art 9 — 无 `target_user_id` 参数);包装 `createPersonalInboundToken` |
| `revoke_admin_token` | `token_id`(uuid), `reason?` | `{ revoked: true }` | Admin-only(v0.22.2,scope `admin:tokens`)。包装 `adminRevokeInboundToken`,可吊销任意 `personal_inbound_tokens` row;idempotent;owner 收到 push + audit |
| `revoke_personal_token` | `token_id`(uuid) | `{ revoked: true }` | v0.22.2,scope `write:inbox`。只能吊销**自己** owner 的 row;idempotent;包装 `revokePersonalInboundToken` |
| `invite_user` | `email`, `name`, `role?`(默认 `member`,枚举 `member`/`admin`/`hr_admin`/`super_admin`/`assistant`), `employment_type?`(默认 `full_time`), `location?`(默认 `overseas`), `ui_language?`(默认 `zh`), `status?`(默认 `active`) | `{ user_id, email, name, status, invite_sent: true }` | Admin-only(v0.24.11,scope `admin:users`)。包装 `createUserAdmin` 表单 action 抽出来的 `createUserCore`(`src/app/(internal-os)/users/actions.ts`):新建 user row + onboarding 清单 + 通知偏好 + 欢迎 inbox push。`domain_not_allowed` / `user_already_exists` 走 problem+json(见 § 8.4)。Charter Art 9:audit 记 caller 为 actor |
| `search_user_by_username` | `username`(email 局部,即 `@` 前那段;不带 `@`), `only_active?`(默认 `true`) | `{ items: [{ user_id, email, name, role, status, domain, created_at }] }` | Admin-only(v0.24.12,scope `admin:users`)。按 `email ILIKE '<username>@%'` 服务端查全部允许域名下匹配的 user 行,返回 0+ 条,每条带 `domain`(`fluxprimestudio.com` / `zohomail.com` …),caller 自己挑 canonical 邮箱。v0.24.x 放开 COMPANY_EMAIL_DOMAINS 后 fd1 CLI 不能再硬编码后缀,需要这条来 disambiguate。空结果 = `{items: []}`(不是 422,合法 outcome)。`username` 带 `@` → 422 `username_contains_at`(让 caller 改成只传 local-part)。Charter Art 7:admin 本来就有 user list 读权,回显 email/name/role OK;不返 password hash / token |

每个写操作都进 `audit_logs` 表(`source: "mcp"`)。token owner 在 /me/agent 看得到自己的调用历史摘要;admin 在 /settings/audit-logs 可审计全公司。

---

## 5. 典型用法

### 5.1 早上 standup

> 「Claude,帮我看看 inbox 有哪些昨晚我不在的时候发生的新东西」

Claude 调 `list_inbox({ unreadOnly: true })` → 给你摘要,如果有重要的会建议你点开看 / 标已读。

### 5.2 提交今日同步

> 「我今天专注处理 REQ-3421 的登录回归。设我状态为 focused」

Claude 调 `list_tasks({ assignee: "me" })` 找到 REQ-3421 的 id → 调 `submit_daily_update({ availabilityStatus: "focused", mainTaskIds: ["<id>"] })`。

### 5.3 关闭一个 task

> 「REQ-1234 我刚部署上线了,标记完成」

Claude 调 `update_task_status({ taskId: "...", status: "done", note: "已部署到 prod" })`。

### 5.4 批量看反馈

> 「最近 3 天反馈池里跟 'standup' 相关的都有谁提的」

Claude 调 list_inbox / list_tasks 反复查 + 给你聚合摘要。

### 5.5 给别人创建需求前读取接单偏好

> 「帮我给 Leslie 建一个官网 UI 改版需求，尽量按她接单习惯把背景问清楚」

Claude 先调 `get_owner_intake_templates({ owner_email: "leslie@..." })`，读取系统固定标题 / 描述起稿，以及 Leslie 维护的需求模板；如果 Leslie 没维护模板，返回 admin 设置的系统默认模板。然后 Claude 用 `description.label` / `description.body` 做起草骨架，把 `checklists` 中合适的模板融入真实描述，你确认后再调 `create_task({ assignee_email: "leslie@...", ... })`。

这条路径只是在创建前补齐背景，不会把清单变成强制字段，也不会绕过项目 / 任务权限。

---

## 6. 限频、安全、可见性

- **Rate limit**:60 次 / 分钟、5000 次 / 天(per token)。超过返回 `429`,带 `Retry-After`。
- **Auth scope**:token = 单个 user。Agent 只能看到 / 改这个 user 自己能访问的数据。**没有跨用户 token**。
- **可见性守底**(Charter Art 5):agent 调 `list_tasks` 也只能看到你参与的项目里的需求;别的项目即使存在也读不到。同样适用 list_inbox / list_projects。
- **写操作 audit**:所有写都进 `audit_logs`,`source` 字段标 `mcp`,`metadata` 里带 tool 名 + 输入摘要。一旦 token 泄漏,你或 admin 可以从 audit 找到所有可疑写操作。
- **Token 轮换**:发现泄漏 → /me/agent 「撤销」当前 token → 「+ 生成新 Token」→ 把新 token 配进客户端。被撤销的 token 立即 401(无 grace window,跟 webhook 那边的 24h overlap 不同 —— MCP 是 agent 主动调用,轮换造成的"agent 偶尔失败"是正常重试范畴)。

---

## 7. Token 权限范围 (Scopes)

> v0.17.71 起:每个 token 携带一个 `scopes: string[]` 字段;tool 调用前会校验 token scope 是否覆盖 tool 声明的 `requiredScopes`。

### 7.1 向后兼容(已有 token 不受影响)

老 token + 默认场景 = 全部权限。具体:

- 0.17.70 及以前签发的 token 在 0104 migration 后自动获得 `scopes = ['*']`(wildcard,等价旧行为)。
- `/me/agent` 「+ 生成新 Token」如果不展开 scope 选择器,默认勾 `*`,跟旧 UX 一致。
- 没有任何 in-place 升级 / 重新生成需求;直接 deploy 就完事。

### 7.2 Scope 词表

| Scope key | 覆盖的工具 |
|---|---|
| `*` | **全部**(默认,向后兼容)|
| `read:*` | 所有 `read:` 类工具 |
| `read:tasks` | `list_tasks`、`get_task`、`get_owner_intake_templates` |
| `read:feedback` | `list_feedback`、`get_feedback` |
| `read:inbox` | `list_inbox`、`get_notification` |
| `read:audit` | 读自己 / admin 读全公司 audit |
| `read:meta` | `list_projects`、`whoami`、组织元数据 |
| `read:projects` | `list_project_repositories`、`list_project_repository_commits` |
| `read:docs` | `list_docs` / `get_doc` / `search_docs` / `list_doc_drafts` |
| `write:tasks` | `create_task`、`claim_task`、`update_task_status`、`add_task_comment` |
| `write:feedback` | `add_feedback_comment` 等 |
| `write:docs` | 文档写父 scope。兼容覆盖 `write:docs:draft` + `write:docs:publish` |
| `write:docs:draft` | `create_doc_draft` / `update_doc_draft` |
| `write:docs:publish` | `publish_doc_draft` / `archive_doc` / generic live-doc write flow |
| `write:inbox` | `mark_notification_read`、`mark_all_notifications_read` |
| `write:inbound` | 入站事件投递 |
| `write:inbound:bulk` | 入站事件**批量**投递 |
| `admin:catalog` | catalog row / bundle CRUD(create / update / delete / list / get + 6 bundle 工具)+ `list_inbound_tokens` 发现(给 `broadcast_event` 拿 `tools_token_id` UUID,admin only) |
| `admin:approvals` | 审批类工具:`assign_catalog_to_employee` / `approve_team_support_request` / `revoke_catalog_approval` / `list_pending_team_support_requests` / `list_approvals_for_user`(v0.21.0 起从 `admin:catalog` 拆出;team-lead approver token 用这个就够,不需要给 catalog 写权限) |
| `admin:tokens` | 集成接入 Token 的 mint / revoke:`issue_admin_tools_claim_token` / `issue_admin_moderation_token` / `revoke_admin_token`(v0.22.2 起从 `admin:catalog` 拆出。整合 CLI 拿这一条就够 mint 自家 `flux-tools-…` 接入 token + 应急吊销,不需要 catalog CRUD 权限) |
| `admin:broadcast` | `broadcast_event` 系统广播(v0.21.0 起从 `write:inbound:bulk` 迁过来:广播本质上是 admin 操作,不是普通 bulk ingest) |
| `admin:on_behalf_of` | 代用户提交 /team-support 申请(admin only,**比 `admin:catalog` 更窄**:只能 file pending,不能 approve / revoke) |
| `admin:projects` | 项目管理类工具：`bind_project_repository` / `unbind_project_repository` |
| `admin:users` | 创建新成员:`invite_user`(v0.24.11 起新增。fd1 UX#15 #35 P1 — 之前没有 MCP 路径建账号,整合 CLI 必须打开浏览器走 /users 表单。Kept narrow — 不解锁 catalog CRUD 或 token mint;granting alone 就能在 CLI 一键拉新用户)|
| `admin:*` | 所有 admin 工具(含 catalog / approvals / tokens / broadcast / on_behalf_of / projects / users)|

**v0.21.0 back-compat 注意**: `admin:catalog` token **仍然**满足要求 `admin:approvals` 的工具(`hasScope` 内置 one-way superset),旧 token 不会 silently 401。反向不通(`admin:approvals` 不解锁 catalog 写),这是拆分的目的。

**v0.22.2 back-compat 注意**: 同样的 one-way superset 也覆盖 `admin:catalog` ↦ `admin:tokens`。已签发的 `admin:catalog` token 可以直接调 5 个 token mint/revoke 工具;新的 `admin:tokens` 窄 token 不能反向 unlock catalog CRUD。

**v0.24.11 back-compat 注意**: 同样的 one-way superset 也覆盖 `admin:catalog` ↦ `admin:users`。已签发的 `admin:catalog` token 可以直接调 `invite_user`,无需 re-mint;新的 `admin:users` 窄 token 不能反向 unlock catalog CRUD。

### 7.3 通配符规则

- `*` 匹配**所有** scope。
- `<module>:*`(例:`read:*`、`write:*`、`admin:*`)匹配该模块下所有 scope。注意 **module 之间不互通** —— `read:*` 不会授权 `write:tasks`,反之亦然。
- 否则按字面值精确匹配。
- Tool 没有声明 `requiredScopes` 的(例如 `ping` / `whoami`)在任意 token 下都可调用,这是为了让用户随时能验证 token 还在工作 + 检查身份。

### 7.4 怎么挑

| 场景 | 推荐 scope |
|---|---|
| 个人 Claude Code / Desktop 日常使用 | `*`(简单省事) |
| CI / 只读巡检脚本 | `read:*` |
| 跨系统投递入站事件的 receiver | `write:inbound`(批量场景再加 `write:inbound:bulk`) |
| 给一个 sandbox 自动化 agent | `read:*` + `write:tasks`(明确不给 admin) |
| Admin 自己的 token | `*` 或 `admin:*` + `read:*` |

### 7.5 缩窄 scope 后 token 还能用吗?

能。任何用 `*` 签发的 token 跟之前完全一致。如果你**主动**生成了一个 `read:*` 的 token,然后让 agent 去 `create_task`,server 会回 `403 FORBIDDEN: Token missing required scope`。这时:撤销该 token → 重新生成一个带 `write:tasks` 的就行。

### 7.6 客户端怎么知道自己有哪些 scope?

- `whoami` 返回里包含 `scopes` 字段。
- `tools/list` 返回的每个 tool metadata 里带 `requiredScopes`,客户端可以预测哪些工具会被拒。

---

## 8. 错误码

| HTTP / JSON-RPC code | 含义 | 应对 |
|---|---|---|
| `401 UNAUTHORIZED` | token 缺 / 错 / 已吊销 / 已过期 | 重新生成 |
| `403 FORBIDDEN` | tool 调用涉及别人的数据 / 你没权限的 project | 不重试,改输入 |
| `404 NOT_FOUND` | id 不存在 或 不在你的可见范围里 | 不重试 |
| `429 TOO_MANY_REQUESTS` | 限频 | 按 `Retry-After` 重试 |
| `400 INVALID_INPUT` | 输入 schema 错(例如 `priority` 不是 p0/p1/p2) | 不重试,修客户端 |
| `500 INTERNAL_ERROR` | FluxDesk 内部 bug | 重试一次;持续报告给 admin |

JSON-RPC 错误体:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "INVALID_INPUT: priority must be p0/p1/p2",
    "data": { "field": "priority" }
  }
}
```

### 8.1 Tool-result 错误信封(v0.19.5)

MCP 把 tool 的执行结果统一包在 `result.content[0].text` 里(JSON-RPC 层面不区分成功 / 失败)。**信封里**的 JSON 形态目前有两种,客户端两种都要会解:

**flat-string(默认,绝大多数写 tool):**

```json
{ "error": "feedback not found" }
```

**结构化 RFC 7807-shaped(v0.19.5 起,目前 `add_feedback_comment` + `create_feedback`(v0.19.9)):**

```json
{
  "error": {
    "type": "validation_failed",
    "title": "Body too long",
    "detail": "body must be ≤ 8000 chars",
    "field": "body"
  }
}
```

`type` ∈ `{ validation_failed | not_found | forbidden | conflict | internal }`。`field` 可选,指向出问题的输入字段。后续会逐 tool 迁移过去 —— 客户端先按 `typeof result.error === "string"` 做兜底分流即可。

### 8.2 `broadcast_event` 的 `error_envelope.next_action`(v0.24.8 起)

`broadcast_event` 每条 per-recipient 失败 + 顶层失败的 `error_envelope` 现在多带一个 `next_action` 字段 —— 一句话告诉 sender「下一步该做什么」,避免 caller 拿到 `code` 后还要回头查文档:

```json
{
  "delivered": [
    {
      "email": "alice@x.com",
      "error": "no_approved_request",
      "error_envelope": {
        "code": "no_approved_request",
        "title": "No approved team-support request",
        "detail": "recipient has no approved team-support request for the supplied tools_token_id; auto-claim requires opt-in via the /team-support catalog first",
        "field": "recipients",
        "next_action": "recipient must approve the linked catalog row (use `list_inbound_tokens` to see `linked_catalog_slug`) via /team-support → 「我的申请」 first; or call `assign_catalog_to_employee` to file + auto-approve on their behalf"
      }
    }
  ]
}
```

**code → next_action 映射**(完整列表;optional,某些 code 没有有意义的下一步就不带这个字段):

| code | next_action 大致方向 |
|---|---|
| `tools_token_not_owned` | mint your own admin tools-claim token via `issue_admin_tools_claim_token` MCP tool, then pass its UUID |
| `no_approved_request` | recipient approves the linked catalog via `/team-support`,或 admin 用 `assign_catalog_to_employee` 代办 |
| `no_token` | pass `tools_token_id` + 默认 `auto_claim:true` 内联 mint |
| `user_not_found` / `user_inactive` | drop offending email / reactivate via `/users` |
| `recipient_hint_forbidden` / `_required` | 删除 / 添加 `recipient` 字段 |
| `cross_project_blocked` / `project_not_found` / `project_archived` | 删除 `project_slug` 或换活跃项目 |
| `token_disabled` | recipient 自己 / admin 重新启用 token |
| `circuit_open` | 等 breaker 自动恢复或 admin 手动 reset |
| `rate_limit_global` | 等 daily 计数 00:00 UTC 重置或 admin 调高 `daily_limit` |
| `forbidden_admin_only` | 换 admin 调用,或者改用 `/api/v1/events` 单发 |
| `schema_invalid` | 修对应 `field` 后重试 |

**Charter Art 7**:`next_action` 永远只指动作路径(MCP tool 名、UI 路径、运营动作),**绝不**泄露 token 值、邮箱列表、其它用户身份等敏感信息。整合 CLI 把这个 hint 原样透传给操作员即可,无需脱敏。

### 8.3 admin write 工具 `user_not_found` 统一 problem+json(v1.9 起)

之前几个 admin write 工具(`assign_catalog_to_employee` / `create_team_support_request_on_behalf` / `revoke_catalog_approval` 等)在收到「邮箱查不到 user」时返回 flat-string `{"error":"user not found"}`,跟 v0.19.5 起其它 tool 的 problem+json 风格不一致。

v1.9 起,**所有** admin write tool(走 `findUserByEmail` 邮箱寻址那批)在 user-not-found 时统一返回:

```json
{
  "error": {
    "type": "https://docs.fluxdesk.net/api/v1/errors/user-not-found",
    "title": "User not found",
    "status": 422,
    "detail": "No user matched email 'alice@x.com'",
    "email": "alice@x.com"
  }
}
```

对应 REST endpoint(`POST` / `DELETE /api/v1/catalog/approvals`)直接返回 422 problem+json body(去掉外层 `{"error":...}` 包装,因为 REST 用 `Content-Type: application/problem+json` 直接吐结构化体):

```json
{
  "type": "https://docs.fluxdesk.net/api/v1/errors/user-not-found",
  "title": "User not found",
  "status": 422,
  "detail": "No user matched email 'alice@x.com'",
  "email": "alice@x.com"
}
```

源码常量在 `src/lib/api/problem-json.ts` → `ProblemTypes.UserNotFound`,builder 是 `userNotFoundProblem(email)`(纯数据形)+ `userNotFound(email, opts)`(直接返 `NextResponse`)。整合 CLI 应该 route on `type` URI(stable),不要 fuzzy-match `detail`。

覆盖范围:`assign_catalog_to_employee`、`create_team_support_request_on_behalf`、`revoke_catalog_approval`(仅 `catalog_slug + email` 寻址路径;`approval_id` 路径 user_not_found 含义是数据完整性问题、没有有意义的 caller-supplied email,保留 flat-string)。`POST /api/v1/catalog/approvals`(assign)+ `DELETE /api/v1/catalog/approvals`(revoke)同样切到 problem+json。

**Charter Art 7**:这些都是 admin 工具(token scope `admin:approvals` / `admin:catalog` / `admin:on_behalf_of`),admin 本来就有 user list 访问权,所以回显 email 是 OK 的;非 admin 场景(`inbound/personal` / `/api/v1/events` 的匿名探测路径)依然走 401 兜底、不会回显邮箱(见 `checkTargetMemberApproval` 的 gating 规则)。

### 8.4 `invite_user` 错误类型(v1.11 起)

`invite_user`(scope `admin:users`)有两个独立的 problem+json 错误类型,跟 § 8.3 同走 RFC 7807 信封:

**域不在白名单**(`COMPANY_EMAIL_DOMAINS` env 控制):

```json
{
  "error": {
    "type": "https://docs.fluxdesk.net/api/v1/errors/domain-not-allowed",
    "title": "Email domain not allowed",
    "status": 422,
    "detail": "Email 'alice@randomdomain.com' is not in the configured company-domain allowlist",
    "email": "alice@randomdomain.com",
    "domain": "randomdomain.com"
  }
}
```

**邮箱已存在用户**(idempotency miss):

```json
{
  "error": {
    "type": "https://docs.fluxdesk.net/api/v1/errors/user-already-exists",
    "title": "User already exists",
    "status": 422,
    "detail": "A user with email 'alice@company.com' already exists",
    "email": "alice@company.com",
    "user_id": "abc12345-..."
  }
}
```

CLI 应该 route on `type` URI(stable),不要 fuzzy-match `detail`。`user_already_exists` 体里附带的 `user_id` 让 CLI 可以选择把它当成 idempotency hit(直接用已有 row,不报错)还是硬失败。源码:`src/lib/api/problem-json.ts` → `ProblemTypes.DomainNotAllowed` / `ProblemTypes.UserAlreadyExists`,builder `domainNotAllowedProblem(email)` / `userAlreadyExistsProblem(email, userId)`。

**Charter Art 7 守底**:`invite_user` 是 admin-only(scope `admin:users` + `isAdminRole` 二次校验),回显 email + `user_id` OK;非 admin 路径不会触达这两个错误类型(scope 校验先于业务逻辑)。

---

## 9. 故障排查

**`401 token_not_found`** — token 拼错 / 已撤销 / 没复制全。重新生成,Authorization header 是 `Bearer <token>`(注意 Bearer 后有空格)。

**`Connection refused` / 超时** — 网络问题。先 `curl https://my.fluxdesk.net/api/internal-os/healthz` 看 FluxDesk 是否可达,再排查公司 DNS / VPN。

**Claude / Cursor 不显示 tools** — 多半是 config 文件路径错或者 JSON 语法错。看客户端的 MCP 日志(Claude Code: `~/Library/Logs/Claude/`,Mac;Cursor: 设置 → MCP → 「View Logs」)。

**调用返回 `403 cross_project_blocked`** — 你想读的 task / project 不在你参与的范围里。这是 Charter Art 5 守底,不是 bug。让对应项目的 admin 把你加进去就能读。

**写操作"成功"但页面没变化** — 大概率是 fetch / SW 缓存。F5 硬刷新一次。如果还不行,看 `/me/agent` 下的 audit 摘要,确认 server 确实收到调用。

---

## 10. 反模式

| ❌ | ✓ |
|---|---|
| 把 token 写进 git commit / 公开仓库 | 用客户端的 secret manager(macOS Keychain / Windows Credential Manager / 1Password) |
| 跟同事共用 token | 同事自己去 /me/agent 申请;token 1:1 强绑 |
| 用 cron 每 5s 调 list_inbox 轮询 | 用 webhook 接事件;MCP 留给 agent on-demand |
| Agent 自动每天提交固定的 standup | standup 是结构化协作信号,要人参与决定,不该机器代填 |
| 写脚本批量 create_task 灌历史数据 | 历史数据走 admin 后台 / migration,不走 user MCP |

---

## 11. 服务端实施参考(给后续维护)

| 实施点 | 文件 |
|---|---|
| Express handler | `src/mcp/server.ts` (`/mcp` endpoint) |
| Auth | `src/mcp/auth.ts`(读 `accessCredentials.aiTokenPrefix` + bcrypt) |
| Tools 注册 | `src/mcp/tools/index.ts` + `tools/*.ts`(inbox / tasks / projects / daily / writes) |
| Audit | `src/mcp/lib/audit.ts`(每个写 tool 写 `audit_logs` `source=mcp`) |
| Token CRUD | `src/app/(internal-os)/me/agent/actions.ts`(generate / revoke) |
| Token Manager UI | `src/components/internal-os/mcp-token-manager.tsx` |
| 这份文档 | 源 `docs/MCP_GUIDE.md`,烤进 `src/lib/internal-os/mcp-protocol-md.ts`(`scripts/build-mcp-protocol-md.cjs`) |
| 渲染页 | `/me/agent/protocol` |
| 纯 markdown endpoint | `/api/internal-os/agent/protocol`(无需 session,curl 即可) |

---

## 12. FAQ

**Q: MCP 跟 outbound webhook 必须二选一吗?**
A: 不用,完全可以并用。MCP 走 pull(agent 决定),webhook 走 push(事件发生即触发)。

**Q: token 能不能限制只读?**
A: 可以,见 § 7 Token 权限范围。生成 token 时展开「限制 Token 权限范围(高级)」勾 `read:*`,得到一个纯读 token。

**Q: 能用 stdio transport 不用 HTTP 吗?**
A: 不行。FluxDesk 是 multi-user server,只支持 HTTP transport(每个 request 自带 Bearer token 区分用户身份)。stdio 假设单一用户单一进程,不适用这种场景。

**Q: 我能不能让 agent 替我审批别人的请假?**
A: 技术上可以(如果你是 admin),但**强烈不建议**。审批是宪章 Article 9(作者完整性)所守底的"人做的动作"。让 agent 替你审批等于把用户权益放到不可追溯的位置。如果真要做,至少加一层"agent 提议 → 人 confirm"的中间步骤。

**Q: 离职后 token 自动失效吗?**
A: 是。standard offboarding flow 会 revoke 全部 access credentials,包括 agent token。配在客户端的会立即开始返回 401。

---

## 13. Catalog row slug 命名约定

> v0.19.5 起:`team_support_resources.slug` 是 catalog 行的稳定 ID,REST `/api/v1/catalog/items/{slug}` + MCP `catalog_item_*` 都拿它做主键。每个新 row 都应该按下面的约定起 slug,避免后续维护痛点。

### 13.1 推荐:`<integrator-slug>.<resource>`

新 row 优先用「整合方 slug + 资源名」二段式,中间用半角点分隔:

| 场景 | slug 示例 |
|---|---|
| fd1 的入站 webhook | `fd1.inbound` |
| fd1 的 bastion SSH 凭据 | `fd1.bastion-ssh` |
| Sentry 的告警接入 | `sentry.alerts-channel` |
| OpenAI 的 API key | `openai.api-key` |

好处:

- 一眼看出来这条 row 属于哪个整合方
- `integrator_slug` 过滤时能跟 `personal_inbound_tokens.integrator_id` 对得上
- 卸载整合方时,grep 一个前缀就能列出所有相关 catalog row

### 13.2 历史保留:`<category>-<resource>`

旧 row(v0.19.5 之前手工建的)用的是「类别 + 资源名」单段格式,例:

- `network-vpn-access`
- `saas-figma-account`
- `repo-monorepo-write`

这些**保持原样**(不强制改名),只在新建 row 时按 13.1 起名即可。两种格式同时存在不会冲突 ── `slug` 字段只要求 `^[a-z0-9][a-z0-9._-]{0,79}$`。

### 13.3 `category` 字段

`category` 是路由用的机器 slug,不是显示标签:

- ASCII only,小写,半角连字符分隔
- **新建 row 用机器 slug,UI 自动映射到带 emoji 的显示标签** —— 见下表
- 老 row 里有 emoji-prefixed category(`🌐 网络资源`)的是 v0.16 之前的 legacy 数据,后端会兼容显示(原样透传)但新 row 不应该这样起

**v0.19.9 — 7 个标准机器 slug + 显示标签:**

| 机器 slug | 中文显示 | English display |
|---|---|---|
| `llm-backend` | 🤖 公司付费 AI 订阅 | 🤖 Paid AI Subscriptions |
| `network` | 🔑 接入凭证 | 🔑 Access Credentials |
| `inbound-integration` | 📥 外部工具消息接入 | 📥 Inbound Integrations |
| `outbound-integration` | 📡 出站 Webhook | 📡 Outbound Webhooks |
| `mcp-agent` | 🧠 MCP / Agent | 🧠 MCP / Agent |
| `dev-environment` | 🖥 开发环境 | 🖥 Dev environment |
| `cross-border-comm` | 📞 跨境通讯 | 📞 Cross-border Comms |

实现:`src/lib/internal-os/catalog-categories.ts` 是唯一来源,`/team-support` UI 与 REST `/api/v1/catalog/items` 响应都从它读。

**REST 响应里的两个字段:**

- `category` — 原始机器 slug(整合方应当用这个做 routing / 过滤);如果是 legacy 行则原样保留 emoji 字符串
- `categoryDisplay` — server 的 best-effort 英文显示(给 CLI / 日志 / README 用,非 i18n 客户端不需要自己查表)

整合方拿到响应后,如果是 UI 渲染,**优先用 `category` 机器 slug** + 自己的本地化表;只在没有 i18n 上下文时退回 `categoryDisplay`。

### 13.4 完整 row 创建示例

```bash
curl -X POST https://my.fluxdesk.net/api/v1/catalog/items \
  -H "Authorization: Bearer flux-tools-XXXX" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: fd1-inbound-bootstrap-001" \
  -d '{
    "slug": "fd1.inbound",
    "name": "fd1 入站事件接入",
    "type": "api_key",
    "category": "inbound-integration",
    "description": "fd1 → FluxDesk 的入站事件 webhook token",
    "application_guide": "在 /me/integrations 申请;审批后 fd1 receiver 自动拿到 personal token",
    "approval_required": true,
    "is_pinned": false,
    "display_order": 10
  }'
```

返回 201 + 完整的 row(`item.slug` / `item.id` / `item.created_at` 等)。

MCP 等价:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "catalog_item_create",
    "arguments": {
      "slug": "fd1.inbound",
      "name": "fd1 入站事件接入",
      "type": "api_key",
      "category": "inbound-integration"
    }
  }
}
```

### 13.5 用 `id` 替代 slug 寻址(v0.21.4 起)

`team_support_resources.slug` 是可空字段,极少数历史行的 `slug = NULL`,只能靠主键 UUID 寻址。从 v0.21.4 起,以下三个 MCP 工具接受 `id`(uuid)作为 slug 的替代输入,**必须二选一,不可同时给**:

| Tool | slug 字段 | id 字段(新) |
|---|---|---|
| `assign_catalog_to_employee` | `catalog_slug?` | `catalog_id?` (uuid) |
| `catalog_item_update` | `slug?` | `id?` (uuid) |
| `catalog_item_delete` | `slug?` | `id?` (uuid) |

验证规则:

- 同时给 slug + id → `specify exactly one of ...`
- 两个都不给 → `must specify ...`
- 给的 slug / id 找不到活跃行 → `catalog not found or archived`

审计日志(`audit_logs` + MCP 自带 `logAudit`)在 metadata 里同时记录:

- `slug`:解析到的真实 slug(legacy 行可能为 `null`)
- `catalogResourceId`:总是有的 UUID
- `addressedBy`:`"slug"` 或 `"id"`,标记调用方实际用了哪条路径

例:用 `catalog_id` 直接分配一个 slug-NULL 的 legacy row 给用户:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "assign_catalog_to_employee",
    "arguments": {
      "catalog_id": "242fa5c3-1234-4567-89ab-cdef01234567",
      "email": "alice@fluxprimestudio.com",
      "note": "legacy row, addressing by uuid"
    }
  }
}
```

**REST `/api/v1/catalog/items/[slug]` 不动**:那条路由是 slug-keyed URL,本身就要求 slug 存在,legacy slug-NULL 行通过 REST 不可达,因此这个能力**只在 MCP 暴露**。

### 13.6 `assign_catalog_to_employee` 返回 `idempotent` 标志(v0.22.3 起)

`assign_catalog_to_employee` 现在返回 `{ approval_id, status, idempotent }`:

- `idempotent: false` — 这次调用新建了一条 `team_support_requests` row
- `idempotent: true` — 目标用户已经有一条非终态的 (pending / approved / in_execution / completed) row,server 直接复用现有 `approval_id`,**没有副作用**

整合 CLI 渲染 `idempotent=True/False` 替代之前的 `idempotent=?`。fd1 反馈 iter 61 polish B。

### 13.7 `revoke_catalog_approval` 支持 `approval_id` 寻址(v0.22.3 起)

补齐 § 13.5 一致性最后一块:`revoke_catalog_approval` 现在接受 `approval_id`(`team_support_requests.id` UUID)作为 `catalog_slug + email` 的**替代**入参。XOR 校验:`approval_id` 与 `(catalog_slug AND email)` 必须二选一,同给/全空都报错。响应增加 `addressedBy: "slug+email" | "approval_id"`,审计日志 metadata 同步带这个字段(对齐 catalog_item_update / catalog_item_delete 的 `addressedBy` 模式)。

### 13.8 `revoke_catalog_approval` 自动通知用户 + `notify_employee` 开关(v0.24.7 起)

之前 `revoke_catalog_approval` 静默撤销,用户要等下次 401 才察觉。这条不对称:`assign_catalog_to_employee` 在授权时就走 inbox + push notify。fd1 UX#8 #15 把撤销侧补齐。

**新行为**:撤销成功且实际有改动(`revoked=true`)→ 自动给目标用户写一条 `notifications` row + 触发 web push(沿用 `notifyActivityPush` 通道,跟 assign 走同一套):

| 字段 | 值 |
|---|---|
| `title` | `🔒 资源已撤销 · ${catalog.name}` |
| `body` | `${actor.name} 已撤销你对「${catalog.name}」的访问权 · 在 /team-support「我的权限资产」查看` |
| `url` | `/team-support?tab=assets` |
| `tag` | `team-support-revoked-${requestId | assetId | catalogId}` |
| `type` | `system.notice` |

Charter Art 7:body **只**带 catalog 名称 + actor 显示名,**绝**不带 token 值或任何敏感字段。

**新入参 `notify_employee?: boolean`**(default `true`):

- 不传 / `true` — 走通知(常态)。
- `false` — 跳过 inbox + push。用于批量 offboard、迁移脚本、用户已经 inactive 或通过其它通道告知的场景。

幂等 re-call(`revoked=false`,什么都没变)**无论如何**都不通知,避免重复打扰。

REST `DELETE /api/v1/catalog/approvals` / `POST /api/internal-os/integrations/catalog/approval` 这两条 REST 路径也走同一 `revokeCatalogApprovalCore`,默认同样开通知;现暂无 query 参数透出 `notify_employee`(MCP-only 入参),后续按需补。

### 13.9 `revoke_catalog_approval` 级联吊销行为(v0.24.8 起,文档化)

`revoke_catalog_approval` 的副作用范围跟 catalog 行的类型挂钩 —— 之前一直是隐含行为,这里讲清楚,免得整合方误以为「revoke 一行就一定砍掉所有用户的接入 token」。

| Catalog row 类型 | Token 级联 |
|---|---|
| `tools-claim-*`(`type='api_key'` + 通过 `issue_admin_tools_claim_token` / `linked_tools_token_id` 关联了 tools-claim row) | **级联**吊销所有从这个 catalog row mint 出来的 `personal_inbound_tokens` 行(目标用户通过该 token 走的 inbox channel 会立刻 401 `token_revoked`) |
| 其它 catalog 类型(普通 access asset、文档链接、人工流程等)| **不**做 token 级联;只置 access asset `revoked_at` |

响应里的 `tokens_invalidated` 计数 = 这次级联实际吊销的 token 行数(级联不发生时为 `0`)。配合 `revoked: boolean` 看一次调用整体效果:

- `revoked=true, tokens_invalidated=0` — access asset 撤了,但这条 catalog 不属于 tools-claim 类(没有 token 可级联)
- `revoked=true, tokens_invalidated=N` — access asset 撤了 + N 条 personal token 同步吊销
- `revoked=false, tokens_invalidated=0` — 幂等 re-call,什么都没变

整合 CLI 在 batch offboard 时建议**先**调 `list_inbound_tokens({owner_email})` 拿到用户自己 mint 的其它 token,**再** `revoke_catalog_approval`,否则只看 `tokens_invalidated` 数字容易误判「这个用户是不是真的所有接入都干净了」。

### 13.10 `approve_team_support_request` 响应字段 canonical 化(v0.24.7 起)

fd1 UX#8 #16:审批通过分支返回的 access asset id 字段名跟数据库列 (`access_assets`) 对不上,叫 `asset_id` 容易跟 task asset / file asset 等其它"asset"概念混。

**新响应**(单 row 分支):

```json
{
  "request_id": "...",
  "status": "approved",
  "access_asset_id": "uuid",   // canonical · 跟 access_assets.id 一致
  "asset_id": "uuid"            // deprecated alias · ONE release 后移除
}
```

Bundle 分支的 `children[].asset_id` 同样增补 `children[].access_asset_id` canonical 字段,deprecated alias 同样保留一个 release。

跟 v1.6 `status`/`new_status` 同款 deprecation 节奏:整合方先切到 canonical 字段,下个 release 我们就移除 alias。

---

## 14. Changelog

### v1.20 · 2026-06-03 (一键添加 Phase 1)

- `/me/agent` 的 Guided setup 对 Claude Desktop / Cursor / Cline / Aider 显示「一键添加」按钮,打开 `fluxdesk://connect` deep link。
- Deep link 只携带短期 `session_id` / `tool` / `base_url`,不携带原始 `sk-flux-...` token。目标客户端必须确认后,再调用 `/api/internal-os/agent/connect-callback` 换取真正 token。
- 一键添加生成的 token 在 Agent Token 列表中显示「通过一键添加」标记,撤销按钮显示「撤销接入」。复制配置 / 下载配置继续作为所有客户端的 fallback。
- 入口从头像菜单提升到左侧栏底部固定显示的「AI 助手 / Agent 接入」,避免关键接入能力藏在身份菜单里。

### v1.19 · 2026-06-03 (AI Agent onboarding Phase 1 边界)

- `/me/agent` 文档同步 Phase 1 现实边界:Guided setup 是 deterministic state machine,不是 LLM 对话。
- 补充 quickstart cards、copy/download config、read-first token prompt、现有 token activity 作为当前可用接入体验。
- 明确 per-agent identity schema、deep links、AI-for-AI token mint / scope change / rotate / archive 仍是 RFC,且必须经用户 UI 确认和安全审查后才能 ship。

### v1.18 · 2026-06-03 (AI 文档术语收口)

- 明确用户侧文案使用 **AI 助手 / Agent 接入**,MCP 只在开发者协议、客户端配置、tool name、JSON-RPC 和 scope 语境里使用。
- Token quickstart 补充:全权限 token 只适合短期调试,不要长期在多个客户端复用。

### v1.17 · 2026-06-02 (AI 助手 / Agent 接入 framing)

- 用户侧命名改为 **AI 助手 / Agent 接入**,把 MCP 保留为开发者协议名和客户端配置名。
- `/me/agent` 现在是自助生成 Agent Token 的入口;文档移除旧的审批前置步骤。
- 新增安全快速开始:先读不写、写操作先草稿后确认、按需收窄 scope、定期审计 / 撤销 token。

### v1.16 · 2026-06-01 (项目代码库与代码动态 MCP)

- 新增 `bind_project_repository` / `list_project_repositories` / `unbind_project_repository`，用于管理项目与 Gitone / Gitea / GitHub 代码库绑定。
- 新增 `list_project_repository_commits`，按项目成员权限读取已同步 commit 摘要，便于 AI 在项目上下文里理解近期代码变更。
- Scope 表新增 `read:projects` 和 `admin:projects`；绑定 / 解绑仍是 admin 操作，读取项目 commit 摘要按项目成员关系控制。

### v1.16 · 2026-06-01 (系统固定起稿 + 负责人需求模板)

- `get_owner_intake_templates` 响应结构不变，但语义调整：`description.label` / `description.body` 是系统固定起稿，不再来自负责人或 admin 可编辑默认项。
- 负责人只维护 `checklists` / `templates` 里的需求模板；负责人没有模板时返回系统默认模板。
- 调用方应把系统起稿当作 scaffolding，结合真实上下文和负责人模板改写后再创建需求。

### v1.15 · 2026-05-31 (历史：接单偏好自动起稿语义)

- 当时 `get_owner_intake_templates` 曾支持负责人 / 系统可编辑默认主题和默认描述。v1.16 起已收敛为系统固定起稿。
- Web 创建需求表单会在选定负责人后自动载入这两个字段；MCP agent 应采用同样语义，先尊重接单人的起稿偏好，再结合上下文改写成真实需求。
- 多条 `checklists` 只作为额外需求模板 / checklist 使用，不再把唯一的标题 / 描述模板混进插入清单。

### v1.14 · 2026-05-30 (历史：负责人接单偏好模板)

- 新增 MCP read tool **`get_owner_intake_templates`**(scope `read:tasks`,NEW v0.25.37)。v1.16 起只把负责人个人内容放在 `checklists` / `templates` 里。
- 返回中会标明 `description_source` / `checklist_source`：`owner` 表示负责人个人维护，`system` 表示 admin 设置的系统默认兜底，`none` 表示暂无内容。
- `create_task` 文档补充：给别人创建需求前建议先调这条读工具，把返回内容作为 `description` 起点，再由人确认后创建。这个流程用于尊重执行者、减少模糊交接，不增加强制审批字段。

### v1.13 · 2026-05-27 (FluxDocs MCP draft / publish / archive)

- 新增 FluxDocs 读写工具:
  - `list_docs`
  - `get_doc`
  - `search_docs`
  - `list_doc_drafts`
  - `create_doc_draft`
  - `update_doc_draft`
  - `publish_doc_draft`
  - `archive_doc`
- 草稿默认不对用户公开；发布仍守 author / admin / assistant / lead 权限。
- `archive_doc` 会级联归档子文档，和 web UI 行为保持一致。
- 新增 docs scopes:
  - `read:docs`
  - `write:docs`
  - `write:docs:draft`
  - `write:docs:publish`

### v1.12 · 2026-05-23 (新增 `search_user_by_username` · 多域名 lookup)

- 新增 MCP tool **`search_user_by_username`**(scope `admin:users`,NEW v0.24.12)。按 email 局部(`@` 前)服务端 ILIKE 查 users,返回 0+ 条 + 每条带 `domain` 字段。整合 CLI 不用再硬编码 `@fluxprimestudio.com` —— v0.24.x 放开 `COMPANY_EMAIL_DOMAINS` 同时允许 `@zohomail.com` 之后,光按 username 加固定后缀拼邮箱会漏掉用 zoho 域名注册的用户。
- 默认 `only_active=true`(过滤 `status='active'`),避免 CLI 误选 offboarded 行;passwd/hash/token 不返。
- Edge cases:空结果 → `{items: []}`(不是 422,合法);`username` 含 `@` → 422 `username_contains_at`;`limit=20`(单 username 在多域名下命中数永远是个位数,够用)。

### v1.11 · 2026-05-23 (新增 `invite_user` · prod onboarding gap 修复)

fd1 UX#15 #35 P1 blocker:

- 新增 MCP tool **`invite_user`**(scope `admin:users`,NEW v0.24.11)。包装 `createUserCore`(从 `/users` 表单的 `createUserAdmin` server action 抽出来的 typed core),给整合 CLI 一键拉用户进 fdesk —— 之前必须打开浏览器走 /users 表单,这是 prod onboarding 的硬阻塞。
- 入参全套 enum: `email` / `name`(必填) + `role?`(`member`/`admin`/`hr_admin`/`super_admin`/`assistant`,默认 `member`)/ `employment_type?`(`full_time`/`part_time`/`contractor`,默认 `full_time`)/ `location?`(`china`/`overseas`,默认 `overseas`)/ `ui_language?`(`en`/`zh`/`zh-TW`/`ja`,默认 `zh`)/ `status?`(`active`/`offboarding`/`offboarded`,默认 `active`)。
- 返回:`{ user_id, email, name, status, invite_sent: true }`。
- 同时新建对应 onboarding 清单 + 通知偏好 row + 给新用户 inbox 发欢迎 push —— 跟 web 表单完全同款 pipeline,只是 `audit_logs.metadata.viaMcp = true` 区分入口。
- 错误信封:`domain_not_allowed`(域不在 `COMPANY_EMAIL_DOMAINS` 白名单)+ `user_already_exists`(邮箱已存在,响应体附 `user_id` 让 CLI 选择是否当 idempotency hit)。两者都走 § 8.4 文档化的 problem+json 形态。
- 新 scope `admin:users`:跟 `admin:catalog` / `admin:tokens` 保持同样的拆分原则(kept narrow,不解锁 catalog CRUD 或 token mint;`admin:catalog` 走 one-way superset 自动兼容已签发 token)。
- Charter 守底:Art 9 — audit `actorId = caller`(super_admin 拿着 bearer token);Art 7 — 响应回显 email/name OK(admin 上下文);Art 11 — 双重 gate(scope + `isAdminRole`)。

### v1.10 · 2026-05-23 (新增 `list_approvals_for_user` · offboard 真实清单)

fd1 UX#14 #34:

- 新增 MCP tool **`list_approvals_for_user`**(scope `admin:approvals`)。给 offboard CLI 按 email 枚举用户的全部 approval row(filter by status:`approved` / `revoked` / `pending` / `all`),取代之前硬编码的 catalog 列表 —— 之前每加一行 catalog,fd1 offboard 就漏一项,会留 orphan approval。
- Status 语义跟 request-side 对齐:`approved` = `approved` / `in_execution` / `completed`(mirror `revokeCatalogApprovalCore` 的 live-access 定义);`revoked` = `withdrawn` / `rejected` / `cancelled`;`pending` = `pending`。
- 返回字段:`approval_id` / `catalog_slug` / `catalog_name` / `status` / `created_at` / `decided_at` / `decided_by`(reviewer email)。**不**返回任何 token 值或 secret(Charter Art 7)。
- `user_not_found` 走 v1.9 统一的 problem+json 信封(`email` 扩展字段);非 admin 调用直接 `forbidden`。
- 审计:每次调用都进 `audit_logs`(action `mcp.catalog.list_approvals_for_user`,metadata 带 email / status / count)。

### v1.9 · 2026-05-23 (admin write `user_not_found` 统一 problem+json)

fd1 UX#11 #22:

- **`ProblemTypes.UserNotFound`** 新增(stable URI `https://docs.fluxdesk.net/api/v1/errors/user-not-found`)。
- **所有 admin write 工具**邮箱查不到 user 时返回 problem+json shape(status 422,body 带 `email` 扩展字段)代替原 flat-string `{"error":"user not found"}`。覆盖 MCP `assign_catalog_to_employee` / `create_team_support_request_on_behalf` / `revoke_catalog_approval`(slug+email 寻址路径)+ REST `POST` / `DELETE /api/v1/catalog/approvals`。
- **未触及**:`approve_team_support_request` / `catalog_item_update` / `catalog_item_delete`(都不做 email-based user lookup,通过 `request_id` / `slug` / `id` 寻址)。`approval_id` 寻址下的 revoke 也保留 flat-string(那是数据完整性问题、没有 caller-supplied email 可回显)。`/v1/tokens/claim` 仍按现状返 403 Forbidden(claim 路径不是 admin-only,刻意模糊 user_not_found / user_inactive 避免列举)。
- 详见 § 8.3。Charter Art 7 合规:所有 emitter 都是 admin-gated,admin 本来就有 user list 访问权,回显 email 是 OK 的。

### v1.8 · 2026-05-23 (inbound polish · errors[] + next_action + cascade docs)

fd1 UX#10 #18 / #20 / #21:

- **`broadcast_event`** `error_envelope` 增 `next_action` 字段 — 每个 code 配一句「下一步该做什么」。整合 CLI 拿到失败时直接给操作员看,不再要 caller 自己查文档。详见 § 8.2。Charter Art 7:只点动作路径,绝不泄露 token / 邮箱清单。
- **`revoke_catalog_approval`** 级联吊销行为文档化 — 之前是隐含规则,现在 § 13.9 讲清楚:`tools-claim-*` 类型 → 级联 personal token;其它类型 → 只置 access asset。`tokens_invalidated` 计数 = 实际级联吊销的 token 数。无代码变化。
- **Inbound schema 校验** 多错一次性返回 — `/api/v1/events` + `/api/internal-os/integrations/inbound/personal` 返回新增 `errors: [{field, reason}]` 数组(全部 zod issue),`reason` / `field` 保留 = 第一条,back-compat 不破。fd1 dev 不再要 5 个 round-trip。

### v1.7 · 2026-05-23 (revoke 自动通知用户 + approve 响应 canonical access_asset_id)

fd1 UX sweep #8 findings #15 + #16:

- **`revoke_catalog_approval`** 撤销成功后**自动**给用户发 inbox + push notify(对称于 assign 侧)。新增 `notify_employee?: boolean`(default `true`),`false` 用于批量 offboard / 已 inactive 用户跳过通知。详见 § 13.8。
- **`approve_team_support_request`** 响应新增 canonical `access_asset_id` 字段(匹配 `access_assets` 表名),deprecated `asset_id` alias 保留**一个 release**。Bundle children 同款。详见 § 13.10。

### v1.6 · 2026-05-23 (task tool API 一致化 + create_task project_slug 必填)

fd1 UX sweep #5 findings #8 + #10:

- **`get_task`** / **`update_task_status`**:`code` 现在是 canonical 入参(匹配响应里的 `task.code`)。`task_code` 仍接受作为 deprecated alias,**下个 release 移除**。两个都给时 `code` wins。
- **`update_task_status`** 响应:新增 canonical `status` 字段(匹配 `get_task.task.status` 命名)。原 `new_status` 保留为 deprecated duplicate **一个 release**,然后移除。
- **`create_task`** 入参:`project_slug` 现在**必填**(非 draft 路径)。kingston 拍板:之前的 optional 是 oversight,不是 by design。缺 `project_slug` 返回 `{error: "project_slug is required"}`。**`as_draft=true` 例外**(草稿允许不完整)。历史 `project_id IS NULL` 行不迁移,enforcement 只对新建生效。

### v1.5 · 2026-05-23 (revoke_catalog_approval 可按 approval_id 寻址)

新增 § 13.7 — `revoke_catalog_approval` 支持 `approval_id` 作为 `catalog_slug + email` 的替代入参,XOR 校验,响应/审计带 `addressedBy`。补齐 v0.22.2 Charter 一致性(assign / item_update / item_delete 已支持 slug-or-id,revoke 是最后一块)。

### v1.4 · 2026-05-22 (assign_catalog_to_employee 返回 idempotent)

新增 § 13.6 — `assign_catalog_to_employee` 响应额外携带 `idempotent: boolean`,标记本次调用是新建 row 还是复用了已存在的非终态 row。fd1 反馈 iter 61 polish B。

### v1.3 · 2026-05-22 (catalog 行可按 id 寻址)

新增 § 13.5 — `assign_catalog_to_employee` / `catalog_item_update` / `catalog_item_delete` 三个 MCP 工具支持 `catalog_id` / `id`(uuid)作为 slug 的替代输入,用于寻址历史 slug-NULL 行。fd1 反馈 242fa5c3。

### v1.2 · 2026-05-21 (添加 catalog ROW CRUD)

新增 § 13 Catalog row slug 命名约定 + REST `/api/v1/catalog/items` 五端点 + MCP `catalog_item_create` / `_list` / `_update` / `_delete` 四工具。

### v1.1 · 2026-05-21 (添加 Token Scopes)

新增 § 7 Token 权限范围。已签发的 token 自动获得 `scopes = ['*']`(向后兼容)。



### v1.0 · 2026-05-21

初版。MCP server 自 v0.10.x 起已经在 prod 跑,但一直只有 `/me/agent` 页面里几个 disclosure 面板做文档;这次抽出独立 spec + 渲染页 + curl-able markdown endpoint,跟 INBOUND_INTEGRATION_SPEC.md 一致。
