kernelcad-parts · diff
git:20261018.64d7ffb to git:20261019.0e8218a
11 added, 8 removed. Audit A to A.
---
name: kernelcad-parts
description: Bundled parts catalog — discover, fetch, and mate standard fasteners, bearings, motors, headers, and connectors. Use whenever the model needs an off-the-shelf component instead of hand-modeled placeholder geometry.
---
# kernelcad-parts
Load this skill when the agent needs an off-the-shelf component (M-series
fastener, deep-groove ball bearing, NEMA stepper, pin header, JST-XH housing)
and does not already know a STEP file path.
The catalog ships **bundled** with the npm package — `npm install` resolves
every bundled id offline. When a query has no bundled match, discovery and
fetch fall through to a built-in public parts catalog by default (see "Remote
tier" below), so kernelCAD can find off-the-shelf parts it does not bundle.
Set `KERNELCAD_PARTS_BASE_URL=off` to stay offline on the bundled seed only.
## Four-tool discovery flow
When the agent does not know which id covers the intent, walk the discovery
chain. Each step narrows the search:
1. `inspect({ of: 'part-categories' })` → top-level buckets (e.g. `fastener`, `bearing`,
`motor`, `connector`, `shaft`).
2. `inspect({ of: 'part-families', category })` → families within the bucket
(e.g. `socket-head-cap-screw`, `deep-groove-ball-bearing`).
3. `find_part` `{ query, family?, standard?, tag?, licenseClass? }` → ranked
records that match the fuzzy query plus filters. Tokens AND-combine;
cross-facet filters AND-combine. `licenseClass` (`'permissive'` |
`'share-alike'` | `'fetch-only'`) filters by redistribution license; records
with no class are treated as `permissive` (the bundled catalog).
4. `fetch_part` `{ id }` → resolves the id to a record, writes the STEP into
the cache, returns the cache path + sha256.
For tasks where the id is already known, skip straight to `fetch_part` or
use the typed `lib.standard.*` shortcuts.
### Fetch-by-URL (parts not in the catalog)
For one handoff part, use
`fetch_part_model({ url, expected_sha256, ref?, bbox_mm? })` for a direct asset,
or `{ catalog_id }` / `{ mpn }` to resolve through `fetch_part` and `find_part`.
Explicit URLs require a 64-character expected SHA-256 and source/license
provenance. The tool caches URL-fetched assets by content digest and compares
STEP bounds, or GLB POSITION accessor bounds when present, dimension-by-
dimension with `bbox_mm` (default relative tolerance 25%). A GLB without usable
accessor bounds reports `not_checked`; missing or ambiguous catalog results are
returned with a specific `errorCode`. This resolves a model asset but does not
place it or perform a fit check.
`fetch_part { url }` pulls geometry on demand from an **allowlisted** host
(GitHub/GitLab raw + release-asset hosts and the kernelCAD parts host) — it is
cached locally, never re-hosted (`redistribution: 'fetch-only'`):
- A `.step`/`.stp` URL → inspected, connectors synthesized, returned as a record.
- A `.stl`/`.dae`/`.obj` URL → returned as a `mesh-import` handle (non-BREP,
`attributes.geometryKind = 'mesh'`) for blockout/reference.
- A **vendor configurator** URL (igus/MISUMI/Pololu/TraceParts) → NOT fetched;
returns `{ kind: 'link_out', url, instruction }`. Download the STEP from the
configurator and ingest it locally with `fetch_part { file }`.
- A disallowed host → `{ error: 'url_host_not_allowed' }`.
## Authoring surface
Inside a `.kcad.ts` script, three entry points cover the matrix:
```typescript
// 1. Generic verbs — work for ANY id (bundled or remote).
const motor = await lib.fetchPart('nema-17');
const matches = await lib.findPart('M3 screw');
// 2. Typed shortcuts — bundled-only, autocomplete-friendly.
const bolt = await lib.standard.boltSHCS({ thread: 'M3', lengthMm: 12 });
const nut = await lib.standard.nutHex({ thread: 'M3' });
const washer = await lib.standard.washerFlat({ thread: 'M3' });
const bearing = await lib.standard.bearing608();
const stepper = await lib.standard.nema17();
const header = await lib.standard.pinHeader254({ pins: 8 });
const conn = await lib.standard.jstXH({ pins: 4 });
// 3. Table-driven fasteners (lib.part / lib.stack) and hole-driven insert.
const screw = await lib.part({ family: 'socket-head-cap-screw', thread: 'M5', lengthMm: 16 });
const stack = await lib.stack({ thread: 'M5', gripMm: 10, mode: 'bolt' });
const fitted = await lib.insert(plate, { diameterMm: 5.5, family: 'socket-head-cap-screw', mode: 'bolt' });
// 4. Machining-fixture hardware.
const set = await lib.fixture.clampSet({ thread: 'M10', workpieceHeightMm: 36 });
const tNut = await lib.fixture.tSlotNut({ thread: 'M10' });
const stud = await lib.fixture.clampStud({ thread: 'M10', lengthMm: 80 });
const clamp = await lib.fixture.strapClamp({ thread: 'M10' });
const block = await lib.fixture.stepBlock({ thread: 'M10' });
```
`lib.fetchPart('iso-4762-m3x12')` and
`lib.standard.boltSHCS({ thread: 'M3', lengthMm: 12 })` resolve to the same
record; pick the typed call when arguments are static, the generic call
when ids are computed. `lib.part` resolves the same catalog ids from the
family table and builds the envelope in memory when no STEP is on disk.
## Fastener table — lib.part and lib.stack
`lib.part({ family, thread, lengthMm })` builds one stocked fastener:
socket-head (ISO 4762), button-head (ISO 7380) and countersunk (ISO 10642)
screws, hex nuts (ISO 4032), lock nuts (DIN 985), flat washers (ISO 7089)
- and lock washers (DIN 127B), M2 through M6. Screws need a stocked length;
+ and lock washers (DIN 127B), M2 through M12. Screws need a stocked length;
an unknown family, thread or length fails with a diagnostic that names the
stocked values. Every solid is an envelope (head plus shank), no helical
thread.
`lib.stack({ thread, gripMm, mode })` returns `lengthMm` plus the members,
each positioned against a seat at z = 0 (the grip's top face).
`mode: 'screw'` places the screw alone; `mode: 'bolt'` adds a washer under
the head and a washer plus hex nut on the back. The screw length is the
shortest stocked one at or above grip + washer + nut (for socket heads —
the only family `lib.stack` plans).
## Hole-driven insert — lib.insert
```typescript
const fitted = await lib.insert(plate, {
diameterMm: 5.5, // or near: [x, y, z] to seed from one hole
family: 'socket-head-cap-screw', // or button-head-cap-screw / flat-head-countersunk
mode: 'bolt', // or 'screw'
gripMm: 6, // defaults to the bore's wall length
seat: 'auto', // 'none' or 'counterbore' also accepted
});
// fitted.shape — the plate with seats cut
// fitted.holes — one placed stack per hole: screw (+ headWasher, tailWasher, nut)
```
The holes already on the shape drive everything. Sizing reads the bore
- diameter against ISO 273 normal clearance (Ø2.4 M2, Ø2.9 M2.5, Ø3.4 M3,
- Ø4.5 M4, Ø5.5 M5, Ø6.6 M6). Selection covers every similar hole (same
+ diameter against ISO 273 clearance (Ø2.4 M2, Ø2.9 M2.5, Ø3.4 M3, Ø4.5 M4,
+ Ø5.5 M5, Ø6.6 M6, Ø9 M8, Ø11 M10, Ø13.5 M12 normal; close and loose bores
+ also size, and `holes[i].fit` reports `'close' | 'normal' | 'loose'`).
+ Selection covers every similar hole (same
diameter, parallel axis), so a patterned hole is covered without naming the
pattern. The seat follows the family: flat-head cuts a 90° countersink to
the head diameter, cap heads cut nothing unless `seat: 'counterbore'`.
Through holes take the head at the end nearest `near` (or the larger Z);
`headAt` overrides. A blind hole takes `mode: 'screw'` and the longest
stocked length that fits; `mode: 'bolt'` requires a through bore.
Two bores need a second look:
- A bore that sits exactly on a larger thread's tap drill (ISO 2306:
Ø2.5→M3, Ø3.3→M4, Ø4.2→M5, Ø5.0→M6, Ø6.8→M8) is reported on the hole as
`pilotFor: 'M4'`. The detector cannot tell a tapped pilot from clearance,
so check `pilotFor` before trusting the size.
- A coaxial larger bore that already reaches the mouth (a counterbore) is
reported as `existingSeatMm`; `seat: 'auto'` then leaves the hole alone
instead of cutting a second seat into it.
## Machining-fixture hardware — lib.fixture
`lib.fixture.*` is the clamping kit for a T-slot machine table, keyed by the
thread the hardware uses (M8 / M10 / M12). T-slot nuts follow the DIN 508
table and collar nuts DIN 6331; strap clamps and step blocks have no DIN table
and stay nominal kit dimensions. Check the supplier drawing before ordering.
- `tSlotNut({ thread })` — T-slot nut (DIN 508): slot 10 / 12 / 14 mm for
M8 / M10 / M12; heights 12 / 14 / 16 mm, head height 6 / 7 / 8 mm. Bore
axis +Z, top face flush at z = 0, base below.
Connectors: `mating-face` (wing tops), `top-face`, `thread-bore`.
- `clampStud({ thread, lengthMm })` — clamping stud (DIN 6379 form); stocked
lengths 63 / 100 (M8), 80 / 125 (M10), 100 / 160 (M12). Connectors:
`table-end`, `clamp-end`, `axis`.
- `flangeNut({ thread })` — collar (flange) nut (DIN 6331 form) for the
clamp top: across flats 13 / 16 / 18 mm, height 12 / 15 / 18 mm, collar
18 / 22 / 25 mm.
Connectors: `mating-face`, `top-face`, `inner-bore`.
- `strapClamp({ thread })` — strap clamp; the slot center is the origin and
the nose points toward -X, so `slot-axis` is where the stud goes.
Connectors: `nose-bearing`, `tail-bearing`, `slot-axis`, `top-face`.
- `stepBlock({ thread })` — step block; treads rise along +X at 10 / 20 / 30 /
40 (M8), 12 / 24 / 36 / 48 (M10), 15 / 30 / 45 / 60 (M12). Connectors:
`base-face`, `step-1` … `step-4` on the treads.
- `clampSet({ thread, workpieceHeightMm })` — one call for a complete set:
T-slot nut, clamping stud, strap clamp, step block and flange nut, placed in
one frame (stud axis at the origin, table surface at z = 0, workpiece toward
-X, block at +X, clamp level with the workpiece). The workpiece height must
equal a stocked tread of that thread's block; the stud is the shortest
stocked length that clears clamp + nut. Returns `{ studLengthMm, treadMm,
clampTopMm, members }`; mirror the set for the other side of the workpiece.
## Pre-shipped connector convention
Every bundled record carries connector frames so the part participates in
assemblies without any `partRef.connector(...)` boilerplate. Naming follows
the @kc grammar (kebab, alpha-leading) so refs round-trip.
| Family | Connectors |
|-------------------------------|-------------------------------------------------------------|
| socket-head-cap-screw | `head-bearing`, `thread-tip`, `head-top` |
| button-head-cap-screw | `head-bearing`, `thread-tip`, `head-top` |
| flat-head-countersunk | `mating-face`, `thread-tip` |
| hex-nut / lock-nut | `mating-face`, `top-face`, `inner-bore` |
| flat-washer / lock-washer | `mating-face`, `top-face`, `inner-bore` |
| heat-set-insert | `mating-face`, `top-face`, `inner-bore` |
| deep-groove-ball-bearing | `inner-bore`, `outer-face`, `mating-face`, `top-face` |
| linear-shaft | `end-a`, `end-b`, `axis` |
| stepper-motor | `mounting-face`, `output-shaft`, `back-face`, `bolt-holes-1..4` |
| pin-header (2.54 / 1.27 mm) | `mating-face`, `pin-1` |
| jst-xh | `mating-face`, `back-face` |
The `head-bearing` frame on a bolt points DOWN (-Z), so mating it against a
bracket's hole top face places the bolt with the shank sticking into the
hole — the canonical "bolt through bracket" configuration with no manual
flip.
## Mate API (v0.6) — 4-arg only
Canonical form is **`arm.mate(name, aRef, bRef, type, opts?)`** where
`aRef` / `bRef` are `'partName.connectorName'` strings and `type` is one of
`fastened` | `revolute` | `prismatic` | `cylindrical` | `planar` | `ball` |
`pin_slot`.
**Do not** use the retired 3-arg form `arm.mate(a, b, { kind })` — capture
rejects it. Catalog cookbooks and `kernelcad-assemblies` all use 4-arg.
## The universal `bolt-holes-N` auto-rule
Any `.hole(...)` or `.holes(...)` feature on an authored or imported Shape
automatically receives `bolt-holes-N` connector frames at the hole's
bottom face plus through-axis. Names are numbered first by feature order,
then by `(u, v)` to break ties, so the same script always emits the same
connector names across re-runs.
**Caveat (agent-facing):** those auto frames attach to the **holes feature
id**, not the assembly part name. A mate ref like `'bracket.bolt-holes-1'`
will not resolve after `arm.part('bracket', shapeWithHoles)`. Prefer
**manual** `partRef.connector('bolt-holes-N', { type: 'frame', origin:
{ kind: 'vec3', value: [...] } })` on the part (same pattern as cookbook
`nema-motor-mounting-plate`). Bundled / catalog parts already expose
`bolt-holes-N` on the **part** itself — those refs work without a manual
frame.
```typescript
const arm = assembly('arm');
const bracketShape = box(40, 20, 3).holes('top', {
positions: [{ u: -10, v: 0 }, { u: 10, v: 0 }],
diameter: 3.2,
depth: 'through',
});
const bracket = arm.part('bracket', bracketShape);
// Manual frame on the PART — .holes() auto-connectors stay on the feature id.
bracket.connector('bolt-holes-1', {
type: 'frame',
origin: { kind: 'vec3', value: [-10, 0, 1.5] },
});
const bolt = await lib.standard.boltSHCS({ thread: 'M3', lengthMm: 10 });
arm.part('bolt', bolt);
// Catalog bolt already has head-bearing on the part; plate side is manual.
arm.mate('bolt-1', 'bolt.head-bearing', 'bracket.bolt-holes-1', 'fastened');
return arm.model();
```
## `@kc[...]` ref form
Bundled connectors and the bracket-side auto-emitted ones expose the same
ref grammar as authored faces and edges:
```
@kc[bolt/connector/head-bearing] // typed shortcut for bolt.head-bearing
@kc[bracket/connector/bolt-holes-1] // typed shortcut for bracket.bolt-holes-1
```
Either form works as the source / target of `add_mate` and survives
`query({ mode: 'resolve' })`. The dot form is shorter for human-authored scripts;
the @kc form is canonical in MCP tool outputs and diagnostic payloads.
See `kernelcad-mcp` for the full grammar.
## Bundled-only by default
- Bundled tier coverage (293 records on the seed catalog):
+ Bundled tier coverage (422 records on the seed catalog):
- - M2 / M2.5 / M3 / M4 / M5 / M6 socket head, button head, flat head
- countersunk screws (ISO 4762 / 7380 / 10642).
- - M2 / M2.5 / M3 / M4 / M5 / M6 hex nuts, lock nuts, flat / lock washers
- (ISO 4032 / DIN 985 / ISO 7089 / DIN 127B).
+ - M2 / M2.5 / M3 / M4 / M5 / M6 / M8 / M10 / M12 socket head and flat head
+ countersunk screws (ISO 4762 / 10642); button head M3–M10 (ISO 7380).
+ - M2 / M2.5 / M3 / M4 / M5 / M6 / M8 / M10 / M12 hex nuts and flat washers
+ (ISO 4032 / ISO 7089); lock nuts and lock washers M3–M12
+ (DIN 985 / DIN 127B).
- M2.5 / M3 / M4 heat-set inserts (3.8 mm and 5.7 mm length).
- Deep-groove ball bearings 608 / 623 / 624 / 625 / 626 / 6800 / 688 / 6900.
- Linear shaft Ø3..Ø12 × 20..200 mm.
- Spur gears module 1 / 2, 12–40 teeth (20° pressure angle, bored Ø5 / Ø6).
- NEMA 8 / 11 / 14 / 17 / 23 stepper motor envelopes with auto-emitted
4-bolt-hole connector frames at the standard bolt circle.
- 2.54 mm and 1.27 mm pin headers, 2–20 pins, straight and right-angle.
- JST-XH housings, 2 / 3 / 4 / 5 / 6 pin.
- Machining-fixture hardware M8 / M10 / M12: T-slot nuts (slot 10 / 12 /
14 mm), clamping studs (63–160 mm), flange nuts, strap clamps (100 / 125 /
160 mm) and step blocks (40 / 48 / 60 mm of treads), envelope solids with
placement connectors (`lib.fixture.*`).
Bundled parts ship under kernelCAD's MIT license — see `record.license`.
## Remote tier — built-in public catalog (default)
When a query has no bundled match, discovery and fetch fall through to a
built-in public parts catalog automatically — this is how kernelCAD finds
off-the-shelf parts (motors, bearings, connectors, brackets) it does not bundle.
No configuration is required.
```typescript
// Falls through to the public catalog when not bundled — nothing to configure.
const matches = await lib.findPart('flanged linear bearing');
const bearing = await lib.fetchPart('linear-bearing-lmk10luu');
```
What kernelCAD adds on top of the raw catalog, so a *found* part is as usable as
a bundled one:
- **Byte integrity.** The STEP is downloaded to `~/.cache/kernelcad/parts/` and
verified against the catalog's `sha256` before use.
- **Synthesized connectors.** Catalog STEP ships no connector frames, so they
are derived from the geometry at fetch time: `mating-face` / `top-face` from
the bounding box, `bolt-holes-N` from detected mounting holes (coaxial
segments collapsed; deterministically numbered), and `bore` from a distinct
central hole. The part participates in `add_mate` with no manual
`partRef.connector(...)`. Oddly-shaped parts may still need a hand-authored
frame — inspect the synthesized set before relying on it.
- **Provenance.** The default catalog is built from a license-clean mechanical
parts library (CC-BY-3.0), so each record carries its own `license` (commonly
`'CC-BY-3.0'`) plus `attribution`. Read `record.license` / `record.attribution`
rather than assuming — a record's terms govern it. Keep the attribution when
shipping a deliverable that embeds the geometry.
**Overriding or disabling the source:**
- Point at a different catalog (e.g. a self-hosted index serving the same
`/v1/parts` schema) with `partsBaseUrl` per call or the
`KERNELCAD_PARTS_BASE_URL` env var.
- Set `KERNELCAD_PARTS_BASE_URL=off` (or `none`) to disable the remote tier
entirely; unmatched lookups then return `parts.fetch.remote-disabled` and you
stay fully offline on the bundled seed.
Bytes returned from the remote tier are cached under
`~/.cache/kernelcad/parts/<sha256>.step` with sha256 verification at write
time, so a flaky upstream cannot poison the cache.
## License and provenance
Every record carries `license` + optional `attribution`. The assembly's
provenance manifest records which parts came from where so downstream
review (CHANGELOG, license audit) has a single source of truth.
## Cookbook snippets
### M3 bolt through bracket (fastened mate)
```typescript
const arm = assembly('arm');
const bracketShape = box(40, 20, 3).holes('top', {
positions: [{ u: -10, v: 0 }, { u: 10, v: 0 }],
diameter: 3.2,
depth: 'through',
});
const bracket = arm.part('bracket', bracketShape);
bracket.connector('bolt-holes-1', {
type: 'frame',
origin: { kind: 'vec3', value: [-10, 0, 1.5] },
});
const bolt = await lib.standard.boltSHCS({ thread: 'M3', lengthMm: 10 });
arm.part('bolt', bolt);
arm.mate('bolt-1', 'bolt.head-bearing', 'bracket.bolt-holes-1', 'fastened');
return arm.model();
```
### 608 bearing on an 8 mm shaft (cylindrical mate)
There is no `lib.standard.*` shortcut for linear shafts — fetch the catalog
record by id (`shaft-d<diameter>-l<length>`, Ø3..Ø12 × 20..200 mm). The
shaft's `axis` connector mates into the bearing's `inner-bore`. Catalog
parts already carry those connectors on the **part** — no manual frames.
Use the 4-arg mate (see cookbook `shaft-and-bearing-cylindrical-mate` for
the offline BREP stand-in when the catalog is absent).
```typescript
const arm = assembly('arm');
const shaft = await lib.fetchPart('shaft-d8-l50'); // Ø8 × 50 mm linear shaft
const bearing = await lib.standard.bearing608(); // Ø8 bore deep-groove bearing
arm.part('shaft', shaft);
arm.part('bearing', bearing);
arm.mate('seat', 'shaft.axis', 'bearing.inner-bore', 'cylindrical');
return arm.model();
```
### NEMA 17 mounted to a plate
Catalog `nema17` already exposes `bolt-holes-1..4` on the **motor part**.
Plate-side `.holes()` auto-connectors stay on the holes **feature id**, so
declare matching frames on the plate part (cookbook
`nema-motor-mounting-plate`).
```typescript
const arm = assembly('arm');
const plateShape = box(80, 80, 5).holes('top', {
positions: [
{ u: -15.5, v: -15.5 }, { u: 15.5, v: -15.5 },
{ u: -15.5, v: 15.5 }, { u: 15.5, v: 15.5 },
],
diameter: 3.2,
depth: 'through',
});
const plate = arm.part('plate', plateShape);
const offs = [[-15.5, -15.5], [15.5, -15.5], [-15.5, 15.5], [15.5, 15.5]] as const;
offs.forEach(([u, v], i) => {
plate.connector(`bolt-holes-${i + 1}`, {
type: 'frame',
origin: { kind: 'vec3', value: [u, v, 2.5] },
});
});
const motor = await lib.standard.nema17();
arm.part('motor', motor);
arm.mate('b1', 'motor.bolt-holes-1', 'plate.bolt-holes-1', 'fastened');
arm.mate('b2', 'motor.bolt-holes-2', 'plate.bolt-holes-2', 'fastened');
arm.mate('b3', 'motor.bolt-holes-3', 'plate.bolt-holes-3', 'fastened');
arm.mate('b4', 'motor.bolt-holes-4', 'plate.bolt-holes-4', 'fastened');
return arm.model();
```
## Diagnostic recovery map
| Code | When | Recover by |
|-----------------------------------|-------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
| `parts.input.id-or-query-required`| `find_part` / `fetch_part` called with no id and no query. | Pass `id` (known record) OR `query` (fuzzy). |
| `parts.fetch.offline-and-uncached`| Remote needed, network unreachable, cache miss. | Call `find_part { source: 'local' }`; or restore network. |
| `parts.fetch.checksum-mismatch` | Downloaded bytes sha256 ≠ record sha256. | Bytes discarded; do not retry against the same endpoint without verifying upstream. |
| `parts.fetch.checksum-drift` | Cached bytes still match; remote now reports a different sha256. | Re-fetch with `--refresh-parts-cache` to opt in to the new bytes. |
| `parts.fetch.api-error` | Remote returned non-2xx or network failed. | Retry later, check `partsBaseUrl` reachability, or fall back to `source: 'local'`. |
| `parts.fetch.remote-disabled` | A tool needed remote, but no `partsBaseUrl` (arg or env) was set. | Pass `partsBaseUrl`, set `KERNELCAD_PARTS_BASE_URL`, or stick to bundled ids. |
### Verify installed catalog screws
For a placed catalog screw use `verify({ check: 'fastener-installation', file,
fasteners: [{ part: 'screw', against: ['washer', 'plate'], engagementPart: 'nut',
minEngagementMm: 2, maxProtrusionMm: 1 }] })`. The names are assembly instances,
not catalog ids. The verifier uses retained hash-bound catalog interfaces and
actual BREP geometry/world poses. Locally imported catalog sidecars qualify only
when the imported STEP bytes match the catalog hash and sidecar identity validates.
Flat annular-bearing screws and full open cylindrical bores are supported;
unsupported heads, manual primitives/metadata, blind or ambiguous bores and failed
probes are unknown. Empty stations produce `no-coverage`, never a passing audit.
Measured axial engagement and protrusion meet user-declared geometric limits;
simplified catalog threads cannot prove thread contact, strength, torque or preload.