This is the third part of the MCP server guide. Part 1 is <https://help.auditbadger.com/hc/auditbadger-help-center/articles/1775898378-mcp-server-connect-ai-assistants-to-humadroid> — tokens, scopes, delegation, and client setup. Part 2 is <https://help.auditbadger.com/hc/auditbadger-help-center/articles/1788254081-mcp-server-tools-review-flow-and-limits> — what the assistant can read and propose, and how a proposal becomes a change.

This page is the reference an assistant works against once it is doing real work: how a file reaches a proposal, what the server will not let it do too often, what each error code means, and how you hear that a proposal is waiting.

## **Attaching files**

An assistant can attach a screenshot or a PDF to evidence, or a SOC 2 report to a vendor assurance review. The flow is four steps:

1. **`request_upload_url`** — the assistant declares the filename, content type, byte size, and SHA-256 checksum. It gets back a `staged_upload_id`, a `source_ref`, and a **PUT** target on AuditBadger's own host: a `url`, the `method`, empty `fields`, and the `headers` to send. The capability is valid for **5 minutes**, so have the file ready before calling.

2. **PUT the bytes** to that URL as the raw request body, with the returned headers and nothing else. The capability is bound to the exact byte size, SHA-256, and content type declared, so the file that arrives is the file that was described; anything else is refused on the spot, with the same reasons `complete_staged_upload` uses.

3. **`complete_staged_upload`** — AuditBadger verifies the checksum, sniffs the real content type from the bytes rather than trusting the name, neutralizes the content, and seals it.

4. **Bind it** by passing `staged_upload_ids` on the proposal, together with the `source_ref` from step 1 in the item's `source_refs`. Which operation an upload may be attached to is decided then, against that operation's own scopes.

**Replacing a file.** On draft evidence, `evidence.update` takes `staged_upload_ids` to add files and `remove_attachment_ids` to delete files; send both in one proposal to replace a file. `operation_companion` for `evidence.update` lists the current files with their ids. Evidence that is submitted or verified does not change in place: propose a new version with `evidence.create` and `parent_evidence_id`. Automated evidence and historical versions do not change in place either, and they take no new version from an assistant: the collector versions automated evidence, and a historical version is replaced by the current one.

**The upload goes to AuditBadger, not to an object store.** The `url` is on the same host as the MCP endpoint, so an assistant that can reach these tools can reach it, and nothing extra needs allowlisting. It carries its own authority: no bearer, session, or extra header is needed, which is what lets a sandboxed assistant, or a person acting for one, send the bytes. If the assistant cannot send bytes itself, it can hand a person one command and continue once it has run:

```
curl -X PUT -H "Content-Type: application/pdf" --data-binary @report.pdf "<url>"
```

A `204` means the bytes are in place; call `complete_staged_upload` next. A `401` means the five-minute window passed or the URL was altered; request a new one. A `422` names the mismatch. A `503` is the object store; PUT again.

**Retrying `complete_staged_upload`.** When it answers `retryable: true` (`upload_storage_unavailable`, `upload_scan_unavailable`), call it again with the **same** `idempotency_key`. The first call reserved the upload for that key; a retry under a new key is refused as `upload_invalid` with reason `completion_reserved`. When it answers `upload_invalid`, the `reason` says which check failed and `guidance` what to do — for every reason but `completion_reserved` the staged upload is finished and the fix is a new `request_upload_url`; `next_action` says which:

* `object_missing` — nothing arrived at the upload URL. The PUT did not complete; upload again.

* `size_mismatch` / `checksum_mismatch` — the stored bytes are not the file that was declared. Declare the exact size and SHA-256 of the file you PUT.

* `content_type_mismatch` — the bytes are not the declared type, or the type is not allowed.

* `capability_expired` — the five-minute window passed, or the upload was already completed or bound. Replaying `request_upload_url` with the original `idempotency_key` after the window answers this too, rather than handing back the dead URL; request again under a new key.

* `completion_reserved` — a retry under a different `idempotency_key`; replay the original one instead.

* `object_version_missing` — the store returned no immutable version for the file. Upload again; if it recurs, report it with `send_feedback`.

**Provenance.** The `source_ref` from `request_upload_url` is the one reference AuditBadger verifies itself: its `content_sha256` is the checksum completion checks against the uploaded bytes, and binding requires that exact reference on the item. Pass it through unchanged.

**Allowed types**: PNG, JPEG, GIF, WebP, and PDF. Nothing else. Stricter than ordinary evidence upload on purpose: every extra format is another parser. SVG is excluded despite being an image, because it is a document format that executes script.

**Size**: 10 MB per file, up to 10 uploads and 50 MB per change set.

**Content neutralization** happens on every file:

* A **PDF** is refused if its bytes carry a name that brings active content — script, open or page actions, launch actions, embedded files, rich media, XFA forms — or encryption, which could hide any of those. You get `upload_active_content`. Send a flattened PDF.

* An **image** is decoded and written back in the same format with every metadata block dropped: EXIF, ICC, XMP, comments, and anything appended after the image data. A rotated photo stays upright because orientation is applied before it is stripped; an animated image keeps its first frame. If re-encoding pushes it past 10 MB you get `upload_neutralized_too_large`.

Uploads are not scanned for malware today. Neutralization is what stands in for it, and it cannot see inside a compressed PDF object stream.

**Scopes**: an upload capability needs at least one `<module>:propose` scope for a module with an operation that accepts attachments — evidence and vendor assurance. A vendors-only token can upload the report it attaches; a controls-only token cannot upload at all, and gets `token_scope_missing`.

## **Limits and errors**

### **Rate limits**

* **100 requests per minute per token.** The bucket is keyed by a digest of the credential, never the credential itself.

