# QC Database - MCP Server Specification

> A guide for building Model Context Protocol (MCP) servers, AI agents, and other
> headless integrations on top of the QC Database public API.

This document explains what QC Database is, how its data is organized, how to
authenticate, and how the API should be used to do real quality-control work -
most importantly, how to assemble a complete **turnover package**.

It is intentionally a high-level guide. It references API endpoints by path and
scope, but it does **not** restate every request and response field. For the
authoritative, always-current contract (parameters, schemas, examples) use the
public, interactive API documentation:

- **Swagger UI:** [`/api/docs/`](/api/docs/)
- **ReDoc:** [`/api/redoc/`](/api/redoc/)
- **OpenAPI schema (JSON):** [`/api/schema/`](/api/schema/)

These pages are public and require no authentication, so an MCP server can read
them directly to discover the live surface.

---

## 1. What QC Database Is

QC Database is a multi-tenant SaaS platform for construction quality control. It
helps contractors collect, organize, and turn over the quality records that
every project requires - mapping inspection requirements and action items
directly onto construction drawings.

The platform's purpose is to produce a **complete, accurate, fully traceable
turnover package**: every weld, flange, and heat number mapped by a human, with
every supporting record (WPS, WPQ, NDE report, MTR, PWHT report, inspection
form, etc.) attached in its original form. AI is used to get there faster, but
every step is auditable and traceable back to a source document.

An MCP server's job, in this domain, is to act as an intelligent assistant that
helps move work toward that complete turnover package - uploading records,
extracting structured data, creating and fulfilling references, and reporting on
what is still missing.

---

## 2. Data Model: First-Class Items

QC Database is organized as a strict hierarchy. Everything is **tenant-scoped**,
and almost everything below the project level is **project-scoped** and gated by
**project membership**. Understanding this hierarchy is the key to using the API
correctly.

```
Tenant (organization)
+-- Project
    +-- Job (a work order / sub-scope of the project)
    |   +-- Package (a collection of work, e.g. a Turnover package)
    |       +-- ITP Line Item (a required inspection/test step)
    |       +-- Drawing (isometric sheet; usually under a package, may be "unsorted")
    |           +-- Map Item (a weld, flange, fitting... always pinned to a drawing)
    +-- Large Format Drawing (plan / elevation / P&ID / overview)
    +-- Line Spec (a line specification and its requirements)
    +-- Document (an uploaded record: MTR, NDE report, certificate...)
    +-- Form / Form Submission (a digital inspection form)
    +-- Note (an observation, optionally pinned to a project URL)
```

Each item below is a **first-class resource** with its own API surface.

### Tenant
The organization (company) that owns everything else. A user may belong to more
than one tenant. **A token is always pinned to exactly one tenant** (see OAuth
below), so every call you make operates inside that one organization. List the
tenants the current user belongs to via `GET /api/tenants/` (`tenants:read`).
There is intentionally no write access to tenants over the API.

### Project
The top-level container for actual quality work. Most other resources are
addressed as children of a project (e.g. `/api/.../projects/<project_id>/...`).
Use `/api/projects/` (`projects:read` / `projects:write`) to list, create, and
update projects, and `/api/projects/<id>/users/` (`projects:read`) to list active
members for assignee pickers.

### Job
A work order or sub-section of a project - a way to slice a project's scope into
manageable units. Managed under `/api/jobs/` (`jobs:read` / `jobs:write`).

### Package
A collection of work and the central organizing unit for turnover. A package
belongs to a **job**, and most of a project's drawings and ITP line items live
under a package (drawings may also be left "unsorted"). Packages have
a type (for example a Hydro Test package, a Weld Map package, or a **Turnover**
package). A turnover package is the final compilation of all records required to
hand a system over to the client. Managed under `/api/packages/`
(`packages:read` / `packages:write`).

### ITP Line Item
An entry in an **Inspection and Test Plan** - a specific, required inspection or
test step within a package (e.g. "Visual Weld Inspection", "Hydro Test"). These
are the checklist of activities that must be completed and accepted. Managed
under `/api/packages/projects/<project_id>/itp-line-items/`
(`packages:read` / `packages:write`), with explicit
`mark-complete/` and `mark-accepted/` actions. Completion and acceptance are
buy-off steps performed by people, not AI.

### Drawing
An isometric sheet uploaded to a project (P&IDs are handled as **Large Format
Drawings**, not here). A drawing typically belongs to a **package**, but may be
left **"unsorted"** at the project level. Drawings carry
AI-**extracted data** (drawing number, revision, fluid service, bill of
materials) and are the canvas onto which **map items** are placed. Managed under
`/api/drawings/` (`drawings:read` / `drawings:write`); upload to a project via
`POST /api/drawings/projects/<project_id>/upload/` and export a rendered PDF via
`GET /api/drawings/projects/<project_id>/drawing/<id>/export/`. For MCP clients
that cannot fetch storage URLs, the same PDF is available as base64 at
`.../drawing/<id>/download/`, and the raw canvas image (for overlaying map items
in an HTML artifact) at `.../drawing/<id>/image/` (see Section 8).

### Large Format Drawing (LFD)
A high-level, large-format sheet - plans, elevations, **P&IDs**, and overview
drawings. LFDs capture spatial and equipment-level data (tagged equipment,
instrumentation, areas/buildings) and show where a package's scope lives in the
plant. **Packages can be called out on an LFD as markups**, which makes it the
natural place to visualize P&ID and plan-view package scope. Upload via
`POST /api/drawings/projects/<project_id>/large-format/upload/`
and export via `.../large-format/<id>/export/` (both under `drawings:write` /
`drawings:read`). The rendered PDF is also available as base64 at
`.../large-format/<id>/download/`, and the raw canvas image at
`.../large-format/<id>/image/` (see Section 8).

