fluidcad 0.0.32 → 0.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/bin/commands/init.js +55 -0
- package/bin/commands/mcp.js +33 -0
- package/bin/commands/serve.js +77 -0
- package/bin/fluidcad.js +15 -107
- package/lib/dist/common/scene-object.d.ts +4 -1
- package/lib/dist/common/scene-object.js +9 -2
- package/lib/dist/common/solid.d.ts +4 -1
- package/lib/dist/common/solid.js +13 -0
- package/lib/dist/core/2d/tarc.d.ts +20 -2
- package/lib/dist/core/2d/tarc.js +24 -0
- package/lib/dist/core/index.d.ts +2 -1
- package/lib/dist/core/index.js +1 -0
- package/lib/dist/core/interfaces.d.ts +107 -2
- package/lib/dist/core/load.d.ts +2 -2
- package/lib/dist/core/repeat.js +62 -46
- package/lib/dist/core/rib.d.ts +18 -0
- package/lib/dist/core/rib.js +37 -0
- package/lib/dist/features/2d/arc.d.ts +8 -2
- package/lib/dist/features/2d/arc.js +94 -17
- package/lib/dist/features/2d/back.js +3 -2
- package/lib/dist/features/2d/sketch.d.ts +4 -0
- package/lib/dist/features/2d/sketch.js +21 -0
- package/lib/dist/features/2d/tarc-constrained.d.ts +2 -0
- package/lib/dist/features/2d/tarc-constrained.js +8 -0
- package/lib/dist/features/2d/tarc-radius-to-object.d.ts +16 -0
- package/lib/dist/features/2d/tarc-radius-to-object.js +58 -0
- package/lib/dist/features/2d/tarc-to-object.d.ts +18 -0
- package/lib/dist/features/2d/tarc-to-object.js +66 -0
- package/lib/dist/features/2d/tarc-to-point-tangent.d.ts +2 -0
- package/lib/dist/features/2d/tarc-to-point-tangent.js +3 -0
- package/lib/dist/features/2d/tarc-to-point.d.ts +2 -0
- package/lib/dist/features/2d/tarc-to-point.js +3 -0
- package/lib/dist/features/2d/tarc-with-tangent.d.ts +2 -0
- package/lib/dist/features/2d/tarc-with-tangent.js +3 -0
- package/lib/dist/features/2d/tarc.d.ts +2 -0
- package/lib/dist/features/2d/tarc.js +3 -0
- package/lib/dist/features/extrude-base.d.ts +9 -0
- package/lib/dist/features/extrude-base.js +22 -0
- package/lib/dist/features/extrude-to-face.js +1 -5
- package/lib/dist/features/extrude-two-distances.js +1 -2
- package/lib/dist/features/extrude.js +1 -2
- package/lib/dist/features/load.d.ts +6 -0
- package/lib/dist/features/load.js +53 -1
- package/lib/dist/features/mirror-feature.d.ts +3 -2
- package/lib/dist/features/mirror-feature.js +1 -1
- package/lib/dist/features/repeat-circular.d.ts +3 -3
- package/lib/dist/features/repeat-circular.js +8 -1
- package/lib/dist/features/repeat-linear.d.ts +4 -2
- package/lib/dist/features/repeat-linear.js +10 -1
- package/lib/dist/features/repeat-matrix.d.ts +3 -1
- package/lib/dist/features/repeat-matrix.js +7 -2
- package/lib/dist/features/rib.d.ts +31 -0
- package/lib/dist/features/rib.js +321 -0
- package/lib/dist/features/select.d.ts +1 -0
- package/lib/dist/features/select.js +81 -10
- package/lib/dist/features/shell.d.ts +4 -1
- package/lib/dist/features/shell.js +14 -3
- package/lib/dist/filters/edge/belongs-to-face.d.ts +12 -9
- package/lib/dist/filters/edge/belongs-to-face.js +64 -15
- package/lib/dist/filters/filter-builder-base.d.ts +25 -0
- package/lib/dist/filters/filter-builder-base.js +47 -0
- package/lib/dist/filters/filter.js +39 -14
- package/lib/dist/filters/from-object.d.ts +4 -0
- package/lib/dist/filters/from-object.js +10 -0
- package/lib/dist/helpers/clone-transform.d.ts +2 -1
- package/lib/dist/helpers/scene-helpers.d.ts +1 -1
- package/lib/dist/helpers/scene-helpers.js +146 -12
- package/lib/dist/index.d.ts +7 -1
- package/lib/dist/index.js +3 -3
- package/lib/dist/io/file-import.d.ts +5 -1
- package/lib/dist/io/file-import.js +29 -18
- package/lib/dist/math/lazy-matrix.d.ts +31 -0
- package/lib/dist/math/lazy-matrix.js +66 -0
- package/lib/dist/oc/color-transfer.d.ts +19 -8
- package/lib/dist/oc/color-transfer.js +70 -12
- package/lib/dist/oc/constraints/constraint-solver-adaptor.d.ts +5 -0
- package/lib/dist/oc/constraints/constraint-solver-adaptor.js +16 -0
- package/lib/dist/oc/constraints/constraint-solver.d.ts +4 -0
- package/lib/dist/oc/constraints/curve/curve-constraint-solver.d.ts +4 -0
- package/lib/dist/oc/constraints/curve/curve-constraint-solver.js +3 -0
- package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.d.ts +6 -1
- package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.js +4 -0
- package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.d.ts +8 -0
- package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.js +111 -0
- package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.d.ts +8 -0
- package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.js +161 -0
- package/lib/dist/oc/extrude-ops.d.ts +2 -1
- package/lib/dist/oc/extrude-ops.js +51 -2
- package/lib/dist/oc/mesh.d.ts +9 -4
- package/lib/dist/oc/mesh.js +14 -13
- package/lib/dist/oc/rib-ops.d.ts +35 -0
- package/lib/dist/oc/rib-ops.js +619 -0
- package/lib/dist/oc/shell-ops.d.ts +2 -1
- package/lib/dist/oc/shell-ops.js +5 -2
- package/lib/dist/oc/topology-index.d.ts +6 -0
- package/lib/dist/oc/topology-index.js +36 -0
- package/lib/dist/rendering/mesh-builder.d.ts +3 -0
- package/lib/dist/rendering/mesh-builder.js +8 -4
- package/lib/dist/rendering/render-edge.d.ts +2 -1
- package/lib/dist/rendering/render-edge.js +2 -2
- package/lib/dist/rendering/render-face.d.ts +2 -1
- package/lib/dist/rendering/render-face.js +2 -2
- package/lib/dist/rendering/render-solid.d.ts +2 -1
- package/lib/dist/rendering/render-solid.js +3 -5
- package/lib/dist/rendering/render-wire.d.ts +2 -1
- package/lib/dist/rendering/render-wire.js +2 -2
- package/lib/dist/rendering/render.d.ts +4 -0
- package/lib/dist/rendering/render.js +50 -2
- package/lib/dist/rendering/scene-compare.js +3 -0
- package/lib/dist/rendering/scene.d.ts +1 -0
- package/lib/dist/rendering/scene.js +4 -0
- package/lib/dist/scene-manager.d.ts +4 -2
- package/lib/dist/scene-manager.js +12 -4
- package/lib/dist/tests/features/2d/arc.test.js +64 -0
- package/lib/dist/tests/features/2d/back.test.js +17 -1
- package/lib/dist/tests/features/2d/tarc.test.js +157 -0
- package/lib/dist/tests/features/color-lineage.test.js +18 -0
- package/lib/dist/tests/features/filter-positional.test.d.ts +1 -0
- package/lib/dist/tests/features/filter-positional.test.js +129 -0
- package/lib/dist/tests/features/repeat-user-repro.test.d.ts +1 -0
- package/lib/dist/tests/features/repeat-user-repro.test.js +60 -0
- package/lib/dist/tests/features/rib.test.d.ts +1 -0
- package/lib/dist/tests/features/rib.test.js +598 -0
- package/lib/dist/tests/features/shell.test.js +36 -0
- package/lib/dist/tests/global-setup.js +2 -1
- package/lib/dist/tests/helpers/extract-blocks.d.ts +9 -0
- package/lib/dist/tests/helpers/extract-blocks.js +56 -0
- package/lib/dist/tests/llm-docs-examples.test.d.ts +1 -0
- package/lib/dist/tests/llm-docs-examples.test.js +62 -0
- package/lib/dist/tests/scene-compare.test.d.ts +1 -0
- package/lib/dist/tests/scene-compare.test.js +77 -0
- package/lib/dist/tests/setup.js +2 -1
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/llm-docs/.coverage-allowlist.txt +9 -0
- package/llm-docs/api/arc.md +48 -0
- package/llm-docs/api/axis.md +42 -0
- package/llm-docs/api/bezier.md +42 -0
- package/llm-docs/api/booleans.md +44 -0
- package/llm-docs/api/chamfer.md +40 -0
- package/llm-docs/api/circle.md +36 -0
- package/llm-docs/api/color.md +34 -0
- package/llm-docs/api/connect.md +41 -0
- package/llm-docs/api/constraint-qualifiers.md +48 -0
- package/llm-docs/api/copy.md +63 -0
- package/llm-docs/api/cursor-lines.md +50 -0
- package/llm-docs/api/cursor-move.md +61 -0
- package/llm-docs/api/cut.md +55 -0
- package/llm-docs/api/draft.md +36 -0
- package/llm-docs/api/edge-filter.md +57 -0
- package/llm-docs/api/ellipse.md +34 -0
- package/llm-docs/api/extrude.md +74 -0
- package/llm-docs/api/face-filter.md +61 -0
- package/llm-docs/api/fillet.md +51 -0
- package/llm-docs/api/index.json +139 -0
- package/llm-docs/api/line.md +42 -0
- package/llm-docs/api/load.md +37 -0
- package/llm-docs/api/local.md +38 -0
- package/llm-docs/api/loft.md +37 -0
- package/llm-docs/api/mirror.md +44 -0
- package/llm-docs/api/offset.md +36 -0
- package/llm-docs/api/part.md +40 -0
- package/llm-docs/api/plane.md +44 -0
- package/llm-docs/api/polygon.md +37 -0
- package/llm-docs/api/primitive-solids.md +39 -0
- package/llm-docs/api/project-intersect.md +48 -0
- package/llm-docs/api/rect.md +48 -0
- package/llm-docs/api/remove.md +32 -0
- package/llm-docs/api/repeat.md +79 -0
- package/llm-docs/api/revolve.md +38 -0
- package/llm-docs/api/rib.md +40 -0
- package/llm-docs/api/rotate.md +37 -0
- package/llm-docs/api/select.md +41 -0
- package/llm-docs/api/shell.md +41 -0
- package/llm-docs/api/sketch.md +76 -0
- package/llm-docs/api/slot.md +36 -0
- package/llm-docs/api/split-trim.md +42 -0
- package/llm-docs/api/sweep.md +43 -0
- package/llm-docs/api/tarc.md +45 -0
- package/llm-docs/api/tcircle.md +38 -0
- package/llm-docs/api/tline.md +42 -0
- package/llm-docs/api/translate.md +40 -0
- package/llm-docs/api/types/aline.md +35 -0
- package/llm-docs/api/types/arc-angles.md +29 -0
- package/llm-docs/api/types/arc-points.md +48 -0
- package/llm-docs/api/types/axis-like.md +38 -0
- package/llm-docs/api/types/axis.md +21 -0
- package/llm-docs/api/types/boolean-operation.md +50 -0
- package/llm-docs/api/types/circular-repeat-options.md +31 -0
- package/llm-docs/api/types/common.md +32 -0
- package/llm-docs/api/types/cut.md +125 -0
- package/llm-docs/api/types/draft.md +21 -0
- package/llm-docs/api/types/extrudable-geometry.md +23 -0
- package/llm-docs/api/types/extrude.md +194 -0
- package/llm-docs/api/types/geometry.md +51 -0
- package/llm-docs/api/types/hline.md +35 -0
- package/llm-docs/api/types/linear-repeat-options.md +31 -0
- package/llm-docs/api/types/loft.md +154 -0
- package/llm-docs/api/types/mirror.md +35 -0
- package/llm-docs/api/types/offset.md +31 -0
- package/llm-docs/api/types/plane-like.md +35 -0
- package/llm-docs/api/types/plane-transform-options.md +29 -0
- package/llm-docs/api/types/plane.md +21 -0
- package/llm-docs/api/types/point-like.md +22 -0
- package/llm-docs/api/types/point2dlike.md +26 -0
- package/llm-docs/api/types/polygon.md +46 -0
- package/llm-docs/api/types/rect.md +128 -0
- package/llm-docs/api/types/revolve.md +102 -0
- package/llm-docs/api/types/rib.md +133 -0
- package/llm-docs/api/types/scene-object.md +33 -0
- package/llm-docs/api/types/select.md +21 -0
- package/llm-docs/api/types/shell.md +54 -0
- package/llm-docs/api/types/slot.md +43 -0
- package/llm-docs/api/types/sweep.md +189 -0
- package/llm-docs/api/types/tangent-arc-two-objects.md +46 -0
- package/llm-docs/api/types/transformable.md +93 -0
- package/llm-docs/api/types/trim.md +27 -0
- package/llm-docs/api/types/two-objects-tangent-line.md +46 -0
- package/llm-docs/api/types/vertex.md +17 -0
- package/llm-docs/api/types/vline.md +35 -0
- package/llm-docs/concepts/coordinate-system.md +45 -0
- package/llm-docs/concepts/history-and-rollback.md +40 -0
- package/llm-docs/concepts/last-selection.md +49 -0
- package/llm-docs/concepts/scene-graph.md +37 -0
- package/llm-docs/index.json +1750 -0
- package/mcp/dist/client.d.ts +64 -0
- package/mcp/dist/client.js +248 -0
- package/mcp/dist/discovery.d.ts +11 -0
- package/mcp/dist/discovery.js +78 -0
- package/mcp/dist/docs-index.d.ts +81 -0
- package/mcp/dist/docs-index.js +261 -0
- package/mcp/dist/resources.d.ts +4 -0
- package/mcp/dist/resources.js +115 -0
- package/mcp/dist/server.d.ts +12 -0
- package/mcp/dist/server.js +489 -0
- package/mcp/dist/tools/coordination.d.ts +9 -0
- package/mcp/dist/tools/coordination.js +46 -0
- package/mcp/dist/tools/docs.d.ts +66 -0
- package/mcp/dist/tools/docs.js +122 -0
- package/mcp/dist/tools/engine.d.ts +56 -0
- package/mcp/dist/tools/engine.js +145 -0
- package/mcp/dist/tools/inspection.d.ts +75 -0
- package/mcp/dist/tools/inspection.js +121 -0
- package/mcp/dist/tools/screenshot.d.ts +63 -0
- package/mcp/dist/tools/screenshot.js +263 -0
- package/mcp/dist/tools/source.d.ts +84 -0
- package/mcp/dist/tools/source.js +434 -0
- package/mcp/dist/tools/workspaces.d.ts +13 -0
- package/mcp/dist/tools/workspaces.js +33 -0
- package/mcp/dist/types.d.ts +18 -0
- package/mcp/dist/types.js +11 -0
- package/package.json +19 -5
- package/server/dist/code-editor.d.ts +36 -0
- package/server/dist/code-editor.js +8 -0
- package/server/dist/fluidcad-server.d.ts +50 -0
- package/server/dist/fluidcad-server.js +153 -1
- package/server/dist/global-registry.d.ts +30 -0
- package/server/dist/global-registry.js +126 -0
- package/server/dist/index.js +171 -26
- package/server/dist/instance-file.d.ts +31 -0
- package/server/dist/instance-file.js +73 -0
- package/server/dist/lint-fluid-js.d.ts +15 -0
- package/server/dist/lint-fluid-js.js +271 -0
- package/server/dist/routes/editor.d.ts +24 -0
- package/server/dist/routes/editor.js +44 -0
- package/server/dist/routes/export.d.ts +1 -1
- package/server/dist/routes/export.js +45 -8
- package/server/dist/routes/health.d.ts +7 -0
- package/server/dist/routes/health.js +14 -0
- package/server/dist/routes/lint.d.ts +10 -0
- package/server/dist/routes/lint.js +28 -0
- package/server/dist/routes/render.d.ts +33 -0
- package/server/dist/routes/render.js +34 -0
- package/server/dist/routes/scene.d.ts +5 -0
- package/server/dist/routes/scene.js +48 -0
- package/server/dist/routes/screenshot.js +68 -1
- package/server/dist/ws-protocol.d.ts +56 -2
- package/ui/dist/assets/{index-DMw0OYCF.js → index-BdqrMDRu.js} +30 -30
- package/ui/dist/index.html +1 -1
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
// Builds the MCP server and exposes transport-agnostic entry points.
|
|
2
|
+
//
|
|
3
|
+
// The server itself is transport-agnostic: `buildServer()` constructs an
|
|
4
|
+
// `McpServer` with every tool registered, and `runStdio()` is the only piece
|
|
5
|
+
// that knows about stdio. Phase 12 will add a parallel `runHttp(app)` that
|
|
6
|
+
// binds the same `McpServer` to a streamable HTTP transport.
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { listWorkspaces } from "./tools/workspaces.js";
|
|
13
|
+
import { getApiSignature, getTypeDefinition, listDocs, readDoc, searchDocs, } from "./tools/docs.js";
|
|
14
|
+
import { getCompileError, getEdgeProperties, getFaceProperties, getSceneSummary, getShapeProperties, hitTest, listShapes, } from "./tools/inspection.js";
|
|
15
|
+
import { getCameraState, screenshot, screenshotMulti, screenshotShape, } from "./tools/screenshot.js";
|
|
16
|
+
import { waitForIdle } from "./tools/coordination.js";
|
|
17
|
+
import { editRange, listFluidFiles, readFile, writeFile, } from "./tools/source.js";
|
|
18
|
+
import { addBreakpoint, clearBreakpoints, exportShapes, importStep, recompute, rollbackTo, } from "./tools/engine.js";
|
|
19
|
+
import { loadDocsIndex } from "./docs-index.js";
|
|
20
|
+
import { registerDocResources } from "./resources.js";
|
|
21
|
+
export const SERVER_NAME = 'FluidCAD';
|
|
22
|
+
export const SERVER_VERSION = readPackageVersion();
|
|
23
|
+
function readPackageVersion() {
|
|
24
|
+
// Read the root `fluidcad` package.json — npm always ships it with the
|
|
25
|
+
// published package, and it's the version bumped by `npm run release`.
|
|
26
|
+
// From `mcp/dist/server.js`, this resolves to `<pkg-root>/package.json`
|
|
27
|
+
// both in the source tree and when installed under `node_modules/fluidcad/`.
|
|
28
|
+
try {
|
|
29
|
+
const pkgPath = path.resolve(import.meta.dirname, '../../package.json');
|
|
30
|
+
const parsed = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
31
|
+
if (typeof parsed.version === 'string') {
|
|
32
|
+
return parsed.version;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Fall through to unknown.
|
|
37
|
+
}
|
|
38
|
+
return '0.0.0';
|
|
39
|
+
}
|
|
40
|
+
export function buildServer(options = {}) {
|
|
41
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, {
|
|
42
|
+
capabilities: {
|
|
43
|
+
tools: {},
|
|
44
|
+
resources: {},
|
|
45
|
+
},
|
|
46
|
+
instructions: [
|
|
47
|
+
'Drives a running FluidCAD workspace.',
|
|
48
|
+
'Call list_workspaces first to find available workspaces.',
|
|
49
|
+
'Use list_docs/search_docs/read_doc/get_api_signature to learn the API.',
|
|
50
|
+
'When a signature mentions an unfamiliar type (e.g. PlaneLike, AxisLike,',
|
|
51
|
+
'SceneObject, LinearRepeatOptions), call get_type_definition with the',
|
|
52
|
+
'type name to resolve its accepted forms / methods / properties.',
|
|
53
|
+
'All paths are workspace-absolute.',
|
|
54
|
+
'',
|
|
55
|
+
'`.fluid.js` files MUST import every FluidCAD symbol they use:',
|
|
56
|
+
' import { sketch, rect, extrude } from "fluidcad/core";',
|
|
57
|
+
' import { face, edge } from "fluidcad/filters";',
|
|
58
|
+
' import { outside, enclosed } from "fluidcad/constraints";',
|
|
59
|
+
'write_file and edit_range refuse `.fluid.js` writes that use a known',
|
|
60
|
+
'FluidCAD symbol without an import (code: "missing-imports"). The error',
|
|
61
|
+
'`details.suggestion` is a copy-pasteable block of the imports to add.',
|
|
62
|
+
'',
|
|
63
|
+
'write_file, edit_range, recompute, rollback_to, and import_step are',
|
|
64
|
+
'synchronous: they return once the render settles. write_file and',
|
|
65
|
+
'edit_range additionally carry the outcome under `render` — check',
|
|
66
|
+
'`render.state === "rendered"` before calling screenshot or',
|
|
67
|
+
'inspection. On `compile-error`, the previous scene is still being',
|
|
68
|
+
'served; fix the source and retry.',
|
|
69
|
+
'write_file and edit_range refuse to clobber a buffer the editor has',
|
|
70
|
+
'unsaved changes for (code: "dirty-buffer"). Surface the conflicting',
|
|
71
|
+
'paths to the user before retrying with `force: true`.',
|
|
72
|
+
].join('\n'),
|
|
73
|
+
});
|
|
74
|
+
server.registerTool('list_workspaces', {
|
|
75
|
+
title: 'List running FluidCAD workspaces',
|
|
76
|
+
description: 'Returns every running FluidCAD workspace on this machine (from ~/.fluidcad/instances.json), with a quick liveness probe per entry.',
|
|
77
|
+
inputSchema: {},
|
|
78
|
+
}, async () => {
|
|
79
|
+
const result = await listWorkspaces();
|
|
80
|
+
return toMcp(result);
|
|
81
|
+
});
|
|
82
|
+
const docsIndex = options.docsIndex ?? loadDocsIndex();
|
|
83
|
+
server.registerTool('list_docs', {
|
|
84
|
+
title: 'List FluidCAD docs',
|
|
85
|
+
description: 'Returns every entry in the LLM doc set with id, title, summary, and tags. Optionally filter to a single tag (e.g. "solid", "concept").',
|
|
86
|
+
inputSchema: {
|
|
87
|
+
tag: z
|
|
88
|
+
.string()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe('Restrict the result to docs that carry this tag.'),
|
|
91
|
+
},
|
|
92
|
+
}, async ({ tag }) => toMcp(listDocs(docsIndex, { tag })));
|
|
93
|
+
server.registerTool('read_doc', {
|
|
94
|
+
title: 'Read a FluidCAD doc by id',
|
|
95
|
+
description: 'Returns the full markdown body of a doc identified by id (e.g. "api/extrude", "concepts/scene-graph"). Use list_docs or search_docs to find ids.',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
id: z.string().min(1).describe('Doc id from the manifest (e.g. "api/extrude").'),
|
|
98
|
+
},
|
|
99
|
+
}, async ({ id }) => toMcp(readDoc(docsIndex, { id })));
|
|
100
|
+
server.registerTool('search_docs', {
|
|
101
|
+
title: 'Keyword search across FluidCAD docs',
|
|
102
|
+
description: 'Ranked keyword search over titles, summaries, tags, and bodies. Returns id/title/snippet/score for each hit.',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
query: z.string().min(1).describe('Free-text query — keyword AND/OR is implicit.'),
|
|
105
|
+
limit: z
|
|
106
|
+
.number()
|
|
107
|
+
.int()
|
|
108
|
+
.positive()
|
|
109
|
+
.optional()
|
|
110
|
+
.describe('Maximum number of results to return (default 10).'),
|
|
111
|
+
},
|
|
112
|
+
}, async ({ query, limit }) => toMcp(searchDocs(docsIndex, { query, limit })));
|
|
113
|
+
server.registerTool('get_api_signature', {
|
|
114
|
+
title: 'Get the signature block for an API symbol',
|
|
115
|
+
description: 'Looks up a single API symbol by name (e.g. "extrude") and returns its first code-block signature, the owning doc id, the doc title, and the one-line summary.',
|
|
116
|
+
inputSchema: {
|
|
117
|
+
name: z.string().min(1).describe('API symbol name, e.g. "extrude" or "fillet".'),
|
|
118
|
+
},
|
|
119
|
+
}, async ({ name }) => toMcp(getApiSignature(docsIndex, { name })));
|
|
120
|
+
server.registerTool('get_type_definition', {
|
|
121
|
+
title: 'Get the definition of a documented type',
|
|
122
|
+
description: 'Resolves a type name (e.g. "PlaneLike", "SceneObject", "LinearRepeatOptions") to its TypeScript definition, accepted forms / methods / properties, and the owning doc id. Accepts both display names and internal aliases (e.g. "ISceneObject" → "SceneObject").',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
name: z
|
|
125
|
+
.string()
|
|
126
|
+
.min(1)
|
|
127
|
+
.describe('Type name, e.g. "PlaneLike", "AxisLike", "SceneObject", "LinearRepeatOptions".'),
|
|
128
|
+
},
|
|
129
|
+
}, async ({ name }) => toMcp(getTypeDefinition(docsIndex, { name })));
|
|
130
|
+
registerDocResources(server, docsIndex);
|
|
131
|
+
const workspaceArg = {
|
|
132
|
+
workspace: z
|
|
133
|
+
.string()
|
|
134
|
+
.optional()
|
|
135
|
+
.describe('Absolute workspace path of the target FluidCAD instance. Optional when only one workspace is running.'),
|
|
136
|
+
};
|
|
137
|
+
const shapeIdArg = z.string().min(1).describe('Shape id from list_shapes or get_scene_summary.');
|
|
138
|
+
const faceIndexArg = z
|
|
139
|
+
.number()
|
|
140
|
+
.int()
|
|
141
|
+
.nonnegative()
|
|
142
|
+
.describe('Zero-based face index inside the shape.');
|
|
143
|
+
const edgeIndexArg = z
|
|
144
|
+
.number()
|
|
145
|
+
.int()
|
|
146
|
+
.nonnegative()
|
|
147
|
+
.describe('Zero-based edge index inside the shape.');
|
|
148
|
+
const vec3 = z
|
|
149
|
+
.tuple([z.number(), z.number(), z.number()])
|
|
150
|
+
.describe('World-space [x, y, z] vector.');
|
|
151
|
+
server.registerTool('get_scene_summary', {
|
|
152
|
+
title: 'Get the feature tree for a workspace',
|
|
153
|
+
description: 'Returns a JSON projection of the current scene: every scene object with its index, id, kind, parameters, source location, and the shape ids it produced. Use this before list_shapes when you need feature-tree context.',
|
|
154
|
+
inputSchema: workspaceArg,
|
|
155
|
+
}, async ({ workspace }) => toMcp(await getSceneSummary({ workspace })));
|
|
156
|
+
server.registerTool('list_shapes', {
|
|
157
|
+
title: 'List all shapes in the current scene',
|
|
158
|
+
description: 'Returns a flat list of `{ shapeId, type, sceneObjectId }`. Cheaper than get_scene_summary when you only need ids — use this before calling shape/face/edge property tools.',
|
|
159
|
+
inputSchema: workspaceArg,
|
|
160
|
+
}, async ({ workspace }) => toMcp(await listShapes({ workspace })));
|
|
161
|
+
server.registerTool('get_compile_error', {
|
|
162
|
+
title: 'Get the last cached compile error',
|
|
163
|
+
description: 'Returns `{ compileError: { message, filePath?, sourceLocation? } | null }`. Useful when the scene looks stale — a non-null value means the most recent render failed and the previous scene is still being served.',
|
|
164
|
+
inputSchema: workspaceArg,
|
|
165
|
+
}, async ({ workspace }) => toMcp(await getCompileError({ workspace })));
|
|
166
|
+
server.registerTool('get_shape_properties', {
|
|
167
|
+
title: 'Get geometric properties of a shape',
|
|
168
|
+
description: 'Returns volume, surface area, bounding box, center of mass, and similar measurements for a single shape.',
|
|
169
|
+
inputSchema: { ...workspaceArg, shapeId: shapeIdArg },
|
|
170
|
+
}, async ({ workspace, shapeId }) => toMcp(await getShapeProperties({ workspace, shapeId })));
|
|
171
|
+
server.registerTool('get_face_properties', {
|
|
172
|
+
title: 'Get geometric properties of a face',
|
|
173
|
+
description: 'Returns area, normal, surface kind (plane/cylinder/...), and related measurements for a single face on a shape.',
|
|
174
|
+
inputSchema: {
|
|
175
|
+
...workspaceArg,
|
|
176
|
+
shapeId: shapeIdArg,
|
|
177
|
+
faceIndex: faceIndexArg,
|
|
178
|
+
},
|
|
179
|
+
}, async ({ workspace, shapeId, faceIndex }) => toMcp(await getFaceProperties({ workspace, shapeId, faceIndex })));
|
|
180
|
+
server.registerTool('get_edge_properties', {
|
|
181
|
+
title: 'Get geometric properties of an edge',
|
|
182
|
+
description: 'Returns length, curve kind, endpoints, and related measurements for a single edge on a shape.',
|
|
183
|
+
inputSchema: {
|
|
184
|
+
...workspaceArg,
|
|
185
|
+
shapeId: shapeIdArg,
|
|
186
|
+
edgeIndex: edgeIndexArg,
|
|
187
|
+
},
|
|
188
|
+
}, async ({ workspace, shapeId, edgeIndex }) => toMcp(await getEdgeProperties({ workspace, shapeId, edgeIndex })));
|
|
189
|
+
const namedViewArg = z.enum([
|
|
190
|
+
'front', 'back', 'left', 'right', 'top', 'bottom',
|
|
191
|
+
'iso-ftr', 'iso-fbr', 'iso-ftl', 'iso-fbl',
|
|
192
|
+
'iso-btr', 'iso-bbr', 'iso-btl', 'iso-bbl',
|
|
193
|
+
]).describe('Named view direction. Cardinal axes (front, top, …) or one of 8 iso octants (iso-ftr = front-top-right, etc.).');
|
|
194
|
+
const screenshotViewArg = z
|
|
195
|
+
.discriminatedUnion('kind', [
|
|
196
|
+
z.object({ kind: z.literal('current') }),
|
|
197
|
+
z.object({ kind: z.literal('named'), name: namedViewArg }),
|
|
198
|
+
z.object({
|
|
199
|
+
kind: z.literal('orbit-from-current'),
|
|
200
|
+
azimuthDeg: z.number().describe('Spin around the up axis, in degrees.'),
|
|
201
|
+
elevationDeg: z.number().describe('Tilt up/down relative to the current elevation, in degrees.'),
|
|
202
|
+
}),
|
|
203
|
+
z.object({
|
|
204
|
+
kind: z.literal('look-from'),
|
|
205
|
+
eye: vec3,
|
|
206
|
+
target: vec3.optional(),
|
|
207
|
+
}),
|
|
208
|
+
])
|
|
209
|
+
.describe('Stateless camera view for this screenshot. Does not move the user\'s interactive camera.');
|
|
210
|
+
const widthArg = z
|
|
211
|
+
.number()
|
|
212
|
+
.int()
|
|
213
|
+
.min(1)
|
|
214
|
+
.max(8192)
|
|
215
|
+
.optional()
|
|
216
|
+
.describe('Output width in pixels (default 800).');
|
|
217
|
+
const heightArg = z
|
|
218
|
+
.number()
|
|
219
|
+
.int()
|
|
220
|
+
.min(1)
|
|
221
|
+
.max(8192)
|
|
222
|
+
.optional()
|
|
223
|
+
.describe('Output height in pixels (default 800).');
|
|
224
|
+
const marginArg = z.number().nonnegative().optional();
|
|
225
|
+
server.registerTool('screenshot', {
|
|
226
|
+
title: 'Capture a PNG of the current scene from a stateless view',
|
|
227
|
+
description: 'Renders the current FluidCAD scene to a PNG using a stateless camera view. The user\'s interactive camera is never moved. `view` defaults to the agent\'s last seen camera state — pass a `named` view (e.g. {kind:"named", name:"iso-ftr"}) for "show me from the front-top-right" or `look-from` for a precise vantage. Returns an MCP image content block.',
|
|
228
|
+
inputSchema: {
|
|
229
|
+
...workspaceArg,
|
|
230
|
+
view: screenshotViewArg.optional(),
|
|
231
|
+
width: widthArg,
|
|
232
|
+
height: heightArg,
|
|
233
|
+
showGrid: z.boolean().optional(),
|
|
234
|
+
showAxes: z.boolean().optional(),
|
|
235
|
+
transparent: z.boolean().optional(),
|
|
236
|
+
autoCrop: z.boolean().optional(),
|
|
237
|
+
fitToModel: z.boolean().optional(),
|
|
238
|
+
margin: marginArg,
|
|
239
|
+
},
|
|
240
|
+
}, async (args) => toMcp(await screenshot(args)));
|
|
241
|
+
server.registerTool('screenshot_multi', {
|
|
242
|
+
title: 'Capture a 2×2 composite of front/top/right/iso views',
|
|
243
|
+
description: 'Renders a single PNG showing four canonical views (front, top, right, iso-ftr) as a 2×2 grid. Use this when the agent needs to "see all sides at once" without four separate tool calls. The user\'s interactive camera is never moved.',
|
|
244
|
+
inputSchema: {
|
|
245
|
+
...workspaceArg,
|
|
246
|
+
width: widthArg,
|
|
247
|
+
height: heightArg,
|
|
248
|
+
showGrid: z.boolean().optional(),
|
|
249
|
+
showAxes: z.boolean().optional(),
|
|
250
|
+
transparent: z.boolean().optional(),
|
|
251
|
+
margin: marginArg,
|
|
252
|
+
},
|
|
253
|
+
}, async (args) => toMcp(await screenshotMulti(args)));
|
|
254
|
+
server.registerTool('screenshot_shape', {
|
|
255
|
+
title: 'Capture a framed iso view of a single shape',
|
|
256
|
+
description: 'Fetches the shape\'s bounding box and renders a PNG from an iso vantage point that frames it with a small margin. Useful for "show me this specific feature" requests.',
|
|
257
|
+
inputSchema: {
|
|
258
|
+
...workspaceArg,
|
|
259
|
+
shapeId: shapeIdArg,
|
|
260
|
+
margin: z
|
|
261
|
+
.number()
|
|
262
|
+
.positive()
|
|
263
|
+
.optional()
|
|
264
|
+
.describe('Distance multiplier on the bounding sphere (default 1.2). Larger values pull the camera farther back.'),
|
|
265
|
+
width: widthArg,
|
|
266
|
+
height: heightArg,
|
|
267
|
+
showGrid: z.boolean().optional(),
|
|
268
|
+
showAxes: z.boolean().optional(),
|
|
269
|
+
transparent: z.boolean().optional(),
|
|
270
|
+
},
|
|
271
|
+
}, async (args) => toMcp(await screenshotShape(args)));
|
|
272
|
+
server.registerTool('get_camera_state', {
|
|
273
|
+
title: 'Get the user\'s current camera position and target',
|
|
274
|
+
description: 'Returns `{ position, target, up, projection }` for the user\'s interactive camera, as last broadcast by the UI. Useful before computing an orbit-from-current view.',
|
|
275
|
+
inputSchema: workspaceArg,
|
|
276
|
+
}, async ({ workspace }) => toMcp(await getCameraState({ workspace })));
|
|
277
|
+
const timeoutMsArg = z
|
|
278
|
+
.number()
|
|
279
|
+
.positive()
|
|
280
|
+
.optional()
|
|
281
|
+
.describe('Hard upper bound in milliseconds (default 10000).');
|
|
282
|
+
server.registerTool('wait_for_idle', {
|
|
283
|
+
title: 'Wait until renders have settled',
|
|
284
|
+
description: 'Blocks until no `render-version: start` has been observed for `stableMs` (default 200ms), or `timeoutMs` elapses (default 10000ms). Useful when the user might be live-editing in the editor and the agent wants to capture a stable scene. Returns `{ idleMs, lastVersion }`.',
|
|
285
|
+
inputSchema: {
|
|
286
|
+
...workspaceArg,
|
|
287
|
+
timeoutMs: timeoutMsArg,
|
|
288
|
+
stableMs: z
|
|
289
|
+
.number()
|
|
290
|
+
.nonnegative()
|
|
291
|
+
.optional()
|
|
292
|
+
.describe('Quiet window in milliseconds (default 200). Must be strictly less than `timeoutMs`.'),
|
|
293
|
+
},
|
|
294
|
+
}, async ({ workspace, timeoutMs, stableMs }) => toMcp(await waitForIdle({ workspace, timeoutMs, stableMs })));
|
|
295
|
+
server.registerTool('hit_test', {
|
|
296
|
+
title: 'Ray-test a shape',
|
|
297
|
+
description: 'Cast a ray against a shape and return the face/edge it hits (if any). `rayOrigin` and `rayDir` are world-space [x, y, z]. `edgeThreshold` is a screen-space tolerance for edge hits.',
|
|
298
|
+
inputSchema: {
|
|
299
|
+
...workspaceArg,
|
|
300
|
+
shapeId: shapeIdArg,
|
|
301
|
+
rayOrigin: vec3,
|
|
302
|
+
rayDir: vec3,
|
|
303
|
+
edgeThreshold: z
|
|
304
|
+
.number()
|
|
305
|
+
.nonnegative()
|
|
306
|
+
.optional()
|
|
307
|
+
.describe('Optional edge-hit tolerance (default 0 — face-only hit test).'),
|
|
308
|
+
},
|
|
309
|
+
}, async ({ workspace, shapeId, rayOrigin, rayDir, edgeThreshold }) => toMcp(await hitTest({
|
|
310
|
+
workspace,
|
|
311
|
+
shapeId,
|
|
312
|
+
rayOrigin: rayOrigin,
|
|
313
|
+
rayDir: rayDir,
|
|
314
|
+
edgeThreshold,
|
|
315
|
+
})));
|
|
316
|
+
// -------------------------------------------------------------------------
|
|
317
|
+
// Source editing — read/write `.fluid.js` files inside the workspace.
|
|
318
|
+
// -------------------------------------------------------------------------
|
|
319
|
+
const pathArg = z
|
|
320
|
+
.string()
|
|
321
|
+
.min(1)
|
|
322
|
+
.describe('Path relative to the workspace root (or absolute, as long as it resolves inside the workspace).');
|
|
323
|
+
const forceArg = z
|
|
324
|
+
.boolean()
|
|
325
|
+
.optional()
|
|
326
|
+
.describe('Destructive override — write even if the editor has unsaved changes for this file. Surface the dirty-files list to the user before passing true.');
|
|
327
|
+
const positionArg = z
|
|
328
|
+
.object({
|
|
329
|
+
line: z.number().int().nonnegative().describe('Zero-based line number.'),
|
|
330
|
+
column: z.number().int().nonnegative().describe('Zero-based UTF-16 column.'),
|
|
331
|
+
})
|
|
332
|
+
.describe('Source position (LSP-style, 0-based line and 0-based UTF-16 column).');
|
|
333
|
+
server.registerTool('list_fluid_files', {
|
|
334
|
+
title: 'List every .fluid.js file in the workspace',
|
|
335
|
+
description: 'Walks the workspace recursively and returns workspace-relative paths for every `.fluid.js` file. Skips `node_modules`, `.git`, `.fluidcad`, `dist`, `build`.',
|
|
336
|
+
inputSchema: workspaceArg,
|
|
337
|
+
}, async ({ workspace }) => toMcp(await listFluidFiles({ workspace })));
|
|
338
|
+
server.registerTool('read_file', {
|
|
339
|
+
title: 'Read a UTF-8 file from the workspace',
|
|
340
|
+
description: 'Returns the full contents of a file under the workspace root. Paths that escape the workspace (via `..` or symlinks) are rejected.',
|
|
341
|
+
inputSchema: { ...workspaceArg, path: pathArg },
|
|
342
|
+
}, async ({ workspace, path }) => toMcp(await readFile({ workspace, path })));
|
|
343
|
+
server.registerTool('write_file', {
|
|
344
|
+
title: 'Replace a file inside the workspace (atomic)',
|
|
345
|
+
description: 'Writes `content` to `path` (UTF-8, tmp+rename atomic), then synchronously triggers a render and returns the outcome under `render` (`state`: rendered | compile-error | superseded | no-scene-manager | render-failed, plus `version`, `durationMs`, optional `compileError`). For `.fluid.js` files, refuses writes that use a known FluidCAD symbol without an `import { … } from "fluidcad/…"` line — fails with code `missing-imports` and `details.suggestion` shows the imports to add. Also refuses to clobber a file the editor extension reports as dirty — fails with code `dirty-buffer` whose `details.dirtyFiles` lists every dirty path. Pass `force: true` to override either guard.',
|
|
346
|
+
inputSchema: {
|
|
347
|
+
...workspaceArg,
|
|
348
|
+
path: pathArg,
|
|
349
|
+
content: z.string().describe('Full UTF-8 file contents to write.'),
|
|
350
|
+
force: forceArg,
|
|
351
|
+
},
|
|
352
|
+
}, async ({ workspace, path, content, force }) => toMcp(await writeFile({ workspace, path, content, force })));
|
|
353
|
+
server.registerTool('edit_range', {
|
|
354
|
+
title: 'Replace a [start, end) range inside a workspace file (atomic)',
|
|
355
|
+
description: 'Replaces the half-open range `[start, end)` in `path` with `newText`. Positions are 0-based `{ line, column }` (UTF-16 columns). End-of-line and end-of-file overrun clamp gracefully. Same dirty-buffer guard, missing-imports guard (for `.fluid.js` files), `force` semantics, and synchronous `render` outcome as `write_file`.',
|
|
356
|
+
inputSchema: {
|
|
357
|
+
...workspaceArg,
|
|
358
|
+
path: pathArg,
|
|
359
|
+
start: positionArg,
|
|
360
|
+
end: positionArg,
|
|
361
|
+
newText: z.string().describe('Replacement text (may be empty to delete the range).'),
|
|
362
|
+
force: forceArg,
|
|
363
|
+
},
|
|
364
|
+
}, async ({ workspace, path, start, end, newText, force }) => toMcp(await editRange({ workspace, path, start, end, newText, force })));
|
|
365
|
+
// -------------------------------------------------------------------------
|
|
366
|
+
// Engine control — recompute, rollback, breakpoints, import/export.
|
|
367
|
+
// -------------------------------------------------------------------------
|
|
368
|
+
server.registerTool('recompute', {
|
|
369
|
+
title: 'Force a full recompute of the current file',
|
|
370
|
+
description: 'Discards the cached scene and re-runs the current `.fluid.js` file. Synchronous — returns once the render settles.',
|
|
371
|
+
inputSchema: workspaceArg,
|
|
372
|
+
}, async ({ workspace }) => toMcp(await recompute({ workspace })));
|
|
373
|
+
server.registerTool('rollback_to', {
|
|
374
|
+
title: 'Temporarily roll back the rendered scene to a feature index',
|
|
375
|
+
description: 'Stops rendering at scene-object `index` so the UI shows the model up to that step. Mutates only UI/render state — the source file is unchanged, and the next `recompute` or live-update resets to the full scene. Synchronous — returns once the rollback render settles.',
|
|
376
|
+
inputSchema: {
|
|
377
|
+
...workspaceArg,
|
|
378
|
+
index: z
|
|
379
|
+
.number()
|
|
380
|
+
.int()
|
|
381
|
+
.nonnegative()
|
|
382
|
+
.describe('Zero-based scene-object index to stop rendering at. Use 0 to show only the first feature.'),
|
|
383
|
+
},
|
|
384
|
+
}, async ({ workspace, index }) => toMcp(await rollbackTo({ workspace, index })));
|
|
385
|
+
server.registerTool('add_breakpoint', {
|
|
386
|
+
title: 'Set a breakpoint on a source line',
|
|
387
|
+
description: 'Halts rendering at the given line on the next recompute. Subsequent `recompute` produces a partial scene up to (but not including) the line. Use `clear_breakpoints` to remove all breakpoints.',
|
|
388
|
+
inputSchema: {
|
|
389
|
+
...workspaceArg,
|
|
390
|
+
file: z
|
|
391
|
+
.string()
|
|
392
|
+
.min(1)
|
|
393
|
+
.describe('Absolute path to the .fluid.js file to break in (e.g. from `get_scene_summary().file`).'),
|
|
394
|
+
line: z.number().int().nonnegative().describe('Zero-based line number to break on.'),
|
|
395
|
+
},
|
|
396
|
+
}, async ({ workspace, file, line }) => toMcp(await addBreakpoint({ workspace, file, line })));
|
|
397
|
+
server.registerTool('clear_breakpoints', {
|
|
398
|
+
title: 'Remove every breakpoint in the workspace',
|
|
399
|
+
description: 'Clears all source-line breakpoints; subsequent renders run end-to-end.',
|
|
400
|
+
inputSchema: workspaceArg,
|
|
401
|
+
}, async ({ workspace }) => toMcp(await clearBreakpoints({ workspace })));
|
|
402
|
+
server.registerTool('import_step', {
|
|
403
|
+
title: 'Import a STEP file into the workspace',
|
|
404
|
+
description: 'Reads `path` from disk, base64-encodes the bytes, and posts to the server\'s import pipeline. The imported geometry shows up as a new shape in the current scene.',
|
|
405
|
+
inputSchema: {
|
|
406
|
+
...workspaceArg,
|
|
407
|
+
path: z
|
|
408
|
+
.string()
|
|
409
|
+
.min(1)
|
|
410
|
+
.describe('Absolute path to a STEP file (.step or .stp) on the local filesystem.'),
|
|
411
|
+
},
|
|
412
|
+
}, async ({ workspace, path }) => toMcp(await importStep({ workspace, path })));
|
|
413
|
+
server.registerTool('export', {
|
|
414
|
+
title: 'Export shapes to STEP or STL',
|
|
415
|
+
description: 'Exports the listed shapes to a STEP or STL file. Prefer `saveAsPath` (must resolve inside the workspace root) — the encoded bytes can be multi-MB and shouldn\'t round-trip through the agent\'s context. Returns `{ savedTo, bytesWritten }` when saved, or `{ format, mimeType, base64, bytes }` otherwise. For STL, `resolution: "fine"` produces the cleanest mesh but is slow; default to `"medium"` unless the user asks for higher fidelity.',
|
|
416
|
+
inputSchema: {
|
|
417
|
+
...workspaceArg,
|
|
418
|
+
format: z.enum(['step', 'stl']).describe('Output format.'),
|
|
419
|
+
shapeIds: z
|
|
420
|
+
.array(z.string().min(1))
|
|
421
|
+
.min(1)
|
|
422
|
+
.describe('Shape ids to export (from `list_shapes` or `get_scene_summary`).'),
|
|
423
|
+
saveAsPath: z
|
|
424
|
+
.string()
|
|
425
|
+
.optional()
|
|
426
|
+
.describe('Workspace-relative or absolute path to write the export to. Must resolve inside the workspace root.'),
|
|
427
|
+
resolution: z
|
|
428
|
+
.enum(['coarse', 'medium', 'fine'])
|
|
429
|
+
.optional()
|
|
430
|
+
.describe('STL mesh resolution. Ignored for STEP. Defaults to "medium".'),
|
|
431
|
+
includeColors: z
|
|
432
|
+
.boolean()
|
|
433
|
+
.optional()
|
|
434
|
+
.describe('Include per-face color metadata (STEP/STL with color extension).'),
|
|
435
|
+
},
|
|
436
|
+
}, async ({ workspace, format, shapeIds, saveAsPath, resolution, includeColors }) => toMcp(await exportShapes({
|
|
437
|
+
workspace,
|
|
438
|
+
format,
|
|
439
|
+
shapeIds,
|
|
440
|
+
saveAsPath,
|
|
441
|
+
resolution,
|
|
442
|
+
includeColors,
|
|
443
|
+
})));
|
|
444
|
+
return server;
|
|
445
|
+
}
|
|
446
|
+
export async function runStdio() {
|
|
447
|
+
const server = buildServer();
|
|
448
|
+
const transport = new StdioServerTransport();
|
|
449
|
+
await server.connect(transport);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Render a tool result into the MCP `CallToolResult` shape. Success: a JSON
|
|
453
|
+
* text block. Failure: also a text block, but with `isError: true` so MCP
|
|
454
|
+
* clients render it as a tool-error rather than a normal response.
|
|
455
|
+
*/
|
|
456
|
+
function toMcp(result) {
|
|
457
|
+
if (result.ok === true) {
|
|
458
|
+
const data = result.data;
|
|
459
|
+
// Image results are rendered as MCP `image` blocks so multimodal clients
|
|
460
|
+
// can display the PNG inline without burning the agent's text budget.
|
|
461
|
+
if (data && typeof data === 'object' && data.image && typeof data.image.base64 === 'string') {
|
|
462
|
+
return {
|
|
463
|
+
content: [
|
|
464
|
+
{
|
|
465
|
+
type: 'image',
|
|
466
|
+
data: data.image.base64,
|
|
467
|
+
mimeType: data.image.mimeType ?? 'image/png',
|
|
468
|
+
},
|
|
469
|
+
],
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
content: [
|
|
474
|
+
{ type: 'text', text: JSON.stringify(result.data, null, 2) },
|
|
475
|
+
],
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const failure = result;
|
|
479
|
+
return {
|
|
480
|
+
isError: true,
|
|
481
|
+
content: [
|
|
482
|
+
{
|
|
483
|
+
type: 'text',
|
|
484
|
+
text: JSON.stringify({ code: failure.code, message: failure.message, details: failure.details }, null, 2),
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
export { z };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type WorkspaceArg } from './inspection.ts';
|
|
2
|
+
import { type IdleResult } from '../client.ts';
|
|
3
|
+
import { type ToolResult } from '../types.ts';
|
|
4
|
+
export type WaitForIdleInput = WorkspaceArg & {
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
stableMs?: number;
|
|
7
|
+
};
|
|
8
|
+
export type WaitForIdleResult = IdleResult;
|
|
9
|
+
export declare function waitForIdle(input: WaitForIdleInput): Promise<ToolResult<WaitForIdleResult>>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Coordination tools — let the agent settle into a stable scene before
|
|
2
|
+
// capturing a screenshot. Currently just `wait_for_idle` for observing
|
|
3
|
+
// user-driven live edits; the agent's own writes resolve synchronously
|
|
4
|
+
// via write_file / edit_range, so no wait-for-render is needed.
|
|
5
|
+
import { resolveClient } from "./inspection.js";
|
|
6
|
+
import { TimeoutError, WsError } from "../client.js";
|
|
7
|
+
import { err, ok } from "../types.js";
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
9
|
+
const DEFAULT_STABLE_MS = 200;
|
|
10
|
+
export async function waitForIdle(input) {
|
|
11
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
12
|
+
const stableMs = input.stableMs ?? DEFAULT_STABLE_MS;
|
|
13
|
+
if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
14
|
+
return err('invalid-input', '`timeoutMs` must be a positive finite number when provided.');
|
|
15
|
+
}
|
|
16
|
+
if (typeof stableMs !== 'number' || !Number.isFinite(stableMs) || stableMs < 0) {
|
|
17
|
+
return err('invalid-input', '`stableMs` must be a non-negative finite number when provided.');
|
|
18
|
+
}
|
|
19
|
+
if (stableMs >= timeoutMs) {
|
|
20
|
+
return err('invalid-input', '`stableMs` must be less than `timeoutMs`.');
|
|
21
|
+
}
|
|
22
|
+
const resolved = resolveClient(input);
|
|
23
|
+
if (resolved.ok === false) {
|
|
24
|
+
return resolved;
|
|
25
|
+
}
|
|
26
|
+
const { client } = resolved.data;
|
|
27
|
+
try {
|
|
28
|
+
const result = await client.nextIdle(stableMs, timeoutMs);
|
|
29
|
+
return ok(result);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
return wrapWaitError(e);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await client.close().catch(() => { });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function wrapWaitError(e) {
|
|
39
|
+
if (e instanceof TimeoutError) {
|
|
40
|
+
return err('timeout', e.message);
|
|
41
|
+
}
|
|
42
|
+
if (e instanceof WsError) {
|
|
43
|
+
return err('ws-error', e.message);
|
|
44
|
+
}
|
|
45
|
+
return err('internal', e?.message ?? String(e));
|
|
46
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { DocsIndex } from '../docs-index.ts';
|
|
2
|
+
import { type ToolResult } from '../types.ts';
|
|
3
|
+
export type DocListEntry = {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
summary: string;
|
|
7
|
+
tags?: string[];
|
|
8
|
+
};
|
|
9
|
+
export type ListDocsInput = {
|
|
10
|
+
tag?: string;
|
|
11
|
+
};
|
|
12
|
+
export type ListDocsOutput = {
|
|
13
|
+
docs: DocListEntry[];
|
|
14
|
+
};
|
|
15
|
+
export declare function listDocs(index: DocsIndex, input?: ListDocsInput): ToolResult<ListDocsOutput>;
|
|
16
|
+
export type ReadDocInput = {
|
|
17
|
+
id: string;
|
|
18
|
+
};
|
|
19
|
+
export type ReadDocOutput = {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
body: string;
|
|
23
|
+
seeAlso?: string[];
|
|
24
|
+
};
|
|
25
|
+
export declare function readDoc(index: DocsIndex, input: ReadDocInput): ToolResult<ReadDocOutput>;
|
|
26
|
+
export type SearchDocsInput = {
|
|
27
|
+
query: string;
|
|
28
|
+
limit?: number;
|
|
29
|
+
};
|
|
30
|
+
export type SearchDocsOutput = {
|
|
31
|
+
results: Array<{
|
|
32
|
+
id: string;
|
|
33
|
+
title: string;
|
|
34
|
+
snippet: string;
|
|
35
|
+
score: number;
|
|
36
|
+
}>;
|
|
37
|
+
};
|
|
38
|
+
export declare function searchDocs(index: DocsIndex, input: SearchDocsInput): ToolResult<SearchDocsOutput>;
|
|
39
|
+
export type GetApiSignatureInput = {
|
|
40
|
+
name: string;
|
|
41
|
+
};
|
|
42
|
+
export type GetApiSignatureOutput = {
|
|
43
|
+
symbol: string;
|
|
44
|
+
docId: string;
|
|
45
|
+
title: string;
|
|
46
|
+
signature: string;
|
|
47
|
+
summary: string;
|
|
48
|
+
};
|
|
49
|
+
export declare function getApiSignature(index: DocsIndex, input: GetApiSignatureInput): ToolResult<GetApiSignatureOutput>;
|
|
50
|
+
export type GetTypeDefinitionInput = {
|
|
51
|
+
name: string;
|
|
52
|
+
};
|
|
53
|
+
export type GetTypeDefinitionOutput = {
|
|
54
|
+
/** The display name as documented (e.g. "PlaneLike"). */
|
|
55
|
+
name: string;
|
|
56
|
+
/** Doc id, e.g. "api/types/plane-like". */
|
|
57
|
+
docId: string;
|
|
58
|
+
title: string;
|
|
59
|
+
/** First code block — the type signature (`type X = ...` or `interface X { ... }`). */
|
|
60
|
+
definition: string;
|
|
61
|
+
/** Full markdown body, frontmatter already stripped. */
|
|
62
|
+
body: string;
|
|
63
|
+
summary: string;
|
|
64
|
+
seeAlso?: string[];
|
|
65
|
+
};
|
|
66
|
+
export declare function getTypeDefinition(index: DocsIndex, input: GetTypeDefinitionInput): ToolResult<GetTypeDefinitionOutput>;
|