@dolusoft/claude-collab 1.9.1 → 1.9.3
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/cli.js +333 -131
- package/dist/cli.js.map +1 -1
- package/dist/mcp-main.js +333 -131
- package/dist/mcp-main.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp-main.js
CHANGED
|
@@ -297,8 +297,8 @@ var injectionQueue = new InjectionQueue();
|
|
|
297
297
|
|
|
298
298
|
// src/infrastructure/p2p/p2p-node.ts
|
|
299
299
|
var P2PNode = class {
|
|
300
|
-
constructor(
|
|
301
|
-
this.
|
|
300
|
+
constructor(portRange = [1e4, 19999]) {
|
|
301
|
+
this.portRange = portRange;
|
|
302
302
|
}
|
|
303
303
|
server = null;
|
|
304
304
|
myName = "";
|
|
@@ -314,14 +314,22 @@ var P2PNode = class {
|
|
|
314
314
|
sentQuestions = /* @__PURE__ */ new Map();
|
|
315
315
|
questionToSender = /* @__PURE__ */ new Map();
|
|
316
316
|
pendingHandlers = /* @__PURE__ */ new Set();
|
|
317
|
+
// Push-based answer resolution: questionId → resolve callback
|
|
318
|
+
answerWaiters = /* @__PURE__ */ new Map();
|
|
319
|
+
// Answers queued for offline peers: peerName → AnswerMsg (delivered on reconnect)
|
|
320
|
+
pendingOutboundAnswers = /* @__PURE__ */ new Map();
|
|
317
321
|
broadcaster = null;
|
|
318
322
|
stopPeerWatcher = null;
|
|
323
|
+
boundPort = 0;
|
|
319
324
|
// ---------------------------------------------------------------------------
|
|
320
325
|
// ICollabClient implementation
|
|
321
326
|
// ---------------------------------------------------------------------------
|
|
322
327
|
get isConnected() {
|
|
323
328
|
return this.running;
|
|
324
329
|
}
|
|
330
|
+
get port() {
|
|
331
|
+
return this.boundPort;
|
|
332
|
+
}
|
|
325
333
|
get currentTeamId() {
|
|
326
334
|
return this.myName || void 0;
|
|
327
335
|
}
|
|
@@ -335,7 +343,7 @@ var P2PNode = class {
|
|
|
335
343
|
teamName: name,
|
|
336
344
|
displayName,
|
|
337
345
|
status: "ONLINE",
|
|
338
|
-
port: this.
|
|
346
|
+
port: this.boundPort
|
|
339
347
|
};
|
|
340
348
|
}
|
|
341
349
|
async ask(toPeer, content, format) {
|
|
@@ -353,9 +361,27 @@ var P2PNode = class {
|
|
|
353
361
|
await ackPromise;
|
|
354
362
|
return questionId;
|
|
355
363
|
}
|
|
364
|
+
waitForAnswer(questionId, timeoutMs) {
|
|
365
|
+
const cached = this.receivedAnswers.get(questionId);
|
|
366
|
+
if (cached) {
|
|
367
|
+
return Promise.resolve(this.formatAnswer(questionId, cached));
|
|
368
|
+
}
|
|
369
|
+
return new Promise((resolve) => {
|
|
370
|
+
const timeout = setTimeout(() => {
|
|
371
|
+
this.answerWaiters.delete(questionId);
|
|
372
|
+
resolve(null);
|
|
373
|
+
}, timeoutMs);
|
|
374
|
+
this.answerWaiters.set(questionId, (result) => {
|
|
375
|
+
clearTimeout(timeout);
|
|
376
|
+
resolve(result);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
}
|
|
356
380
|
async checkAnswer(questionId) {
|
|
357
381
|
const cached = this.receivedAnswers.get(questionId);
|
|
358
|
-
|
|
382
|
+
return cached ? this.formatAnswer(questionId, cached) : null;
|
|
383
|
+
}
|
|
384
|
+
formatAnswer(questionId, cached) {
|
|
359
385
|
return {
|
|
360
386
|
questionId,
|
|
361
387
|
from: { displayName: `${cached.fromName} Claude`, teamName: cached.fromName },
|
|
@@ -372,16 +398,20 @@ var P2PNode = class {
|
|
|
372
398
|
question.answerFormat = format;
|
|
373
399
|
const senderName = this.questionToSender.get(questionId);
|
|
374
400
|
if (senderName) {
|
|
401
|
+
const answerMsg = {
|
|
402
|
+
type: "ANSWER",
|
|
403
|
+
from: this.myName,
|
|
404
|
+
questionId,
|
|
405
|
+
content,
|
|
406
|
+
format,
|
|
407
|
+
answeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
408
|
+
};
|
|
375
409
|
const ws = this.peerConnections.get(senderName);
|
|
376
410
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
377
|
-
this.sendToWs(ws,
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
content,
|
|
382
|
-
format,
|
|
383
|
-
answeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
384
|
-
});
|
|
411
|
+
this.sendToWs(ws, answerMsg);
|
|
412
|
+
} else {
|
|
413
|
+
this.pendingOutboundAnswers.set(senderName, answerMsg);
|
|
414
|
+
console.error(`[p2p] "${senderName}" is offline, answer queued for delivery on reconnect`);
|
|
385
415
|
}
|
|
386
416
|
}
|
|
387
417
|
injectionQueue.notifyReplied();
|
|
@@ -402,7 +432,7 @@ var P2PNode = class {
|
|
|
402
432
|
getInfo() {
|
|
403
433
|
return {
|
|
404
434
|
teamName: this.myName,
|
|
405
|
-
port: this.
|
|
435
|
+
port: this.boundPort,
|
|
406
436
|
connectedPeers: [...this.peerConnections.keys()]
|
|
407
437
|
};
|
|
408
438
|
}
|
|
@@ -445,47 +475,67 @@ var P2PNode = class {
|
|
|
445
475
|
// Private: server startup
|
|
446
476
|
// ---------------------------------------------------------------------------
|
|
447
477
|
startServer() {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
} catch {
|
|
465
|
-
}
|
|
478
|
+
const [min, max] = this.portRange;
|
|
479
|
+
const pick = () => Math.floor(Math.random() * (max - min + 1)) + min;
|
|
480
|
+
const tryBind = (attemptsLeft) => {
|
|
481
|
+
if (attemptsLeft === 0) {
|
|
482
|
+
return Promise.reject(new Error(`No free port found in range ${min}-${max}`));
|
|
483
|
+
}
|
|
484
|
+
const port = pick();
|
|
485
|
+
return new Promise((resolve, reject) => {
|
|
486
|
+
const wss = new WebSocketServer({ port });
|
|
487
|
+
wss.once("listening", () => {
|
|
488
|
+
this.server = wss;
|
|
489
|
+
this.boundPort = port;
|
|
490
|
+
this.running = true;
|
|
491
|
+
console.error(`[p2p] listening on port ${port} as "${this.myName}"`);
|
|
492
|
+
this.attachServerHandlers(wss);
|
|
493
|
+
resolve();
|
|
466
494
|
});
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
console.error(`[p2p] peer disconnected (inbound): ${name}`);
|
|
474
|
-
}
|
|
495
|
+
wss.once("error", (err) => {
|
|
496
|
+
wss.close();
|
|
497
|
+
if (err.code === "EADDRINUSE") {
|
|
498
|
+
tryBind(attemptsLeft - 1).then(resolve, reject);
|
|
499
|
+
} else {
|
|
500
|
+
reject(err);
|
|
475
501
|
}
|
|
476
502
|
});
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
return tryBind(20);
|
|
506
|
+
}
|
|
507
|
+
attachServerHandlers(wss) {
|
|
508
|
+
wss.on("connection", (ws) => {
|
|
509
|
+
ws.on("message", (data) => {
|
|
510
|
+
try {
|
|
511
|
+
this.handleMessage(ws, parse(data.toString()));
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
ws.on("close", () => {
|
|
516
|
+
const name = this.wsToName.get(ws);
|
|
517
|
+
if (name) {
|
|
518
|
+
this.wsToName.delete(ws);
|
|
519
|
+
if (this.peerConnections.get(name) === ws) {
|
|
520
|
+
this.peerConnections.delete(name);
|
|
521
|
+
console.error(`[p2p] peer disconnected (inbound): ${name}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
ws.on("error", (err) => {
|
|
526
|
+
console.error("[p2p] inbound ws error:", err.message);
|
|
480
527
|
});
|
|
481
528
|
});
|
|
529
|
+
wss.on("error", (err) => {
|
|
530
|
+
console.error("[p2p] server error:", err.message);
|
|
531
|
+
});
|
|
482
532
|
}
|
|
483
533
|
// ---------------------------------------------------------------------------
|
|
484
534
|
// Private: discovery + outbound connections
|
|
485
535
|
// ---------------------------------------------------------------------------
|
|
486
536
|
startDiscovery() {
|
|
487
537
|
this.broadcaster = new PeerBroadcaster();
|
|
488
|
-
this.broadcaster.start(this.myName, this.
|
|
538
|
+
this.broadcaster.start(this.myName, this.boundPort);
|
|
489
539
|
this.stopPeerWatcher = watchForPeer((peer) => {
|
|
490
540
|
if (peer.name === this.myName) return;
|
|
491
541
|
if (this.peerConnections.has(peer.name)) return;
|
|
@@ -537,6 +587,7 @@ var P2PNode = class {
|
|
|
537
587
|
this.connectingPeers.delete(msg.name);
|
|
538
588
|
this.sendToWs(ws, { type: "HELLO_ACK", name: this.myName });
|
|
539
589
|
console.error(`[p2p] peer joined (inbound): ${msg.name}`);
|
|
590
|
+
this.deliverPendingAnswer(msg.name, ws);
|
|
540
591
|
break;
|
|
541
592
|
}
|
|
542
593
|
case "HELLO_ACK": {
|
|
@@ -548,6 +599,7 @@ var P2PNode = class {
|
|
|
548
599
|
this.wsToName.set(ws, msg.name);
|
|
549
600
|
this.connectingPeers.delete(msg.name);
|
|
550
601
|
console.error(`[p2p] connected to peer: ${msg.name}`);
|
|
602
|
+
this.deliverPendingAnswer(msg.name, ws);
|
|
551
603
|
break;
|
|
552
604
|
}
|
|
553
605
|
case "ASK":
|
|
@@ -555,12 +607,18 @@ var P2PNode = class {
|
|
|
555
607
|
break;
|
|
556
608
|
case "ANSWER":
|
|
557
609
|
if (!this.receivedAnswers.has(msg.questionId)) {
|
|
558
|
-
|
|
610
|
+
const record = {
|
|
559
611
|
content: msg.content,
|
|
560
612
|
format: msg.format,
|
|
561
613
|
answeredAt: msg.answeredAt,
|
|
562
614
|
fromName: msg.from
|
|
563
|
-
}
|
|
615
|
+
};
|
|
616
|
+
this.receivedAnswers.set(msg.questionId, record);
|
|
617
|
+
const waiter = this.answerWaiters.get(msg.questionId);
|
|
618
|
+
if (waiter) {
|
|
619
|
+
this.answerWaiters.delete(msg.questionId);
|
|
620
|
+
waiter(this.formatAnswer(msg.questionId, record));
|
|
621
|
+
}
|
|
564
622
|
}
|
|
565
623
|
break;
|
|
566
624
|
}
|
|
@@ -586,6 +644,14 @@ var P2PNode = class {
|
|
|
586
644
|
ageMs: 0
|
|
587
645
|
});
|
|
588
646
|
}
|
|
647
|
+
deliverPendingAnswer(peerName, ws) {
|
|
648
|
+
const pending = this.pendingOutboundAnswers.get(peerName);
|
|
649
|
+
if (pending) {
|
|
650
|
+
this.pendingOutboundAnswers.delete(peerName);
|
|
651
|
+
this.sendToWs(ws, pending);
|
|
652
|
+
console.error(`[p2p] delivered queued answer to "${peerName}" after reconnect`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
589
655
|
sendToWs(ws, msg) {
|
|
590
656
|
if (ws.readyState === WebSocket.OPEN) {
|
|
591
657
|
ws.send(serialize(msg));
|
|
@@ -608,110 +674,144 @@ var P2PNode = class {
|
|
|
608
674
|
});
|
|
609
675
|
}
|
|
610
676
|
};
|
|
677
|
+
var ASK_DESCRIPTION = `Send a question to another Claude instance on the LAN and wait for their answer.
|
|
678
|
+
|
|
679
|
+
WHEN TO USE:
|
|
680
|
+
- You need input, a decision, or work output from a specific peer Claude
|
|
681
|
+
- You want to delegate a subtask to another Claude and use their result
|
|
682
|
+
- You need to coordinate or synchronize work across multiple Claudes
|
|
683
|
+
|
|
684
|
+
WORKFLOW:
|
|
685
|
+
1. Call peers() first to confirm the target is online and get their exact name
|
|
686
|
+
2. Call ask(peer, question) \u2014 this blocks until they reply (up to 5 minutes)
|
|
687
|
+
3. Use their answer directly in your ongoing task
|
|
688
|
+
|
|
689
|
+
WRITING GOOD QUESTIONS:
|
|
690
|
+
- Include all context the other Claude needs \u2014 they cannot see your conversation
|
|
691
|
+
- Be specific about what format or level of detail you expect in the answer
|
|
692
|
+
- If you need code, specify language and constraints
|
|
693
|
+
- One focused question per call works better than multiple combined
|
|
694
|
+
|
|
695
|
+
DO NOT use this tool if:
|
|
696
|
+
- The peer is not listed in peers() \u2014 the call will fail immediately
|
|
697
|
+
- You just want to share information without needing a response (there is no broadcast tool yet)`;
|
|
611
698
|
var askSchema = {
|
|
612
|
-
peer: z.string().describe(
|
|
613
|
-
question: z.string().describe(
|
|
699
|
+
peer: z.string().describe("Exact name of the peer to ask. Use peers() first to see who is online and get the correct name."),
|
|
700
|
+
question: z.string().describe(
|
|
701
|
+
"Your question in markdown. Include all necessary context \u2014 the other Claude cannot see your conversation history. Be specific about what kind of answer you need."
|
|
702
|
+
)
|
|
614
703
|
};
|
|
615
704
|
function registerAskTool(server, client) {
|
|
616
|
-
server.tool("ask", askSchema, async (args) => {
|
|
705
|
+
server.tool("ask", ASK_DESCRIPTION, askSchema, async (args) => {
|
|
617
706
|
const targetPeer = args.peer;
|
|
618
707
|
const question = args.question;
|
|
619
708
|
try {
|
|
620
709
|
if (!client.currentTeamId) {
|
|
621
710
|
return {
|
|
622
|
-
content: [
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
627
|
-
],
|
|
711
|
+
content: [{
|
|
712
|
+
type: "text",
|
|
713
|
+
text: "P2P node is not ready yet. Wait a moment and try again."
|
|
714
|
+
}],
|
|
628
715
|
isError: true
|
|
629
716
|
};
|
|
630
717
|
}
|
|
631
718
|
const questionId = await client.ask(targetPeer, question, "markdown");
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (answer !== null) {
|
|
639
|
-
return {
|
|
640
|
-
content: [
|
|
641
|
-
{
|
|
642
|
-
type: "text",
|
|
643
|
-
text: `**${answer.from.displayName} (${answer.from.teamName}) cevaplad\u0131:**
|
|
719
|
+
const answer = await client.waitForAnswer(questionId, 5 * 60 * 1e3);
|
|
720
|
+
if (answer !== null) {
|
|
721
|
+
return {
|
|
722
|
+
content: [{
|
|
723
|
+
type: "text",
|
|
724
|
+
text: `**${answer.from.displayName} answered:**
|
|
644
725
|
|
|
645
726
|
${answer.content}`
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
};
|
|
649
|
-
}
|
|
727
|
+
}]
|
|
728
|
+
};
|
|
650
729
|
}
|
|
651
730
|
return {
|
|
652
|
-
content: [
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
Question ID: \`${questionId}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
731
|
+
content: [{
|
|
732
|
+
type: "text",
|
|
733
|
+
text: [
|
|
734
|
+
`Question sent to "${targetPeer}" but no answer arrived within 5 minutes.`,
|
|
735
|
+
`Question ID: \`${questionId}\``,
|
|
736
|
+
``,
|
|
737
|
+
`The peer may be busy or offline. You can:`,
|
|
738
|
+
`- Call peers() to check if they are still connected`,
|
|
739
|
+
`- Continue with your task and follow up later`
|
|
740
|
+
].join("\n")
|
|
741
|
+
}]
|
|
661
742
|
};
|
|
662
743
|
} catch (error) {
|
|
663
744
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
745
|
+
const isPeerOffline = errorMessage.includes("not connected");
|
|
664
746
|
return {
|
|
665
|
-
content: [
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
}
|
|
670
|
-
],
|
|
747
|
+
content: [{
|
|
748
|
+
type: "text",
|
|
749
|
+
text: isPeerOffline ? `Peer "${targetPeer}" is not connected. Call peers() to see who is currently online.` : `Failed to send question: ${errorMessage}`
|
|
750
|
+
}],
|
|
671
751
|
isError: true
|
|
672
752
|
};
|
|
673
753
|
}
|
|
674
754
|
});
|
|
675
755
|
}
|
|
756
|
+
var REPLY_DESCRIPTION = `Send your answer back to a Claude instance that asked you a question.
|
|
757
|
+
|
|
758
|
+
WHEN TO USE:
|
|
759
|
+
- A question has been injected into your terminal by another Claude (you will see it appear automatically)
|
|
760
|
+
- You have finished thinking through the answer and are ready to respond
|
|
761
|
+
|
|
762
|
+
WORKFLOW:
|
|
763
|
+
1. Read the injected question carefully \u2014 it includes a questionId at the top
|
|
764
|
+
2. Think through a complete answer
|
|
765
|
+
3. Call reply(questionId, answer) \u2014 your answer is sent directly back to the asking Claude
|
|
766
|
+
4. The asking Claude's ask() call unblocks and they receive your answer immediately
|
|
767
|
+
|
|
768
|
+
WRITING GOOD ANSWERS:
|
|
769
|
+
- Be thorough \u2014 the asking Claude will use your answer directly in their task
|
|
770
|
+
- Use markdown for code blocks, lists, and structure
|
|
771
|
+
- Include any caveats, assumptions, or follow-up suggestions that would help them
|
|
772
|
+
- If you cannot answer fully, say so clearly and explain why
|
|
773
|
+
|
|
774
|
+
IMPORTANT:
|
|
775
|
+
- Each question can only be replied to once
|
|
776
|
+
- The questionId is a UUID shown in the injected question prompt \u2014 copy it exactly`;
|
|
676
777
|
var replySchema = {
|
|
677
|
-
questionId: z.string().describe(
|
|
678
|
-
|
|
778
|
+
questionId: z.string().describe(
|
|
779
|
+
"The UUID of the question to reply to. Shown at the top of the injected question prompt in your terminal."
|
|
780
|
+
),
|
|
781
|
+
answer: z.string().describe(
|
|
782
|
+
"Your complete answer in markdown. Be thorough \u2014 the asking Claude is waiting and will use your response directly in their work."
|
|
783
|
+
)
|
|
679
784
|
};
|
|
680
785
|
function registerReplyTool(server, client) {
|
|
681
|
-
server.tool("reply", replySchema, async (args) => {
|
|
786
|
+
server.tool("reply", REPLY_DESCRIPTION, replySchema, async (args) => {
|
|
682
787
|
const questionId = args.questionId;
|
|
683
788
|
const answer = args.answer;
|
|
684
789
|
try {
|
|
685
790
|
if (!client.currentTeamId) {
|
|
686
791
|
return {
|
|
687
|
-
content: [
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
692
|
-
],
|
|
792
|
+
content: [{
|
|
793
|
+
type: "text",
|
|
794
|
+
text: "P2P node is not ready yet. Wait a moment and try again."
|
|
795
|
+
}],
|
|
693
796
|
isError: true
|
|
694
797
|
};
|
|
695
798
|
}
|
|
696
799
|
await client.reply(questionId, answer, "markdown");
|
|
697
800
|
injectionQueue.notifyReplied();
|
|
698
801
|
return {
|
|
699
|
-
content: [
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
]
|
|
802
|
+
content: [{
|
|
803
|
+
type: "text",
|
|
804
|
+
text: `Answer sent. The peer's ask() call has been unblocked and they received your response.`
|
|
805
|
+
}]
|
|
705
806
|
};
|
|
706
807
|
} catch (error) {
|
|
707
808
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
809
|
+
const isNotFound = errorMessage.includes("not found");
|
|
708
810
|
return {
|
|
709
|
-
content: [
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
],
|
|
811
|
+
content: [{
|
|
812
|
+
type: "text",
|
|
813
|
+
text: isNotFound ? `Question \`${questionId}\` not found. Check that you copied the questionId exactly from the injected prompt.` : `Failed to send reply: ${errorMessage}`
|
|
814
|
+
}],
|
|
715
815
|
isError: true
|
|
716
816
|
};
|
|
717
817
|
}
|
|
@@ -719,18 +819,51 @@ function registerReplyTool(server, client) {
|
|
|
719
819
|
}
|
|
720
820
|
|
|
721
821
|
// src/presentation/mcp/tools/peers.tool.ts
|
|
822
|
+
var PEERS_DESCRIPTION = `Show your identity on the P2P network and list all currently connected peers.
|
|
823
|
+
|
|
824
|
+
WHEN TO USE:
|
|
825
|
+
- Before calling ask() \u2014 to confirm the target peer is online and get their exact name
|
|
826
|
+
- After startup \u2014 to verify you joined the network successfully
|
|
827
|
+
- When a peer seems unreachable \u2014 to check if they are still connected
|
|
828
|
+
|
|
829
|
+
WHAT IT SHOWS:
|
|
830
|
+
- Your own name and the port you are listening on
|
|
831
|
+
- All peers who have established a direct connection to you
|
|
832
|
+
|
|
833
|
+
IF NO PEERS ARE LISTED:
|
|
834
|
+
- Peers discover each other automatically via LAN broadcast every 3 seconds
|
|
835
|
+
- If a peer just started, wait a few seconds and call peers() again
|
|
836
|
+
- Call firewall_open() so peers on other machines can connect inbound to you
|
|
837
|
+
- If still no peers, verify all machines are on the same LAN/subnet`;
|
|
722
838
|
function registerPeersTool(server, client) {
|
|
723
|
-
server.tool("peers", {}, async () => {
|
|
839
|
+
server.tool("peers", PEERS_DESCRIPTION, {}, async () => {
|
|
724
840
|
const info = client.getInfo();
|
|
725
841
|
const myName = info.teamName ?? "(starting...)";
|
|
726
842
|
const myPort = info.port ?? "?";
|
|
727
843
|
const connected = info.connectedPeers;
|
|
844
|
+
if (!client.isConnected) {
|
|
845
|
+
return {
|
|
846
|
+
content: [{
|
|
847
|
+
type: "text",
|
|
848
|
+
text: [
|
|
849
|
+
`P2P server is not running yet (port ${myPort} may be in use).`,
|
|
850
|
+
`Check the MCP process logs for the error details.`
|
|
851
|
+
].join("\n")
|
|
852
|
+
}],
|
|
853
|
+
isError: true
|
|
854
|
+
};
|
|
855
|
+
}
|
|
728
856
|
if (connected.length === 0) {
|
|
729
857
|
return {
|
|
730
858
|
content: [{
|
|
731
859
|
type: "text",
|
|
732
|
-
text:
|
|
733
|
-
|
|
860
|
+
text: [
|
|
861
|
+
`You are "${myName}" (listening on port ${myPort}).`,
|
|
862
|
+
`No peers connected yet.`,
|
|
863
|
+
``,
|
|
864
|
+
`Peers auto-discover via LAN broadcast \u2014 wait a few seconds if they just started.`,
|
|
865
|
+
`Call firewall_open() if peers on other machines cannot reach you.`
|
|
866
|
+
].join("\n")
|
|
734
867
|
}]
|
|
735
868
|
};
|
|
736
869
|
}
|
|
@@ -746,14 +879,27 @@ ${list}`
|
|
|
746
879
|
}
|
|
747
880
|
|
|
748
881
|
// src/presentation/mcp/tools/history.tool.ts
|
|
882
|
+
var HISTORY_DESCRIPTION = `Show all questions and answers exchanged this session \u2014 both sent and received.
|
|
883
|
+
|
|
884
|
+
WHEN TO USE:
|
|
885
|
+
- To review what was already discussed before asking a follow-up question
|
|
886
|
+
- To check if a previously sent question has been answered yet
|
|
887
|
+
- To find a questionId you need to reference
|
|
888
|
+
- To catch up on received questions you may have missed
|
|
889
|
+
|
|
890
|
+
OUTPUT FORMAT:
|
|
891
|
+
- \u2192 peer: question you sent, with their answer below
|
|
892
|
+
- \u2190 peer: question they sent you, with your reply below
|
|
893
|
+
- (no answer yet) means the question is still waiting for a response`;
|
|
749
894
|
function registerHistoryTool(server, client) {
|
|
750
|
-
server.tool("history", {}, async () => {
|
|
895
|
+
server.tool("history", HISTORY_DESCRIPTION, {}, async () => {
|
|
751
896
|
const entries = client.getHistory();
|
|
752
897
|
if (entries.length === 0) {
|
|
753
898
|
return {
|
|
754
|
-
content: [{ type: "text", text: "No questions yet this session." }]
|
|
899
|
+
content: [{ type: "text", text: "No questions exchanged yet this session." }]
|
|
755
900
|
};
|
|
756
901
|
}
|
|
902
|
+
const unanswered = entries.filter((e) => !e.answer);
|
|
757
903
|
const lines = entries.map((e) => {
|
|
758
904
|
const time = new Date(e.askedAt).toLocaleTimeString();
|
|
759
905
|
if (e.direction === "sent") {
|
|
@@ -761,13 +907,16 @@ function registerHistoryTool(server, client) {
|
|
|
761
907
|
return `[${time}] \u2192 ${e.peer}: ${e.question}
|
|
762
908
|
${answerLine}`;
|
|
763
909
|
} else {
|
|
764
|
-
const answerLine = e.answer ? ` \u21B3 you: ${e.answer}` : ` \u21B3 (not replied yet)`;
|
|
910
|
+
const answerLine = e.answer ? ` \u21B3 you: ${e.answer}` : ` \u21B3 (not replied yet \u2014 use reply() if you haven't answered)`;
|
|
765
911
|
return `[${time}] \u2190 ${e.peer}: ${e.question}
|
|
766
912
|
${answerLine}`;
|
|
767
913
|
}
|
|
768
914
|
});
|
|
915
|
+
const summary = unanswered.length > 0 ? `
|
|
916
|
+
|
|
917
|
+
\u26A0 ${unanswered.length} question(s) still waiting for a response.` : "";
|
|
769
918
|
return {
|
|
770
|
-
content: [{ type: "text", text: lines.join("\n\n") }]
|
|
919
|
+
content: [{ type: "text", text: lines.join("\n\n") + summary }]
|
|
771
920
|
};
|
|
772
921
|
});
|
|
773
922
|
}
|
|
@@ -828,72 +977,123 @@ async function removeFirewallRule(port) {
|
|
|
828
977
|
}
|
|
829
978
|
|
|
830
979
|
// src/presentation/mcp/tools/firewall-open.tool.ts
|
|
980
|
+
var FIREWALL_OPEN_DESCRIPTION = `Open a Windows Firewall inbound rule so peers on the LAN can connect directly to you.
|
|
981
|
+
|
|
982
|
+
WHEN YOU NEED THIS:
|
|
983
|
+
- Other peers cannot see you in their peers() list even though you are on the same LAN
|
|
984
|
+
- You just started and want to make sure all peers can reach you
|
|
985
|
+
- A peer explicitly says they cannot connect to you
|
|
986
|
+
|
|
987
|
+
WHEN YOU DO NOT NEED THIS:
|
|
988
|
+
- You can already see peers in peers() \u2014 the connection is working
|
|
989
|
+
- You are connecting outbound to others (outbound connections do not require firewall rules)
|
|
990
|
+
|
|
991
|
+
WHAT HAPPENS:
|
|
992
|
+
- On most systems: the rule is applied immediately (terminal is already elevated)
|
|
993
|
+
- On standard Windows: a UAC popup appears \u2014 the user must click Yes
|
|
994
|
+
- On VMs or headless setups: applied directly if running as Administrator
|
|
995
|
+
|
|
996
|
+
The rule is named "claude-collab-{port}" and allows inbound TCP on your current listen port.
|
|
997
|
+
Call firewall_close() when you are done to clean up the rule.`;
|
|
831
998
|
function registerFirewallOpenTool(server, client) {
|
|
832
999
|
server.tool(
|
|
833
1000
|
"firewall_open",
|
|
834
|
-
|
|
1001
|
+
FIREWALL_OPEN_DESCRIPTION,
|
|
835
1002
|
{
|
|
836
|
-
port: z.number().min(1024).max(65535).optional().describe(
|
|
1003
|
+
port: z.number().min(1024).max(65535).optional().describe(
|
|
1004
|
+
"Port to open. Defaults to your current listen port \u2014 omit this unless you have a specific reason to override."
|
|
1005
|
+
)
|
|
837
1006
|
},
|
|
838
1007
|
async ({ port }) => {
|
|
839
1008
|
const targetPort = port ?? client.getInfo().port;
|
|
840
1009
|
if (!targetPort) {
|
|
841
1010
|
return {
|
|
842
|
-
content: [{ type: "text", text: "
|
|
1011
|
+
content: [{ type: "text", text: "P2P node is not running yet \u2014 port unknown. Try again in a moment." }],
|
|
843
1012
|
isError: true
|
|
844
1013
|
};
|
|
845
1014
|
}
|
|
846
1015
|
try {
|
|
847
1016
|
const { method } = await addFirewallRule(targetPort);
|
|
848
|
-
const how = method === "direct" ? "applied directly (
|
|
1017
|
+
const how = method === "direct" ? "applied directly (already elevated)" : "applied via UAC popup";
|
|
849
1018
|
return {
|
|
850
1019
|
content: [{
|
|
851
1020
|
type: "text",
|
|
852
1021
|
text: [
|
|
853
|
-
`Firewall rule opened for port ${targetPort} (
|
|
854
|
-
`
|
|
855
|
-
`
|
|
1022
|
+
`Firewall rule opened for port ${targetPort} (claude-collab-${targetPort}). ${how}.`,
|
|
1023
|
+
`Peers on the LAN can now connect to you inbound.`,
|
|
1024
|
+
`Call firewall_close() when you are done with this session.`
|
|
856
1025
|
].join("\n")
|
|
857
1026
|
}]
|
|
858
1027
|
};
|
|
859
1028
|
} catch (err) {
|
|
860
1029
|
const msg = err instanceof Error ? err.message : String(err);
|
|
861
1030
|
return {
|
|
862
|
-
content: [{
|
|
1031
|
+
content: [{
|
|
1032
|
+
type: "text",
|
|
1033
|
+
text: [
|
|
1034
|
+
`Failed to open firewall: ${msg}`,
|
|
1035
|
+
``,
|
|
1036
|
+
`Try running your terminal as Administrator and call firewall_open() again.`
|
|
1037
|
+
].join("\n")
|
|
1038
|
+
}],
|
|
863
1039
|
isError: true
|
|
864
1040
|
};
|
|
865
1041
|
}
|
|
866
1042
|
}
|
|
867
1043
|
);
|
|
868
1044
|
}
|
|
1045
|
+
var FIREWALL_CLOSE_DESCRIPTION = `Remove the Windows Firewall inbound rule that was opened by firewall_open().
|
|
1046
|
+
|
|
1047
|
+
WHEN TO USE:
|
|
1048
|
+
- At the end of a collaboration session to clean up
|
|
1049
|
+
- When you no longer want to accept inbound peer connections
|
|
1050
|
+
- As general hygiene \u2014 open firewall rules should not be left indefinitely
|
|
1051
|
+
|
|
1052
|
+
WHAT HAPPENS:
|
|
1053
|
+
- The inbound TCP rule "claude-collab-{port}" is deleted from Windows Firewall
|
|
1054
|
+
- Existing active connections are not dropped \u2014 only new inbound connections are blocked
|
|
1055
|
+
- On standard Windows: a UAC popup appears \u2014 the user must click Yes
|
|
1056
|
+
- On VMs or admin terminals: applied directly without a popup
|
|
1057
|
+
|
|
1058
|
+
NOTE: Because ports are random each session, each startup creates a new rule name.
|
|
1059
|
+
Old rules from previous sessions can be removed by passing the port explicitly.`;
|
|
869
1060
|
function registerFirewallCloseTool(server, client) {
|
|
870
1061
|
server.tool(
|
|
871
1062
|
"firewall_close",
|
|
872
|
-
|
|
1063
|
+
FIREWALL_CLOSE_DESCRIPTION,
|
|
873
1064
|
{
|
|
874
|
-
port: z.number().min(1024).max(65535).optional().describe(
|
|
1065
|
+
port: z.number().min(1024).max(65535).optional().describe(
|
|
1066
|
+
"Port whose rule to remove. Defaults to your current listen port. Pass a specific port to clean up a rule from a previous session."
|
|
1067
|
+
)
|
|
875
1068
|
},
|
|
876
1069
|
async ({ port }) => {
|
|
877
1070
|
const targetPort = port ?? client.getInfo().port;
|
|
878
1071
|
if (!targetPort) {
|
|
879
1072
|
return {
|
|
880
|
-
content: [{ type: "text", text: "
|
|
1073
|
+
content: [{ type: "text", text: "P2P node is not running yet \u2014 port unknown. Try again in a moment." }],
|
|
881
1074
|
isError: true
|
|
882
1075
|
};
|
|
883
1076
|
}
|
|
884
1077
|
try {
|
|
885
1078
|
const { method } = await removeFirewallRule(targetPort);
|
|
886
|
-
const how = method === "direct" ? "applied directly (
|
|
1079
|
+
const how = method === "direct" ? "applied directly (already elevated)" : "applied via UAC popup";
|
|
887
1080
|
return {
|
|
888
1081
|
content: [{
|
|
889
1082
|
type: "text",
|
|
890
|
-
text: `Firewall rule removed for port ${targetPort} (
|
|
1083
|
+
text: `Firewall rule removed for port ${targetPort} (claude-collab-${targetPort}). ${how}.`
|
|
891
1084
|
}]
|
|
892
1085
|
};
|
|
893
1086
|
} catch (err) {
|
|
894
1087
|
const msg = err instanceof Error ? err.message : String(err);
|
|
895
1088
|
return {
|
|
896
|
-
content: [{
|
|
1089
|
+
content: [{
|
|
1090
|
+
type: "text",
|
|
1091
|
+
text: [
|
|
1092
|
+
`Failed to remove firewall rule: ${msg}`,
|
|
1093
|
+
``,
|
|
1094
|
+
`The rule may not exist (if firewall_open was never called this session), or try running as Administrator.`
|
|
1095
|
+
].join("\n")
|
|
1096
|
+
}],
|
|
897
1097
|
isError: true
|
|
898
1098
|
};
|
|
899
1099
|
}
|
|
@@ -923,7 +1123,6 @@ async function startMcpServer(options) {
|
|
|
923
1123
|
}
|
|
924
1124
|
|
|
925
1125
|
// src/mcp-main.ts
|
|
926
|
-
var P2P_PORT = 9999;
|
|
927
1126
|
function getArg(flag) {
|
|
928
1127
|
const idx = process.argv.indexOf(flag);
|
|
929
1128
|
return idx !== -1 ? process.argv[idx + 1] : void 0;
|
|
@@ -934,9 +1133,12 @@ async function main() {
|
|
|
934
1133
|
console.error("--name is required");
|
|
935
1134
|
process.exit(1);
|
|
936
1135
|
}
|
|
937
|
-
const node = new P2PNode(
|
|
938
|
-
|
|
939
|
-
|
|
1136
|
+
const node = new P2PNode();
|
|
1137
|
+
const mcpReady = startMcpServer({ client: node });
|
|
1138
|
+
node.join(name, name).catch((err) => {
|
|
1139
|
+
console.error(`[mcp-main] P2P server failed to start: ${err.message}`);
|
|
1140
|
+
});
|
|
1141
|
+
await mcpReady;
|
|
940
1142
|
}
|
|
941
1143
|
main().catch((error) => {
|
|
942
1144
|
console.error("Unexpected error:", error);
|