### Form / Form Submission
A digital inspection form (e.g. a hydro test report) filled out in the app. A
**Form Submission** is one completed instance of a form schema, with revision
tracking. Create, fill, and complete submissions under
`/api/forms/projects/<project_id>/submissions/` (`forms:read` / `forms:write`),
including a `complete/` action. Completed forms are evidence that can satisfy
references (see below).

### Document
Any uploaded record that proves quality or completion - MTRs, NDE reports,
certificates, procedures, and so on - stored in its original PDF/image form.
Documents also carry AI-extracted data driven by a per-folder schema. That
extraction can run **in the cloud** (QC Database extracts against the folder
schema after upload), or you can **bring your own AI**: extract with the
tenant's in-house model and submit the structured JSON yourself when uploading
via the API or an MCP server (upload with `do_not_extract=true`, then write the
fields back - see Bring Your Own AI in Section 5). Managed
under `/api/documents/` (`documents:read` / `documents:write`); upload to a
project via `POST /api/documents/projects/<project_id>/upload/`, and fetch the
original file as base64 (for MCP clients that cannot fetch storage URLs) via
`GET /api/documents/projects/<project_id>/document/<id>/download/` (see
Section 8). A document folder can also publish a **fillable PDF template** that
you fill and submit back as a document - see Section 7.

### Note
An observation, action item, or issue. A note is pinned to a **URL within a
project** (the page it was raised on), or left unpinned - notes are **not** map
items and are **not** attached to drawings. Notes can be public or private, can
@mention and assign members, and can be resolved. Create and read via
`/api/notes/projects/<project_id>/...`
(`notes:read` / `notes:write`), including `mine/`, `feed/`, and a `resolve/`
action. A private note is only readable/resolvable by its author or a mentioned
member.

### Map Item
A point or region placed on a **drawing** - a single weld, flange, fitting, or
other inspected feature. A map item is **always pinned to a drawing** (a regular
drawing - never a Large Format Drawing). Each map item follows a **map item
schema** (see Extraction Schemas) that defines its custom fields, and each is a
candidate **source** for references. Text fields can embed **list-item pills**
(see List-Item Pills) to reference controlled-vocabulary values. Managed under
`/api/mapping/items/` (`mapping:read` / `mapping:write`); schemas live under
`/api/mapping/schemata/`. Completion and acceptance are buy-off steps with
explicit `mark-complete/` and `mark-accepted/` actions (mirroring ITP line
items): they stamp the acting user and time **server-side** and cannot be set
by writing the `status` field directly. `mark-accepted` requires the item to be
completed first, and both are blocked while the item has open reference
requests.

### Line Spec
A **line specification** - the piping class / material spec that governs a line -
together with its **requirements**. A line spec's requirements should be
**populated from the extracted JSON of referenced documents** (the spec sheets
and material standards a line cites), so the requirements stay grounded in real
source data. Those requirements then **inform the ITP line items** - whether
AI-generated or written by a person - that must be completed and accepted to
satisfy the spec. Managed under `/api/line-specs/`
(`linespecs:read` / `linespecs:write`).

### Supporting resources
- **Photos** - attach images to objects like map items:
  `/api/photos/projects/<project_id>/<object_type>/<object_id>/`
  (`photos:read` / `photos:write`).
- **Shippers** - shipping/receiving data:
  `/api/shippers/projects/<project_id>/...` (`shippers:read` / `shippers:write`).
  With `shippers:write` a client can create a shipper (header + `line_items[]`
  in one atomic `POST`), `PATCH` its header fields / status, add, update and
  remove line items, and soft-`DELETE` a shipper. The server fills every
  `frozen_*` field from the drawing; locked shippers answer `409
  {"code": "shipper_locked"}` and received lines `409 {"code":
  "line_item_received"}`. Pass your own `external_ref` on create and find the
  shipper again with `?external_ref=`. Receiving, checklists and unlocking stay
  in the web app (no API).

### Archived objects (never deleted)

Packages, drawings, large format drawings, documents and form submissions are
never permanently deleted in-app - they are **archived**. An archived object is
out of the work scope for every client: it is absent from list endpoints, its
detail endpoint returns **404**, and progress endpoints (`export/progress/` at
job/package/drawing level, LFD package progress) exclude it and everything
under it (map items / ITP items of an archived package, sheets of an archived
multi-sheet drawing). Archiving a multi-sheet drawing archives its sheets.
Objects only come back through a manual restore on the project's Archive page
(project admins, CSA admins, PQS admins) or the restore endpoints (e.g.
`POST /api/documents/<id>/restore/`), after which they reappear everywhere
immediately. Treat a 404 on a previously-known id as "archived" - do not
recreate it.

### Deep Links (`web_url`)
Most resources include a `web_url` - an **absolute link to that item's page in
the human web app** - so an MCP server can show the user a clickable link back
into QC Database instead of a bare UUID. It appears on projects, jobs, packages,
ITP line items, drawings, large format drawings, documents, form submissions,
and list items. Listing endpoints that represent a whole page also return a
**top-level** `web_url` for that page:

- the folder-scoped document listing
  (`GET /api/documents/?project=<id>&folder=<id>`) adds `web_url` alongside the
  paginated `results` (present only when both `project` and `folder` are given);
- the list-item listing (`GET /api/lists/projects/<id>/<list_id>/items/`)
  returns `{ "web_url": <list page>, "items": [...] }`;
- the notes listings (`GET /api/notes/projects/<id>/feed/` and `.../mine/`)
  return `{ "web_url": <notes page>, "notes": [...] }`.

