# Connect an AI assistant

> Give an AI assistant read and authoring access to your Hodoflow organizations over MCP, using a personal access token you create and rotate yourself.

Hodoflow speaks the Model Context Protocol at `POST /api/mcp`. Connect an
assistant — Claude Desktop, Claude Code, an IDE agent — and it can answer
questions about your records, your workflows, and the product itself, and build
the pipeline that produces them, using tools instead of guesswork.

The surface **reads and it authors**. An assistant can create a data model, save
a workflow, activate it, and build the dashboard that reads the result — the
same work the builder does, done over tools. What it never does is destroy:
nothing here deletes a workflow, a data model, a dashboard, or a widget.

Twenty-eight tools, eighteen of which only read. A read tool opens its
description with "Read-only." and is served with a `readOnlyHint` annotation; a
write tool does neither. A client that asks for approval before a tool runs
therefore asks for exactly the ten that change something.

## Authenticate with a personal access token

Unlike the other API surfaces, MCP does not use a service account. It uses a
**personal access token**, which belongs to you rather than to one workspace:

| | Service account | Personal access token |
|---|---|---|
| Belongs to | One workspace | One person |
| Reaches | That workspace | Every organization you are an active member of |
| Acts as | Its run-as member | You |
| Created at | Settings → Service Accounts | Your profile |

One token is enough no matter how many organizations you work in, which is why
an assistant is configured once rather than once per organization.

### Mint a token

1. Open your profile from the account menu.
2. Under **Personal access token**, select **Create token**.
3. Copy the value immediately.

The token starts with `pat_` and is shown once; there is no way to read it back
later. You hold one at a time, and it does not expire on a schedule — a
credential that dies on its own only ever dies at an inconvenient moment, and an
expiry date does nothing to protect a token that has already leaked.

Two controls replace it:

| Control | Effect |
|---|---|
| **Rotate** | Issues a new token and kills the old value in the same moment. |
| **Expire** | Kills the token and leaves you with none until you create another. |

Neither has a grace period, which is the point: after rotating, the leaked value
is dead with nothing left to clean up.

### What the token consents to

A token does whatever its owner can do, in every organization its owner belongs
to. It sees the organizations you are an active member of, and it stops seeing
an organization the moment your membership or that organization is deactivated.

There is no scope to narrow it with, and authoring is not gated on role:
Hodoflow lets any active member build and run pipelines, so a token can create,
change, and activate workflows in every organization you belong to, exactly as
you can in the application. Hold it as you hold your password, and expire it
when you are done with the tool that has it.

The step-by-step version for people who are not integrating anything lives in
the Help Center under Credentials & connections → Personal access tokens.

## Connect the assistant

```bash
claude mcp add --transport http hodoflow https://hodoflow.com/api/mcp \
  --header "Authorization: Bearer pat_..."
```

Other clients take the same three pieces: the URL `https://hodoflow.com/api/mcp`,
the streamable HTTP transport, and an `Authorization: Bearer` header carrying the
token. A self-hosted deployment substitutes its own application origin.

Every rejected request — missing token, mistyped token, rotated-away token,
expired token — answers `401` with the same body, so a failing connection means
"check the token", not "the token exists but something else is wrong".

## Ask for an organization first

An assistant starts with no organization selected, because the token reaches
several. The first call is always discovery:

1. `list_my_organizations` returns each organization with a `domain`.
2. Every tool that touches your data takes that value as its `organization`
   argument.
3. Tools also accept an optional `workspace` argument — a `slug` from
   `list_workspaces`. Leave it out and the organization's default workspace is
   used, the same one the application opens on.

Naming an organization you are not a member of returns an error that points back
at `list_my_organizations`. It never returns another organization's data, and it
never reveals whether that organization exists.

## What an assistant can read

### Discovery

| Tool | Returns |
|---|---|
| `list_my_organizations` | The organizations you can work in: `domain`, name, and your role |
| `list_workspaces` | The workspaces in one organization: `slug`, name, and which is the default |

### Records

