letagents 0.7.0 → 0.8.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.
Files changed (2) hide show
  1. package/dist/mcp/server.js +124 -77
  2. package/package.json +1 -1
@@ -115,6 +115,36 @@ function toRoomState(input) {
115
115
  joined_via: input.joined_via,
116
116
  };
117
117
  }
118
+ function toPublicRoomState(state) {
119
+ if (!state) {
120
+ return null;
121
+ }
122
+ return {
123
+ room_id: state.room_id,
124
+ code: state.code ?? null,
125
+ joined_via: state.joined_via,
126
+ };
127
+ }
128
+ function toPublicStoredRoomSession(session) {
129
+ if (!session) {
130
+ return null;
131
+ }
132
+ return {
133
+ room_id: session.room_id,
134
+ code: session.code ?? null,
135
+ joined_via: session.joined_via,
136
+ joined_at: session.joined_at,
137
+ last_seen_at: session.last_seen_at,
138
+ last_message_id: session.last_message_id ?? null,
139
+ };
140
+ }
141
+ function toPublicRoomResponse(response, fallbackRoomId) {
142
+ const { id: _legacyId, project_id: _legacyProjectId, ...rest } = response;
143
+ return {
144
+ ...rest,
145
+ room_id: typeof rest.room_id === "string" ? rest.room_id : fallbackRoomId,
146
+ };
147
+ }
118
148
  function rememberRoom(state, lastMessageId) {
119
149
  currentRoom = state;
120
150
  saveRoomSession({
@@ -143,8 +173,8 @@ function touchCurrentRoom(lastMessageId) {
143
173
  function getTargetRoomId(roomId) {
144
174
  return roomId || currentRoom?.room_id || null;
145
175
  }
146
- function getTargetProjectId(projectId) {
147
- return projectId || currentRoom?.project_id || null;
176
+ function getFallbackProjectId() {
177
+ return currentRoom?.project_id ?? null;
148
178
  }
149
179
  function getLastMessageId(payload) {
150
180
  if (!payload || typeof payload !== "object") {
@@ -168,7 +198,7 @@ async function roomScopedApiCall(input) {
168
198
  }
169
199
  }
170
200
  if (!input.project_id) {
171
- throw new Error("No room_id or project_id is available for this request.");
201
+ throw new Error("No room is available for this request.");
172
202
  }
173
203
  const result = await apiCall(input.project_path(input.project_id), input.options);
174
204
  if (input.room_id) {
@@ -248,6 +278,41 @@ async function joinRoomIdentifier(identifier, joinedVia) {
248
278
  },
249
279
  };
250
280
  }
281
+ async function createInviteRoom() {
282
+ const project = await apiCall("/projects", { method: "POST" });
283
+ const roomId = typeof project.code === "string"
284
+ ? project.code
285
+ : typeof project.id === "string"
286
+ ? project.id
287
+ : "unknown-room";
288
+ const room = rememberRoom(toRoomState({
289
+ room_id: roomId,
290
+ project_id: typeof project.id === "string" ? project.id : null,
291
+ code: typeof project.code === "string" ? project.code : roomId,
292
+ joined_via: "join_code",
293
+ }));
294
+ await autoRegisterAgentIdentity();
295
+ return {
296
+ room,
297
+ response: toPublicRoomResponse(project, roomId),
298
+ };
299
+ }
300
+ async function joinInviteCode(code) {
301
+ const joined = await joinRoomIdentifier(code, "join_code");
302
+ await autoRegisterAgentIdentity();
303
+ return {
304
+ ...toPublicRoomResponse(joined.response, joined.room.room_id),
305
+ joined_via: "join_code",
306
+ };
307
+ }
308
+ async function joinNamedRoom(name) {
309
+ const joined = await joinRoomIdentifier(name, "join_room");
310
+ await autoRegisterAgentIdentity();
311
+ return {
312
+ ...toPublicRoomResponse(joined.response, joined.room.room_id),
313
+ joined_via: "join_room",
314
+ };
315
+ }
251
316
  async function autoRegisterAgentIdentity() {
252
317
  if (!AGENT_NAME || !currentRoom) {
253
318
  return;
@@ -299,55 +364,52 @@ server.resource("room_messages", new ResourceTemplate("letagents://rooms/{room_i
299
364
  ],
300
365
  };
301
366
  });
302
- server.resource("project_messages", new ResourceTemplate("letagents://projects/{project_id}/messages", {
303
- list: undefined,
304
- }), async (uri, { project_id }) => {
305
- const result = await apiCall(`/projects/${encodeURIComponent(project_id)}/messages`);
367
+ // -- create_room ------------------------------------------------------------
368
+ server.tool("create_room", "Create a new invite room on Let Agents Chat. Returns the room ID and join code.", {}, async () => {
369
+ const created = await createInviteRoom();
306
370
  return {
307
- contents: [
371
+ content: [
308
372
  {
309
- uri: uri.href,
310
- mimeType: "application/json",
311
- text: JSON.stringify(result, null, 2),
373
+ type: "text",
374
+ text: JSON.stringify(created.response, null, 2),
312
375
  },
313
376
  ],
314
377
  };
315
378
  });
316
379
  // -- create_project ---------------------------------------------------------
317
- server.tool("create_project", "Create a new project on Let Agents Chat. Returns a project ID and a join code that other agents can use to join.", {}, async () => {
318
- const project = await apiCall("/projects", { method: "POST" });
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();
380
+ server.tool("create_project", "Legacy alias for create_room. Creates a new invite room and returns its join code.", {}, async () => {
381
+ const created = await createInviteRoom();
331
382
  return {
332
383
  content: [
333
384
  {
334
385
  type: "text",
335
- text: JSON.stringify({ ...project, room_id: roomId }, null, 2),
386
+ text: JSON.stringify(created.response, null, 2),
387
+ },
388
+ ],
389
+ };
390
+ });
391
+ // -- join_code --------------------------------------------------------------
392
+ server.tool("join_code", "Join an existing room using an invite code.", {
393
+ code: z.string().describe("The invite code shared for the room (e.g. 'ABCX-7291')"),
394
+ }, async ({ code }) => {
395
+ return {
396
+ content: [
397
+ {
398
+ type: "text",
399
+ text: JSON.stringify(await joinInviteCode(code), null, 2),
336
400
  },
337
401
  ],
338
402
  };
339
403
  });
340
404
  // -- join_project -----------------------------------------------------------
341
- server.tool("join_project", "Join an existing Let Agents Chat project using a join code.", {
342
- code: z.string().describe("The join code shared by the project creator (e.g. 'ABCX-7291')"),
405
+ server.tool("join_project", "Legacy alias for join_code. Join an existing room using an invite code.", {
406
+ code: z.string().describe("The invite code shared for the room (e.g. 'ABCX-7291')"),
343
407
  }, async ({ code }) => {
344
- const joined = await joinRoomIdentifier(code, "join_code");
345
- await autoRegisterAgentIdentity();
346
408
  return {
347
409
  content: [
348
410
  {
349
411
  type: "text",
350
- text: JSON.stringify({ ...joined.response, joined_via: "join_code" }, null, 2),
412
+ text: JSON.stringify(await joinInviteCode(code), null, 2),
351
413
  },
352
414
  ],
353
415
  };
@@ -356,13 +418,11 @@ server.tool("join_project", "Join an existing Let Agents Chat project using a jo
356
418
  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.", {
357
419
  name: z.string().describe("The room name to join (e.g. 'github.com/owner/repo')"),
358
420
  }, async ({ name }) => {
359
- const joined = await joinRoomIdentifier(name, "join_room");
360
- await autoRegisterAgentIdentity();
361
421
  return {
362
422
  content: [
363
423
  {
364
424
  type: "text",
365
- text: JSON.stringify({ ...joined.response, joined_via: "join_room" }, null, 2),
425
+ text: JSON.stringify(await joinNamedRoom(name), null, 2),
366
426
  },
367
427
  ],
368
428
  };
@@ -385,7 +445,7 @@ server.tool("get_current_room", "Get information about the currently joined room
385
445
  type: "text",
386
446
  text: JSON.stringify({
387
447
  connected: true,
388
- ...currentRoom,
448
+ ...toPublicRoomState(currentRoom),
389
449
  auth: getStoredAuth()
390
450
  ? {
391
451
  source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
@@ -432,11 +492,11 @@ server.tool("check_repo", "Inspect the current repository context for Let Agents
432
492
  config_file: configPath ?? null,
433
493
  config_contents: configContents,
434
494
  derived_room_from_git: derivedRoom ?? null,
435
- current_room: currentRoom ?? null,
495
+ current_room: toPublicRoomState(currentRoom),
436
496
  join_hint: !currentRoom
437
497
  ? repoRoot
438
- ? "Run initialize_repo to set up .letagents.json, or join_room/join_project to connect."
439
- : "Not inside a git repo. Use join_project or join_room to connect manually."
498
+ ? "Run initialize_repo to set up .letagents.json, or use join_room/join_code to connect."
499
+ : "Not inside a git repo. Use create_room, join_code, or join_room to connect manually."
440
500
  : null,
441
501
  }, null, 2),
442
502
  },
@@ -454,13 +514,9 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
454
514
  .string()
455
515
  .optional()
456
516
  .describe("Canonical room ID. Defaults to the current room."),
457
- project_id: z
458
- .string()
459
- .optional()
460
- .describe("Legacy project ID. Defaults to the current room if joined."),
461
- }, async ({ sender, status, room_id, project_id }) => {
517
+ }, async ({ sender, status, room_id }) => {
462
518
  const targetRoomId = getTargetRoomId(room_id);
463
- const targetProjectId = getTargetProjectId(project_id);
519
+ const targetProjectId = getFallbackProjectId();
464
520
  if (!targetRoomId && !targetProjectId) {
465
521
  return {
466
522
  content: [
@@ -468,8 +524,8 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
468
524
  type: "text",
469
525
  text: JSON.stringify({
470
526
  success: false,
471
- error: "No room_id or project_id provided and not currently in a room.",
472
- hint: "Join a room first with join_project or join_room, or pass room_id explicitly.",
527
+ error: "No room_id provided and not currently in a room.",
528
+ hint: "Join or create a room first, or pass room_id explicitly.",
473
529
  }, null, 2),
474
530
  },
475
531
  ],
@@ -509,7 +565,7 @@ const TASK_STATUSES = [
509
565
  "proposed", "accepted", "assigned", "in_progress",
510
566
  "blocked", "in_review", "merged", "done", "cancelled",
511
567
  ];
512
- server.tool("add_task", "Add a new task to the project board. Tasks normally start as 'proposed' and must be " +
568
+ server.tool("add_task", "Add a new task to the room board. Tasks normally start as 'proposed' and must be " +
513
569
  "accepted before an agent can claim them, but tasks created by trusted agents already " +
514
570
  "active in the room may be auto-accepted. Use this when a human or agent identifies " +
515
571
  "work that needs to be done.", {
@@ -518,10 +574,9 @@ server.tool("add_task", "Add a new task to the project board. Tasks normally sta
518
574
  created_by: z.string().describe("Name of the agent or human creating the task"),
519
575
  source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
520
576
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
521
- project_id: z.string().optional().describe("Legacy project ID. Defaults to current room."),
522
- }, async ({ title, description, created_by, source_message_id, room_id, project_id }) => {
577
+ }, async ({ title, description, created_by, source_message_id, room_id }) => {
523
578
  const targetRoomId = getTargetRoomId(room_id);
524
- const targetProjectId = getTargetProjectId(project_id);
579
+ const targetProjectId = getFallbackProjectId();
525
580
  if (!targetRoomId && !targetProjectId) {
526
581
  return {
527
582
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
@@ -541,16 +596,15 @@ server.tool("add_task", "Add a new task to the project board. Tasks normally sta
541
596
  content: [{ type: "text", text: JSON.stringify({ success: true, task }, null, 2) }],
542
597
  };
543
598
  });
544
- server.tool("get_board", "Get the current task board for the project. By default shows only open tasks " +
599
+ server.tool("get_board", "Get the current task board for the room. By default shows only open tasks " +
545
600
  "(not done/cancelled). Agents should check this on startup and when idle to " +
546
601
  "see if there is unassigned work to claim.", {
547
602
  status: z.enum(TASK_STATUSES).optional().describe("Filter by specific status"),
548
603
  open_only: z.boolean().optional().describe("If true (default), only show tasks not done/cancelled"),
549
604
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
550
- project_id: z.string().optional().describe("Legacy project ID. Defaults to current room."),
551
- }, async ({ status, open_only, room_id, project_id }) => {
605
+ }, async ({ status, open_only, room_id }) => {
552
606
  const targetRoomId = getTargetRoomId(room_id);
553
- const targetProjectId = getTargetProjectId(project_id);
607
+ const targetProjectId = getFallbackProjectId();
554
608
  if (!targetRoomId && !targetProjectId) {
555
609
  return {
556
610
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
@@ -578,10 +632,9 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
578
632
  task_id: z.string().describe("The task ID to claim, e.g. 'task_1'"),
579
633
  assignee: z.string().describe("Your agent name, e.g. 'antigravity'"),
580
634
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
581
- project_id: z.string().optional().describe("Legacy project ID. Defaults to current room."),
582
- }, async ({ task_id, assignee, room_id, project_id }) => {
635
+ }, async ({ task_id, assignee, room_id }) => {
583
636
  const targetRoomId = getTargetRoomId(room_id);
584
- const targetProjectId = getTargetProjectId(project_id);
637
+ const targetProjectId = getFallbackProjectId();
585
638
  if (!targetRoomId && !targetProjectId) {
586
639
  return {
587
640
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
@@ -616,10 +669,9 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
616
669
  assignee: z.string().optional().describe("New assignee for the task"),
617
670
  pr_url: z.string().optional().describe("PR URL to link to the task"),
618
671
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
619
- project_id: z.string().optional().describe("Legacy project ID. Defaults to current room."),
620
- }, async ({ task_id, status, assignee, pr_url, room_id, project_id }) => {
672
+ }, async ({ task_id, status, assignee, pr_url, room_id }) => {
621
673
  const targetRoomId = getTargetRoomId(room_id);
622
- const targetProjectId = getTargetProjectId(project_id);
674
+ const targetProjectId = getFallbackProjectId();
623
675
  if (!targetRoomId && !targetProjectId) {
624
676
  return {
625
677
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
@@ -652,10 +704,9 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
652
704
  task_id: z.string().describe("The task ID to submit for review"),
653
705
  pr_url: z.string().optional().describe("GitHub PR URL for the work"),
654
706
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
655
- project_id: z.string().optional().describe("Legacy project ID. Defaults to current room."),
656
- }, async ({ task_id, pr_url, room_id, project_id }) => {
707
+ }, async ({ task_id, pr_url, room_id }) => {
657
708
  const targetRoomId = getTargetRoomId(room_id);
658
- const targetProjectId = getTargetProjectId(project_id);
709
+ const targetProjectId = getFallbackProjectId();
659
710
  if (!targetRoomId && !targetProjectId) {
660
711
  return {
661
712
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room." }) }],
@@ -799,7 +850,6 @@ server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat
799
850
  success: true,
800
851
  created: configPath,
801
852
  room_id: joined.room.room_id,
802
- project_id: joined.room.project_id ?? null,
803
853
  code: joined.room.code ?? null,
804
854
  joined: true,
805
855
  hint: "Consider adding .letagents.json to git so other contributors auto-join the same room.",
@@ -830,12 +880,11 @@ server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat
830
880
  // -- send_message -----------------------------------------------------------
831
881
  server.tool("send_message", "Send a message to a Let Agents Chat room.", {
832
882
  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
883
  sender: z.string().describe("Name identifying the sending agent (e.g. 'antigravity-agent')"),
835
884
  text: z.string().describe("The message text to send"),
836
- }, async ({ room_id, project_id, sender, text }) => {
885
+ }, async ({ room_id, sender, text }) => {
837
886
  const targetRoomId = getTargetRoomId(room_id);
838
- const targetProjectId = getTargetProjectId(project_id);
887
+ const targetProjectId = getFallbackProjectId();
839
888
  if (!targetRoomId && !targetProjectId) {
840
889
  throw new Error("No room is currently selected. Join a room first or pass room_id.");
841
890
  }
@@ -862,10 +911,9 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
862
911
  // -- read_messages ----------------------------------------------------------
863
912
  server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
864
913
  room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
865
- project_id: z.string().optional().describe("Legacy project ID. Defaults to the current room."),
866
- }, async ({ room_id, project_id }) => {
914
+ }, async ({ room_id }) => {
867
915
  const targetRoomId = getTargetRoomId(room_id);
868
- const targetProjectId = getTargetProjectId(project_id);
916
+ const targetProjectId = getFallbackProjectId();
869
917
  const result = await roomScopedApiCall({
870
918
  room_id: targetRoomId,
871
919
  project_id: targetProjectId,
@@ -886,7 +934,6 @@ const MAX_POLL_TIMEOUT_MS = 180000; // 3 minutes
886
934
  const DEFAULT_POLL_TIMEOUT_MS = 30000; // 30 seconds
887
935
  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
936
  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
937
  after_message_id: z
891
938
  .string()
892
939
  .optional()
@@ -895,9 +942,9 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
895
942
  .number()
896
943
  .optional()
897
944
  .describe("Maximum wait time in milliseconds. If set to 0, the default timeout will be used."),
898
- }, async ({ room_id, project_id, after_message_id, timeout }) => {
945
+ }, async ({ room_id, after_message_id, timeout }) => {
899
946
  const targetRoomId = getTargetRoomId(room_id);
900
- const targetProjectId = getTargetProjectId(project_id);
947
+ const targetProjectId = getFallbackProjectId();
901
948
  const serverTimeout = Math.min(Math.max(timeout || DEFAULT_POLL_TIMEOUT_MS, 1000), MAX_POLL_TIMEOUT_MS);
902
949
  const clientTimeout = serverTimeout + 5000; // 5s buffer over server timeout
903
950
  const params = new URLSearchParams();
@@ -962,8 +1009,8 @@ server.tool("get_onboarding_status", "Inspect local Let Agents MCP auth and room
962
1009
  account: storedAuth?.account ?? null,
963
1010
  token_expires_at: storedAuth?.expires_at ?? null,
964
1011
  pending_device_auth: pendingAuth,
965
- current_room: currentRoom,
966
- saved_current_room: savedCurrentRoom,
1012
+ current_room: toPublicRoomState(currentRoom),
1013
+ saved_current_room: toPublicStoredRoomSession(savedCurrentRoom),
967
1014
  detected_room_from_context: detectedRoom,
968
1015
  repo_root: repoRoot,
969
1016
  next_step: nextStep,
@@ -1197,7 +1244,7 @@ server.tool("check_repo_visibility", "Auto-detect the current repo's git remote
1197
1244
  type: "text",
1198
1245
  text: JSON.stringify({
1199
1246
  error: "Not in a git repository or no remote configured",
1200
- suggestion: "Use create_project to create an invite room instead",
1247
+ suggestion: "Use create_room to create an invite room instead",
1201
1248
  }, null, 2),
1202
1249
  },
1203
1250
  ],
@@ -1243,7 +1290,7 @@ async function main() {
1243
1290
  return;
1244
1291
  }
1245
1292
  // 4. No context found
1246
- console.error("ℹ️ No .letagents.json, git remote, or saved room found — use join_project or join_room to connect.");
1293
+ console.error("ℹ️ No .letagents.json, git remote, or saved room found — use create_room, join_code, or join_room to connect.");
1247
1294
  }
1248
1295
  catch (err) {
1249
1296
  // Auto-join failure should never block the MCP server
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Let Agents Chat — MCP server for AI agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/server.js",