`web_url` is built from the **host of the incoming request**, so the links point
back through whatever domain the client connected on (this keeps them correct
under future per-tenant subdomains). It is `null` when no page applies (for
example a tenant- or global-scoped document with no project). Treat it as a
display convenience - it is never required as input.

### List-Item Pills (controlled vocabulary)
Lists hold a project's controlled vocabulary - welders, weld types, materials,
pipe sizes, equipment, and similar - and a **list item** is one entry in such a
list. Read them via `/api/lists/projects/<project_id>/` and
`/api/lists/projects/<project_id>/<list_id>/items/` (`lists:read`; there is no
write scope - list items are managed in the app). Every returned item carries a
ready-to-embed `pseudo_code` token, for example
`[[item_name:John Smith|item_id:<uuid>|project_id:<uuid>]]`. Drop that token
verbatim into a map item field value (or any other text field that renders
references) and QC Database shows it as a live, linked **pill** instead of free
text, keeping the value tied to the canonical list entry. Always prefer
inserting a list item's `pseudo_code` over typing its name by hand.

---

## 3. Authentication: The OAuth2 Flow

QC Database is an **OAuth2 authorization server and resource server** built for
headless and MCP clients. It is *not* itself an MCP server - it is the system an
MCP server connects to on a user's behalf. Authentication is standards-based
(OAuth 2.0 Authorization Code + PKCE) and every token is **pinned to a single
tenant**.

### 3.1 Discovery

Start by reading the standard metadata documents. They list every endpoint and
the supported scopes, and are CORS-enabled for browser clients:

- **Authorization server metadata (RFC 8414):**
  `/.well-known/oauth-authorization-server`
- **Protected resource metadata (RFC 9728):**
  `/.well-known/oauth-protected-resource`

### 3.2 Dynamic Client Registration (RFC 7591)

Register a client programmatically at `POST /oauth/register/`. The server issues
a **public, PKCE-required** authorization-code client (no client secret).

- You may request a `scope` string at registration. This becomes a **permanent
  scope ceiling** for the client - the client can never obtain a token broader
  than what it registered for, even if the user has wider permissions. If you
  omit it, the client defaults to the read-only scopes.
- Redirect URIs are strictly validated: no fragments, no wildcards, no
  whitespace/control characters, and must be `https`, loopback `http`
  (`127.0.0.1`/`localhost`), or a private custom scheme. Registration is rate
  limited.

Register for the **narrowest** set of scopes your integration actually needs.

### 3.3 Authorization (with tenant selection)

Send the user to `GET /oauth/authorize/` with the standard Authorization Code +
PKCE parameters (`response_type=code`, `client_id`, `redirect_uri`,
`code_challenge`, `code_challenge_method=S256`, `scope`, `state`).

The user is shown a branded consent screen where they **must pick a tenant** from
the organizations they belong to. That choice **pins** the resulting grant - and
every token derived from it - to that one tenant. This is the mechanism that
guarantees one connected app can only ever see one workspace's data.

### 3.4 Token exchange and refresh

Exchange the authorization code at `POST /oauth/token/`
(`grant_type=authorization_code`) with your PKCE `code_verifier`. You receive an
access token and a refresh token, both carrying the pinned tenant.

Refresh with `grant_type=refresh_token` at the same endpoint. Refresh rotation is
on, and the new token is **re-pinned to the same tenant** as the old one. The
tenant pin can never drift.

**OpenID Connect scopes.** MCP clients may request the identity scopes
`openid profile email` alongside data scopes (Claude, for example, requests
every scope in `scopes_supported`). They grant no API access. When the server
has OpenID Connect enabled, the token response (code exchange and refresh) also
carries an RS256-signed `id_token` with the user's `sub`, `email`, `name` and
the pinned `tenant_id` / `tenant_slug`; verify it against `jwks_uri` from
`/.well-known/openid-configuration`. When OpenID Connect is not enabled the same
request succeeds with no `id_token`. You never need the ID token to call the
API - keep using the access token.

### 3.5 Calling the API

Send the access token as a bearer header:

```
Authorization: Bearer <access_token>
```

The bearer authenticator runs first and **fails closed**: a present-but-invalid
or under-scoped token is rejected (it cannot be masked by a session cookie). On a
valid token the server sets the request's tenant from the token's pin, so you
never pass a tenant id yourself.

Effective access is the **intersection** of four things:

```
user's permissions  AND  granted scopes  AND  pinned tenant  AND  client scope ceiling
```

Scopes only ever *narrow* access. A token can never grant something the user
lacks, reach outside the pinned tenant, or exceed the client's registered ceiling.

**Access errors.** Project-scoped endpoints enforce **project membership** on
every request, not just tenant scoping. If you are authenticated but not a
member of the project you address, the server returns **403**; a project (or
other resource) id that does not exist inside your pinned tenant returns
**404**. On writes, supplying a related id from another project or tenant - for
example a drawing, job, line spec, schema, or assignee that does not belong to
the target project - is rejected with **400** and a field error. Handle all
three so an agent fails clearly instead of silently writing into the wrong
place.

### 3.6 Scopes

Scopes are `area:read` or `area:write`. Safe methods (`GET`/`HEAD`/`OPTIONS`)
require `:read`; all other methods require `:write`. The area is derived from the
first path segment after `/api/`. Unmapped areas are denied (fail-closed).