* **150 bearer-carrying requests per minute per IP address**, across all tokens. This is what stops someone rotating bearer values to earn a fresh budget per guess. A team behind one shared egress address can reach it without any single token exceeding its own limit.

* **20 requests per minute per IP for requests with no bearer header at all.**

* **An IP that presents 30 rejected bearers within 10 minutes is banned for 30 minutes.** A client still polling with a token you revoked can walk into this; fix the configuration and wait it out.

Exceeding a throttle returns `429` with a `Retry-After` header.

### **Authentication**

Every rejection is the same `401` with `agent_token_required`. It does not say whether the subdomain, the token, or the credential kind was wrong — deliberately, so the endpoint cannot be used to enumerate accounts. A valid REST API token lands here too, and gets the same `401`: it authenticates elsewhere but is not valid for this surface.

Check, in order: the subdomain in the URL, the token's expiry, whether it was revoked, whether your account still has API access enabled, and whether you are still an active member.

### **Maintenance pauses**

If we pause the ingress for incident response, every request answers `503 headless_compliance_ingress_paused` with `retryable: true`, a `Retry-After: 60` header, and `retry_after_seconds: 60` in the body. That tells a client to come back in a minute rather than re-authenticate or give up. Work already approved keeps applying.

### **Error codes**

Errors are stable codes with a `retryable` flag and a `next_action`, so an assistant can act on them rather than parse prose. A refusal about the request itself also says where to look: `item_position` names the item (1-based), `field` is a JSON pointer into it (`/params/owner_id`, `/observed_versions`, `/source_refs/2`), and `detail` is one sentence about the rule it broke — so a 25-item set is fixed in one round rather than by bisection. Those three describe only what the assistant sent or what a read already returned to it; a domain refusal whose remedy is the assistant's — a subcontrol identifier that does not extend its parent, say — carries them too, and the rest stay a bare code. The ones you are most likely to see:

* `version_stale` / `version_required` — the record moved, or the assistant did not send what it observed. `next_action` is `repropose`: read again and try once more.

* `token_scope_missing` — the token lacks the module scope for that operation. Rotate with wider scopes, or narrow the task.

* `unauthorized` — the sponsor cannot perform that action on that record. The agent inherits your permissions; it does not exceed them.

* `idempotency_key_reused` — the same key with different content. A replay is fine; a second, different proposal needs a new key.

* `explicit_judgment_confirmation_required` — delegated approval was asked to approve a judgment call. Open the review queue.

* `proposal_limit_reached` — 20 open sets per token, 100 per account. Decide some.

* `control_cannot_hold_evidence` — evidence was proposed on a criterion rather than a subcontrol beneath it. Only SOC 2 and HIPAA have criteria; ISO 27001 and the other frameworks list every control flat with `criteria_root: false`, and there is no hierarchy to walk.

* `upload_invalid` — the staged upload failed a check; `reason` names it and `guidance` says what to do. See [Attaching files](#attaching-files).

* `source_refs_invalid` — provenance was missing or malformed; `field` names the reference and `detail` the rule (`https` or `urn` without a query string, an explicitly zoned `observed_at`).

* `invalid_request` — the request broke a structural rule; `detail` says which: an unknown field, a wrong type, `compliance_project_id` present on an account-scoped set or missing on a project-scoped one, an item mixing scopes, a subcontrol identifier that is not the parent identifier plus a numeric suffix (`CC1.1.4` under `CC1.1`).

* `headless_compliance_ingress_unavailable` — the pause above, seen from inside a tool result.

One error arrives without any of these fields, because it comes from the MCP protocol layer before AuditBadger sees the request: **`-32602 Invalid params`** with `Tool not found: <name>` in `data`. The name is not a tool. If it is a read key from the catalogue in [part 2](guide_mcp-server-usage.md#reads-68), call `read` with that `key`; if it is an operation key or one of the tools removed on 2026-09-19, call `propose` with the `operation_key`. Arguments never fix this one, and some clients show only the first line.

### **Workload limits**

A single change set holds at most 25 items, with dependency chains at most 5 deep and 10 wide. Each item may carry 10 source references and 256 KB of serialized content; a set may carry 50 references and 1 MB. Titles are capped at 200 characters, rationales and decision notes at 5,000. Lists page 50 at a time, 100 at most. Proposals expire 14 days after submission by default, and never later than 30 days.

The assistant can read all of these from `whoami` rather than discovering them by failing. `whoami` also reports whether self-approval is enabled on the token, and both `whoami` and `describe_operations` give each operation its `scope_kind` (whether a set needs `compliance_project_id`) and `approval_policy` (`explicit_judgment` items are decided one by one by a person, so a set containing one is never approved through `approve_change_set`).

## **Notifications and webhooks**

Seven lifecycle events are published, and both channels use the same list, so a subscription and a notification can never disagree about what exists:

* `agent.change_set_proposed`

* `agent.change_set_decisions_finalized`

* `agent.change_set_applied`

* `agent.change_set_partially_applied`

* `agent.change_set_failed`

* `agent.change_set_cancelled`

* `agent.change_set_expired`

**Notifications** reach reviewers by **email** by default, linking straight to the review page. Each person tunes their own channels in notification preferences; in-app is selectable for the record it keeps, but nothing renders these in-app yet, so email is what a reviewer should rely on.

**Webhooks** are configured in Account Settings and can be account-wide or filtered to a single compliance project. Payloads carry the change set's identity, event, revision, state, item count, and review path — not the proposal's content, so a webhook can wake an external orchestrator without leaking what was proposed. Delivery is durable: it retries, and a redelivered event produces neither a second notification nor a second agent turn.

A webhook is the right way to build "propose overnight, review in the morning". Polling `get_change_set` works too, and is simpler if the assistant is already running.