@constructorfleet/sketchforge3d

中文文档
1.30.0发布于 2026/10/28AGPL-3.0-onlyTypeScriptES Module构建来源已验证近 30 天下载 5074

Typed client and macro runner for the SketchForge REST API.

原始使用文档
文档目录11 节
  1. The MCP bridge
  2. Connecting
  3. The client
  4. Macros
  5. Direct command-line operations
  6. Project modes, and why there is no default
  7. Parameters
  8. Examples
  9. Development
  10. Publishing
  11. Local MCP mode

@constructorfleet/sketchforge3d

A typed client for the SketchForge REST API, and a runner for macros: short parameterised scripts that build a part.

npm install @constructorfleet/sketchforge3d

The API is headless -- it reads and writes projects in server-side storage with no browser open -- so a macro is a build script for a design, the way a Makefile is for a binary.

The MCP bridge

This package also carries the SketchForge MCP server. It works against local project files by default; an LLM can pass a url on an individual tool call to target a running SketchForge server instead:

{
  "mcpServers": {
    "sketchforge": {
      "command": "npx",
      "args": ["-y", "-p", "@constructorfleet/sketchforge3d", "sketchforge-mcp"],
      "env": { "SKETCHFORGE_PROJECTS_DIR": "/path/to/SketchForgeProjects" }
    }
  }
}

It speaks stdio JSON-RPC. Without url, project and geometry tools use the local engine and SKETCHFORGE_PROJECTS_DIR, with no browser, HTTP server, or API token. Pass url: "http://127.0.0.1:3000" to a tool call to use a running server; editor tools then require an open editor tab (/?editor=1). Remote REST operations also require SKETCHFORGE_API_TOKEN in the MCP process environment.

Two of its tools run macros -- the scripts this package's runner executes -- and only files already sitting in SKETCHFORGE_MACRO_DIR. There is deliberately no tool that accepts code to run. A Python macro needs sketchforge3d installed from PyPI, since only a checkout carries the Python source.

Connecting

import { SketchForgeClient } from "@constructorfleet/sketchforge3d";

const client = new SketchForgeClient({
  baseUrl: "http://localhost:3000",       // with or without /api/v1
  token: process.env.SKETCHFORGE_API_TOKEN!,
});

const health = await client.check();
if (!health.ok) throw new Error(health.reason);