| Area | Read scope | Write scope |
| --- | --- | --- |
| Tenants | `tenants:read` | _(none - read-only)_ |
| Projects | `projects:read` | `projects:write` |
| Jobs | `jobs:read` | `jobs:write` |
| Packages / ITP | `packages:read` | `packages:write` |
| Drawings & Large Format | `drawings:read` | `drawings:write` |
| Mapping (map items & schemas) | `mapping:read` | `mapping:write` |
| Documents | `documents:read` | `documents:write` |
| Forms | `forms:read` | `forms:write` |
| Notes | `notes:read` | `notes:write` |
| References | `references:read` | `references:write` |
| Photos | `photos:read` | `photos:write` |
| Line specs | `linespecs:read` | `linespecs:write` |
| Shippers | `shippers:read` | `shippers:write` |
| Notifications (caller's own inbox + preferences) | `notifications:read` | `notifications:write` |

> The user can review and revoke any connected app at any time. Authorize and
> revoke actions are audited. Design your MCP server to handle a revoked or
> expired token gracefully (re-run the authorization flow).

---

### 3.7 Approved MCP origins (per-workspace allow-list)

Each workspace (tenant) carries an **Approved MCP origins** list, managed by
its CSA administrators under Tenant Settings. It names the hosts (one per line,
`host` or `host:port`, no scheme) that MCP servers / AI clients may connect
from. The default list is just `mcp.qcdatabase.ai` (the hosted QC Database MCP
server); a self-hosted or loopback server (`localhost:3000`, `127.0.0.1`) works
as soon as the admin lists it.

Because a dynamically-registered client's **redirect-URI host is the MCP
server's origin**, the list is enforced against the client, not the user:

- **Consent:** the tenant picker only offers workspaces that approve the
  client's redirect host. If none do, the user sees *"This MCP origin is not
  approved by your workspace administrator."* and no code is issued.
- **Token minting:** code exchange and refresh are refused (`invalid_grant`)
  for a blocked client, so a blocked server cannot keep minting tokens.
- **Every API call:** an existing bearer token from a host that has since been
  removed from the list is rejected with **HTTP 403** and this body:

  ```json
  {"error": "This MCP origin is not approved by your workspace administrator.",
   "detail": "...", "code": "mcp_origin_not_approved"}
  ```

  Surface this to the user verbatim (it is an administrative decision, not an
  auth failure); do **not** retry or restart the OAuth flow - it will fail the
  same way until the workspace administrator adds your host.

Only `https://` redirect URIs (and loopback `http://`) establish an origin. A
client whose redirect URIs include a private-use scheme (`myapp://...`) has no
web origin and is always blocked by the list - register an `https` callback.

Only dynamically-registered (RFC 7591) clients are subject to the list.
First-party clients, session logins and X-API-Key requests are unaffected.

### 3.8 Safe retries: the `Idempotency-Key` header

Network retries from a worker or MCP server can duplicate writes - a retried
document upload creates a second version, a retried shipper create makes two
shippers. The create endpoints most exposed to this accept an **optional**
`Idempotency-Key` request header (any client-generated string up to 255
characters; a UUID per logical operation is ideal):

- `POST /api/documents/projects/<project_id>/upload/`
- `POST /api/shippers/projects/<project_id>/` and
  `POST .../<shipper_id>/line-items/`
- the three extracted-data submits (drawing, large-format drawing, document
  `extracted-data/`)
- `POST /api/mapping/projects/<project_id>/zipmaps/`

Semantics, for 24 hours after the first request:

- **Same key, same request** (same body fields, same `If-Match`
  precondition and the same file bytes - the multipart boundary may differ)
  → the stored response is replayed verbatim:
  same status code, same JSON body, same `Content-Type` (and `Location` where
  the original had one), plus `Idempotent-Replayed: true`. Nothing is written
  again.
- **Same key, different request** → `422 {"code": "idempotency_key_reuse"}`.
  Generate a new key for a new operation; never reuse one.
- **Same key while the first request is still running** →
  `409 {"code": "idempotency_in_flight"}`. Back off briefly and retry the same
  request; it then replays once the original has finished.
- A malformed header (empty or over 255 characters) → `400
  {"code": "idempotency_key_invalid"}`.

Keys are scoped to the pinned tenant **and** to the caller - the OAuth client
(any of its tokens), the API key, or the session user - so two integrations
can never collide on the same key. A replay re-checks the caller's current
project membership and permissions first: if access was revoked after the
original request you get the usual 403/404, never the stored body. Only
responses the server actually
*returned* are stored; a request that was rejected before executing (400
validation, 403, 404) leaves the key free, so the corrected request may reuse
it. Requests without the header behave exactly as before. The header is
documented on each covered operation in the OpenAPI schema.

## 4. Session Configuration: Set the Project Once

Almost every endpoint in this API is **project-scoped** - the project id is part
of the URL path (`/api/.../projects/<project_id>/...`). Forcing a human, or an
LLM acting on their behalf, to repeat the project id on every single call is
tedious and error-prone, and it invites mistakes like writing evidence into the
wrong project.

**Strong recommendation: an MCP server for QC Database should expose a `config`
(or "set project") tool that pins the active project once per user session.**
This is the project-level analogue of tenant pinning. The OAuth token already
pins the **tenant** for the whole session - the user chose it on the consent
screen. A `config` tool extends that idea one level down: the user picks the
**project** once, the server remembers it for that user's session, and every
later tool call defaults to it. The user works "inside" a project the same way
they already work inside a tenant.

### What the config tool should do
- Offer the user the projects available in the pinned tenant (from
  `GET /api/projects/`) and let them choose one.
- Store the chosen `project_id` as **per-user, per-session** state inside the MCP
  server. Never hard-code it, and never share it between users.
- Inject that stored `project_id` automatically into every project-scoped call,
  so other tools - upload a document, list reference requests, create a map item,
  and so on - do not require the caller to pass it each time.
- Let the user view and change the active project at any time, and re-confirm it
  when a session is resumed.

### Treat it as mandatory for everyday use
Because the large majority of endpoints require a project id, a normal
daily-driver MCP server should treat "a project is configured" as a hard
**precondition**. If no project has been set, the server should refuse
project-scoped tools and prompt the user to run `config` first, rather than
guessing a project or failing deep inside an operation. This single rule
eliminates the most common class of mistakes for day-to-day users and is the
strongly recommended default.

### Reasonable exceptions
A few specialized servers legitimately operate above the project level and should
*not* force a single project. A **tenant-admin** server, for example, may list or
compare many projects, audit members, or report across an entire tenant; for
those, treat the project as an optional filter rather than a required pin. For
anything aimed at a normal daily user, project configuration should be required.

---

## 5. Extraction Schemas

Several resource types carry **extracted data** - structured JSON populated from
an uploaded file. An **extraction schema** is the template that defines which
fields the AI should populate. Understanding schemas lets an MCP server either
consume the data QC Database extracted, or **bring its own AI** and supply the
structured data directly.

There are three file-extraction surfaces and one annotation schema:

### Drawings (fixed schema)
Standard drawings (isometrics) use a built-in schema targeting
construction metadata - drawing number, revision, fluid service - plus a detailed
**bill of materials (BOM)**. Discover the active shape via
`GET /api/projects/<project_id>/schemas/drawing/`.

### Large Format Drawings (fixed schema)
LFDs (plans, elevations, P&IDs, overviews) use a built-in schema focused on
spatial/equipment data - tagged equipment, instrumentation, areas/buildings.
Discover it via `GET /api/projects/<project_id>/schemas/large-format-drawing/`.

### Documents (custom, per-folder schema)
Document extraction is **configurable per document folder**. Each folder defines
its own `extraction_schema` (for example, an MTR folder might extract heat
number, material grade, and certificate number). Fields are declared with a
simple `"type - description"` convention. Discover available document schemas via
`GET /api/projects/<project_id>/schemas/documents/`.

### Map Item Schemas
Map items follow a **map item schema** (`/api/mapping/schemata/`) whose
`schema_definition` declares the custom fields for that kind of item (e.g. a Weld
schema with a "Weld ID" field). Schemas can be project-scoped or promoted to the
tenant for reuse, and can drive automatic, per-schema numbering of map items.
Discover usable schemas via `GET /api/projects/<project_id>/schemas/map-items/`.
Use this project-scoped discovery endpoint rather than the generic schema API
when placing map items. Each result includes:

- `is_rect`: the tenant/schema default (`true` = Rectangle, `false` = Flag).
- `project_settings`: only the settings explicitly saved for that schema in the
  requested project, including any shape override, marker sizing, photo behavior,
  numbering rules, and column visibility.
- `effective_is_rect` and `effective_shape`: the final project-resolved shape.

Agents should render and create geometry from `effective_is_rect` (or its
equivalent `effective_shape` value), not from `is_rect` alone. A missing key in
`project_settings` means that setting is inherited or unset; it does not mean
the complete project configuration was returned. The detail endpoint
`GET /api/projects/<project_id>/schemas/map-items/<schema_id>/` returns the same
fields for one schema.

### Bring Your Own AI
The upload endpoints for documents, drawings, and large format drawings accept a
`do_not_extract` flag. When set, QC Database stores the file and leaves the
record ready with **empty** extracted data and **no** server-side AI run. This is
the recommended path for an MCP server that runs its own extraction: upload with
`do_not_extract=true`, run your model against the schema you discovered, then
write the structured fields back via the resource's update endpoint.

---

## 6. References & Reference Requests - the Heart of Turnover

This is the most important workflow to get right. **References are how a turnover
package becomes complete.** An MCP server that understands references can
meaningfully accelerate a project; one that ignores them only moves files around.

### 6.1 What a reference is

An **Item Reference** links a **source** (the thing that needs proof) to a
**target** (the proof itself):

- **Sources** - the work that must be backed by evidence:
  - a **map item** (e.g. a specific weld or flange on a drawing),
  - a **list item** (e.g. a welder or a heat number on a project list),
  - an **ITP line item** (a required inspection/test activity).
- **Targets** - the evidence that satisfies it:
  - a **document** (e.g. an MTR, NDE report, or certificate),
  - a **form submission** (e.g. a completed hydro test report).

Each reference is one link in a **chain of evidence**. "Forward" references go
from a source to its proof; the same link is visible in reverse from the target,
so a document can show every item it satisfies. A turnover package is *complete*
when every required source item has its supporting references in place.

List and create references under
`/api/references/projects/<project_id>/<source_type>/<source_id>/`
(`references:read` / `references:write`), where `source_type` is one of
`map_item`, `list_item`, or `itp_line_item`. When creating a reference, the
create payload's `target_type` is either `document` or `form` (a form
submission).

