@composer-app/mcp 0.0.1-beta.0 → 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.
@@ -1,6 +1,7 @@
1
1
  // src/mcp.ts
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
5
  import {
5
6
  CallToolRequestSchema,
6
7
  ListToolsRequestSchema
@@ -8,12 +9,119 @@ import {
8
9
  import { nanoid as nanoid2 } from "nanoid";
9
10
  import path3 from "path";
10
11
  import os2 from "os";
12
+ import http from "http";
13
+ import { randomUUID } from "crypto";
11
14
 
12
15
  // src/roomState.ts
13
16
  import * as Y3 from "yjs";
14
17
  import YProvider from "y-partyserver/provider";
15
18
  import WebSocket from "ws";
16
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
+
17
125
  // src/docReaders.ts
18
126
  import * as Y from "yjs";
19
127
  function isXmlElement(node) {
@@ -104,9 +212,7 @@ function renderBlockAsMarkdown(block) {
104
212
  return text;
105
213
  }
106
214
  function serializeDocAsMarkdown(doc) {
107
- const blocks = topLevelBlocks(doc);
108
- if (blocks.length === 0) return "";
109
- return blocks.map(renderBlockAsMarkdown).join("\n\n");
215
+ return markdownFromYFragment(doc.getXmlFragment("default"));
110
216
  }
111
217
 
112
218
  // src/anchors.ts
@@ -326,8 +432,40 @@ var RoomState = class {
326
432
  this.provider.awareness.setLocalStateField("user", {
327
433
  name: opts.actingAs,
328
434
  color: opts.identity.color,
329
- userId: opts.identity.userId
435
+ userId: opts.identity.userId,
436
+ isAgent: true
330
437
  });
438
+ this.installAwarenessHeartbeat();
439
+ }
440
+ /**
441
+ * Re-broadcast the MCP's awareness every 15s.
442
+ *
443
+ * y-partyserver's provider disables the y-protocols awareness
444
+ * `_checkInterval` (see `clearInterval(awareness._checkInterval)` in
445
+ * `y-partyserver/dist/provider/index.js`), so the MCP sends its awareness
446
+ * exactly once — on connect — and never heartbeats after that. Combined
447
+ * with Cloudflare Durable Object hibernation (which evicts the server's
448
+ * in-memory `document.awareness` Map on wake), this means a browser that
449
+ * connects more than ~60s after the MCP sees an empty awareness dump in
450
+ * `onConnect` and never learns the agent is there. The user's own
451
+ * awareness flows the other direction fine (they send on connect, server
452
+ * broadcasts to the MCP), which is why the failure is asymmetric.
453
+ *
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.
460
+ */
461
+ installAwarenessHeartbeat() {
462
+ const heartbeat = setInterval(() => {
463
+ const local = this.provider.awareness.getLocalState();
464
+ if (local !== null) {
465
+ this.provider.awareness.setLocalState({ ...local, _hb: Date.now() });
466
+ }
467
+ }, 15e3);
468
+ heartbeat.unref?.();
331
469
  }
332
470
  /**
333
471
  * Resolves when the provider has completed its first sync handshake.
@@ -486,19 +624,24 @@ import * as fs from "fs/promises";
486
624
  import * as path from "path";
487
625
  import { nanoid } from "nanoid";
488
626
  var PALETTE = [
489
- "#a855f7",
490
- "#6366f1",
491
- "#ef4444",
492
- "#14b8a6",
493
- "#f59e0b",
494
- "#06b6d4",
495
- "#ec4899",
496
- "#3b82f6",
497
- "#22c55e",
498
- "#f97316",
499
- "#8b5cf6",
500
- "#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
501
641
  ];
642
+ function isPaletteColor(value) {
643
+ return PALETTE.includes(value);
644
+ }
502
645
  var FILE_NAME = "user.json";
503
646
  var FILE_MODE = 384;
504
647
  function pickColor() {
@@ -515,7 +658,18 @@ async function loadOrCreateIdentity(dir) {
515
658
  const raw = await fs.readFile(filePath, "utf8");
516
659
  const parsed = JSON.parse(raw);
517
660
  if (isValidIdentity(parsed)) {
518
- 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;
519
673
  }
520
674
  } catch (err) {
521
675
  const code = err.code;
@@ -534,50 +688,8 @@ async function loadOrCreateIdentity(dir) {
534
688
  }
535
689
 
536
690
  // src/mdToFragment.ts
537
- import { marked } from "marked";
538
- import * as Y4 from "yjs";
539
691
  function writeMarkdownToFragment(fragment, markdown) {
540
- const doc = fragment.doc;
541
- if (!doc) throw new Error("fragment must be attached to a Y.Doc");
542
- const tokens = marked.lexer(markdown);
543
- doc.transact(() => {
544
- for (const token of tokens) {
545
- const element = buildElementForToken(token);
546
- if (element) fragment.push([element]);
547
- }
548
- });
549
- }
550
- function buildElementForToken(token) {
551
- if (token.type === "heading") {
552
- const heading = token;
553
- return makeBlock("heading", heading.text ?? "", {
554
- level: String(heading.depth ?? 1)
555
- });
556
- }
557
- if (token.type === "paragraph") {
558
- const paragraph = token;
559
- return makeBlock("paragraph", paragraph.text ?? "");
560
- }
561
- if (token.type === "space") {
562
- return null;
563
- }
564
- const raw = token.raw?.trim();
565
- if (!raw) return null;
566
- return makeBlock("paragraph", raw);
567
- }
568
- function makeBlock(nodeName, text, attrs) {
569
- const element = new Y4.XmlElement(nodeName);
570
- if (attrs) {
571
- for (const [key, value] of Object.entries(attrs)) {
572
- element.setAttribute(key, value);
573
- }
574
- }
575
- if (text.length > 0) {
576
- const xmlText = new Y4.XmlText();
577
- xmlText.insert(0, text);
578
- element.insert(0, [xmlText]);
579
- }
580
- return element;
692
+ writeMarkdownToYFragment(fragment, markdown);
581
693
  }
582
694
 
583
695
  // src/logger.ts
@@ -959,6 +1071,7 @@ ${resolved.currentSectionText}`
959
1071
  authorName: state.actingAs,
960
1072
  authorColor: state.identity.color,
961
1073
  authorUserId: state.identity.userId,
1074
+ authorIsAgent: true,
962
1075
  text,
963
1076
  createdAt: Date.now(),
964
1077
  resolved: false,
@@ -986,6 +1099,7 @@ function handleReplyComment(args) {
986
1099
  authorName: state.actingAs,
987
1100
  authorColor: state.identity.color,
988
1101
  authorUserId: state.identity.userId,
1102
+ authorIsAgent: true,
989
1103
  text,
990
1104
  createdAt: Date.now()
991
1105
  };
@@ -1044,6 +1158,7 @@ ${resolved.currentSectionText}`
1044
1158
  authorName: state.actingAs,
1045
1159
  authorColor: state.identity.color,
1046
1160
  authorUserId: state.identity.userId,
1161
+ authorIsAgent: true,
1047
1162
  createdAt: Date.now(),
1048
1163
  status: "pending",
1049
1164
  anchorFrom,
@@ -1073,6 +1188,7 @@ function handleReplySuggestion(args) {
1073
1188
  authorName: state.actingAs,
1074
1189
  authorColor: state.identity.color,
1075
1190
  authorUserId: state.identity.userId,
1191
+ authorIsAgent: true,
1076
1192
  text,
1077
1193
  createdAt: Date.now()
1078
1194
  };
@@ -1124,9 +1240,7 @@ async function dispatchTool(name, args) {
1124
1240
  return errorResult(`unknown tool: ${name}`);
1125
1241
  }
1126
1242
  }
1127
- async function startMcpServer() {
1128
- installCrashHandlers();
1129
- log("mcp server starting", { pid: process.pid, node: process.version });
1243
+ function buildServer() {
1130
1244
  const server = new Server(
1131
1245
  { name: "composer-mcp", version: "0.0.1" },
1132
1246
  { capabilities: { tools: {} } }
@@ -1160,18 +1274,90 @@ async function startMcpServer() {
1160
1274
  return errorResult(message);
1161
1275
  }
1162
1276
  });
1277
+ return server;
1278
+ }
1279
+ async function startMcpServer() {
1280
+ installCrashHandlers();
1281
+ log("mcp server starting", {
1282
+ pid: process.pid,
1283
+ node: process.version,
1284
+ build: "awareness-heartbeat-v1"
1285
+ });
1286
+ const server = buildServer();
1163
1287
  const transport = new StdioServerTransport();
1164
1288
  await server.connect(transport);
1165
1289
  log("mcp server connected", {
1290
+ transport: "stdio",
1166
1291
  serverHost: SERVER_HOST,
1167
1292
  appBase: APP_BASE,
1168
1293
  logFile: LOG_FILE_PATH,
1169
1294
  pid: process.pid
1170
1295
  });
1171
1296
  }
1297
+ async function startMcpHttpServer(opts) {
1298
+ installCrashHandlers();
1299
+ log("mcp http server starting", {
1300
+ port: opts.port,
1301
+ pid: process.pid,
1302
+ node: process.version,
1303
+ build: "awareness-heartbeat-v1"
1304
+ });
1305
+ const server = buildServer();
1306
+ const transport = new StreamableHTTPServerTransport({
1307
+ sessionIdGenerator: () => randomUUID()
1308
+ });
1309
+ await server.connect(transport);
1310
+ const httpServer = http.createServer(async (req, res) => {
1311
+ try {
1312
+ const url = req.url ?? "";
1313
+ if (url === "/mcp" || url.startsWith("/mcp?") || url.startsWith("/mcp/")) {
1314
+ await transport.handleRequest(req, res);
1315
+ return;
1316
+ }
1317
+ if (url === "/health") {
1318
+ res.writeHead(200, { "Content-Type": "application/json" });
1319
+ res.end(JSON.stringify({ ok: true, serverHost: SERVER_HOST }));
1320
+ return;
1321
+ }
1322
+ res.writeHead(404, { "Content-Type": "text/plain" });
1323
+ res.end("composer-mcp: POST /mcp");
1324
+ } catch (err) {
1325
+ logError("http handler error", err);
1326
+ if (!res.headersSent) {
1327
+ res.writeHead(500, { "Content-Type": "text/plain" });
1328
+ res.end("internal error");
1329
+ } else {
1330
+ res.end();
1331
+ }
1332
+ }
1333
+ });
1334
+ httpServer.listen(opts.port, "127.0.0.1", () => {
1335
+ const url = `http://127.0.0.1:${opts.port}/mcp`;
1336
+ log("mcp http server listening", {
1337
+ url,
1338
+ serverHost: SERVER_HOST,
1339
+ appBase: APP_BASE,
1340
+ logFile: LOG_FILE_PATH,
1341
+ pid: process.pid
1342
+ });
1343
+ console.error(
1344
+ `composer-mcp http listening on ${url}
1345
+ serverHost=${SERVER_HOST}
1346
+ appBase=${APP_BASE}`
1347
+ );
1348
+ });
1349
+ const shutdown = () => {
1350
+ log("mcp http server shutting down");
1351
+ httpServer.close(() => process.exit(0));
1352
+ setTimeout(() => process.exit(0), 500).unref();
1353
+ };
1354
+ process.on("SIGTERM", shutdown);
1355
+ process.on("SIGINT", shutdown);
1356
+ }
1172
1357
 
1173
1358
  export {
1174
1359
  loadOrCreateIdentity,
1175
1360
  logError,
1176
- startMcpServer
1361
+ startMcpServer,
1362
+ startMcpHttpServer
1177
1363
  };
package/dist/cli.js CHANGED
@@ -2,8 +2,9 @@
2
2
  import {
3
3
  loadOrCreateIdentity,
4
4
  logError,
5
+ startMcpHttpServer,
5
6
  startMcpServer
6
- } from "./chunk-UPWB2QWR.js";
7
+ } from "./chunk-SZ67UYAY.js";
7
8
 
8
9
  // src/cli.ts
9
10
  import { execFile } from "child_process";
@@ -13,13 +14,36 @@ import path from "path";
13
14
  import os from "os";
14
15
  import { fileURLToPath } from "url";
15
16
  var exec = promisify(execFile);
17
+ var DEFAULT_HTTP_PORT = 3456;
18
+ function resolveHttpPort() {
19
+ const argv = process.argv.slice(3);
20
+ for (let i = 0; i < argv.length; i++) {
21
+ const arg = argv[i];
22
+ if (arg.startsWith("--port=")) {
23
+ const n = Number(arg.slice("--port=".length));
24
+ if (Number.isFinite(n) && n > 0) return n;
25
+ } else if (arg === "--port" && argv[i + 1]) {
26
+ const n = Number(argv[i + 1]);
27
+ if (Number.isFinite(n) && n > 0) return n;
28
+ }
29
+ }
30
+ const envPort = process.env.COMPOSER_MCP_PORT;
31
+ if (envPort) {
32
+ const n = Number(envPort);
33
+ if (Number.isFinite(n) && n > 0) return n;
34
+ }
35
+ return DEFAULT_HTTP_PORT;
36
+ }
16
37
  async function main() {
17
38
  const cmd = process.argv[2];
18
39
  if (cmd === "mcp") return startMcpServer();
40
+ if (cmd === "http") return startMcpHttpServer({ port: resolveHttpPort() });
19
41
  if (cmd === "setup") return setup();
20
42
  console.log(`composer-mcp
21
43
  setup Register the MCP server with your agent
22
- mcp Run as an MCP server (invoked by the host CLI)`);
44
+ mcp Run as an MCP server over stdio (invoked by the host CLI)
45
+ http Run as an MCP server over HTTP (for local dev + HMR)
46
+ flags: --port N (or COMPOSER_MCP_PORT env; default ${DEFAULT_HTTP_PORT})`);
23
47
  }
24
48
  async function setup() {
25
49
  const dir = path.join(os.homedir(), ".composer-mcp");
package/dist/mcp.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import {
2
+ startMcpHttpServer,
2
3
  startMcpServer
3
- } from "./chunk-UPWB2QWR.js";
4
+ } from "./chunk-SZ67UYAY.js";
4
5
  export {
6
+ startMcpHttpServer,
5
7
  startMcpServer
6
8
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@composer-app/mcp",
3
- "version": "0.0.1-beta.0",
3
+ "version": "0.0.1-beta.2",
4
4
  "description": "Composer MCP",
5
5
  "license": "MIT",
6
6
  "author": "Josh Philpott",
@@ -27,13 +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
+ "start:local": "tsx --env-file=.env.local src/cli.ts mcp",
33
+ "start:prod": "tsx --env-file=.env.production src/cli.ts mcp",
34
+ "typecheck": "tsc --noEmit -p .",
32
35
  "test": "vitest run"
33
36
  },
34
37
  "dependencies": {
35
38
  "@modelcontextprotocol/sdk": "^1.0.0",
36
- "marked": "^14.1.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",
37
55
  "nanoid": "^5.1.7",
38
56
  "partysocket": "^1.1.16",
39
57
  "ws": "^8.20.0",
@@ -44,6 +62,7 @@
44
62
  "@types/node": "^24.12.0",
45
63
  "@types/ws": "^8.18.1",
46
64
  "tsup": "^8.3.0",
65
+ "tsx": "^4.21.0",
47
66
  "typescript": "~5.9.3",
48
67
  "vitest": "^4.1.3"
49
68
  }