@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.
- package/dist/index.js +219 -13
- 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,
|
|
24
|
-
const url = `${this.baseUrl}/api/v1${
|
|
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} ${
|
|
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", `/
|
|
110
|
+
return this.request("GET", `/contexts${query ? `?${query}` : ""}`);
|
|
111
111
|
}
|
|
112
112
|
getContextEntry(identifier) {
|
|
113
|
-
return this.request("GET", `/
|
|
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", `/
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
700
|
-
summary:
|
|
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.
|
|
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);
|