Each reference carries a `status`: `pass` / `compliant` (passing), `fail` /
`non-compliant` (failing), or `pending-review`. **Pending review** means the
source is linked but a human has not yet decided the outcome - it is neither
passing nor failing, renders amber in the app, and never blocks marking an item
completed or accepted. Send `pending-review` when you can tell the source covers
the item but cannot tell whether it passed; the default when omitted is `pass`.

### 6.2 What a reference request is

A **Reference Request** is a tracked placeholder for a reference that *should*
exist but doesn't yet - a to-do that says "this item still needs this proof."

- It names what is required: a `reference_type` of either a **document** (tied to
  a document folder) or a **custom form** (tied to a form schema), and the
  `item_type` it applies to (`map_item`, `itp_item`, or `list_item`).
- It carries a `description`, an optional **assignee**, and a `status` of `open`,
  `fulfilled`, or `cancelled`.
- It is **fulfilled** when a matching item reference is created; the request then
  links to the fulfilling reference and flips to `fulfilled`.

Manage requests under
`/api/references/projects/<project_id>/reference-requests/`
(`references:read` / `references:write`).

### 6.3 Why this matters for turnover

Reference requests turn "what's missing?" into a concrete, assignable,
trackable list. The set of **open** reference requests on a project *is* the
punch list standing between the team and a complete turnover package. QC Database
can even **auto-fulfill** requests: when a new document is uploaded or a form is
completed, it compares the new evidence against matching open requests for
supported item types (map items and ITP items) and, on a match, creates the
reference and closes the request automatically. Requests for other item types
(such as list items) may need a reference created manually.

