# Core — Rosetta MCP tool vocabulary

Runtime-agnostic catalog of the `rosetta_*` MCP tools the plugin exposes via the
bundled `rosetta-mcp-server` binary. As of **server v0.11.0** (api 1.12.0)
the surface is **46 tools** — **5 composites + 41 read/write 1:1 wrappers**, with
36 wrappers excluded — behind a server-side **deny-by-default tool-policy** (§7).
Every key sees the same tools; the upstream server decides per route what a key
may do (§7.1). Both adapter runtimes
(Claude Code, Cowork) bind to this vocabulary; the adapter MUST NOT rename tools,
collapse the composite/1:1 distinction, invent parallel names, or call a tool the
server tool-policy denies.

The binary registers the 1:1 wrappers mechanically from the vendored
`api/rosetta-v1.yaml` (`rosetta_<operationId-snake-cased>`); the composites come
from `config/mcp/composites.yaml`. The plugin manifest does **not** declare the
tools — the binary does, at startup.

This catalog covers READ and WRITE. The cross-cutting write discipline
(confirm-before-destructive, idempotency, enum validation, partition/access) is
`core/write-safety.md`; per-family detail is in `core/engagement-twin.md`,
`core/governance-lifecycle.md`, `core/engagement-lifecycle.md`,
`core/engagement-canvas.md`. Reach for those before driving a write.

## 1. Five composites (task-shaped) — the default reach

Composites bundle a common multi-call flow into one MCP `tools/call`. Source of
truth: `config/mcp/composites.yaml`.

| Composite | Purpose | Fans out to |
| --- | --- | --- |
| `rosetta_turn` | One conversation turn end-to-end (project-scoped). | `rosetta_create_conversation` (idempotent) → `rosetta_persist_conversation_turn` → `rosetta_decide_flow_next_action`. Returns the flow decision. |
| `rosetta_engagement_turn` | One **engagement-scoped** turn; binds the session to an engagement and is meant to write turn semantics into the twin. | same three, with `engagementId` (slug) on create + an optional `semanticProjection` (decisions / gaps / citations) on persist. Returns the flow decision. In server v0.11.0 the persist step does not carry `engagementId`, so the projection is not written; use the 1:1 wrappers for that (`core/conversation-lifecycle.md` §11). |
| `rosetta_record` | Governance batch write. Always a DecisionTrace; conditionally an ADRApplication + a QueryTrace. Partial-success contract. | `rosetta_create_decision_trace` → (`rosetta_record_adr_application` if present) → (`rosetta_create_query_trace` if present). |
| `rosetta_query` | Context retrieval with `expand=full` (inlines `governingADRs[]`, `semanticEntities[]`, `vocabularyTerms[]`; removes N+1 hydration). | `rosetta_create_context_retrieval` with `expand=full` forced. |
| `rosetta_engagement_canvas_slack` | An engagement's canvas as publish-ready Slack-canvas markdown, one call. | `rosetta_get_engagement_canvas` with `format=slack`; returns `{ status, text }`. Hand `text` to the Slack MCP. See `core/engagement-canvas.md`. |

Reach for a composite first; fall back to the 1:1 wrappers (§2/§3) for
fine-grained control or an endpoint no composite covers.

## 2. Read wrappers (fine-grained)

Grouped by family. All are non-mutating.

**Context / patterns / ADRs / spec-manifest**
| Tool | REST |
| --- | --- |
| `rosetta_create_context_retrieval` | `POST /api/v1/context` (sparse; `expand=full` opt) |
| `rosetta_get_pattern` | `GET /api/v1/patterns/[id]` |
| `rosetta_get_a_d_r` | `GET /api/v1/adrs/[id]` |
| `rosetta_get_spec_manifest` | `GET /api/v1/spec-manifests/[id]` |

**Conversation / identity**
| Tool | REST |
| --- | --- |
| `rosetta_get_conversation_context` | `GET /api/v1/conversations/[id]` |
| `rosetta_get_authenticated_user` | `GET /api/v1/users/me` |

