If you live in an editor, you can drive most of Catalio from there. Catalio exposes its domain actions as MCP tools, so an MCP client (Claude Code, Claude Desktop, Cursor, or your own) can start a scan, review what it found, and enrich the specification without you switching to the browser.
This guide walks the end-to-end loop and is explicit about the places where you still need the UI.
What the loop looks like
Six steps, all reachable as tools:
- Point at an application and start a scan.
- Poll until the scan finishes.
- List the candidates it drafted.
- Confirm or dismiss each candidate.
- Enrich the requirements the scan produced or touched.
- Resolve change proposals — apply, dismiss, or restore.
Step 1: Start a scan
start_extract_scan takes an application_id and an optional stage (defaults to capabilities). It wraps the same enqueuer the web UI uses, so a colleague watching the application in the browser sees your scan appear live.
start_extract_scan(application_id: "…", stage: "capabilities")
→ %{status: :enqueued, application_id: "…", stage: "capabilities"}
Two things worth knowing:
- A second call inside the dedupe window returns
:already_running, not a new job. This is a distinct status rather than a silent success, so your script can tell “I started it” from “it was already going.” component_idsdefaults to the application’s source components, capped. If you want a narrower scan, pass the ids explicitly. An application with no source components is rejected rather than enqueuing a scan that would find nothing.
Stages without a backing scan worker (validation, rules) are rejected with a specific error naming the stage. That is deliberate: silently substituting the capability worker would give you results from a scan you did not ask for.
Step 2: Poll for completion
get_extract_status returns per-stage counts and which stage, if any, is currently running.
get_extract_status(application_id: "…")
→ %{counts: %{"capabilities" => 12}, running_stage: nil}
Poll until running_stage is nil. Note that a failed scan also ends with running_stage: nil — there is no pollable error field today, so a scan that dies mid-run looks like one that finished with no new candidates. If you are automating this, treat “no count change and no running stage” as inconclusive rather than as success, and check the application in the UI.
Step 3: List what it found
list_extract_candidates reads the candidates for a stage, filtered by status (detected, confirmed, or dismissed; defaults to detected).
list_extract_candidates(application_id: "…", stage: "capabilities", status: "detected")
→ [%{id: "…", name: "Invoice Approval", confidence: 0.82, …}, …]
Step 4: Review each candidate
Three tools cover the review decision:
| Tool | Effect |
|---|---|
confirm_extract_candidate |
Marks the candidate confirmed and links it to an org-level capability |
dismiss_extract_candidate |
Marks it dismissed, optionally recording a reason |
undismiss_extract_candidate |
Returns a dismissed candidate to detected and clears the reason |
confirm_extract_candidate requires a capability_id. A confirmed candidate has to point at a capability in your organization’s catalog, so create or find that capability first (list_capabilities / create_capability), then pass its id.
The dismiss reason is not just bookkeeping. It feeds back into the next scan so the detector stops re-suggesting the same thing. Recording why you rejected something is worth the extra argument.
Dismissing a candidate you already confirmed is rejected — unconfirm it first.
The confirm boundary: MCP confirm is the light path
This is the one place where the MCP surface is genuinely narrower than the UI, and it is worth being precise about.
In the web app, approving a detected capability runs a promotion flow: it can mint the org-level capability for you, carry the candidate’s description and evidence across, and wire up the links in one action. Over MCP, confirm_extract_candidate does the status flip and the link only. It does not promote.
So the MCP sequence is: create or pick the capability yourself, then confirm the candidate against it. Same end state, more steps, and you own the capability’s attributes rather than inheriting them from the candidate. If you want promotion-on-approve behavior, use the UI for that step.
Step 5: Enrich requirements
Standard authoring tools apply — list_requirements, list_requirements_filtered, get_requirement, create_requirement, update_requirement — plus a link/unlink pair for each association: link_requirement_to_policy, link_requirement_to_process, link_requirement_to_persona, and link_requirement_to_component, each with a matching unlink_requirement_from_*.
This part of the surface is essentially complete. If you can do it in the requirements UI, you can do it here.
Step 6: Resolve change proposals
Change proposals are Catalio’s suggested edits to your specification. The full lifecycle is now reachable:
| Tool | Effect |
|---|---|
list_change_proposals |
Read an initiative’s proposals |
apply_change_proposal |
Accept it and write the change through |
dismiss_change_proposal |
Reject it, optionally with a reason and free-text feedback |
restore_change_proposal |
Undo a dismissal, putting it back in the queue |
delete_change_proposal |
Remove it entirely |
dismiss_change_proposal accepts a dismissal_reason (a fixed set — not_relevant, inaccurate, duplicate, and others) and a dismissal_feedback string. Both are optional; dismissing with neither still works.
Restore clears the reason and the feedback along with the dismissal, because once a proposal is back in the queue the old rejection no longer describes it. An already-applied proposal cannot be dismissed or restored.
What stays in the UI
Two parts of the workflow are UI-first by design rather than oversight, and one is reachable only in a narrower form:
Document upload. Uploading a PDF, spec, or export means multipart file transfer, which MCP does not carry. Upload documents in the web app; everything downstream of the upload is reachable over MCP.
Interactive discovery sessions. Discovery is a guided conversation — Catalio asks follow-up questions and adapts based on your answers, with the session state driving the UI. It is led by the interface rather than by a sequence of tool calls, so there is no useful way to script it. Run discovery in the app, then work the resulting requirements from your editor.
Capability promotion, as covered above, is a third partial: reachable in a narrower form, full behavior in the UI only.
Authorization
Every tool runs as you, scoped to your organization. Catalio’s resources are tenant-isolated at the data layer, so the record a tool acts on is invisible if it belongs to another organization: the call fails as not-found rather than as a permission error. There is no way to reach across organizations by guessing an id, and no partial write happens on a rejected call.
Identifiers you pass inside a call are a separate check. confirm_extract_candidate takes a capability_id naming the capability to link the candidate to, and tenant isolation on the candidate says nothing about where that second id points. It is validated explicitly: a capability_id from another organization fails as a validation error against that field, not as not-found, and it fails before any write. The distinction matters when you are reading errors programmatically — a missing candidate and a foreign capability_id surface differently.
Your own permissions still apply within your organization: the tools go through the same policies as the UI.