@composer-app/mcp 0.0.1-beta.1 → 0.0.1-beta.2

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.
@@ -17,8 +17,112 @@ import * as Y3 from "yjs";
17
17
  import YProvider from "y-partyserver/provider";
18
18
  import WebSocket from "ws";
19
19
 
20
+ // ../shared/src/editor-extensions.ts
21
+ import StarterKit from "@tiptap/starter-kit";
22
+ import { Code } from "@tiptap/extension-code";
23
+ import CodeBlock from "@tiptap/extension-code-block";
24
+ import Image from "@tiptap/extension-image";
25
+ import TaskList from "@tiptap/extension-task-list";
26
+ import TaskItem from "@tiptap/extension-task-item";
27
+ import Highlight from "@tiptap/extension-highlight";
28
+ import Subscript from "@tiptap/extension-subscript";
29
+ import Superscript from "@tiptap/extension-superscript";
30
+ import { Table } from "@tiptap/extension-table";
31
+ import { TableRow } from "@tiptap/extension-table-row";
32
+ import { TableCell } from "@tiptap/extension-table-cell";
33
+ import { TableHeader } from "@tiptap/extension-table-header";
34
+ var CodeWithCombinableMarks = Code.extend({ excludes: "" });
35
+ var FrontmatterSchema = CodeBlock.extend({
36
+ name: "frontmatter",
37
+ addInputRules() {
38
+ return [];
39
+ },
40
+ renderMarkdown: (node) => {
41
+ const text = (node.content ?? []).map((child) => child.text ?? "").join("").trim();
42
+ if (!text) return "";
43
+ return `---
44
+ ${text}
45
+ ---`;
46
+ }
47
+ });
48
+ function buildEditorExtensions(opts = {}) {
49
+ const table = opts.table ?? Table;
50
+ const frontmatter = opts.frontmatter ?? FrontmatterSchema;
51
+ return [
52
+ StarterKit.configure({
53
+ undoRedo: false,
54
+ code: false,
55
+ link: {
56
+ openOnClick: true,
57
+ HTMLAttributes: { target: "_blank", rel: "noopener noreferrer" },
58
+ shouldAutoLink: (url) => /^https?:\/\/\S+$/i.test(url)
59
+ }
60
+ }),
61
+ CodeWithCombinableMarks,
62
+ Image,
63
+ TaskList,
64
+ TaskItem.configure({ nested: true }),
65
+ Highlight,
66
+ Subscript,
67
+ Superscript,
68
+ table.configure({ resizable: false }),
69
+ TableRow,
70
+ TableCell,
71
+ TableHeader,
72
+ frontmatter
73
+ ];
74
+ }
75
+ var editorExtensions = buildEditorExtensions();
76
+
77
+ // ../shared/src/frontmatter.ts
78
+ function extractFrontmatter(text) {
79
+ const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
80
+ if (!match) return null;
81
+ return { yaml: match[1].trim(), body: match[2] };
82
+ }
83
+
84
+ // ../shared/src/markdown.ts
85
+ import { getSchema } from "@tiptap/core";
86
+ import { MarkdownManager } from "@tiptap/markdown";
87
+ import {
88
+ prosemirrorJSONToYXmlFragment,
89
+ yXmlFragmentToProsemirrorJSON
90
+ } from "@tiptap/y-tiptap";
91
+ function markdownToDocJSON(markdown, extensions = editorExtensions) {
92
+ const manager = new MarkdownManager({ extensions });
93
+ const parsed = manager.parse(markdown);
94
+ if (parsed && parsed.type === "doc") return parsed;
95
+ return { type: "doc", content: parsed?.content ?? [] };
96
+ }
97
+ function writeMarkdownToYFragment(fragment, markdown, extensions = editorExtensions) {
98
+ const doc = fragment.doc;
99
+ if (!doc) throw new Error("fragment must be attached to a Y.Doc");
100
+ const fm = extractFrontmatter(markdown);
101
+ const body = fm ? fm.body : markdown;
102
+ const schema = getSchema(extensions);
103
+ const bodyDoc = markdownToDocJSON(body, extensions);
104
+ const children = [];
105
+ if (fm) {
106
+ children.push({
107
+ type: "frontmatter",
108
+ attrs: { language: "yaml" },
109
+ content: fm.yaml ? [{ type: "text", text: fm.yaml }] : void 0
110
+ });
111
+ }
112
+ if (bodyDoc.content) children.push(...bodyDoc.content);
113
+ const fullDoc = { type: "doc", content: children };
114
+ doc.transact(() => {
115
+ prosemirrorJSONToYXmlFragment(schema, fullDoc, fragment);
116
+ });
117
+ }
118
+ function markdownFromYFragment(fragment, extensions = editorExtensions) {
119
+ const json = yXmlFragmentToProsemirrorJSON(fragment);
120
+ if (!json || !json.content || json.content.length === 0) return "";
121
+ const manager = new MarkdownManager({ extensions });
122
+ return manager.serialize(json);
123
+ }
124
+
20
125
  // src/docReaders.ts
