---
name: classcad-pdm
description: Work in the company's ClassCAD PDM as the user - find documents, create parts and assemblies, edit models, make versions, release, run generators. Use when the user asks to do something in their PDM.
---

# Working in the ClassCAD PDM

The PDM keeps ClassCAD models as **documents** (parts, assemblies, generators, material libraries) with their
history. You work in it **as the user**, with exactly their rights, through two MCP servers:

- **The PDM's MCP** (`pdm`): `login`, `logout`, `guide`, `api`, `engine_open`, `editor_join`, `engine_close`. It signs in with the
  user's PDM login (their company's) and calls the PDM's REST API; it adds the user's token itself, you name paths,
  never a token. It also hands out ClassCAD engines on the PDM's servers (section 4).
- **The ClassCAD MCP** (`classcad`): the models themselves. A model is loaded from the PDM into a drawing, changed
  there (`run_script`, `tree`, `snapshot`), and saved back. The drawing runs on the ClassCAD MCP's own engine, or on
  an engine of the PDM (section 4). Buerligons in the PDM does not need to be open. The ClassCAD MCP has its own
  sign-in, a free classcad.ch account of the user; the PDM's login has nothing to do with it.

## 0. Setup and sign-in

**The PDM's MCP is not installed yet** (no `pdm` tools): the PDM serves it at `<PDM>/agent/pdm-mcp.mjs`, one file
that needs Node 20 or newer and nothing else. In Claude Code:

```bash
mkdir -p ~/.classcad-pdm && curl -fsSL <PDM>/agent/pdm-mcp.mjs -o ~/.classcad-pdm/pdm-mcp.mjs
claude mcp add --scope user pdm -e PDM_URL=<PDM> -- node ~/.classcad-pdm/pdm-mcp.mjs
```

(`<PDM>` is the address the user opens in the browser, e.g. `https://pdm.example.com`; the PDM shows both lines with
its address under **Connect Claude** in the user menu.) The ClassCAD MCP comes as the `classcad` plugin. Both start
with the next session; tell the user to restart Claude Code.

**Sign in:** `login` (or `login({ url: "<PDM>" })`). The first call opens the PDM's connect page; show the link, the
user signs in with their PDM login if needed and clicks **Allow**, call `login` again and it returns who you work as.
Once per machine and PDM; the token lives until it expires or the user revokes it under **My tokens**. A `401` from
`api` means it is gone: sign in again.

Check: `api({ path: "/users/me" })` names the user and their roles.

## 1. The model of the PDM

| Thing | What it is | Where |
|---|---|---|
| Document | One part, assembly, generator (`code`), library. `number` (e.g. `BRK-2002`) and `name`. | `/documents` |
| Workspace | A line of work of a document: `main`, plus branches. Each has exactly one **draft**. | `/documents/{id}/workspaces` |
| Draft | The editable state of a workspace (a version with `kind: "draft"`). All changes go here. | `workspace.draft` |
| Version | A frozen snapshot of a draft (`POST /workspaces/{id}/versions`). Numbered per document. | `/versions/{id}` |
| Revision | A released version with a label (`A`, `B`, …). Released documents can no longer be deleted. | `/versions/{id}/release` |
| Artifact | A file of a version: `part-source` (the model, OFB), `asm-json` (an assembly), and derived files the PDM's worker makes from them: `part-scg`, `part-meta`, `thumbnail`, … | `/versions/{id}/artifacts` |
| Job | Work of the PDM's worker after an upload (`part-derive`, `asm-derive`, …): derived files, checks. | `/versions/{id}/jobs` |

Ids are UUIDs; numbers are for people. `GET /documents?q=BRK-2002` finds a document by number or name;
`GET /documents/search?q=bracket steel` searches wider (description, folder, material, generator).

`GET /documents/{id}/workspaces` gives each workspace **with its draft and the draft's artifacts**: one call tells
you the draft id, the source file (`part-source`, its `fileName`, `blob.sha256`) and the state of the jobs.

## 2. Ground rules

- **Ask before what cannot be undone**: releasing, deleting a document or a branch, archiving. Creating documents,
  uploading into a draft and making versions are everyday work.
- **The draft is shared.** Writing needs no reservation, but someone may hold the workspace's lease (`lease` in the
  workspace): then uploads answer `409`, so tell the user who holds it. Do not take a lease the user did not ask for.
- **Optimistic locking**: send `If-Match` with the ETag of the draft (`GET /versions/{draftId}` returns it in
  `headers.etag`) when you upload a source, so a change someone made in between is not overwritten (`412`).