| Tool | Returns |
|---|---|
| `list_data_models` | The data models in a workspace, each with its JSON Schema |
| `list_data_model_fields` | One data model's fields — the columns you can filter, sort, and aggregate by |
| `query_records` | Records for one data model: the latest state per entity, or the full timeline |
| `aggregate_records` | One number, one bucket per value, or one point per time bucket, without returning the records themselves |

`query_records` takes a `data_model_slug` plus an optional time window, filters,
sort, and paging. It returns at most 250 records per call along with a total
`count`, so an assistant can page rather than guess. Filters name a field of that
data model; anything else is rejected.

`aggregate_records` answers "how many orders per status this month" in one call.
It is also what to run before saving a widget, since a widget draws the same
aggregation.

### Workflows

| Tool | Returns |
|---|---|
| `list_workflows` | Workflow versions: name, version number, type, and whether the version is active |
| `get_workflow` | One version's whole authored graph: its steps with their configuration and dependencies, its triggers, and the field mappings of any step that writes to a data model |
| `list_workflow_runs` | Runs, newest first, with state, timing, and any error message |
| `get_run_steps` | How each step of one run turned out: status, attempt, timing, error |
| `list_triggers` | What starts each workflow, and whether its version is live |

`get_run_steps` is the tool that answers "why did this run fail?". It reports
outcomes only — the data flowing through a step is never returned.

`get_workflow` is also the first half of every edit, because saving replaces the
whole graph. It returns a webhook trigger's ingest URL, the one credential-shaped
value any tool returns: a webhook nobody can deliver to is not authored, and the
workflow screen shows the same value to the same readers. No other secret is
returned by any tool, `list_triggers` included.

### Integrations

| Tool | Returns |
|---|---|
| `list_integrations` | Connectors, service accounts, and imported API specs in one call — the ids and names authoring needs |
| `get_api_spec` | One imported spec's operations, so a step can be bound to one by `operation_ref` |

Stored credentials, client secrets, and the uploaded spec document itself are
never returned.

### Dashboards

| Tool | Returns |
|---|---|
| `list_dashboards` | The dashboards in a workspace |
| `list_widgets` | The charts, tables, and stat cards a workspace has |

Sharing links are never returned by either tool.

### Authoring reference and documentation

| Tool | Returns |
|---|---|
| `describe_authoring_schema` | The vocabulary for authoring: every step and trigger type with its configuration keys, the shape of a workflow save, data model field types, the formula language, and the widget and aggregation vocabulary |
| `search_docs` | The best-matching Help Center and developer pages for a question |
| `read_doc` | One documentation page in full, as markdown |

These three take no organization: they serve content compiled into the release,
the same for everyone. `search_docs` and `read_doc` let an assistant answer "how
do I do X in Hodoflow?" from the current documentation rather than from memory.

`describe_authoring_schema` is the one to call before writing anything. None of
the authoring vocabulary is guessable, and it is deliberately kept out of the
other tools' descriptions so that it costs one call rather than riding in every
request.

## What an assistant can change

Eleven tools write, and every one of them is additive: they create records or
change the ones you name, and nothing else moves.

### Records

| Tool | Does |
|---|---|
| `create_data_model` | Creates a data model and its fields in one transaction. A name or identifier already in use is refused rather than overwritten |
| `update_data_model` | Renames a model, rewrites its description, or adds fields. Existing fields, and the records already written, are untouched |

The identifier other tools address a model by is fixed once the model exists, and
a field a workflow may be writing to cannot be renamed or removed. How long
records are kept is not changeable here at all: shortening retention deletes
records, so it stays a decision made in the app.

### Workflows

| Tool | Does |
|---|---|
| `save_workflow` | Replaces one version's whole graph — a step or trigger left out of the document is deleted from it |
| `activate_workflow` | Puts a version into service |
| `deactivate_workflow` | Takes a version out of service |
| `test_step` | Issues the request an `http_request` step is configured to make, once, and reports the fields it answered with |

Because the save is a whole-graph replace, an edit starts by reading the version
with `get_workflow` and sending it back changed. Saving over a version that has
been **published** never edits it: the save forks a new version and leaves the
published one running untouched. Each save is one transaction, so a document the
server refuses changes nothing — correct it and send it again.

`test_step` is how an assistant learns a source's field names before authoring a
step that reads them. Nothing is saved and no workflow needs to exist yet.