21
- import { markdownFromYFragment } from "@composer-app/shared";
22
126
  import * as Y from "yjs";
23
127
  function isXmlElement(node) {
24
128
  return node instanceof Y.XmlElement;
@@ -291,64 +395,6 @@ function resolveServerAnchor(doc, spec) {
291
395
  };
292
396
  }
293
397
 
294
- // src/logger.ts
295
- import * as fs from "fs";
296
- import * as path from "path";
297
- import * as os from "os";
298
- var COMPOSER_DIR = process.env.COMPOSER_CONFIG_DIR ?? path.join(os.homedir(), ".composer");
299
- var LOG_FILE = process.env.COMPOSER_LOG_FILE ?? path.join(COMPOSER_DIR, "mcp.log");
300
- var ensured = false;
301
- function ensureDir() {
302
- if (ensured) return;
303
- try {
304
- fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
305
- ensured = true;
306
- } catch {
307
- }
308
- }
309
- function write(line) {
310
- ensureDir();
311
- try {
312
- fs.appendFileSync(LOG_FILE, line);
313
- } catch {
314
- }
315
- try {
316
- process.stderr.write(line);
317
- } catch {
318
- }
319
- }
320
- function log(message, meta) {
321
- const ts = (/* @__PURE__ */ new Date()).toISOString();
322
- const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
323
- write(`${ts} [composer-mcp] ${message}${metaStr}
324
- `);
325
- }
326
- function logError(message, err) {
327
- const ts = (/* @__PURE__ */ new Date()).toISOString();
328
- const detail = err instanceof Error ? `${err.name}: ${err.message}
329
- ${err.stack ?? ""}` : typeof err === "string" ? err : JSON.stringify(err);
330
- write(`${ts} [composer-mcp] ERROR ${message}
331
- ${detail}
332
- `);
333
- }
334
- var crashHandlersInstalled = false;
335
- function installCrashHandlers() {
336
- if (crashHandlersInstalled) return;
337
- crashHandlersInstalled = true;
338
- process.on("uncaughtException", (err) => {
339
- logError("uncaughtException", err);
340
- process.exit(1);
341
- });
342
- process.on("unhandledRejection", (reason) => {
343
- logError("unhandledRejection", reason);
344
- process.exit(1);
345
- });
346
- process.on("exit", (code) => {
347
- log("process exit", { code });
348
- });
349
- }
350
- var LOG_FILE_PATH = LOG_FILE;
351
-
352
398
  // src/roomState.ts
353
399
  var RoomState = class {
354
400
  doc = new Y3.Doc();
@@ -390,7 +436,6 @@ var RoomState = class {
390
436
  isAgent: true
391
437
  });
392
438
  this.installAwarenessHeartbeat();
393
- this.installAwarenessDiagnostics();
394
439
  }
395
440
  /**
396
441
  * Re-broadcast the MCP's awareness every 15s.
@@ -406,55 +451,22 @@ var RoomState = class {
406
451
  * awareness flows the other direction fine (they send on connect, server
407
452
  * broadcasts to the MCP), which is why the failure is asymmetric.
408
453
  *
409
- * Calling `setLocalState(getLocalState())` forces an awareness `update`
410
- * event, which the provider's handler broadcasts as a normal awareness
411
- * frame. 15s is well under any realistic hibernation gap.
454
+ * y-partyserver's provider listens to `awareness.on("change", ...)`, and
455
+ * y-protocols only fires `change` when the new state is deep-unequal to
456
+ * the previous one. Re-setting an identical state emits `update` but NOT
457
+ * `change`, so the provider never sends a wire frame. We bump a throwaway
458
+ * `_hb` field each tick to guarantee deep-inequality, forcing the change
459
+ * event and a broadcast. 15s is well under any realistic hibernation gap.
412
460
  */
