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,261 @@
|
|
|
1
|
+
// In-memory index over `llm-docs/`.
|
|
2
|
+
//
|
|
3
|
+
// Loaded once at MCP startup. Resolves the on-disk docs root (either the
|
|
4
|
+
// published `node_modules/fluidcad/llm-docs/` or the in-repo `<root>/llm-docs/`
|
|
5
|
+
// during dev), reads the two manifests written by `scripts/build-llm-docs.ts`,
|
|
6
|
+
// and builds an inverted index for `search_docs`.
|
|
7
|
+
//
|
|
8
|
+
// Bodies are loaded lazily on first read and cached. The seed set is small
|
|
9
|
+
// (single-digit MB at most) so we don't bother with any external search dep
|
|
10
|
+
// or LRU bookkeeping — a Map is plenty.
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { createRequire } from 'module';
|
|
15
|
+
const FIELD_WEIGHT = {
|
|
16
|
+
title: 5,
|
|
17
|
+
summary: 3,
|
|
18
|
+
tags: 4,
|
|
19
|
+
body: 1,
|
|
20
|
+
};
|
|
21
|
+
// Small fixed stopword set. Kept tiny on purpose — the corpus is small and we
|
|
22
|
+
// want exact lookups like "z" or "ts" to still work for users.
|
|
23
|
+
const STOPWORDS = new Set([
|
|
24
|
+
'a', 'an', 'and', 'are', 'as', 'at', 'be', 'but', 'by', 'for', 'from', 'has',
|
|
25
|
+
'have', 'in', 'into', 'is', 'it', 'its', 'of', 'on', 'or', 'that', 'the', 'to',
|
|
26
|
+
'was', 'were', 'will', 'with',
|
|
27
|
+
]);
|
|
28
|
+
export class DocsIndex {
|
|
29
|
+
/** Absolute path to the docs root directory. */
|
|
30
|
+
root;
|
|
31
|
+
/** Every doc record, in the order they were emitted by the build script. */
|
|
32
|
+
docs;
|
|
33
|
+
/** Symbol -> docId (from `llm-docs/api/index.json`). */
|
|
34
|
+
symbols;
|
|
35
|
+
/**
|
|
36
|
+
* Type-name -> docId, restricted to docs under `api/types/`. Populated by
|
|
37
|
+
* filtering `symbols` so the same symbol map drives both `get_api_signature`
|
|
38
|
+
* and `get_type_definition`, but the latter rejects function symbols with a
|
|
39
|
+
* clear "not a type" error rather than returning a feature signature.
|
|
40
|
+
*/
|
|
41
|
+
types;
|
|
42
|
+
byId;
|
|
43
|
+
bodyCache = new Map();
|
|
44
|
+
/** token -> docId -> per-field hit counts. */
|
|
45
|
+
inverted;
|
|
46
|
+
constructor(root, index, apiIndex) {
|
|
47
|
+
this.root = root;
|
|
48
|
+
this.docs = index.docs;
|
|
49
|
+
this.symbols = apiIndex.symbols;
|
|
50
|
+
this.types = Object.fromEntries(Object.entries(apiIndex.symbols).filter(([, docId]) => docId.startsWith('api/types/')));
|
|
51
|
+
this.byId = new Map(index.docs.map((d) => [d.id, d]));
|
|
52
|
+
this.inverted = this.buildInverted();
|
|
53
|
+
}
|
|
54
|
+
/** All docs, optionally filtered to those carrying a given tag. */
|
|
55
|
+
list(tag) {
|
|
56
|
+
if (!tag) {
|
|
57
|
+
return [...this.docs];
|
|
58
|
+
}
|
|
59
|
+
return this.docs.filter((d) => (d.tags ?? []).includes(tag));
|
|
60
|
+
}
|
|
61
|
+
get(id) {
|
|
62
|
+
return this.byId.get(id) ?? null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Load the markdown body for a doc id, stripping the YAML frontmatter so
|
|
66
|
+
* callers see the same text the docs author wrote (no metadata clutter).
|
|
67
|
+
*/
|
|
68
|
+
body(id) {
|
|
69
|
+
const cached = this.bodyCache.get(id);
|
|
70
|
+
if (cached !== undefined) {
|
|
71
|
+
return cached;
|
|
72
|
+
}
|
|
73
|
+
const doc = this.byId.get(id);
|
|
74
|
+
if (!doc) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const raw = fs.readFileSync(path.join(this.root, doc.file), 'utf8');
|
|
78
|
+
const body = stripFrontmatter(raw);
|
|
79
|
+
this.bodyCache.set(id, body);
|
|
80
|
+
return body;
|
|
81
|
+
}
|
|
82
|
+
/** Keyword search. Returns ranked hits with an ~80-char snippet per result. */
|
|
83
|
+
search(query, limit = 10) {
|
|
84
|
+
const tokens = tokenize(query);
|
|
85
|
+
if (tokens.length === 0) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const scores = new Map();
|
|
89
|
+
for (const token of tokens) {
|
|
90
|
+
const postings = this.inverted.get(token);
|
|
91
|
+
if (!postings) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
for (const [docId, hits] of postings) {
|
|
95
|
+
let local = 0;
|
|
96
|
+
local += hits.title * FIELD_WEIGHT.title;
|
|
97
|
+
local += hits.summary * FIELD_WEIGHT.summary;
|
|
98
|
+
local += hits.tags * FIELD_WEIGHT.tags;
|
|
99
|
+
local += hits.body * FIELD_WEIGHT.body;
|
|
100
|
+
scores.set(docId, (scores.get(docId) ?? 0) + local);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Exact-symbol boost — agents often type the bare function name.
|
|
104
|
+
for (const token of tokens) {
|
|
105
|
+
const docId = this.symbols[token];
|
|
106
|
+
if (docId) {
|
|
107
|
+
scores.set(docId, (scores.get(docId) ?? 0) + 10);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const ranked = [...scores.entries()]
|
|
111
|
+
.sort((a, b) => b[1] - a[1])
|
|
112
|
+
.slice(0, limit);
|
|
113
|
+
return ranked.map(([docId, score]) => {
|
|
114
|
+
const doc = this.byId.get(docId);
|
|
115
|
+
return {
|
|
116
|
+
id: doc.id,
|
|
117
|
+
title: doc.title,
|
|
118
|
+
snippet: this.snippet(doc.id, tokens),
|
|
119
|
+
score,
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** First fenced code block in the body, used as a symbol's signature surface. */
|
|
124
|
+
firstCodeBlock(id) {
|
|
125
|
+
const body = this.body(id);
|
|
126
|
+
if (!body) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const match = body.match(/```[a-zA-Z0-9_-]*\n([\s\S]*?)```/);
|
|
130
|
+
return match ? match[1].trimEnd() : null;
|
|
131
|
+
}
|
|
132
|
+
buildInverted() {
|
|
133
|
+
const inverted = new Map();
|
|
134
|
+
const add = (token, docId, field) => {
|
|
135
|
+
let perDoc = inverted.get(token);
|
|
136
|
+
if (!perDoc) {
|
|
137
|
+
perDoc = new Map();
|
|
138
|
+
inverted.set(token, perDoc);
|
|
139
|
+
}
|
|
140
|
+
let hits = perDoc.get(docId);
|
|
141
|
+
if (!hits) {
|
|
142
|
+
hits = { title: 0, summary: 0, tags: 0, body: 0 };
|
|
143
|
+
perDoc.set(docId, hits);
|
|
144
|
+
}
|
|
145
|
+
hits[field] += 1;
|
|
146
|
+
};
|
|
147
|
+
for (const doc of this.docs) {
|
|
148
|
+
for (const t of tokenize(doc.title)) {
|
|
149
|
+
add(t, doc.id, 'title');
|
|
150
|
+
}
|
|
151
|
+
for (const t of tokenize(doc.summary)) {
|
|
152
|
+
add(t, doc.id, 'summary');
|
|
153
|
+
}
|
|
154
|
+
for (const tag of doc.tags ?? []) {
|
|
155
|
+
for (const t of tokenize(tag)) {
|
|
156
|
+
add(t, doc.id, 'tags');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const body = this.body(doc.id);
|
|
160
|
+
if (body) {
|
|
161
|
+
for (const t of tokenize(body)) {
|
|
162
|
+
add(t, doc.id, 'body');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return inverted;
|
|
167
|
+
}
|
|
168
|
+
snippet(docId, tokens) {
|
|
169
|
+
const body = this.body(docId);
|
|
170
|
+
if (!body) {
|
|
171
|
+
return '';
|
|
172
|
+
}
|
|
173
|
+
const lower = body.toLowerCase();
|
|
174
|
+
let bestIdx = -1;
|
|
175
|
+
for (const token of tokens) {
|
|
176
|
+
const i = lower.indexOf(token);
|
|
177
|
+
if (i !== -1 && (bestIdx === -1 || i < bestIdx)) {
|
|
178
|
+
bestIdx = i;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (bestIdx === -1) {
|
|
182
|
+
return body.slice(0, 160).replace(/\s+/g, ' ').trim();
|
|
183
|
+
}
|
|
184
|
+
const start = Math.max(0, bestIdx - 40);
|
|
185
|
+
const end = Math.min(body.length, bestIdx + 120);
|
|
186
|
+
const prefix = start > 0 ? '…' : '';
|
|
187
|
+
const suffix = end < body.length ? '…' : '';
|
|
188
|
+
return prefix + body.slice(start, end).replace(/\s+/g, ' ').trim() + suffix;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Locate the on-disk `llm-docs/` directory.
|
|
193
|
+
*
|
|
194
|
+
* Resolution order:
|
|
195
|
+
* 1. The installed package — `require.resolve('fluidcad/package.json')` gives
|
|
196
|
+
* us the consumer's `node_modules/fluidcad/`, and `llm-docs/` sits next to
|
|
197
|
+
* that `package.json`.
|
|
198
|
+
* 2. The in-repo path — walk up from this file (`mcp/src/` or `mcp/dist/`)
|
|
199
|
+
* until we hit a sibling `llm-docs/` directory.
|
|
200
|
+
*
|
|
201
|
+
* We never trust `process.cwd()`: the MCP process is launched by an external
|
|
202
|
+
* client (Claude Desktop, etc.) so its CWD is whatever they happen to be in.
|
|
203
|
+
*/
|
|
204
|
+
export function resolveDocsRoot() {
|
|
205
|
+
const here = fileURLToPath(import.meta.url);
|
|
206
|
+
const require_ = createRequire(here);
|
|
207
|
+
try {
|
|
208
|
+
const pkg = require_.resolve('fluidcad/package.json');
|
|
209
|
+
const candidate = path.join(path.dirname(pkg), 'llm-docs');
|
|
210
|
+
if (fs.existsSync(path.join(candidate, 'index.json'))) {
|
|
211
|
+
return candidate;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Falls through to the dev-walkup path below.
|
|
216
|
+
}
|
|
217
|
+
let dir = path.dirname(here);
|
|
218
|
+
for (let i = 0; i < 8; i++) {
|
|
219
|
+
const candidate = path.join(dir, 'llm-docs');
|
|
220
|
+
if (fs.existsSync(path.join(candidate, 'index.json'))) {
|
|
221
|
+
return candidate;
|
|
222
|
+
}
|
|
223
|
+
const parent = path.dirname(dir);
|
|
224
|
+
if (parent === dir) {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
dir = parent;
|
|
228
|
+
}
|
|
229
|
+
throw new Error(`Could not locate llm-docs/ from ${here}. ` +
|
|
230
|
+
`Run \`npm run build:llm-docs\` to generate the manifest, ` +
|
|
231
|
+
`or ensure the package is installed.`);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Load the manifests from `<root>/index.json` and `<root>/api/index.json` and
|
|
235
|
+
* construct the in-memory index. Throws (with a helpful message) if either
|
|
236
|
+
* manifest is missing — that almost always means `build:llm-docs` was skipped.
|
|
237
|
+
*/
|
|
238
|
+
export function loadDocsIndex(root) {
|
|
239
|
+
const resolved = root ?? resolveDocsRoot();
|
|
240
|
+
const indexPath = path.join(resolved, 'index.json');
|
|
241
|
+
const apiIndexPath = path.join(resolved, 'api', 'index.json');
|
|
242
|
+
if (!fs.existsSync(indexPath)) {
|
|
243
|
+
throw new Error(`llm-docs manifest missing at ${indexPath}. Run \`npm run build:llm-docs\`.`);
|
|
244
|
+
}
|
|
245
|
+
if (!fs.existsSync(apiIndexPath)) {
|
|
246
|
+
throw new Error(`llm-docs api manifest missing at ${apiIndexPath}. Run \`npm run build:llm-docs\`.`);
|
|
247
|
+
}
|
|
248
|
+
const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
249
|
+
const apiIndex = JSON.parse(fs.readFileSync(apiIndexPath, 'utf8'));
|
|
250
|
+
return new DocsIndex(resolved, index, apiIndex);
|
|
251
|
+
}
|
|
252
|
+
function tokenize(text) {
|
|
253
|
+
return text
|
|
254
|
+
.toLowerCase()
|
|
255
|
+
.split(/[^a-z0-9_]+/)
|
|
256
|
+
.filter((t) => t.length > 0 && !STOPWORDS.has(t));
|
|
257
|
+
}
|
|
258
|
+
function stripFrontmatter(raw) {
|
|
259
|
+
const match = raw.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)$/);
|
|
260
|
+
return match ? match[1] : raw;
|
|
261
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { DocsIndex } from './docs-index.ts';
|
|
3
|
+
export declare const URI_OVERVIEW = "fluidcad-docs://overview";
|
|
4
|
+
export declare function registerDocResources(server: McpServer, index: DocsIndex): void;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// MCP resource registrations for `fluidcad-docs://*`.
|
|
2
|
+
//
|
|
3
|
+
// Resources are a bonus surface — clients that auto-attach them (e.g. Claude
|
|
4
|
+
// Desktop) get the docs for free without burning tool calls. Clients that
|
|
5
|
+
// ignore resources fall back to the `list_docs` / `read_doc` tools, so the
|
|
6
|
+
// two surfaces are designed to be self-sufficient.
|
|
7
|
+
//
|
|
8
|
+
// Every resource is registered statically. The seed set is small (under a
|
|
9
|
+
// dozen entries) so we trade the template machinery for a simple per-doc
|
|
10
|
+
// `registerResource` call — the resulting list shows up explicitly in the
|
|
11
|
+
// MCP client's resource picker.
|
|
12
|
+
export const URI_OVERVIEW = 'fluidcad-docs://overview';
|
|
13
|
+
export function registerDocResources(server, index) {
|
|
14
|
+
server.registerResource('fluidcad-docs-overview', URI_OVERVIEW, {
|
|
15
|
+
title: 'FluidCAD docs — overview',
|
|
16
|
+
description: 'Single-document aggregate of the FluidCAD doc set: titles, summaries, and tags for every API symbol and concept.',
|
|
17
|
+
mimeType: 'text/markdown',
|
|
18
|
+
}, async (uri) => ({
|
|
19
|
+
contents: [
|
|
20
|
+
{
|
|
21
|
+
uri: uri.href,
|
|
22
|
+
mimeType: 'text/markdown',
|
|
23
|
+
text: renderOverview(index),
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
}));
|
|
27
|
+
// One static resource per API symbol. We use the symbol map (not the doc
|
|
28
|
+
// list) so a single doc that documents multiple symbols still appears under
|
|
29
|
+
// each. The doc body is identical across symbol resources — that's fine,
|
|
30
|
+
// clients dedupe by content.
|
|
31
|
+
for (const [symbol, docId] of Object.entries(index.symbols)) {
|
|
32
|
+
const doc = index.get(docId);
|
|
33
|
+
if (!doc) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
server.registerResource(`fluidcad-docs-api-${symbol}`, `fluidcad-docs://api/${symbol}`, {
|
|
37
|
+
title: doc.title,
|
|
38
|
+
description: doc.summary,
|
|
39
|
+
mimeType: 'text/markdown',
|
|
40
|
+
}, async (uri) => ({
|
|
41
|
+
contents: [
|
|
42
|
+
{
|
|
43
|
+
uri: uri.href,
|
|
44
|
+
mimeType: 'text/markdown',
|
|
45
|
+
text: index.body(doc.id) ?? '',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
// One static resource per non-API doc, exposed as `guide/<slug>`. Slug is
|
|
51
|
+
// the doc id stripped of any leading category prefix (e.g.
|
|
52
|
+
// `concepts/scene-graph` → `scene-graph`). If two ids collide on slug, we
|
|
53
|
+
// keep the first and skip the rest — collisions are flagged here rather
|
|
54
|
+
// than during manifest validation since slug uniqueness is a resource-layer
|
|
55
|
+
// concern, not a doc-content concern.
|
|
56
|
+
const seenSlugs = new Set();
|
|
57
|
+
for (const doc of index.docs) {
|
|
58
|
+
if (isApiDoc(doc)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const slug = guideSlug(doc.id);
|
|
62
|
+
if (seenSlugs.has(slug)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
seenSlugs.add(slug);
|
|
66
|
+
server.registerResource(`fluidcad-docs-guide-${slug}`, `fluidcad-docs://guide/${slug}`, {
|
|
67
|
+
title: doc.title,
|
|
68
|
+
description: doc.summary,
|
|
69
|
+
mimeType: 'text/markdown',
|
|
70
|
+
}, async (uri) => ({
|
|
71
|
+
contents: [
|
|
72
|
+
{
|
|
73
|
+
uri: uri.href,
|
|
74
|
+
mimeType: 'text/markdown',
|
|
75
|
+
text: index.body(doc.id) ?? '',
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function isApiDoc(doc) {
|
|
82
|
+
return doc.id.startsWith('api/');
|
|
83
|
+
}
|
|
84
|
+
function guideSlug(id) {
|
|
85
|
+
const slash = id.indexOf('/');
|
|
86
|
+
return slash === -1 ? id : id.slice(slash + 1);
|
|
87
|
+
}
|
|
88
|
+
function renderOverview(index) {
|
|
89
|
+
const lines = [];
|
|
90
|
+
lines.push('# FluidCAD docs');
|
|
91
|
+
lines.push('');
|
|
92
|
+
lines.push('Generated from `llm-docs/`. Each entry lists a doc id, its title, and a one-line summary.');
|
|
93
|
+
lines.push('');
|
|
94
|
+
const apiDocs = index.docs.filter(isApiDoc);
|
|
95
|
+
const otherDocs = index.docs.filter((d) => !isApiDoc(d));
|
|
96
|
+
if (apiDocs.length > 0) {
|
|
97
|
+
lines.push('## API');
|
|
98
|
+
lines.push('');
|
|
99
|
+
for (const doc of apiDocs) {
|
|
100
|
+
lines.push(`- **${doc.id}** — ${doc.title}`);
|
|
101
|
+
lines.push(` ${doc.summary}`);
|
|
102
|
+
}
|
|
103
|
+
lines.push('');
|
|
104
|
+
}
|
|
105
|
+
if (otherDocs.length > 0) {
|
|
106
|
+
lines.push('## Concepts and guides');
|
|
107
|
+
lines.push('');
|
|
108
|
+
for (const doc of otherDocs) {
|
|
109
|
+
lines.push(`- **${doc.id}** — ${doc.title}`);
|
|
110
|
+
lines.push(` ${doc.summary}`);
|
|
111
|
+
}
|
|
112
|
+
lines.push('');
|
|
113
|
+
}
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { type DocsIndex } from './docs-index.ts';
|
|
4
|
+
export declare const SERVER_NAME = "FluidCAD";
|
|
5
|
+
export declare const SERVER_VERSION: string;
|
|
6
|
+
export type BuildServerOptions = {
|
|
7
|
+
/** Pre-built docs index. Tests use this to inject a custom docs root. */
|
|
8
|
+
docsIndex?: DocsIndex;
|
|
9
|
+
};
|
|
10
|
+
export declare function buildServer(options?: BuildServerOptions): McpServer;
|
|
11
|
+
export declare function runStdio(): Promise<void>;
|
|
12
|
+
export { z };
|