@dolusoft/claude-collab 0.1.0
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/LICENSE +21 -0
- package/README.md +236 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2064 -0
- package/dist/cli.js.map +1 -0
- package/dist/hub-main.d.ts +1 -0
- package/dist/hub-main.js +1497 -0
- package/dist/hub-main.js.map +1 -0
- package/dist/mcp-main.d.ts +1 -0
- package/dist/mcp-main.js +684 -0
- package/dist/mcp-main.js.map +1 -0
- package/package.json +80 -0
package/dist/mcp-main.js
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
import { v4 } from 'uuid';
|
|
4
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import { createConnection } from 'net';
|
|
9
|
+
|
|
10
|
+
// src/config/index.ts
|
|
11
|
+
var config = {
|
|
12
|
+
/**
|
|
13
|
+
* WebSocket Hub configuration
|
|
14
|
+
*/
|
|
15
|
+
hub: {
|
|
16
|
+
/**
|
|
17
|
+
* Default port for the Hub server
|
|
18
|
+
*/
|
|
19
|
+
port: parseInt(process.env["CLAUDE_COLLAB_PORT"] ?? "9999", 10),
|
|
20
|
+
/**
|
|
21
|
+
* Host to bind the Hub server to
|
|
22
|
+
*/
|
|
23
|
+
host: process.env["CLAUDE_COLLAB_HOST"] ?? "localhost",
|
|
24
|
+
/**
|
|
25
|
+
* Heartbeat interval in milliseconds
|
|
26
|
+
*/
|
|
27
|
+
heartbeatInterval: 3e4},
|
|
28
|
+
/**
|
|
29
|
+
* Communication configuration
|
|
30
|
+
*/
|
|
31
|
+
communication: {
|
|
32
|
+
/**
|
|
33
|
+
* Default timeout for waiting for an answer (in milliseconds)
|
|
34
|
+
*/
|
|
35
|
+
defaultTimeout: 3e4},
|
|
36
|
+
/**
|
|
37
|
+
* Auto-start configuration
|
|
38
|
+
*/
|
|
39
|
+
autoStart: {
|
|
40
|
+
/**
|
|
41
|
+
* Maximum retries when connecting to hub
|
|
42
|
+
*/
|
|
43
|
+
maxRetries: 3,
|
|
44
|
+
/**
|
|
45
|
+
* Delay between retries in milliseconds
|
|
46
|
+
*/
|
|
47
|
+
retryDelay: 1e3
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// src/infrastructure/websocket/message-protocol.ts
|
|
52
|
+
function serializeMessage(message) {
|
|
53
|
+
return JSON.stringify(message);
|
|
54
|
+
}
|
|
55
|
+
function parseHubMessage(data) {
|
|
56
|
+
return JSON.parse(data);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/infrastructure/websocket/hub-client.ts
|
|
60
|
+
var HubClient = class {
|
|
61
|
+
constructor(options = {}, events = {}) {
|
|
62
|
+
this.options = options;
|
|
63
|
+
this.events = events;
|
|
64
|
+
}
|
|
65
|
+
ws = null;
|
|
66
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
67
|
+
reconnectAttempts = 0;
|
|
68
|
+
isClosing = false;
|
|
69
|
+
memberId;
|
|
70
|
+
teamId;
|
|
71
|
+
teamName;
|
|
72
|
+
displayName;
|
|
73
|
+
/**
|
|
74
|
+
* Connects to the Hub server
|
|
75
|
+
*/
|
|
76
|
+
async connect() {
|
|
77
|
+
const host = this.options.host ?? config.hub.host;
|
|
78
|
+
const port = this.options.port ?? config.hub.port;
|
|
79
|
+
const url = `ws://${host}:${port}`;
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
try {
|
|
82
|
+
this.ws = new WebSocket(url);
|
|
83
|
+
this.ws.on("open", () => {
|
|
84
|
+
this.reconnectAttempts = 0;
|
|
85
|
+
this.startPingInterval();
|
|
86
|
+
this.events.onConnected?.();
|
|
87
|
+
resolve();
|
|
88
|
+
});
|
|
89
|
+
this.ws.on("message", (data) => {
|
|
90
|
+
this.handleMessage(data.toString());
|
|
91
|
+
});
|
|
92
|
+
this.ws.on("close", () => {
|
|
93
|
+
this.handleDisconnect();
|
|
94
|
+
});
|
|
95
|
+
this.ws.on("error", (error) => {
|
|
96
|
+
this.events.onError?.(error);
|
|
97
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
98
|
+
reject(error);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
reject(error);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Disconnects from the Hub server
|
|
108
|
+
*/
|
|
109
|
+
async disconnect() {
|
|
110
|
+
this.isClosing = true;
|
|
111
|
+
if (this.memberId) {
|
|
112
|
+
this.send({ type: "LEAVE" });
|
|
113
|
+
}
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
if (this.ws) {
|
|
116
|
+
this.ws.close();
|
|
117
|
+
this.ws = null;
|
|
118
|
+
}
|
|
119
|
+
this.isClosing = false;
|
|
120
|
+
resolve();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Joins a team
|
|
125
|
+
*/
|
|
126
|
+
async join(teamName, displayName) {
|
|
127
|
+
v4();
|
|
128
|
+
this.send({
|
|
129
|
+
type: "JOIN",
|
|
130
|
+
teamName,
|
|
131
|
+
displayName
|
|
132
|
+
});
|
|
133
|
+
const response = await this.waitForResponse(
|
|
134
|
+
(msg) => msg.type === "JOINED",
|
|
135
|
+
3e4
|
|
136
|
+
);
|
|
137
|
+
this.memberId = response.member.memberId;
|
|
138
|
+
this.teamId = response.member.teamId;
|
|
139
|
+
this.teamName = teamName;
|
|
140
|
+
this.displayName = displayName;
|
|
141
|
+
return response.member;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Asks a question to another team
|
|
145
|
+
*/
|
|
146
|
+
async ask(toTeam, content, format = "markdown", timeoutMs = config.communication.defaultTimeout) {
|
|
147
|
+
const requestId = v4();
|
|
148
|
+
this.send({
|
|
149
|
+
type: "ASK",
|
|
150
|
+
toTeam,
|
|
151
|
+
content,
|
|
152
|
+
format,
|
|
153
|
+
requestId
|
|
154
|
+
});
|
|
155
|
+
await this.waitForResponse(
|
|
156
|
+
(msg) => msg.type === "QUESTION_SENT" && "requestId" in msg && msg.requestId === requestId,
|
|
157
|
+
5e3
|
|
158
|
+
);
|
|
159
|
+
const answer = await this.waitForResponse(
|
|
160
|
+
(msg) => msg.type === "ANSWER",
|
|
161
|
+
timeoutMs
|
|
162
|
+
);
|
|
163
|
+
return answer;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Gets the inbox (pending questions)
|
|
167
|
+
*/
|
|
168
|
+
async getInbox() {
|
|
169
|
+
const requestId = v4();
|
|
170
|
+
this.send({
|
|
171
|
+
type: "GET_INBOX",
|
|
172
|
+
requestId
|
|
173
|
+
});
|
|
174
|
+
return this.waitForResponse(
|
|
175
|
+
(msg) => msg.type === "INBOX" && msg.requestId === requestId,
|
|
176
|
+
5e3
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Replies to a question
|
|
181
|
+
*/
|
|
182
|
+
async reply(questionId, content, format = "markdown") {
|
|
183
|
+
this.send({
|
|
184
|
+
type: "REPLY",
|
|
185
|
+
questionId,
|
|
186
|
+
content,
|
|
187
|
+
format
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Checks if connected
|
|
192
|
+
*/
|
|
193
|
+
get isConnected() {
|
|
194
|
+
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Gets the current member ID
|
|
198
|
+
*/
|
|
199
|
+
get currentMemberId() {
|
|
200
|
+
return this.memberId;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Gets the current team ID
|
|
204
|
+
*/
|
|
205
|
+
get currentTeamId() {
|
|
206
|
+
return this.teamId;
|
|
207
|
+
}
|
|
208
|
+
send(message) {
|
|
209
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
210
|
+
this.ws.send(serializeMessage(message));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
handleMessage(data) {
|
|
214
|
+
try {
|
|
215
|
+
const message = parseHubMessage(data);
|
|
216
|
+
for (const [requestId, pending] of this.pendingRequests) {
|
|
217
|
+
}
|
|
218
|
+
switch (message.type) {
|
|
219
|
+
case "QUESTION":
|
|
220
|
+
this.events.onQuestion?.(message);
|
|
221
|
+
break;
|
|
222
|
+
case "ANSWER":
|
|
223
|
+
this.events.onAnswer?.(message);
|
|
224
|
+
break;
|
|
225
|
+
case "MEMBER_JOINED":
|
|
226
|
+
this.events.onMemberJoined?.(message.member);
|
|
227
|
+
break;
|
|
228
|
+
case "MEMBER_LEFT":
|
|
229
|
+
this.events.onMemberLeft?.(message.memberId, message.teamId);
|
|
230
|
+
break;
|
|
231
|
+
case "ERROR":
|
|
232
|
+
this.events.onError?.(new Error(`${message.code}: ${message.message}`));
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
this.resolvePendingRequest(message);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error("Failed to parse message:", error);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
resolvePendingRequest(message) {
|
|
241
|
+
for (const [requestId, pending] of this.pendingRequests) {
|
|
242
|
+
this.pendingRequests.delete(requestId);
|
|
243
|
+
clearTimeout(pending.timeout);
|
|
244
|
+
pending.resolve(message);
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
waitForResponse(filter, timeoutMs) {
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
const requestId = v4();
|
|
251
|
+
const timeout = setTimeout(() => {
|
|
252
|
+
this.pendingRequests.delete(requestId);
|
|
253
|
+
reject(new Error("Request timed out"));
|
|
254
|
+
}, timeoutMs);
|
|
255
|
+
const pending = {
|
|
256
|
+
resolve: (msg) => {
|
|
257
|
+
if (filter(msg)) {
|
|
258
|
+
resolve(msg);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
reject,
|
|
262
|
+
timeout,
|
|
263
|
+
filter
|
|
264
|
+
};
|
|
265
|
+
this.pendingRequests.set(requestId, pending);
|
|
266
|
+
const originalHandler = this.handleMessage.bind(this);
|
|
267
|
+
const checkFilter = (data) => {
|
|
268
|
+
try {
|
|
269
|
+
const message = parseHubMessage(data);
|
|
270
|
+
if (filter(message)) {
|
|
271
|
+
this.pendingRequests.delete(requestId);
|
|
272
|
+
clearTimeout(timeout);
|
|
273
|
+
resolve(message);
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
originalHandler(data);
|
|
278
|
+
};
|
|
279
|
+
if (this.ws) {
|
|
280
|
+
this.ws.removeAllListeners("message");
|
|
281
|
+
this.ws.on("message", (data) => checkFilter(data.toString()));
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
handleDisconnect() {
|
|
286
|
+
this.events.onDisconnected?.();
|
|
287
|
+
if (this.isClosing) return;
|
|
288
|
+
const shouldReconnect = this.options.reconnect ?? true;
|
|
289
|
+
const maxAttempts = this.options.maxReconnectAttempts ?? config.autoStart.maxRetries;
|
|
290
|
+
if (shouldReconnect && this.reconnectAttempts < maxAttempts) {
|
|
291
|
+
this.reconnectAttempts++;
|
|
292
|
+
const delay = this.options.reconnectDelay ?? config.autoStart.retryDelay;
|
|
293
|
+
setTimeout(() => {
|
|
294
|
+
this.connect().then(() => {
|
|
295
|
+
if (this.teamName && this.displayName) {
|
|
296
|
+
return this.join(this.teamName, this.displayName);
|
|
297
|
+
}
|
|
298
|
+
}).catch((error) => {
|
|
299
|
+
this.events.onError?.(error);
|
|
300
|
+
});
|
|
301
|
+
}, delay);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
pingInterval = null;
|
|
305
|
+
startPingInterval() {
|
|
306
|
+
this.pingInterval = setInterval(() => {
|
|
307
|
+
this.send({ type: "PING" });
|
|
308
|
+
}, config.hub.heartbeatInterval);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
var joinSchema = {
|
|
312
|
+
team: z.string().describe('Team name to join (e.g., "frontend", "backend", "devops")'),
|
|
313
|
+
displayName: z.string().optional().describe('Display name for this terminal (default: team + " Claude")')
|
|
314
|
+
};
|
|
315
|
+
function registerJoinTool(server, hubClient) {
|
|
316
|
+
server.tool("join", joinSchema, async (args2) => {
|
|
317
|
+
const teamName = args2.team;
|
|
318
|
+
const displayName = args2.displayName ?? `${teamName} Claude`;
|
|
319
|
+
try {
|
|
320
|
+
if (!hubClient.isConnected) {
|
|
321
|
+
await hubClient.connect();
|
|
322
|
+
}
|
|
323
|
+
const member = await hubClient.join(teamName, displayName);
|
|
324
|
+
return {
|
|
325
|
+
content: [
|
|
326
|
+
{
|
|
327
|
+
type: "text",
|
|
328
|
+
text: `Successfully joined team "${member.teamName}" as "${member.displayName}".
|
|
329
|
+
|
|
330
|
+
Your member ID: ${member.memberId}
|
|
331
|
+
Team ID: ${member.teamId}
|
|
332
|
+
Status: ${member.status}`
|
|
333
|
+
}
|
|
334
|
+
]
|
|
335
|
+
};
|
|
336
|
+
} catch (error) {
|
|
337
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
338
|
+
return {
|
|
339
|
+
content: [
|
|
340
|
+
{
|
|
341
|
+
type: "text",
|
|
342
|
+
text: `Failed to join team: ${errorMessage}`
|
|
343
|
+
}
|
|
344
|
+
],
|
|
345
|
+
isError: true
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
var askSchema = {
|
|
351
|
+
team: z.string().describe('Target team name to ask (e.g., "backend", "frontend")'),
|
|
352
|
+
question: z.string().describe("The question to ask (supports markdown)"),
|
|
353
|
+
timeout: z.number().optional().describe(`Timeout in seconds to wait for answer (default: ${config.communication.defaultTimeout / 1e3}s)`)
|
|
354
|
+
};
|
|
355
|
+
function registerAskTool(server, hubClient) {
|
|
356
|
+
server.tool("ask", askSchema, async (args2) => {
|
|
357
|
+
const targetTeam = args2.team;
|
|
358
|
+
const question = args2.question;
|
|
359
|
+
const timeoutMs = (args2.timeout ?? config.communication.defaultTimeout / 1e3) * 1e3;
|
|
360
|
+
try {
|
|
361
|
+
if (!hubClient.currentTeamId) {
|
|
362
|
+
return {
|
|
363
|
+
content: [
|
|
364
|
+
{
|
|
365
|
+
type: "text",
|
|
366
|
+
text: 'You must join a team first. Use the "join" tool to join a team.'
|
|
367
|
+
}
|
|
368
|
+
],
|
|
369
|
+
isError: true
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
const answer = await hubClient.ask(targetTeam, question, "markdown", timeoutMs);
|
|
373
|
+
return {
|
|
374
|
+
content: [
|
|
375
|
+
{
|
|
376
|
+
type: "text",
|
|
377
|
+
text: `**Answer from ${answer.from.displayName} (${answer.from.teamName}):**
|
|
378
|
+
|
|
379
|
+
${answer.content}`
|
|
380
|
+
}
|
|
381
|
+
]
|
|
382
|
+
};
|
|
383
|
+
} catch (error) {
|
|
384
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
385
|
+
if (errorMessage.includes("timed out")) {
|
|
386
|
+
return {
|
|
387
|
+
content: [
|
|
388
|
+
{
|
|
389
|
+
type: "text",
|
|
390
|
+
text: `No response received from team "${targetTeam}" within ${timeoutMs / 1e3} seconds. The question has been delivered but no one answered yet.`
|
|
391
|
+
}
|
|
392
|
+
]
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
content: [
|
|
397
|
+
{
|
|
398
|
+
type: "text",
|
|
399
|
+
text: `Failed to ask question: ${errorMessage}`
|
|
400
|
+
}
|
|
401
|
+
],
|
|
402
|
+
isError: true
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/presentation/mcp/tools/inbox.tool.ts
|
|
409
|
+
var inboxSchema = {};
|
|
410
|
+
function registerInboxTool(server, hubClient) {
|
|
411
|
+
server.tool("inbox", inboxSchema, async () => {
|
|
412
|
+
try {
|
|
413
|
+
if (!hubClient.currentTeamId) {
|
|
414
|
+
return {
|
|
415
|
+
content: [
|
|
416
|
+
{
|
|
417
|
+
type: "text",
|
|
418
|
+
text: 'You must join a team first. Use the "join" tool to join a team.'
|
|
419
|
+
}
|
|
420
|
+
],
|
|
421
|
+
isError: true
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const inbox = await hubClient.getInbox();
|
|
425
|
+
if (inbox.questions.length === 0) {
|
|
426
|
+
return {
|
|
427
|
+
content: [
|
|
428
|
+
{
|
|
429
|
+
type: "text",
|
|
430
|
+
text: "No pending questions in your inbox."
|
|
431
|
+
}
|
|
432
|
+
]
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const questionsList = inbox.questions.map((q, i) => {
|
|
436
|
+
const ageSeconds = Math.floor(q.ageMs / 1e3);
|
|
437
|
+
const ageStr = ageSeconds < 60 ? `${ageSeconds}s ago` : `${Math.floor(ageSeconds / 60)}m ago`;
|
|
438
|
+
return `### ${i + 1}. Question from ${q.from.displayName} (${q.from.teamName}) - ${ageStr}
|
|
439
|
+
**ID:** \`${q.questionId}\`
|
|
440
|
+
**Status:** ${q.status}
|
|
441
|
+
|
|
442
|
+
${q.content}
|
|
443
|
+
|
|
444
|
+
---`;
|
|
445
|
+
}).join("\n\n");
|
|
446
|
+
return {
|
|
447
|
+
content: [
|
|
448
|
+
{
|
|
449
|
+
type: "text",
|
|
450
|
+
text: `# Inbox (${inbox.pendingCount} pending, ${inbox.totalCount} total)
|
|
451
|
+
|
|
452
|
+
${questionsList}
|
|
453
|
+
|
|
454
|
+
Use the "reply" tool with the question ID to answer a question.`
|
|
455
|
+
}
|
|
456
|
+
]
|
|
457
|
+
};
|
|
458
|
+
} catch (error) {
|
|
459
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
460
|
+
return {
|
|
461
|
+
content: [
|
|
462
|
+
{
|
|
463
|
+
type: "text",
|
|
464
|
+
text: `Failed to get inbox: ${errorMessage}`
|
|
465
|
+
}
|
|
466
|
+
],
|
|
467
|
+
isError: true
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
var replySchema = {
|
|
473
|
+
questionId: z.string().describe("The ID of the question to reply to (from inbox)"),
|
|
474
|
+
answer: z.string().describe("Your answer to the question (supports markdown)")
|
|
475
|
+
};
|
|
476
|
+
function registerReplyTool(server, hubClient) {
|
|
477
|
+
server.tool("reply", replySchema, async (args2) => {
|
|
478
|
+
const questionId = args2.questionId;
|
|
479
|
+
const answer = args2.answer;
|
|
480
|
+
try {
|
|
481
|
+
if (!hubClient.currentTeamId) {
|
|
482
|
+
return {
|
|
483
|
+
content: [
|
|
484
|
+
{
|
|
485
|
+
type: "text",
|
|
486
|
+
text: 'You must join a team first. Use the "join" tool to join a team.'
|
|
487
|
+
}
|
|
488
|
+
],
|
|
489
|
+
isError: true
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
await hubClient.reply(questionId, answer, "markdown");
|
|
493
|
+
return {
|
|
494
|
+
content: [
|
|
495
|
+
{
|
|
496
|
+
type: "text",
|
|
497
|
+
text: `Reply sent successfully to question \`${questionId}\`.`
|
|
498
|
+
}
|
|
499
|
+
]
|
|
500
|
+
};
|
|
501
|
+
} catch (error) {
|
|
502
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
503
|
+
return {
|
|
504
|
+
content: [
|
|
505
|
+
{
|
|
506
|
+
type: "text",
|
|
507
|
+
text: `Failed to send reply: ${errorMessage}`
|
|
508
|
+
}
|
|
509
|
+
],
|
|
510
|
+
isError: true
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/presentation/mcp/server.ts
|
|
517
|
+
function createMcpServer(options) {
|
|
518
|
+
const server = new McpServer({
|
|
519
|
+
name: "claude-collab",
|
|
520
|
+
version: "0.1.0"
|
|
521
|
+
});
|
|
522
|
+
registerJoinTool(server, options.hubClient);
|
|
523
|
+
registerAskTool(server, options.hubClient);
|
|
524
|
+
registerInboxTool(server, options.hubClient);
|
|
525
|
+
registerReplyTool(server, options.hubClient);
|
|
526
|
+
return server;
|
|
527
|
+
}
|
|
528
|
+
async function startMcpServer(options) {
|
|
529
|
+
const server = createMcpServer(options);
|
|
530
|
+
const transport = new StdioServerTransport();
|
|
531
|
+
await server.connect(transport);
|
|
532
|
+
}
|
|
533
|
+
async function isHubRunning(host = config.hub.host, port = config.hub.port) {
|
|
534
|
+
return new Promise((resolve) => {
|
|
535
|
+
const socket = createConnection({ host, port }, () => {
|
|
536
|
+
socket.end();
|
|
537
|
+
resolve(true);
|
|
538
|
+
});
|
|
539
|
+
socket.on("error", () => {
|
|
540
|
+
resolve(false);
|
|
541
|
+
});
|
|
542
|
+
socket.setTimeout(1e3, () => {
|
|
543
|
+
socket.destroy();
|
|
544
|
+
resolve(false);
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
async function waitForHub(host = config.hub.host, port = config.hub.port, maxRetries = config.autoStart.maxRetries, retryDelay = config.autoStart.retryDelay) {
|
|
549
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
550
|
+
if (await isHubRunning(host, port)) {
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
554
|
+
}
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
function startHubProcess(options = {}) {
|
|
558
|
+
const host = options.host ?? config.hub.host;
|
|
559
|
+
const port = options.port ?? config.hub.port;
|
|
560
|
+
const hubProcess = spawn(
|
|
561
|
+
process.execPath,
|
|
562
|
+
[
|
|
563
|
+
"--experimental-specifier-resolution=node",
|
|
564
|
+
new URL("../../hub-main.js", import.meta.url).pathname,
|
|
565
|
+
"--host",
|
|
566
|
+
host,
|
|
567
|
+
"--port",
|
|
568
|
+
port.toString()
|
|
569
|
+
],
|
|
570
|
+
{
|
|
571
|
+
detached: true,
|
|
572
|
+
stdio: "ignore"
|
|
573
|
+
}
|
|
574
|
+
);
|
|
575
|
+
hubProcess.unref();
|
|
576
|
+
return hubProcess;
|
|
577
|
+
}
|
|
578
|
+
async function ensureHubRunning(options = {}) {
|
|
579
|
+
const host = options.host ?? config.hub.host;
|
|
580
|
+
const port = options.port ?? config.hub.port;
|
|
581
|
+
const maxRetries = options.maxRetries ?? config.autoStart.maxRetries;
|
|
582
|
+
const retryDelay = options.retryDelay ?? config.autoStart.retryDelay;
|
|
583
|
+
if (await isHubRunning(host, port)) {
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
console.log(`Hub not running. Starting hub on ${host}:${port}...`);
|
|
587
|
+
startHubProcess({ host, port });
|
|
588
|
+
const isRunning = await waitForHub(host, port, maxRetries, retryDelay);
|
|
589
|
+
if (isRunning) {
|
|
590
|
+
console.log("Hub started successfully");
|
|
591
|
+
} else {
|
|
592
|
+
console.error("Failed to start hub");
|
|
593
|
+
}
|
|
594
|
+
return isRunning;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/mcp-main.ts
|
|
598
|
+
var args = process.argv.slice(2);
|
|
599
|
+
function parseArgs() {
|
|
600
|
+
const options = {
|
|
601
|
+
host: config.hub.host,
|
|
602
|
+
port: config.hub.port,
|
|
603
|
+
autoHub: false
|
|
604
|
+
};
|
|
605
|
+
for (let i = 0; i < args.length; i++) {
|
|
606
|
+
const arg = args[i];
|
|
607
|
+
const nextArg = args[i + 1];
|
|
608
|
+
if (arg === "--team" && nextArg) {
|
|
609
|
+
options.team = nextArg;
|
|
610
|
+
i++;
|
|
611
|
+
} else if (arg === "--host" && nextArg) {
|
|
612
|
+
options.host = nextArg;
|
|
613
|
+
i++;
|
|
614
|
+
} else if (arg === "--port" && nextArg) {
|
|
615
|
+
options.port = parseInt(nextArg, 10);
|
|
616
|
+
i++;
|
|
617
|
+
} else if (arg === "--auto-hub") {
|
|
618
|
+
options.autoHub = true;
|
|
619
|
+
} else if (arg === "-h" || arg === "--help") {
|
|
620
|
+
console.error(`
|
|
621
|
+
Claude Collab MCP Client
|
|
622
|
+
|
|
623
|
+
Usage:
|
|
624
|
+
mcp-main [options]
|
|
625
|
+
|
|
626
|
+
Options:
|
|
627
|
+
--team <team> Team to auto-join (optional)
|
|
628
|
+
--host <host> Hub host (default: ${config.hub.host})
|
|
629
|
+
--port <port> Hub port (default: ${config.hub.port})
|
|
630
|
+
--auto-hub Auto-start hub if not running
|
|
631
|
+
-h, --help Show this help message
|
|
632
|
+
`);
|
|
633
|
+
process.exit(0);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return options;
|
|
637
|
+
}
|
|
638
|
+
async function main() {
|
|
639
|
+
const options = parseArgs();
|
|
640
|
+
if (options.autoHub) {
|
|
641
|
+
const hubRunning = await ensureHubRunning({
|
|
642
|
+
host: options.host,
|
|
643
|
+
port: options.port
|
|
644
|
+
});
|
|
645
|
+
if (!hubRunning) {
|
|
646
|
+
console.error("Failed to start hub server. Exiting.");
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const hubClient = new HubClient(
|
|
651
|
+
{
|
|
652
|
+
host: options.host,
|
|
653
|
+
port: options.port,
|
|
654
|
+
reconnect: true
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
onError: (error) => {
|
|
658
|
+
console.error("Hub client error:", error.message);
|
|
659
|
+
},
|
|
660
|
+
onQuestion: (question) => {
|
|
661
|
+
console.error(`[Question received from ${question.from.displayName}]`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
);
|
|
665
|
+
try {
|
|
666
|
+
await hubClient.connect();
|
|
667
|
+
if (options.team) {
|
|
668
|
+
await hubClient.join(options.team, `${options.team} Claude`);
|
|
669
|
+
console.error(`Auto-joined team: ${options.team}`);
|
|
670
|
+
}
|
|
671
|
+
} catch (error) {
|
|
672
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
673
|
+
console.error(`Failed to connect to hub: ${errorMessage}`);
|
|
674
|
+
console.error("Make sure the hub server is running or use --auto-hub flag.");
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
await startMcpServer({ hubClient });
|
|
678
|
+
}
|
|
679
|
+
main().catch((error) => {
|
|
680
|
+
console.error("Unexpected error:", error);
|
|
681
|
+
process.exit(1);
|
|
682
|
+
});
|
|
683
|
+
//# sourceMappingURL=mcp-main.js.map
|
|
684
|
+
//# sourceMappingURL=mcp-main.js.map
|