forma-dsl-mcp 0.1.4
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/LICENSE +21 -0
- package/README.md +97 -0
- package/package.json +52 -0
- package/src/bin.js +30 -0
- package/src/catalogue.js +703 -0
- package/src/document.js +285 -0
- package/src/examples.js +212 -0
- package/src/guide.js +313 -0
- package/src/reference/README.md +153 -0
- package/src/reference/api/describe-parameters.md +167 -0
- package/src/reference/api/evaluation-context.md +221 -0
- package/src/reference/api/export.md +185 -0
- package/src/reference/api/geometry-node.md +215 -0
- package/src/reference/api/kernel.md +182 -0
- package/src/reference/api/low-level.md +239 -0
- package/src/reference/api/render.md +217 -0
- package/src/reference/api/values.md +223 -0
- package/src/reference/errors.md +253 -0
- package/src/reference/language/control-flow.md +160 -0
- package/src/reference/language/expressions.md +206 -0
- package/src/reference/language/overview.md +156 -0
- package/src/reference/language/scope.md +184 -0
- package/src/reference/language/syntax.md +210 -0
- package/src/reference/reference/align.md +189 -0
- package/src/reference/reference/booleans.md +231 -0
- package/src/reference/reference/component.md +248 -0
- package/src/reference/reference/conversions.md +269 -0
- package/src/reference/reference/functions.md +314 -0
- package/src/reference/reference/local.md +197 -0
- package/src/reference/reference/model.md +213 -0
- package/src/reference/reference/param.md +230 -0
- package/src/reference/reference/part.md +229 -0
- package/src/reference/reference/refinement.md +284 -0
- package/src/reference/reference/shapes-2d.md +309 -0
- package/src/reference/reference/shapes-3d.md +279 -0
- package/src/reference/reference/transforms.md +246 -0
- package/src/reference.js +160 -0
- package/src/server.js +219 -0
- package/src/tools.js +523 -0
- package/src/workspace.js +204 -0
package/src/server.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server itself: what it exposes, and over what.
|
|
3
|
+
*
|
|
4
|
+
* Tools are the part a model calls; resources are the part a client can attach to a
|
|
5
|
+
* conversation by hand, and carry the same knowledge in a form a person can browse. Both
|
|
6
|
+
* read from the same modules, so there is one description of the language here, not two.
|
|
7
|
+
*/
|
|
8
|
+
/** @import { Workspace } from './workspace.js' */
|
|
9
|
+
|
|
10
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
|
|
13
|
+
import { CONSTRUCTS, LANGUAGE_CONSTANTS, LANGUAGE_FUNCTIONS } from './catalogue.js';
|
|
14
|
+
import { EXAMPLES, EXAMPLES_BY_NAME } from './examples.js';
|
|
15
|
+
import { PAGES, readPage } from './reference.js';
|
|
16
|
+
import { guide } from './guide.js';
|
|
17
|
+
import { registerTools } from './tools.js';
|
|
18
|
+
|
|
19
|
+
/** The package's own version, reported in the handshake. */
|
|
20
|
+
export const VERSION = '0.1.0';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What the server tells a client it is for, offered as instructions during initialisation.
|
|
24
|
+
*
|
|
25
|
+
* Clients put this in front of the model before it has called anything, so it is the one
|
|
26
|
+
* place to say the thing that changes behaviour most: check before you claim it works.
|
|
27
|
+
*/
|
|
28
|
+
const INSTRUCTIONS = `This server knows forma, a declarative DSL for 3D solids, and can write
|
|
29
|
+
and solve .forma documents.
|
|
30
|
+
|
|
31
|
+
Start with \`forma_guide\` — it is the language in one page. Use \`forma_blocks\` for the
|
|
32
|
+
attributes of the blocks you are about to write, \`forma_examples\` for a working document of
|
|
33
|
+
a similar shape, and \`forma_reference\` when you need the manual.
|
|
34
|
+
|
|
35
|
+
Never hand a .forma document to anyone without running \`forma_check\` on it first. The check
|
|
36
|
+
builds the geometry with the real kernel and reports the bounding box, the volume and the
|
|
37
|
+
triangle count of every part, which is the only way to know that the model is the size that
|
|
38
|
+
was asked for and has not silently solved to nothing. Write files with \`forma_write\`, which
|
|
39
|
+
runs the same check and refuses a document that does not render.`;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Builds the server, with every tool, resource and prompt registered.
|
|
43
|
+
*
|
|
44
|
+
* @param {Workspace} workspace The patch of filesystem the file tools may touch.
|
|
45
|
+
* @returns {McpServer} The server, not yet connected to a transport.
|
|
46
|
+
*/
|
|
47
|
+
export function createServer(workspace) {
|
|
48
|
+
const server = new McpServer(
|
|
49
|
+
{ name: 'forma-dsl', version: VERSION },
|
|
50
|
+
{ capabilities: { tools: {}, resources: {}, prompts: {} }, instructions: INSTRUCTIONS },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
registerTools(server, workspace);
|
|
54
|
+
registerResources(server);
|
|
55
|
+
registerPrompts(server);
|
|
56
|
+
|
|
57
|
+
return server;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Registers the browsable copies of what the tools serve.
|
|
62
|
+
*
|
|
63
|
+
* @param {McpServer} server The server.
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
function registerResources(server) {
|
|
67
|
+
server.registerResource('guide', 'forma://guide', {
|
|
68
|
+
title: 'The forma language in brief',
|
|
69
|
+
description: 'Everything needed to write a document: structure, nesting, dimensionality, syntax, pitfalls.',
|
|
70
|
+
mimeType: 'text/markdown',
|
|
71
|
+
}, async (uri) => ({
|
|
72
|
+
contents: [{ uri: uri.href, mimeType: 'text/markdown', text: guide() }],
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
server.registerResource('catalogue', 'forma://catalogue', {
|
|
76
|
+
title: 'Block and function catalogue',
|
|
77
|
+
description: 'Every block with its attributes, and every builtin function, as JSON.',
|
|
78
|
+
mimeType: 'application/json',
|
|
79
|
+
}, async (uri) => ({
|
|
80
|
+
contents: [{
|
|
81
|
+
uri: uri.href,
|
|
82
|
+
mimeType: 'application/json',
|
|
83
|
+
text: JSON.stringify({
|
|
84
|
+
blocks: [...CONSTRUCTS.values()],
|
|
85
|
+
functions: LANGUAGE_FUNCTIONS,
|
|
86
|
+
constants: LANGUAGE_CONSTANTS,
|
|
87
|
+
}, null, 2),
|
|
88
|
+
}],
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
server.registerResource(
|
|
92
|
+
'reference',
|
|
93
|
+
new ResourceTemplate('forma://reference/{+path}', {
|
|
94
|
+
list: async () => ({
|
|
95
|
+
resources: PAGES.map((page) => ({
|
|
96
|
+
uri: `forma://reference/${page.path}`,
|
|
97
|
+
name: page.title,
|
|
98
|
+
description: page.about,
|
|
99
|
+
mimeType: 'text/markdown',
|
|
100
|
+
})),
|
|
101
|
+
}),
|
|
102
|
+
complete: {
|
|
103
|
+
path: (value) => PAGES.map((page) => page.path).filter((path) => path.startsWith(value)),
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
{ title: 'Reference manual', description: 'One page of the manual.', mimeType: 'text/markdown' },
|
|
107
|
+
async (uri, { path }) => {
|
|
108
|
+
const page = await readPage(Array.isArray(path) ? path[0] : path);
|
|
109
|
+
return { contents: [{ uri: uri.href, mimeType: 'text/markdown', text: page.markdown }] };
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
server.registerResource(
|
|
114
|
+
'example',
|
|
115
|
+
new ResourceTemplate('forma://example/{name}', {
|
|
116
|
+
list: async () => ({
|
|
117
|
+
resources: EXAMPLES.map((example) => ({
|
|
118
|
+
uri: `forma://example/${example.name}`,
|
|
119
|
+
name: example.name,
|
|
120
|
+
description: example.summary,
|
|
121
|
+
mimeType: 'text/plain',
|
|
122
|
+
})),
|
|
123
|
+
}),
|
|
124
|
+
complete: {
|
|
125
|
+
name: (value) => EXAMPLES.map((example) => example.name)
|
|
126
|
+
.filter((name) => name.startsWith(value)),
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
{ title: 'Worked example', description: 'A complete document that renders.', mimeType: 'text/plain' },
|
|
130
|
+
async (uri, { name }) => {
|
|
131
|
+
const key = Array.isArray(name) ? name[0] : name;
|
|
132
|
+
const example = EXAMPLES_BY_NAME.get(key);
|
|
133
|
+
if (!example) throw new Error(`no example "${key}"`);
|
|
134
|
+
return { contents: [{ uri: uri.href, mimeType: 'text/plain', text: example.source }] };
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Registers the prompts, which are the workflows worth having a name for.
|
|
141
|
+
*
|
|
142
|
+
* @param {McpServer} server The server.
|
|
143
|
+
* @returns {void}
|
|
144
|
+
*/
|
|
145
|
+
function registerPrompts(server) {
|
|
146
|
+
server.registerPrompt('model_a_part', {
|
|
147
|
+
title: 'Model a part in forma',
|
|
148
|
+
description: 'Walks from a description of a physical object to a checked .forma document.',
|
|
149
|
+
argsSchema: {
|
|
150
|
+
part: z.string().describe('What to model, e.g. "a wall bracket for a 35 mm pipe".'),
|
|
151
|
+
constraints: z.string().optional()
|
|
152
|
+
.describe('Dimensions, tolerances, print constraints — anything the result has to satisfy.'),
|
|
153
|
+
path: z.string().optional().describe('Where to write the finished document.'),
|
|
154
|
+
},
|
|
155
|
+
}, ({ part, constraints, path }) => ({
|
|
156
|
+
messages: [{
|
|
157
|
+
role: 'user',
|
|
158
|
+
content: {
|
|
159
|
+
type: 'text',
|
|
160
|
+
text: [
|
|
161
|
+
`Model this in forma: ${part}.`,
|
|
162
|
+
constraints ? `\nIt has to satisfy: ${constraints}` : '',
|
|
163
|
+
'',
|
|
164
|
+
'Work in this order:',
|
|
165
|
+
'1. Call `forma_guide` if you have not already read it this session.',
|
|
166
|
+
'2. Call `forma_examples` and read the one closest to what this is.',
|
|
167
|
+
'3. Decide what is a `param` (anything a person would reasonably change, with a '
|
|
168
|
+
+ 'default, a min and a max) and what is a `local` (anything derived from those).',
|
|
169
|
+
'4. Write the document. Prefer a 2D profile given thickness by `extrude` or '
|
|
170
|
+
+ '`revolve` over composing solids. Name any repeated feature — including the '
|
|
171
|
+
+ 'negative of one — as a `component`. Give each physical piece its own `part` with '
|
|
172
|
+
+ 'a colour.',
|
|
173
|
+
'5. Call `forma_check` with `solve: true`. Read the bounding box and the volume back '
|
|
174
|
+
+ 'and confirm they are what was asked for. A part with zero volume means the model '
|
|
175
|
+
+ 'silently produced nothing — fix it, do not report it as done.',
|
|
176
|
+
'6. Iterate until it renders and measures correctly.',
|
|
177
|
+
path
|
|
178
|
+
? `7. Write it to \`${path}\` with \`forma_write\`.`
|
|
179
|
+
: '7. Offer to write it with `forma_write` once it checks out.',
|
|
180
|
+
'',
|
|
181
|
+
'Report the finished document along with its measured size, and say which params '
|
|
182
|
+
+ 'drive which dimension.',
|
|
183
|
+
].filter(Boolean).join('\n'),
|
|
184
|
+
},
|
|
185
|
+
}],
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
server.registerPrompt('review_a_document', {
|
|
189
|
+
title: 'Review a .forma document',
|
|
190
|
+
description: 'Checks an existing document and reports what is wrong or fragile about it.',
|
|
191
|
+
argsSchema: { path: z.string().describe('The document to review.') },
|
|
192
|
+
}, ({ path }) => ({
|
|
193
|
+
messages: [{
|
|
194
|
+
role: 'user',
|
|
195
|
+
content: {
|
|
196
|
+
type: 'text',
|
|
197
|
+
text: [
|
|
198
|
+
`Review the forma document at \`${path}\`.`,
|
|
199
|
+
'',
|
|
200
|
+
'Call `forma_read` on it, then look for:',
|
|
201
|
+
'- parts that solved to nothing, or a bounding box that is not what the document '
|
|
202
|
+
+ 'seems to intend;',
|
|
203
|
+
'- a `part` nested inside a transform or a boolean, which is silently dropped;',
|
|
204
|
+
'- a `difference` whose cutter ends exactly on a face, leaving a zero-thickness skin;',
|
|
205
|
+
'- dimensions written as bare numbers where a `param` or a `local` would say what '
|
|
206
|
+
+ 'they mean;',
|
|
207
|
+
'- a `param` whose `min`/`max` the model does not actually clamp, since those bounds '
|
|
208
|
+
+ 'are advisory and never enforced;',
|
|
209
|
+
'- geometry that would not print: unsupported overhangs, walls thinner than a nozzle, '
|
|
210
|
+
+ 'a non-zero genus where a solid was meant.',
|
|
211
|
+
'',
|
|
212
|
+
'Re-check with different param values at the ends of their declared ranges — a model '
|
|
213
|
+
+ 'that only works at its defaults is the common failure. Report what you found, and '
|
|
214
|
+
+ 'propose the smallest edit that fixes each one.',
|
|
215
|
+
].join('\n'),
|
|
216
|
+
},
|
|
217
|
+
}],
|
|
218
|
+
}));
|
|
219
|
+
}
|