413
461
  installAwarenessHeartbeat() {
414
462
  const heartbeat = setInterval(() => {
415
463
  const local = this.provider.awareness.getLocalState();
416
464
  if (local !== null) {
417
- this.provider.awareness.setLocalState(local);
465
+ this.provider.awareness.setLocalState({ ...local, _hb: Date.now() });
418
466
  }
419
467
  }, 15e3);
420
468
  heartbeat.unref?.();
421
469
  }
422
- /**
423
- * Diagnostic: prove whether our own awareness state is set locally and
424
- * whether the server echoes peer states back to us. Logs four snapshots
425
- * over the first 30s of a room's life, then stops. Remove once the
426
- * "agent avatar not visible" bug is root-caused.
427
- */
428
- installAwarenessDiagnostics() {
429
- const snapshot = (label) => {
430
- const ws = this.provider.ws;
431
- const entries = Array.from(this.provider.awareness.getStates().entries());
432
- log("awareness-diag", {
433
- roomId: this.roomId,
434
- label,
435
- wsReadyState: ws?.readyState ?? null,
436
- synced: this.provider.synced,
437
- localClientId: this.doc.clientID,
438
- localState: this.provider.awareness.getLocalState(),
439
- peerCount: entries.length,
440
- peers: entries.map(([clientId, state]) => ({
441
- clientId,
442
- user: state?.user ?? null
443
- }))
444
- });
445
- };
446
- snapshot("t+0 (post-setLocalState)");
447
- setTimeout(() => snapshot("t+2s"), 2e3);
448
- setTimeout(() => snapshot("t+10s"), 1e4);
449
- setTimeout(() => snapshot("t+30s"), 3e4);
450
- let tick = 1;
451
- const interval = setInterval(() => {
452
- snapshot(`t+${tick}min`);
453
- tick += 1;
454
- if (tick > 10) clearInterval(interval);
455
- }, 6e4);
456
- interval.unref?.();
457
- }
458
470
  /**
459
471
  * Resolves when the provider has completed its first sync handshake.
460
472
  * Rejects if the handshake does not complete within `timeoutMs` (default
@@ -608,23 +620,28 @@ function hashState(doc) {
608
620
  }
609
621
 
610
622
  // src/identity.ts
611
- import * as fs2 from "fs/promises";
612
- import * as path2 from "path";
623
+ import * as fs from "fs/promises";
624
+ import * as path from "path";
613
625
  import { nanoid } from "nanoid";
614
626
  var PALETTE = [
615
- "#a855f7",
616
- "#6366f1",
617
- "#ef4444",
618
- "#14b8a6",
619
- "#f59e0b",
620
- "#06b6d4",
621
- "#ec4899",
622
- "#3b82f6",
623
- "#22c55e",
624
- "#f97316",
625
- "#8b5cf6",
626
- "#10b981"
627
+ "#9333ea",
628
+ // purple-600
629
+ "#4f46e5",
630
+ // indigo-600
631
+ "#dc2626",
632
+ // red-600
633
+ "#db2777",
634
+ // pink-600
635
+ "#0f766e",
636
+ // teal-700
637
+ "#b45309",
638
+ // amber-700
639
+ "#0e7490"
640
+ // cyan-700
627
641
  ];
642
+ function isPaletteColor(value) {
643
+ return PALETTE.includes(value);
644
+ }
628
645
  var FILE_NAME = "user.json";
629
646
  var FILE_MODE = 384;
630
647
  function pickColor() {
@@ -636,12 +653,23 @@ function isValidIdentity(value) {
636
653
  return typeof v.userId === "string" && typeof v.color === "string";
637
654
  }
638
655
  async function loadOrCreateIdentity(dir) {
639
- const filePath = path2.join(dir, FILE_NAME);
656
+ const filePath = path.join(dir, FILE_NAME);
640
657
  try {
641
- const raw = await fs2.readFile(filePath, "utf8");
658
+ const raw = await fs.readFile(filePath, "utf8");
642
659
  const parsed = JSON.parse(raw);
643
660
  if (isValidIdentity(parsed)) {
644
- return { userId: parsed.userId, color: parsed.color };
661
+ if (isPaletteColor(parsed.color)) {
662
+ return { userId: parsed.userId, color: parsed.color };
663
+ }
664
+ const migrated = {
665
+ userId: parsed.userId,
666
+ color: pickColor()
667
+ };
668
+ await fs.mkdir(dir, { recursive: true });
669
+ await fs.writeFile(filePath, JSON.stringify(migrated, null, 2), {
670
+ mode: FILE_MODE
671
+ });
672
+ return migrated;
645
673
  }
646
674
  } catch (err) {
647
675
  const code = err.code;
@@ -652,19 +680,76 @@ async function loadOrCreateIdentity(dir) {
652
680
  userId: nanoid(),
653
681
  color: pickColor()
654
682
  };
655
- await fs2.mkdir(dir, { recursive: true });
656
- await fs2.writeFile(filePath, JSON.stringify(identity, null, 2), {
683
+ await fs.mkdir(dir, { recursive: true });
684
+ await fs.writeFile(filePath, JSON.stringify(identity, null, 2), {
657
685
  mode: FILE_MODE
658
686
  });
659
687
  return identity;
660
688
  }
661
689
 
662
690
  // src/mdToFragment.ts
663
- import { writeMarkdownToYFragment } from "@composer-app/shared";
664
691
  function writeMarkdownToFragment(fragment, markdown) {
665
692
  writeMarkdownToYFragment(fragment, markdown);
666
693
  }
667
694
 
695
+ // src/logger.ts
696
+ import * as fs2 from "fs";
697
+ import * as path2 from "path";
698
+ import * as os from "os";
699
+ var COMPOSER_DIR = process.env.COMPOSER_CONFIG_DIR ?? path2.join(os.homedir(), ".composer");
700
+ var LOG_FILE = process.env.COMPOSER_LOG_FILE ?? path2.join(COMPOSER_DIR, "mcp.log");
701
+ var ensured = false;
702
+ function ensureDir() {
703
+ if (ensured) return;
704
+ try {
705
+ fs2.mkdirSync(path2.dirname(LOG_FILE), { recursive: true });
706
+ ensured = true;
707
+ } catch {
708
+ }
709
+ }
710
+ function write(line) {
711
+ ensureDir();
712
+ try {
713
+ fs2.appendFileSync(LOG_FILE, line);
714
+ } catch {
715
+ }
716
+ try {
717
+ process.stderr.write(line);
718
+ } catch {
719
+ }
720
+ }
721
+ function log(message, meta) {
722
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
723
+ const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
724
+ write(`${ts} [composer-mcp] ${message}${metaStr}
725
+ `);
726
+ }
727
+ function logError(message, err) {
728
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
729
+ const detail = err instanceof Error ? `${err.name}: ${err.message}
730
+ ${err.stack ?? ""}` : typeof err === "string" ? err : JSON.stringify(err);
731
+ write(`${ts} [composer-mcp] ERROR ${message}
732
+ ${detail}
733
+ `);
734
+ }
735
+ var crashHandlersInstalled = false;
736
+ function installCrashHandlers() {
737
+ if (crashHandlersInstalled) return;
738
+ crashHandlersInstalled = true;
739
+ process.on("uncaughtException", (err) => {
740
+ logError("uncaughtException", err);
741
+ process.exit(1);
742
+ });
743
+ process.on("unhandledRejection", (reason) => {
744
+ logError("unhandledRejection", reason);
745
+ process.exit(1);
746
+ });
747
+ process.on("exit", (code) => {
748
+ log("process exit", { code });
749
+ });
750
+ }
751
+ var LOG_FILE_PATH = LOG_FILE;
752
+
668
753
  // src/mcp.ts
669
754
  var COMPOSER_DIR2 = process.env.COMPOSER_CONFIG_DIR ?? path3.join(os2.homedir(), ".composer");
670
755
  var SERVER_HOST = process.env.COMPOSER_SERVER_HOST ?? "usecomposer.app";
@@ -986,6 +1071,7 @@ ${resolved.currentSectionText}`
986
1071
  authorName: state.actingAs,
987
1072
  authorColor: state.identity.color,
988
1073
  authorUserId: state.identity.userId,
1074
+ authorIsAgent: true,
989
1075
  text,
990
1076
  createdAt: Date.now(),
991
1077
  resolved: false,
@@ -1013,6 +1099,7 @@ function handleReplyComment(args) {
1013
1099
  authorName: state.actingAs,
1014
1100
  authorColor: state.identity.color,
1015
1101
  authorUserId: state.identity.userId,
1102
+ authorIsAgent: true,
1016
1103
  text,
1017
1104
  createdAt: Date.now()
1018
1105
  };
@@ -1071,6 +1158,7 @@ ${resolved.currentSectionText}`
1071
1158
  authorName: state.actingAs,
1072
1159
  authorColor: state.identity.color,
1073
1160
  authorUserId: state.identity.userId,
1161
+ authorIsAgent: true,
1074
1162
  createdAt: Date.now(),
1075
1163
  status: "pending",
1076
1164
  anchorFrom,
@@ -1100,6 +1188,7 @@ function handleReplySuggestion(args) {
1100
1188
  authorName: state.actingAs,
1101
1189
  authorColor: state.identity.color,
1102
1190
  authorUserId: state.identity.userId,
1191
+ authorIsAgent: true,
1103
1192
  text,
1104
1193
  createdAt: Date.now()
1105
1194
  };
@@ -1267,8 +1356,8 @@ async function startMcpHttpServer(opts) {
1267
1356
  }
1268
1357
 
1269
1358
  export {
1270
- logError,
1271
1359
  loadOrCreateIdentity,
1360
+ logError,
1272
1361
  startMcpServer,
1273
1362
  startMcpHttpServer
1274
1363
  };
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  logError,
5
5
  startMcpHttpServer,
6
6
  startMcpServer
7
- } from "./chunk-PQWVQMLP.js";
7
+ } from "./chunk-SZ67UYAY.js";
8
8
 
9
9
  // src/cli.ts
10
10
  import { execFile } from "child_process";
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  startMcpHttpServer,
3
3
  startMcpServer
4
- } from "./chunk-PQWVQMLP.js";
4
+ } from "./chunk-SZ67UYAY.js";
5
5
  export {
6
6
  startMcpHttpServer,
7
7
  startMcpServer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@composer-app/mcp",
3
- "version": "0.0.1-beta.1",
3
+ "version": "0.0.1-beta.2",
4
4
  "description": "Composer MCP",
5
5
  "license": "MIT",
6
6
  "author": "Josh Philpott",
@@ -27,16 +27,31 @@
27
27
  "access": "public"
28
28
  },
29
29
  "scripts": {
30
- "build": "tsup src/cli.ts src/mcp.ts --format esm --target node20 --out-dir dist --clean",
31
- "dev": "tsup src/cli.ts src/mcp.ts --format esm --target node20 --out-dir dist --watch",
30
+ "build": "tsup",
31
+ "dev": "tsup --watch",
32
32
  "start:local": "tsx --env-file=.env.local src/cli.ts mcp",
33
33
  "start:prod": "tsx --env-file=.env.production src/cli.ts mcp",
34
34
  "typecheck": "tsc --noEmit -p .",
35
35
  "test": "vitest run"
36
36
  },
37
37
  "dependencies": {
38
- "@composer-app/shared": "*",
39
38
  "@modelcontextprotocol/sdk": "^1.0.0",
39
+ "@tiptap/core": "^3.22.0",
40
+ "@tiptap/extension-code": "^3.22.0",
41
+ "@tiptap/extension-code-block": "^3.22.0",
42
+ "@tiptap/extension-highlight": "^3.22.0",
43
+ "@tiptap/extension-image": "^3.22.0",
44
+ "@tiptap/extension-subscript": "^3.22.0",
45
+ "@tiptap/extension-superscript": "^3.22.0",
46
+ "@tiptap/extension-table": "^3.22.0",
47
+ "@tiptap/extension-table-cell": "^3.22.0",
48
+ "@tiptap/extension-table-header": "^3.22.0",
49
+ "@tiptap/extension-table-row": "^3.22.0",
50
+ "@tiptap/extension-task-item": "^3.22.0",
51
+ "@tiptap/extension-task-list": "^3.22.0",
52
+ "@tiptap/markdown": "^3.22.1",
53
+ "@tiptap/starter-kit": "^3.22.0",
54
+ "@tiptap/y-tiptap": "^3.0.2",
40
55
  "nanoid": "^5.1.7",
41
56
  "partysocket": "^1.1.16",
42
57
  "ws": "^8.20.0",