clientFromEnvironment() does the same from SKETCHFORGE_API_TOKEN and SKETCHFORGE_URL (default http://127.0.0.1:3000). Prefer it: the token is a password for every project on the server and does not belong in a script or in shell history.

The server needs both SKETCHFORGE_API_TOKEN and SKETCHFORGE_PROJECTS_DIR set, or the API answers 404 and 503 respectively. check() tells you which.

The client

new SketchForgeClient({ baseUrl, token, fetch?, timeoutMs?, retryOnBusy?, headers?, expectedRevision? })

client.apiRoot: string
client.check(): Promise<{ ok: true; projects: number } | { ok: false; reason: string }>
client.project(projectId): ProjectHandle

client.projects.list(): Promise<ProjectSummary[]>
client.projects.listPage({ limit?, cursor? }): Promise<{ items: ProjectSummary[]; nextCursor: string | null }>
client.projects.get(projectId): Promise<ProjectDetail>
client.projects.create(name): Promise<ProjectSummary>
client.projects.delete(projectId): Promise<DeleteResult>
client.projects.findByName(name): Promise<ProjectSummary | null>

client.shapes.list(projectId): Promise<ShapeSummary[]>
client.shapes.listPage(projectId, { limit?, cursor? }): Promise<{ items: ShapeSummary[]; nextCursor: string | null }>
client.shapes.create(projectId, shape): Promise<ShapeSummary>
client.shapes.createMany(projectId, shapes): Promise<ShapeWriteResult>
client.shapes.update(projectId, shapeId, changes): Promise<ShapePatchResult>
client.shapes.delete(projectId, shapeId): Promise<DeleteResult>
client.shapes.deleteAll(projectId): Promise<number>

client.edges.list(projectId, shapeId): Promise<EdgeSummary[]>
client.edges.filletableIndexes(projectId, shapeId): Promise<number[]>

client.treatments.apply(projectId, shapeId, { kind, amount, angle?, edges? }): Promise<TreatmentResult>
client.treatments.fillet(projectId, shapeId, radius, edges?): Promise<TreatmentResult>
client.treatments.chamfer(projectId, shapeId, distance, { angle?, edges? }): Promise<TreatmentResult>

client.faces.list(projectId, shapeId, { topology? }?): Promise<FaceSummary[]>
client.faces.facing(projectId, shapeId, direction): Promise<number[]>

client.measurements.list(projectId, shapeId): Promise<MeasurementSummary[]>
client.measurements.withRadius(projectId, shapeId): Promise<MeasurementSummary[]>
client.measurements.between(projectId, from, to): Promise<MeasurementPair>
client.properties.get(projectId, shapeId): Promise<ShapeProperties>
client.interference.check(projectId, { ids?, clearance? }): Promise<InterferenceReport>
client.mirrors.bake(projectId, ids, axis): Promise<BakedMirrorResult>
client.explosions.explode(projectId, shapeId): Promise<ShapeWriteResult>
client.thickness.estimate(projectId, shapeId): Promise<WallThicknessEstimate>
client.drawings.dxf(projectId, { plane, offset, shapeIds?, name? }): Promise<SectionDxfArtifact>
client.exports.export(projectId, { format, shapeIds?, name?, view?, featureAngle?, section? }): Promise<ProjectExportArtifact>

client.booleans.cut(projectId, { solidIds, holeIds }): Promise<BooleanCutResult>
client.groups.group(projectId, ids): Promise<GroupResult>
client.groups.ungroup(projectId, shapeId): Promise<ShapeWriteResult>
client.parts.separate(projectId, shapeId): Promise<ShapeWriteResult>

client.shell.apply(projectId, shapeId, { thickness, openFaces? }): Promise<ShellResult>
client.shell.hollow(projectId, shapeId, thickness): Promise<ShellResult>
client.shell.tray(projectId, shapeId, thickness, openFaces?): Promise<ShellResult>
client.draftPrism.apply(projectId, shapeId, angle): Promise<TreatmentResult>

client.align.apply(projectId, { ids, axis, target, anchorId? }): Promise<AlignResult>
client.align.tops(projectId, ids, anchorId?) | bottoms(...) | centers(projectId, ids, axis, anchorId?)

client.distribute.apply(projectId, { ids, axis?, spacing? }): Promise<DistributeResult>
client.distribute.gaps(projectId, ids, axis?) | pitch(projectId, ids, axis?)

client.patterns.apply(projectId, { ids, kind?, count?, stepX?, stepZ?, centerX?, centerZ?, sweepAngle?, rotateCopies? }): Promise<PatternResult>
client.patterns.row(projectId, ids, count, stepX, stepZ?): Promise<PatternResult>
client.patterns.ring(projectId, ids, count, { x, z }, { sweepAngle?, rotateCopies? }?): Promise<PatternResult>

client.imports.apply(projectId, { fileName, bytes, mediaType? }): Promise<MeshImportResult>
client.imports.readable(fileName): boolean

Set expectedRevision to the revision from a project's previous response to send If-Match: "<revision>" with each write request. A stale value raises SketchForgeApiError with status 409 and code revision_mismatch; the error's revision is the current project revision. Use a client configured for one conditional write at a time, then create a new one with the returned revision for the next write.

shapes.create accepts SketchSweepShapeRequest for server-built sketch sweeps. Its profile contains the closed section path(s), one open spine, and optional sweepFrames; the response retains that data as sketchProfile. It also returns the stable sketchSourceId used by project feature metadata. The REST API accepts profile updates that rebuild persisted sketch sweeps. Send the replacement sketchProfile to PATCH /api/v1/shapes, targeting either the output shape ID or its sketchSourceId; the rebuild preserves the same shape and source-sketch IDs. See the REST sweep update contract.

A ProjectHandle is the same surface with the project id bound, which is what a macro works through:

project.get() | delete() | listShapes() | add(shape) | addMany(shapes) | update(id, changes)
project.remove(id) | clear() | edges(id) | filletableEdges(id)
project.treat(id, treatment) | fillet(id, radius, edges?) | chamfer(id, distance, options?)
project.faces(id, { topology? }?) | facesFacing(id, direction)
project.measurements(id) | measurementsWithRadius(id) | measureBetween(from, to) | propertiesOf(id) | interferenceReport({ ids?, clearance? })
project.sectionDrawing({ plane: "xy", offset: 3, shapeIds? })
project.sectionDxf({ plane: "xy", offset: 3, shapeIds? }) | estimateWallThickness(id) | explodeStepAssembly(id)
project.export({ format, shapeIds?, name?, view?, section? })
project.cut({ solidIds, holeIds }) | group(ids) | ungroup(id) | separate(id)
project.mirrorBaked(ids, axis)
project.shell(id, { thickness, openFaces? }) | hollow(id, thickness) | tray(id, thickness, openFaces?)
project.draftPrism(id, angle)
project.align({ ids, axis, target }) | alignTops(ids, anchorId?) | distribute({ ids, axis?, spacing? })
project.pattern(request) | row(ids, count, stepX, stepZ?) | ring(ids, count, { x, z }, options?)
project.importMesh({ fileName, bytes })

Shapes are described as parameters, with a union per kind so a field that belongs to another kind is a compile error:

await project.addMany([
  { kind: "box", name: "Plate", width: 120, depth: 40, height: 6 },
  { kind: "cylinder", width: 8, height: 12, sides: 6, x: -40, elevation: -1, hole: true },
  { kind: "thread", size: "M8", length: 10, hole: true, x: 40 },
  { kind: "revolve", profile: [[8, 0], [10, 0], [10, 2], [8, 2]], pitch: 6, turns: 6 },
]);

addMany is one write of the project package per request and splits lists longer than the server's 200-shape limit -- which also means a longer list is no longer atomic.

For a retained sketch sweep, replace sketchProfile to rebuild the solid while keeping the same shape and source-sketch IDs:

const sweep = await client.shapes.create(projectId, { kind: "sketchSweep", profile });
const widerProfile = {
  ...sweep.sketchProfile!,
  points: sweep.sketchProfile!.points.map((point) =>
    point.sweepRole === "section" ? { ...point, x: point.x * 1.1 } : point,
  ),
};
await client.shapes.update(projectId, sweep.id, { sketchProfile: widerProfile });

exports.export covers every format in PROJECT_EXPORT_FORMATS -- stl, obj, 3mf, step, skf, zip, svg and dxf -- and returns the file whole as data: Uint8Array, with the server's filename, mimeType, bytes and shapeCount. A STEP export also reports exportedCount and the skipped shapes it could not write as exact solids. view (one of EXPORT_VIEWS) applies only to SVG, featureAngle sets the mesh crease angle from 0 to 180 degrees on SVG projections (default 30; exact solids ignore it), and section ({ plane, offset }) applies to SVG and DXF. The client refuses unsupported option combinations before sending anything, with the server's own message:

import { writeFile } from "node:fs/promises";

const stl = await project.export({ format: "stl", name: "bracket" });
await writeFile(stl.filename, stl.data);
const top = await project.export({ format: "svg", view: "top" });

client.faces.list(projectId, shapeId, { topology: "exact" }) and project.faces(shapeId, { topology: "exact" }) select analytic faces, including plain revolves. Omitting the option preserves the default mesh listing; topology: "mesh" selects it explicitly.

ShapePatch also accepts faceDraft to retain one face draft on a sketch extrusion. The source face is named by its stable ID from GET /faces; the draft stores the signed local pull axis and angle. Profile edits replay it only when one planar face matches its original normal. Send faceDraft: null to clear the operation. .skf preserves the operation and source frame; prism drafts and edge treatments cannot be combined with it yet.

Failures are SketchForgeApiError (status, detail, plus looksDisabled, needsStorage, isProjectBusy, and isLimitExceeded) or SketchForgeTransportError. isLimitExceeded recognises 413 limit_exceeded and the legacy too_many_shapes code. A write that lost the project's lock is retried; a locked shape, which answers 409 for an unrelated reason, is not.

Macros

A macro is a file that exports a definition and builds something when it is handed a connected project:

// macros/bolt-plate.macro.mjs
export default {
  name: "Bolt plate",
  project: { mode: "replace", name: "Bolt plate" },
  params: { holes: 5, spacing: 25, size: "M8", thickness: 8 },

  async run({ project, params, layout, log }) {
    log(`${params.holes} ${params.size} holes`);
    await project.addMany([
      { kind: "box", name: "Plate", width: 200, depth: 40, height: params.thickness },
      ...layout.row(params.holes, params.spacing).map((at, index) => ({
        kind: "thread",
        name: `hole ${index + 1}`,
        size: params.size,
        length: params.thickness + 2,
        hole: true,
        x: at.x,
        z: at.z,
        elevation: -1,
      })),
    ]);
  },
};
SKETCHFORGE_API_TOKEN=... npx sketchforge-macro macros/bolt-plate.macro.mjs --holes=7 --spacing=30

The context carries everything the macro needs -- client, project, params, layout, log -- so a macro file imports nothing and can live in any folder.

Direct command-line operations

Use the same sketchforge-macro executable for project management and file transfer without writing a macro:

npx sketchforge-macro projects list
npx sketchforge-macro projects create "Bracket"
npx sketchforge-macro projects delete <project-id>
npx sketchforge-macro export --format stl --out bracket.stl --project <project-id>
npx sketchforge-macro import bracket.stl

Export uses --project or SKETCHFORGE_PROJECT_ID. Import writes into that selected project when given; without one, it creates a project named after the file. Direct file import supports STL, OBJ, and SVG; export supports STL, OBJ, 3MF, STEP, SKF, ZIP, SVG, and DXF.

Project modes, and why there is no default

The second run is what makes a macro useful or dangerous. A script that appends to a project it found doubles the part every time, and nobody notices until they open the project. So the mode is declared and the runner, not the macro body, acts on it:

projectWhat the runner does firstUse it for
"fresh"Creates a new project, name stamped with the timeSomething you generate several of to compare
{ mode: "replace", name }Finds that project and empties it, or creates itA part you iterate on; re-running is idempotent
{ mode: "reuse", name }Finds or creates it, and leaves the contents aloneAdding a feature to whatever is there
"target"Requires SKETCHFORGE_PROJECT_ID or --project, touches nothing elseOperating on a project someone else named

A module that exports a bare function instead of a definition runs in a fresh project, because that is the only mode that is safe to assume.

Parameters

params is both the defaults and the schema. A command-line override is coerced to the type of its default, and a name the macro does not declare is refused -- so --hoels=7 is an error rather than a silently ignored typo. Parameters are resolved before the project is created, so a bad one costs nothing.

Examples

examples/ holds the four worked macros, which double as the integration tests:

MacroBuilds
bolt-plate.macro.mjsA plate sized around a computed row of tapped holes
bolt-circle.macro.mjsA flange with N tapped holes on a ring of radius R
filleted-box.macro.mjsA box from three numbers, with every roundable edge filleted on the server
tap-drill-plate.macro.mjsA plate cut with an M8 thread through project.cut, exported as STL

Tapped holes are kind: "thread" with hole: true: the form is cut wider by a clearance and the shape is marked as a hole, so grouping it with the plate in the editor or through project.group taps the plate. project.cut can also subtract a thread mesh directly. tap-drill-plate.macro.mjs runs the whole cut-and-export workflow through the API with an M8 thread cutter.

Development

npm run client:build       # compile to dist/, from the repository root
npm run client:typecheck   # including the tests
npm test                   # unit tests, in the repository's vitest run
npm run test:integration   # the examples, against a live server

The integration tests need a real server and are not part of npm test:

SKETCHFORGE_PROJECTS_DIR=$(mktemp -d) SKETCHFORGE_API_TOKEN=test-token \
  npx next dev apps/web -p 3100 &
SKETCHFORGE_API_TOKEN=test-token SKETCHFORGE_URL=http://127.0.0.1:3100 npm run test:integration

Publishing

See docs/PUBLISHING_CLIENTS.md for what has to be configured on npm before the release workflow can publish this package.

Local MCP mode

The MCP uses the extracted engine package directly against project files, with no browser, HTTP server, or API token. Set SKETCHFORGE_PROJECTS_DIR to the directory containing <projectId>.json and <projectId>.skf files:

SKETCHFORGE_PROJECTS_DIR="$HOME/SketchForgeProjects" npm run mcp:sketchforge

The directory uses the same <projectId>.json and <projectId>.skf files as server-side project storage. In local mode, use sketchforge_list_projects to discover projects, sketchforge_create_project to start an empty one, or sketchforge_import_project with a local .skf filePath to add an existing package. sketchforge_import_file imports .stl, .obj, or .svg files into a chosen project; its options are projectId, filePath, and optional url. The source filename supplies the imported shape name, and color and placement use importer defaults. With a url, the file is read from the MCP host and sent as base64 to POST /api/v1/imports using SKETCHFORGE_API_TOKEN. sketchforge_import_stl remains a local-only compatibility alias that also accepts name, color, x, z, and elevation; it rejects url. Local import and export paths are confined to SKETCHFORGE_MCP_FILES_DIR, or to SKETCHFORGE_PROJECTS_DIR when the new setting is unset. Set overwrite: true to replace an existing export; otherwise existing files are preserved. For compatibility, paths are unrestricted when both root variables are unset. Symlinks are resolved before checking the file root. The configured directory is created when needed. These tools return the new projectId for other project tools. sketchforge_read_scene accepts shapeIds, excludeShapeIds, includeRawShapes, and includeRawShapeIds; raw mesh geometry is omitted by default and can be requested for selected shapes. Mutation tools return compact shape metadata, while sketchforge_list_objects always returns object summaries. STL, OBJ, 3MF, ZIP-kit, SKF, STEP, and SVG exports can write to outputPath; the project name supplies the default filename. sketchforge_project_view returns the SVG drawing itself -- the kernel's hidden-line projection of the stored geometry, as the four-view grid or any one named view -- which is the readable picture of a part for an agent without vision. sketchforge_section_view returns section contours as SVG for a named xy, xz, or yz plane and an offset; the plane must cross the selected geometry. Exact shapes are cut by the kernel and mesh shapes are sliced directly, up to 2,000,000 mesh triangles across the selection.

Local mode covers project reads and edits, shape creation/import, arranging, mirroring, grouping, booleans, edge and face operations, measurements and per-shape mass properties, hollowing, mesh and STEP exports, scene topology, clearance and intersection checks, and JavaScript macros. Image capture rasterises that same drawing to a PNG (a named face is one view; home and current are the grid); it does not capture editor chrome or the live viewport camera. The npm MCP client and @constructorfleet/sketchforge-engine are published together by the release workflow. Editor discovery and editor error inspection require a URL and open editor. Python macros require a URL and the HTTP adapter.

版本与发布记录

@constructorfleet/sketchforge3d 共发布 32 个版本,首次发布于 2026/10/20;过去 12 个月发布了 32 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.30.0latest2026/10/2813 天前
1.29.12026/10/2714 天前
1.29.02026/10/2615 天前
1.28.02026/10/2615 天前
1.27.02026/10/2516 天前
1.26.02026/10/2417 天前

关于 @constructorfleet/sketchforge3d 的常见问题

@constructorfleet/sketchforge3d 是什么?

Typed client and macro runner for the SketchForge REST API.

如何安装 @constructorfleet/sketchforge3d?

在项目目录运行 npm install @constructorfleet/sketchforge3d,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 sketchforge-mcp、sketchforge-macro,可以用 npx sketchforge-mcp 直接运行。

@constructorfleet/sketchforge3d 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@constructorfleet/sketchforge3d 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@constructorfleet/sketchforge3d 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@constructorfleet/sketchforge3d 可以商用吗?

@constructorfleet/sketchforge3d 使用 AGPL-3.0-only 许可。最严格的 Copyleft:除分发外,通过网络向用户提供基于它的服务也需要开源对应源码。闭源 SaaS 使用前务必评估。以上为概括,不构成法律意见,请以许可原文为准。

@constructorfleet/sketchforge3d 的下载量有多少?

根据 npm 官方下载统计,@constructorfleet/sketchforge3d 在 2026/10/11 – 2026/11/09 这 30 天内被下载 5,074 次,日均约 169 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@constructorfleet/sketchforge3d 还在维护吗?

最新版本 1.30.0 发布于 2026/10/28(13 天前);至今共发布 32 个版本,其中过去 12 个月发布了 32 个;首次发布于 2026/10/20。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。