- **Wait for the worker.** After an upload, `GET /versions/{draftId}/jobs` until the newest job is `succeeded`
  (or `failed`: read its `error` and tell the user). A version can be released only after a successful job.
- Models go through files, never through the conversation: `api` with `saveTo` downloads, with `bodyFile` uploads;
  `load` and `save` of a drawing take the same paths. Use a scratch folder.

## 3. Recipes

### Look at a part
1. `api({ path: "/documents", query: { q: "L-bracket" } })` → its `id`.
2. `api({ path: "/documents/<id>/workspaces" })` → `main`'s `draft.id` and artifacts.
3. `api({ path: "/versions/<draftId>/artifacts/part-meta/file" })`: volume, mass, bounding box, material.
   For the model itself: download and `load` it (next recipe) and use `tree`, `snapshot`.

### Change a part
1. Find the draft (above). Note the draft's ETag: `api({ path: "/versions/<draftId>" })` → `headers.etag`.
2. Download the source: `api({ path: "/versions/<draftId>/artifacts/part-source/file", saveTo: "<dir>/<fileName>" })`.
3. `load({ drawing: "new", format: "OFB", path: "<dir>/<fileName>" })`, then `tree`, `run_script`, `snapshot` as
   the ClassCAD skill teaches. Keep the features: change the model, do not rebuild it from scratch.
4. `save({ drawing, format: "OFB", path: "<dir>/<fileName>" })`.
5. Upload into the draft:
   `api({ method: "POST", path: "/versions/<draftId>/artifacts", query: { kind: "part-source" }, bodyFile: "<dir>/<fileName>", headers: { "X-File-Name": "<fileName>", "If-Match": "<etag>" } })`.
6. Wait for the jobs (`/versions/<draftId>/jobs`), then check `part-meta` again.
7. If the user wants it kept as a step: make a version (below).

### Create a new part
1. `api({ method: "POST", path: "/documents", json: { name: "Spacer 20x5", kind: "part", folderId: "<optional>" } })`
   → the document; the number is generated unless you pass `number`. Folders: `GET /folders`.
2. `api({ path: "/documents/<id>/workspaces" })` → `main`'s `draft.id`.
3. Build the part in a new drawing (`run_script` with `drawing: "new"`), `save` it as OFB, upload it as in
   "Change a part", step 5 (no `If-Match` needed for the first source), wait for the jobs.

A STEP file can go in as well: upload it with `kind: "part-origin"`; the worker derives the OFB source from it.

### Make a version, release it
- Version: `api({ method: "POST", path: "/workspaces/<workspaceId>/versions", json: { comment: "Holes 2 mm larger" } })`.
- Release (ask first): `api({ method: "POST", path: "/versions/<versionId>/release", json: { comment: "…" } })`;
  `409` names what is missing (no source, no successful job, …).
- History: `GET /documents/<id>/version-graph`; an older state back into the draft:
  `POST /workspaces/<id>/restore { versionId }` (`409` over unversioned changes unless `force: true`; ask first).
- A branch: `POST /documents/<id>/workspaces { name, fromVersionId }`.

### Change an assembly
An assembly's source is its `asm-json`: instances (placed parts), constraints and templates. A template points
at another document with `reference.location: "pdm://documents/<number>"` (optionally `@<label>` to pin a
revision). Structure changes need no CAD engine:

1. `api({ path: "/versions/<draftId>/artifacts/asm-json/file", saveTo: "<dir>/<name>.asm.json" })`, and the ETag.
2. Edit the JSON: add a template `{ "type": "part", "ident": "Washer", "reference": { "location": "pdm://documents/BRK-2005" } }`
   and an instance `{ "ident": "Washer1", "template": "Washer", "transform": "[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]" }`
   (a 4×4 matrix, row-major, mm; translation in the last column), and constraints if it should be mated
   (copy the shape of the existing ones; `mate.csys` names a coordinate system of the part).
3. Upload with `query: { kind: "asm-json" }`, `X-File-Name` and `If-Match`, then wait for the jobs.
4. `GET /versions/<draftId>/references` and `GET /versions/<draftId>/bom` show what the assembly now uses.

Newer versions of referenced parts: `GET /versions/<draftId>/reference-updates`, apply with
`POST /versions/<draftId>/reference-updates`. Where a part is used: `GET /documents/<id>/where-used`.
To look at an assembly in 3D, `GET /versions/<id>/asm-json/resolved` gives it with loadable URLs.

