Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions CLAUDE.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ MCP server that gives LLMs the ability to author, inspect, and iterate on 3D CAD

Part of the [OCCTSwift ecosystem](https://github.com/SecondMouseAU/OCCTSwift/blob/main/docs/ecosystem.md) — see the ecosystem map for how this package sits on top of the kernel, viewport, bridge, and AIS layers. SemVer-stable from v1.0.0.

The Swift implementation calls OCCT directly in-process — no subprocess, no JSONL marshalling — and exposes 73 typed MCP tools that cover authoring, scene reads, mutation, introspection, construction, analysis, I/O, mesh, drawing, selection / remap, mesh-zone analysis, mesh inspection, alignment, and dimension overlays.
The Swift implementation calls OCCT directly in-process — no subprocess, no JSONL marshalling — and exposes 74 typed MCP tools that cover authoring, scene reads, mutation, introspection, construction, analysis, I/O, mesh, drawing, selection / remap, mesh-zone analysis, mesh inspection, alignment, and dimension overlays.

## How It Works

Expand All @@ -23,7 +23,7 @@ For novel geometry the typed tools don't cover, the LLM falls back to `execute_s

## Tools

73 tools, organized below. Call `get_api_reference({ category: "mcp_tools" })` to dump every tool's JSON Schema in one shot — useful for LLM auto-discovery. Most flows can answer "what's the volume?", "make it red", "boolean-subtract these", "render a preview", "add a dimension between these two faces", "export to STEP", and "draw this" without ever touching `execute_script`.
74 tools, organized below. Call `get_api_reference({ category: "mcp_tools" })` to dump every tool's JSON Schema in one shot — useful for LLM auto-discovery. Most flows can answer "what's the volume?", "make it red", "boolean-subtract these", "render a preview", "add a dimension between these two faces", "export to STEP", and "draw this" without ever touching `execute_script`.

### Authoring

Expand Down Expand Up @@ -124,6 +124,7 @@ The mesh-domain check-list / measurement surface (Phase 2 of the mesh-analysis e
| `detect_symmetry` | Detect reflective (mirror-plane) symmetry: 3 PCA candidate planes through the area-weighted centroid, each verified by reflecting sampled points and measuring their residual distance back to the surface. Rotational/axis symmetry detection is deferred to a later phase |
| `align_bodies` (#104) | GOM-style alignment: register a source body onto a reference body via point-to-plane ICP (PCA pre-align + normal-space sampling + trimmed correspondence). `mode: "bestFit"` (default, full pipeline) or `"preAlign"` (coarse PCA/bbox pose only). Returns the recovered transform (row-major, translation + axis-angle rotation) and residual stats; `apply: true` writes it onto the source body in place with the same history semantics as `transform_body`. The step scan-vs-CAD deviation tools need before their numbers mean anything |
| `mesh_curvature` | Per-vertex discrete curvature (Rusinkiewicz per-face tensor) over a body's own welded mesh: principal curvatures k1/k2, mean, gaussian, plus a colored render (`colorBy`) and bounded stats (medians, flatFraction, highCurvatureFraction). No reference body needed |
| `detect_mesh_features` (#108) | Crease-ring feature outlines (doors, panels, window returns, recesses) on a raw scan mesh via dihedral-fold-edge detection: welds the mesh, chains fold edges exceeding `minAngleDegrees` into closed rings and open paths (largest-first), for meshes where `recognize_features` (BREP/AAG) has no B-rep structure to work against. Junction-aware (Y/T intersections split cleanly). Reports each ring's `containingZones` when `segment_mesh_zones` has already run for the body. Optional render: the surface plus each ring as its own categorically-colored wireframe overlay |

### Selection & remap

Expand Down Expand Up @@ -195,7 +196,7 @@ LLM read/write over an attributed reconstruction graph — annotate per-node dec

This repo ships two implementations side-by-side:

- **Swift** (`Sources/`, `Package.swift`) — the **primary** server. In-process against OCCTSwift / OCCTSwiftMesh / OCCTSwiftTools / OCCTSwiftAIS / DrawingComposer using the [official Swift MCP SDK](https://swiftpackageindex.com/modelcontextprotocol/swift-sdk). 73 tools. macOS 15+ (the OCCT.xcframework arm64 platform).
- **Swift** (`Sources/`, `Package.swift`) — the **primary** server. In-process against OCCTSwift / OCCTSwiftMesh / OCCTSwiftTools / OCCTSwiftAIS / DrawingComposer using the [official Swift MCP SDK](https://swiftpackageindex.com/modelcontextprotocol/swift-sdk). 74 tools. macOS 15+ (the OCCT.xcframework arm64 platform).
- **Node / TypeScript** (`src/`, `dist/`) — the original implementation. Shells out to the `occtkit` CLI for everything Swift-side. 37 tools (the pre-v0.4 surface; selection / remap / annotations are Swift-only). Useful if you can't run a macOS binary.

Both speak stdio MCP and read/write the same manifest format.
Expand Down
41 changes: 40 additions & 1 deletion Sources/OCCTMCPCore/Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public enum OCCTMCPVersion {
public static let serverName = "occtmcp"
/// Keep in step with the release tag: clients report this string, and a
/// stale value makes version triage ambiguous (noted in #75).
public static let serverVersion = "1.25.0"
public static let serverVersion = "1.26.0"
}

/// Shared by the three tools that share DeviationTools' signed-distance engine
Expand Down Expand Up @@ -1407,6 +1407,24 @@ func catalogTools() -> [Tool] {
"additionalProperties": .bool(false),
])
),
Tool(
name: "detect_mesh_features",
description: "Crease-ring feature outlines (doors, panels, window returns, recesses) on a raw scan mesh via dihedral-fold-edge detection (OCCTSwiftMesh.Mesh.creaseEdges, OCCTSwiftMesh#28), for meshes where recognize_features (BREP/AAG) cannot operate at all — a scanned/STL body has no B-rep face/edge structure to recognize features against. Meshes the body, welds it (MANDATORY precondition: on unwelded input every edge is a boundary edge and the dihedral angle is undefined, so zero creases are ever found regardless of the body's actual geometry), then chains dihedral-fold edges exceeding minAngleDegrees into closed rings (e.g. a door outline) and open paths (a crease running off an open mesh boundary), largest-first. Y/T junctions where 3+ creases meet split cleanly into separate rings/paths rather than being wandered through arbitrarily; leftover edges that couldn't be chained are counted in unchainedCreaseEdgeCount, never dropped. When segment_mesh_zones has already been run for this body (same mesh state, verified by signature), each ring reports containingZones: the zone id(s) whose triangles are incident to the ring's own vertices, majority first — omitted with a warning if the zone table is stale or the internal weld guard failed, omitted silently (no warning) if no zones are registered for this body at all. Optional render: the body surface as a neutral translucent grey mesh, plus each ring as its own categorically-colored wireframe overlay with a legend.",
inputSchema: .object([
"type": .string("object"),
"properties": .object([
"bodyId": .object(["type": .string("string")]),
"minAngleDegrees": .object(["type": .string("number"), "exclusiveMinimum": .double(0), "maximum": .double(180), "description": .string("Dihedral fold-angle threshold in degrees; an edge whose two triangles' normals differ by at least this much is a crease. Default 30.")]),
"maxRings": .object(["type": .string("integer"), "minimum": .int(1), "description": .string("Cap on returned rings/paths; the largest (by length) are kept, the rest counted in a warning. Default 64.")]),
"deflection": .object(["type": .string("number"), "description": .string("Mesh linear deflection. Default 0.5% of the body's bbox diagonal.")]),
"render": .object(["type": .string("boolean"), "description": .string("Render the body with each ring overlaid as a categorically-colored wireframe, with a legend. Default true.")]),
"renderPath": .object(["type": .string("string"), "description": .string("Override the default render path (<output_dir>/<bodyId>_features.png).")]),
"options": .object(["type": .string("object"), "description": .string("Render options — same shape as render_preview.options (camera, width, height, background).")]),
]),
"required": .array([.string("bodyId")]),
"additionalProperties": .bool(false),
])
),
]
}

Expand Down Expand Up @@ -2334,6 +2352,27 @@ func dispatch(callName: String, arguments: [String: Value]) async -> CallTool.Re
options: parseRenderOptions(arguments["options"])
).asCallToolResult()

case "detect_mesh_features":
guard let bodyId = arguments["bodyId"]?.stringValue else {
return ToolText("detect_mesh_features requires `bodyId`.", isError: true).asCallToolResult()
}
// Dispatch-level guard (the #106 convention): an invalid minAngleDegrees must error here
// too, not just inside the tool function — this is the layer an MCP client's own schema
// validation can be bypassed at.
let minAngle = arguments["minAngleDegrees"]?.numberValue ?? 30
guard minAngle > 0, minAngle <= 180 else {
return ToolText("detect_mesh_features: minAngleDegrees must be in (0, 180].", isError: true).asCallToolResult()
}
return await MeshFeatureTools.detectMeshFeatures(
bodyId: bodyId,
minAngleDegrees: minAngle,
maxRings: arguments["maxRings"]?.intValue ?? 64,
deflection: arguments["deflection"]?.numberValue,
render: arguments["render"]?.boolValue ?? true,
renderPath: arguments["renderPath"]?.stringValue,
options: parseRenderOptions(arguments["options"])
).asCallToolResult()

case "fit_primitives":
guard let bodyId = arguments["bodyId"]?.stringValue else {
return ToolText("fit_primitives requires `bodyId`.", isError: true).asCallToolResult()
Expand Down
2 changes: 1 addition & 1 deletion Sources/OCCTMCPCore/Tools/FitPrimitivesTools.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ public enum FitPrimitivesTools {
return IntrospectionTools.encode(FitReport(
bodyId: bodyId, zoneId: zoneId, strategy: strategyLabel, strategyScores: strategyScores,
primitives: [], uncoveredFraction: uncoveredFraction, renderPath: nil,
warnings: warnings + ["No primitive met minSupportTriangles; nothing to report."]
warnings: warnings + ["No primitives to report: none met minSupportTriangles, or maxPrimitives removed them all (see any cap warning above)."]
))
}

Expand Down
Loading