@lineadraw/mcp 0.3.0 → 0.5.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 +8 -3
- package/dist/editor.html +226 -214
- package/dist/server.js +107 -174
- package/dist/server.js.map +1 -1
- package/dist/{widget-LQz2fm1O.html → widget-DhBjwIr2.html} +10 -10
- package/package.json +1 -5
package/dist/server.js
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/server.ts
|
|
4
4
|
import { readFileSync } from "fs";
|
|
5
|
+
import http from "http";
|
|
5
6
|
import https from "https";
|
|
6
|
-
import cors from "cors";
|
|
7
|
-
import express from "express";
|
|
8
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
9
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -13,21 +12,18 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
13
12
|
import { newDrawing, openDrawing, drawingFromJson } from "@lineadraw/sdk";
|
|
14
13
|
var DocumentRegistry = class {
|
|
15
14
|
docs = /* @__PURE__ */ new Map();
|
|
16
|
-
revisions = /* @__PURE__ */ new Map();
|
|
17
15
|
activeId = null;
|
|
18
16
|
create(name) {
|
|
19
17
|
const doc = newDrawing(name);
|
|
20
18
|
const id = doc.document.id;
|
|
21
|
-
this.docs.set(id, doc);
|
|
22
|
-
this.revisions.set(id, 0);
|
|
19
|
+
this.docs.set(id, { doc, revision: 0 });
|
|
23
20
|
this.activeId = id;
|
|
24
21
|
return { id, doc };
|
|
25
22
|
}
|
|
26
23
|
async open(path2) {
|
|
27
24
|
const doc = await openDrawing(path2);
|
|
28
25
|
const id = doc.document.id;
|
|
29
|
-
this.docs.set(id, doc);
|
|
30
|
-
this.revisions.set(id, 0);
|
|
26
|
+
this.docs.set(id, { doc, revision: 0 });
|
|
31
27
|
this.activeId = id;
|
|
32
28
|
return { id, doc };
|
|
33
29
|
}
|
|
@@ -35,32 +31,33 @@ var DocumentRegistry = class {
|
|
|
35
31
|
get(id) {
|
|
36
32
|
const key = id ?? this.activeId;
|
|
37
33
|
if (!key) throw new Error("No document \u2014 call linea_new_document first");
|
|
38
|
-
const
|
|
39
|
-
if (!
|
|
40
|
-
return { id: key, doc };
|
|
34
|
+
const entry = this.docs.get(key);
|
|
35
|
+
if (!entry) throw new Error(`Unknown document "${key}"`);
|
|
36
|
+
return { id: key, doc: entry.doc };
|
|
41
37
|
}
|
|
42
38
|
/**
|
|
43
39
|
* Replace a document's full state (validated + migrated) — how in-chat
|
|
44
40
|
* editor edits land. The id must match; conversation undo history resets.
|
|
45
41
|
*/
|
|
46
42
|
replace(id, raw) {
|
|
47
|
-
|
|
43
|
+
const entry = this.docs.get(id);
|
|
44
|
+
if (!entry) throw new Error(`Unknown document "${id}"`);
|
|
48
45
|
const doc = drawingFromJson(JSON.stringify(raw));
|
|
49
46
|
if (doc.document.id !== id)
|
|
50
47
|
throw new Error(
|
|
51
48
|
`Document id mismatch: payload has "${doc.document.id}", expected "${id}"`
|
|
52
49
|
);
|
|
53
|
-
|
|
50
|
+
entry.doc = doc;
|
|
54
51
|
return { doc, revision: this.bump(id) };
|
|
55
52
|
}
|
|
56
53
|
/** Mark a document changed; the viewer compares revisions to refresh. */
|
|
57
54
|
bump(id) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
return
|
|
55
|
+
const entry = this.docs.get(id);
|
|
56
|
+
if (!entry) return 0;
|
|
57
|
+
return ++entry.revision;
|
|
61
58
|
}
|
|
62
59
|
revision(id) {
|
|
63
|
-
return this.
|
|
60
|
+
return this.docs.get(id)?.revision ?? 0;
|
|
64
61
|
}
|
|
65
62
|
};
|
|
66
63
|
|
|
@@ -94,12 +91,6 @@ var toolResult = (data) => ({
|
|
|
94
91
|
structuredContent: data
|
|
95
92
|
});
|
|
96
93
|
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
94
|
var toolError = (e) => ({
|
|
104
95
|
content: [
|
|
105
96
|
{ type: "text", text: String(e instanceof Error ? e.message : e) }
|
|
@@ -115,11 +106,6 @@ var runTool = async (def, ctx, args) => {
|
|
|
115
106
|
return toolError(e);
|
|
116
107
|
}
|
|
117
108
|
};
|
|
118
|
-
var docPayload = (s) => ({
|
|
119
|
-
documentId: s.id,
|
|
120
|
-
revision: s.revision(),
|
|
121
|
-
document: s.getDocument()
|
|
122
|
-
});
|
|
123
109
|
var coreLineaTools = [
|
|
124
110
|
{
|
|
125
111
|
name: "linea_add_objects",
|
|
@@ -273,7 +259,7 @@ var coreLineaTools = [
|
|
|
273
259
|
{
|
|
274
260
|
name: "linea_set_dim_style",
|
|
275
261
|
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.",
|
|
262
|
+
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 (arial|osifont|jetbrains-mono|source-serif), widthFactor. NOTE: annotation scale is NOT a style key \u2014 set the top-level `scale` field on each dimension object.",
|
|
277
263
|
inputSchema: {
|
|
278
264
|
patch: z.record(z.string(), z.unknown()),
|
|
279
265
|
styleId: z.string().optional(),
|
|
@@ -289,7 +275,7 @@ var coreLineaTools = [
|
|
|
289
275
|
{
|
|
290
276
|
name: "linea_set_text_style",
|
|
291
277
|
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.",
|
|
278
|
+
description: "Patch a document text style (default: the first): font (arial|osifont|jetbrains-mono|source-serif), widthFactor, textHeight (default height for new text), arrowType, arrowSize (leader arrows). Per-object styleOverride still wins over the style.",
|
|
293
279
|
inputSchema: {
|
|
294
280
|
patch: z.record(z.string(), z.unknown()),
|
|
295
281
|
styleId: z.string().optional(),
|
|
@@ -401,7 +387,8 @@ var coreLineaTools = [
|
|
|
401
387
|
content: [
|
|
402
388
|
{
|
|
403
389
|
type: "image",
|
|
404
|
-
|
|
390
|
+
// renderPng exists on the Node adapter only, so Buffer is here.
|
|
391
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
405
392
|
mimeType: "image/png"
|
|
406
393
|
}
|
|
407
394
|
]
|
|
@@ -448,7 +435,8 @@ var coreLineaTools = [
|
|
|
448
435
|
description: "The full document (validated .linea JSON) + revision. The in-chat viewer calls this to refresh; agents usually want linea_summary instead.",
|
|
449
436
|
inputSchema: { documentId },
|
|
450
437
|
run(ctx, args) {
|
|
451
|
-
|
|
438
|
+
const s = ctx.get(args.documentId);
|
|
439
|
+
return { documentId: s.id, revision: s.revision(), document: s.getDocument() };
|
|
452
440
|
}
|
|
453
441
|
},
|
|
454
442
|
{
|
|
@@ -469,135 +457,67 @@ var coreLineaTools = [
|
|
|
469
457
|
|
|
470
458
|
// src/tools.ts
|
|
471
459
|
var EDITOR_RESOURCE_URI = "ui://linea/editor.html";
|
|
472
|
-
var
|
|
473
|
-
|
|
474
|
-
revision: registry2.revision(id),
|
|
475
|
-
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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);
|
|
460
|
+
var drawingSession = (registry2, id, doc) => ({
|
|
461
|
+
id,
|
|
462
|
+
revision: () => registry2.revision(id),
|
|
463
|
+
bump: () => registry2.bump(id),
|
|
464
|
+
add: (objects, opts) => doc.add(objects, { layout: opts?.layout }),
|
|
465
|
+
update: (entries) => doc.update(entries),
|
|
466
|
+
remove: (ids) => {
|
|
467
|
+
const removed = ids.filter((oid) => doc.getObject(oid) !== void 0);
|
|
468
|
+
doc.remove(ids);
|
|
503
469
|
return removed.length;
|
|
504
|
-
}
|
|
505
|
-
query(filter) {
|
|
470
|
+
},
|
|
471
|
+
query: (filter) => {
|
|
506
472
|
const { ids, ...rest } = filter;
|
|
507
|
-
let objects =
|
|
473
|
+
let objects = doc.query(rest);
|
|
508
474
|
if (ids) {
|
|
509
475
|
const wanted = new Set(ids);
|
|
510
476
|
objects = objects.filter((o) => wanted.has(o.id));
|
|
511
477
|
}
|
|
512
478
|
return objects;
|
|
513
|
-
}
|
|
514
|
-
measure(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
};
|
|
479
|
+
},
|
|
480
|
+
measure: (oid) => ({
|
|
481
|
+
bbox: doc.bboxOf([oid]),
|
|
482
|
+
length: doc.length(oid),
|
|
483
|
+
area: doc.area(oid)
|
|
484
|
+
}),
|
|
485
|
+
listLayers: () => {
|
|
486
|
+
const current = doc.document.currentLayerId;
|
|
487
|
+
return doc.layers.map((l) => ({ ...l, current: l.id === current }));
|
|
488
|
+
},
|
|
489
|
+
addLayer: (props) => doc.addLayer(props),
|
|
490
|
+
updateLayer: doc.updateLayer.bind(doc),
|
|
491
|
+
removeLayer: doc.removeLayer.bind(doc),
|
|
492
|
+
setCurrentLayer: doc.setCurrentLayer.bind(doc),
|
|
493
|
+
renderSvg: doc.toSvg.bind(doc),
|
|
494
|
+
renderPng: doc.toPng.bind(doc),
|
|
495
|
+
undo: doc.undo.bind(doc),
|
|
496
|
+
redo: doc.redo.bind(doc),
|
|
497
|
+
move: doc.move.bind(doc),
|
|
498
|
+
rotate: doc.rotate.bind(doc),
|
|
499
|
+
scale: doc.scale.bind(doc),
|
|
500
|
+
mirror: doc.mirror.bind(doc),
|
|
501
|
+
copy: doc.copy.bind(doc),
|
|
502
|
+
array: doc.array.bind(doc),
|
|
503
|
+
createLayout: (name, opts) => doc.createLayout(name, {
|
|
504
|
+
sheet: opts.sheet,
|
|
505
|
+
orientation: opts.orientation,
|
|
506
|
+
viewport: opts.scale !== void 0 ? { scale: opts.scale } : void 0
|
|
507
|
+
}),
|
|
508
|
+
updateLayout: doc.updateLayoutMeta.bind(doc),
|
|
509
|
+
removeLayout: doc.removeLayout.bind(doc),
|
|
510
|
+
setDimStyle: (patch, styleId) => doc.setDimStyle(patch, styleId),
|
|
511
|
+
setTextStyle: (patch, styleId) => doc.setTextStyle(patch, styleId),
|
|
512
|
+
summary: () => doc.summary(),
|
|
513
|
+
getDocument: () => doc.document,
|
|
514
|
+
replace: (raw) => registry2.replace(id, raw).revision
|
|
515
|
+
});
|
|
596
516
|
function registerLineaTools(server, registry2, opts = {}) {
|
|
597
517
|
const ctx = {
|
|
598
518
|
get(idArg) {
|
|
599
519
|
const { id, doc } = registry2.get(idArg);
|
|
600
|
-
return
|
|
520
|
+
return drawingSession(registry2, id, doc);
|
|
601
521
|
}
|
|
602
522
|
};
|
|
603
523
|
for (const def of coreLineaTools) {
|
|
@@ -698,7 +618,11 @@ function registerLineaTools(server, registry2, opts = {}) {
|
|
|
698
618
|
text: `Showing "${doc.document.name}" in the in-chat editor (${doc.objects.length} model objects)`
|
|
699
619
|
}
|
|
700
620
|
],
|
|
701
|
-
structuredContent:
|
|
621
|
+
structuredContent: {
|
|
622
|
+
documentId: id,
|
|
623
|
+
revision: registry2.revision(id),
|
|
624
|
+
document: doc.document
|
|
625
|
+
}
|
|
702
626
|
};
|
|
703
627
|
} catch (e) {
|
|
704
628
|
return toolError(e);
|
|
@@ -736,37 +660,46 @@ var buildServer = () => {
|
|
|
736
660
|
return server;
|
|
737
661
|
};
|
|
738
662
|
function startHttpServer() {
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
663
|
+
const handle = async (req, res) => {
|
|
664
|
+
const path2 = (req.url ?? "/").split("?")[0];
|
|
665
|
+
console.log(`${(/* @__PURE__ */ new Date()).toISOString()} ${req.method} ${path2}`);
|
|
666
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
667
|
+
res.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,OPTIONS");
|
|
668
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
669
|
+
if (req.method === "OPTIONS") {
|
|
670
|
+
res.writeHead(204).end();
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (path2 !== "/" && path2 !== "/mcp") {
|
|
674
|
+
res.writeHead(404).end();
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
747
677
|
const transport = new StreamableHTTPServerTransport({
|
|
748
678
|
sessionIdGenerator: void 0,
|
|
749
679
|
enableJsonResponse: true
|
|
750
680
|
});
|
|
751
681
|
res.on("close", () => transport.close());
|
|
752
682
|
await buildServer().connect(transport);
|
|
753
|
-
await transport.handleRequest(req, res
|
|
754
|
-
}
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
);
|
|
683
|
+
await transport.handleRequest(req, res);
|
|
684
|
+
};
|
|
685
|
+
const listener = (req, res) => {
|
|
686
|
+
void handle(req, res).catch((e) => {
|
|
687
|
+
console.error(e);
|
|
688
|
+
if (!res.headersSent) res.writeHead(500);
|
|
689
|
+
res.end();
|
|
768
690
|
});
|
|
769
|
-
}
|
|
691
|
+
};
|
|
692
|
+
const { TLS_CERT, TLS_KEY } = process.env;
|
|
693
|
+
const tls = Boolean(TLS_CERT && TLS_KEY);
|
|
694
|
+
const server = tls ? https.createServer(
|
|
695
|
+
{ cert: readFileSync(TLS_CERT), key: readFileSync(TLS_KEY) },
|
|
696
|
+
listener
|
|
697
|
+
) : http.createServer(listener);
|
|
698
|
+
server.listen(PORT, () => {
|
|
699
|
+
console.log(
|
|
700
|
+
tls ? `Linea MCP server on https://localhost:${PORT}/mcp` : `Linea MCP server on http://localhost:${PORT}/mcp (set TLS_CERT/TLS_KEY for https)`
|
|
701
|
+
);
|
|
702
|
+
});
|
|
770
703
|
}
|
|
771
704
|
if (process.argv.includes("--stdio")) {
|
|
772
705
|
await buildServer().connect(new StdioServerTransport());
|