内容
<p align="center">
<img src="assets/logo.svg" width="160" alt="RibbonSmith logo — a smith's hammer forging a ribbon command button on an anvil"/>
</p>
<h1 align="center">RibbonSmith</h1>
<p align="center">
<b>声明式地创建 Dynamics 365 命令栏。</b><br/>
一个 MCP 服务器,为 AI 编程代理(和您)提供安全、验证、可恢复的命令栏自定义,就像经典的 Ribbon Workbench 一样。
</p>
---
RibbonSmith 通过您的身份使用普通的 Web API 调用与您的 Dataverse 环境进行通信。**环境中不安装任何内容**;为每个编辑的实体创建一个小的非托管容器解决方案(`RibbonEditMCP_<entity>`),以承载自定义 — 与 Ribbon Workbench 使用的相同“工作区解决方案”机制。
## 为什么
手写 `RibbonDiffXml` 很容易出错:id、位置、模板别名和元素顺序都必须完全正确,而且反馈循环(导入 → 发布 → 检查)很慢。RibbonSmith 为代理关闭了循环:
- **基础** — 读取工具返回 *实际组合的命令栏*(每个有效位置、控件 ID、命令和序列)为紧凑的 JSON。
- **声明式写入** — `ribbon_add_button` 等生成模式正确的 XML;`ribbon_edit_diff` 为高级情况接受原始 XML。
- **验证任何服务器交互之前** — 引用、位置、元素顺序、Web 资源存在、不支持的实体阻止列表。
- **事务发布** — 验证 → 备份 → 异步导入 → 发布 → *通过重新读取组合的命令栏验证更改实际上是实时的*。
- **即时恢复** — 每次检出和发布都会快照一个可恢复的备份。
## 设置
先决条件:Node.js ≥ 20 和具有创建/导入解决方案和发布权限的 Dataverse 用户(例如系统管理员/系统自定义者)。
```bash
npm install
npm run build
```
使用 Claude Code 注册:
```bash
claude mcp add ribbonsmith --env RIBBON_MCP_ENV_URL=https://yourorg.crm4.dynamics.com -- node <absolute-path>/ribbonsmith/dist/index.js
```
或将 [.mcp.json.example](.mcp.json.example) 复制到项目的 `.mcp.json`。
### 身份验证
在需要网络的第一次调用中,RibbonSmith 使用有效的第一个策略获取令牌(使用 `RIBBON_MCP_AUTH` 覆盖):
| 策略 | `RIBBON_MCP_AUTH` | 工作原理 |
|---|---|---|
| 服务主体 | `clientsecret` | 设置 `RIBBON_MCP_TENANT_ID`、`RIBBON_MCP_CLIENT_ID`、`RIBBON_MCP_CLIENT_SECRET`。用于 CI/管道;应用程序注册需要环境中的应用程序用户。 |
| Azure CLI | `azcli` | 重用 `az login` 会话(需要 Azure CLI)。 |
| 浏览器 SSO | `interactive` | 为正常的 Microsoft Entra 登录(身份验证代码 + PKCE 在本地循环访问中)打开浏览器,然后缓存和静默刷新令牌 — 与 Microsoft 官方 Dataverse MCP 本地代理相同的登录体验。 |
在 `auto` 模式(默认)下,顺序为:`clientsecret`(如果存在环境变量)→ `azcli`(如果可用)→ `interactive`。交互流程中的令牌缓存在工作区目录中;删除 `token-cache.json` 以注销。
交互流程使用 Microsoft 在其自己的 Dataverse QuickStart 示例中提供的公共客户端 ID (`51f81489-…`),请求标准的 `user_impersonation` 委托权限 — 因此与官方 Dataverse MCP 服务器的代理不同,**不需要租户管理员同意或 Power Platform 管理中心启用**。如果您的租户限制了该客户端 ID,请注册您自己的公共客户端应用程序(重定向 URI `http://localhost`,Dynamics CRM `user_impersonation` 权限)并设置 `RIBBON_MCP_CLIENT_ID`。
## 编辑生命周期
```
ribbon_checkout ──► 编辑工具(仅限本地) ──► ribbon_preview_diff ──► ribbon_publish
│ │
└── 备份(可恢复) 备份(发布前) ──────────────┘
ribbon_restore_backup ◄── 随时恢复
```
1. **`ribbon_checkout`** `{ entity: "account" }` — 快照当前命令栏自定义(备份)并创建实体的本地工作副本 `RibbonDiffXml`。
2. **本地编辑** — 这些都不影响环境:
`ribbon_get_structure`(您的有效目标映射)、`ribbon_add_button`、`ribbon_hide_control`、`ribbon_customize_command`(复制内置命令到 diff 中进行修改)、`ribbon_edit_diff`(验证的原始 XML)、`ribbon_remove_customization`、`ribbon_discard`。
3. **`ribbon_preview_diff`** — 将要发布的确切 XML + 完整的验证报告。
4. **`ribbon_publish`** — 验证、备份、提交 **异步** 解决方案导入、发布,然后通过重新读取组合的命令栏验证控件是实时(或隐藏)的。服务器端导入需要 1-3 分钟;如果导入超过 `waitSeconds`(默认为 120),则工具返回 `status: "importing"` — 调用 `ribbon_publish_status` 以完成它。
5. **`ribbon_restore_backup`** — 重新导入任何备份并发布:完全恢复。
工作文件和备份位于 `~/.dataverse-ribbon-mcp/<env-host>/`(使用 `RIBBON_MCP_WORKSPACE` 覆盖)。所有内容都是普通文件;最坏的情况是,通过制造者门户手动导入备份 zip。
## 示例:代理会话
> **用户:** 在帐户表单上添加一个“发送到 SAP”的按钮,该按钮调用 `new_/js/sap.js: sendToSap(recordId)`,仅适用于现有记录。
```
ribbon_checkout { entity: "account" }
ribbon_get_structure { entity: "account", location: "Form" }
ribbon_add_button {
entity: "account",
id: "new_.account.SendToSap.Button",
location: "Mscrm.Form.account.MainTab.Save.Controls._children",
label: "发送到 SAP",
sequence: 45,
modernImage: "ExportToExcel",
action: { type: "JavaScriptFunction", library: "new_/js/sap.js",
functionName: "sendToSap",
parameters: [{ type: "CrmParameter", value: "FirstPrimaryItemId" }] },
enableRules: [{ type: "FormStateRule", id: "new_.account.SendToSap.Existing",
state: "Existing", default: true }]
}
ribbon_preview_diff { entity: "account" }
ribbon_publish { entity: "account" }
→ { status: "published", verification: { status: "verified" }, backupId: "..." }
```
## 工具参考
| 工具 | 网络 | 目的 |
|---|---|---|
| `ribbon_status` | 是 | WhoAmI、检出、最近备份 |
| `ribbon_checkout` | 是 | 开始编辑;快照备份 + 本地工作副本 |
| `ribbon_get_structure` | 缓存 | 组合命令栏为 JSON(选项卡 → 组 → 控件) |
| `ribbon_get_command` | 缓存 | 一个命令的操作 + 规则(用于重用/自定义) |
| `ribbon_get_diff` | 否 | 当前工作 `RibbonDiffXml` + 摘要 |
| `ribbon_add_button` | 否* | 声明式按钮/命令/规则/标签 |
| `ribbon_hide_control` | 否* | 现有控件的 `HideCustomAction` |
| `ribbon_customize_command` | 否* | 将 OOTB 命令复制到 diff 中 |
| `ribbon_edit_diff` | 否* | 替换整个工作 diff(验证) |
| `ribbon_remove_customization` | 否 | 按 ID 从 diff 中删除一个元素 |
| `ribbon_preview_diff` | 否* | Diff XML + 验证报告 |
| `ribbon_publish` | 是 | 验证 → 备份 → 异步导入 → 发布 → 验证 |
| `ribbon_publish_status` | 是 | 轮询/完成待处理发布或恢复导入 |
| `ribbon_discard` | 否 | 重置工作副本为上次导出的状态 |
| `ribbon_list_backups` | 否 | 列出可恢复的快照 |
| `ribbon_restore_backup` | 是 | 重新导入快照 + 发布(恢复) |
\* 验证可能会咨询缓存的组合命令栏并通过网络检查 Web 资源是否存在。
## 安全模型
- 每次 `ribbon_checkout` 和每次 `ribbon_publish` 都在进行任何更改之前写入时间戳解决方案 zip;`ribbon_restore_backup` 重新导入它。
- 发布仅影响容器解决方案中导出 `RibbonDiffXml` 节点 — 实体元数据从不往返(实体作为 `unmodified="1"` 外壳提供)。
- 验证阻止发布:格式错误的 XML、错误的 element 顺序、缺少的 `Mscrm.Templates`、重复的 id、未解析的命令/规则/标签引用、缺少的 Web 资源、不支持的系统实体。
- 导入失败会显示导入作业的错误文本;导入是服务器端的事务,因此失败的导入不会更改任何内容。
## 状态和限制
- **实体命令栏:稳定。** 完整的生命周期(添加 → 发布 → 验证 → 隐藏 → 发布 → 验证 → 恢复 → 验证)由实时 E2E 套件 (`test/e2e/run-e2e.ts`) 和 56 个单元测试覆盖真实的组合命令栏。
- **应用程序命令栏(`APPLICATION`):实验性。** 按文档架构(组件类型 50,diff 在 `ImportExportXml` 下)实现,但尚未被实时 E2E 套件覆盖。请准备好备份 ID。
- 仅限经典 `RibbonDiffXml`:使用现代 Power Apps 命令设计器(Power Fx / `appaction`)构建的命令不被读取或写入。经典自定义在统一界面中呈现良好。
- 通过 `ribbon_edit_diff`(原始 XML)进行 Flyout/Menu/组/选项卡创建。
- 假设每个实体一次只有一个编辑器(共享容器解决方案)。
## 开发
```bash
npm test # 单元测试(vitest)
npx tsc --noEmit # 类型检查
npx tsx test/e2e/run-e2e.ts https://yourorg.crm4.dynamics.com
# 实时 E2E — 修改并恢复 'contact' 命令栏在该环境中!
```
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — 模块映射、数据流、为什么是异步导入、测试策略。
- [docs/RIBBON-CONCEPTS.md](docs/RIBBON-CONCEPTS.md) — 命令栏 XML 入门(组合命令栏 vs diff、位置、命令/规则、命令检查器)。
## 致谢
RibbonSmith 作为 MCP 工具重新实现了 **Scott Durow 的 Ribbon Workbench** 开创的声明式编辑模型 — 多年来人类安全自定义 Dynamics 命令栏的方式。该项目与它没有共享代码;机制(工作区解决方案 → RibbonDiffXml 拼接 → 导入 → 发布)从其可观察的行为和微软的公开文档(命令栏模式和解决方案 API)中研究而来。
## 许可证
[MIT](LICENSE)