The connector serves a [Model Context Protocol](https://modelcontextprotocol.io) endpoint at
`/mcp`. An AI agent (Claude Code, Claude Desktop, Cursor, omp, ...) connects to it and can do
what you do in the dashboard: look at the connector's health, build and edit the flow, dry-run
every source, deploy, watch live data, and check printers, push delivery and the license. It is
meant for **commissioning**: build and verify a configuration with an agent, then ship it to the
client.

It is part of the default build (the `mcp` feature) and uses the same HTTP listener as the API
(`bind_addr:health_check_port`, default `127.0.0.1:3000`).

## Enable, disable, restrict

```yaml
connector:
  mcp:
enabled: true      # default; false removes /mcp entirely
read_only: false   # true hides every tool that changes anything
```

- `enabled: false` makes `/mcp` answer 404. A build without the `mcp` feature has no endpoint.
- `read_only: true` removes the mutating tools from the tool list (and refuses calls to them).
  The agent can still inspect, validate scripts and flows, and dry-run `test_flow` with a flow it
  passes in. Use it on a client's production connector.

## Connecting

`/mcp` is behind the **same credentials and Host/Origin checks as `/api/*`**
(see [Security](/docs/security)); the legacy `api_token` (admin) or a named API token opens both. Find the legacy
token in `connector.api_token`, the `OPEX_API_TOKEN` environment variable, or `<data_dir>/api_token` when it was
generated. The transport is MCP *streamable HTTP*: send `Authorization: Bearer <token>` on every request.

**Claude Code**

```bash
claude mcp add --transport http opex-flow http://127.0.0.1:3000/mcp \
  --header "Authorization: Bearer $OPEX_API_TOKEN"
```

**Cursor** (`~/.cursor/mcp.json`)

```json
{
  "mcpServers": {
"opex-flow": {
  "url": "http://127.0.0.1:3000/mcp",
  "headers": { "Authorization": "Bearer <token>" }
}
  }
}
```

**omp** (`~/.omp/agent/mcp.json`, or `.omp/mcp.json` in a project)

```json
{
  "mcpServers": {
"opex-flow": {
  "type": "http",
  "url": "http://127.0.0.1:3000/mcp",
  "headers": { "Authorization": "Bearer <token>" }
}
  }
}
```

**Claude Desktop** (`claude_desktop_config.json`; it reaches remote servers through the
`mcp-remote` bridge)

```json
{
  "mcpServers": {
"opex-flow": {
  "command": "npx",
  "args": ["-y", "mcp-remote", "http://127.0.0.1:3000/mcp",
           "--header", "Authorization:${OPEX_AUTH}"],
  "env": { "OPEX_AUTH": "Bearer <token>" }
}
  }
}
```

A connector on another machine: use its address in the URL. With the default loopback bind, open an
SSH tunnel (`ssh -L 3000:127.0.0.1:3000 user@host`) rather than exposing the port; see
[Deployment](/docs/deployment).

## How an agent works with it

1. `get_status`: is the connector healthy, adapters connected, flow warnings, `unrouted` count?
2. `get_flow {summary: true}`, `get_help`, `read_docs`: learn what exists and what fields a node needs.
3. Stage changes with `upsert_nodes`, `connect`, `remove_nodes`, `create_flow_tab`. These edit a private
   **draft** that belongs to the MCP session; **nothing is live** until you deploy it. Each reply shows
   the draft's diff against the live flow and a validation summary.
4. `validate_flow` (the draft, else the live flow), then `test_flow` once per source: a **dry run** that
   really executes the scripts but records sink deliveries, HTTP calls and printer jobs instead of
   performing them.
5. `deploy_draft`: validates and compiles every source, saves `flow.json` and swaps all pipelines in one
   step. It is refused with a version conflict if the live flow changed since the draft was started
   (`discard_draft` and redo).
6. Verify with `get_node_stats` (counters per node), `watch_debug` (what nodes saw, for up to 30 s) or
   `send_ingest ... capture_debug: true` (one real record with its per-node events).

Node positions (`x`, `y`) can be omitted: they are laid out when the flow is deployed.

The server sends the agent this workflow as its instructions, and offers a prompt,
`commission_checklist`, that walks a pre-client-deploy verification: sources connected, every source
routed, no warnings, a dry run per source, printers reachable and a confirmed test print, push
delivering, license valid, data directory backed up.

## Tools

Every tool has a description written for the model, a JSON-schema input, and MCP annotations
(`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`). Failures come back as tool
errors with the reason (a rejected deploy includes the validation findings). Results over about
48 KB are shortened and carry `truncated: true`.

| Group | Tools | Changes things? |
| --- | --- | --- |
| Overview | `get_status`, `get_config` (secrets redacted), `list_secrets` (names only, admin), `get_help`, `read_docs` | no |
| Sources | `list_sources`, `last_record` | no |
| | `add_source`, `remove_source` | yes (persisted) |
| | `inject_source` | **real run** |
| Flow | `get_flow`, `validate_flow`, `test_flow` (dry run), `get_node_stats`, `watch_debug` | no |
| | `deploy_flow` (full document + `if_version`) | **replaces the live flow** |
| Draft | `get_draft` | no |
| | `upsert_nodes`, `remove_nodes`, `connect`, `disconnect`, `create_flow_tab`, `discard_draft` | draft only |
| | `deploy_draft` | **replaces the live flow** |
| Ingest | `send_ingest` | **real run**, may print |
| Printers | `list_printers`, `get_printer`, `list_printer_lines`, `printer_jobs`, `printer_status` | no (`printer_status` asks the device) |
| | `upsert_printer`, `remove_printer`, `import_printers` (`dry_run` defaults to true) | yes (persisted) |
| | `run_printer_job` | **physically prints** |
| Monitoring | `get_sla_health` (per flow / per external system, SLA state), `get_monthly_report` (json, html or csv), `siem_status`, `get_published_openapi` (OpenAPI 3.1 of every exposed interface; filter with `path_prefix` / `tag`) | no |
| Diagnostics | `list_traces`, `get_trace`, `pipeline_dead_letters`, `get_dead_letter`, `dead_letter_audit`, `push_dead_letters` | no |
| | `edit_dead_letter` | yes (stores a correction beside the original; audited) |
| | `replay_dead_letter`, `replay_dead_letters` | **re-runs records through the flow** (real posts / prints; audited) |
| | `discard_dead_letter`, `discard_dead_letters` | yes (the record is not processed; audited) |
| | `replay_push_dead` | **re-sends to the cloud** |
| Reconciliation | `list_reconcile_reports`, `get_reconcile_report` (`csv: true` for the CSV text), `ledger_tally` | no |
| Exception cases | `list_cases`, `get_case`, `case_summary` | no |
| | `assign_case`, `comment_case`, `set_case_status`, `link_case`, `bulk_update_cases` | yes (case history and audit log; see [Finance controls](/docs/finance-controls)) |
| Approvals | `list_approvals`, `get_approval`, `posting_status` | no |
| | `approve_approval`, `reject_approval`, `resume_approval` | **releases or stops a parked record** (needs the approve node's role; audited) |
| | `request_go_live`, `approve_go_live`, `reject_go_live` | **`approve_go_live` switches a posting node to live** (second approver only; audited) |
| Events | `list_event_types`, `get_event_type`, `validate_event_record`, `export_asyncapi`, `event_log` | no |
| | `upsert_event_type` | yes (writes `events/<name>.json`) |
| Lookups | `list_lookup_tables`, `get_lookup_entries`, `resolve_lookup` | no |
| | `upsert_lookup_table`, `upsert_lookup_entries`, `delete_lookup_entries`, `import_lookup_csv` (`dry_run` defaults to true) | yes (persisted, applies to running flows at once) |
| Apps | `list_apps`, `get_app` | no |
| | `upsert_app` (validated; 422 with `errors:[{path,message}]`), `publish_app` | yes (changes the draft / the app users run; audited; see [Apps](/docs/apps)) |
| Versions | `list_versions`, `get_version`, `diff_versions` | no |
| | `rollback` (redeploys version N as a new version; allowed in a protected environment; audited `flow.rollback`) | yes |
| License | `license_status` | no |
| Scripts | `validate_script` (JS or DSL) | no |

Tools are the same code as the matching REST endpoints ([REST API](/docs/rest-api)): `get_flow` is
`GET /api/flow`, `deploy_flow` is `PUT /api/flow`, `send_ingest` is `POST /api/ingest/{source}/{id}`
and so on, so behaviour, validation and limits cannot differ.

### Resources and prompts

| URI / name | Content |
| --- | --- |
| `opexflow://flow` | The deployed flow document (JSON). |
| `opexflow://docs/<page>` | Any page of this documentation as markdown (`opexflow://docs/printers`, ...). |
| prompt `commission_checklist` | The pre-client-deploy checklist. |

`get_help` serves the same text as the dashboard's help cards: one registry,
`apps/ui/src/help/registry.json`, embedded in the binary.

## Safety

- **Give each agent its own named token with the least role it needs.** The legacy `api_token` is an
  admin: as powerful as the dashboard's administrator. Create `opx_...` tokens in **Admin → Access**
  (see [Security](/docs/security#named-api-tokens)): a `viewer` token can read everything and stage drafts; an `operator`
  token can also replay dead letters, inject and print; an `editor` token can deploy and edit printers, sources,
  the catalogue and lookups; `admin` is only for `get_config` and `list_secrets`. Keep the default loopback bind,
  or put the connector behind a firewall or reverse proxy; do not expose `/mcp` to the internet.
- **Roles are enforced per tool** from the same table as the REST routes: `tools/list` only offers the tools the
  token's role may call, a call above the role is refused (`required_role` in the error), and every state-changing
  call (deploys, sources, printers, dead letters, catalogue, lookups) is written to the
  [audit log](/docs/security#audit-log) under the token's name with `via: mcp`. Dead-letter audit trails name the
  token too; the old `actor` tool argument no longer exists.
- **Destructive tools act for real**: `deploy_flow`, `deploy_draft`, `send_ingest`, `inject_source`,
  `run_printer_job`, `upsert_printer`, `remove_printer`, `import_printers` (with `dry_run: false`),
  `remove_source`, `replay_push_dead`, `replay_dead_letter`, `replay_dead_letters`, `discard_dead_letter`,
  `discard_dead_letters`. Their annotations say so; let your client ask for approval
  rather than auto-approving them, and prefer `test_flow` over `send_ingest`.
- **A client's production connector**: set `connector.mcp.read_only: true`, or `enabled: false`.
- Drafts live in memory per MCP session and expire after 8 hours idle; a restart forgets them. The
  deployed flow is always the saved `flow.json`. A client that uses the sessionless protocol revision
  has no session id and shares one draft with every other such client; `deploy_draft` still refuses a
  stale draft, so two agents cannot silently overwrite each other.
- Nothing secret is returned: `get_config` redacts, `license_status` shows only the last four
  characters of the key.
- Calls are logged at info level (`mcp tool call`, tool name, outcome, duration; not arguments).