### Dashboards

| Tool | Does |
|---|---|
| `save_dashboard` | Creates a dashboard, or renames and re-describes one that exists |
| `save_widget` | Creates a chart, table, or stat card, or rewrites one that exists |
| `place_widget` | Puts an existing widget on a dashboard, or moves one already there |

A widget belongs to the workspace rather than to one dashboard. The same widget
can sit on several, `save_widget` changes it everywhere it is shown, and
`place_widget` places rather than copies.

### Integrations

| Tool | Does |
|---|---|
| `create_service_account` | Creates the service account a webhook trigger names as the sender it accepts, and answers with its id |
| `import_api_spec` | Stores an OpenAPI or Swagger document so a step can be bound to one of its operations |

The document is sent as text; nothing is fetched from a URL. Importing the same
document twice stores a second copy rather than replacing the first.

`create_service_account` is the one place this surface asks for a **role**
rather than membership: creating one is an owner or admin act, and a member is
refused. The account is created with the `webhooks` permission and no other, and
it runs as the caller — widening its permissions, changing whose permissions it
borrows, and every other change to it stay in the app.

Its secret is not in the answer and cannot be recovered from this surface. The
answer carries the id, which is all a trigger needs; the secret is revealed once
under **Settings → Service Accounts**, and whoever configures the calling system
copies it from there.

## What activation and running mean

Activation is where authoring becomes production. `activate_workflow` opens the
version's triggers to deliveries and starts its schedules,
and deactivates whichever version of the same workflow was live. A first
activation marks the version published, after which saving over it forks rather
than edits. `deactivate_workflow` reverses all of that and leaves runs already
under way to finish.

Activation is refused, with the reason, when the graph is not ready to run: a
source schema has gone stale, the workflow has no trigger, a trigger reaches no
step, a step is not fully configured, a step that writes to a data model has no
mappings, the steps form a cycle, or a step is unreachable.

Two consequences leave the deployment, and are worth stating plainly:

- **`test_step` really makes the request.** The third party sees it, and
  anything it does in response is done. Only the shape of the answer comes
  back — how many records it held and the typed field paths of one of them —
  never the response body. Requests to private or internal addresses are
  refused.
- **What an assistant builds counts against the plan.** Data models, fields,
  workflows, active workflows, dashboards, widgets, and API specs all draw on
  the organization's allowances, as do the runs an activated workflow performs
  and the records and storage they produce. A call that would exceed an
  allowance is refused, naming what was exceeded.

## What stays in the app

Deliberately absent from the tool surface, and reachable in the application by
someone with the right role:

- **Deleting anything.** No tool removes a workflow, data model, field,
  dashboard, or widget, or takes a widget off a dashboard.
- **Publishing and sharing.** Public dashboard embeds and share links are
  neither created nor returned.
- **Retention.** Shortening how long records are kept deletes records.
- **People and credentials.** Invitations, memberships, roles, service accounts,
  connector secrets, and billing.

## Rate limits

MCP requests are counted per person, against the platform default of 300
requests per 60 seconds — see [Rate limits](/platform/rate-limits). Reads and
writes count alike. The organization's own API entitlement does not apply here,
because one token spans several organizations and no organization is chosen
until the individual tool call; the organization's plan limits still apply to
everything a write tool creates.

Exceeding the window returns `429` with a `Retry-After` header, in the same
error shape as the rest of the API.

## What an assistant cannot reach

- Anything outside your own memberships.
- Anything in an organization or workspace you did not name in the call.
- Secret material, in either direction. Nothing returns a webhook secret, a
  sharing link, a service-account secret, or another person's token — and no
  tool accepts one as an argument, which is why connectors are created in the
  app: making one means supplying an API key, and an argument cannot be
  withheld the way an answer can. An assistant may create the *reference* and
  never handles the secret behind it: `create_service_account` mints the
  account and answers with its id, and the secret is revealed only here. The
  single exception in the other direction is a webhook trigger's own ingest
  URL, returned by `get_workflow` to readers who already see it on the workflow
  screen.
- The contents of a workflow step's data.
- Anything destructive: deletion, publishing, retention, and administration all
  stay in the app.