**Engagement (twin / memory / lifecycle / canvas)**
| Tool | REST | Notes |
| --- | --- | --- |
| `rosetta_list_engagements` | `GET /api/v1/engagements` | discover engagementIds (slugs) |
| `rosetta_get_engagement` | `GET /api/v1/engagements/[uid]` | |
| `rosetta_get_engagement_twin` | `GET /api/v1/engagements/[uid]/twin` | full TwinDocument; `since=[int revision]` for delta; **no `include=`** (filter client-side) |
| `rosetta_recall_engagement_memory` | `GET /api/v1/engagements/[uid]/memory` | hybrid graph+vector; `q`, `kinds=decision\|artifact\|gap\|session`, `limit`, `cursor` |
| `rosetta_get_engagement_canvas` | `GET /api/v1/engagements/[uid]/canvas` | `format=json` (default) or `slack`; `include=` section subset |
| `rosetta_get_digital_twin_node` | `GET /api/v1/digital-twin/nodes/[id]` | one node by its UUID `uid`; `include=history`, `asOf` |
| `rosetta_get_digital_twin_node_by_key` | `GET /api/v1/digital-twin/nodes/[label]/[key]` | one node by label + catalog merge key (§8); reaches seeded canonical content, whose uids are slugs, and nodes with no uid; `include=history`, `asOf`. An engagement node needs a grant on that engagement or its account's owner/admin (403 otherwise); any other node is readable by any authenticated key |

**Governance** — reads included, every row needs a **platform admin's key**
(403 otherwise; §7.1)
| Tool | REST |
| --- | --- |
| `rosetta_get_promotion_candidate` | `GET /api/v1/promotion-candidates/[id]` |
| `rosetta_list_promotion_candidates` | `GET /api/v1/promotion-candidates` (filters: status, decisionPoint) |
| `rosetta_get_governance_review` | `GET /api/v1/governance-reviews/[id]` |

## 3. Write wrappers (fine-grained) — see `core/write-safety.md` first

**Conversation / governance-batch members**
| Tool | REST | Idempotency |
| --- | --- | --- |
| `rosetta_create_conversation` | `POST /api/v1/conversations` | 201 create / 200 exists on `(sessionId, projectId, userId)` |
| `rosetta_persist_conversation_turn` | `POST /api/v1/conversations/[id]/turns` | new turn each call; `turnNumber` skip = race. With `engagementId` (slug) in the body (api 1.10.0+): needs a grant on that engagement (403); `semanticProjection` decisions and gaps are validated, and a gap needs `gapType` + `description` (400/422); a key another lane, session or engagement holds is 409, refused before the turn is persisted. Without `engagementId` the projection is ignored. `core/conversation-lifecycle.md` §12 |
| `rosetta_decide_flow_next_action` | `POST /api/v1/flow/next-action` | read-shaped (no persist) |
| `rosetta_create_decision_trace` | `POST /api/v1/decision-traces` | 201/200 on `(sessionUid, decisionPoint)` |
| `rosetta_record_adr_application` | `POST /api/v1/adr-applications` | on `(adrUid, engagementUid, surfacedBy)` |
| `rosetta_create_query_trace` | `POST /api/v1/query-traces` | per `RESOLVED_VIA` target |
| `rosetta_upsert_practitioner` | `POST /api/v1/practitioners` | upsert on `email` (200 always) |

