@limeadelabs/launchpad-mcp 1.0.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/README.md +99 -0
- package/bin/launchpad-mcp.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +590 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @limeadelabs/launchpad-mcp
|
|
2
|
+
|
|
3
|
+
LaunchPad MCP server for Claude Code — manage tasks, pull project context, and track work directly from your coding environment.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install via Claude Code CLI
|
|
9
|
+
claude mcp add launchpad -- npx @limeadelabs/launchpad-mcp
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Or add manually to `~/.claude/claude_desktop_config.json`:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"launchpad": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["@limeadelabs/launchpad-mcp"],
|
|
20
|
+
"env": {
|
|
21
|
+
"LAUNCHPAD_API_KEY": "your-api-key",
|
|
22
|
+
"LAUNCHPAD_URL": "https://launchpad.limeadelabs.co"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Environment Variables
|
|
30
|
+
|
|
31
|
+
| Variable | Required | Default | Description |
|
|
32
|
+
|----------|----------|---------|-------------|
|
|
33
|
+
| `LAUNCHPAD_API_KEY` | Yes | — | API key from LaunchPad workspace settings |
|
|
34
|
+
| `LAUNCHPAD_URL` | No | `https://launchpad.limeadelabs.co` | LaunchPad instance URL |
|
|
35
|
+
| `LAUNCHPAD_TIMEOUT_MS` | No | `10000` | Request timeout in milliseconds |
|
|
36
|
+
|
|
37
|
+
## Tools
|
|
38
|
+
|
|
39
|
+
| Tool | Description |
|
|
40
|
+
|------|-------------|
|
|
41
|
+
| `lp_bootstrap` | One-call onboarding — workspace, projects, tasks, conventions |
|
|
42
|
+
| `lp_list_projects` | List projects with optional stage filter |
|
|
43
|
+
| `lp_get_project` | Get project details + agent_instructions |
|
|
44
|
+
| `lp_list_tasks` | List tasks with filters (project, status, priority, label, assignee) |
|
|
45
|
+
| `lp_get_task` | Get full task context — description, comments, links |
|
|
46
|
+
| `lp_create_task` | Create a new task |
|
|
47
|
+
| `lp_update_task` | Update task status/fields (enforces workflow transitions) |
|
|
48
|
+
| `lp_claim_task` | Claim a task before working on it |
|
|
49
|
+
| `lp_release_task` | Release a claimed task |
|
|
50
|
+
| `lp_heartbeat` | Keep a claim alive (expires after 30 min without heartbeat) |
|
|
51
|
+
| `lp_add_comment` | Post a comment on a task |
|
|
52
|
+
| `lp_log_time` | Track time spent on a task |
|
|
53
|
+
| `lp_generate_prompt` | Generate a build-ready spec for a task |
|
|
54
|
+
| `lp_list_pages` | List spec/doc pages for a project |
|
|
55
|
+
| `lp_get_page` | Get a page's full content |
|
|
56
|
+
| `lp_get_workflow` | Get valid workflow states and transitions |
|
|
57
|
+
|
|
58
|
+
## Resources
|
|
59
|
+
|
|
60
|
+
- `launchpad://project/{id}/context` — Project instructions, conventions, and page summaries
|
|
61
|
+
- `launchpad://task/{id}/spec` — Full generated task spec
|
|
62
|
+
|
|
63
|
+
## Prompts
|
|
64
|
+
|
|
65
|
+
- `lp_start_task` — Guided workflow: find → claim → get spec → start building
|
|
66
|
+
- `lp_submit_task` — Guided workflow: comment → update status → release claim
|
|
67
|
+
|
|
68
|
+
## Example Usage
|
|
69
|
+
|
|
70
|
+
In Claude Code:
|
|
71
|
+
- "List my LaunchPad tasks"
|
|
72
|
+
- "Claim task #500 and show me the spec"
|
|
73
|
+
- "What's the status of the LaunchPad project?"
|
|
74
|
+
|
|
75
|
+
## Troubleshooting
|
|
76
|
+
|
|
77
|
+
**"LAUNCHPAD_API_KEY environment variable is required"**
|
|
78
|
+
Set your API key in the MCP server config. Get one from LaunchPad workspace settings.
|
|
79
|
+
|
|
80
|
+
**"Invalid LAUNCHPAD_API_KEY"**
|
|
81
|
+
Your API key is expired or incorrect. Generate a new one from LaunchPad.
|
|
82
|
+
|
|
83
|
+
**"Cannot reach LaunchPad"**
|
|
84
|
+
Check your `LAUNCHPAD_URL` and network connectivity.
|
|
85
|
+
|
|
86
|
+
**"Request timed out"**
|
|
87
|
+
LaunchPad may be slow. Increase `LAUNCHPAD_TIMEOUT_MS` or try again.
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm install
|
|
93
|
+
npm run build
|
|
94
|
+
npm test
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
|
|
5
|
+
// src/client.ts
|
|
6
|
+
var LaunchPadError = class extends Error {
|
|
7
|
+
constructor(message, statusCode) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.statusCode = statusCode;
|
|
10
|
+
this.name = "LaunchPadError";
|
|
11
|
+
}
|
|
12
|
+
statusCode;
|
|
13
|
+
};
|
|
14
|
+
var LaunchPadClient = class {
|
|
15
|
+
baseUrl;
|
|
16
|
+
apiKey;
|
|
17
|
+
timeoutMs;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
20
|
+
this.apiKey = config.apiKey;
|
|
21
|
+
this.timeoutMs = config.timeoutMs ?? 1e4;
|
|
22
|
+
}
|
|
23
|
+
async request(method, path, body) {
|
|
24
|
+
const url = `${this.baseUrl}/api/v1${path}`;
|
|
25
|
+
const res = await fetch(url, {
|
|
26
|
+
method,
|
|
27
|
+
headers: {
|
|
28
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
29
|
+
"Content-Type": "application/json"
|
|
30
|
+
},
|
|
31
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
32
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const error = await res.json().catch(() => ({ error: res.statusText }));
|
|
36
|
+
throw new LaunchPadError(
|
|
37
|
+
`${method} ${path}: ${res.status} \u2014 ${error.error || res.statusText}`,
|
|
38
|
+
res.status
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return res.json();
|
|
42
|
+
}
|
|
43
|
+
bootstrap() {
|
|
44
|
+
return this.request("GET", "/agent/bootstrap");
|
|
45
|
+
}
|
|
46
|
+
listProjects(params) {
|
|
47
|
+
const query = params?.stage ? `?stage=${encodeURIComponent(params.stage)}` : "";
|
|
48
|
+
return this.request("GET", `/projects${query}`);
|
|
49
|
+
}
|
|
50
|
+
getProject(id) {
|
|
51
|
+
return this.request("GET", `/projects/${id}`);
|
|
52
|
+
}
|
|
53
|
+
listTasks(params) {
|
|
54
|
+
const searchParams = new URLSearchParams();
|
|
55
|
+
if (params.project_id !== void 0) searchParams.set("project_id", String(params.project_id));
|
|
56
|
+
if (params.status) searchParams.set("status", params.status);
|
|
57
|
+
if (params.assignee_id !== void 0) searchParams.set("assignee_id", String(params.assignee_id));
|
|
58
|
+
if (params.priority) searchParams.set("priority", params.priority);
|
|
59
|
+
if (params.label) searchParams.set("label", params.label);
|
|
60
|
+
const query = searchParams.toString();
|
|
61
|
+
return this.request("GET", `/tasks${query ? `?${query}` : ""}`);
|
|
62
|
+
}
|
|
63
|
+
getTaskContext(id) {
|
|
64
|
+
return this.request("GET", `/tasks/${id}/context`);
|
|
65
|
+
}
|
|
66
|
+
createTask(data) {
|
|
67
|
+
return this.request("POST", "/tasks", data);
|
|
68
|
+
}
|
|
69
|
+
updateTask(id, data) {
|
|
70
|
+
return this.request("PATCH", `/tasks/${id}`, data);
|
|
71
|
+
}
|
|
72
|
+
claimTask(id) {
|
|
73
|
+
return this.request("POST", `/tasks/${id}/claim`);
|
|
74
|
+
}
|
|
75
|
+
releaseTask(id) {
|
|
76
|
+
return this.request("POST", `/tasks/${id}/release`);
|
|
77
|
+
}
|
|
78
|
+
heartbeat(id) {
|
|
79
|
+
return this.request("POST", `/tasks/${id}/heartbeat`);
|
|
80
|
+
}
|
|
81
|
+
addComment(taskId, body) {
|
|
82
|
+
return this.request("POST", `/tasks/${taskId}/comments`, { comment: { body } });
|
|
83
|
+
}
|
|
84
|
+
generatePrompt(id) {
|
|
85
|
+
return this.request("POST", `/tasks/${id}/generate_prompt`);
|
|
86
|
+
}
|
|
87
|
+
logTime(taskId, durationMinutes, description) {
|
|
88
|
+
return this.request("POST", `/tasks/${taskId}/time_entries`, {
|
|
89
|
+
duration_minutes: durationMinutes,
|
|
90
|
+
...description !== void 0 && { description }
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
listPages(projectId) {
|
|
94
|
+
return this.request("GET", `/projects/${projectId}/pages`);
|
|
95
|
+
}
|
|
96
|
+
getPage(projectId, pageId) {
|
|
97
|
+
return this.request("GET", `/projects/${projectId}/pages/${pageId}`);
|
|
98
|
+
}
|
|
99
|
+
getWorkflow(projectId) {
|
|
100
|
+
return this.request("GET", `/projects/${projectId}/workflow`);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/tools/shared.ts
|
|
105
|
+
function handleError(error, timeoutMs2) {
|
|
106
|
+
const message = error instanceof DOMException && error.name === "TimeoutError" ? `Request timed out after ${timeoutMs2}ms. LaunchPad may be slow or unreachable.` : `Error: ${error.message}`;
|
|
107
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/tools/bootstrap.ts
|
|
111
|
+
function registerBootstrapTool(server2, client2) {
|
|
112
|
+
server2.tool(
|
|
113
|
+
"lp_bootstrap",
|
|
114
|
+
"One-call onboarding: returns workspace identity, projects, available tasks, and conventions",
|
|
115
|
+
{},
|
|
116
|
+
async () => {
|
|
117
|
+
try {
|
|
118
|
+
const result = await client2.bootstrap();
|
|
119
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return handleError(error, client2.timeoutMs);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/tools/projects.ts
|
|
128
|
+
import { z } from "zod";
|
|
129
|
+
function registerProjectTools(server2, client2) {
|
|
130
|
+
server2.tool(
|
|
131
|
+
"lp_list_projects",
|
|
132
|
+
"List all accessible LaunchPad projects with metadata",
|
|
133
|
+
{
|
|
134
|
+
stage: z.string().optional().describe("Filter by stage: idea, greenlit, building, shipped, killed")
|
|
135
|
+
},
|
|
136
|
+
async (params) => {
|
|
137
|
+
try {
|
|
138
|
+
const result = await client2.listProjects(params.stage ? { stage: params.stage } : void 0);
|
|
139
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return handleError(error, client2.timeoutMs);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
server2.tool(
|
|
146
|
+
"lp_get_project",
|
|
147
|
+
"Get project details including agent_instructions",
|
|
148
|
+
{
|
|
149
|
+
project_id: z.number().describe("Project ID")
|
|
150
|
+
},
|
|
151
|
+
async (params) => {
|
|
152
|
+
try {
|
|
153
|
+
const result = await client2.getProject(params.project_id);
|
|
154
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return handleError(error, client2.timeoutMs);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/tools/tasks.ts
|
|
163
|
+
import { z as z2 } from "zod";
|
|
164
|
+
function registerTaskTools(server2, client2) {
|
|
165
|
+
server2.tool(
|
|
166
|
+
"lp_list_tasks",
|
|
167
|
+
"List tasks from LaunchPad with optional filters (project, status, priority, label, assignee)",
|
|
168
|
+
{
|
|
169
|
+
project_id: z2.number().optional().describe("Filter by project ID"),
|
|
170
|
+
status: z2.string().optional().describe("Filter by status: todo, ready, in_progress, review, done"),
|
|
171
|
+
priority: z2.string().optional().describe("Filter by priority: low, medium, high"),
|
|
172
|
+
label: z2.string().optional().describe("Filter by label name"),
|
|
173
|
+
assignee_id: z2.number().optional().describe("Filter by assignee user ID")
|
|
174
|
+
},
|
|
175
|
+
async (params) => {
|
|
176
|
+
try {
|
|
177
|
+
const result = await client2.listTasks(params);
|
|
178
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return handleError(error, client2.timeoutMs);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
server2.tool(
|
|
185
|
+
"lp_get_task",
|
|
186
|
+
"Get full task context \u2014 description, comments, links, specs",
|
|
187
|
+
{
|
|
188
|
+
task_id: z2.number().describe("Task ID")
|
|
189
|
+
},
|
|
190
|
+
async (params) => {
|
|
191
|
+
try {
|
|
192
|
+
const result = await client2.getTaskContext(params.task_id);
|
|
193
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return handleError(error, client2.timeoutMs);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
server2.tool(
|
|
200
|
+
"lp_create_task",
|
|
201
|
+
"Create a new task in LaunchPad",
|
|
202
|
+
{
|
|
203
|
+
project_id: z2.number().describe("Project ID to create the task in"),
|
|
204
|
+
title: z2.string().describe("Task title"),
|
|
205
|
+
description: z2.string().optional().describe("Task description"),
|
|
206
|
+
status: z2.string().optional().describe("Initial status: todo, ready, in_progress, review, done"),
|
|
207
|
+
priority: z2.string().optional().describe("Priority: low, medium, high")
|
|
208
|
+
},
|
|
209
|
+
async (params) => {
|
|
210
|
+
try {
|
|
211
|
+
const result = await client2.createTask(params);
|
|
212
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return handleError(error, client2.timeoutMs);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
server2.tool(
|
|
219
|
+
"lp_update_task",
|
|
220
|
+
"Update task status and/or fields. LaunchPad enforces workflow transitions \u2014 use lp_get_workflow to check valid transitions before changing status.",
|
|
221
|
+
{
|
|
222
|
+
task_id: z2.number().describe("Task ID"),
|
|
223
|
+
status: z2.string().optional().describe("New status (must be a valid workflow transition)"),
|
|
224
|
+
priority: z2.string().optional().describe("New priority: low, medium, high"),
|
|
225
|
+
assignee_id: z2.number().optional().describe("Assignee user ID"),
|
|
226
|
+
title: z2.string().optional().describe("New title"),
|
|
227
|
+
description: z2.string().optional().describe("New description")
|
|
228
|
+
},
|
|
229
|
+
async (params) => {
|
|
230
|
+
try {
|
|
231
|
+
const { task_id, ...data } = params;
|
|
232
|
+
const result = await client2.updateTask(task_id, data);
|
|
233
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return handleError(error, client2.timeoutMs);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/tools/claims.ts
|
|
242
|
+
import { z as z3 } from "zod";
|
|
243
|
+
function registerClaimTools(server2, client2) {
|
|
244
|
+
server2.tool(
|
|
245
|
+
"lp_claim_task",
|
|
246
|
+
"Claim a task before working on it. Prevents other agents from picking it up.",
|
|
247
|
+
{
|
|
248
|
+
task_id: z3.number().describe("Task ID to claim")
|
|
249
|
+
},
|
|
250
|
+
async (params) => {
|
|
251
|
+
try {
|
|
252
|
+
const result = await client2.claimTask(params.task_id);
|
|
253
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return handleError(error, client2.timeoutMs);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
server2.tool(
|
|
260
|
+
"lp_release_task",
|
|
261
|
+
"Release a claimed task so others can pick it up",
|
|
262
|
+
{
|
|
263
|
+
task_id: z3.number().describe("Task ID to release")
|
|
264
|
+
},
|
|
265
|
+
async (params) => {
|
|
266
|
+
try {
|
|
267
|
+
const result = await client2.releaseTask(params.task_id);
|
|
268
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return handleError(error, client2.timeoutMs);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
server2.tool(
|
|
275
|
+
"lp_heartbeat",
|
|
276
|
+
"Send a heartbeat for a claimed task to keep the claim alive. Claims expire after 30 minutes without a heartbeat.",
|
|
277
|
+
{
|
|
278
|
+
task_id: z3.number().describe("Task ID to heartbeat")
|
|
279
|
+
},
|
|
280
|
+
async (params) => {
|
|
281
|
+
try {
|
|
282
|
+
const result = await client2.heartbeat(params.task_id);
|
|
283
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
284
|
+
} catch (error) {
|
|
285
|
+
return handleError(error, client2.timeoutMs);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/tools/comments.ts
|
|
292
|
+
import { z as z4 } from "zod";
|
|
293
|
+
function registerCommentTools(server2, client2) {
|
|
294
|
+
server2.tool(
|
|
295
|
+
"lp_add_comment",
|
|
296
|
+
"Post a comment on a task (progress updates, notes, questions)",
|
|
297
|
+
{
|
|
298
|
+
task_id: z4.number().describe("Task ID to comment on"),
|
|
299
|
+
body: z4.string().describe("Comment body text")
|
|
300
|
+
},
|
|
301
|
+
async (params) => {
|
|
302
|
+
try {
|
|
303
|
+
const result = await client2.addComment(params.task_id, params.body);
|
|
304
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
305
|
+
} catch (error) {
|
|
306
|
+
return handleError(error, client2.timeoutMs);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/tools/time.ts
|
|
313
|
+
import { z as z5 } from "zod";
|
|
314
|
+
function registerTimeTools(server2, client2) {
|
|
315
|
+
server2.tool(
|
|
316
|
+
"lp_log_time",
|
|
317
|
+
"Track time spent on a task",
|
|
318
|
+
{
|
|
319
|
+
task_id: z5.number().describe("Task ID"),
|
|
320
|
+
duration_minutes: z5.number().positive().describe("Duration in minutes"),
|
|
321
|
+
description: z5.string().optional().describe("Description of work done")
|
|
322
|
+
},
|
|
323
|
+
async (params) => {
|
|
324
|
+
try {
|
|
325
|
+
const result = await client2.logTime(params.task_id, params.duration_minutes, params.description);
|
|
326
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
327
|
+
} catch (error) {
|
|
328
|
+
return handleError(error, client2.timeoutMs);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/tools/prompts.ts
|
|
335
|
+
import { z as z6 } from "zod";
|
|
336
|
+
function registerPromptTools(server2, client2) {
|
|
337
|
+
server2.tool(
|
|
338
|
+
"lp_generate_prompt",
|
|
339
|
+
"Generate a build-ready prompt/spec for a task with full context, conventions, and acceptance criteria",
|
|
340
|
+
{
|
|
341
|
+
task_id: z6.number().describe("Task ID")
|
|
342
|
+
},
|
|
343
|
+
async (params) => {
|
|
344
|
+
try {
|
|
345
|
+
const result = await client2.generatePrompt(params.task_id);
|
|
346
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
347
|
+
} catch (error) {
|
|
348
|
+
return handleError(error, client2.timeoutMs);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/tools/pages.ts
|
|
355
|
+
import { z as z7 } from "zod";
|
|
356
|
+
function registerPageTools(server2, client2) {
|
|
357
|
+
server2.tool(
|
|
358
|
+
"lp_list_pages",
|
|
359
|
+
"List spec/doc pages for a project",
|
|
360
|
+
{
|
|
361
|
+
project_id: z7.number().describe("Project ID")
|
|
362
|
+
},
|
|
363
|
+
async (params) => {
|
|
364
|
+
try {
|
|
365
|
+
const result = await client2.listPages(params.project_id);
|
|
366
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
367
|
+
} catch (error) {
|
|
368
|
+
return handleError(error, client2.timeoutMs);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
server2.tool(
|
|
373
|
+
"lp_get_page",
|
|
374
|
+
"Get a page's full content",
|
|
375
|
+
{
|
|
376
|
+
project_id: z7.number().describe("Project ID"),
|
|
377
|
+
page_id: z7.number().describe("Page ID")
|
|
378
|
+
},
|
|
379
|
+
async (params) => {
|
|
380
|
+
try {
|
|
381
|
+
const result = await client2.getPage(params.project_id, params.page_id);
|
|
382
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
383
|
+
} catch (error) {
|
|
384
|
+
return handleError(error, client2.timeoutMs);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
server2.tool(
|
|
389
|
+
"lp_get_workflow",
|
|
390
|
+
"Get valid workflow states and transitions for a project",
|
|
391
|
+
{
|
|
392
|
+
project_id: z7.number().describe("Project ID")
|
|
393
|
+
},
|
|
394
|
+
async (params) => {
|
|
395
|
+
try {
|
|
396
|
+
const result = await client2.getWorkflow(params.project_id);
|
|
397
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
398
|
+
} catch (error) {
|
|
399
|
+
return handleError(error, client2.timeoutMs);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/resources/project-context.ts
|
|
406
|
+
function registerProjectContextResource(server2, client2) {
|
|
407
|
+
server2.resource(
|
|
408
|
+
"project-context",
|
|
409
|
+
"launchpad://project/{project_id}/context",
|
|
410
|
+
{
|
|
411
|
+
description: "Project agent_instructions, conventions, and active spec summaries"
|
|
412
|
+
},
|
|
413
|
+
async (uri) => {
|
|
414
|
+
const match = uri.pathname.match(/^\/\/project\/(\d+)\/context$/);
|
|
415
|
+
if (!match) {
|
|
416
|
+
throw new Error(`Invalid URI: ${uri.href}`);
|
|
417
|
+
}
|
|
418
|
+
const projectId = parseInt(match[1], 10);
|
|
419
|
+
const project = await client2.getProject(projectId);
|
|
420
|
+
const pages = await client2.listPages(projectId);
|
|
421
|
+
const parts = [];
|
|
422
|
+
parts.push(`# ${project.project.name}`);
|
|
423
|
+
if (project.project.description) {
|
|
424
|
+
parts.push(`
|
|
425
|
+
${project.project.description}`);
|
|
426
|
+
}
|
|
427
|
+
if (project.project.agent_instructions) {
|
|
428
|
+
parts.push(`
|
|
429
|
+
## Agent Instructions
|
|
430
|
+
|
|
431
|
+
${project.project.agent_instructions}`);
|
|
432
|
+
}
|
|
433
|
+
if (pages.pages.length > 0) {
|
|
434
|
+
parts.push(`
|
|
435
|
+
## Pages
|
|
436
|
+
`);
|
|
437
|
+
for (const page of pages.pages) {
|
|
438
|
+
parts.push(`- **${page.title}** (updated ${page.updated_at})`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
contents: [
|
|
443
|
+
{
|
|
444
|
+
uri: uri.href,
|
|
445
|
+
mimeType: "text/markdown",
|
|
446
|
+
text: parts.join("\n")
|
|
447
|
+
}
|
|
448
|
+
]
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/resources/task-spec.ts
|
|
455
|
+
function registerTaskSpecResource(server2, client2) {
|
|
456
|
+
server2.resource(
|
|
457
|
+
"task-spec",
|
|
458
|
+
"launchpad://task/{task_id}/spec",
|
|
459
|
+
{
|
|
460
|
+
description: "Full task spec generated by LaunchPad"
|
|
461
|
+
},
|
|
462
|
+
async (uri) => {
|
|
463
|
+
const match = uri.pathname.match(/^\/\/task\/(\d+)\/spec$/);
|
|
464
|
+
if (!match) {
|
|
465
|
+
throw new Error(`Invalid URI: ${uri.href}`);
|
|
466
|
+
}
|
|
467
|
+
const taskId = parseInt(match[1], 10);
|
|
468
|
+
const result = await client2.generatePrompt(taskId);
|
|
469
|
+
return {
|
|
470
|
+
contents: [
|
|
471
|
+
{
|
|
472
|
+
uri: uri.href,
|
|
473
|
+
mimeType: "text/markdown",
|
|
474
|
+
text: result.prompt
|
|
475
|
+
}
|
|
476
|
+
]
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/prompts/start-task.ts
|
|
483
|
+
import { z as z8 } from "zod";
|
|
484
|
+
function registerStartTaskPrompt(server2) {
|
|
485
|
+
server2.prompt(
|
|
486
|
+
"lp_start_task",
|
|
487
|
+
"Guided workflow: find a task, claim it, get context, start working",
|
|
488
|
+
{
|
|
489
|
+
project_id: z8.string().optional().describe("Optional project ID to filter tasks")
|
|
490
|
+
},
|
|
491
|
+
async (args) => {
|
|
492
|
+
const projectLine = args.project_id ? ` Project ID: ${args.project_id}.` : "";
|
|
493
|
+
return {
|
|
494
|
+
messages: [
|
|
495
|
+
{
|
|
496
|
+
role: "user",
|
|
497
|
+
content: {
|
|
498
|
+
type: "text",
|
|
499
|
+
text: `I want to start working on a LaunchPad task.${projectLine}
|
|
500
|
+
|
|
501
|
+
Please:
|
|
502
|
+
1. Call lp_bootstrap to see available projects and tasks
|
|
503
|
+
2. Show me the available tasks (status: ready) and let me pick one
|
|
504
|
+
3. Once I pick a task, call lp_claim_task to claim it
|
|
505
|
+
4. Call lp_get_task to get the full context
|
|
506
|
+
5. Call lp_generate_prompt to get the build spec
|
|
507
|
+
6. Present the spec and let's start building
|
|
508
|
+
|
|
509
|
+
IMPORTANT: If this task takes more than 20 minutes, call lp_heartbeat every 20 minutes to keep the claim alive. Claims expire after 30 minutes without a heartbeat.`
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
]
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/prompts/submit-task.ts
|
|
519
|
+
import { z as z9 } from "zod";
|
|
520
|
+
function registerSubmitTaskPrompt(server2) {
|
|
521
|
+
server2.prompt(
|
|
522
|
+
"lp_submit_task",
|
|
523
|
+
"Guided workflow: mark task done, add summary comment, release claim",
|
|
524
|
+
{
|
|
525
|
+
task_id: z9.string().describe("Task ID to submit"),
|
|
526
|
+
summary: z9.string().describe("Summary of what was completed")
|
|
527
|
+
},
|
|
528
|
+
async (args) => {
|
|
529
|
+
return {
|
|
530
|
+
messages: [
|
|
531
|
+
{
|
|
532
|
+
role: "user",
|
|
533
|
+
content: {
|
|
534
|
+
type: "text",
|
|
535
|
+
text: `I've finished working on LaunchPad task #${args.task_id}.
|
|
536
|
+
|
|
537
|
+
Summary of what was done: ${args.summary}
|
|
538
|
+
|
|
539
|
+
Please:
|
|
540
|
+
1. Call lp_add_comment with a summary of what was completed
|
|
541
|
+
2. Call lp_update_task to move the task to "review" status (check lp_get_workflow first to confirm valid transitions)
|
|
542
|
+
3. Call lp_release_task to release the claim
|
|
543
|
+
4. Confirm everything is done`
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
]
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/index.ts
|
|
553
|
+
var apiKey = process.env.LAUNCHPAD_API_KEY;
|
|
554
|
+
if (!apiKey) {
|
|
555
|
+
console.error("Error: LAUNCHPAD_API_KEY environment variable is required.");
|
|
556
|
+
console.error("Get an API key from your LaunchPad workspace settings.");
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
var baseUrl = process.env.LAUNCHPAD_URL || "https://launchpad.limeadelabs.co";
|
|
560
|
+
var timeoutMs = parseInt(process.env.LAUNCHPAD_TIMEOUT_MS || "10000", 10);
|
|
561
|
+
var client = new LaunchPadClient({ baseUrl, apiKey, timeoutMs });
|
|
562
|
+
try {
|
|
563
|
+
const bootstrap = await client.bootstrap();
|
|
564
|
+
console.error(`LaunchPad MCP: Connected as "${bootstrap.agent?.name}" (${bootstrap.projects?.length || 0} projects)`);
|
|
565
|
+
} catch (err) {
|
|
566
|
+
if (err instanceof LaunchPadError && err.statusCode === 401) {
|
|
567
|
+
console.error("Error: Invalid LAUNCHPAD_API_KEY. Get a valid key from LaunchPad workspace settings.");
|
|
568
|
+
} else {
|
|
569
|
+
console.error(`Error: Cannot reach LaunchPad at ${baseUrl}. Check LAUNCHPAD_URL. (${err.message})`);
|
|
570
|
+
}
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
var server = new McpServer({
|
|
574
|
+
name: "launchpad",
|
|
575
|
+
version: "1.0.0"
|
|
576
|
+
});
|
|
577
|
+
registerBootstrapTool(server, client);
|
|
578
|
+
registerProjectTools(server, client);
|
|
579
|
+
registerTaskTools(server, client);
|
|
580
|
+
registerClaimTools(server, client);
|
|
581
|
+
registerCommentTools(server, client);
|
|
582
|
+
registerTimeTools(server, client);
|
|
583
|
+
registerPromptTools(server, client);
|
|
584
|
+
registerPageTools(server, client);
|
|
585
|
+
registerProjectContextResource(server, client);
|
|
586
|
+
registerTaskSpecResource(server, client);
|
|
587
|
+
registerStartTaskPrompt(server);
|
|
588
|
+
registerSubmitTaskPrompt(server);
|
|
589
|
+
var transport = new StdioServerTransport();
|
|
590
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@limeadelabs/launchpad-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "LaunchPad MCP server for Claude Code — AI-native project management integration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"launchpad-mcp": "bin/launchpad-mcp.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
20
|
+
"zod": "^3.23.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.0.0",
|
|
24
|
+
"tsup": "^8.0.0",
|
|
25
|
+
"tsx": "^4.0.0",
|
|
26
|
+
"typescript": "^5.7.0",
|
|
27
|
+
"vitest": "^3.0.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"keywords": ["mcp", "launchpad", "claude", "project-management", "ai-agents"],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"bin",
|
|
40
|
+
"README.md"
|
|
41
|
+
]
|
|
42
|
+
}
|