@lineadraw/mcp 0.1.0
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 +85 -0
- package/dist/editor.html +2340 -0
- package/dist/server.js +777 -0
- package/dist/server.js.map +1 -0
- package/dist/widget-LQz2fm1O.html +10 -0
- package/package.json +62 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server.ts
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import https from "https";
|
|
6
|
+
import cors from "cors";
|
|
7
|
+
import express from "express";
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
11
|
+
|
|
12
|
+
// src/documents.ts
|
|
13
|
+
import { newDrawing, openDrawing, drawingFromJson } from "@lineadraw/sdk";
|
|
14
|
+
var DocumentRegistry = class {
|
|
15
|
+
docs = /* @__PURE__ */ new Map();
|
|
16
|
+
revisions = /* @__PURE__ */ new Map();
|
|
17
|
+
activeId = null;
|
|
18
|
+
create(name) {
|
|
19
|
+
const doc = newDrawing(name);
|
|
20
|
+
const id = doc.document.id;
|
|
21
|
+
this.docs.set(id, doc);
|
|
22
|
+
this.revisions.set(id, 0);
|
|
23
|
+
this.activeId = id;
|
|
24
|
+
return { id, doc };
|
|
25
|
+
}
|
|
26
|
+
async open(path2) {
|
|
27
|
+
const doc = await openDrawing(path2);
|
|
28
|
+
const id = doc.document.id;
|
|
29
|
+
this.docs.set(id, doc);
|
|
30
|
+
this.revisions.set(id, 0);
|
|
31
|
+
this.activeId = id;
|
|
32
|
+
return { id, doc };
|
|
33
|
+
}
|
|
34
|
+
/** Resolve a document by id, or the active one when omitted. */
|
|
35
|
+
get(id) {
|
|
36
|
+
const key = id ?? this.activeId;
|
|
37
|
+
if (!key) throw new Error("No document \u2014 call linea_new_document first");
|
|
38
|
+
const doc = this.docs.get(key);
|
|
39
|
+
if (!doc) throw new Error(`Unknown document "${key}"`);
|
|
40
|
+
return { id: key, doc };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Replace a document's full state (validated + migrated) — how in-chat
|
|
44
|
+
* editor edits land. The id must match; conversation undo history resets.
|
|
45
|
+
*/
|
|
46
|
+
replace(id, raw) {
|
|
47
|
+
if (!this.docs.has(id)) throw new Error(`Unknown document "${id}"`);
|
|
48
|
+
const doc = drawingFromJson(JSON.stringify(raw));
|
|
49
|
+
if (doc.document.id !== id)
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Document id mismatch: payload has "${doc.document.id}", expected "${id}"`
|
|
52
|
+
);
|
|
53
|
+
this.docs.set(id, doc);
|
|
54
|
+
return { doc, revision: this.bump(id) };
|
|
55
|
+
}
|
|
56
|
+
/** Mark a document changed; the viewer compares revisions to refresh. */
|
|
57
|
+
bump(id) {
|
|
58
|
+
const next = (this.revisions.get(id) ?? 0) + 1;
|
|
59
|
+
this.revisions.set(id, next);
|
|
60
|
+
return next;
|
|
61
|
+
}
|
|
62
|
+
revision(id) {
|
|
63
|
+
return this.revisions.get(id) ?? 0;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/tools.ts
|
|
68
|
+
import { readFile } from "fs/promises";
|
|
69
|
+
import path from "path";
|
|
70
|
+
import { fileURLToPath } from "url";
|
|
71
|
+
import { z as z2 } from "zod";
|
|
72
|
+
import {
|
|
73
|
+
registerAppTool,
|
|
74
|
+
registerAppResource,
|
|
75
|
+
RESOURCE_MIME_TYPE
|
|
76
|
+
} from "@modelcontextprotocol/ext-apps/server";
|
|
77
|
+
|
|
78
|
+
// ../mcp-tools/src/tools.ts
|
|
79
|
+
import { z } from "zod";
|
|
80
|
+
var DEG2RAD = Math.PI / 180;
|
|
81
|
+
var box = z.object({
|
|
82
|
+
minX: z.number(),
|
|
83
|
+
minY: z.number(),
|
|
84
|
+
maxX: z.number(),
|
|
85
|
+
maxY: z.number()
|
|
86
|
+
}).describe("An axis-aligned box in world mm");
|
|
87
|
+
var point = z.tuple([z.number(), z.number()]).describe("A world point in mm: [x, y]");
|
|
88
|
+
var looseObject = z.record(z.string(), z.unknown()).describe(
|
|
89
|
+
'A Linea model object, e.g. {"type":"line","a":[0,0],"b":[100,0]}. Types: line, point (p; paints as a dot sized by lineWidth), polyline (points with bulge, closed), circle, arc, ellipse, hatch, text, dimension, block. Optional layer (name), color (#rrggbb), lineType.'
|
|
90
|
+
);
|
|
91
|
+
var documentId = z.string().optional().describe("Document id; omit for the active document");
|
|
92
|
+
var toolResult = (data) => ({
|
|
93
|
+
content: [{ type: "text", text: JSON.stringify(data) }],
|
|
94
|
+
structuredContent: data
|
|
95
|
+
});
|
|
96
|
+
var isRawToolResult = (v) => typeof v === "object" && v !== null && Array.isArray(v.content);
|
|
97
|
+
var bytesToBase64 = (bytes) => {
|
|
98
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
|
|
99
|
+
let bin = "";
|
|
100
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
101
|
+
return btoa(bin);
|
|
102
|
+
};
|
|
103
|
+
var toolError = (e) => ({
|
|
104
|
+
content: [
|
|
105
|
+
{ type: "text", text: String(e instanceof Error ? e.message : e) }
|
|
106
|
+
],
|
|
107
|
+
isError: true
|
|
108
|
+
});
|
|
109
|
+
var runTool = async (def, ctx, args) => {
|
|
110
|
+
try {
|
|
111
|
+
const parsed = z.object(def.inputSchema).parse(args ?? {});
|
|
112
|
+
const out = await def.run(ctx, parsed);
|
|
113
|
+
return isRawToolResult(out) ? out : toolResult(out);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return toolError(e);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var docPayload = (s) => ({
|
|
119
|
+
documentId: s.id,
|
|
120
|
+
revision: s.revision(),
|
|
121
|
+
document: s.getDocument()
|
|
122
|
+
});
|
|
123
|
+
var coreLineaTools = [
|
|
124
|
+
{
|
|
125
|
+
name: "linea_add_objects",
|
|
126
|
+
title: "Add objects",
|
|
127
|
+
description: "Add objects to model space (or a paper layout via `layout`). Returns the created ids.",
|
|
128
|
+
inputSchema: {
|
|
129
|
+
objects: z.array(looseObject).min(1),
|
|
130
|
+
layout: z.string().optional().describe("Layout name/id for sheet annotations"),
|
|
131
|
+
documentId
|
|
132
|
+
},
|
|
133
|
+
run(ctx, args) {
|
|
134
|
+
const { objects, layout, documentId: idArg } = args;
|
|
135
|
+
const s = ctx.get(idArg);
|
|
136
|
+
const ids = s.add(objects, { layout });
|
|
137
|
+
return { ids, revision: s.bump() };
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "linea_update_objects",
|
|
142
|
+
title: "Update objects",
|
|
143
|
+
description: "Patch objects by id: entries are {id, ...fieldsToChange}. One undo step.",
|
|
144
|
+
inputSchema: {
|
|
145
|
+
entries: z.array(z.record(z.string(), z.unknown())).min(1),
|
|
146
|
+
documentId
|
|
147
|
+
},
|
|
148
|
+
run(ctx, args) {
|
|
149
|
+
const { entries, documentId: idArg } = args;
|
|
150
|
+
const s = ctx.get(idArg);
|
|
151
|
+
s.update(entries);
|
|
152
|
+
return { updated: entries.length, revision: s.bump() };
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "linea_remove_objects",
|
|
157
|
+
title: "Remove objects",
|
|
158
|
+
description: "Delete objects by id. Unknown ids are skipped; `removed` reports how many objects were actually deleted.",
|
|
159
|
+
inputSchema: { ids: z.array(z.string()).min(1), documentId },
|
|
160
|
+
run(ctx, args) {
|
|
161
|
+
const { ids, documentId: idArg } = args;
|
|
162
|
+
const s = ctx.get(idArg);
|
|
163
|
+
const removed = s.remove(ids);
|
|
164
|
+
return { removed, revision: s.bump() };
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "linea_transform",
|
|
169
|
+
title: "Transform objects",
|
|
170
|
+
description: "move (delta) | rotate (angleDeg, center?) | scale (factor, center?) | mirror (axisA, axisB) | copy (delta \u2192 new ids) | array (count TOTAL, delta \u2192 new ids). rotate/scale default their center to the selection bbox center.",
|
|
171
|
+
inputSchema: {
|
|
172
|
+
kind: z.enum(["move", "rotate", "scale", "mirror", "copy", "array"]),
|
|
173
|
+
ids: z.array(z.string()).min(1),
|
|
174
|
+
delta: point.optional(),
|
|
175
|
+
angleDeg: z.number().optional(),
|
|
176
|
+
factor: z.number().optional(),
|
|
177
|
+
center: point.optional(),
|
|
178
|
+
axisA: point.optional(),
|
|
179
|
+
axisB: point.optional(),
|
|
180
|
+
count: z.number().int().optional(),
|
|
181
|
+
documentId
|
|
182
|
+
},
|
|
183
|
+
run(ctx, args) {
|
|
184
|
+
const a = args;
|
|
185
|
+
const s = ctx.get(a.documentId);
|
|
186
|
+
const need = (v, name) => {
|
|
187
|
+
if (v === void 0) throw new Error(`${a.kind} requires "${name}"`);
|
|
188
|
+
return v;
|
|
189
|
+
};
|
|
190
|
+
let created;
|
|
191
|
+
switch (a.kind) {
|
|
192
|
+
case "move":
|
|
193
|
+
s.move(a.ids, need(a.delta, "delta"));
|
|
194
|
+
break;
|
|
195
|
+
case "rotate":
|
|
196
|
+
s.rotate(a.ids, need(a.angleDeg, "angleDeg") * DEG2RAD, a.center);
|
|
197
|
+
break;
|
|
198
|
+
case "scale":
|
|
199
|
+
s.scale(a.ids, need(a.factor, "factor"), a.center);
|
|
200
|
+
break;
|
|
201
|
+
case "mirror":
|
|
202
|
+
s.mirror(a.ids, need(a.axisA, "axisA"), need(a.axisB, "axisB"));
|
|
203
|
+
break;
|
|
204
|
+
case "copy":
|
|
205
|
+
created = s.copy(a.ids, need(a.delta, "delta"));
|
|
206
|
+
break;
|
|
207
|
+
case "array":
|
|
208
|
+
created = s.array(a.ids, need(a.count, "count"), need(a.delta, "delta"));
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
return { ...created ? { ids: created } : {}, revision: s.bump() };
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "linea_create_layout",
|
|
216
|
+
title: "Create paper layout",
|
|
217
|
+
description: 'Add a paper layout (default: A3 landscape, one viewport fitted to the model extents). scale accepts "1:50" strings.',
|
|
218
|
+
inputSchema: {
|
|
219
|
+
name: z.string(),
|
|
220
|
+
// Named size + optional custom mm — two typed params instead of a
|
|
221
|
+
// union so every property keeps a top-level JSON-schema `type`
|
|
222
|
+
// (see `point` for why bare anyOf properties are relay-hostile).
|
|
223
|
+
sheet: z.enum(["A4", "A3", "A2", "A1", "A0"]).optional(),
|
|
224
|
+
sheetSize: z.object({ width: z.number(), height: z.number() }).optional().describe("Custom sheet size in mm (overrides `sheet`)"),
|
|
225
|
+
orientation: z.enum(["portrait", "landscape"]).optional(),
|
|
226
|
+
scale: z.string().optional().describe('Viewport scale, e.g. "1:50"'),
|
|
227
|
+
documentId
|
|
228
|
+
},
|
|
229
|
+
run(ctx, args) {
|
|
230
|
+
const { name, sheet, sheetSize, orientation, scale, documentId: idArg } = args;
|
|
231
|
+
const s = ctx.get(idArg);
|
|
232
|
+
const layoutId = s.createLayout(name, {
|
|
233
|
+
sheet: sheetSize ?? sheet,
|
|
234
|
+
orientation,
|
|
235
|
+
scale
|
|
236
|
+
});
|
|
237
|
+
return { layoutId, revision: s.bump() };
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "linea_update_layout",
|
|
242
|
+
title: "Update layout",
|
|
243
|
+
description: `Patch a paper layout's name, sheet size (mm), or print palette (printPalette "monochrome" prints/renders everything black).`,
|
|
244
|
+
inputSchema: {
|
|
245
|
+
layout: z.string().describe("Layout name/id"),
|
|
246
|
+
name: z.string().optional().describe("New layout name"),
|
|
247
|
+
sheetSize: z.object({ width: z.number(), height: z.number() }).optional().describe("New sheet size in mm"),
|
|
248
|
+
printPalette: z.enum(["color", "grayscale", "monochrome"]).optional(),
|
|
249
|
+
documentId
|
|
250
|
+
},
|
|
251
|
+
run(ctx, args) {
|
|
252
|
+
const { layout, name, sheetSize, printPalette, documentId: idArg } = args;
|
|
253
|
+
const s = ctx.get(idArg);
|
|
254
|
+
s.updateLayout(layout, { name, sheet: sheetSize, printPalette });
|
|
255
|
+
return { revision: s.bump() };
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: "linea_remove_layout",
|
|
260
|
+
title: "Remove layout",
|
|
261
|
+
description: "Delete a paper layout (and its sheet objects) by name or id.",
|
|
262
|
+
inputSchema: {
|
|
263
|
+
layout: z.string().describe("Layout name/id"),
|
|
264
|
+
documentId
|
|
265
|
+
},
|
|
266
|
+
run(ctx, args) {
|
|
267
|
+
const { layout, documentId: idArg } = args;
|
|
268
|
+
const s = ctx.get(idArg);
|
|
269
|
+
s.removeLayout(layout);
|
|
270
|
+
return { revision: s.bump() };
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: "linea_set_dim_style",
|
|
275
|
+
title: "Set dimension style",
|
|
276
|
+
description: "Patch a document dimension style (default: the first): textHeight, textOffset, arrowType (filled|open|tick|dot|none), arrowSize, extensionType (long|short), extensionOffset, extensionOvershoot, precision, font, widthFactor. NOTE: annotation scale is NOT a style key \u2014 set the top-level `scale` field on each dimension object.",
|
|
277
|
+
inputSchema: {
|
|
278
|
+
patch: z.record(z.string(), z.unknown()),
|
|
279
|
+
styleId: z.string().optional(),
|
|
280
|
+
documentId
|
|
281
|
+
},
|
|
282
|
+
run(ctx, args) {
|
|
283
|
+
const { patch, styleId, documentId: idArg } = args;
|
|
284
|
+
const s = ctx.get(idArg);
|
|
285
|
+
s.setDimStyle(patch, styleId);
|
|
286
|
+
return { revision: s.bump() };
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "linea_set_text_style",
|
|
291
|
+
title: "Set text style",
|
|
292
|
+
description: "Patch a document text style (default: the first): font, widthFactor, textHeight (default height for new text), arrowType, arrowSize (leader arrows). Per-object styleOverride still wins over the style.",
|
|
293
|
+
inputSchema: {
|
|
294
|
+
patch: z.record(z.string(), z.unknown()),
|
|
295
|
+
styleId: z.string().optional(),
|
|
296
|
+
documentId
|
|
297
|
+
},
|
|
298
|
+
run(ctx, args) {
|
|
299
|
+
const { patch, styleId, documentId: idArg } = args;
|
|
300
|
+
const s = ctx.get(idArg);
|
|
301
|
+
s.setTextStyle(patch, styleId);
|
|
302
|
+
return { revision: s.bump() };
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: "linea_query",
|
|
307
|
+
title: "Query objects",
|
|
308
|
+
description: "Filtered read over model-space objects \u2014 or one paper layout's sheet objects (viewports, annotations) via `layout` \u2014 all filters AND: type, layer (name), color, lineType, content (text substring), bbox (intersects), inside (contained), near ({point, tol}), ids. `measure: true` adds bbox/length/area per object. Prefer this over linea_get_document \u2014 it returns compact data.",
|
|
309
|
+
inputSchema: {
|
|
310
|
+
ids: z.array(z.string()).optional(),
|
|
311
|
+
type: z.string().optional(),
|
|
312
|
+
layer: z.string().optional(),
|
|
313
|
+
color: z.string().optional(),
|
|
314
|
+
lineType: z.string().optional(),
|
|
315
|
+
content: z.string().optional(),
|
|
316
|
+
bbox: box.optional(),
|
|
317
|
+
inside: box.optional(),
|
|
318
|
+
near: z.object({ point, tol: z.number().positive() }).optional(),
|
|
319
|
+
layout: z.string().optional().describe("Search this layout's sheet objects (name/id) instead of model space"),
|
|
320
|
+
measure: z.boolean().optional(),
|
|
321
|
+
limit: z.number().int().positive().optional().describe("Max objects returned (default 100)"),
|
|
322
|
+
documentId
|
|
323
|
+
},
|
|
324
|
+
run(ctx, args) {
|
|
325
|
+
const { measure, limit, documentId: idArg, ...filter } = args;
|
|
326
|
+
const s = ctx.get(idArg);
|
|
327
|
+
const all = s.query(filter);
|
|
328
|
+
const cap = limit ?? 100;
|
|
329
|
+
const objects = all.slice(0, cap).map(
|
|
330
|
+
(o) => measure ? { ...o, measure: s.measure(String(o.id)) } : o
|
|
331
|
+
);
|
|
332
|
+
return { total: all.length, returned: objects.length, objects };
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: "linea_list_layers",
|
|
337
|
+
title: "List layers",
|
|
338
|
+
description: "All layers with full metadata (color, hidden, locked, \u2026) and which one is current (the default for new objects).",
|
|
339
|
+
inputSchema: { documentId },
|
|
340
|
+
run(ctx, args) {
|
|
341
|
+
const s = ctx.get(args.documentId);
|
|
342
|
+
return { layers: s.listLayers() };
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "linea_layer",
|
|
347
|
+
title: "Manage layers",
|
|
348
|
+
description: "add (name, props?) | update (layer, props \u2014 rename via props.name) | remove (layer; must be empty and not current) | set-current (layer). `layer` targets by name or id. Returns the resulting layer list.",
|
|
349
|
+
inputSchema: {
|
|
350
|
+
kind: z.enum(["add", "update", "remove", "set-current"]),
|
|
351
|
+
name: z.string().optional().describe("New layer name (kind: add)"),
|
|
352
|
+
layer: z.string().optional().describe("Target layer name/id"),
|
|
353
|
+
props: z.record(z.string(), z.unknown()).optional().describe("Layer fields, e.g. {color, hidden, locked, name}"),
|
|
354
|
+
documentId
|
|
355
|
+
},
|
|
356
|
+
run(ctx, args) {
|
|
357
|
+
const { kind, name, layer, props, documentId: idArg } = args;
|
|
358
|
+
const s = ctx.get(idArg);
|
|
359
|
+
const need = (v, what) => {
|
|
360
|
+
if (v === void 0) throw new Error(`${kind} requires "${what}"`);
|
|
361
|
+
return v;
|
|
362
|
+
};
|
|
363
|
+
let layerId;
|
|
364
|
+
if (kind === "add") layerId = s.addLayer({ ...props, name: need(name, "name") });
|
|
365
|
+
else if (kind === "update") s.updateLayer(need(layer, "layer"), props ?? {});
|
|
366
|
+
else if (kind === "remove") s.removeLayer(need(layer, "layer"));
|
|
367
|
+
else s.setCurrentLayer(need(layer, "layer"));
|
|
368
|
+
return {
|
|
369
|
+
...layerId ? { layerId } : {},
|
|
370
|
+
layers: s.listLayers(),
|
|
371
|
+
revision: s.bump()
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "linea_render",
|
|
377
|
+
title: "Render snapshot",
|
|
378
|
+
description: "Render model space (or a paper layout via `layout`) for visual verification. svg returns SVG markup as text; png returns an image (Node MCP server only \u2014 the in-browser editor supports svg).",
|
|
379
|
+
inputSchema: {
|
|
380
|
+
format: z.enum(["svg", "png"]).optional().describe("Default svg"),
|
|
381
|
+
layout: z.string().optional().describe("Layout name/id"),
|
|
382
|
+
bbox: box.optional(),
|
|
383
|
+
padding: z.number().optional().describe("Around the content bbox, mm"),
|
|
384
|
+
width: z.number().int().positive().optional().describe("png only, px"),
|
|
385
|
+
background: z.string().optional().describe("png only, e.g. #ffffff"),
|
|
386
|
+
palette: z.enum(["color", "grayscale", "monochrome"]).optional().describe(
|
|
387
|
+
"Print-style color remap \u2014 use monochrome with a white background (light layer colors are authored for the dark canvas)"
|
|
388
|
+
),
|
|
389
|
+
documentId
|
|
390
|
+
},
|
|
391
|
+
async run(ctx, args) {
|
|
392
|
+
const { format, documentId: idArg, ...opts } = args;
|
|
393
|
+
const s = ctx.get(idArg);
|
|
394
|
+
if (format === "png") {
|
|
395
|
+
if (!s.renderPng)
|
|
396
|
+
throw new Error(
|
|
397
|
+
'png rendering is only available on the Node MCP server \u2014 use format: "svg"'
|
|
398
|
+
);
|
|
399
|
+
const bytes = await s.renderPng(opts);
|
|
400
|
+
return {
|
|
401
|
+
content: [
|
|
402
|
+
{
|
|
403
|
+
type: "image",
|
|
404
|
+
data: bytesToBase64(bytes),
|
|
405
|
+
mimeType: "image/png"
|
|
406
|
+
}
|
|
407
|
+
]
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return { content: [{ type: "text", text: s.renderSvg(opts) }] };
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: "linea_undo",
|
|
415
|
+
title: "Undo",
|
|
416
|
+
description: "Revert the last change (agent or user edit alike).",
|
|
417
|
+
inputSchema: { documentId },
|
|
418
|
+
run(ctx, args) {
|
|
419
|
+
const s = ctx.get(args.documentId);
|
|
420
|
+
const undone = s.undo();
|
|
421
|
+
return { undone, revision: undone ? s.bump() : s.revision() };
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: "linea_redo",
|
|
426
|
+
title: "Redo",
|
|
427
|
+
description: "Re-apply the last undone change.",
|
|
428
|
+
inputSchema: { documentId },
|
|
429
|
+
run(ctx, args) {
|
|
430
|
+
const s = ctx.get(args.documentId);
|
|
431
|
+
const redone = s.redo();
|
|
432
|
+
return { redone, revision: redone ? s.bump() : s.revision() };
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: "linea_summary",
|
|
437
|
+
title: "Document summary",
|
|
438
|
+
description: "Counts by type/layer, bbox, layers, layouts \u2014 the cheap 'what is in this document' observation.",
|
|
439
|
+
inputSchema: { documentId },
|
|
440
|
+
run(ctx, args) {
|
|
441
|
+
const s = ctx.get(args.documentId);
|
|
442
|
+
return { documentId: s.id, revision: s.revision(), ...s.summary() };
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: "linea_get_document",
|
|
447
|
+
title: "Get document JSON",
|
|
448
|
+
description: "The full document (validated .linea JSON) + revision. The in-chat viewer calls this to refresh; agents usually want linea_summary instead.",
|
|
449
|
+
inputSchema: { documentId },
|
|
450
|
+
run(ctx, args) {
|
|
451
|
+
return docPayload(ctx.get(args.documentId));
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: "linea_replace_document",
|
|
456
|
+
title: "Replace document state",
|
|
457
|
+
description: "Replace the FULL document with the given .linea JSON (validated + migrated). Used by the in-chat editor to persist interactive edits; agents should prefer the granular add/update/transform tools.",
|
|
458
|
+
inputSchema: {
|
|
459
|
+
document: z.record(z.string(), z.unknown()),
|
|
460
|
+
documentId
|
|
461
|
+
},
|
|
462
|
+
run(ctx, args) {
|
|
463
|
+
const { document, documentId: idArg } = args;
|
|
464
|
+
const s = ctx.get(idArg);
|
|
465
|
+
return { documentId: s.id, revision: s.replace(document) };
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
];
|
|
469
|
+
|
|
470
|
+
// src/tools.ts
|
|
471
|
+
var EDITOR_RESOURCE_URI = "ui://linea/editor.html";
|
|
472
|
+
var docPayload2 = (registry2, id, doc) => ({
|
|
473
|
+
documentId: id,
|
|
474
|
+
revision: registry2.revision(id),
|
|
475
|
+
document: doc.document
|
|
476
|
+
});
|
|
477
|
+
var DrawingSession = class {
|
|
478
|
+
constructor(registry2, id, doc) {
|
|
479
|
+
this.registry = registry2;
|
|
480
|
+
this.id = id;
|
|
481
|
+
this.doc = doc;
|
|
482
|
+
}
|
|
483
|
+
registry;
|
|
484
|
+
id;
|
|
485
|
+
doc;
|
|
486
|
+
revision() {
|
|
487
|
+
return this.registry.revision(this.id);
|
|
488
|
+
}
|
|
489
|
+
bump() {
|
|
490
|
+
return this.registry.bump(this.id);
|
|
491
|
+
}
|
|
492
|
+
add(objects, opts) {
|
|
493
|
+
return this.doc.add(objects, {
|
|
494
|
+
layout: opts?.layout
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
update(entries) {
|
|
498
|
+
this.doc.update(entries);
|
|
499
|
+
}
|
|
500
|
+
remove(ids) {
|
|
501
|
+
const removed = ids.filter((id) => this.doc.getObject(id) !== void 0);
|
|
502
|
+
this.doc.remove(ids);
|
|
503
|
+
return removed.length;
|
|
504
|
+
}
|
|
505
|
+
query(filter) {
|
|
506
|
+
const { ids, ...rest } = filter;
|
|
507
|
+
let objects = this.doc.query(rest);
|
|
508
|
+
if (ids) {
|
|
509
|
+
const wanted = new Set(ids);
|
|
510
|
+
objects = objects.filter((o) => wanted.has(o.id));
|
|
511
|
+
}
|
|
512
|
+
return objects;
|
|
513
|
+
}
|
|
514
|
+
measure(id) {
|
|
515
|
+
return {
|
|
516
|
+
bbox: this.doc.bboxOf([id]),
|
|
517
|
+
length: this.doc.length(id),
|
|
518
|
+
area: this.doc.area(id)
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
listLayers() {
|
|
522
|
+
const current = this.doc.document.currentLayerId;
|
|
523
|
+
return this.doc.layers.map((l) => ({ ...l, current: l.id === current }));
|
|
524
|
+
}
|
|
525
|
+
addLayer(props) {
|
|
526
|
+
return this.doc.addLayer(props);
|
|
527
|
+
}
|
|
528
|
+
updateLayer(nameOrId, patch) {
|
|
529
|
+
this.doc.updateLayer(nameOrId, patch);
|
|
530
|
+
}
|
|
531
|
+
removeLayer(nameOrId) {
|
|
532
|
+
this.doc.removeLayer(nameOrId);
|
|
533
|
+
}
|
|
534
|
+
setCurrentLayer(nameOrId) {
|
|
535
|
+
this.doc.setCurrentLayer(nameOrId);
|
|
536
|
+
}
|
|
537
|
+
renderSvg(opts) {
|
|
538
|
+
return this.doc.toSvg(opts);
|
|
539
|
+
}
|
|
540
|
+
renderPng(opts) {
|
|
541
|
+
return this.doc.toPng(opts);
|
|
542
|
+
}
|
|
543
|
+
undo() {
|
|
544
|
+
return this.doc.undo();
|
|
545
|
+
}
|
|
546
|
+
redo() {
|
|
547
|
+
return this.doc.redo();
|
|
548
|
+
}
|
|
549
|
+
move(ids, delta) {
|
|
550
|
+
this.doc.move(ids, delta);
|
|
551
|
+
}
|
|
552
|
+
rotate(ids, angleRad, center) {
|
|
553
|
+
this.doc.rotate(ids, angleRad, center);
|
|
554
|
+
}
|
|
555
|
+
scale(ids, factor, center) {
|
|
556
|
+
this.doc.scale(ids, factor, center);
|
|
557
|
+
}
|
|
558
|
+
mirror(ids, axisA, axisB) {
|
|
559
|
+
this.doc.mirror(ids, axisA, axisB);
|
|
560
|
+
}
|
|
561
|
+
copy(ids, delta) {
|
|
562
|
+
return this.doc.copy(ids, delta);
|
|
563
|
+
}
|
|
564
|
+
array(ids, count, delta) {
|
|
565
|
+
return this.doc.array(ids, count, delta);
|
|
566
|
+
}
|
|
567
|
+
createLayout(name, opts) {
|
|
568
|
+
return this.doc.createLayout(name, {
|
|
569
|
+
sheet: opts.sheet,
|
|
570
|
+
orientation: opts.orientation,
|
|
571
|
+
viewport: opts.scale !== void 0 ? { scale: opts.scale } : void 0
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
updateLayout(nameOrId, patch) {
|
|
575
|
+
this.doc.updateLayoutMeta(nameOrId, patch);
|
|
576
|
+
}
|
|
577
|
+
removeLayout(nameOrId) {
|
|
578
|
+
this.doc.removeLayout(nameOrId);
|
|
579
|
+
}
|
|
580
|
+
setDimStyle(patch, styleId) {
|
|
581
|
+
this.doc.setDimStyle(patch, styleId);
|
|
582
|
+
}
|
|
583
|
+
setTextStyle(patch, styleId) {
|
|
584
|
+
this.doc.setTextStyle(patch, styleId);
|
|
585
|
+
}
|
|
586
|
+
summary() {
|
|
587
|
+
return this.doc.summary();
|
|
588
|
+
}
|
|
589
|
+
getDocument() {
|
|
590
|
+
return this.doc.document;
|
|
591
|
+
}
|
|
592
|
+
replace(raw) {
|
|
593
|
+
return this.registry.replace(this.id, raw).revision;
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
function registerLineaTools(server, registry2, opts = {}) {
|
|
597
|
+
const ctx = {
|
|
598
|
+
get(idArg) {
|
|
599
|
+
const { id, doc } = registry2.get(idArg);
|
|
600
|
+
return new DrawingSession(registry2, id, doc);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
for (const def of coreLineaTools) {
|
|
604
|
+
server.registerTool(
|
|
605
|
+
def.name,
|
|
606
|
+
{
|
|
607
|
+
title: def.title,
|
|
608
|
+
description: def.description,
|
|
609
|
+
inputSchema: def.inputSchema
|
|
610
|
+
},
|
|
611
|
+
async (args) => await runTool(def, ctx, args)
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
const run = (fn) => {
|
|
615
|
+
return async (args) => {
|
|
616
|
+
try {
|
|
617
|
+
return toolResult(await fn(args));
|
|
618
|
+
} catch (e) {
|
|
619
|
+
return toolError(e);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
};
|
|
623
|
+
server.registerTool(
|
|
624
|
+
"linea_new_document",
|
|
625
|
+
{
|
|
626
|
+
title: "New Linea document",
|
|
627
|
+
description: "Create a fresh CAD document (mm, Y-up) and make it active. Call linea_show_document afterwards to display it in chat.",
|
|
628
|
+
inputSchema: { name: z2.string().optional() }
|
|
629
|
+
},
|
|
630
|
+
run(({ name }) => {
|
|
631
|
+
const { id, doc } = registry2.create(name);
|
|
632
|
+
return { documentId: id, summary: doc.summary() };
|
|
633
|
+
})
|
|
634
|
+
);
|
|
635
|
+
server.registerTool(
|
|
636
|
+
"linea_open_document",
|
|
637
|
+
{
|
|
638
|
+
title: "Open .linea file",
|
|
639
|
+
description: "Open a .linea file (or legacy .lineadraw) from disk and make it active.",
|
|
640
|
+
inputSchema: { path: z2.string() }
|
|
641
|
+
},
|
|
642
|
+
run(async ({ path: p }) => {
|
|
643
|
+
const { id, doc } = await registry2.open(p);
|
|
644
|
+
return { documentId: id, migration: doc.migration, summary: doc.summary() };
|
|
645
|
+
})
|
|
646
|
+
);
|
|
647
|
+
server.registerTool(
|
|
648
|
+
"linea_save_document",
|
|
649
|
+
{
|
|
650
|
+
title: "Save .linea file",
|
|
651
|
+
description: "Save the document to disk as .linea JSON.",
|
|
652
|
+
inputSchema: { path: z2.string(), documentId }
|
|
653
|
+
},
|
|
654
|
+
run(async ({ path: p, documentId: idArg }) => {
|
|
655
|
+
const { doc } = registry2.get(idArg);
|
|
656
|
+
await doc.save(p);
|
|
657
|
+
return { saved: p };
|
|
658
|
+
})
|
|
659
|
+
);
|
|
660
|
+
server.registerTool(
|
|
661
|
+
"linea_export",
|
|
662
|
+
{
|
|
663
|
+
title: "Export document",
|
|
664
|
+
description: "Write the document to disk as svg | png | pdf | dxf | dwg.",
|
|
665
|
+
inputSchema: {
|
|
666
|
+
format: z2.enum(["svg", "png", "pdf", "dxf", "dwg"]),
|
|
667
|
+
path: z2.string(),
|
|
668
|
+
layout: z2.string().optional().describe("Render a layout instead of model space (svg/png)"),
|
|
669
|
+
documentId
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
run(async ({ format, path: p, layout, documentId: idArg }) => {
|
|
673
|
+
const { doc } = registry2.get(idArg);
|
|
674
|
+
if (format === "svg") await doc.exportSvg(p, { layout });
|
|
675
|
+
else if (format === "png") await doc.exportPng(p, { layout });
|
|
676
|
+
else if (format === "pdf") await doc.exportPdf(p);
|
|
677
|
+
else if (format === "dwg") await doc.exportDwg(p);
|
|
678
|
+
else await doc.exportDxf(p);
|
|
679
|
+
return { exported: p };
|
|
680
|
+
})
|
|
681
|
+
);
|
|
682
|
+
registerAppTool(
|
|
683
|
+
server,
|
|
684
|
+
"linea_show_document",
|
|
685
|
+
{
|
|
686
|
+
title: "Show Linea document",
|
|
687
|
+
description: "Open the document in the FULL interactive Linea editor INSIDE the chat: the user can view, pan/zoom, draw, snap, dimension, and edit; their changes flow back into the document. Call this after creating or meaningfully changing a document, or whenever the user wants to see or edit it.",
|
|
688
|
+
inputSchema: { documentId },
|
|
689
|
+
_meta: { ui: { resourceUri: EDITOR_RESOURCE_URI } }
|
|
690
|
+
},
|
|
691
|
+
(async ({ documentId: idArg }) => {
|
|
692
|
+
try {
|
|
693
|
+
const { id, doc } = registry2.get(idArg);
|
|
694
|
+
return {
|
|
695
|
+
content: [
|
|
696
|
+
{
|
|
697
|
+
type: "text",
|
|
698
|
+
text: `Showing "${doc.document.name}" in the in-chat editor (${doc.objects.length} model objects)`
|
|
699
|
+
}
|
|
700
|
+
],
|
|
701
|
+
structuredContent: docPayload2(registry2, id, doc)
|
|
702
|
+
};
|
|
703
|
+
} catch (e) {
|
|
704
|
+
return toolError(e);
|
|
705
|
+
}
|
|
706
|
+
})
|
|
707
|
+
);
|
|
708
|
+
registerAppResource(
|
|
709
|
+
server,
|
|
710
|
+
"Linea editor",
|
|
711
|
+
EDITOR_RESOURCE_URI,
|
|
712
|
+
{ mimeType: RESOURCE_MIME_TYPE },
|
|
713
|
+
async () => ({
|
|
714
|
+
contents: [
|
|
715
|
+
{
|
|
716
|
+
uri: EDITOR_RESOURCE_URI,
|
|
717
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
718
|
+
text: await (opts.editorHtml ?? (async () => {
|
|
719
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
720
|
+
return readFile(path.join(here, "editor.html"), "utf-8").catch(
|
|
721
|
+
() => readFile(path.join(here, "..", "dist", "editor.html"), "utf-8")
|
|
722
|
+
);
|
|
723
|
+
}))()
|
|
724
|
+
}
|
|
725
|
+
]
|
|
726
|
+
})
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/server.ts
|
|
731
|
+
var PORT = Number(process.env.PORT ?? 3001);
|
|
732
|
+
var registry = new DocumentRegistry();
|
|
733
|
+
var buildServer = () => {
|
|
734
|
+
const server = new McpServer({ name: "Lineadraw CAD", version: "0.1.0" });
|
|
735
|
+
registerLineaTools(server, registry);
|
|
736
|
+
return server;
|
|
737
|
+
};
|
|
738
|
+
function startHttpServer() {
|
|
739
|
+
const app = express();
|
|
740
|
+
app.use(cors());
|
|
741
|
+
app.use(express.json({ limit: "16mb" }));
|
|
742
|
+
app.use((req, _res, next) => {
|
|
743
|
+
console.log(`${(/* @__PURE__ */ new Date()).toISOString()} ${req.method} ${req.path}`);
|
|
744
|
+
next();
|
|
745
|
+
});
|
|
746
|
+
app.all(["/", "/mcp"], async (req, res) => {
|
|
747
|
+
const transport = new StreamableHTTPServerTransport({
|
|
748
|
+
sessionIdGenerator: void 0,
|
|
749
|
+
enableJsonResponse: true
|
|
750
|
+
});
|
|
751
|
+
res.on("close", () => transport.close());
|
|
752
|
+
await buildServer().connect(transport);
|
|
753
|
+
await transport.handleRequest(req, res, req.body);
|
|
754
|
+
});
|
|
755
|
+
const { TLS_CERT, TLS_KEY } = process.env;
|
|
756
|
+
if (TLS_CERT && TLS_KEY) {
|
|
757
|
+
https.createServer(
|
|
758
|
+
{ cert: readFileSync(TLS_CERT), key: readFileSync(TLS_KEY) },
|
|
759
|
+
app
|
|
760
|
+
).listen(PORT, () => {
|
|
761
|
+
console.log(`Linea MCP server on https://localhost:${PORT}/mcp`);
|
|
762
|
+
});
|
|
763
|
+
} else {
|
|
764
|
+
app.listen(PORT, () => {
|
|
765
|
+
console.log(
|
|
766
|
+
`Linea MCP server on http://localhost:${PORT}/mcp (set TLS_CERT/TLS_KEY for https)`
|
|
767
|
+
);
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (process.argv.includes("--stdio")) {
|
|
772
|
+
await buildServer().connect(new StdioServerTransport());
|
|
773
|
+
console.error("Linea MCP server on stdio");
|
|
774
|
+
} else {
|
|
775
|
+
startHttpServer();
|
|
776
|
+
}
|
|
777
|
+
//# sourceMappingURL=server.js.map
|