> ## Documentation Index
> Fetch the complete documentation index at: https://docs.anyformat.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Agents over MCP

> A real end-to-end session against the anyformat MCP server: stage a local file, build a workflow, run it, and read the results. Every payload comes from a live run.

This guide walks one real session against the [MCP server](/api-reference-v3/mcp). An agent takes an invoice PDF that exists only on the local machine, builds a parse → extract → validate workflow, runs the document through it, and reads the structured result. Every request and response below comes from a live production run, with identifiers shortened. The [MCP server reference](/api-reference-v3/mcp) covers connection setup and the full tool table. This page is the flow.

The whole session is five tool calls:

1. `stage_files` turns a local file into a reference the API accepts.
2. `create_workflow` builds the typed graph.
3. `upload_documents` puts the file into the workflow.
4. `run_document_packet` starts the extraction, retry-safe.
5. `get_run` waits for the result and reads it.

## 1. Stage a local file

An MCP tool call carries JSON, not bytes, so no tool takes a file directly. `stage_files` closes the gap: it returns an upload form for the bytes, plus a short `object_id` to refer to them by afterwards.

```json theme={null}
// stage_files
{ "body": { "files": [
  { "filename": "single_invoice.pdf", "declared_size": 2087, "content_type": "application/pdf" }
] } }
```

The response carries one slot per file:

```json theme={null}
{ "slots": [ {
  "object_id": "06a953d9792572568000f838b051ca9b",
  "upload": { "url": "https://s3.eu-west-1.amazonaws.com/...", "fields": { "...": "..." } },
  "read_url": "https://s3.eu-west-1.amazonaws.com/...?X-Amz-...",
  "read_url_expires_in_seconds": 900
} ] }
```

The agent POSTs the bytes itself as multipart/form-data: every `upload.fields` entry verbatim, then the file.

```bash theme={null}
curl -F 'Content-Type=application/pdf' -F 'key=...' -F '...' \
  -F file=@single_invoice.pdf https://s3.eu-west-1.amazonaws.com/...
# → 204
```

From here on the file is the pair `{ "object_id": "06a953d9…", "filename": "single_invoice.pdf" }`. Prefer the id over `read_url` everywhere inside the API. The id is a few characters where the presigned URL is kilobytes, and it does not expire mid-conversation. `read_url` exists for handing the bytes to something *outside* anyformat.

<Note>
  Staged bytes are scratch. They belong to no workflow, and storage deletes them within a day. Stage, then consume promptly. A file that must live on goes into a workflow, in step 3.
</Note>

## 2. Create the workflow

`create_workflow` takes the same typed graph as [`POST /v3/workflows/`](/api-reference-v3/workflows/create): exactly one `parse` node, an `extract` node with the field schema, and here a `validate` node with two deterministic rules. A deterministic rule is a check that runs in code, instantly and for free.

```json theme={null}
{ "body": {
  "name": "Invoice extraction",
  "nodes": [
    { "id": "parse", "type": "parse", "mode": "standard" },
    { "id": "extract", "type": "extract", "mode": "standard", "extraction_schema": { "fields": [
      { "name": "invoice_number", "data_type": "string", "description": "Invoice number as printed on the document." },
      { "name": "invoice_date", "data_type": "date", "description": "Date the invoice was issued." },
      { "name": "total_amount", "data_type": "float", "description": "Invoice total in the document's currency." },
      { "name": "line_items", "data_type": "object", "description": "One row per invoice line item.", "nested_fields": [
        { "name": "description", "data_type": "string", "description": "Line item description." },
        { "name": "amount", "data_type": "float", "description": "Line item amount." }
      ] }
    ] } },
    { "id": "validate", "type": "validate", "rules": [
      { "id": "invoice-number-present", "kind": "deterministic", "check": { "type": "required", "field": "invoice_number" } },
      { "id": "total-positive", "kind": "deterministic", "check": { "type": "comparison", "left": "total_amount", "op": ">", "right": { "source": "literal", "value": 0 } } }
    ] }
  ],
  "edges": [
    { "source": "parse", "target": "extract" },
    { "source": "extract", "target": "validate" }
  ]
} }
```

The response is the stored workflow, and every field now carries a server-assigned `persistent_id`:

```json theme={null}
{ "id": "06a953dd-84bf-7267-8000-f043cef61fdd",
  "nodes": [ { "id": "extract", "type": "extract", "extraction_schema": { "fields": [
    { "persistent_id": "66bf650f1cc84788a7ec1a4500054dff", "name": "invoice_number", "data_type": "string", "..." : "..." }
  ] } } ] }
```

Keep those ids. When you later edit the workflow, **echo each existing field's `persistent_id` unchanged, including across renames**. That edit loop is `get_workflow`, change the graph, `update_workflow`. The id is the field's identity. It keeps quality metrics, ground truth and analytics attached. A field sent without one is always treated as new, and silently detaches all of that.

If the graph is invalid, the error names every broken rule. This is a real rejection of a graph with no parse node:

```json theme={null}
{ "error_code": "TOPOLOGY_INVALID", "status": 400, "retryable": false,
  "detail": { "violations": [
    { "rule": "exactly_one_parse_node", "message": "workflow must contain exactly one `parse` node, found 0", "node_ids": [] },
    { "rule": "every_non_parse_node_has_inbound_edge", "message": "node 'extract' (extract) has no inbound edge", "node_ids": ["extract"] }
  ] },
  "request_id": "6a953e3a…" }
```

