@limeadelabs/launchpad-mcp 1.1.0 → 1.2.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.
Files changed (2) hide show
  1. package/dist/index.js +243 -37
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -20,8 +20,8 @@ var LaunchPadClient = class {
20
20
  this.apiKey = config.apiKey;
21
21
  this.timeoutMs = config.timeoutMs ?? 1e4;
22
22
  }
23
- async request(method, path, body) {
24
- const url = `${this.baseUrl}/api/v1${path}`;
23
+ async request(method, path2, body) {
24
+ const url = `${this.baseUrl}/api/v1${path2}`;
25
25
  const res = await fetch(url, {
26
26
  method,
27
27
  headers: {
@@ -34,7 +34,7 @@ var LaunchPadClient = class {
34
34
  if (!res.ok) {
35
35
  const error = await res.json().catch(() => ({ error: res.statusText }));
36
36
  throw new LaunchPadError(
37
- `${method} ${path}: ${res.status} \u2014 ${error.error || res.statusText}`,
37
+ `${method} ${path2}: ${res.status} \u2014 ${error.error || res.statusText}`,
38
38
  res.status
39
39
  );
40
40
  }
@@ -107,16 +107,35 @@ var LaunchPadClient = class {
107
107
  if (params?.limit !== void 0) searchParams.set("limit", String(params.limit));
108
108
  if (params?.offset !== void 0) searchParams.set("offset", String(params.offset));
109
109
  const query = searchParams.toString();
110
- return this.request("GET", `/context_entries${query ? `?${query}` : ""}`);
110
+ return this.request("GET", `/contexts${query ? `?${query}` : ""}`);
111
111
  }
112
112
  getContextEntry(identifier) {
113
- return this.request("GET", `/context_entries/${encodeURIComponent(identifier)}`);
113
+ return this.request("GET", `/contexts/${encodeURIComponent(identifier)}`);
114
114
  }
115
115
  getTaskContextPackage(taskId) {
116
116
  return this.request("GET", `/tasks/${taskId}/context`);
117
117
  }
118
118
  updateContextEntry(identifier, data) {
119
- return this.request("PATCH", `/context_entries/${encodeURIComponent(identifier)}`, data);
119
+ return this.request("PATCH", `/contexts/${encodeURIComponent(identifier)}`, data);
120
+ }
121
+ createSession(taskId, agentType, agentId) {
122
+ return this.request("POST", "/sessions", {
123
+ task_id: taskId,
124
+ agent_type: agentType ?? "claude_code",
125
+ ...agentId !== void 0 && { agent_id: agentId }
126
+ });
127
+ }
128
+ sessionHeartbeat(sessionId) {
129
+ return this.request("POST", `/sessions/${sessionId}/heartbeat`);
130
+ }
131
+ updateSession(sessionId, data) {
132
+ return this.request("PATCH", `/sessions/${sessionId}`, data);
133
+ }
134
+ createSessionEvent(sessionId, eventType, payload) {
135
+ return this.request("POST", `/sessions/${sessionId}/events`, {
136
+ event_type: eventType,
137
+ payload
138
+ });
120
139
  }
121
140
  };
122
141
 
@@ -165,7 +184,7 @@ function registerProjectTools(server2, client2) {
165
184
  "lp_get_project",
166
185
  "Get project details including agent_instructions",
167
186
  {
168
- project_id: z.number().describe("Project ID")
187
+ project_id: z.coerce.number().describe("Project ID")
169
188
  },
170
189
  async (params) => {
171
190
  try {
@@ -185,11 +204,11 @@ function registerTaskTools(server2, client2) {
185
204
  "lp_list_tasks",
186
205
  "List tasks from LaunchPad with optional filters (project, status, priority, label, assignee)",
187
206
  {
188
- project_id: z2.number().optional().describe("Filter by project ID"),
207
+ project_id: z2.coerce.number().optional().describe("Filter by project ID"),
189
208
  status: z2.string().optional().describe("Filter by status: todo, ready, in_progress, review, done"),
190
209
  priority: z2.string().optional().describe("Filter by priority: low, medium, high"),
191
210
  label: z2.string().optional().describe("Filter by label name"),
192
- assignee_id: z2.number().optional().describe("Filter by assignee user ID")
211
+ assignee_id: z2.coerce.number().optional().describe("Filter by assignee user ID")
193
212
  },
194
213
  async (params) => {
195
214
  try {
@@ -204,7 +223,7 @@ function registerTaskTools(server2, client2) {
204
223
  "lp_get_task",
205
224
  "Get full task context \u2014 description, comments, links, specs",
206
225
  {
207
- task_id: z2.number().describe("Task ID")
226
+ task_id: z2.coerce.number().describe("Task ID")
208
227
  },
209
228
  async (params) => {
210
229
  try {
@@ -219,7 +238,7 @@ function registerTaskTools(server2, client2) {
219
238
  "lp_create_task",
220
239
  "Create a new task in LaunchPad",
221
240
  {
222
- project_id: z2.number().describe("Project ID to create the task in"),
241
+ project_id: z2.coerce.number().describe("Project ID to create the task in"),
223
242
  title: z2.string().describe("Task title"),
224
243
  description: z2.string().optional().describe("Task description"),
225
244
  status: z2.string().optional().describe("Initial status: todo, ready, in_progress, review, done"),
@@ -238,10 +257,10 @@ function registerTaskTools(server2, client2) {
238
257
  "lp_update_task",
239
258
  "Update task status and/or fields. LaunchPad enforces workflow transitions \u2014 use lp_get_workflow to check valid transitions before changing status.",
240
259
  {
241
- task_id: z2.number().describe("Task ID"),
260
+ task_id: z2.coerce.number().describe("Task ID"),
242
261
  status: z2.string().optional().describe("New status (must be a valid workflow transition)"),
243
262
  priority: z2.string().optional().describe("New priority: low, medium, high"),
244
- assignee_id: z2.number().optional().describe("Assignee user ID"),
263
+ assignee_id: z2.coerce.number().optional().describe("Assignee user ID"),
245
264
  title: z2.string().optional().describe("New title"),
246
265
  description: z2.string().optional().describe("New description")
247
266
  },
@@ -264,7 +283,7 @@ function registerClaimTools(server2, client2) {
264
283
  "lp_claim_task",
265
284
  "Claim a task before working on it. Prevents other agents from picking it up.",
266
285
  {
267
- task_id: z3.number().describe("Task ID to claim")
286
+ task_id: z3.coerce.number().describe("Task ID to claim")
268
287
  },
269
288
  async (params) => {
270
289
  try {
@@ -279,7 +298,7 @@ function registerClaimTools(server2, client2) {
279
298
  "lp_release_task",
280
299
  "Release a claimed task so others can pick it up",
281
300
  {
282
- task_id: z3.number().describe("Task ID to release")
301
+ task_id: z3.coerce.number().describe("Task ID to release")
283
302
  },
284
303
  async (params) => {
285
304
  try {
@@ -294,7 +313,7 @@ function registerClaimTools(server2, client2) {
294
313
  "lp_heartbeat",
295
314
  "Send a heartbeat for a claimed task to keep the claim alive. Claims expire after 30 minutes without a heartbeat.",
296
315
  {
297
- task_id: z3.number().describe("Task ID to heartbeat")
316
+ task_id: z3.coerce.number().describe("Task ID to heartbeat")
298
317
  },
299
318
  async (params) => {
300
319
  try {
@@ -314,7 +333,7 @@ function registerCommentTools(server2, client2) {
314
333
  "lp_add_comment",
315
334
  "Post a comment on a task (progress updates, notes, questions)",
316
335
  {
317
- task_id: z4.number().describe("Task ID to comment on"),
336
+ task_id: z4.coerce.number().describe("Task ID to comment on"),
318
337
  body: z4.string().describe("Comment body text")
319
338
  },
320
339
  async (params) => {
@@ -335,8 +354,8 @@ function registerTimeTools(server2, client2) {
335
354
  "lp_log_time",
336
355
  "Track time spent on a task",
337
356
  {
338
- task_id: z5.number().describe("Task ID"),
339
- duration_minutes: z5.number().positive().describe("Duration in minutes"),
357
+ task_id: z5.coerce.number().describe("Task ID"),
358
+ duration_minutes: z5.coerce.number().positive().describe("Duration in minutes"),
340
359
  description: z5.string().optional().describe("Description of work done")
341
360
  },
342
361
  async (params) => {
@@ -357,7 +376,7 @@ function registerPromptTools(server2, client2) {
357
376
  "lp_generate_prompt",
358
377
  "Generate a build-ready prompt/spec for a task with full context, conventions, and acceptance criteria",
359
378
  {
360
- task_id: z6.number().describe("Task ID")
379
+ task_id: z6.coerce.number().describe("Task ID")
361
380
  },
362
381
  async (params) => {
363
382
  try {
@@ -377,7 +396,7 @@ function registerPageTools(server2, client2) {
377
396
  "lp_list_pages",
378
397
  "List spec/doc pages for a project",
379
398
  {
380
- project_id: z7.number().describe("Project ID")
399
+ project_id: z7.coerce.number().describe("Project ID")
381
400
  },
382
401
  async (params) => {
383
402
  try {
@@ -392,8 +411,8 @@ function registerPageTools(server2, client2) {
392
411
  "lp_get_page",
393
412
  "Get a page's full content",
394
413
  {
395
- project_id: z7.number().describe("Project ID"),
396
- page_id: z7.number().describe("Page ID")
414
+ project_id: z7.coerce.number().describe("Project ID"),
415
+ page_id: z7.coerce.number().describe("Page ID")
397
416
  },
398
417
  async (params) => {
399
418
  try {
@@ -408,7 +427,7 @@ function registerPageTools(server2, client2) {
408
427
  "lp_get_workflow",
409
428
  "Get valid workflow states and transitions for a project",
410
429
  {
411
- project_id: z7.number().describe("Project ID")
430
+ project_id: z7.coerce.number().describe("Project ID")
412
431
  },
413
432
  async (params) => {
414
433
  try {
@@ -428,11 +447,11 @@ function registerContextTools(server2, client2) {
428
447
  "lp_context_list",
429
448
  "List available context entries with optional filters",
430
449
  {
431
- project_id: z8.number().optional().describe("Filter by project ID"),
450
+ project_id: z8.coerce.number().optional().describe("Filter by project ID"),
432
451
  entry_type: z8.string().optional().describe("Filter by entry type"),
433
452
  search: z8.string().optional().describe("Text search on entry name"),
434
- limit: z8.number().min(1).max(100).optional().describe("Max entries to return (default 50)"),
435
- offset: z8.number().min(0).optional().describe("Offset for pagination")
453
+ limit: z8.coerce.number().min(1).max(100).optional().describe("Max entries to return (default 50)"),
454
+ offset: z8.coerce.number().min(0).optional().describe("Offset for pagination")
436
455
  },
437
456
  async (params) => {
438
457
  try {
@@ -448,7 +467,7 @@ function registerContextTools(server2, client2) {
448
467
  "Fetch a specific context entry by slug or ID",
449
468
  {
450
469
  slug: z8.string().optional().describe("Entry slug"),
451
- id: z8.number().optional().describe("Entry ID")
470
+ id: z8.coerce.number().optional().describe("Entry ID")
452
471
  },
453
472
  async (params) => {
454
473
  const identifier = params.slug || (params.id !== void 0 ? String(params.id) : void 0);
@@ -468,9 +487,9 @@ function registerContextTools(server2, client2) {
468
487
  );
469
488
  server2.tool(
470
489
  "lp_context_package",
471
- "Get the assembled context package for a task \u2014 org entries, project entries, specs, comments",
490
+ "Get the assembled context package for a task \u2014 task details, context entries, specs, pages, comments",
472
491
  {
473
- task_id: z8.number().describe("Task ID")
492
+ task_id: z8.coerce.number().describe("Task ID")
474
493
  },
475
494
  async (params) => {
476
495
  try {
@@ -486,7 +505,7 @@ function registerContextTools(server2, client2) {
486
505
  "Update a context entry's content (requires write access)",
487
506
  {
488
507
  slug: z8.string().optional().describe("Entry slug"),
489
- id: z8.number().optional().describe("Entry ID"),
508
+ id: z8.coerce.number().optional().describe("Entry ID"),
490
509
  content: z8.string().describe("New markdown content"),
491
510
  change_summary: z8.string().describe("What changed and why")
492
511
  },
@@ -511,6 +530,191 @@ function registerContextTools(server2, client2) {
511
530
  );
512
531
  }
513
532
 
533
+ // src/tools/sessions.ts
534
+ import { z as z9 } from "zod";
535
+
536
+ // src/session-store.ts
537
+ import fs from "fs";
538
+ import os from "os";
539
+ import path from "path";
540
+ var SESSION_FILE = path.join(os.homedir(), ".launchpad", "active-session.json");
541
+ function saveSession(taskId, sessionId) {
542
+ const dir = path.dirname(SESSION_FILE);
543
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
544
+ const data = fs.existsSync(SESSION_FILE) ? JSON.parse(fs.readFileSync(SESSION_FILE, "utf8")) : {};
545
+ data[String(taskId)] = sessionId;
546
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2));
547
+ }
548
+ function getSessionId(taskId) {
549
+ if (!fs.existsSync(SESSION_FILE)) return null;
550
+ const data = JSON.parse(fs.readFileSync(SESSION_FILE, "utf8"));
551
+ return data[String(taskId)] ?? null;
552
+ }
553
+ function clearSession(taskId) {
554
+ if (!fs.existsSync(SESSION_FILE)) return;
555
+ const data = JSON.parse(fs.readFileSync(SESSION_FILE, "utf8"));
556
+ delete data[String(taskId)];
557
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2));
558
+ }
559
+
560
+ // src/tools/sessions.ts
561
+ function registerSessionTools(server2, client2) {
562
+ server2.tool(
563
+ "lp_session_start",
564
+ "Start a new agent session for a task. Creates session via API and saves session_id to disk for later lookup.",
565
+ {
566
+ task_id: z9.coerce.number().describe("LaunchPad task ID"),
567
+ agent_type: z9.string().optional().describe("Agent type (default: claude_code)")
568
+ },
569
+ async ({ task_id, agent_type }) => {
570
+ try {
571
+ const result = await client2.createSession(task_id, agent_type);
572
+ saveSession(task_id, result.session.id);
573
+ return {
574
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
575
+ };
576
+ } catch (error) {
577
+ return handleError(error, client2.timeoutMs);
578
+ }
579
+ }
580
+ );
581
+ server2.tool(
582
+ "lp_session_heartbeat",
583
+ "Send a heartbeat for an active session. Provide session_id directly or task_id to look up session from disk. Graceful no-op if neither provided.",
584
+ {
585
+ session_id: z9.coerce.number().optional().describe("Session ID"),
586
+ task_id: z9.coerce.number().optional().describe("Task ID to look up session from disk")
587
+ },
588
+ async ({ session_id, task_id }) => {
589
+ try {
590
+ const resolvedId = session_id ?? (task_id !== void 0 ? getSessionId(task_id) : null);
591
+ if (resolvedId === null) {
592
+ return {
593
+ content: [{ type: "text", text: "No active session found. Provide session_id or task_id." }]
594
+ };
595
+ }
596
+ const result = await client2.sessionHeartbeat(resolvedId);
597
+ return {
598
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
599
+ };
600
+ } catch (error) {
601
+ return handleError(error, client2.timeoutMs);
602
+ }
603
+ }
604
+ );
605
+ server2.tool(
606
+ "lp_session_progress",
607
+ 'Report progress on an active session. Sends an activity update with action "progress".',
608
+ {
609
+ session_id: z9.coerce.number().describe("Session ID"),
610
+ detail: z9.string().describe("Description of progress made")
611
+ },
612
+ async ({ session_id, detail }) => {
613
+ try {
614
+ const result = await client2.updateSession(session_id, {
615
+ activity: { action: "progress", detail }
616
+ });
617
+ return {
618
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
619
+ };
620
+ } catch (error) {
621
+ return handleError(error, client2.timeoutMs);
622
+ }
623
+ }
624
+ );
625
+ server2.tool(
626
+ "lp_session_event",
627
+ "Log a discrete event for a session (commit, ci_pass, ci_fail, pr_opened, blocker, cost_update).",
628
+ {
629
+ session_id: z9.coerce.number().describe("Session ID"),
630
+ event_type: z9.enum(["commit", "ci_pass", "ci_fail", "pr_opened", "blocker", "cost_update"]).describe("Type of event"),
631
+ payload: z9.record(z9.unknown()).describe("Event payload data")
632
+ },
633
+ async ({ session_id, event_type, payload }) => {
634
+ try {
635
+ const result = await client2.createSessionEvent(session_id, event_type, payload);
636
+ return {
637
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
638
+ };
639
+ } catch (error) {
640
+ return handleError(error, client2.timeoutMs);
641
+ }
642
+ }
643
+ );
644
+ server2.tool(
645
+ "lp_session_blocked",
646
+ 'Mark a session as blocked. Updates status to "blocked" and logs a blocker event.',
647
+ {
648
+ session_id: z9.coerce.number().describe("Session ID"),
649
+ reason: z9.string().describe("Reason the session is blocked")
650
+ },
651
+ async ({ session_id, reason }) => {
652
+ try {
653
+ const [sessionResult, eventResult] = await Promise.all([
654
+ client2.updateSession(session_id, { status: "blocked" }),
655
+ client2.createSessionEvent(session_id, "blocker", { reason })
656
+ ]);
657
+ return {
658
+ content: [{
659
+ type: "text",
660
+ text: JSON.stringify({ session: sessionResult.session, event: eventResult.event }, null, 2)
661
+ }]
662
+ };
663
+ } catch (error) {
664
+ return handleError(error, client2.timeoutMs);
665
+ }
666
+ }
667
+ );
668
+ server2.tool(
669
+ "lp_session_complete",
670
+ "Mark a session as completed with a result summary. Clears session from disk.",
671
+ {
672
+ session_id: z9.coerce.number().describe("Session ID"),
673
+ result_summary: z9.string().describe("Summary of what was accomplished"),
674
+ task_id: z9.coerce.number().optional().describe("Task ID to clear from disk (if not provided, derived from session)")
675
+ },
676
+ async ({ session_id, result_summary, task_id }) => {
677
+ try {
678
+ const result = await client2.updateSession(session_id, {
679
+ status: "completed",
680
+ result_summary
681
+ });
682
+ const resolvedTaskId = task_id ?? result.session.task_id;
683
+ clearSession(resolvedTaskId);
684
+ return {
685
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
686
+ };
687
+ } catch (error) {
688
+ return handleError(error, client2.timeoutMs);
689
+ }
690
+ }
691
+ );
692
+ server2.tool(
693
+ "lp_session_fail",
694
+ "Mark a session as failed with an error detail. Clears session from disk.",
695
+ {
696
+ session_id: z9.coerce.number().describe("Session ID"),
697
+ error_detail: z9.string().describe("Description of the failure"),
698
+ task_id: z9.coerce.number().optional().describe("Task ID to clear from disk (if not provided, derived from session)")
699
+ },
700
+ async ({ session_id, error_detail, task_id }) => {
701
+ try {
702
+ const result = await client2.updateSession(session_id, {
703
+ status: "failed",
704
+ result_summary: error_detail
705
+ });
706
+ const resolvedTaskId = task_id ?? result.session.task_id;
707
+ clearSession(resolvedTaskId);
708
+ return {
709
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
710
+ };
711
+ } catch (error) {
712
+ return handleError(error, client2.timeoutMs);
713
+ }
714
+ }
715
+ );
716
+ }
717
+
514
718
  // src/resources/project-context.ts
515
719
  function registerProjectContextResource(server2, client2) {
516
720
  server2.resource(
@@ -625,6 +829,7 @@ function registerContextEntriesResource(server2, client2) {
625
829
  throw new Error(`Invalid URI: ${uri.href}`);
626
830
  }
627
831
  const slug = decodeURIComponent(uri.pathname.slice(1));
832
+ if (!slug) throw new Error("Empty slug in context entry URI");
628
833
  const result = await client2.getContextEntry(slug);
629
834
  const entry = result.entry;
630
835
  const text = [
@@ -654,13 +859,13 @@ function registerContextEntriesResource(server2, client2) {
654
859
  }
655
860
 
656
861
  // src/prompts/start-task.ts
657
- import { z as z9 } from "zod";
862
+ import { z as z10 } from "zod";
658
863
  function registerStartTaskPrompt(server2) {
659
864
  server2.prompt(
660
865
  "lp_start_task",
661
866
  "Guided workflow: find a task, claim it, get context, start working",
662
867
  {
663
- project_id: z9.string().optional().describe("Optional project ID to filter tasks")
868
+ project_id: z10.string().optional().describe("Optional project ID to filter tasks")
664
869
  },
665
870
  async (args) => {
666
871
  const projectLine = args.project_id ? ` Project ID: ${args.project_id}.` : "";
@@ -690,14 +895,14 @@ IMPORTANT: If this task takes more than 20 minutes, call lp_heartbeat every 20 m
690
895
  }
691
896
 
692
897
  // src/prompts/submit-task.ts
693
- import { z as z10 } from "zod";
898
+ import { z as z11 } from "zod";
694
899
  function registerSubmitTaskPrompt(server2) {
695
900
  server2.prompt(
696
901
  "lp_submit_task",
697
902
  "Guided workflow: mark task done, add summary comment, release claim",
698
903
  {
699
- task_id: z10.string().describe("Task ID to submit"),
700
- summary: z10.string().describe("Summary of what was completed")
904
+ task_id: z11.string().describe("Task ID to submit"),
905
+ summary: z11.string().describe("Summary of what was completed")
701
906
  },
702
907
  async (args) => {
703
908
  return {
@@ -746,7 +951,7 @@ try {
746
951
  }
747
952
  var server = new McpServer({
748
953
  name: "launchpad",
749
- version: "1.1.0"
954
+ version: "1.2.0"
750
955
  });
751
956
  registerBootstrapTool(server, client);
752
957
  registerProjectTools(server, client);
@@ -757,6 +962,7 @@ registerTimeTools(server, client);
757
962
  registerPromptTools(server, client);
758
963
  registerPageTools(server, client);
759
964
  registerContextTools(server, client);
965
+ registerSessionTools(server, client);
760
966
  registerProjectContextResource(server, client);
761
967
  registerTaskSpecResource(server, client);
762
968
  registerContextEntriesResource(server, client);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limeadelabs/launchpad-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "LaunchPad MCP server for Claude Code — AI-native project management integration",
5
5
  "type": "module",
6
6
  "exports": {