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

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,1363 +0,0 @@
1
- // src/mcp.ts
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
- import {
6
- CallToolRequestSchema,
7
- ListToolsRequestSchema
8
- } from "@modelcontextprotocol/sdk/types.js";
9
- import { nanoid as nanoid2 } from "nanoid";
10
- import path3 from "path";
11
- import os2 from "os";
12
- import http from "http";
13
- import { randomUUID } from "crypto";
14
-
15
- // src/roomState.ts
16
- import * as Y3 from "yjs";
17
- import YProvider from "y-partyserver/provider";
18
- import WebSocket from "ws";
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
-
125
- // src/docReaders.ts
126
- import * as Y from "yjs";
127
- function isXmlElement(node) {
128
- return node instanceof Y.XmlElement;
129
- }
130
- function getElementText(el) {
131
- let out = "";
132
- for (const child of el.toArray()) {
133
- if (child instanceof Y.XmlText) {
134
- out += child.toString();
135
- } else if (isXmlElement(child)) {
136
- out += getElementText(child);
137
- }
138
- }
139
- return out;
140
- }
141
- function getHeadingLevel(el) {
142
- const raw = el.getAttribute("level");
143
- const n = typeof raw === "string" ? Number.parseInt(raw, 10) : Number(raw);
144
- if (!Number.isFinite(n) || n < 1) return 1;
145
- return n;
146
- }
147
- function slugify(text) {
148
- const base = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/g, "");
149
- return base.length > 0 ? base : "section";
150
- }
151
- function makeHeadingId(text, index) {
152
- return `${slugify(text)}-${index}`;
153
- }
154
- function topLevelBlocks(doc) {
155
- const fragment = doc.getXmlFragment("default");
156
- const blocks = [];
157
- for (const node of fragment.toArray()) {
158
- if (isXmlElement(node)) {
159
- blocks.push(node);
160
- }
161
- }
162
- return blocks;
163
- }
164
- function getOutline(doc) {
165
- const blocks = topLevelBlocks(doc);
166
- const outline = [];
167
- blocks.forEach((block, index) => {
168
- if (block.nodeName !== "heading") return;
169
- const text = getElementText(block);
170
- const level = getHeadingLevel(block);
171
- outline.push({
172
- headingId: makeHeadingId(text, index),
173
- level,
174
- text,
175
- index
176
- });
177
- });
178
- return outline;
179
- }
180
- function getSection(doc, headingId) {
181
- const blocks = topLevelBlocks(doc);
182
- let startIndex = -1;
183
- let startLevel = 0;
184
- for (let i = 0; i < blocks.length; i++) {
185
- const block = blocks[i];
186
- if (block.nodeName !== "heading") continue;
187
- const id = makeHeadingId(getElementText(block), i);
188
- if (id === headingId) {
189
- startIndex = i;
190
- startLevel = getHeadingLevel(block);
191
- break;
192
- }
193
- }
194
- if (startIndex === -1) return "";
195
- const pieces = [];
196
- for (let i = startIndex; i < blocks.length; i++) {
197
- const block = blocks[i];
198
- if (i > startIndex && block.nodeName === "heading") {
199
- const level = getHeadingLevel(block);
200
- if (level <= startLevel) break;
201
- }
202
- pieces.push(renderBlockAsMarkdown(block));
203
- }
204
- return pieces.join("\n\n");
205
- }
206
- function renderBlockAsMarkdown(block) {
207
- const text = getElementText(block);
208
- if (block.nodeName === "heading") {
209
- const level = getHeadingLevel(block);
210
- return `${"#".repeat(level)} ${text}`;
211
- }
212
- return text;
213
- }
214
- function serializeDocAsMarkdown(doc) {
215
- return markdownFromYFragment(doc.getXmlFragment("default"));
216
- }
217
-
218
- // src/anchors.ts
219
- import * as Y2 from "yjs";
220
- function buildFlatMap(fragment) {
221
- let flat = "";
222
- const map = [];
223
- const walk = (node) => {
224
- if (node instanceof Y2.XmlText) {
225
- const text = node.toString();
226
- for (let i = 0; i < text.length; i++) {
227
- map.push({
228
- xmlText: node,
229
- offsetInText: i,
230
- flatIndex: flat.length + i
231
- });
232
- }
233
- flat += text;
234
- return;
235
- }
236
- for (const child of node.toArray()) {
237
- if (child instanceof Y2.XmlText || child instanceof Y2.XmlElement) {
238
- walk(child);
239
- }
240
- }
241
- };
242
- const topLevel = fragment.toArray();
243
- topLevel.forEach((node, idx) => {
244
- if (node instanceof Y2.XmlText || node instanceof Y2.XmlElement) {
245
- walk(node);
246
- }
247
- if (idx < topLevel.length - 1) {
248
- flat += "\n";
249
- }
250
- });
251
- return { flat, map };
252
- }
253
- function findNthOccurrence(flat, needle, n) {
254
- if (needle.length === 0) return null;
255
- let searchFrom = 0;
256
- let found = -1;
257
- for (let i = 0; i < n; i++) {
258
- const idx = flat.indexOf(needle, searchFrom);
259
- if (idx < 0) return null;
260
- found = idx;
261
- searchFrom = idx + 1;
262
- }
263
- return found;
264
- }
265
- function lookupFlatIndex(map, flatIndex) {
266
- for (const entry of map) {
267
- if (entry.flatIndex === flatIndex) return entry;
268
- }
269
- for (const entry of map) {
270
- if (entry.flatIndex >= flatIndex) return entry;
271
- }
272
- return null;
273
- }
274
- function lookupFlatIndexEnd(map, flatIndex) {
275
- let best = null;
276
- for (const entry of map) {
277
- if (entry.flatIndex <= flatIndex) {
278
- best = entry;
279
- } else {
280
- break;
281
- }
282
- }
283
- return best;
284
- }
285
- function resolveAnchoredContext(doc, anchorFrom, anchorTo) {
286
- const fromAbs = decodeAbs(doc, anchorFrom);
287
- const toAbs = decodeAbs(doc, anchorTo);
288
- if (!fromAbs) return {};
289
- const fragment = doc.getXmlFragment("default");
290
- const { flat, map } = buildFlatMap(fragment);
291
- const fromFlat = flatIndexFor(map, fromAbs);
292
- const toFlat = toAbs ? flatIndexFor(map, toAbs) : null;
293
- const ctx = {};
294
- if (fromFlat !== null && toFlat !== null && toFlat >= fromFlat && toFlat <= flat.length) {
295
- ctx.anchoredText = flat.slice(fromFlat, toFlat);
296
- }
297
- const topLevel = fragment.toArray();
298
- let containingBlockIdx = -1;
299
- for (let i = 0; i < topLevel.length; i++) {
300
- const block = topLevel[i];
301
- if (block instanceof Y2.XmlElement && containsType(block, fromAbs.type)) {
302
- containingBlockIdx = i;
303
- break;
304
- }
305
- }
306
- if (containingBlockIdx < 0) return ctx;
307
- const outline = getOutline(doc);
308
- let heading = null;
309
- for (const entry of outline) {
310
- if (entry.index <= containingBlockIdx) heading = entry;
311
- else break;
312
- }
313
- if (heading) {
314
- ctx.headingId = heading.headingId;
315
- ctx.headingText = heading.text;
316
- ctx.sectionMarkdown = getSection(doc, heading.headingId);
317
- }
318
- return ctx;
319
- }
320
- function decodeAbs(doc, encoded) {
321
- if (!encoded) return null;
322
- try {
323
- const rel = Y2.decodeRelativePosition(encoded);
324
- const abs = Y2.createAbsolutePositionFromRelativePosition(rel, doc);
325
- if (!abs) return null;
326
- return { type: abs.type, index: abs.index };
327
- } catch {
328
- return null;
329
- }
330
- }
331
- function flatIndexFor(map, abs) {
332
- for (const entry of map) {
333
- if (entry.xmlText === abs.type && entry.offsetInText === abs.index) {
334
- return entry.flatIndex;
335
- }
336
- }
337
- let last = null;
338
- for (const entry of map) {
339
- if (entry.xmlText === abs.type) last = entry;
340
- }
341
- if (last && abs.index === last.offsetInText + 1) return last.flatIndex + 1;
342
- return null;
343
- }
344
- function containsType(el, target) {
345
- for (const child of el.toArray()) {
346
- if (child === target) return true;
347
- if (child instanceof Y2.XmlElement && containsType(child, target)) {
348
- return true;
349
- }
350
- }
351
- return false;
352
- }
353
- function resolveServerAnchor(doc, spec) {
354
- const occurrence = spec.occurrence ?? 1;
355
- const outline = getOutline(doc);
356
- const entry = outline.find((e) => e.headingId === spec.headingId);
357
- if (!entry) {
358
- return {
359
- ok: false,
360
- error: "section_not_found",
361
- currentSectionText: ""
362
- };
363
- }
364
- const currentSectionText = getSection(doc, spec.headingId);
365
- if (!currentSectionText.includes(spec.textToFind)) {
366
- return { ok: false, error: "text_not_found", currentSectionText };
367
- }
368
- const fragment = doc.getXmlFragment("default");
369
- const { flat, map } = buildFlatMap(fragment);
370
- const flatStart = findNthOccurrence(flat, spec.textToFind, occurrence);
371
- if (flatStart === null) {
372
- return { ok: false, error: "text_not_found", currentSectionText };
373
- }
374
- const flatEnd = flatStart + spec.textToFind.length;
375
- const startEntry = lookupFlatIndex(map, flatStart);
376
- if (!startEntry) {
377
- return { ok: false, error: "text_not_found", currentSectionText };
378
- }
379
- const lastCharEntry = lookupFlatIndexEnd(map, flatEnd - 1);
380
- if (!lastCharEntry) {
381
- return { ok: false, error: "text_not_found", currentSectionText };
382
- }
383
- const fromRelPos = Y2.createRelativePositionFromTypeIndex(
384
- startEntry.xmlText,
385
- startEntry.offsetInText
386
- );
387
- const toRelPos = Y2.createRelativePositionFromTypeIndex(
388
- lastCharEntry.xmlText,
389
- lastCharEntry.offsetInText + 1
390
- );
391
- return {
392
- ok: true,
393
- from: Y2.encodeRelativePosition(fromRelPos),
394
- to: Y2.encodeRelativePosition(toRelPos)
395
- };
396
- }
397
-
398
- // src/roomState.ts
399
- var RoomState = class {
400
- doc = new Y3.Doc();
401
- actingAs;
402
- identity;
403
- roomId;
404
- provider;
405
- queue = [];
406
- waiters = [];
407
- /**
408
- * Dedupe key set for mentions we've already emitted OR that we've already
409
- * observed via a local write (so our own writes bouncing through the
410
- * observer don't trigger). Keyed on `threadId` for body/replacementText
411
- * mentions and `threadId:replyId` for reply mentions.
412
- */
413
- seen = /* @__PURE__ */ new Set();
414
- /**
415
- * Threads the agent has already written to (created a comment, added a
416
- * suggestion, or replied). Once a thread is "active", subsequent remote
417
- * replies on it are surfaced to the model even if they don't say
418
- * `@agent` — the conversation is already in progress, and requiring a
419
- * re-mention every turn is bad UX.
420
- */
421
- activeThreads = /* @__PURE__ */ new Set();
422
- constructor(opts) {
423
- this.roomId = opts.roomId;
424
- this.actingAs = opts.actingAs;
425
- this.identity = opts.identity;
426
- this.watchMentions();
427
- this.provider = new YProvider(opts.serverHost, opts.roomId, this.doc, {
428
- party: "composer-room",
429
- connect: true,
430
- WebSocketPolyfill: WebSocket
431
- });
432
- this.provider.awareness.setLocalStateField("user", {
433
- name: opts.actingAs,
434
- color: opts.identity.color,
435
- userId: opts.identity.userId,
436
- isAgent: true
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?.();
469
- }
470
- /**
471
- * Resolves when the provider has completed its first sync handshake.
472
- * Rejects if the handshake does not complete within `timeoutMs` (default
473
- * 15s) so callers fail loudly on unreachable hosts or dead rooms instead
474
- * of hanging the MCP tool call indefinitely.
475
- */
476
- async waitForInitialSync(timeoutMs = 15e3) {
477
- if (this.provider.synced) return;
478
- await new Promise((resolve, reject) => {
479
- const onSync = (synced) => {
480
- if (!synced) return;
481
- clearTimeout(timer);
482
- this.provider.off("sync", onSync);
483
- resolve();
484
- };
485
- const timer = setTimeout(() => {
486
- this.provider.off("sync", onSync);
487
- reject(
488
- new Error(
489
- `timed out after ${timeoutMs}ms waiting for sync handshake on room "${this.roomId}"`
490
- )
491
- );
492
- }, timeoutMs);
493
- this.provider.on("sync", onSync);
494
- });
495
- }
496
- snapshot() {
497
- return {
498
- fullDoc: serializeDocAsMarkdown(this.doc),
499
- outline: getOutline(this.doc),
500
- docVersion: hashState(this.doc),
501
- threadsBacklog: []
502
- };
503
- }
504
- async nextEvent(timeoutMs) {
505
- const pending = this.queue.shift();
506
- if (pending) return pending;
507
- return new Promise((resolve) => {
508
- const timer = setTimeout(() => {
509
- const idx = this.waiters.indexOf(waiter);
510
- if (idx >= 0) this.waiters.splice(idx, 1);
511
- resolve({ kind: "timeout" });
512
- }, timeoutMs);
513
- const waiter = (ev) => {
514
- clearTimeout(timer);
515
- resolve(ev);
516
- };
517
- this.waiters.push(waiter);
518
- });
519
- }
520
- destroy() {
521
- this.provider.destroy();
522
- this.doc.destroy();
523
- }
524
- /**
525
- * Mark a thread as active so subsequent remote replies on it surface as
526
- * mentions even without an explicit `@agent`. Called by MCP write-tool
527
- * handlers right after the agent creates or replies on a thread.
528
- */
529
- markThreadActive(threadId) {
530
- this.activeThreads.add(threadId);
531
- }
532
- enqueue(ev) {
533
- const waiter = this.waiters.shift();
534
- if (waiter) waiter(ev);
535
- else this.queue.push(ev);
536
- }
537
- watchMentions() {
538
- attachMentionObserver(this.doc, {
539
- enqueue: (ev) => this.enqueue(ev),
540
- seen: this.seen,
541
- activeThreads: this.activeThreads,
542
- identityUserId: this.identity.userId
543
- });
544
- }
545
- };
546
- var AGENT_MENTION_RE = /@agent/i;
547
- var hasAgentMention = (text) => AGENT_MENTION_RE.test(text);
548
- function attachMentionObserver(doc, opts) {
549
- const seen = opts.seen ?? /* @__PURE__ */ new Set();
550
- const activeThreads = opts.activeThreads ?? /* @__PURE__ */ new Set();
551
- const enqueue = opts.enqueue;
552
- const identityUserId = opts.identityUserId;
553
- const scan = (kind, threadId, entry, isLocal) => {
554
- if (!entry || typeof entry !== "object") return;
555
- const record = entry;
556
- const replies = Array.isArray(record.replies) ? record.replies : [];
557
- let lastAgentIdx = -1;
558
- if (identityUserId !== void 0) {
559
- for (let i = 0; i < replies.length; i++) {
560
- const r = replies[i];
561
- if (r && typeof r === "object" && r.authorUserId === identityUserId) {
562
- lastAgentIdx = i;
563
- }
564
- }
565
- if (lastAgentIdx >= 0) activeThreads.add(threadId);
566
- }
567
- const bodyAnswered = lastAgentIdx >= 0;
568
- const body = typeof record.text === "string" ? record.text : typeof record.replacementText === "string" ? record.replacementText : "";
569
- if (hasAgentMention(body) && !seen.has(threadId)) {
570
- seen.add(threadId);
571
- if (!isLocal && !bodyAnswered) {
572
- enqueue({
573
- kind: "mention",
574
- threadId,
575
- threadKind: kind,
576
- threadText: body,
577
- reason: "direct_mention",
578
- ...resolveAnchoredContext(doc, record.anchorFrom, record.anchorTo)
579
- });
580
- }
581
- }
582
- for (let i = 0; i < replies.length; i++) {
583
- const r = replies[i];
584
- if (!r || typeof r !== "object") continue;
585
- const reply = r;
586
- if (typeof reply.id !== "string") continue;
587
- if (typeof reply.text !== "string") continue;
588
- const key = `${threadId}:${reply.id}`;
589
- if (seen.has(key)) continue;
590
- seen.add(key);
591
- if (isLocal) continue;
592
- if (i <= lastAgentIdx) continue;
593
- const isDirect = hasAgentMention(reply.text);
594
- const inActiveThread = activeThreads.has(threadId);
595
- if (!isDirect && !inActiveThread) continue;
596
- enqueue({
597
- kind: "mention",
598
- threadId,
599
- threadKind: kind,
600
- threadText: reply.text,
601
- replyId: reply.id,
602
- reason: isDirect ? "direct_mention" : "active_thread",
603
- ...resolveAnchoredContext(doc, record.anchorFrom, record.anchorTo)
604
- });
605
- }
606
- };
607
- const onChange = (kind) => (event) => {
608
- const isLocal = event.transaction.local;
609
- event.changes.keys.forEach((change, id) => {
610
- if (change.action === "add" || change.action === "update") {
611
- scan(kind, id, event.target.get(id), isLocal);
612
- }
613
- });
614
- };
615
- doc.getMap("comments").observe(onChange("comment"));
616
- doc.getMap("suggestions").observe(onChange("suggestion"));
617
- }
618
- function hashState(doc) {
619
- return Buffer.from(Y3.encodeStateVector(doc)).toString("base64");
620
- }
621
-
622
- // src/identity.ts
623
- import * as fs from "fs/promises";
624
- import * as path from "path";
625
- import { nanoid } from "nanoid";
626
- var PALETTE = [
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
641
- ];
642
- function isPaletteColor(value) {
643
- return PALETTE.includes(value);
644
- }
645
- var FILE_NAME = "user.json";
646
- var FILE_MODE = 384;
647
- function pickColor() {
648
- return PALETTE[Math.floor(Math.random() * PALETTE.length)];
649
- }
650
- function isValidIdentity(value) {
651
- if (typeof value !== "object" || value === null) return false;
652
- const v = value;
653
- return typeof v.userId === "string" && typeof v.color === "string";
654
- }
655
- async function loadOrCreateIdentity(dir) {
656
- const filePath = path.join(dir, FILE_NAME);
657
- try {
658
- const raw = await fs.readFile(filePath, "utf8");
659
- const parsed = JSON.parse(raw);
660
- if (isValidIdentity(parsed)) {
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;
673
- }
674
- } catch (err) {
675
- const code = err.code;
676
- if (code && code !== "ENOENT") {
677
- }
678
- }
679
- const identity = {
680
- userId: nanoid(),
681
- color: pickColor()
682
- };
683
- await fs.mkdir(dir, { recursive: true });
684
- await fs.writeFile(filePath, JSON.stringify(identity, null, 2), {
685
- mode: FILE_MODE
686
- });
687
- return identity;
688
- }
689
-
690
- // src/mdToFragment.ts
691
- function writeMarkdownToFragment(fragment, markdown) {
692
- writeMarkdownToYFragment(fragment, markdown);
693
- }
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
-
753
- // src/mcp.ts
754
- var COMPOSER_DIR2 = process.env.COMPOSER_CONFIG_DIR ?? path3.join(os2.homedir(), ".composer");
755
- var SERVER_HOST = process.env.COMPOSER_SERVER_HOST ?? "usecomposer.app";
756
- var APP_BASE = process.env.COMPOSER_APP_BASE ?? "https://usecomposer.app";
757
- var rooms = /* @__PURE__ */ new Map();
758
- var identityCache = null;
759
- async function getIdentity() {
760
- if (!identityCache) {
761
- identityCache = await loadOrCreateIdentity(COMPOSER_DIR2);
762
- }
763
- return identityCache;
764
- }
765
- function getOrError(roomId) {
766
- const state = rooms.get(roomId);
767
- if (!state) throw new Error(`not attached to room: ${roomId}`);
768
- return state;
769
- }
770
- function browserUrlFor(roomId) {
771
- return `${APP_BASE.replace(/\/$/, "")}/r/${roomId}`;
772
- }
773
- function parseRoomIdFromUrl(url) {
774
- const match = url.match(/\/r\/([^/?#]+)/);
775
- if (!match || !match[1]) {
776
- throw new Error(`could not extract roomId from url: ${url}`);
777
- }
778
- return match[1];
779
- }
780
- var TOOL_DEFS = [
781
- {
782
- name: "composer_create_room",
783
- description: "Create a new Composer room. Returns { roomId, browserUrl, snapshot }. Optionally seeds the doc with markdown before snapshotting.",
784
- inputSchema: {
785
- type: "object",
786
- properties: {
787
- name: { type: "string" },
788
- seedMarkdown: { type: "string" },
789
- actingAs: {
790
- type: "string",
791
- description: `Agent display name for this room, e.g. "Josh's Agent".`
792
- }
793
- },
794
- required: ["actingAs"]
795
- }
796
- },
797
- {
798
- name: "composer_join_room",
799
- description: "Join an existing Composer room by browser URL. Returns the attach snapshot.",
800
- inputSchema: {
801
- type: "object",
802
- properties: {
803
- url: { type: "string" },
804
- actingAs: { type: "string" }
805
- },
806
- required: ["url", "actingAs"]
807
- }
808
- },
809
- {
810
- name: "composer_attach_room",
811
- description: "Fetch a fresh snapshot for a room this agent is already attached to.",
812
- inputSchema: {
813
- type: "object",
814
- properties: { roomId: { type: "string" } },
815
- required: ["roomId"]
816
- }
817
- },
818
- {
819
- name: "composer_next_event",
820
- description: "Block until a remote @agent mention arrives or the timeout elapses.",
821
- inputSchema: {
822
- type: "object",
823
- properties: {
824
- roomId: { type: "string" },
825
- timeoutSec: { type: "number", default: 300 }
826
- },
827
- required: ["roomId"]
828
- }
829
- },
830
- {
831
- name: "composer_get_section",
832
- description: "Return the markdown rendering of a section, identified by its stable headingId.",
833
- inputSchema: {
834
- type: "object",
835
- properties: {
836
- roomId: { type: "string" },
837
- headingId: { type: "string" }
838
- },
839
- required: ["roomId", "headingId"]
840
- }
841
- },
842
- {
843
- name: "composer_get_full_doc",
844
- description: "Return the entire doc as markdown.",
845
- inputSchema: {
846
- type: "object",
847
- properties: { roomId: { type: "string" } },
848
- required: ["roomId"]
849
- }
850
- },
851
- {
852
- name: "composer_add_comment",
853
- description: "Post a comment anchored to a text span. Anchor is { headingId, textToFind, occurrence? }. Returns { id } on success or an isError result if the anchor cannot be resolved.",
854
- inputSchema: {
855
- type: "object",
856
- properties: {
857
- roomId: { type: "string" },
858
- anchor: {
859
- type: "object",
860
- properties: {
861
- headingId: { type: "string" },
862
- textToFind: { type: "string" },
863
- occurrence: { type: "number" }
864
- },
865
- required: ["headingId", "textToFind"]
866
- },
867
- text: { type: "string" }
868
- },
869
- required: ["roomId", "anchor", "text"]
870
- }
871
- },
872
- {
873
- name: "composer_reply_comment",
874
- description: "Append a reply to an existing comment thread.",
875
- inputSchema: {
876
- type: "object",
877
- properties: {
878
- roomId: { type: "string" },
879
- threadId: { type: "string" },
880
- text: { type: "string" }
881
- },
882
- required: ["roomId", "threadId", "text"]
883
- }
884
- },
885
- {
886
- name: "composer_add_suggestion",
887
- description: "Post a text replacement suggestion. When responding to a thread, default to passing fromThreadId \u2014 the suggestion inherits the source thread's exact stored anchor, which is the right span whenever the user's request is scoped to what they selected (the common case). Supply an `anchor` instead when the user's request explicitly targets a different span (e.g. they highlight one word but ask to rewrite the whole paragraph), or for proactive suggestions with no source thread. Pick one: supplying both is rejected. Returns { id } on success or an isError result if the anchor cannot be resolved.",
888
- inputSchema: {
889
- type: "object",
890
- properties: {
891
- roomId: { type: "string" },
892
- fromThreadId: {
893
- type: "string",
894
- description: "ID of the comment or suggestion thread this responds to. When set, the new suggestion copies that thread's anchor bytes verbatim \u2014 do not also pass `anchor`."
895
- },
896
- anchor: {
897
- type: "object",
898
- properties: {
899
- headingId: { type: "string" },
900
- textToFind: { type: "string" },
901
- occurrence: { type: "number" }
902
- },
903
- required: ["headingId", "textToFind"]
904
- },
905
- replacementText: { type: "string" }
906
- },
907
- required: ["roomId", "replacementText"]
908
- }
909
- },
910
- {
911
- name: "composer_reply_suggestion",
912
- description: "Append a reply to an existing suggestion thread.",
913
- inputSchema: {
914
- type: "object",
915
- properties: {
916
- roomId: { type: "string" },
917
- threadId: { type: "string" },
918
- text: { type: "string" }
919
- },
920
- required: ["roomId", "threadId", "text"]
921
- }
922
- },
923
- {
924
- name: "composer_resolve_thread",
925
- description: "Mark a comment thread as resolved.",
926
- inputSchema: {
927
- type: "object",
928
- properties: {
929
- roomId: { type: "string" },
930
- threadId: { type: "string" }
931
- },
932
- required: ["roomId", "threadId"]
933
- }
934
- }
935
- ];
936
- function okResult(data) {
937
- return {
938
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
939
- };
940
- }
941
- function errorResult(message) {
942
- return {
943
- content: [{ type: "text", text: message }],
944
- isError: true
945
- };
946
- }
947
- function asObject(args) {
948
- if (!args || typeof args !== "object") {
949
- throw new Error("tool arguments must be an object");
950
- }
951
- return args;
952
- }
953
- function asString(value, field) {
954
- if (typeof value !== "string" || value.length === 0) {
955
- throw new Error(`${field} must be a non-empty string`);
956
- }
957
- return value;
958
- }
959
- function asOptionalString(value, field) {
960
- if (value === void 0) return void 0;
961
- if (typeof value !== "string") {
962
- throw new Error(`${field} must be a string`);
963
- }
964
- return value;
965
- }
966
- function asAnchor(value) {
967
- if (!value || typeof value !== "object") {
968
- throw new Error("anchor must be an object");
969
- }
970
- const anchor = value;
971
- const headingId = asString(anchor.headingId, "anchor.headingId");
972
- const textToFind = asString(anchor.textToFind, "anchor.textToFind");
973
- let occurrence;
974
- if (anchor.occurrence !== void 0) {
975
- if (typeof anchor.occurrence !== "number" || !Number.isFinite(anchor.occurrence)) {
976
- throw new Error("anchor.occurrence must be a number");
977
- }
978
- occurrence = anchor.occurrence;
979
- }
980
- return { headingId, textToFind, occurrence };
981
- }
982
- async function handleCreateRoom(args) {
983
- const a = asObject(args);
984
- const actingAs = asString(a.actingAs, "actingAs");
985
- const seedMarkdown = asOptionalString(a.seedMarkdown, "seedMarkdown");
986
- asOptionalString(a.name, "name");
987
- const identity = await getIdentity();
988
- const roomId = nanoid2(10);
989
- const state = new RoomState({
990
- roomId,
991
- serverHost: SERVER_HOST,
992
- actingAs,
993
- identity
994
- });
995
- await state.waitForInitialSync();
996
- if (seedMarkdown) {
997
- writeMarkdownToFragment(state.doc.getXmlFragment("default"), seedMarkdown);
998
- }
999
- rooms.set(roomId, state);
1000
- return okResult({
1001
- roomId,
1002
- browserUrl: browserUrlFor(roomId),
1003
- snapshot: state.snapshot()
1004
- });
1005
- }
1006
- async function handleJoinRoom(args) {
1007
- const a = asObject(args);
1008
- const url = asString(a.url, "url");
1009
- const actingAs = asString(a.actingAs, "actingAs");
1010
- const roomId = parseRoomIdFromUrl(url);
1011
- const identity = await getIdentity();
1012
- const existing = rooms.get(roomId);
1013
- if (existing) {
1014
- return okResult({ roomId, snapshot: existing.snapshot() });
1015
- }
1016
- const state = new RoomState({
1017
- roomId,
1018
- serverHost: SERVER_HOST,
1019
- actingAs,
1020
- identity
1021
- });
1022
- await state.waitForInitialSync();
1023
- rooms.set(roomId, state);
1024
- return okResult({ roomId, snapshot: state.snapshot() });
1025
- }
1026
- function handleAttachRoom(args) {
1027
- const a = asObject(args);
1028
- const roomId = asString(a.roomId, "roomId");
1029
- const state = getOrError(roomId);
1030
- return okResult({ roomId, snapshot: state.snapshot() });
1031
- }
1032
- async function handleNextEvent(args) {
1033
- const a = asObject(args);
1034
- const roomId = asString(a.roomId, "roomId");
1035
- const timeoutSec = typeof a.timeoutSec === "number" && Number.isFinite(a.timeoutSec) ? a.timeoutSec : 300;
1036
- const state = getOrError(roomId);
1037
- const event = await state.nextEvent(timeoutSec * 1e3);
1038
- return okResult(event);
1039
- }
1040
- function handleGetSection(args) {
1041
- const a = asObject(args);
1042
- const roomId = asString(a.roomId, "roomId");
1043
- const headingId = asString(a.headingId, "headingId");
1044
- const state = getOrError(roomId);
1045
- const section = getSection(state.doc, headingId);
1046
- return okResult({ headingId, markdown: section });
1047
- }
1048
- function handleGetFullDoc(args) {
1049
- const a = asObject(args);
1050
- const roomId = asString(a.roomId, "roomId");
1051
- const state = getOrError(roomId);
1052
- return okResult({ markdown: serializeDocAsMarkdown(state.doc) });
1053
- }
1054
- function handleAddComment(args) {
1055
- const a = asObject(args);
1056
- const roomId = asString(a.roomId, "roomId");
1057
- const anchor = asAnchor(a.anchor);
1058
- const text = asString(a.text, "text");
1059
- const state = getOrError(roomId);
1060
- const resolved = resolveServerAnchor(state.doc, anchor);
1061
- if (!resolved.ok) {
1062
- return errorResult(
1063
- `anchor ${resolved.error}. Current section:
1064
- ${resolved.currentSectionText}`
1065
- );
1066
- }
1067
- const id = nanoid2();
1068
- const comments = state.doc.getMap("comments");
1069
- comments.set(id, {
1070
- id,
1071
- authorName: state.actingAs,
1072
- authorColor: state.identity.color,
1073
- authorUserId: state.identity.userId,
1074
- authorIsAgent: true,
1075
- text,
1076
- createdAt: Date.now(),
1077
- resolved: false,
1078
- anchorFrom: resolved.from,
1079
- anchorTo: resolved.to,
1080
- replies: []
1081
- });
1082
- state.markThreadActive(id);
1083
- return okResult({ id });
1084
- }
1085
- function handleReplyComment(args) {
1086
- const a = asObject(args);
1087
- const roomId = asString(a.roomId, "roomId");
1088
- const threadId = asString(a.threadId, "threadId");
1089
- const text = asString(a.text, "text");
1090
- const state = getOrError(roomId);
1091
- const comments = state.doc.getMap("comments");
1092
- const existing = comments.get(threadId);
1093
- if (!existing) {
1094
- return errorResult(`comment not found: ${threadId}`);
1095
- }
1096
- const replyId = nanoid2();
1097
- const reply = {
1098
- id: replyId,
1099
- authorName: state.actingAs,
1100
- authorColor: state.identity.color,
1101
- authorUserId: state.identity.userId,
1102
- authorIsAgent: true,
1103
- text,
1104
- createdAt: Date.now()
1105
- };
1106
- comments.set(threadId, {
1107
- ...existing,
1108
- replies: [...existing.replies ?? [], reply]
1109
- });
1110
- state.markThreadActive(threadId);
1111
- return okResult({ replyId });
1112
- }
1113
- function handleAddSuggestion(args) {
1114
- const a = asObject(args);
1115
- const roomId = asString(a.roomId, "roomId");
1116
- const replacementText = asString(a.replacementText, "replacementText");
1117
- const fromThreadId = asOptionalString(a.fromThreadId, "fromThreadId");
1118
- const state = getOrError(roomId);
1119
- let anchorFrom;
1120
- let anchorTo;
1121
- let originalText;
1122
- if (fromThreadId) {
1123
- if (a.anchor !== void 0) {
1124
- return errorResult(
1125
- "pass fromThreadId OR anchor, not both \u2014 fromThreadId copies the source thread's anchor and makes `anchor` meaningless"
1126
- );
1127
- }
1128
- const source = state.doc.getMap("comments").get(fromThreadId) ?? state.doc.getMap("suggestions").get(fromThreadId);
1129
- if (!source) {
1130
- return errorResult(`thread not found: ${fromThreadId}`);
1131
- }
1132
- if (!source.anchorFrom || !source.anchorTo) {
1133
- return errorResult(
1134
- `thread ${fromThreadId} is unanchored \u2014 cannot inherit its span`
1135
- );
1136
- }
1137
- anchorFrom = source.anchorFrom;
1138
- anchorTo = source.anchorTo;
1139
- const ctx = resolveAnchoredContext(state.doc, anchorFrom, anchorTo);
1140
- originalText = ctx.anchoredText ?? "";
1141
- } else {
1142
- const anchor = asAnchor(a.anchor);
1143
- const resolved = resolveServerAnchor(state.doc, anchor);
1144
- if (!resolved.ok) {
1145
- return errorResult(
1146
- `anchor ${resolved.error}. Current section:
1147
- ${resolved.currentSectionText}`
1148
- );
1149
- }
1150
- anchorFrom = resolved.from;
1151
- anchorTo = resolved.to;
1152
- originalText = anchor.textToFind;
1153
- }
1154
- const id = nanoid2();
1155
- const suggestions = state.doc.getMap("suggestions");
1156
- suggestions.set(id, {
1157
- id,
1158
- authorName: state.actingAs,
1159
- authorColor: state.identity.color,
1160
- authorUserId: state.identity.userId,
1161
- authorIsAgent: true,
1162
- createdAt: Date.now(),
1163
- status: "pending",
1164
- anchorFrom,
1165
- anchorTo,
1166
- replacementText,
1167
- originalText,
1168
- replies: []
1169
- });
1170
- state.markThreadActive(id);
1171
- if (fromThreadId) state.markThreadActive(fromThreadId);
1172
- return okResult({ id });
1173
- }
1174
- function handleReplySuggestion(args) {
1175
- const a = asObject(args);
1176
- const roomId = asString(a.roomId, "roomId");
1177
- const threadId = asString(a.threadId, "threadId");
1178
- const text = asString(a.text, "text");
1179
- const state = getOrError(roomId);
1180
- const suggestions = state.doc.getMap("suggestions");
1181
- const existing = suggestions.get(threadId);
1182
- if (!existing) {
1183
- return errorResult(`suggestion not found: ${threadId}`);
1184
- }
1185
- const replyId = nanoid2();
1186
- const reply = {
1187
- id: replyId,
1188
- authorName: state.actingAs,
1189
- authorColor: state.identity.color,
1190
- authorUserId: state.identity.userId,
1191
- authorIsAgent: true,
1192
- text,
1193
- createdAt: Date.now()
1194
- };
1195
- suggestions.set(threadId, {
1196
- ...existing,
1197
- replies: [...existing.replies ?? [], reply]
1198
- });
1199
- state.markThreadActive(threadId);
1200
- return okResult({ replyId });
1201
- }
1202
- function handleResolveThread(args) {
1203
- const a = asObject(args);
1204
- const roomId = asString(a.roomId, "roomId");
1205
- const threadId = asString(a.threadId, "threadId");
1206
- const state = getOrError(roomId);
1207
- const comments = state.doc.getMap("comments");
1208
- const existing = comments.get(threadId);
1209
- if (!existing) {
1210
- return errorResult(`comment not found: ${threadId}`);
1211
- }
1212
- comments.set(threadId, { ...existing, resolved: true });
1213
- return okResult({ threadId, resolved: true });
1214
- }
1215
- async function dispatchTool(name, args) {
1216
- switch (name) {
1217
- case "composer_create_room":
1218
- return handleCreateRoom(args);
1219
- case "composer_join_room":
1220
- return handleJoinRoom(args);
1221
- case "composer_attach_room":
1222
- return handleAttachRoom(args);
1223
- case "composer_next_event":
1224
- return handleNextEvent(args);
1225
- case "composer_get_section":
1226
- return handleGetSection(args);
1227
- case "composer_get_full_doc":
1228
- return handleGetFullDoc(args);
1229
- case "composer_add_comment":
1230
- return handleAddComment(args);
1231
- case "composer_reply_comment":
1232
- return handleReplyComment(args);
1233
- case "composer_add_suggestion":
1234
- return handleAddSuggestion(args);
1235
- case "composer_reply_suggestion":
1236
- return handleReplySuggestion(args);
1237
- case "composer_resolve_thread":
1238
- return handleResolveThread(args);
1239
- default:
1240
- return errorResult(`unknown tool: ${name}`);
1241
- }
1242
- }
1243
- function buildServer() {
1244
- const server = new Server(
1245
- { name: "composer-mcp", version: "0.0.1" },
1246
- { capabilities: { tools: {} } }
1247
- );
1248
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
1249
- tools: TOOL_DEFS
1250
- }));
1251
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
1252
- const { name, arguments: args } = req.params;
1253
- const roomId = args && typeof args === "object" && "roomId" in args ? args.roomId : void 0;
1254
- const started = Date.now();
1255
- log(`tool ${name} call`, { roomId });
1256
- try {
1257
- const result = await dispatchTool(name, args);
1258
- const elapsedMs = Date.now() - started;
1259
- if (result.isError) {
1260
- const detail = result.content[0]?.text ?? "";
1261
- log(`tool ${name} returned isError`, {
1262
- roomId,
1263
- elapsedMs,
1264
- detail: detail.slice(0, 200)
1265
- });
1266
- } else {
1267
- log(`tool ${name} ok`, { roomId, elapsedMs });
1268
- }
1269
- return result;
1270
- } catch (err) {
1271
- const elapsedMs = Date.now() - started;
1272
- logError(`tool ${name} threw after ${elapsedMs}ms (roomId=${roomId ?? "n/a"})`, err);
1273
- const message = err instanceof Error ? err.message : String(err);
1274
- return errorResult(message);
1275
- }
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();
1287
- const transport = new StdioServerTransport();
1288
- await server.connect(transport);
1289
- log("mcp server connected", {
1290
- transport: "stdio",
1291
- serverHost: SERVER_HOST,
1292
- appBase: APP_BASE,
1293
- logFile: LOG_FILE_PATH,
1294
- pid: process.pid
1295
- });
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
- }
1357
-
1358
- export {
1359
- loadOrCreateIdentity,
1360
- logError,
1361
- startMcpServer,
1362
- startMcpHttpServer
1363
- };