**Engagement twin (`core/engagement-twin.md`)**
| Tool | REST | Notes |
| --- | --- | --- |
| `rosetta_apply_engagement_twin` | `POST /api/v1/engagements/[uid]/twin:apply` | **forensic** lane: atomic `edits[]` for confirmed graph-canonical state; send `Idempotency-Key`; 200/409 |
| `rosetta_apply_engagement_twin_rehearsal` | `POST /api/v1/engagements/[uid]/twin:apply-rehearsal` | **rehearsal** lane: the honesty-routing target for `[SIMULATED]` / `status:PROPOSED` / `simulationRunId` content (`core/write-safety.md` §1.5). Admits `traceKind:"runtime"` and stamps the revision ProvenanceEvent `provenance:"simulated"`. Available since **mcp v0.8.7** (generated from the `applyEngagementTwinRehearsal` operation, tag `engagement-twin`). |
| `rosetta_create_digital_twin_node` | `POST /api/v1/digital-twin/nodes` | **platform admin only**, any label (403 otherwise); body `nodeType`, `properties`, `domainIdentity` (five-field identity, valid `overrideMode`), `projectId` (uuid); an own-lane label is 422 (§8) |
| `rosetta_update_digital_twin_node` | `PATCH /api/v1/digital-twin/nodes/[id]` | `properties` patch by UUID `uid`; optional `baseVersion` on a node outside any engagement only (stale = 409; on an engagement node, 422); same authz and 422s as the keyed form below |
| `rosetta_update_digital_twin_node_by_key` | `PATCH /api/v1/digital-twin/nodes/[label]/[key]` | patch by label + merge key (§8). An engagement node needs that engagement's lead or its account's owner/admin; **every other (canonical) node needs a platform admin** (403 otherwise). A change to the key that addresses the node is 422, and so is an own-lane label |

**Engagement lifecycle (`core/engagement-lifecycle.md`)**
| Tool | REST | Notes |
| --- | --- | --- |
| `rosetta_create_engagement` | `POST /api/v1/engagements` | **`clientUid` REQUIRED** (api 1.5.0; `:Client.uid` slug, lazily MERGEd; absent/empty/malformed → 422) + `name`; optional `engagementUid` slug ≤64; dual-write; 201/409/422 |
| `rosetta_update_engagement` | `PATCH /api/v1/engagements/[uid]` | name / status / clientUid (non-empty = edge **move**; `""` = **unlink**, confirm-before; omitted = unchanged) |
| `rosetta_create_project` | `POST /api/v1/projects` | new project namespace |
| `rosetta_compile_spec_manifest` | `POST /api/v1/spec-manifests/compile` | exactly one of `projectId` / `engagementId` |

**Governance three-gate (`core/governance-lifecycle.md`)** — every row needs a
**platform admin's key** (403 otherwise; §7.1)
| Tool | REST | Gate |
| --- | --- | --- |
| `rosetta_evaluate_decision_trace` | `POST /api/v1/decision-traces/[id]/evaluate` | Gate 1, one trace |
| `rosetta_run_gate1` | `POST /api/v1/governance/gate1-run` | Gate 1, every pending trace across all engagements |
| `rosetta_create_promotion_candidate` | `POST /api/v1/promotion-candidates` | Gate 2 (`decisionPoint`) |
| `rosetta_create_governance_review` | `POST /api/v1/governance-reviews` | Gate 3, open a review |
| `rosetta_approve_governance_review` | `POST /api/v1/governance-reviews/[id]/approve` | Gate 3 (irreversible) |
| `rosetta_reject_governance_review` | `POST /api/v1/governance-reviews/[id]/reject` | Gate 3 (destructive) |
| `rosetta_request_governance_review_revision` | `POST /api/v1/governance-reviews/[id]/request-revision` | Gate 3 |
| `rosetta_rollback_a_d_r` | `POST /api/v1/adrs/[id]/rollback` | ADR (destructive) |

**Content ingest (`content-ingest` tag; mcp v0.11.0)** — **dry runs only
against production until the DT-RX-036 switch** (§8, `core/write-safety.md` §1.7)
| Tool | REST | Notes |
| --- | --- | --- |
| `rosetta_ingest_content` | `POST /api/v1/domains/[domainKey]/content:ingest` | **platform admin only** (403 otherwise). One plugin run's canonical bundle, shaped like a twin:apply document: envelope `bundleId`, `producer`, `runId`, `sourceBundleSha256` (+ `note`), then `nodes[]` and `edges[]`. One transaction; `dryRun: true` validates and returns the plan (`status: planned`, 200) without writing. A refused bundle is a 422 that lists every failure |

