@limeadelabs/launchpad-mcp 1.0.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 (3) hide show
  1. package/README.md +10 -0
  2. package/dist/index.js +391 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -54,11 +54,21 @@ Or add manually to `~/.claude/claude_desktop_config.json`:
54
54
  | `lp_list_pages` | List spec/doc pages for a project |
55
55
  | `lp_get_page` | Get a page's full content |
56
56
  | `lp_get_workflow` | Get valid workflow states and transitions |
57
+ | `lp_context_list` | List context entries with optional filters (project, type, search) |
58
+ | `lp_context_get` | Fetch a context entry's full content by slug or ID |
59
+ | `lp_context_package` | Get assembled context package for a task (org + project entries, specs, comments) |
60
+ | `lp_context_update` | Update a context entry's content (requires write access) |
57
61
 
58
62
  ## Resources
59
63
 
60
64
  - `launchpad://project/{id}/context` — Project instructions, conventions, and page summaries
61
65
  - `launchpad://task/{id}/spec` — Full generated task spec
66
+ - `context://entries` — Browse all active context entries
67
+ - `context://entries/{slug}` — Read a specific context entry
68
+
69
+ ### Resources vs Tools
70
+
71
+ Resources are for **passive browsing** in the Claude Code resource panel. Tools are for **active agent invocation** during conversation. Both access the same underlying context registry API.
62
72
 
63
73
  ## Prompts
64
74
 
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
  }
@@ -99,6 +99,44 @@ var LaunchPadClient = class {
99
99
  getWorkflow(projectId) {
100
100
  return this.request("GET", `/projects/${projectId}/workflow`);
101
101
  }
102
+ listContextEntries(params) {
103
+ const searchParams = new URLSearchParams();
104
+ if (params?.project_id !== void 0) searchParams.set("project_id", String(params.project_id));
105
+ if (params?.entry_type) searchParams.set("entry_type", params.entry_type);
106
+ if (params?.search) searchParams.set("search", params.search);
107
+ if (params?.limit !== void 0) searchParams.set("limit", String(params.limit));
108
+ if (params?.offset !== void 0) searchParams.set("offset", String(params.offset));
109
+ const query = searchParams.toString();
110
+ return this.request("GET", `/contexts${query ? `?${query}` : ""}`);
111
+ }
112
+ getContextEntry(identifier) {
113
+ return this.request("GET", `/contexts/${encodeURIComponent(identifier)}`);
114
+ }
115
+ getTaskContextPackage(taskId) {
116
+ return this.request("GET", `/tasks/${taskId}/context`);
117
+ }
118
+ updateContextEntry(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
+ });
139
+ }
102
140
  };
103
141
 
104
142
  // src/tools/shared.ts
@@ -402,6 +440,281 @@ function registerPageTools(server2, client2) {
402
440
  );
403
441
  }
404
442
 
443
+ // src/tools/context.ts
444
+ import { z as z8 } from "zod";
445
+ function registerContextTools(server2, client2) {
446
+ server2.tool(
447
+ "lp_context_list",
448
+ "List available context entries with optional filters",
449
+ {
450
+ project_id: z8.number().optional().describe("Filter by project ID"),
451
+ entry_type: z8.string().optional().describe("Filter by entry type"),
452
+ search: z8.string().optional().describe("Text search on entry name"),
453
+ limit: z8.number().min(1).max(100).optional().describe("Max entries to return (default 50)"),
454
+ offset: z8.number().min(0).optional().describe("Offset for pagination")
455
+ },
456
+ async (params) => {
457
+ try {
458
+ const result = await client2.listContextEntries(params);
459
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
460
+ } catch (error) {
461
+ return handleError(error, client2.timeoutMs);
462
+ }
463
+ }
464
+ );
465
+ server2.tool(
466
+ "lp_context_get",
467
+ "Fetch a specific context entry by slug or ID",
468
+ {
469
+ slug: z8.string().optional().describe("Entry slug"),
470
+ id: z8.number().optional().describe("Entry ID")
471
+ },
472
+ async (params) => {
473
+ const identifier = params.slug || (params.id !== void 0 ? String(params.id) : void 0);
474
+ if (!identifier) {
475
+ return {
476
+ content: [{ type: "text", text: JSON.stringify({ error: true, code: "validation_error", message: "Either slug or id is required" }) }],
477
+ isError: true
478
+ };
479
+ }
480
+ try {
481
+ const result = await client2.getContextEntry(identifier);
482
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
483
+ } catch (error) {
484
+ return handleError(error, client2.timeoutMs);
485
+ }
486
+ }
487
+ );
488
+ server2.tool(
489
+ "lp_context_package",
490
+ "Get the assembled context package for a task \u2014 task details, context entries, specs, pages, comments",
491
+ {
492
+ task_id: z8.number().describe("Task ID")
493
+ },
494
+ async (params) => {
495
+ try {
496
+ const result = await client2.getTaskContextPackage(params.task_id);
497
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
498
+ } catch (error) {
499
+ return handleError(error, client2.timeoutMs);
500
+ }
501
+ }
502
+ );
503
+ server2.tool(
504
+ "lp_context_update",
505
+ "Update a context entry's content (requires write access)",
506
+ {
507
+ slug: z8.string().optional().describe("Entry slug"),
508
+ id: z8.number().optional().describe("Entry ID"),
509
+ content: z8.string().describe("New markdown content"),
510
+ change_summary: z8.string().describe("What changed and why")
511
+ },
512
+ async (params) => {
513
+ const identifier = params.slug || (params.id !== void 0 ? String(params.id) : void 0);
514
+ if (!identifier) {
515
+ return {
516
+ content: [{ type: "text", text: JSON.stringify({ error: true, code: "validation_error", message: "Either slug or id is required" }) }],
517
+ isError: true
518
+ };
519
+ }
520
+ try {
521
+ const result = await client2.updateContextEntry(identifier, {
522
+ content: params.content,
523
+ change_summary: params.change_summary
524
+ });
525
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
526
+ } catch (error) {
527
+ return handleError(error, client2.timeoutMs);
528
+ }
529
+ }
530
+ );
531
+ }
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
+
405
718
  // src/resources/project-context.ts