### Run a generator
A generator is a `code` document: code in a repository that makes models from parameters.
1. Its draft or version: `GET /documents/<id>/workspaces`; its parameters:
   `GET /versions/<versionId>/artifacts/code-manifest/file` (`parameters` with defaults, min, max, units).
2. `api({ method: "POST", path: "/versions/<versionId>/runs", json: { params: { outerDiameter: 150, holeCount: 8 } } })`
   → a run (`200`: an equal run exists already and is reused).
3. `GET /runs/<runId>` until it is done; its outputs are documents of their own (variants), with the usual drafts.

### Materials, exports
- `GET /versions/<id>/material`, `PUT /versions/<draftId>/material { material, appearance }` (from
  `GET /libraries?kind=material-library`).
- A whole assembly as one file: `POST /versions/<id>/exports { format: "STP" | "OFB" }`, then download the new artifact.

## 4. Engines and subagents

Models can be built on the PDM's own servers instead of on this machine: the PDM's ClassCAD (its licence and
version, the one its own editor and workers use), placed on a free worker by its scheduler. Prefer them for
assemblies and whenever several models are worked on at once.

1. `engine_open({ count: 2, purpose: "BRK-2002 flange, BRK-2005 washer" })` → up to two sessions, each with an `id`
   and a `connectUrl`. The PDM hands out only what its servers have room for; `notOpened` says how many it could
   not, so plan with what came back (fewer subagents, or one doing two jobs one after the other). The PDM's MCP keeps
   them alive while it runs.
2. Hand ONE `connectUrl` to ONE subagent, with its task: which document and draft, what to change, where to save
   the file. Its first ClassCAD call is `use_session({ drawing: "new", url: <connectUrl> })`; after that it works as
   usual (`load`, `run_script`, `save`) in the drawing that call named. The url carries the session's token: give it
   to that subagent only, never show it to the user. It also names the session: when the connection drops or the
   ClassCAD MCP restarts, `use_session` with the same url finds the model again, as long as the session is open
   (on an older ClassCAD scheduler it starts empty: load the model again). Read `tree` before going on.
3. The subagent saves the model to a file; the PDM steps (download before, upload after) are done by whoever has the
   PDM's MCP: the main agent, or the subagent itself.
4. `engine_close()` when the subagents are done. The workers are shared with the user's colleagues; the PDM limits
   how many sessions one user holds at once and refuses more with `409`.

Rules for working in parallel:

- **One writer per draft.** Give each subagent its own document. Two uploads into the same draft overwrite each
  other; with `If-Match` the PDM refuses the second (`412`), but plan so it does not happen. Parts first, the
  assembly that uses them last.
- Without engines from the PDM, each drawing runs on the ClassCAD MCP's local engine, which costs several hundred MB;
  it keeps about three awake and parks the others, so more subagents than that wait for each other.
- Versions and releases are the main agent's: make them once the subagents are done and the jobs succeeded.

## 5. Working live with the user

When the user has a model open in the PDM's editor and wants you in it ("join my editor", "look at what I have
open"), work in **their** model instead of a copy:

1. They click **Let Claude in** in the editor's bar (a person icon next to save). If they have not, `editor_join`
   says so: ask them to.
2. `editor_join({ document: "BRK-2002" })` (the number; omit it for the editor they opened last) → a `connectUrl`.
3. `use_session({ drawing: "new", url: <connectUrl> })` of the ClassCAD MCP, then `tree` before anything else: it is
   the user's live model, with what they did so far.
4. Change it with `run_script` as usual. They see every change at once, and you see theirs; read the tree again after
   they worked. Say what you changed.
5. **They save**, from their editor, as always; do not upload this model with `api`. Tell them when you are done.
   When they click **Let Claude out** or close the editor, your drawing is cut off.

The editor's engine may run in their browser; then the ClassCAD MCP on this machine carries the connection, and it
must be the one this session uses.

## 6. When something fails

- `403`: the user may not do this here (their role, the folder). Say so; do not look for another way.
- `404`: wrong id, or a document the user cannot see.
- `503` from `engine_open`: the PDM's ClassCAD has no room right now; work with fewer engines or try later.
- `409`: the state does not allow it (lease held by someone else, archived, released, missing job). The message says why.
- `412`: the draft changed since you read it. Read it again, look at what changed, then decide with the user.
- A failed job: its `error` says why (often an unreadable source). The draft keeps the upload; fix and upload again.

The complete API is described by its OpenAPI document; when you need a route not listed here, ask the user or
use what `GET /documents/{id}`, `/workspaces/{id}` and `/versions/{id}` return to find your way.