## 4. When to use which tier

- **Composite first** for the high-frequency flows (turn, engagement-turn,
  governance batch, full-context retrieval, canvas→Slack).
- **1:1 read wrapper** when you hold a uid and want the canonical record, or when
  the sparse context default suffices.
- **1:1 write wrapper** when a composite does too much, when writes are spread
  across turns, or when you need fine-grained error handling — always under
  `core/write-safety.md`.

The composite-vs-1:1 choice is intent-driven, not capability-driven.

## 5. Idempotency contract

| Tool(s) | Key / behavior |
| --- | --- |
| `rosetta_turn` / `rosetta_engagement_turn` | `(sessionId, projectId, userId, turnNumber)`; create step idempotent, turnNumber increments server-side |
| `rosetta_create_conversation` | `(sessionId, projectId, userId)` → 201 create / 200 exists (NOT an error) |
| `rosetta_create_decision_trace` / `rosetta_record` | `(sessionUid, decisionPoint)` |
| `rosetta_record_adr_application` | `(adrUid, engagementUid, surfacedBy)` |
| `rosetta_upsert_practitioner` | `email` (always 200) |
| `rosetta_apply_engagement_twin` | client-supplied **`Idempotency-Key`** (uuid); 200 success / 409 conflict |
| `rosetta_update_digital_twin_node` / `_by_key` | optional `baseVersion` (the node's `version` from a read), for a node outside any engagement only; a stale one is 409, not an overwrite. On an engagement node (it keeps time on the engagement revision) a `baseVersion` is 422 |
| `rosetta_ingest_content` | `bundleId`: the same `bundleId` with the same body returns the first result (`replayed: true`, 200); the same `bundleId` with a different body is 409. A commit is 201 |
| reads | idempotent by definition |

Status codes: **200** where you expected 201 = idempotent hit (normal; do not
alarm). **409** = conflict → reload state, do not blind-retry; on a turn
persist it means the projection reused another lane's, session's or
engagement's key and, unless the detail says otherwise, the turn was not
persisted (`core/conversation-lifecycle.md` §12). **422** =
referential or contract failure → surface and fix the payload: an invalid
`userId` FK on conversation create (since v0.8.3); a node write that would change
the key addressing the node; a node create or patch on an own-lane label; a
`baseVersion` on an engagement node's patch; a refused ingest bundle (the body lists every failure). **401** = missing/invalid
bearer. **403** = this key may not do this (§7.1): on a governance tool, a
canonical node write, any node create, or `rosetta_ingest_content` it means
**this key isn't a platform admin**; on an engagement-scoped call it means no
grant on that engagement (or, for a write, not its lead or the account's
owner/admin). Neither is retryable (`core/write-safety.md` §4). Retry is safe on
the idempotent writes above; it is NOT safe on a 409.

## 6. Enum catalog (writes are validated server-side; off-enum = 400)

| Field | Allowed values |
| --- | --- |
| `decisionType` | `architecture`, `configuration`, `trade-off`, `scope` |
| `outcome` | `accepted`, `rejected`, `modified`, `deferred` |
| `retrievalStrategy` | `graphrag`, `keyword`, `hybrid`, `direct_lookup` |
| `surfacedBy` | `agent_recommendation`, `manual_attachment`, `architecture_review` |
| `phase` (flow state) | `discovery`, `clarification`, `pattern-matching`, `recommendation`, `validation` |
| `solutionMode` / `icp` | `customer-360`, `agentforce-first`, `marketing-cdp`, `data-governance`, `analytics-first`, `integration-first`, `unknown` |
| `overrideMode` | `base`, `supplement`, `refine`, `replace`, `suppress` (corrected v0.8.2 — the old `extend/inherit` values are gone) |
| `gapType` (a turn projection's gap; required, with `description`) | `missing_intent`, `ambiguous_decision`, `unmatched_configuration`, `contradictory_signals` |

Pre-validate against this table before any write; the server rejects off-enum
values with 400.

## 7. Server tool-policy (the boundary)

The server exposes tools by **allowlist (deny-by-default)** —
`config/mcp/tool-policy.yaml`. **Allowed tags:** `context`, `patterns`, `adrs`,
`digital-twin`, `spec-manifests`, `projects`, `conversations`, `flow`,
`governance`, `practitioners`, `identity`, `engagement-twin`,
`engagement-memory`, `engagements`, `engagement-canvas`, `content-ingest`.
**Denied** (NOT exposed, not callable, must never be hand-rolled): `accounts`,
`account-members`, `platform-admin` (cross-account administration, including
granting, listing and revoking platform admins), `engagement-access` (ACL
grant/revoke), `export` (bulk Cypher/Markdown exfiltration), `presence`
(web-only SSE/heartbeat), `twin-ratification` (the human ratification lane;
`core/write-safety.md` §1.6), and the operations `forkProject`,
`ratifyEngagementTwin` and `rejectEngagementTwin`. If a flow seems to need a
denied capability, that is an operator / out-of-band action, not a plugin call.

### 7.1 The key decides (upstream authorization)

The tool list is the same for every key. The upstream rosetta-app-server
authorizes each call against the bearer's user, so the same tool can succeed on
one key and answer 403 on another:

| Tools | Who may call (403 otherwise) |
| --- | --- |
| Governance: `rosetta_run_gate1`, `rosetta_evaluate_decision_trace`, `rosetta_create_promotion_candidate`, `rosetta_list_promotion_candidates`, `rosetta_get_promotion_candidate`, `rosetta_create_governance_review`, `rosetta_get_governance_review`, `rosetta_approve_governance_review`, `rosetta_reject_governance_review`, `rosetta_request_governance_review_revision`, `rosetta_rollback_a_d_r` | a **platform admin**, reads included (api 1.11.0+, app-server v0.8.0) |
| `rosetta_create_digital_twin_node` | a platform admin, any label |
| `rosetta_update_digital_twin_node` / `_by_key` on a node outside any engagement (canonical content) | a platform admin |
| `rosetta_update_digital_twin_node` / `_by_key` on an engagement's node | that engagement's lead, or its account's owner/admin |
| `rosetta_get_digital_twin_node` / `_by_key` | an engagement's node: any grant on that engagement, or its account's owner/admin; any other node: any authenticated key |
| `rosetta_ingest_content` | a platform admin |
| `rosetta_get_engagement_twin` / `rosetta_apply_engagement_twin` | read: any grant on that engagement, or its account's owner/admin; apply: its lead, or the account's owner/admin |
| `rosetta_persist_conversation_turn` with an `engagementId` | the conversation's owner, holding any grant on that engagement or being its account's owner/admin (api 1.10.0+) |

- **Platform admin** is a property of the user who owns the key: a row in the
  app-server's `platform_admins` table. It is not a key scope, and it is not the
  `role` that `rosetta_get_authenticated_user` returns (a `role` of `admin` is a
  practitioner role, not a platform admin). No allowlisted tool reports it; the
  first 403 from one of the tools above is the signal.
- **Granting it is never a plugin action.** The grant, list and revoke routes
  sit in the denied `platform-admin` tag, so no tool on this surface reaches
  them; an existing platform admin makes a grant, out of band.
- **Not gated on platform admin:** `rosetta_create_decision_trace`,
  `rosetta_record_adr_application`, `rosetta_create_query_trace` and the
  `rosetta_record` composite. They are governance-tagged but are not gate
  routes.
- **The decision verbs stay on the agent surface** (app-server
  `gate3-governance-authz.md`, ruling Q-C-2): on a platform admin's key an agent
  can approve, reject, revise and roll back. The plugin's confirm-before rule
  (`core/write-safety.md` §1) is then the only interlock, and it is a
  client-side courtesy, not a control.

## 8. Wire-contract notes

- **`engagementId` is a slug**, not a UUID: `^[A-Za-z0-9_.:\-]+$`, ≤64 (e.g.
  `nto-02`, `eng-nto-01`). Applies to context, conversation create, the turn
  persist body, `rosetta_engagement_turn`, twin/canvas/memory/lifecycle calls.
  `sessionId` / `projectId` / `userId` remain UUIDs.
- **`rosetta_get_engagement_twin`** takes NO `include=`; read the full
  TwinDocument and filter node types client-side. Use `since=[int revision]` for
  a delta read.
- **`rosetta_compile_spec_manifest`** takes exactly one of `projectId` (uuid) or
  `engagementId` (slug) — engagement-scoped compile since v0.8.1.
- **422** documents referential-integrity failures on conversation create
  (MySQL `user_id` FK; the `project_id` FK was dropped in platform migration 033).
- **Keyed node address** (`*_by_key`, mcp v0.10.0): `label` is a catalog label;
  `key` is its catalog merge key — `qualifiedKey` on the eight inheritable labels
  (ContextPattern, ADR, BusinessIntent, VocabularyTerm, SemanticEntity,
  PlatformObject, EngineFunction, ContextArtifact) **and on SourceArtifact**
  (app-server v0.8.1), the label's own natural key otherwise (`traceId`,
  `engagementId`, ...). The binary percent-encodes the key as one path segment.
  A write that would change that key is refused with 422, on the UUID route too.
- **Own-lane labels** — `Engagement`, `Client`, `Practitioner`,
  `EnablementSession`, `PromotionCandidate`, `GovernanceReview`, `SpecManifest`,
  `QualityFlag`, `QueryTrace` — have their own API or gate. Node create and patch
  on them are refused with 422; reads are unaffected.
- **`rosetta_ingest_content`** lands canonical, engagement-less labels only
  (ContextPattern, ADR, BusinessIntent, VocabularyTerm, SemanticEntity,
  PlatformObject, EngineFunction, SourceArtifact), each keyed by `qualifiedKey`
  and in the path's `domainKey`. Created patterns land `draft`, ADRs and
  BusinessIntents `proposed`. Its closed edge allowlist is pinned to
  rosetta-platform's edge registry (`db/schema/edge-registry.yaml`) at the
  release the app-server vendors; an unlisted edge or edge property is 422,
  never ignored. **Until the DT-RX-036 switch (rosetta-platform manifest §11B,
  E9-I1), no plugin submits to the production ingestion API: production use is
  `dryRun: true` only.**

## 9. Schema introspection

Per-tool schemas live in `api/rosetta-v1.yaml#/components/schemas/`; the
`tools/list` response carries a `description` per tool. Read the OpenAPI spec
directly — do NOT introspect by calling a tool with an empty payload (the error
is not a schema).

## 10. What NOT to do

- **Do not invent tool names** or call a tool the §7 tool-policy denies.
- **Do not call `/api/v1/*` directly**, bypassing MCP — the bearer lives in the
  binary (RCP-C-6); bypassing it leaks the token.
- **Do not call a composite for one of its writes** — use the 1:1 wrapper.
- **Do not blind-retry a 409**; do not treat a 200-where-you-expected-201 as an
  error.
- **Do not perform a destructive write without operator confirmation**
  (`core/write-safety.md`).
- **Do not retry 401 with a different token** (config error; `core/mcp-token.md`).
- **Do not work around a 403.** On a governance tool, a canonical node write, a
  node create or `rosetta_ingest_content` it means this key isn't a platform
  admin: surface that and stop (§7.1).
- **Do not commit an ingest bundle to production** (`rosetta_ingest_content`
  without `dryRun: true`) before the DT-RX-036 switch (§8).

## Consumed by

- `skills/agent-orchestration/SKILL.md`
- `skills/context-retrieval-flows/SKILL.md`
- `skills/conversation-lifecycle/SKILL.md`
- `skills/decision-trace-capture/SKILL.md`
- `skills/engagement-twin/SKILL.md`
- `skills/governance-lifecycle/SKILL.md`
- `skills/engagement-lifecycle/SKILL.md`
- `skills/engagement-canvas/SKILL.md`