An MCP server should treat reference requests as its primary "definition of
done." Querying open requests, helping satisfy them, and confirming they close
out is the single highest-value thing an integration can do here.

---

## 7. Fillable PDF Templates

Some document folders publish a **fillable PDF template** - a blank PDF form
(a Receiving and Inspection Report (RIR), an inspection checklist, a test
report) that field teams fill out and file as a document. The API exposes the
full fill-and-file loop so a headless or AI client can complete these forms end
to end. All four endpoints live under `/api/documents/` and need
`documents:read` (or `documents:write` to submit), plus membership of the
project.

1. **List templates.**
   `GET /api/documents/projects/<project_id>/fillable-templates/`
   returns the folders that have a template attached, each with the template's
   name and URLs for its fields, blank PDF, and submit endpoint.

2. **Read the fields.**
   `GET /api/documents/projects/<project_id>/fillable-templates/<folder_id>/`
   returns the template's form fields (each `name` plus a `type` of `text`,
   `checkbox`, `radio`, or `dropdown`) and its **autofill** map: the fields the
   folder is configured to pre-fill, each with its `source` (`project.name`,
   `project.code`, `project.client_name`, `project.description`,
   `current_date`, or `current_datetime`) and the resolved `value` for this
   project. Fill those fields with the supplied values and let a human or AI
   complete the rest.

3. **Download the blank PDF.**
   `GET /api/documents/projects/<project_id>/fillable-templates/<folder_id>/download/`
   streams the blank template bytes so you can fill it client-side (or with your
   own PDF tooling) and, ideally, flatten it.

4. **Submit the filled PDF.**
   `POST /api/documents/projects/<project_id>/fillable-templates/<folder_id>/submit/`
   (multipart) stores the filled PDF as a document in the template's folder.
   Send the file as `filled_pdf`, optionally a `file_name` (re-using a name
   creates a new **version** of that document), and `do_not_extract=true` to
   skip server-side AI extraction when you ran your own. Otherwise the folder's
   extraction schema runs in the background, just like a normal upload (see
   Extraction Schemas).

