@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/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 doc = this.docs.get(key);
39
- if (!doc) throw new Error(`Unknown document "${key}"`);
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
- if (!this.docs.has(id)) throw new Error(`Unknown document "${id}"`);
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
- this.docs.set(id, doc);
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 next = (this.revisions.get(id) ?? 0) + 1;
59
- this.revisions.set(id, next);
60
- return next;
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.revisions.get(id) ?? 0;
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
- data: bytesToBase64(bytes),
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
- return docPayload(ctx.get(args.documentId));
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 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);
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 = this.doc.query(rest);
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(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
- };
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 new DrawingSession(registry2, id, doc);
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: docPayload2(registry2, id, doc)
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 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) => {
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, 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
- );
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());