406
719
  function registerProjectContextResource(server2, client2) {
407
720
  server2.resource(
@@ -479,14 +792,80 @@ function registerTaskSpecResource(server2, client2) {
479
792
  );
480
793
  }
481
794
 
795
+ // src/resources/context-entries.ts
796
+ function registerContextEntriesResource(server2, client2) {
797
+ server2.resource(
798
+ "context-entries",
799
+ "context://entries",
800
+ {
801
+ description: "Browse all active context entries"
802
+ },
803
+ async (uri) => {
804
+ const result = await client2.listContextEntries();
805
+ const lines = ["# Context Entries\n"];
806
+ for (const entry of result.entries) {
807
+ const project = entry.project ? ` (${entry.project.name})` : "";
808
+ lines.push(`- **${entry.name}**${project} \u2014 \`${entry.slug}\` [${entry.entry_type}] (v${entry.version_count}, updated ${entry.updated_at})`);
809
+ }
810
+ return {
811
+ contents: [
812
+ {
813
+ uri: uri.href,
814
+ mimeType: "text/markdown",
815
+ text: lines.join("\n")
816
+ }
817
+ ]
818
+ };
819
+ }
820
+ );
821
+ server2.resource(
822
+ "context-entry",
823
+ "context://entries/{slug}",
824
+ {
825
+ description: "Read a specific context entry by slug"
826
+ },
827
+ async (uri) => {
828
+ if (uri.host !== "entries" || !uri.pathname || uri.pathname === "/") {
829
+ throw new Error(`Invalid URI: ${uri.href}`);
830
+ }
831
+ const slug = decodeURIComponent(uri.pathname.slice(1));
832
+ if (!slug) throw new Error("Empty slug in context entry URI");
833
+ const result = await client2.getContextEntry(slug);
834
+ const entry = result.entry;
835
+ const text = [
836
+ `# ${entry.name}`,
837
+ ``,
838
+ `- **Type:** ${entry.entry_type}`,
839
+ `- **Slug:** ${entry.slug}`,
840
+ `- **Version:** ${entry.version}`,
841
+ `- **Updated:** ${entry.updated_at}`,
842
+ entry.project ? `- **Project:** ${entry.project.name}` : null,
843
+ ``,
844
+ `---`,
845
+ ``,
846
+ entry.content
847
+ ].filter(Boolean).join("\n");
848
+ return {
849
+ contents: [
850
+ {
851
+ uri: uri.href,
852
+ mimeType: "text/markdown",
853
+ text
854
+ }
855
+ ]
856
+ };
857
+ }
858
+ );
859
+ }
860
+
482
861
  // src/prompts/start-task.ts
483
- import { z as z8 } from "zod";
862
+ import { z as z10 } from "zod";
484
863
  function registerStartTaskPrompt(server2) {
485
864
  server2.prompt(
486
865
  "lp_start_task",
487
866
  "Guided workflow: find a task, claim it, get context, start working",
488
867
  {
489
- project_id: z8.string().optional().describe("Optional project ID to filter tasks")
868
+ project_id: z10.string().optional().describe("Optional project ID to filter tasks")
490
869
  },
491
870
  async (args) => {
492
871
  const projectLine = args.project_id ? ` Project ID: ${args.project_id}.` : "";
@@ -516,14 +895,14 @@ IMPORTANT: If this task takes more than 20 minutes, call lp_heartbeat every 20 m
516
895
  }
517
896
 
518
897
  // src/prompts/submit-task.ts
519
- import { z as z9 } from "zod";
898
+ import { z as z11 } from "zod";
520
899
  function registerSubmitTaskPrompt(server2) {
521
900
  server2.prompt(
522
901
  "lp_submit_task",
523
902
  "Guided workflow: mark task done, add summary comment, release claim",
524
903
  {
525
- task_id: z9.string().describe("Task ID to submit"),
526
- summary: z9.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")
527
906
  },
528
907
  async (args) => {
529
908
  return {
@@ -572,7 +951,7 @@ try {
572
951
  }
573
952
  var server = new McpServer({
574
953
  name: "launchpad",
575
- version: "1.0.0"
954
+ version: "1.2.0"
576
955
  });
577
956
  registerBootstrapTool(server, client);
578
957
  registerProjectTools(server, client);
@@ -582,8 +961,11 @@ registerCommentTools(server, client);
582
961
  registerTimeTools(server, client);
583
962
  registerPromptTools(server, client);
584
963
  registerPageTools(server, client);
964
+ registerContextTools(server, client);
965
+ registerSessionTools(server, client);
585
966
  registerProjectContextResource(server, client);
586
967
  registerTaskSpecResource(server, client);
968
+ registerContextEntriesResource(server, client);
587
969
  registerStartTaskPrompt(server);
588
970
  registerSubmitTaskPrompt(server);
589
971
  var transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limeadelabs/launchpad-mcp",
3
- "version": "1.0.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": {