@limeadelabs/launchpad-mcp 1.1.0 → 1.2.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/index.js +219 -13
  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
 
@@ -468,7 +487,7 @@ 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
492
  task_id: z8.number().describe("Task ID")
474
493
  },
@@ -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.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.number().optional().describe("Session ID"),
586
+ task_id: z9.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.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.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.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.number().describe("Session ID"),
673
+ result_summary: z9.string().describe("Summary of what was accomplished"),
674
+ task_id: z9.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.number().describe("Session ID"),
697
+ error_detail: z9.string().describe("Description of the failure"),
698
+ task_id: z9.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.0",
4
4
  "description": "LaunchPad MCP server for Claude Code — AI-native project management integration",
5
5
  "type": "module",
6
6
  "exports": {