letagents 0.7.0 → 0.8.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/mcp/local-state.js +1 -0
- package/dist/mcp/server.js +269 -112
- package/package.json +1 -1
package/dist/mcp/local-state.js
CHANGED
|
@@ -103,6 +103,7 @@ export function saveRoomSession(input) {
|
|
|
103
103
|
room_id: input.room_id,
|
|
104
104
|
project_id: input.project_id ?? existing?.project_id ?? null,
|
|
105
105
|
code: input.code ?? existing?.code ?? null,
|
|
106
|
+
display_name: input.display_name ?? existing?.display_name ?? null,
|
|
106
107
|
joined_via: input.joined_via,
|
|
107
108
|
joined_at: existing?.joined_at ?? now,
|
|
108
109
|
last_seen_at: now,
|
package/dist/mcp/server.js
CHANGED
|
@@ -64,6 +64,16 @@ class ApiError extends Error {
|
|
|
64
64
|
this.body = body;
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
class RepoRoomAuthRequiredError extends Error {
|
|
68
|
+
roomId;
|
|
69
|
+
pendingAuth;
|
|
70
|
+
constructor(roomId, pendingAuth) {
|
|
71
|
+
super(`Repo room '${roomId}' requires authentication. Device flow started: open ${pendingAuth.verification_uri} and enter code ${pendingAuth.user_code}, then run poll_device_auth.`);
|
|
72
|
+
this.name = "RepoRoomAuthRequiredError";
|
|
73
|
+
this.roomId = roomId;
|
|
74
|
+
this.pendingAuth = pendingAuth;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
67
77
|
function getLetagentsToken() {
|
|
68
78
|
return process.env.LETAGENTS_TOKEN || getStoredAuth()?.token || "";
|
|
69
79
|
}
|
|
@@ -79,6 +89,34 @@ function isMissingRouteError(error) {
|
|
|
79
89
|
(error.status === 404 || error.status === 405) &&
|
|
80
90
|
/Cannot (GET|POST|PATCH)|Not Found|Cannot GET \/rooms|Cannot POST \/rooms/i.test(error.body));
|
|
81
91
|
}
|
|
92
|
+
function parseApiErrorPayload(error) {
|
|
93
|
+
if (!(error instanceof ApiError)) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(error.body);
|
|
98
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function resolveApiPath(urlOrPath) {
|
|
105
|
+
if (!urlOrPath) {
|
|
106
|
+
return "/auth/device/start";
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const parsed = new URL(urlOrPath, `${API_URL}/`);
|
|
110
|
+
const apiBase = new URL(`${API_URL}/`);
|
|
111
|
+
if (parsed.origin !== apiBase.origin) {
|
|
112
|
+
return "/auth/device/start";
|
|
113
|
+
}
|
|
114
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return "/auth/device/start";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
82
120
|
async function apiCall(path, options) {
|
|
83
121
|
const headers = {
|
|
84
122
|
"Content-Type": "application/json",
|
|
@@ -107,20 +145,90 @@ async function apiCall(path, options) {
|
|
|
107
145
|
}
|
|
108
146
|
return JSON.parse(body);
|
|
109
147
|
}
|
|
148
|
+
async function startPendingDeviceAuth(roomId, deviceFlowUrl) {
|
|
149
|
+
const existing = getPendingDeviceAuth();
|
|
150
|
+
if (existing?.suggested_room_id === roomId) {
|
|
151
|
+
return existing;
|
|
152
|
+
}
|
|
153
|
+
const response = await apiCall(resolveApiPath(deviceFlowUrl), {
|
|
154
|
+
method: "POST",
|
|
155
|
+
});
|
|
156
|
+
return setPendingDeviceAuth({
|
|
157
|
+
request_id: response.request_id,
|
|
158
|
+
user_code: response.user_code,
|
|
159
|
+
verification_uri: response.verification_uri,
|
|
160
|
+
interval_seconds: response.interval,
|
|
161
|
+
expires_at: new Date(Date.now() + response.expires_in * 1000).toISOString(),
|
|
162
|
+
started_at: new Date().toISOString(),
|
|
163
|
+
suggested_room_id: roomId,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function maybeHandleRepoRoomAuthRequired(error, roomId) {
|
|
167
|
+
const payload = parseApiErrorPayload(error);
|
|
168
|
+
if (!(error instanceof ApiError) || error.status !== 401 || payload?.error !== "auth_required") {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const pendingAuth = await startPendingDeviceAuth(roomId, typeof payload.device_flow_url === "string" ? payload.device_flow_url : undefined);
|
|
172
|
+
throw new RepoRoomAuthRequiredError(roomId, pendingAuth);
|
|
173
|
+
}
|
|
174
|
+
function toRepoRoomAuthRequiredResult(error) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: "auth_required",
|
|
178
|
+
room_id: error.roomId,
|
|
179
|
+
next_step: "poll_device_auth",
|
|
180
|
+
pending_device_auth: error.pendingAuth,
|
|
181
|
+
message: error.message,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
110
184
|
function toRoomState(input) {
|
|
111
185
|
return {
|
|
112
186
|
room_id: input.room_id,
|
|
113
187
|
project_id: input.project_id ?? null,
|
|
114
188
|
code: input.code ?? null,
|
|
189
|
+
display_name: input.display_name ?? null,
|
|
115
190
|
joined_via: input.joined_via,
|
|
116
191
|
};
|
|
117
192
|
}
|
|
193
|
+
function toPublicRoomState(state) {
|
|
194
|
+
if (!state) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
room_id: state.room_id,
|
|
199
|
+
code: state.code ?? null,
|
|
200
|
+
display_name: state.display_name ?? null,
|
|
201
|
+
joined_via: state.joined_via,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function toPublicStoredRoomSession(session) {
|
|
205
|
+
if (!session) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
room_id: session.room_id,
|
|
210
|
+
code: session.code ?? null,
|
|
211
|
+
display_name: session.display_name ?? null,
|
|
212
|
+
joined_via: session.joined_via,
|
|
213
|
+
joined_at: session.joined_at,
|
|
214
|
+
last_seen_at: session.last_seen_at,
|
|
215
|
+
last_message_id: session.last_message_id ?? null,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function toPublicRoomResponse(response, fallbackRoomId) {
|
|
219
|
+
const { id: _legacyId, project_id: _legacyProjectId, ...rest } = response;
|
|
220
|
+
return {
|
|
221
|
+
...rest,
|
|
222
|
+
room_id: typeof rest.room_id === "string" ? rest.room_id : fallbackRoomId,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
118
225
|
function rememberRoom(state, lastMessageId) {
|
|
119
226
|
currentRoom = state;
|
|
120
227
|
saveRoomSession({
|
|
121
228
|
room_id: state.room_id,
|
|
122
229
|
project_id: state.project_id ?? null,
|
|
123
230
|
code: state.code ?? null,
|
|
231
|
+
display_name: state.display_name ?? null,
|
|
124
232
|
joined_via: state.joined_via,
|
|
125
233
|
last_message_id: lastMessageId,
|
|
126
234
|
});
|
|
@@ -143,8 +251,8 @@ function touchCurrentRoom(lastMessageId) {
|
|
|
143
251
|
function getTargetRoomId(roomId) {
|
|
144
252
|
return roomId || currentRoom?.room_id || null;
|
|
145
253
|
}
|
|
146
|
-
function
|
|
147
|
-
return
|
|
254
|
+
function getFallbackProjectId() {
|
|
255
|
+
return currentRoom?.project_id ?? null;
|
|
148
256
|
}
|
|
149
257
|
function getLastMessageId(payload) {
|
|
150
258
|
if (!payload || typeof payload !== "object") {
|
|
@@ -162,13 +270,14 @@ async function roomScopedApiCall(input) {
|
|
|
162
270
|
return result;
|
|
163
271
|
}
|
|
164
272
|
catch (error) {
|
|
273
|
+
await maybeHandleRepoRoomAuthRequired(error, input.room_id);
|
|
165
274
|
if (!input.project_id || !isMissingRouteError(error)) {
|
|
166
275
|
throw error;
|
|
167
276
|
}
|
|
168
277
|
}
|
|
169
278
|
}
|
|
170
279
|
if (!input.project_id) {
|
|
171
|
-
throw new Error("No
|
|
280
|
+
throw new Error("No room is available for this request.");
|
|
172
281
|
}
|
|
173
282
|
const result = await apiCall(input.project_path(input.project_id), input.options);
|
|
174
283
|
if (input.room_id) {
|
|
@@ -191,6 +300,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
191
300
|
: looksLikeInviteCode(joinedRoomId)
|
|
192
301
|
? joinedRoomId
|
|
193
302
|
: null,
|
|
303
|
+
display_name: typeof response.display_name === "string" ? response.display_name : null,
|
|
194
304
|
joined_via: joinedVia,
|
|
195
305
|
}));
|
|
196
306
|
return {
|
|
@@ -199,6 +309,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
199
309
|
};
|
|
200
310
|
}
|
|
201
311
|
catch (error) {
|
|
312
|
+
await maybeHandleRepoRoomAuthRequired(error, roomId);
|
|
202
313
|
if (!isMissingRouteError(error)) {
|
|
203
314
|
throw error;
|
|
204
315
|
}
|
|
@@ -212,6 +323,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
212
323
|
room_id: legacyRoomId,
|
|
213
324
|
project_id: typeof project.id === "string" ? project.id : null,
|
|
214
325
|
code: typeof project.code === "string" ? project.code : legacyRoomId,
|
|
326
|
+
display_name: typeof project.display_name === "string" ? project.display_name : null,
|
|
215
327
|
joined_via: joinedVia,
|
|
216
328
|
}));
|
|
217
329
|
return {
|
|
@@ -237,6 +349,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
237
349
|
: looksLikeInviteCode(legacyRoomId)
|
|
238
350
|
? legacyRoomId
|
|
239
351
|
: null,
|
|
352
|
+
display_name: typeof project.display_name === "string" ? project.display_name : null,
|
|
240
353
|
joined_via: joinedVia,
|
|
241
354
|
}));
|
|
242
355
|
return {
|
|
@@ -248,6 +361,42 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
248
361
|
},
|
|
249
362
|
};
|
|
250
363
|
}
|
|
364
|
+
async function createInviteRoom() {
|
|
365
|
+
const project = await apiCall("/projects", { method: "POST" });
|
|
366
|
+
const roomId = typeof project.code === "string"
|
|
367
|
+
? project.code
|
|
368
|
+
: typeof project.id === "string"
|
|
369
|
+
? project.id
|
|
370
|
+
: "unknown-room";
|
|
371
|
+
const room = rememberRoom(toRoomState({
|
|
372
|
+
room_id: roomId,
|
|
373
|
+
project_id: typeof project.id === "string" ? project.id : null,
|
|
374
|
+
code: typeof project.code === "string" ? project.code : roomId,
|
|
375
|
+
display_name: typeof project.display_name === "string" ? project.display_name : null,
|
|
376
|
+
joined_via: "join_code",
|
|
377
|
+
}));
|
|
378
|
+
await autoRegisterAgentIdentity();
|
|
379
|
+
return {
|
|
380
|
+
room,
|
|
381
|
+
response: toPublicRoomResponse(project, roomId),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
async function joinInviteCode(code) {
|
|
385
|
+
const joined = await joinRoomIdentifier(code, "join_code");
|
|
386
|
+
await autoRegisterAgentIdentity();
|
|
387
|
+
return {
|
|
388
|
+
...toPublicRoomResponse(joined.response, joined.room.room_id),
|
|
389
|
+
joined_via: "join_code",
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
async function joinNamedRoom(name) {
|
|
393
|
+
const joined = await joinRoomIdentifier(name, "join_room");
|
|
394
|
+
await autoRegisterAgentIdentity();
|
|
395
|
+
return {
|
|
396
|
+
...toPublicRoomResponse(joined.response, joined.room.room_id),
|
|
397
|
+
joined_via: "join_room",
|
|
398
|
+
};
|
|
399
|
+
}
|
|
251
400
|
async function autoRegisterAgentIdentity() {
|
|
252
401
|
if (!AGENT_NAME || !currentRoom) {
|
|
253
402
|
return;
|
|
@@ -299,101 +448,103 @@ server.resource("room_messages", new ResourceTemplate("letagents://rooms/{room_i
|
|
|
299
448
|
],
|
|
300
449
|
};
|
|
301
450
|
});
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const result = await apiCall(`/projects/${encodeURIComponent(project_id)}/messages`);
|
|
451
|
+
// -- create_room ------------------------------------------------------------
|
|
452
|
+
server.tool("create_room", "Create a new invite room on Let Agents Chat. Returns the room ID and join code.", {}, async () => {
|
|
453
|
+
const created = await createInviteRoom();
|
|
306
454
|
return {
|
|
307
|
-
|
|
455
|
+
content: [
|
|
308
456
|
{
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
text: JSON.stringify(result, null, 2),
|
|
457
|
+
type: "text",
|
|
458
|
+
text: JSON.stringify(created.response, null, 2),
|
|
312
459
|
},
|
|
313
460
|
],
|
|
314
461
|
};
|
|
315
462
|
});
|
|
316
463
|
// -- create_project ---------------------------------------------------------
|
|
317
|
-
server.tool("create_project", "
|
|
318
|
-
const
|
|
319
|
-
const roomId = typeof project.code === "string"
|
|
320
|
-
? project.code
|
|
321
|
-
: typeof project.id === "string"
|
|
322
|
-
? project.id
|
|
323
|
-
: "unknown-room";
|
|
324
|
-
rememberRoom(toRoomState({
|
|
325
|
-
room_id: roomId,
|
|
326
|
-
project_id: typeof project.id === "string" ? project.id : null,
|
|
327
|
-
code: typeof project.code === "string" ? project.code : roomId,
|
|
328
|
-
joined_via: "join_code",
|
|
329
|
-
}));
|
|
330
|
-
await autoRegisterAgentIdentity();
|
|
464
|
+
server.tool("create_project", "Legacy alias for create_room. Creates a new invite room and returns its join code.", {}, async () => {
|
|
465
|
+
const created = await createInviteRoom();
|
|
331
466
|
return {
|
|
332
467
|
content: [
|
|
333
468
|
{
|
|
334
469
|
type: "text",
|
|
335
|
-
text: JSON.stringify(
|
|
470
|
+
text: JSON.stringify(created.response, null, 2),
|
|
336
471
|
},
|
|
337
472
|
],
|
|
338
473
|
};
|
|
339
474
|
});
|
|
340
|
-
// --
|
|
341
|
-
server.tool("
|
|
342
|
-
code: z.string().describe("The
|
|
475
|
+
// -- join_code --------------------------------------------------------------
|
|
476
|
+
server.tool("join_code", "Join an existing room using an invite code.", {
|
|
477
|
+
code: z.string().describe("The invite code shared for the room (e.g. 'ABCX-7291')"),
|
|
343
478
|
}, async ({ code }) => {
|
|
344
|
-
const joined = await joinRoomIdentifier(code, "join_code");
|
|
345
|
-
await autoRegisterAgentIdentity();
|
|
346
479
|
return {
|
|
347
480
|
content: [
|
|
348
481
|
{
|
|
349
482
|
type: "text",
|
|
350
|
-
text: JSON.stringify(
|
|
483
|
+
text: JSON.stringify(await joinInviteCode(code), null, 2),
|
|
351
484
|
},
|
|
352
485
|
],
|
|
353
486
|
};
|
|
354
487
|
});
|
|
355
|
-
// --
|
|
356
|
-
server.tool("
|
|
357
|
-
|
|
358
|
-
}, async ({
|
|
359
|
-
const joined = await joinRoomIdentifier(name, "join_room");
|
|
360
|
-
await autoRegisterAgentIdentity();
|
|
488
|
+
// -- join_project -----------------------------------------------------------
|
|
489
|
+
server.tool("join_project", "Legacy alias for join_code. Join an existing room using an invite code.", {
|
|
490
|
+
code: z.string().describe("The invite code shared for the room (e.g. 'ABCX-7291')"),
|
|
491
|
+
}, async ({ code }) => {
|
|
361
492
|
return {
|
|
362
493
|
content: [
|
|
363
494
|
{
|
|
364
495
|
type: "text",
|
|
365
|
-
text: JSON.stringify(
|
|
496
|
+
text: JSON.stringify(await joinInviteCode(code), null, 2),
|
|
366
497
|
},
|
|
367
498
|
],
|
|
368
499
|
};
|
|
369
500
|
});
|
|
370
|
-
// --
|
|
371
|
-
server.tool("
|
|
372
|
-
|
|
501
|
+
// -- join_room --------------------------------------------------------------
|
|
502
|
+
server.tool("join_room", "Join a named room on Let Agents Chat. Creates the room if it doesn't exist. Use this for repo-based room joining.", {
|
|
503
|
+
name: z.string().describe("The room name to join (e.g. 'github.com/owner/repo')"),
|
|
504
|
+
}, async ({ name }) => {
|
|
505
|
+
try {
|
|
373
506
|
return {
|
|
374
507
|
content: [
|
|
375
508
|
{
|
|
376
509
|
type: "text",
|
|
377
|
-
text: JSON.stringify(
|
|
510
|
+
text: JSON.stringify(await joinNamedRoom(name), null, 2),
|
|
378
511
|
},
|
|
379
512
|
],
|
|
380
513
|
};
|
|
381
514
|
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
if (error instanceof RepoRoomAuthRequiredError) {
|
|
517
|
+
return {
|
|
518
|
+
content: [
|
|
519
|
+
{
|
|
520
|
+
type: "text",
|
|
521
|
+
text: JSON.stringify(toRepoRoomAuthRequiredResult(error), null, 2),
|
|
522
|
+
},
|
|
523
|
+
],
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
// -- get_current_room -------------------------------------------------------
|
|
530
|
+
server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {}, async () => {
|
|
382
531
|
return {
|
|
383
532
|
content: [
|
|
384
533
|
{
|
|
385
534
|
type: "text",
|
|
386
|
-
text: JSON.stringify(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
535
|
+
text: JSON.stringify(currentRoom
|
|
536
|
+
? {
|
|
537
|
+
connected: true,
|
|
538
|
+
...toPublicRoomState(currentRoom),
|
|
539
|
+
auth: getStoredAuth()
|
|
540
|
+
? {
|
|
541
|
+
source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
|
|
542
|
+
expires_at: getStoredAuth()?.expires_at ?? null,
|
|
543
|
+
account: getStoredAuth()?.account ?? null,
|
|
544
|
+
}
|
|
545
|
+
: null,
|
|
546
|
+
}
|
|
547
|
+
: { connected: false, message: "Not currently in any room" }, null, 2),
|
|
397
548
|
},
|
|
398
549
|
],
|
|
399
550
|
};
|
|
@@ -432,11 +583,11 @@ server.tool("check_repo", "Inspect the current repository context for Let Agents
|
|
|
432
583
|
config_file: configPath ?? null,
|
|
433
584
|
config_contents: configContents,
|
|
434
585
|
derived_room_from_git: derivedRoom ?? null,
|
|
435
|
-
current_room: currentRoom
|
|
586
|
+
current_room: toPublicRoomState(currentRoom),
|
|
436
587
|
join_hint: !currentRoom
|
|
437
588
|
? repoRoot
|
|
438
|
-
? "Run initialize_repo to set up .letagents.json, or join_room/
|
|
439
|
-
: "Not inside a git repo. Use
|
|
589
|
+
? "Run initialize_repo to set up .letagents.json, or use join_room/join_code to connect."
|
|
590
|
+
: "Not inside a git repo. Use create_room, join_code, or join_room to connect manually."
|
|
440
591
|
: null,
|
|
441
592
|
}, null, 2),
|
|
442
593
|
},
|
|
@@ -454,13 +605,9 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
454
605
|
.string()
|
|
455
606
|
.optional()
|
|
456
607
|
.describe("Canonical room ID. Defaults to the current room."),
|
|
457
|
-
|
|
458
|
-
.string()
|
|
459
|
-
.optional()
|
|
460
|
-
.describe("Legacy project ID. Defaults to the current room if joined."),
|
|
461
|
-
}, async ({ sender, status, room_id, project_id }) => {
|
|
608
|
+
}, async ({ sender, status, room_id }) => {
|
|
462
609
|
const targetRoomId = getTargetRoomId(room_id);
|
|
463
|
-
const targetProjectId =
|
|
610
|
+
const targetProjectId = getFallbackProjectId();
|
|
464
611
|
if (!targetRoomId && !targetProjectId) {
|
|
465
612
|
return {
|
|
466
613
|
content: [
|
|
@@ -468,8 +615,8 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
468
615
|
type: "text",
|
|
469
616
|
text: JSON.stringify({
|
|
470
617
|
success: false,
|
|
471
|
-
error: "No room_id
|
|
472
|
-
hint: "Join a room first
|
|
618
|
+
error: "No room_id provided and not currently in a room.",
|
|
619
|
+
hint: "Join or create a room first, or pass room_id explicitly.",
|
|
473
620
|
}, null, 2),
|
|
474
621
|
},
|
|
475
622
|
],
|
|
@@ -509,7 +656,7 @@ const TASK_STATUSES = [
|
|
|
509
656
|
"proposed", "accepted", "assigned", "in_progress",
|
|
510
657
|
"blocked", "in_review", "merged", "done", "cancelled",
|
|
511
658
|
];
|
|
512
|
-
server.tool("add_task", "Add a new task to the
|
|
659
|
+
server.tool("add_task", "Add a new task to the room board. Tasks normally start as 'proposed' and must be " +
|
|
513
660
|
"accepted before an agent can claim them, but tasks created by trusted agents already " +
|
|
514
661
|
"active in the room may be auto-accepted. Use this when a human or agent identifies " +
|
|
515
662
|
"work that needs to be done.", {
|
|
@@ -518,10 +665,9 @@ server.tool("add_task", "Add a new task to the project board. Tasks normally sta
|
|
|
518
665
|
created_by: z.string().describe("Name of the agent or human creating the task"),
|
|
519
666
|
source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
|
|
520
667
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
521
|
-
|
|
522
|
-
}, async ({ title, description, created_by, source_message_id, room_id, project_id }) => {
|
|
668
|
+
}, async ({ title, description, created_by, source_message_id, room_id }) => {
|
|
523
669
|
const targetRoomId = getTargetRoomId(room_id);
|
|
524
|
-
const targetProjectId =
|
|
670
|
+
const targetProjectId = getFallbackProjectId();
|
|
525
671
|
if (!targetRoomId && !targetProjectId) {
|
|
526
672
|
return {
|
|
527
673
|
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
|
|
@@ -541,16 +687,15 @@ server.tool("add_task", "Add a new task to the project board. Tasks normally sta
|
|
|
541
687
|
content: [{ type: "text", text: JSON.stringify({ success: true, task }, null, 2) }],
|
|
542
688
|
};
|
|
543
689
|
});
|
|
544
|
-
server.tool("get_board", "Get the current task board for the
|
|
690
|
+
server.tool("get_board", "Get the current task board for the room. By default shows only open tasks " +
|
|
545
691
|
"(not done/cancelled). Agents should check this on startup and when idle to " +
|
|
546
692
|
"see if there is unassigned work to claim.", {
|
|
547
693
|
status: z.enum(TASK_STATUSES).optional().describe("Filter by specific status"),
|
|
548
694
|
open_only: z.boolean().optional().describe("If true (default), only show tasks not done/cancelled"),
|
|
549
695
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
550
|
-
|
|
551
|
-
}, async ({ status, open_only, room_id, project_id }) => {
|
|
696
|
+
}, async ({ status, open_only, room_id }) => {
|
|
552
697
|
const targetRoomId = getTargetRoomId(room_id);
|
|
553
|
-
const targetProjectId =
|
|
698
|
+
const targetProjectId = getFallbackProjectId();
|
|
554
699
|
if (!targetRoomId && !targetProjectId) {
|
|
555
700
|
return {
|
|
556
701
|
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
|
|
@@ -578,10 +723,9 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
|
|
|
578
723
|
task_id: z.string().describe("The task ID to claim, e.g. 'task_1'"),
|
|
579
724
|
assignee: z.string().describe("Your agent name, e.g. 'antigravity'"),
|
|
580
725
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
581
|
-
|
|
582
|
-
}, async ({ task_id, assignee, room_id, project_id }) => {
|
|
726
|
+
}, async ({ task_id, assignee, room_id }) => {
|
|
583
727
|
const targetRoomId = getTargetRoomId(room_id);
|
|
584
|
-
const targetProjectId =
|
|
728
|
+
const targetProjectId = getFallbackProjectId();
|
|
585
729
|
if (!targetRoomId && !targetProjectId) {
|
|
586
730
|
return {
|
|
587
731
|
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
|
|
@@ -616,10 +760,9 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
616
760
|
assignee: z.string().optional().describe("New assignee for the task"),
|
|
617
761
|
pr_url: z.string().optional().describe("PR URL to link to the task"),
|
|
618
762
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
619
|
-
|
|
620
|
-
}, async ({ task_id, status, assignee, pr_url, room_id, project_id }) => {
|
|
763
|
+
}, async ({ task_id, status, assignee, pr_url, room_id }) => {
|
|
621
764
|
const targetRoomId = getTargetRoomId(room_id);
|
|
622
|
-
const targetProjectId =
|
|
765
|
+
const targetProjectId = getFallbackProjectId();
|
|
623
766
|
if (!targetRoomId && !targetProjectId) {
|
|
624
767
|
return {
|
|
625
768
|
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
|
|
@@ -652,10 +795,9 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
|
|
|
652
795
|
task_id: z.string().describe("The task ID to submit for review"),
|
|
653
796
|
pr_url: z.string().optional().describe("GitHub PR URL for the work"),
|
|
654
797
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
655
|
-
|
|
656
|
-
}, async ({ task_id, pr_url, room_id, project_id }) => {
|
|
798
|
+
}, async ({ task_id, pr_url, room_id }) => {
|
|
657
799
|
const targetRoomId = getTargetRoomId(room_id);
|
|
658
|
-
const targetProjectId =
|
|
800
|
+
const targetProjectId = getFallbackProjectId();
|
|
659
801
|
if (!targetRoomId && !targetProjectId) {
|
|
660
802
|
return {
|
|
661
803
|
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
|
|
@@ -799,7 +941,6 @@ server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat
|
|
|
799
941
|
success: true,
|
|
800
942
|
created: configPath,
|
|
801
943
|
room_id: joined.room.room_id,
|
|
802
|
-
project_id: joined.room.project_id ?? null,
|
|
803
944
|
code: joined.room.code ?? null,
|
|
804
945
|
joined: true,
|
|
805
946
|
hint: "Consider adding .letagents.json to git so other contributors auto-join the same room.",
|
|
@@ -830,12 +971,11 @@ server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat
|
|
|
830
971
|
// -- send_message -----------------------------------------------------------
|
|
831
972
|
server.tool("send_message", "Send a message to a Let Agents Chat room.", {
|
|
832
973
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
|
|
833
|
-
project_id: z.string().optional().describe("Legacy project ID. Defaults to the current room."),
|
|
834
974
|
sender: z.string().describe("Name identifying the sending agent (e.g. 'antigravity-agent')"),
|
|
835
975
|
text: z.string().describe("The message text to send"),
|
|
836
|
-
}, async ({ room_id,
|
|
976
|
+
}, async ({ room_id, sender, text }) => {
|
|
837
977
|
const targetRoomId = getTargetRoomId(room_id);
|
|
838
|
-
const targetProjectId =
|
|
978
|
+
const targetProjectId = getFallbackProjectId();
|
|
839
979
|
if (!targetRoomId && !targetProjectId) {
|
|
840
980
|
throw new Error("No room is currently selected. Join a room first or pass room_id.");
|
|
841
981
|
}
|
|
@@ -862,10 +1002,9 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
|
|
|
862
1002
|
// -- read_messages ----------------------------------------------------------
|
|
863
1003
|
server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
|
|
864
1004
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
|
|
865
|
-
|
|
866
|
-
}, async ({ room_id, project_id }) => {
|
|
1005
|
+
}, async ({ room_id }) => {
|
|
867
1006
|
const targetRoomId = getTargetRoomId(room_id);
|
|
868
|
-
const targetProjectId =
|
|
1007
|
+
const targetProjectId = getFallbackProjectId();
|
|
869
1008
|
const result = await roomScopedApiCall({
|
|
870
1009
|
room_id: targetRoomId,
|
|
871
1010
|
project_id: targetProjectId,
|
|
@@ -886,7 +1025,6 @@ const MAX_POLL_TIMEOUT_MS = 180000; // 3 minutes
|
|
|
886
1025
|
const DEFAULT_POLL_TIMEOUT_MS = 30000; // 30 seconds
|
|
887
1026
|
server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat room. Blocks until new messages arrive or 30 seconds elapse. Use the after_message_id parameter to only receive messages newer than a specific message.", {
|
|
888
1027
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
|
|
889
|
-
project_id: z.string().optional().describe("Legacy project ID. Defaults to the current room."),
|
|
890
1028
|
after_message_id: z
|
|
891
1029
|
.string()
|
|
892
1030
|
.optional()
|
|
@@ -895,9 +1033,9 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
895
1033
|
.number()
|
|
896
1034
|
.optional()
|
|
897
1035
|
.describe("Maximum wait time in milliseconds. If set to 0, the default timeout will be used."),
|
|
898
|
-
}, async ({ room_id,
|
|
1036
|
+
}, async ({ room_id, after_message_id, timeout }) => {
|
|
899
1037
|
const targetRoomId = getTargetRoomId(room_id);
|
|
900
|
-
const targetProjectId =
|
|
1038
|
+
const targetProjectId = getFallbackProjectId();
|
|
901
1039
|
const serverTimeout = Math.min(Math.max(timeout || DEFAULT_POLL_TIMEOUT_MS, 1000), MAX_POLL_TIMEOUT_MS);
|
|
902
1040
|
const clientTimeout = serverTimeout + 5000; // 5s buffer over server timeout
|
|
903
1041
|
const params = new URLSearchParams();
|
|
@@ -962,8 +1100,8 @@ server.tool("get_onboarding_status", "Inspect local Let Agents MCP auth and room
|
|
|
962
1100
|
account: storedAuth?.account ?? null,
|
|
963
1101
|
token_expires_at: storedAuth?.expires_at ?? null,
|
|
964
1102
|
pending_device_auth: pendingAuth,
|
|
965
|
-
current_room: currentRoom,
|
|
966
|
-
saved_current_room: savedCurrentRoom,
|
|
1103
|
+
current_room: toPublicRoomState(currentRoom),
|
|
1104
|
+
saved_current_room: toPublicStoredRoomSession(savedCurrentRoom),
|
|
967
1105
|
detected_room_from_context: detectedRoom,
|
|
968
1106
|
repo_root: repoRoot,
|
|
969
1107
|
next_step: nextStep,
|
|
@@ -1164,22 +1302,37 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
|
|
|
1164
1302
|
],
|
|
1165
1303
|
};
|
|
1166
1304
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1305
|
+
try {
|
|
1306
|
+
const joined = await joinRoomIdentifier(savedRoom.room_id, savedRoom.joined_via);
|
|
1307
|
+
await autoRegisterAgentIdentity();
|
|
1308
|
+
return {
|
|
1309
|
+
content: [
|
|
1310
|
+
{
|
|
1311
|
+
type: "text",
|
|
1312
|
+
text: JSON.stringify({
|
|
1313
|
+
success: true,
|
|
1314
|
+
rejoined_from_local_state: true,
|
|
1315
|
+
server_session_resumed: false,
|
|
1316
|
+
last_message_id_before_restart: savedRoom.last_message_id ?? null,
|
|
1317
|
+
room: joined.room,
|
|
1318
|
+
}, null, 2),
|
|
1319
|
+
},
|
|
1320
|
+
],
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
catch (error) {
|
|
1324
|
+
if (error instanceof RepoRoomAuthRequiredError) {
|
|
1325
|
+
return {
|
|
1326
|
+
content: [
|
|
1327
|
+
{
|
|
1328
|
+
type: "text",
|
|
1329
|
+
text: JSON.stringify(toRepoRoomAuthRequiredResult(error), null, 2),
|
|
1330
|
+
},
|
|
1331
|
+
],
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
throw error;
|
|
1335
|
+
}
|
|
1183
1336
|
});
|
|
1184
1337
|
// -- check_repo_visibility --------------------------------------------------
|
|
1185
1338
|
server.tool("check_repo_visibility", "Auto-detect the current repo's git remote and check if it's public or private. Returns the canonical key, provider, visibility, and suggested room type (discoverable for public, invite for private/unknown). Useful for deciding whether to auto-join a discoverable room or create an invite room.", {
|
|
@@ -1197,7 +1350,7 @@ server.tool("check_repo_visibility", "Auto-detect the current repo's git remote
|
|
|
1197
1350
|
type: "text",
|
|
1198
1351
|
text: JSON.stringify({
|
|
1199
1352
|
error: "Not in a git repository or no remote configured",
|
|
1200
|
-
suggestion: "Use
|
|
1353
|
+
suggestion: "Use create_room to create an invite room instead",
|
|
1201
1354
|
}, null, 2),
|
|
1202
1355
|
},
|
|
1203
1356
|
],
|
|
@@ -1243,9 +1396,13 @@ async function main() {
|
|
|
1243
1396
|
return;
|
|
1244
1397
|
}
|
|
1245
1398
|
// 4. No context found
|
|
1246
|
-
console.error("ℹ️ No .letagents.json, git remote, or saved room found — use
|
|
1399
|
+
console.error("ℹ️ No .letagents.json, git remote, or saved room found — use create_room, join_code, or join_room to connect.");
|
|
1247
1400
|
}
|
|
1248
1401
|
catch (err) {
|
|
1402
|
+
if (err instanceof RepoRoomAuthRequiredError) {
|
|
1403
|
+
console.error(`🔐 Repo room auth required for '${err.roomId}'. Open ${err.pendingAuth.verification_uri} and enter code ${err.pendingAuth.user_code}, then run poll_device_auth.`);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1249
1406
|
// Auto-join failure should never block the MCP server
|
|
1250
1407
|
console.error("⚠️ Auto-join failed (server still running):", err instanceof Error ? err.message : err);
|
|
1251
1408
|
}
|