@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/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(port) {
301
- this.port = port;
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.port
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
- if (!cached) return null;
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
- type: "ANSWER",
379
- from: this.myName,
380
- questionId,
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.port,
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
- return new Promise((resolve, reject) => {
449
- const wss = new WebSocketServer({ port: this.port });
450
- this.server = wss;
451
- wss.on("listening", () => {
452
- this.running = true;
453
- console.error(`[p2p] listening on port ${this.port} as "${this.myName}"`);
454
- resolve();
455
- });
456
- wss.on("error", (err) => {
457
- if (!this.running) reject(err);
458
- else console.error("[p2p] server error:", err.message);
459
- });
460
- wss.on("connection", (ws) => {
461
- ws.on("message", (data) => {
462
- try {
463
- this.handleMessage(ws, parse(data.toString()));
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
- ws.on("close", () => {
468
- const name = this.wsToName.get(ws);
469
- if (name) {
470
- this.wsToName.delete(ws);
471
- if (this.peerConnections.get(name) === ws) {
472
- this.peerConnections.delete(name);
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
- ws.on("error", (err) => {
478
- console.error("[p2p] inbound ws error:", err.message);
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.port);
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
- this.receivedAnswers.set(msg.questionId, {
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('Name of the peer to ask (e.g., "alice", "backend")'),
613
- question: z.string().describe("The question to ask (supports markdown)")
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
- type: "text",
625
- text: "Node is not ready yet. Wait a moment and try again."
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 POLL_INTERVAL_MS = 5e3;
633
- const MAX_WAIT_MS = 5 * 60 * 1e3;
634
- const deadline = Date.now() + MAX_WAIT_MS;
635
- while (Date.now() < deadline) {
636
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
637
- const answer = await client.checkAnswer(questionId);
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
- type: "text",
655
- text: `Soru g\xF6nderildi ancak 5 dakika i\xE7inde cevap gelmedi.
656
- Question ID: \`${questionId}\`
657
-
658
- Manuel kontrol i\xE7in "check_answer" tool'unu kullanabilirsin.`
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
- type: "text",
668
- text: `Failed to send question: ${errorMessage}`
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("The ID of the question to reply to (from inbox)"),
678
- answer: z.string().describe("Your answer to the question (supports markdown)")
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
- type: "text",
690
- text: 'You must join a team first. Use the "join" tool to join a team.'
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
- type: "text",
702
- text: `Reply sent successfully to question \`${questionId}\`.`
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
- type: "text",
712
- text: `Failed to send reply: ${errorMessage}`
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: `You are "${myName}" (listening on port ${myPort}). No peers connected yet.
733
- Use firewall_open to allow inbound connections, or wait for peers to connect to you.`
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
- "Open a Windows Firewall inbound rule for your P2P listen port. A UAC popup will appear \u2014 accept it to allow peers to connect to you directly.",
1001
+ FIREWALL_OPEN_DESCRIPTION,
835
1002
  {
836
- port: z.number().min(1024).max(65535).optional().describe("Port to open (defaults to your current listen port)")
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: "Could not determine port. Pass port explicitly." }],
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 (process already elevated)" : "applied via UAC elevation popup";
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} (rule name: claude-collab-${targetPort}).`,
854
- `Method: ${how}.`,
855
- `Peers on the LAN can now connect to you directly.`
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: [{ type: "text", text: `Failed to open firewall: ${msg}` }],
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
- "Remove the Windows Firewall inbound rule for your P2P listen port. A UAC popup will appear \u2014 accept it to close the rule.",
1063
+ FIREWALL_CLOSE_DESCRIPTION,
873
1064
  {
874
- port: z.number().min(1024).max(65535).optional().describe("Port to close (defaults to your current listen port)")
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: "Could not determine port. Pass port explicitly." }],
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 (process already elevated)" : "applied via UAC elevation popup";
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} (rule name: claude-collab-${targetPort}). Method: ${how}.`
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: [{ type: "text", text: `Failed to close firewall: ${msg}` }],
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(P2P_PORT);
938
- await node.join(name, name);
939
- await startMcpServer({ client: node });
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);