Skip to main content
Glama
Longman006

RibbonSmith

by Longman006
README.md
<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>Forge Dynamics 365 command bars, declaratively.</b><br/>
  An MCP server that gives AI coding agents (and you) safe, validated,
  revertible ribbon customization — the way the classic Ribbon Workbench did it.
</p>

---

RibbonSmith talks to your Dataverse environment with plain Web API calls under
your own identity. **Nothing is installed in the environment**; a small
unmanaged container solution (`RibbonEditMCP_<entity>`) is created per edited
entity to carry the customizations — the same "workspace solution" mechanism
the Ribbon Workbench used.

## Why

Hand-writing `RibbonDiffXml` fails easily: ids, locations, template aliases and
element order must all be exactly right, and the feedback loop (import → publish
→ check) is slow. RibbonSmith closes the loop for an agent:

- **Grounding** — read tools return the *real composed ribbon* (every valid
  location, control id, command and sequence) as compact JSON.
- **Declarative writes** — `ribbon_add_button` & friends generate schema-correct
  XML; the escape hatch `ribbon_edit_diff` accepts raw XML for advanced cases.
- **Validation before anything touches the server** — references, locations,
  element order, web-resource existence, unsupported-entity blocklist.
- **Transactional publish** — validate → backup → async import → publish →
  *verify the change is actually live* by re-reading the composed ribbon.
- **Instant revert** — every checkout and publish snapshots a restorable backup.

## Setup

Prerequisites: Node.js ≥ 20 and a Dataverse user with permission to create/
import solutions and publish (e.g. System Administrator / System Customizer).

```bash
npm install
npm run build
```

Register with Claude Code:

```bash
claude mcp add ribbonsmith --env RIBBON_MCP_ENV_URL=https://yourorg.crm4.dynamics.com -- node <absolute-path>/ribbonsmith/dist/index.js
```

Or copy [.mcp.json.example](.mcp.json.example) to your project's `.mcp.json`.

### Authentication

On the first call that needs the network, RibbonSmith acquires a token using
the first strategy that works (override with `RIBBON_MCP_AUTH`):

| Strategy | `RIBBON_MCP_AUTH` | How it works |
|---|---|---|
| Service principal | `clientsecret` | Set `RIBBON_MCP_TENANT_ID`, `RIBBON_MCP_CLIENT_ID`, `RIBBON_MCP_CLIENT_SECRET`. For CI/pipelines; the app registration needs an application user in the environment. |
| Azure CLI | `azcli` | Reuses your `az login` session (requires Azure CLI). |
| Browser SSO | `interactive` | Opens your browser for a normal Microsoft Entra sign-in (auth code + PKCE on a localhost loopback), then caches and silently refreshes tokens — the same sign-in experience as Microsoft's official Dataverse MCP local proxy. |

In `auto` mode (default) the order is: `clientsecret` (if env vars present) →
`azcli` (if available) → `interactive`. Tokens from the interactive flow are
cached in the workspace directory; delete `token-cache.json` to sign out.

The interactive flow uses the public client id Microsoft ships in its own
Dataverse QuickStart samples (`51f81489-…`), requesting the standard
`user_impersonation` delegated permission — so unlike the official Dataverse
MCP server's proxy, **no tenant admin consent or Power Platform admin center
enablement is required**. If your tenant restricts that client id, register
your own public client app (redirect URI `http://localhost`, Dynamics CRM
`user_impersonation` permission) and set `RIBBON_MCP_CLIENT_ID`.

## The edit lifecycle

```
ribbon_checkout ──► edit tools (local only) ──► ribbon_preview_diff ──► ribbon_publish
      │                                                                     │
      └── backup (restorable)            backup (pre-publish) ──────────────┘
                              ribbon_restore_backup  ◄── revert anytime
```

1. **`ribbon_checkout`** `{ entity: "account" }` — snapshots the current ribbon
   customizations (backup) and creates a local working copy of the entity's
   `RibbonDiffXml`.
2. **Edit locally** — none of these touch the environment:
   `ribbon_get_structure` (your map of valid targets), `ribbon_add_button`,
   `ribbon_hide_control`, `ribbon_customize_command` (copy an out-of-the-box
   command into the diff to modify it), `ribbon_edit_diff` (validated raw XML),
   `ribbon_remove_customization`, `ribbon_discard`.
3. **`ribbon_preview_diff`** — the exact XML that will be published + full
   validation report.
4. **`ribbon_publish`** — validates, backs up, submits an **async** solution
   import, publishes, then verifies the controls are live (or hidden) in the
   freshly retrieved composed ribbon. Server-side imports take 1–3 minutes; if
   the import outlasts `waitSeconds` (default 120), the tool returns
   `status: "importing"` — call `ribbon_publish_status` to complete it.
5. **`ribbon_restore_backup`** — re-import any backup and publish: full revert.

Working files and backups live under `~/.dataverse-ribbon-mcp/<env-host>/`
(override with `RIBBON_MCP_WORKSPACE`). Everything is a plain file; worst case,
import a backup zip manually through the maker portal.

## Example: agent session

> **User:** Add a "Send to SAP" button on the account form that calls
> `new_/js/sap.js: sendToSap(recordId)`, only for existing records.

