@composer-app/mcp 0.0.1-beta.0 → 0.0.1-beta.1
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/{chunk-UPWB2QWR.js → chunk-PQWVQMLP.js} +213 -116
- package/dist/cli.js +26 -2
- package/dist/mcp.js +3 -1
- package/package.json +6 -2
|
@@ -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,6 +9,8 @@ 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";
|
|
@@ -15,6 +18,7 @@ import YProvider from "y-partyserver/provider";
|
|
|
15
18
|
import WebSocket from "ws";
|
|
16
19
|
|
|
17
20
|
// src/docReaders.ts
|
|
21
|
+
import { markdownFromYFragment } from "@composer-app/shared";
|
|
18
22
|
import * as Y from "yjs";
|
|
19
23
|
function isXmlElement(node) {
|
|
20
24
|
return node instanceof Y.XmlElement;
|
|
@@ -104,9 +108,7 @@ function renderBlockAsMarkdown(block) {
|
|
|
104
108
|
return text;
|
|
105
109
|
}
|
|
106
110
|
function serializeDocAsMarkdown(doc) {
|
|
107
|
-
|
|
108
|
-
if (blocks.length === 0) return "";
|
|
109
|
-
return blocks.map(renderBlockAsMarkdown).join("\n\n");
|
|
111
|
+
return markdownFromYFragment(doc.getXmlFragment("default"));
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
// src/anchors.ts
|
|
@@ -289,6 +291,64 @@ function resolveServerAnchor(doc, spec) {
|
|
|
289
291
|
};
|
|
290
292
|
}
|
|
291
293
|
|
|
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
|
+
|
|
292
352
|
// src/roomState.ts
|
|
293
353
|
var RoomState = class {
|
|
294
354
|
doc = new Y3.Doc();
|
|
@@ -326,8 +386,74 @@ var RoomState = class {
|
|
|
326
386
|
this.provider.awareness.setLocalStateField("user", {
|
|
327
387
|
name: opts.actingAs,
|
|
328
388
|
color: opts.identity.color,
|
|
329
|
-
userId: opts.identity.userId
|
|
389
|
+
userId: opts.identity.userId,
|
|
390
|
+
isAgent: true
|
|
330
391
|
});
|
|
392
|
+
this.installAwarenessHeartbeat();
|
|
393
|
+
this.installAwarenessDiagnostics();
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Re-broadcast the MCP's awareness every 15s.
|
|
397
|
+
*
|
|
398
|
+
* y-partyserver's provider disables the y-protocols awareness
|
|
399
|
+
* `_checkInterval` (see `clearInterval(awareness._checkInterval)` in
|
|
400
|
+
* `y-partyserver/dist/provider/index.js`), so the MCP sends its awareness
|
|
401
|
+
* exactly once — on connect — and never heartbeats after that. Combined
|
|
402
|
+
* with Cloudflare Durable Object hibernation (which evicts the server's
|
|
403
|
+
* in-memory `document.awareness` Map on wake), this means a browser that
|
|
404
|
+
* connects more than ~60s after the MCP sees an empty awareness dump in
|
|
405
|
+
* `onConnect` and never learns the agent is there. The user's own
|
|
406
|
+
* awareness flows the other direction fine (they send on connect, server
|
|
407
|
+
* broadcasts to the MCP), which is why the failure is asymmetric.
|
|
408
|
+
*
|
|
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.
|
|
412
|
+
*/
|
|
413
|
+
installAwarenessHeartbeat() {
|
|
414
|
+
const heartbeat = setInterval(() => {
|
|
415
|
+
const local = this.provider.awareness.getLocalState();
|
|
416
|
+
if (local !== null) {
|
|
417
|
+
this.provider.awareness.setLocalState(local);
|
|
418
|
+
}
|
|
419
|
+
}, 15e3);
|
|
420
|
+
heartbeat.unref?.();
|
|
421
|
+
}
|
|
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?.();
|
|
331
457
|
}
|
|
332
458
|
/**
|
|
333
459
|
* Resolves when the provider has completed its first sync handshake.
|
|
@@ -482,8 +608,8 @@ function hashState(doc) {
|
|
|
482
608
|
}
|
|
483
609
|
|
|
484
610
|
// src/identity.ts
|
|
485
|
-
import * as
|
|
486
|
-
import * as
|
|
611
|
+
import * as fs2 from "fs/promises";
|
|
612
|
+
import * as path2 from "path";
|
|
487
613
|
import { nanoid } from "nanoid";
|
|
488
614
|
var PALETTE = [
|
|
489
615
|
"#a855f7",
|
|
@@ -510,9 +636,9 @@ function isValidIdentity(value) {
|
|
|
510
636
|
return typeof v.userId === "string" && typeof v.color === "string";
|
|
511
637
|
}
|
|
512
638
|
async function loadOrCreateIdentity(dir) {
|
|
513
|
-
const filePath =
|
|
639
|
+
const filePath = path2.join(dir, FILE_NAME);
|
|
514
640
|
try {
|
|
515
|
-
const raw = await
|
|
641
|
+
const raw = await fs2.readFile(filePath, "utf8");
|
|
516
642
|
const parsed = JSON.parse(raw);
|
|
517
643
|
if (isValidIdentity(parsed)) {
|
|
518
644
|
return { userId: parsed.userId, color: parsed.color };
|
|
@@ -526,118 +652,19 @@ async function loadOrCreateIdentity(dir) {
|
|
|
526
652
|
userId: nanoid(),
|
|
527
653
|
color: pickColor()
|
|
528
654
|
};
|
|
529
|
-
await
|
|
530
|
-
await
|
|
655
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
656
|
+
await fs2.writeFile(filePath, JSON.stringify(identity, null, 2), {
|
|
531
657
|
mode: FILE_MODE
|
|
532
658
|
});
|
|
533
659
|
return identity;
|
|
534
660
|
}
|
|
535
661
|
|
|
536
662
|
// src/mdToFragment.ts
|
|
537
|
-
import {
|
|
538
|
-
import * as Y4 from "yjs";
|
|
663
|
+
import { writeMarkdownToYFragment } from "@composer-app/shared";
|
|
539
664
|
function writeMarkdownToFragment(fragment, markdown) {
|
|
540
|
-
|
|
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;
|
|
665
|
+
writeMarkdownToYFragment(fragment, markdown);
|
|
581
666
|
}
|
|
582
667
|
|
|
583
|
-
// src/logger.ts
|
|
584
|
-
import * as fs2 from "fs";
|
|
585
|
-
import * as path2 from "path";
|
|
586
|
-
import * as os from "os";
|
|
587
|
-
var COMPOSER_DIR = process.env.COMPOSER_CONFIG_DIR ?? path2.join(os.homedir(), ".composer");
|
|
588
|
-
var LOG_FILE = process.env.COMPOSER_LOG_FILE ?? path2.join(COMPOSER_DIR, "mcp.log");
|
|
589
|
-
var ensured = false;
|
|
590
|
-
function ensureDir() {
|
|
591
|
-
if (ensured) return;
|
|
592
|
-
try {
|
|
593
|
-
fs2.mkdirSync(path2.dirname(LOG_FILE), { recursive: true });
|
|
594
|
-
ensured = true;
|
|
595
|
-
} catch {
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
function write(line) {
|
|
599
|
-
ensureDir();
|
|
600
|
-
try {
|
|
601
|
-
fs2.appendFileSync(LOG_FILE, line);
|
|
602
|
-
} catch {
|
|
603
|
-
}
|
|
604
|
-
try {
|
|
605
|
-
process.stderr.write(line);
|
|
606
|
-
} catch {
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
function log(message, meta) {
|
|
610
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
611
|
-
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
612
|
-
write(`${ts} [composer-mcp] ${message}${metaStr}
|
|
613
|
-
`);
|
|
614
|
-
}
|
|
615
|
-
function logError(message, err) {
|
|
616
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
617
|
-
const detail = err instanceof Error ? `${err.name}: ${err.message}
|
|
618
|
-
${err.stack ?? ""}` : typeof err === "string" ? err : JSON.stringify(err);
|
|
619
|
-
write(`${ts} [composer-mcp] ERROR ${message}
|
|
620
|
-
${detail}
|
|
621
|
-
`);
|
|
622
|
-
}
|
|
623
|
-
var crashHandlersInstalled = false;
|
|
624
|
-
function installCrashHandlers() {
|
|
625
|
-
if (crashHandlersInstalled) return;
|
|
626
|
-
crashHandlersInstalled = true;
|
|
627
|
-
process.on("uncaughtException", (err) => {
|
|
628
|
-
logError("uncaughtException", err);
|
|
629
|
-
process.exit(1);
|
|
630
|
-
});
|
|
631
|
-
process.on("unhandledRejection", (reason) => {
|
|
632
|
-
logError("unhandledRejection", reason);
|
|
633
|
-
process.exit(1);
|
|
634
|
-
});
|
|
635
|
-
process.on("exit", (code) => {
|
|
636
|
-
log("process exit", { code });
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
var LOG_FILE_PATH = LOG_FILE;
|
|
640
|
-
|
|
641
668
|
// src/mcp.ts
|
|
642
669
|
var COMPOSER_DIR2 = process.env.COMPOSER_CONFIG_DIR ?? path3.join(os2.homedir(), ".composer");
|
|
643
670
|
var SERVER_HOST = process.env.COMPOSER_SERVER_HOST ?? "usecomposer.app";
|
|
@@ -1124,9 +1151,7 @@ async function dispatchTool(name, args) {
|
|
|
1124
1151
|
return errorResult(`unknown tool: ${name}`);
|
|
1125
1152
|
}
|
|
1126
1153
|
}
|
|
1127
|
-
|
|
1128
|
-
installCrashHandlers();
|
|
1129
|
-
log("mcp server starting", { pid: process.pid, node: process.version });
|
|
1154
|
+
function buildServer() {
|
|
1130
1155
|
const server = new Server(
|
|
1131
1156
|
{ name: "composer-mcp", version: "0.0.1" },
|
|
1132
1157
|
{ capabilities: { tools: {} } }
|
|
@@ -1160,18 +1185,90 @@ async function startMcpServer() {
|
|
|
1160
1185
|
return errorResult(message);
|
|
1161
1186
|
}
|
|
1162
1187
|
});
|
|
1188
|
+
return server;
|
|
1189
|
+
}
|
|
1190
|
+
async function startMcpServer() {
|
|
1191
|
+
installCrashHandlers();
|
|
1192
|
+
log("mcp server starting", {
|
|
1193
|
+
pid: process.pid,
|
|
1194
|
+
node: process.version,
|
|
1195
|
+
build: "awareness-heartbeat-v1"
|
|
1196
|
+
});
|
|
1197
|
+
const server = buildServer();
|
|
1163
1198
|
const transport = new StdioServerTransport();
|
|
1164
1199
|
await server.connect(transport);
|
|
1165
1200
|
log("mcp server connected", {
|
|
1201
|
+
transport: "stdio",
|
|
1166
1202
|
serverHost: SERVER_HOST,
|
|
1167
1203
|
appBase: APP_BASE,
|
|
1168
1204
|
logFile: LOG_FILE_PATH,
|
|
1169
1205
|
pid: process.pid
|
|
1170
1206
|
});
|
|
1171
1207
|
}
|
|
1208
|
+
async function startMcpHttpServer(opts) {
|
|
1209
|
+
installCrashHandlers();
|
|
1210
|
+
log("mcp http server starting", {
|
|
1211
|
+
port: opts.port,
|
|
1212
|
+
pid: process.pid,
|
|
1213
|
+
node: process.version,
|
|
1214
|
+
build: "awareness-heartbeat-v1"
|
|
1215
|
+
});
|
|
1216
|
+
const server = buildServer();
|
|
1217
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1218
|
+
sessionIdGenerator: () => randomUUID()
|
|
1219
|
+
});
|
|
1220
|
+
await server.connect(transport);
|
|
1221
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
1222
|
+
try {
|
|
1223
|
+
const url = req.url ?? "";
|
|
1224
|
+
if (url === "/mcp" || url.startsWith("/mcp?") || url.startsWith("/mcp/")) {
|
|
1225
|
+
await transport.handleRequest(req, res);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
if (url === "/health") {
|
|
1229
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1230
|
+
res.end(JSON.stringify({ ok: true, serverHost: SERVER_HOST }));
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1234
|
+
res.end("composer-mcp: POST /mcp");
|
|
1235
|
+
} catch (err) {
|
|
1236
|
+
logError("http handler error", err);
|
|
1237
|
+
if (!res.headersSent) {
|
|
1238
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1239
|
+
res.end("internal error");
|
|
1240
|
+
} else {
|
|
1241
|
+
res.end();
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
httpServer.listen(opts.port, "127.0.0.1", () => {
|
|
1246
|
+
const url = `http://127.0.0.1:${opts.port}/mcp`;
|
|
1247
|
+
log("mcp http server listening", {
|
|
1248
|
+
url,
|
|
1249
|
+
serverHost: SERVER_HOST,
|
|
1250
|
+
appBase: APP_BASE,
|
|
1251
|
+
logFile: LOG_FILE_PATH,
|
|
1252
|
+
pid: process.pid
|
|
1253
|
+
});
|
|
1254
|
+
console.error(
|
|
1255
|
+
`composer-mcp http listening on ${url}
|
|
1256
|
+
serverHost=${SERVER_HOST}
|
|
1257
|
+
appBase=${APP_BASE}`
|
|
1258
|
+
);
|
|
1259
|
+
});
|
|
1260
|
+
const shutdown = () => {
|
|
1261
|
+
log("mcp http server shutting down");
|
|
1262
|
+
httpServer.close(() => process.exit(0));
|
|
1263
|
+
setTimeout(() => process.exit(0), 500).unref();
|
|
1264
|
+
};
|
|
1265
|
+
process.on("SIGTERM", shutdown);
|
|
1266
|
+
process.on("SIGINT", shutdown);
|
|
1267
|
+
}
|
|
1172
1268
|
|
|
1173
1269
|
export {
|
|
1174
|
-
loadOrCreateIdentity,
|
|
1175
1270
|
logError,
|
|
1176
|
-
|
|
1271
|
+
loadOrCreateIdentity,
|
|
1272
|
+
startMcpServer,
|
|
1273
|
+
startMcpHttpServer
|
|
1177
1274
|
};
|
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-
|
|
7
|
+
} from "./chunk-PQWVQMLP.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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@composer-app/mcp",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.1",
|
|
4
4
|
"description": "Composer MCP",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Josh Philpott",
|
|
@@ -29,11 +29,14 @@
|
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "tsup src/cli.ts src/mcp.ts --format esm --target node20 --out-dir dist --clean",
|
|
31
31
|
"dev": "tsup src/cli.ts src/mcp.ts --format esm --target node20 --out-dir dist --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": {
|
|
38
|
+
"@composer-app/shared": "*",
|
|
35
39
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
36
|
-
"marked": "^14.1.0",
|
|
37
40
|
"nanoid": "^5.1.7",
|
|
38
41
|
"partysocket": "^1.1.16",
|
|
39
42
|
"ws": "^8.20.0",
|
|
@@ -44,6 +47,7 @@
|
|
|
44
47
|
"@types/node": "^24.12.0",
|
|
45
48
|
"@types/ws": "^8.18.1",
|
|
46
49
|
"tsup": "^8.3.0",
|
|
50
|
+
"tsx": "^4.21.0",
|
|
47
51
|
"typescript": "~5.9.3",
|
|
48
52
|
"vitest": "^4.1.3"
|
|
49
53
|
}
|