**PDF fields are not the same as extracted JSON fields.** The form fields on the
PDF and the folder's extraction schema are configured independently, so they may
not line up one-to-one - the admin decides which data they want captured as
structured JSON, which can be a subset, a superset, or a reshaping of what the
PDF itself contains. A truly "bring-your-own-AI" MCP server must therefore do
**two** things: upload the **filled PDF** (the human-readable record) *and*
submit the **extracted JSON from that filled PDF** as a separate object (the
document's structured `extracted_data`). Filling the PDF alone does not populate
the structured fields, and supplying JSON alone leaves no signed record.

This is the backbone of AI-assisted form filling: read the fields, fetch the
autofill values, complete the form, and file it as traceable evidence - which
in turn can satisfy a reference request (see References).

---

## 8. Binary Downloads over MCP (base64)

Some MCP runtimes cannot fetch our object-storage URLs directly: a tool that
returns a signed storage link is often blocked by the sandbox network (it sees a
proxy 403), so the bytes never arrive. To make file content reachable in a single
authenticated API call, the drawings, large format drawings, and documents areas
each expose **base64 download endpoints** that return the file inside a JSON
envelope instead of as a URL or a raw binary stream.

Every base64 endpoint returns the same envelope:

```
{
  "success": true,
  "id": "<object uuid>",
  "filename": "D-001_clean.pdf",
  "content_type": "application/pdf",
  "size": 48213,               // decoded byte length
  "encoding": "base64",
  "data": "<base64 string>"    // decode this to recover the bytes
}
```

Image endpoints add two more fields, `width` and `height` (integer pixels).

All of these require the same scope and project membership as the matching read
endpoint (`drawings:read` for drawings and LFDs, `documents:read` for documents),
resolve the object tenant-scoped (a foreign id returns 404), and exclude
archived drawings, LFDs and documents (404).

**Rendered PDFs (base64).**
- `GET /api/drawings/projects/<project_id>/drawing/<id>/download/`
- `GET /api/drawings/projects/<project_id>/large-format/<id>/download/`

These render the drawing to a PDF and return it as base64, using the same
`variant` options as the `/export/` endpoints: for a regular drawing,
`variant=clean` (bare sheet) or `variant=map&schema_id=<schema_id>` (map items of
one schema overlaid, or all schemas when `schema_id` is omitted); for an LFD,
`variant=clean` or `variant=flagged` (package-flag markups). This is the base64
twin of `/export/`, which still exists and streams the same PDF as a raw binary.

**Original document files (base64).**
- `GET /api/documents/projects/<project_id>/document/<id>/download/`

Returns the document's original stored file (PDF or image) as base64, the base64
twin of the document `/export/` endpoint.

**Canvas images (base64), for building HTML artifacts.**
- `GET /api/drawings/projects/<project_id>/drawing/<id>/image/`
- `GET /api/drawings/projects/<project_id>/large-format/<id>/image/`

These return the exact raster image the mapping interface draws on its HTML
canvas (the drawing's stored `image_file`), plus its pixel `width` and `height`.
Map-item coordinates are absolute pixels in this image's top-left-origin space
(see Map Item in Section 2), so an agent can lay the returned image into an HTML
artifact and overlay map items at their `x_position`/`y_position` directly, with
no rescaling. If the drawing has no rendered image yet, the endpoint returns 404.

**Size cap.** Base64 payloads are capped (20 MB of decoded bytes by default). A
render or file over the cap returns HTTP 413 with the byte `size` and `max_size`
limit; for PDFs, fall back to the raw-binary `/export/` endpoint, which has no
such cap.

---

## 9. Notifications: Polling the User's Inbox

Everything the platform emits (assignments, buy-offs, @mentions, quality holds,
reference requests, uploads, extraction results, membership changes ...) is also
delivered to each affected user's in-app inbox. The `/api/notifications/` area
exposes that inbox to MCP clients so an agent can react to what happened since it
last looked.

**Scoping.** Every endpoint is *caller-scoped*: it only ever returns or changes
the notifications and preferences of the user the token belongs to, inside the
token-pinned tenant. There is no parameter to read another user's inbox, and a
foreign or cross-tenant id is a 404. Reads need `notifications:read` (part of the
default read-only scope set); marking read/unread and changing preferences need
the opt-in `notifications:write` scope.

**Endpoints.**

| Method | Path | Purpose |
| --- | --- | --- |
| GET | `/api/notifications/` | Paginated list, newest first. Filters: `unread=true`, `direct=true`, `category=`, `event_type=`, `project=<uuid>`, `since=<cursor>` |
| GET | `/api/notifications/<id>/` | One notification |
| GET | `/api/notifications/unread-count/` | `{unread_count, by_category}` - cheap, poll this |
| POST | `/api/notifications/<id>/read/` `.../unread/` | Toggle one item's read state |
| POST | `/api/notifications/mark-all-read/` | Mark read. Optional JSON body: `ids`, `since`, `up_to`, plus the list filters. **No body = every unread row** |
| GET | `/api/notifications/event-types/` | The catalog: categories, event types, audience rules, payload keys, levels |
| GET / PUT / DELETE | `/api/notifications/preferences/` | Effective subscription levels; bulk upsert; reset overrides |

Each item includes `id`, `event_type`, `category`, `title`, `message`, `project`
(`{id, name, code}`), `actor` (`{id, full_name}`), `related_object_type` /
`related_object_id`, an absolute `link_url` deep link, the event `payload`,
`is_direct` (the user is the assignee / mentioned / target person rather than a
project watcher), `is_read`, `read_at`, `created_at`, and an opaque `cursor`.

**Recommended polling loop.**

1. On first run, `GET /api/notifications/?page_size=1` and store the envelope's
   `next_cursor` (the newest item). Do not replay history to the user unless
   they ask.
2. Every N seconds, `GET /api/notifications/unread-count/`. Compare its
   `latest_cursor` with your stored cursor. If they are equal, nothing new has
   arrived. Do **not** use `unread_count` alone as the change signal: if the
   user reads one notification in the web app while another arrives, the count
   does not move but `latest_cursor` does.
3. When they differ, `GET /api/notifications/?since=<stored cursor>` (add
   `project=<active project>` or `direct=true` to narrow). Items come back
   oldest -> newest; present them, follow `next` if the page filled up, then
   store the envelope's `next_cursor`.
4. After the user has seen or acted on them, acknowledge **exactly those
   items**: `POST /api/notifications/mark-all-read/` with
   `{"ids": [...ids you presented...]}`, or with the range you just consumed,
   `{"since": "<old cursor>", "up_to": "<new next_cursor>"}` plus the same
   filters you listed with. Anything that arrived after `up_to` (or is not in
   `ids`) stays unread. Use the per-item `.../read/` endpoint when the user
   handles items one by one.

> `mark-all-read` with **no** `ids` / `since` / `up_to` marks *every* unread
> notification matching the filters - including history you never showed the
> user and anything that arrived a moment ago. Only send it unbounded when the
> user explicitly asks to "clear all".

The cursor is stable: it encodes the item's creation time and id, so a poll
never skips or repeats an item even when several arrive in the same instant.

**Presenting preferences.** `GET /api/notifications/preferences/` returns every
category with the user's effective tenant-wide `level` and its `source`
(`default` or `tenant`), plus a `project_overrides` list (`source: "project"`).
Levels are `off` (nothing), `direct` (only events about me), and `all` (every
event in my projects); a project override beats the tenant-wide level, which
beats the registry default. Describe them to the user in those words - "Do you
want everything for map items on Project X, or only items assigned to you?" -
and write the choice with
`PUT /api/notifications/preferences/ {"preferences": [{"category": "map_items", "project": "<uuid>", "level": "direct"}]}`.
Omit `project` for a tenant-wide setting; `DELETE` the same shape (without
`level`) to remove an override. Categories are validated against the catalog and
projects against the user's active memberships. Preferences shape what the user
receives from now on; they never rewrite existing notifications.

---

## 10. Recommended MCP Server Capabilities

This document does **not** prescribe how to build an MCP server. But a genuinely
useful one for QC Database should expose tools (resources/actions) that cover the
following. Map each capability to the endpoints and scopes above, and confirm the
exact contract in [`/api/docs/`](/api/docs/).

**Session configuration (start here)**
- A `config` / "set project" tool that pins the active project once per user
  session (see Section 4). For a normal daily-driver server, treat this as a
  required precondition for every project-scoped tool.
- A way to view and change the active project, and to confirm it on resume.

**Orientation & discovery**
- Resolve the connected tenant and list its projects.
- List members of a project (for assigning requests).
- Discover extraction schemas for drawings, large format drawings, documents,
  and map items.
- List the project's controlled-vocabulary lists and their items, each with an
  embeddable `pseudo_code` pill (`lists:read`).

**Ingestion**
- Upload documents, drawings, and large format drawings to a project - with the
  option to use `do_not_extract` and run the client's own AI.
- Complete fillable PDF templates: list them, read a template's fields and
  autofill values, download the blank PDF, and submit the filled copy (a re-used
  file name creates a new version; `do_not_extract` is supported).
- Attach photos to items.

**Structured data**
- Read extracted data from drawings, LFDs, and documents.
- Write structured fields back (especially after bring-your-own-AI extraction).
- Create and read map items against the correct schema. When a field maps to a
  controlled list, write the list item's `pseudo_code` pill rather than free
  text so the value stays linked to the canonical entry.

**Quality workflow**
- Create, fill, and complete form submissions.
- Create and read notes; assign via @mention; resolve them.
- List ITP line items and map items and report their completion/acceptance
  status. Buy-off (`mark-complete` / `mark-accepted` on both ITP line items and
  map items) records the acting user and time server-side; surface it as a
  human-confirmed action rather than performing it automatically.

**Turnover (highest value)**
- List **open reference requests** for a project ("what's missing").
- Create item references to satisfy requests (link a source to its proof).
- Create new reference requests when a gap is identified, with an assignee.
- Report turnover readiness: open vs. fulfilled requests, items lacking proof,
  ITP items not yet complete or accepted.

**Notifications (stay in the loop)**
- Poll the user's own inbox with the cursor contract in Section 9
  (`unread-count` watermark -> `?since=` -> acknowledge the returned `ids`) and
  surface new events
  such as assignments, @mentions, quality holds, and reference requests.
- Show and change the user's per-category subscription levels (`off` /
  `direct` / `all`, tenant-wide or per project).

**Good citizenship**
- Request the narrowest scopes needed.
- Handle revoked/expired tokens by re-authorizing.
- Completion, acceptance, and sign-off are **the user's** actions. An MCP server
  **may** mark items complete or accepted on a user's behalf, but only with
  **clear, explicit communication that the user is responsible for every action
  the MCP server takes for them** - including buy-offs. Prepare and report by
  default, and never buy off work silently or without the user's informed
  go-ahead.

---

## 11. An Ideal Workflow

A representative end-to-end flow an MCP server might orchestrate, from connection
to a more complete turnover package:

1. **Connect.** Register a client (`/oauth/register/`), run the
   Authorization Code + PKCE flow (`/oauth/authorize/` -> `/oauth/token/`); the
   user picks the tenant on the consent screen, pinning the token.
2. **Set the project.** Read `/api/tenants/` and `/api/projects/`, let the user
   pick the project to work in, and pin it once via the server's `config` tool
   (see Section 4) so every later call defaults to it. For a daily-driver server,
   require this before any project-scoped tool runs.
3. **Orient.** Read the project's members, the relevant extraction schemas, and
   the project's controlled-vocabulary lists - so you can reference list values
   by their `pseudo_code` pill (see Section 2).
4. **Ingest source work.** Upload drawings and large format drawings; create or
   read the map items that represent the welds, flanges, and fittings to be
   inspected. When a map item field maps to a controlled list (welder, weld
   type, material), embed that list item's `pseudo_code` pill instead of typing
   the value, so it stays linked to the canonical list entry.
5. **Find the gaps.** List the project's **open reference requests** - the
   evidence still owed for those items and ITP activities.
6. **Provide the evidence.** Upload the missing documents (MTRs, NDE reports,
   certificates), complete the required forms, or fill a folder's **fillable PDF
   template** (read its fields and project-resolved autofill values, fill it,
   and submit it back as a versioned document - see Section 7). Use
   `do_not_extract` plus your own extraction when you want to control the AI;
   otherwise let QC Database extract against the folder schema.
7. **Close the loop.** Create the item references that link each new document or
   form back to its source item - or let auto-fulfillment match them - so each
   open request flips to `fulfilled`.
8. **Report readiness.** Summarize what remains: open reference requests by
   assignee, source items still lacking proof, and ITP line items not yet
   completed or accepted. This is the live picture of how close the project is to
   a complete, traceable turnover package.
9. **Audit on request.** An MCP server and its AI agent make a valuable
   **auditing assistant** - cross-checking extracted data against source
   documents, finding references that don't match their proof, spotting missing
   or inconsistent records, and verifying line-spec requirements are met. Perform
   any audit the user asks for and report the findings for their review.

Throughout, every artifact stays in its original form and every AI step remains
auditable - the system always "shows its math." An MCP server should preserve
that principle: accelerate the paperwork, but keep humans in command of buy-off
and accountability.

---

## 12. Where to Go Next

- **Interactive API reference:** [`/api/docs/`](/api/docs/) (Swagger),
  [`/api/redoc/`](/api/redoc/) (ReDoc)
- **Machine-readable schema:** [`/api/schema/`](/api/schema/)
- **OAuth discovery:** `/.well-known/oauth-authorization-server`,
  `/.well-known/oauth-protected-resource`
- **Dynamic client registration:** `POST /oauth/register/`

Use the endpoint paths and scopes in this guide as a map, and the API
documentation as the authoritative, always-current detail.