```
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: "Send to 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: "..." }
```

## Tool reference

| Tool | Network | Purpose |
|---|---|---|
| `ribbon_status` | yes | WhoAmI, checkouts, recent backups |
| `ribbon_checkout` | yes | Begin editing; snapshot backup + local working copy |
| `ribbon_get_structure` | cached | Composed ribbon as JSON (tabs → groups → controls) |
| `ribbon_get_command` | cached | One command's actions + rules (for reuse/customization) |
| `ribbon_get_diff` | no | Current working `RibbonDiffXml` + summary |
| `ribbon_add_button` | no* | Declarative button/command/rules/labels |
| `ribbon_hide_control` | no* | `HideCustomAction` for an existing control |
| `ribbon_customize_command` | no* | Copy an OOTB command into the diff |
| `ribbon_edit_diff` | no* | Replace the whole working diff (validated) |
| `ribbon_remove_customization` | no | Remove one element from the diff by id |
| `ribbon_preview_diff` | no* | Diff XML + validation report |
| `ribbon_publish` | yes | Validate → backup → async import → publish → verify |
| `ribbon_publish_status` | yes | Poll/complete a pending publish or restore import |
| `ribbon_discard` | no | Reset working copy to last exported state |
| `ribbon_list_backups` | no | List restorable snapshots |
| `ribbon_restore_backup` | yes | Re-import a snapshot + publish (revert) |

\* validation may consult the cached composed ribbon and check web-resource
existence over the network.

## Safety model

- Every `ribbon_checkout` and every `ribbon_publish` writes a timestamped
  solution zip **before** any change; `ribbon_restore_backup` re-imports it.
- Publishes only ever touch the `RibbonDiffXml` node inside a fresh export of
  the container solution — entity metadata is never round-tripped
  (the entity ships as an `unmodified="1"` shell).
- Validation blocks publishes on: malformed XML, wrong element order, missing
  `Mscrm.Templates`, duplicate ids, unresolved command/rule/label references,
  missing web resources, unsupported system entities.
- Import failures surface the importjob's own error text; imports are
  transactional server-side, so a failed import changes nothing.

## Status & limitations

- **Entity ribbons: stable.** The full lifecycle (add → publish → verify →
  hide → publish → verify → restore → verify) is covered by a live E2E suite
  (`test/e2e/run-e2e.ts`) plus 56 unit tests over a real composed ribbon.
- **Application ribbon (`APPLICATION`): EXPERIMENTAL.** Implemented per the
  documented schema (component type 50, diff under `ImportExportXml`) but not
  yet covered by the live E2E suite. Keep backup ids at hand.
- Classic `RibbonDiffXml` only: commands built with the modern Power Apps
  command designer (Power Fx / `appaction`) are not read or written. Classic
  customizations render fine in Unified Interface.
- Flyout/Menu/group/tab creation goes through `ribbon_edit_diff` (raw XML).
- One editor per entity at a time is assumed (shared container solution).

## Development

```bash
npm test                                    # unit tests (vitest)
npx tsc --noEmit                            # typecheck
npx tsx test/e2e/run-e2e.ts https://yourorg.crm4.dynamics.com
                                            # live E2E — modifies + restores the
                                            # 'contact' ribbon in that environment!
```

- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — module map, data flows, why
  async import, testing strategy.
- [docs/RIBBON-CONCEPTS.md](docs/RIBBON-CONCEPTS.md) — a primer on ribbon XML
  (composed ribbon vs diff, locations, commands/rules, Command Checker).

## Acknowledgments

RibbonSmith re-implements, as MCP tools, the declarative editing model
pioneered by **Scott Durow's Ribbon Workbench** — for years the way humans
customized Dynamics ribbons safely. This project shares no code with it; the
mechanism (workspace solution → RibbonDiffXml splice → import → publish) was
studied from its observable behavior and from Microsoft's public documentation
of the ribbon schemas and solution APIs.

## License

[MIT](LICENSE)

TDQS

A4.2/5.0

Scored across 16 tools

Disambiguation5/5

Each tool targets a distinct aspect of the ribbon editing workflow: status, checkout, inspection, editing actions, validation, publishing, and backup/restore. Even the get_* tools return different data (structure, command, diff), leaving no ambiguity about which tool to call.

Naming Consistency5/5

All tools share the 'ribbon_' prefix and use snake_case with a verb_noun structure (get_structure, add_button, restore_backup). Single-word verbs like status, checkout, and publish are also clear and consistent with the overall pattern, with no mixed conventions.

Tool Count4/5

With 16 tools, the set is slightly above the typical 3-15 range, but each tool maps to a distinct step in the ribbon lifecycle (checkout, edit, validate, publish, backup). The count is justified by the complexity of Dataverse ribbon customization and no tool feels redundant.

Completeness5/5

The tool set provides full lifecycle coverage: start (status, checkout), inspect (get_*), modify (add, hide, customize, edit, remove), validate (preview_diff), publish (publish, publish_status), and revert (backups, restore). The escape hatch 'ribbon_edit_diff' covers any edge cases, leaving no obvious dead ends.

Maintenance

ActivityStale
ResponsivenessUnresponsive