Every failed tool call has this envelope: branch on `error_code`, and quote `request_id` in a support request.

## 3. Upload the document

`upload_documents` puts files into the workflow as one document packet. It accepts fetchable HTTPS `urls`, staged `staged_files`, or a mix. The staged file from step 1 goes in by id:

```json theme={null}
{ "workflow_id": "06a953dd-84bf-7267-8000-f043cef61fdd",
  "body": { "staged_files": [
    { "object_id": "06a953d9792572568000f838b051ca9b", "filename": "single_invoice.pdf" }
  ] } }
```

```json theme={null}
{ "document_packet_id": "06a953de-ab9d-738f-8000-2d176673ece4",
  "files": [ { "id": "06a953de-a873-…", "name": "single_invoice.pdf" } ] }
```

The import is atomic: any fetch failure means nothing is persisted. Uploading runs nothing. That separation makes the run step retry-safe.

## 4. Run it, retry-safe

```json theme={null}
{ "document_packet_id": "06a953de-ab9d-738f-8000-2d176673ece4",
  "idempotency_key": "invoice-demo-2026-08-31-run1" }
```

```json theme={null}
{ "run_id": "06a953df-2425-72c5-8000-c2bf4abac999", "status": "queued" }
```

Every call **without** a key starts a new billed run. That is the re-run affordance after you edit the workflow. With a key, a retried call replays the original run instead of billing a second extraction. The replay reports the run's current status. Retry after the run finished and the answer is the same `run_id` with `status: "processed"`, telling you the result is already there.

## 5. Read the result

`get_run` long-polls server-side, so one call waits instead of the agent polling. `parse_output: "none"` drops the parsed markdown from the response when only the extracted fields matter. On a real document the parse section is about 90% of the payload.

```json theme={null}
{ "run_id": "06a953df-2425-72c5-8000-c2bf4abac999", "wait_seconds": 60, "parse_output": "none" }
```

Fifty seconds later, the run is `processed` and `results.extractions[0]` carries every field with its confidence and the evidence it was read from:

```json theme={null}
{ "fields": {
    "invoice_number": { "value": "INV-2026-9001", "confidence": 99,
      "evidence": [ { "text": "Invoice #: INV-2026-9001 Issue date: 2026-02-20 …", "page_number": 1 } ] },
    "total_amount": { "value": "4594.62", "confidence": 97,
      "evidence": [ { "text": "Subtotal: $4,176.93 Tax (10%): $417.69 **TOTAL: $4,594.62**", "page_number": 1 } ] },
    "line_items": [
      { "description": { "value": "Matrix Extensible Action-Items", "confidence": 98 },
        "amount": { "value": "1974.96", "confidence": 98 } }
    ]
  },
  "validations": [
    { "rule_id": "invoice-number-present", "status": "pass", "detail": "invoice_number is present." },
    { "rule_id": "total-positive", "status": "pass", "detail": "4594.62 > 0.0 is true.",
      "compared_values": { "total_amount": 4594.62, "0.0": 0 } }
  ] }
```

<Warning>
  Every extracted `value` is a JSON **string** on the wire, whatever the field's declared type. The `float` field above answers `"4594.62"`. Parse numbers before comparing or summing them. Confidence is an integer from 0 to 100.
</Warning>

## Editing the workflow later

The edit loop is fetch, edit, replace. `get_workflow` returns the `{name, description, nodes, edges}` shape that `update_workflow` accepts. Renaming `total_amount` to `grand_total` while echoing its `persistent_id` keeps the field's identity, so its metrics and history follow the rename:

```json theme={null}
{ "persistent_id": "c5cae54a0b144673951c49f098e443a3", "name": "grand_total", "data_type": "float" }
```

Runs always execute the latest version. Old versions stay readable for comparison and audit. Call `list_workflow_versions`, then `get_workflow(workflow_id, version=...)`.

## Quick parse, and what it is not

`parse_document` is the one-call shortcut when you need only a single document's markdown. Pass `{object_id, filename}`, or an HTTPS `url`, then call `get_run`. The markdown is at `results.parse.markdown`. The invoice above parsed in about 13 seconds.

It is a quick one-off, not a production pipeline:

* Every call starts a new billed run. There is no idempotency key on this path.
* It runs against an auto-provisioned system parse workflow, not one you manage.
* **Save the markdown yourself.** anyformat does not retain quick-parse output. Treat the run as ephemeral.

Anything recurring, retry-safe, or worth keeping belongs in a workflow: `upload_documents` → `run_document_packet` → `get_run`, as above.

## Asking questions across documents

A workflow with a [Knowledge node](/guides/nodes/knowledge) indexes every parsed document into a navigable corpus. `ask_knowledge` then answers questions about the content, with citations resolved to a page and region of the source PDF. Mint a `thread_id` with the prefix `kb-` to keep follow-ups in context. Every question is billed and can take a few minutes. `KNOWLEDGE_NOT_READY` means the index is still building. Retry.

## Related

* [MCP server reference](/api-reference-v3/mcp): endpoint, auth, client setup, the full tool table.
* [Node schemas](/api-reference-v3/node-schemas): every node type and field the typed graph accepts.
* [Coding assistant](/guides/coding-assistant): the same know-how as an installable skill package.
