@limeadelabs/clarabit-mcp 2.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 +135 -0
- package/bin/clarabit-mcp.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1300 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1300 @@
|
|
|
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 ClarabitError = class extends Error {
|
|
7
|
+
constructor(message, statusCode) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.statusCode = statusCode;
|
|
10
|
+
this.name = "ClarabitError";
|
|
11
|
+
}
|
|
12
|
+
statusCode;
|
|
13
|
+
};
|
|
14
|
+
var ClarabitClient = 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, path2, body) {
|
|
24
|
+
const url = `${this.baseUrl}/api/v1${path2}`;
|
|
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 ClarabitError(
|
|
37
|
+
`${method} ${path2}: ${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
|
+
createPage(projectId, title, body) {
|
|
100
|
+
return this.request("POST", `/projects/${projectId}/pages`, { page: { title, body } });
|
|
101
|
+
}
|
|
102
|
+
updatePage(projectId, pageId, updates, changeSummary) {
|
|
103
|
+
const body = { page: updates };
|
|
104
|
+
if (changeSummary !== void 0) body.change_summary = changeSummary;
|
|
105
|
+
return this.request("PATCH", `/projects/${projectId}/pages/${pageId}`, body);
|
|
106
|
+
}
|
|
107
|
+
listPageVersions(projectId, pageId) {
|
|
108
|
+
return this.request(
|
|
109
|
+
"GET",
|
|
110
|
+
`/projects/${projectId}/pages/${pageId}/versions`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
getPageVersion(projectId, pageId, versionNumber) {
|
|
114
|
+
return this.request(
|
|
115
|
+
"GET",
|
|
116
|
+
`/projects/${projectId}/pages/${pageId}/versions/${versionNumber}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
restorePageVersion(projectId, pageId, versionNumber, changeSummary) {
|
|
120
|
+
const body = changeSummary !== void 0 ? { change_summary: changeSummary } : void 0;
|
|
121
|
+
return this.request(
|
|
122
|
+
"POST",
|
|
123
|
+
`/projects/${projectId}/pages/${pageId}/versions/${versionNumber}/restore`,
|
|
124
|
+
body
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
// Flat (project-id-free) page endpoints — let the server resolve the owning
|
|
128
|
+
// project via the api key's accessible_pages scope so we don't have to walk
|
|
129
|
+
// every project to find one. Requires the Rails-side flat routes (LP #575).
|
|
130
|
+
getPageFlat(pageId) {
|
|
131
|
+
return this.request("GET", `/pages/${pageId}`);
|
|
132
|
+
}
|
|
133
|
+
updatePageFlat(pageId, updates, changeSummary) {
|
|
134
|
+
const body = { page: updates };
|
|
135
|
+
if (changeSummary !== void 0) body.change_summary = changeSummary;
|
|
136
|
+
return this.request("PATCH", `/pages/${pageId}`, body);
|
|
137
|
+
}
|
|
138
|
+
listPageVersionsFlat(pageId) {
|
|
139
|
+
return this.request("GET", `/pages/${pageId}/versions`);
|
|
140
|
+
}
|
|
141
|
+
getPageVersionFlat(pageId, versionNumber) {
|
|
142
|
+
return this.request(
|
|
143
|
+
"GET",
|
|
144
|
+
`/pages/${pageId}/versions/${versionNumber}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
restorePageVersionFlat(pageId, versionNumber, changeSummary) {
|
|
148
|
+
const body = changeSummary !== void 0 ? { change_summary: changeSummary } : void 0;
|
|
149
|
+
return this.request(
|
|
150
|
+
"POST",
|
|
151
|
+
`/pages/${pageId}/versions/${versionNumber}/restore`,
|
|
152
|
+
body
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
listPageComments(projectId, pageId, opts) {
|
|
156
|
+
const query = opts?.resolved === void 0 ? "" : `?resolved=${opts.resolved ? "true" : "false"}`;
|
|
157
|
+
return this.request(
|
|
158
|
+
"GET",
|
|
159
|
+
`/projects/${projectId}/pages/${pageId}/comments${query}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
createPageComment(projectId, pageId, body, opts) {
|
|
163
|
+
return this.request(
|
|
164
|
+
"POST",
|
|
165
|
+
`/projects/${projectId}/pages/${pageId}/comments`,
|
|
166
|
+
{ body, ...opts ?? {} }
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
updatePageComment(projectId, pageId, commentId, body) {
|
|
170
|
+
return this.request(
|
|
171
|
+
"PATCH",
|
|
172
|
+
`/projects/${projectId}/pages/${pageId}/comments/${commentId}`,
|
|
173
|
+
{ comment: { body } }
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
resolvePageComment(projectId, pageId, commentId) {
|
|
177
|
+
return this.request(
|
|
178
|
+
"PATCH",
|
|
179
|
+
`/projects/${projectId}/pages/${pageId}/comments/${commentId}/resolve`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
unresolvePageComment(projectId, pageId, commentId) {
|
|
183
|
+
return this.request(
|
|
184
|
+
"PATCH",
|
|
185
|
+
`/projects/${projectId}/pages/${pageId}/comments/${commentId}/unresolve`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
getWorkflow(projectId) {
|
|
189
|
+
return this.request("GET", `/projects/${projectId}/workflow`);
|
|
190
|
+
}
|
|
191
|
+
listContextEntries(params) {
|
|
192
|
+
const searchParams = new URLSearchParams();
|
|
193
|
+
if (params?.project_id !== void 0) searchParams.set("project_id", String(params.project_id));
|
|
194
|
+
if (params?.entry_type) searchParams.set("entry_type", params.entry_type);
|
|
195
|
+
if (params?.search) searchParams.set("search", params.search);
|
|
196
|
+
if (params?.limit !== void 0) searchParams.set("limit", String(params.limit));
|
|
197
|
+
if (params?.offset !== void 0) searchParams.set("offset", String(params.offset));
|
|
198
|
+
const query = searchParams.toString();
|
|
199
|
+
return this.request("GET", `/contexts${query ? `?${query}` : ""}`);
|
|
200
|
+
}
|
|
201
|
+
getContextEntry(identifier) {
|
|
202
|
+
return this.request("GET", `/contexts/${encodeURIComponent(identifier)}`);
|
|
203
|
+
}
|
|
204
|
+
getTaskContextPackage(taskId) {
|
|
205
|
+
return this.request("GET", `/tasks/${taskId}/context`);
|
|
206
|
+
}
|
|
207
|
+
updateContextEntry(identifier, data) {
|
|
208
|
+
return this.request("PATCH", `/contexts/${encodeURIComponent(identifier)}`, data);
|
|
209
|
+
}
|
|
210
|
+
createSession(taskId, agentType, agentId) {
|
|
211
|
+
return this.request("POST", "/sessions", {
|
|
212
|
+
task_id: taskId,
|
|
213
|
+
agent_type: agentType ?? "claude_code",
|
|
214
|
+
...agentId !== void 0 && { agent_id: agentId }
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
sessionHeartbeat(sessionId) {
|
|
218
|
+
return this.request("POST", `/sessions/${sessionId}/heartbeat`);
|
|
219
|
+
}
|
|
220
|
+
updateSession(sessionId, data) {
|
|
221
|
+
return this.request("PATCH", `/sessions/${sessionId}`, data);
|
|
222
|
+
}
|
|
223
|
+
createSessionEvent(sessionId, eventType, payload) {
|
|
224
|
+
return this.request("POST", `/sessions/${sessionId}/events`, {
|
|
225
|
+
event_type: eventType,
|
|
226
|
+
payload
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/tools/shared.ts
|
|
232
|
+
function handleError(error, timeoutMs2) {
|
|
233
|
+
const message = error instanceof DOMException && error.name === "TimeoutError" ? `Request timed out after ${timeoutMs2}ms. Clarabit may be slow or unreachable.` : `Error: ${error.message}`;
|
|
234
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
235
|
+
}
|
|
236
|
+
async function resolveProjectIdForPage(client2, pageId) {
|
|
237
|
+
const projectsResult = await client2.listProjects();
|
|
238
|
+
const projects = projectsResult.projects ?? [];
|
|
239
|
+
for (const proj of projects) {
|
|
240
|
+
try {
|
|
241
|
+
await client2.getPage(proj.id, pageId);
|
|
242
|
+
return proj.id;
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw new Error(`Page ${pageId} not found in any accessible project`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/tools/bootstrap.ts
|
|
250
|
+
function registerBootstrapTool(server2, client2) {
|
|
251
|
+
server2.tool(
|
|
252
|
+
"cla_bootstrap",
|
|
253
|
+
"One-call onboarding: returns workspace identity, projects, available tasks, and conventions",
|
|
254
|
+
{},
|
|
255
|
+
async () => {
|
|
256
|
+
try {
|
|
257
|
+
const result = await client2.bootstrap();
|
|
258
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return handleError(error, client2.timeoutMs);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/tools/projects.ts
|
|
267
|
+
import { z } from "zod";
|
|
268
|
+
function registerProjectTools(server2, client2) {
|
|
269
|
+
server2.tool(
|
|
270
|
+
"cla_list_projects",
|
|
271
|
+
"List all accessible Clarabit projects with metadata",
|
|
272
|
+
{
|
|
273
|
+
stage: z.string().optional().describe("Filter by stage: idea, greenlit, building, shipped, killed")
|
|
274
|
+
},
|
|
275
|
+
async (params) => {
|
|
276
|
+
try {
|
|
277
|
+
const result = await client2.listProjects(params.stage ? { stage: params.stage } : void 0);
|
|
278
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
279
|
+
} catch (error) {
|
|
280
|
+
return handleError(error, client2.timeoutMs);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
server2.tool(
|
|
285
|
+
"cla_get_project",
|
|
286
|
+
"Get project details including agent_instructions",
|
|
287
|
+
{
|
|
288
|
+
project_id: z.coerce.number().describe("Project ID")
|
|
289
|
+
},
|
|
290
|
+
async (params) => {
|
|
291
|
+
try {
|
|
292
|
+
const result = await client2.getProject(params.project_id);
|
|
293
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
294
|
+
} catch (error) {
|
|
295
|
+
return handleError(error, client2.timeoutMs);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/tools/tasks.ts
|
|
302
|
+
import { z as z2 } from "zod";
|
|
303
|
+
function registerTaskTools(server2, client2) {
|
|
304
|
+
server2.tool(
|
|
305
|
+
"cla_list_tasks",
|
|
306
|
+
"List tasks from Clarabit with optional filters (project, status, priority, label, assignee)",
|
|
307
|
+
{
|
|
308
|
+
project_id: z2.coerce.number().optional().describe("Filter by project ID"),
|
|
309
|
+
status: z2.string().optional().describe("Filter by status: todo, ready, in_progress, review, done"),
|
|
310
|
+
priority: z2.string().optional().describe("Filter by priority: low, medium, high"),
|
|
311
|
+
label: z2.string().optional().describe("Filter by label name"),
|
|
312
|
+
assignee_id: z2.coerce.number().optional().describe("Filter by assignee user ID")
|
|
313
|
+
},
|
|
314
|
+
async (params) => {
|
|
315
|
+
try {
|
|
316
|
+
const result = await client2.listTasks(params);
|
|
317
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
318
|
+
} catch (error) {
|
|
319
|
+
return handleError(error, client2.timeoutMs);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
server2.tool(
|
|
324
|
+
"cla_get_task",
|
|
325
|
+
"Get full task context \u2014 description, comments, links, specs",
|
|
326
|
+
{
|
|
327
|
+
task_id: z2.coerce.number().describe("Task ID")
|
|
328
|
+
},
|
|
329
|
+
async (params) => {
|
|
330
|
+
try {
|
|
331
|
+
const result = await client2.getTaskContext(params.task_id);
|
|
332
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
333
|
+
} catch (error) {
|
|
334
|
+
return handleError(error, client2.timeoutMs);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
);
|
|
338
|
+
server2.tool(
|
|
339
|
+
"cla_create_task",
|
|
340
|
+
"Create a new task in Clarabit",
|
|
341
|
+
{
|
|
342
|
+
project_id: z2.coerce.number().describe("Project ID to create the task in"),
|
|
343
|
+
title: z2.string().describe("Task title"),
|
|
344
|
+
description: z2.string().optional().describe("Task description"),
|
|
345
|
+
status: z2.string().optional().describe("Initial status: todo, ready, in_progress, review, done"),
|
|
346
|
+
priority: z2.string().optional().describe("Priority: low, medium, high")
|
|
347
|
+
},
|
|
348
|
+
async (params) => {
|
|
349
|
+
try {
|
|
350
|
+
const result = await client2.createTask(params);
|
|
351
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
352
|
+
} catch (error) {
|
|
353
|
+
return handleError(error, client2.timeoutMs);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
server2.tool(
|
|
358
|
+
"cla_update_task",
|
|
359
|
+
"Update task status and/or fields. Clarabit enforces workflow transitions \u2014 use cla_get_workflow to check valid transitions before changing status.",
|
|
360
|
+
{
|
|
361
|
+
task_id: z2.coerce.number().describe("Task ID"),
|
|
362
|
+
status: z2.string().optional().describe("New status (must be a valid workflow transition)"),
|
|
363
|
+
priority: z2.string().optional().describe("New priority: low, medium, high"),
|
|
364
|
+
assignee_id: z2.coerce.number().optional().describe("Assignee user ID"),
|
|
365
|
+
title: z2.string().optional().describe("New title"),
|
|
366
|
+
description: z2.string().optional().describe("New description")
|
|
367
|
+
},
|
|
368
|
+
async (params) => {
|
|
369
|
+
try {
|
|
370
|
+
const { task_id, ...data } = params;
|
|
371
|
+
const result = await client2.updateTask(task_id, data);
|
|
372
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
373
|
+
} catch (error) {
|
|
374
|
+
return handleError(error, client2.timeoutMs);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/tools/claims.ts
|
|
381
|
+
import { z as z3 } from "zod";
|
|
382
|
+
function registerClaimTools(server2, client2) {
|
|
383
|
+
server2.tool(
|
|
384
|
+
"cla_claim_task",
|
|
385
|
+
"Claim a task before working on it. Prevents other agents from picking it up.",
|
|
386
|
+
{
|
|
387
|
+
task_id: z3.coerce.number().describe("Task ID to claim")
|
|
388
|
+
},
|
|
389
|
+
async (params) => {
|
|
390
|
+
try {
|
|
391
|
+
const result = await client2.claimTask(params.task_id);
|
|
392
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
393
|
+
} catch (error) {
|
|
394
|
+
return handleError(error, client2.timeoutMs);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
);
|
|
398
|
+
server2.tool(
|
|
399
|
+
"cla_release_task",
|
|
400
|
+
"Release a claimed task so others can pick it up",
|
|
401
|
+
{
|
|
402
|
+
task_id: z3.coerce.number().describe("Task ID to release")
|
|
403
|
+
},
|
|
404
|
+
async (params) => {
|
|
405
|
+
try {
|
|
406
|
+
const result = await client2.releaseTask(params.task_id);
|
|
407
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
408
|
+
} catch (error) {
|
|
409
|
+
return handleError(error, client2.timeoutMs);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
server2.tool(
|
|
414
|
+
"cla_heartbeat",
|
|
415
|
+
"Send a heartbeat for a claimed task to keep the claim alive. Claims expire after 30 minutes without a heartbeat.",
|
|
416
|
+
{
|
|
417
|
+
task_id: z3.coerce.number().describe("Task ID to heartbeat")
|
|
418
|
+
},
|
|
419
|
+
async (params) => {
|
|
420
|
+
try {
|
|
421
|
+
const result = await client2.heartbeat(params.task_id);
|
|
422
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
423
|
+
} catch (error) {
|
|
424
|
+
return handleError(error, client2.timeoutMs);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/tools/comments.ts
|
|
431
|
+
import { z as z4 } from "zod";
|
|
432
|
+
function registerCommentTools(server2, client2) {
|
|
433
|
+
server2.tool(
|
|
434
|
+
"cla_add_comment",
|
|
435
|
+
"Post a comment on a task (progress updates, notes, questions)",
|
|
436
|
+
{
|
|
437
|
+
task_id: z4.coerce.number().describe("Task ID to comment on"),
|
|
438
|
+
body: z4.string().describe("Comment body text")
|
|
439
|
+
},
|
|
440
|
+
async (params) => {
|
|
441
|
+
try {
|
|
442
|
+
const result = await client2.addComment(params.task_id, params.body);
|
|
443
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
444
|
+
} catch (error) {
|
|
445
|
+
return handleError(error, client2.timeoutMs);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/tools/time.ts
|
|
452
|
+
import { z as z5 } from "zod";
|
|
453
|
+
function registerTimeTools(server2, client2) {
|
|
454
|
+
server2.tool(
|
|
455
|
+
"cla_log_time",
|
|
456
|
+
"Track time spent on a task",
|
|
457
|
+
{
|
|
458
|
+
task_id: z5.coerce.number().describe("Task ID"),
|
|
459
|
+
duration_minutes: z5.coerce.number().positive().describe("Duration in minutes"),
|
|
460
|
+
description: z5.string().optional().describe("Description of work done")
|
|
461
|
+
},
|
|
462
|
+
async (params) => {
|
|
463
|
+
try {
|
|
464
|
+
const result = await client2.logTime(params.task_id, params.duration_minutes, params.description);
|
|
465
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
466
|
+
} catch (error) {
|
|
467
|
+
return handleError(error, client2.timeoutMs);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/tools/prompts.ts
|
|
474
|
+
import { z as z6 } from "zod";
|
|
475
|
+
function registerPromptTools(server2, client2) {
|
|
476
|
+
server2.tool(
|
|
477
|
+
"cla_generate_prompt",
|
|
478
|
+
"Generate a build-ready prompt/spec for a task with full context, conventions, and acceptance criteria",
|
|
479
|
+
{
|
|
480
|
+
task_id: z6.coerce.number().describe("Task ID")
|
|
481
|
+
},
|
|
482
|
+
async (params) => {
|
|
483
|
+
try {
|
|
484
|
+
const result = await client2.generatePrompt(params.task_id);
|
|
485
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
486
|
+
} catch (error) {
|
|
487
|
+
return handleError(error, client2.timeoutMs);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/tools/pages.ts
|
|
494
|
+
import { z as z7 } from "zod";
|
|
495
|
+
function registerPageTools(server2, client2) {
|
|
496
|
+
server2.tool(
|
|
497
|
+
"cla_list_pages",
|
|
498
|
+
"List spec/doc pages for a project (titles and IDs only \u2014 use cla_get_page to fetch full content)",
|
|
499
|
+
{
|
|
500
|
+
project_id: z7.coerce.number().describe("Project ID")
|
|
501
|
+
},
|
|
502
|
+
async (params) => {
|
|
503
|
+
try {
|
|
504
|
+
const result = await client2.listPages(params.project_id);
|
|
505
|
+
const pages = result.pages ?? result;
|
|
506
|
+
const summary = Array.isArray(pages) ? pages.map(({ id, title, project, author, created_at, updated_at }) => ({ id, title, project_id: project?.id, project_name: project?.name, author, created_at, updated_at })) : pages;
|
|
507
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
508
|
+
} catch (error) {
|
|
509
|
+
return handleError(error, client2.timeoutMs);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
);
|
|
513
|
+
server2.tool(
|
|
514
|
+
"cla_get_page",
|
|
515
|
+
"Get a page's full content. Accepts page_id or id. project_id is optional \u2014 if omitted, it will be inferred automatically.",
|
|
516
|
+
{
|
|
517
|
+
project_id: z7.coerce.number().optional().describe("Project ID (optional \u2014 inferred if omitted)"),
|
|
518
|
+
page_id: z7.coerce.number().optional().describe("Page ID"),
|
|
519
|
+
id: z7.coerce.number().optional().describe("Page ID (alias for page_id)")
|
|
520
|
+
},
|
|
521
|
+
async (params) => {
|
|
522
|
+
try {
|
|
523
|
+
const pageId = params.page_id ?? params.id;
|
|
524
|
+
if (!pageId) throw new Error("page_id or id is required");
|
|
525
|
+
const result = params.project_id ? await client2.getPage(params.project_id, pageId) : await client2.getPageFlat(pageId);
|
|
526
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
527
|
+
} catch (error) {
|
|
528
|
+
return handleError(error, client2.timeoutMs);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
server2.tool(
|
|
533
|
+
"cla_create_page",
|
|
534
|
+
"Create a new spec/doc page in a Clarabit project",
|
|
535
|
+
{
|
|
536
|
+
project_id: z7.coerce.number().describe("Project ID"),
|
|
537
|
+
title: z7.string().describe("Page title"),
|
|
538
|
+
body: z7.string().describe("Page body (markdown)")
|
|
539
|
+
},
|
|
540
|
+
async (params) => {
|
|
541
|
+
try {
|
|
542
|
+
const result = await client2.createPage(params.project_id, params.title, params.body);
|
|
543
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
544
|
+
} catch (error) {
|
|
545
|
+
return handleError(error, client2.timeoutMs);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
);
|
|
549
|
+
server2.tool(
|
|
550
|
+
"cla_update_page",
|
|
551
|
+
"Update an existing spec/doc page in a Clarabit project. Every MCP edit creates a new PageVersion (no debounce). Optionally pass change_summary to label the snapshot.",
|
|
552
|
+
{
|
|
553
|
+
project_id: z7.coerce.number().optional().describe("Project ID (optional \u2014 inferred if omitted)"),
|
|
554
|
+
page_id: z7.coerce.number().describe("Page ID"),
|
|
555
|
+
title: z7.string().optional().describe("New page title (optional)"),
|
|
556
|
+
body: z7.string().optional().describe("New page body in markdown (optional)"),
|
|
557
|
+
change_summary: z7.string().optional().describe("Short note describing why this edit was made (shown in version history)")
|
|
558
|
+
},
|
|
559
|
+
async (params) => {
|
|
560
|
+
try {
|
|
561
|
+
const updates = {};
|
|
562
|
+
if (params.title) updates.title = params.title;
|
|
563
|
+
if (params.body) updates.body = params.body;
|
|
564
|
+
const result = params.project_id ? await client2.updatePage(params.project_id, params.page_id, updates, params.change_summary) : await client2.updatePageFlat(params.page_id, updates, params.change_summary);
|
|
565
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
566
|
+
} catch (error) {
|
|
567
|
+
return handleError(error, client2.timeoutMs);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
server2.tool(
|
|
572
|
+
"cla_list_page_versions",
|
|
573
|
+
"List version history for a page (newest first). Returns version_number, change_summary, user, and created_at \u2014 call cla_get_page_version to read a specific version's body.",
|
|
574
|
+
{
|
|
575
|
+
project_id: z7.coerce.number().optional().describe("Project ID (optional \u2014 inferred if omitted)"),
|
|
576
|
+
page_id: z7.coerce.number().describe("Page ID")
|
|
577
|
+
},
|
|
578
|
+
async (params) => {
|
|
579
|
+
try {
|
|
580
|
+
const result = params.project_id ? await client2.listPageVersions(params.project_id, params.page_id) : await client2.listPageVersionsFlat(params.page_id);
|
|
581
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
582
|
+
} catch (error) {
|
|
583
|
+
return handleError(error, client2.timeoutMs);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
server2.tool(
|
|
588
|
+
"cla_get_page_version",
|
|
589
|
+
"Get the full body and title of a specific historical page version.",
|
|
590
|
+
{
|
|
591
|
+
project_id: z7.coerce.number().optional().describe("Project ID (optional \u2014 inferred if omitted)"),
|
|
592
|
+
page_id: z7.coerce.number().describe("Page ID"),
|
|
593
|
+
version_number: z7.coerce.number().describe("Version number to fetch")
|
|
594
|
+
},
|
|
595
|
+
async (params) => {
|
|
596
|
+
try {
|
|
597
|
+
const result = params.project_id ? await client2.getPageVersion(params.project_id, params.page_id, params.version_number) : await client2.getPageVersionFlat(params.page_id, params.version_number);
|
|
598
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
599
|
+
} catch (error) {
|
|
600
|
+
return handleError(error, client2.timeoutMs);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
);
|
|
604
|
+
server2.tool(
|
|
605
|
+
"cla_restore_page_version",
|
|
606
|
+
"Restore a page to a previous version. The pre-restore content is itself snapshotted as a new version, so restores are reversible.",
|
|
607
|
+
{
|
|
608
|
+
project_id: z7.coerce.number().optional().describe("Project ID (optional \u2014 inferred if omitted)"),
|
|
609
|
+
page_id: z7.coerce.number().describe("Page ID"),
|
|
610
|
+
version_number: z7.coerce.number().describe("Version number to restore from"),
|
|
611
|
+
change_summary: z7.string().optional().describe("Optional summary for the new snapshot of the pre-restore state")
|
|
612
|
+
},
|
|
613
|
+
async (params) => {
|
|
614
|
+
try {
|
|
615
|
+
const result = params.project_id ? await client2.restorePageVersion(
|
|
616
|
+
params.project_id,
|
|
617
|
+
params.page_id,
|
|
618
|
+
params.version_number,
|
|
619
|
+
params.change_summary
|
|
620
|
+
) : await client2.restorePageVersionFlat(
|
|
621
|
+
params.page_id,
|
|
622
|
+
params.version_number,
|
|
623
|
+
params.change_summary
|
|
624
|
+
);
|
|
625
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
626
|
+
} catch (error) {
|
|
627
|
+
return handleError(error, client2.timeoutMs);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
);
|
|
631
|
+
server2.tool(
|
|
632
|
+
"cla_get_workflow",
|
|
633
|
+
"Get valid workflow states and transitions for a project",
|
|
634
|
+
{
|
|
635
|
+
project_id: z7.coerce.number().describe("Project ID")
|
|
636
|
+
},
|
|
637
|
+
async (params) => {
|
|
638
|
+
try {
|
|
639
|
+
const result = await client2.getWorkflow(params.project_id);
|
|
640
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
641
|
+
} catch (error) {
|
|
642
|
+
return handleError(error, client2.timeoutMs);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/tools/page-comments.ts
|
|
649
|
+
import { z as z8 } from "zod";
|
|
650
|
+
function registerPageCommentTools(server2, client2) {
|
|
651
|
+
server2.tool(
|
|
652
|
+
"cla_create_page_comment",
|
|
653
|
+
"Post a comment on a page. Optionally thread under another comment via parent_comment_id, or anchor to a text selection via anchor_text/anchor_start/anchor_end.",
|
|
654
|
+
{
|
|
655
|
+
project_id: z8.coerce.number().optional().describe("Project ID (optional \u2014 inferred if omitted)"),
|
|
656
|
+
page_id: z8.coerce.number().describe("Page ID to comment on"),
|
|
657
|
+
body: z8.string().describe("Comment body (markdown)"),
|
|
658
|
+
parent_comment_id: z8.coerce.number().optional().describe("Parent comment ID to reply under"),
|
|
659
|
+
anchor_text: z8.string().optional().describe("Text selection the comment is anchored to"),
|
|
660
|
+
anchor_start: z8.coerce.number().optional().describe("Character offset where anchor begins"),
|
|
661
|
+
anchor_end: z8.coerce.number().optional().describe("Character offset where anchor ends"),
|
|
662
|
+
anchor_version: z8.coerce.number().optional().describe("Page version_number the anchor refers to")
|
|
663
|
+
},
|
|
664
|
+
async (params) => {
|
|
665
|
+
try {
|
|
666
|
+
const projectId = params.project_id ?? await resolveProjectIdForPage(client2, params.page_id);
|
|
667
|
+
const result = await client2.createPageComment(projectId, params.page_id, params.body, {
|
|
668
|
+
parent_comment_id: params.parent_comment_id,
|
|
669
|
+
anchor_text: params.anchor_text,
|
|
670
|
+
anchor_start: params.anchor_start,
|
|
671
|
+
anchor_end: params.anchor_end,
|
|
672
|
+
anchor_version: params.anchor_version
|
|
673
|
+
});
|
|
674
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
675
|
+
} catch (error) {
|
|
676
|
+
return handleError(error, client2.timeoutMs);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
);
|
|
680
|
+
server2.tool(
|
|
681
|
+
"cla_update_page_comment",
|
|
682
|
+
"Edit the body of your own page comment. Returns 403 if you are not the author \u2014 only the original author can edit comment text.",
|
|
683
|
+
{
|
|
684
|
+
project_id: z8.coerce.number().optional().describe("Project ID (optional \u2014 inferred if omitted)"),
|
|
685
|
+
page_id: z8.coerce.number().describe("Page ID the comment lives on"),
|
|
686
|
+
comment_id: z8.coerce.number().describe("Comment ID to edit"),
|
|
687
|
+
body: z8.string().describe("New comment body")
|
|
688
|
+
},
|
|
689
|
+
async (params) => {
|
|
690
|
+
try {
|
|
691
|
+
const projectId = params.project_id ?? await resolveProjectIdForPage(client2, params.page_id);
|
|
692
|
+
const result = await client2.updatePageComment(
|
|
693
|
+
projectId,
|
|
694
|
+
params.page_id,
|
|
695
|
+
params.comment_id,
|
|
696
|
+
params.body
|
|
697
|
+
);
|
|
698
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
699
|
+
} catch (error) {
|
|
700
|
+
return handleError(error, client2.timeoutMs);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
);
|
|
704
|
+
server2.tool(
|
|
705
|
+
"cla_resolve_page_comment",
|
|
706
|
+
"Mark a page comment as resolved. Anyone with project access can resolve a comment (including comments authored by others) \u2014 this is non-destructive.",
|
|
707
|
+
{
|
|
708
|
+
project_id: z8.coerce.number().optional().describe("Project ID (optional \u2014 inferred if omitted)"),
|
|
709
|
+
page_id: z8.coerce.number().describe("Page ID the comment lives on"),
|
|
710
|
+
comment_id: z8.coerce.number().describe("Comment ID to resolve")
|
|
711
|
+
},
|
|
712
|
+
async (params) => {
|
|
713
|
+
try {
|
|
714
|
+
const projectId = params.project_id ?? await resolveProjectIdForPage(client2, params.page_id);
|
|
715
|
+
const result = await client2.resolvePageComment(projectId, params.page_id, params.comment_id);
|
|
716
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
717
|
+
} catch (error) {
|
|
718
|
+
return handleError(error, client2.timeoutMs);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
);
|
|
722
|
+
server2.tool(
|
|
723
|
+
"cla_unresolve_page_comment",
|
|
724
|
+
"Reopen a previously resolved page comment. Anyone with project access can unresolve.",
|
|
725
|
+
{
|
|
726
|
+
project_id: z8.coerce.number().optional().describe("Project ID (optional \u2014 inferred if omitted)"),
|
|
727
|
+
page_id: z8.coerce.number().describe("Page ID the comment lives on"),
|
|
728
|
+
comment_id: z8.coerce.number().describe("Comment ID to unresolve")
|
|
729
|
+
},
|
|
730
|
+
async (params) => {
|
|
731
|
+
try {
|
|
732
|
+
const projectId = params.project_id ?? await resolveProjectIdForPage(client2, params.page_id);
|
|
733
|
+
const result = await client2.unresolvePageComment(projectId, params.page_id, params.comment_id);
|
|
734
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
735
|
+
} catch (error) {
|
|
736
|
+
return handleError(error, client2.timeoutMs);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/tools/context.ts
|
|
743
|
+
import { z as z9 } from "zod";
|
|
744
|
+
function registerContextTools(server2, client2) {
|
|
745
|
+
server2.tool(
|
|
746
|
+
"cla_context_list",
|
|
747
|
+
"List available context entries with optional filters",
|
|
748
|
+
{
|
|
749
|
+
project_id: z9.coerce.number().optional().describe("Filter by project ID"),
|
|
750
|
+
entry_type: z9.string().optional().describe("Filter by entry type"),
|
|
751
|
+
search: z9.string().optional().describe("Text search on entry name"),
|
|
752
|
+
limit: z9.coerce.number().min(1).max(100).optional().describe("Max entries to return (default 50)"),
|
|
753
|
+
offset: z9.coerce.number().min(0).optional().describe("Offset for pagination")
|
|
754
|
+
},
|
|
755
|
+
async (params) => {
|
|
756
|
+
try {
|
|
757
|
+
const result = await client2.listContextEntries(params);
|
|
758
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
759
|
+
} catch (error) {
|
|
760
|
+
return handleError(error, client2.timeoutMs);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
);
|
|
764
|
+
server2.tool(
|
|
765
|
+
"cla_context_get",
|
|
766
|
+
"Fetch a specific context entry by slug or ID",
|
|
767
|
+
{
|
|
768
|
+
slug: z9.string().optional().describe("Entry slug"),
|
|
769
|
+
id: z9.coerce.number().optional().describe("Entry ID")
|
|
770
|
+
},
|
|
771
|
+
async (params) => {
|
|
772
|
+
const identifier = params.slug || (params.id !== void 0 ? String(params.id) : void 0);
|
|
773
|
+
if (!identifier) {
|
|
774
|
+
return {
|
|
775
|
+
content: [{ type: "text", text: JSON.stringify({ error: true, code: "validation_error", message: "Either slug or id is required" }) }],
|
|
776
|
+
isError: true
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
try {
|
|
780
|
+
const result = await client2.getContextEntry(identifier);
|
|
781
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
782
|
+
} catch (error) {
|
|
783
|
+
return handleError(error, client2.timeoutMs);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
);
|
|
787
|
+
server2.tool(
|
|
788
|
+
"cla_context_package",
|
|
789
|
+
"Get the assembled context package for a task \u2014 task details, context entries, specs, pages, comments",
|
|
790
|
+
{
|
|
791
|
+
task_id: z9.coerce.number().describe("Task ID")
|
|
792
|
+
},
|
|
793
|
+
async (params) => {
|
|
794
|
+
try {
|
|
795
|
+
const result = await client2.getTaskContextPackage(params.task_id);
|
|
796
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
797
|
+
} catch (error) {
|
|
798
|
+
return handleError(error, client2.timeoutMs);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
);
|
|
802
|
+
server2.tool(
|
|
803
|
+
"cla_context_update",
|
|
804
|
+
"Update a context entry's content (requires write access)",
|
|
805
|
+
{
|
|
806
|
+
slug: z9.string().optional().describe("Entry slug"),
|
|
807
|
+
id: z9.coerce.number().optional().describe("Entry ID"),
|
|
808
|
+
content: z9.string().describe("New markdown content"),
|
|
809
|
+
change_summary: z9.string().describe("What changed and why")
|
|
810
|
+
},
|
|
811
|
+
async (params) => {
|
|
812
|
+
const identifier = params.slug || (params.id !== void 0 ? String(params.id) : void 0);
|
|
813
|
+
if (!identifier) {
|
|
814
|
+
return {
|
|
815
|
+
content: [{ type: "text", text: JSON.stringify({ error: true, code: "validation_error", message: "Either slug or id is required" }) }],
|
|
816
|
+
isError: true
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
const result = await client2.updateContextEntry(identifier, {
|
|
821
|
+
content: params.content,
|
|
822
|
+
change_summary: params.change_summary
|
|
823
|
+
});
|
|
824
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
825
|
+
} catch (error) {
|
|
826
|
+
return handleError(error, client2.timeoutMs);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/tools/sessions.ts
|
|
833
|
+
import { z as z10 } from "zod";
|
|
834
|
+
|
|
835
|
+
// src/session-store.ts
|
|
836
|
+
import fs from "fs";
|
|
837
|
+
import os from "os";
|
|
838
|
+
import path from "path";
|
|
839
|
+
var SESSION_FILE = path.join(os.homedir(), ".clarabit", "active-session.json");
|
|
840
|
+
function saveSession(taskId, sessionId) {
|
|
841
|
+
const dir = path.dirname(SESSION_FILE);
|
|
842
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
843
|
+
const data = fs.existsSync(SESSION_FILE) ? JSON.parse(fs.readFileSync(SESSION_FILE, "utf8")) : {};
|
|
844
|
+
data[String(taskId)] = sessionId;
|
|
845
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2));
|
|
846
|
+
}
|
|
847
|
+
function getSessionId(taskId) {
|
|
848
|
+
if (!fs.existsSync(SESSION_FILE)) return null;
|
|
849
|
+
const data = JSON.parse(fs.readFileSync(SESSION_FILE, "utf8"));
|
|
850
|
+
return data[String(taskId)] ?? null;
|
|
851
|
+
}
|
|
852
|
+
function clearSession(taskId) {
|
|
853
|
+
if (!fs.existsSync(SESSION_FILE)) return;
|
|
854
|
+
const data = JSON.parse(fs.readFileSync(SESSION_FILE, "utf8"));
|
|
855
|
+
delete data[String(taskId)];
|
|
856
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2));
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/tools/sessions.ts
|
|
860
|
+
function registerSessionTools(server2, client2) {
|
|
861
|
+
server2.tool(
|
|
862
|
+
"cla_session_start",
|
|
863
|
+
"Start a new agent session for a task. Creates session via API and saves session_id to disk for later lookup.",
|
|
864
|
+
{
|
|
865
|
+
task_id: z10.coerce.number().describe("Clarabit task ID"),
|
|
866
|
+
agent_type: z10.string().optional().describe("Agent type (default: claude_code)")
|
|
867
|
+
},
|
|
868
|
+
async ({ task_id, agent_type }) => {
|
|
869
|
+
try {
|
|
870
|
+
const result = await client2.createSession(task_id, agent_type);
|
|
871
|
+
saveSession(task_id, result.session.id);
|
|
872
|
+
return {
|
|
873
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
874
|
+
};
|
|
875
|
+
} catch (error) {
|
|
876
|
+
return handleError(error, client2.timeoutMs);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
);
|
|
880
|
+
server2.tool(
|
|
881
|
+
"cla_session_heartbeat",
|
|
882
|
+
"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.",
|
|
883
|
+
{
|
|
884
|
+
session_id: z10.coerce.number().optional().describe("Session ID"),
|
|
885
|
+
task_id: z10.coerce.number().optional().describe("Task ID to look up session from disk")
|
|
886
|
+
},
|
|
887
|
+
async ({ session_id, task_id }) => {
|
|
888
|
+
try {
|
|
889
|
+
const resolvedId = session_id ?? (task_id !== void 0 ? getSessionId(task_id) : null);
|
|
890
|
+
if (resolvedId === null) {
|
|
891
|
+
return {
|
|
892
|
+
content: [{ type: "text", text: "No active session found. Provide session_id or task_id." }]
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
const result = await client2.sessionHeartbeat(resolvedId);
|
|
896
|
+
return {
|
|
897
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
898
|
+
};
|
|
899
|
+
} catch (error) {
|
|
900
|
+
return handleError(error, client2.timeoutMs);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
);
|
|
904
|
+
server2.tool(
|
|
905
|
+
"cla_session_progress",
|
|
906
|
+
"Report progress on an active session. Provide session_id directly or task_id to look up session from disk.",
|
|
907
|
+
{
|
|
908
|
+
session_id: z10.coerce.number().optional().describe("Session ID"),
|
|
909
|
+
task_id: z10.coerce.number().optional().describe("Task ID to look up session from disk"),
|
|
910
|
+
message: z10.string().optional().describe("Progress message (use this)"),
|
|
911
|
+
detail: z10.string().optional().describe("Progress message (alias for message)")
|
|
912
|
+
},
|
|
913
|
+
async ({ session_id, task_id, message, detail }) => {
|
|
914
|
+
try {
|
|
915
|
+
const resolvedId = session_id ?? (task_id !== void 0 ? getSessionId(task_id) : null);
|
|
916
|
+
if (resolvedId === null) {
|
|
917
|
+
return { content: [{ type: "text", text: "No active session found. Provide session_id or task_id." }] };
|
|
918
|
+
}
|
|
919
|
+
const text = message ?? detail;
|
|
920
|
+
if (!text) return { content: [{ type: "text", text: "message or detail is required" }] };
|
|
921
|
+
const result = await client2.updateSession(resolvedId, {
|
|
922
|
+
activity: { action: "progress", detail: text }
|
|
923
|
+
});
|
|
924
|
+
return {
|
|
925
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
926
|
+
};
|
|
927
|
+
} catch (error) {
|
|
928
|
+
return handleError(error, client2.timeoutMs);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
);
|
|
932
|
+
server2.tool(
|
|
933
|
+
"cla_session_event",
|
|
934
|
+
"Log a discrete event for a session (commit, ci_pass, ci_fail, pr_opened, blocker, cost_update). Provide session_id or task_id.",
|
|
935
|
+
{
|
|
936
|
+
session_id: z10.coerce.number().optional().describe("Session ID"),
|
|
937
|
+
task_id: z10.coerce.number().optional().describe("Task ID to look up session from disk"),
|
|
938
|
+
event_type: z10.enum(["commit", "ci_pass", "ci_fail", "pr_opened", "blocker", "cost_update"]).describe("Type of event"),
|
|
939
|
+
payload: z10.record(z10.unknown()).describe("Event payload data")
|
|
940
|
+
},
|
|
941
|
+
async ({ session_id, task_id, event_type, payload }) => {
|
|
942
|
+
try {
|
|
943
|
+
const resolvedId = session_id ?? (task_id !== void 0 ? getSessionId(task_id) : null);
|
|
944
|
+
if (resolvedId === null) {
|
|
945
|
+
return { content: [{ type: "text", text: "No active session found. Provide session_id or task_id." }] };
|
|
946
|
+
}
|
|
947
|
+
const result = await client2.createSessionEvent(resolvedId, event_type, payload);
|
|
948
|
+
return {
|
|
949
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
950
|
+
};
|
|
951
|
+
} catch (error) {
|
|
952
|
+
return handleError(error, client2.timeoutMs);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
);
|
|
956
|
+
server2.tool(
|
|
957
|
+
"cla_session_blocked",
|
|
958
|
+
'Mark a session as blocked. Updates status to "blocked" and logs a blocker event. Provide session_id or task_id.',
|
|
959
|
+
{
|
|
960
|
+
session_id: z10.coerce.number().optional().describe("Session ID"),
|
|
961
|
+
task_id: z10.coerce.number().optional().describe("Task ID to look up session from disk"),
|
|
962
|
+
reason: z10.string().describe("Reason the session is blocked")
|
|
963
|
+
},
|
|
964
|
+
async ({ session_id, task_id, reason }) => {
|
|
965
|
+
try {
|
|
966
|
+
const resolvedId = session_id ?? (task_id !== void 0 ? getSessionId(task_id) : null);
|
|
967
|
+
if (resolvedId === null) {
|
|
968
|
+
return { content: [{ type: "text", text: "No active session found. Provide session_id or task_id." }] };
|
|
969
|
+
}
|
|
970
|
+
const [sessionResult, eventResult] = await Promise.all([
|
|
971
|
+
client2.updateSession(resolvedId, { status: "blocked" }),
|
|
972
|
+
client2.createSessionEvent(resolvedId, "blocker", { reason })
|
|
973
|
+
]);
|
|
974
|
+
return {
|
|
975
|
+
content: [{
|
|
976
|
+
type: "text",
|
|
977
|
+
text: JSON.stringify({ session: sessionResult.session, event: eventResult.event }, null, 2)
|
|
978
|
+
}]
|
|
979
|
+
};
|
|
980
|
+
} catch (error) {
|
|
981
|
+
return handleError(error, client2.timeoutMs);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
);
|
|
985
|
+
server2.tool(
|
|
986
|
+
"cla_session_complete",
|
|
987
|
+
"Mark a session as completed with a result summary. Clears session from disk.",
|
|
988
|
+
{
|
|
989
|
+
session_id: z10.coerce.number().describe("Session ID"),
|
|
990
|
+
result_summary: z10.string().describe("Summary of what was accomplished"),
|
|
991
|
+
task_id: z10.coerce.number().optional().describe("Task ID to clear from disk (if not provided, derived from session)")
|
|
992
|
+
},
|
|
993
|
+
async ({ session_id, result_summary, task_id }) => {
|
|
994
|
+
try {
|
|
995
|
+
const result = await client2.updateSession(session_id, {
|
|
996
|
+
status: "completed",
|
|
997
|
+
result_summary
|
|
998
|
+
});
|
|
999
|
+
const resolvedTaskId = task_id ?? result.session.task_id;
|
|
1000
|
+
clearSession(resolvedTaskId);
|
|
1001
|
+
return {
|
|
1002
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1003
|
+
};
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
return handleError(error, client2.timeoutMs);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
);
|
|
1009
|
+
server2.tool(
|
|
1010
|
+
"cla_session_fail",
|
|
1011
|
+
"Mark a session as failed with an error detail. Clears session from disk.",
|
|
1012
|
+
{
|
|
1013
|
+
session_id: z10.coerce.number().describe("Session ID"),
|
|
1014
|
+
error_detail: z10.string().describe("Description of the failure"),
|
|
1015
|
+
task_id: z10.coerce.number().optional().describe("Task ID to clear from disk (if not provided, derived from session)")
|
|
1016
|
+
},
|
|
1017
|
+
async ({ session_id, error_detail, task_id }) => {
|
|
1018
|
+
try {
|
|
1019
|
+
const result = await client2.updateSession(session_id, {
|
|
1020
|
+
status: "failed",
|
|
1021
|
+
result_summary: error_detail
|
|
1022
|
+
});
|
|
1023
|
+
const resolvedTaskId = task_id ?? result.session.task_id;
|
|
1024
|
+
clearSession(resolvedTaskId);
|
|
1025
|
+
return {
|
|
1026
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1027
|
+
};
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
return handleError(error, client2.timeoutMs);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/resources/project-context.ts
|
|
1036
|
+
function registerProjectContextResource(server2, client2) {
|
|
1037
|
+
server2.resource(
|
|
1038
|
+
"project-context",
|
|
1039
|
+
"clarabit://project/{project_id}/context",
|
|
1040
|
+
{
|
|
1041
|
+
description: "Project agent_instructions, conventions, and active spec summaries"
|
|
1042
|
+
},
|
|
1043
|
+
async (uri) => {
|
|
1044
|
+
const match = uri.pathname.match(/^\/\/project\/(\d+)\/context$/);
|
|
1045
|
+
if (!match) {
|
|
1046
|
+
throw new Error(`Invalid URI: ${uri.href}`);
|
|
1047
|
+
}
|
|
1048
|
+
const projectId = parseInt(match[1], 10);
|
|
1049
|
+
const project = await client2.getProject(projectId);
|
|
1050
|
+
const pages = await client2.listPages(projectId);
|
|
1051
|
+
const parts = [];
|
|
1052
|
+
parts.push(`# ${project.project.name}`);
|
|
1053
|
+
if (project.project.description) {
|
|
1054
|
+
parts.push(`
|
|
1055
|
+
${project.project.description}`);
|
|
1056
|
+
}
|
|
1057
|
+
if (project.project.agent_instructions) {
|
|
1058
|
+
parts.push(`
|
|
1059
|
+
## Agent Instructions
|
|
1060
|
+
|
|
1061
|
+
${project.project.agent_instructions}`);
|
|
1062
|
+
}
|
|
1063
|
+
if (pages.pages.length > 0) {
|
|
1064
|
+
parts.push(`
|
|
1065
|
+
## Pages
|
|
1066
|
+
`);
|
|
1067
|
+
for (const page of pages.pages) {
|
|
1068
|
+
parts.push(`- **${page.title}** (updated ${page.updated_at})`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return {
|
|
1072
|
+
contents: [
|
|
1073
|
+
{
|
|
1074
|
+
uri: uri.href,
|
|
1075
|
+
mimeType: "text/markdown",
|
|
1076
|
+
text: parts.join("\n")
|
|
1077
|
+
}
|
|
1078
|
+
]
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/resources/task-spec.ts
|
|
1085
|
+
function registerTaskSpecResource(server2, client2) {
|
|
1086
|
+
server2.resource(
|
|
1087
|
+
"task-spec",
|
|
1088
|
+
"clarabit://task/{task_id}/spec",
|
|
1089
|
+
{
|
|
1090
|
+
description: "Full task spec generated by Clarabit"
|
|
1091
|
+
},
|
|
1092
|
+
async (uri) => {
|
|
1093
|
+
const match = uri.pathname.match(/^\/\/task\/(\d+)\/spec$/);
|
|
1094
|
+
if (!match) {
|
|
1095
|
+
throw new Error(`Invalid URI: ${uri.href}`);
|
|
1096
|
+
}
|
|
1097
|
+
const taskId = parseInt(match[1], 10);
|
|
1098
|
+
const result = await client2.generatePrompt(taskId);
|
|
1099
|
+
return {
|
|
1100
|
+
contents: [
|
|
1101
|
+
{
|
|
1102
|
+
uri: uri.href,
|
|
1103
|
+
mimeType: "text/markdown",
|
|
1104
|
+
text: result.prompt
|
|
1105
|
+
}
|
|
1106
|
+
]
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/resources/context-entries.ts
|
|
1113
|
+
function registerContextEntriesResource(server2, client2) {
|
|
1114
|
+
server2.resource(
|
|
1115
|
+
"context-entries",
|
|
1116
|
+
"context://entries",
|
|
1117
|
+
{
|
|
1118
|
+
description: "Browse all active context entries"
|
|
1119
|
+
},
|
|
1120
|
+
async (uri) => {
|
|
1121
|
+
const result = await client2.listContextEntries();
|
|
1122
|
+
const lines = ["# Context Entries\n"];
|
|
1123
|
+
for (const entry of result.entries) {
|
|
1124
|
+
const project = entry.project ? ` (${entry.project.name})` : "";
|
|
1125
|
+
lines.push(`- **${entry.name}**${project} \u2014 \`${entry.slug}\` [${entry.entry_type}] (v${entry.version_count}, updated ${entry.updated_at})`);
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
contents: [
|
|
1129
|
+
{
|
|
1130
|
+
uri: uri.href,
|
|
1131
|
+
mimeType: "text/markdown",
|
|
1132
|
+
text: lines.join("\n")
|
|
1133
|
+
}
|
|
1134
|
+
]
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
);
|
|
1138
|
+
server2.resource(
|
|
1139
|
+
"context-entry",
|
|
1140
|
+
"context://entries/{slug}",
|
|
1141
|
+
{
|
|
1142
|
+
description: "Read a specific context entry by slug"
|
|
1143
|
+
},
|
|
1144
|
+
async (uri) => {
|
|
1145
|
+
if (uri.host !== "entries" || !uri.pathname || uri.pathname === "/") {
|
|
1146
|
+
throw new Error(`Invalid URI: ${uri.href}`);
|
|
1147
|
+
}
|
|
1148
|
+
const slug = decodeURIComponent(uri.pathname.slice(1));
|
|
1149
|
+
if (!slug) throw new Error("Empty slug in context entry URI");
|
|
1150
|
+
const result = await client2.getContextEntry(slug);
|
|
1151
|
+
const entry = result.entry;
|
|
1152
|
+
const text = [
|
|
1153
|
+
`# ${entry.name}`,
|
|
1154
|
+
``,
|
|
1155
|
+
`- **Type:** ${entry.entry_type}`,
|
|
1156
|
+
`- **Slug:** ${entry.slug}`,
|
|
1157
|
+
`- **Version:** ${entry.version}`,
|
|
1158
|
+
`- **Updated:** ${entry.updated_at}`,
|
|
1159
|
+
entry.project ? `- **Project:** ${entry.project.name}` : null,
|
|
1160
|
+
``,
|
|
1161
|
+
`---`,
|
|
1162
|
+
``,
|
|
1163
|
+
entry.content
|
|
1164
|
+
].filter(Boolean).join("\n");
|
|
1165
|
+
return {
|
|
1166
|
+
contents: [
|
|
1167
|
+
{
|
|
1168
|
+
uri: uri.href,
|
|
1169
|
+
mimeType: "text/markdown",
|
|
1170
|
+
text
|
|
1171
|
+
}
|
|
1172
|
+
]
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// src/prompts/start-task.ts
|
|
1179
|
+
import { z as z11 } from "zod";
|
|
1180
|
+
function registerStartTaskPrompt(server2) {
|
|
1181
|
+
server2.prompt(
|
|
1182
|
+
"cla_start_task",
|
|
1183
|
+
"Guided workflow: find a task, claim it, get context, start working",
|
|
1184
|
+
{
|
|
1185
|
+
project_id: z11.string().optional().describe("Optional project ID to filter tasks")
|
|
1186
|
+
},
|
|
1187
|
+
async (args) => {
|
|
1188
|
+
const projectLine = args.project_id ? ` Project ID: ${args.project_id}.` : "";
|
|
1189
|
+
return {
|
|
1190
|
+
messages: [
|
|
1191
|
+
{
|
|
1192
|
+
role: "user",
|
|
1193
|
+
content: {
|
|
1194
|
+
type: "text",
|
|
1195
|
+
text: `I want to start working on a Clarabit task.${projectLine}
|
|
1196
|
+
|
|
1197
|
+
Please:
|
|
1198
|
+
1. Call cla_bootstrap to see available projects and tasks
|
|
1199
|
+
2. Show me the available tasks (status: ready) and let me pick one
|
|
1200
|
+
3. Once I pick a task, call cla_claim_task to claim it
|
|
1201
|
+
4. Call cla_get_task to get the full context
|
|
1202
|
+
5. Call cla_generate_prompt to get the build spec
|
|
1203
|
+
6. Present the spec and let's start building
|
|
1204
|
+
|
|
1205
|
+
IMPORTANT: If this task takes more than 20 minutes, call cla_heartbeat every 20 minutes to keep the claim alive. Claims expire after 30 minutes without a heartbeat.`
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
]
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// src/prompts/submit-task.ts
|
|
1215
|
+
import { z as z12 } from "zod";
|
|
1216
|
+
function registerSubmitTaskPrompt(server2) {
|
|
1217
|
+
server2.prompt(
|
|
1218
|
+
"cla_submit_task",
|
|
1219
|
+
"Guided workflow: mark task done, add summary comment, release claim",
|
|
1220
|
+
{
|
|
1221
|
+
task_id: z12.string().describe("Task ID to submit"),
|
|
1222
|
+
summary: z12.string().describe("Summary of what was completed")
|
|
1223
|
+
},
|
|
1224
|
+
async (args) => {
|
|
1225
|
+
return {
|
|
1226
|
+
messages: [
|
|
1227
|
+
{
|
|
1228
|
+
role: "user",
|
|
1229
|
+
content: {
|
|
1230
|
+
type: "text",
|
|
1231
|
+
text: `I've finished working on Clarabit task #${args.task_id}.
|
|
1232
|
+
|
|
1233
|
+
Summary of what was done: ${args.summary}
|
|
1234
|
+
|
|
1235
|
+
Please:
|
|
1236
|
+
1. Call cla_add_comment with a summary of what was completed
|
|
1237
|
+
2. Call cla_update_task to move the task to "review" status (check cla_get_workflow first to confirm valid transitions)
|
|
1238
|
+
3. Call cla_release_task to release the claim
|
|
1239
|
+
4. Confirm everything is done`
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
]
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// src/index.ts
|
|
1249
|
+
function readEnvVar(newName, oldName) {
|
|
1250
|
+
const newVal = process.env[newName];
|
|
1251
|
+
if (newVal !== void 0) return newVal;
|
|
1252
|
+
const oldVal = process.env[oldName];
|
|
1253
|
+
if (oldVal !== void 0) {
|
|
1254
|
+
console.error(`Warning: ${oldName} is deprecated; please rename it to ${newName}. (Old name will be removed in a future release.)`);
|
|
1255
|
+
return oldVal;
|
|
1256
|
+
}
|
|
1257
|
+
return void 0;
|
|
1258
|
+
}
|
|
1259
|
+
var apiKey = readEnvVar("CLARABIT_API_KEY", "LAUNCHPAD_API_KEY");
|
|
1260
|
+
if (!apiKey) {
|
|
1261
|
+
console.error("Error: CLARABIT_API_KEY environment variable is required.");
|
|
1262
|
+
console.error("Get an API key from your Clarabit workspace settings.");
|
|
1263
|
+
process.exit(1);
|
|
1264
|
+
}
|
|
1265
|
+
var baseUrl = readEnvVar("CLARABIT_URL", "LAUNCHPAD_URL") || "https://app.clarabit.ai";
|
|
1266
|
+
var timeoutMs = parseInt(readEnvVar("CLARABIT_TIMEOUT_MS", "LAUNCHPAD_TIMEOUT_MS") || "10000", 10);
|
|
1267
|
+
var client = new ClarabitClient({ baseUrl, apiKey, timeoutMs });
|
|
1268
|
+
try {
|
|
1269
|
+
const bootstrap = await client.bootstrap();
|
|
1270
|
+
console.error(`Clarabit MCP: Connected as "${bootstrap.agent?.name}" (${bootstrap.projects?.length || 0} projects)`);
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
if (err instanceof ClarabitError && err.statusCode === 401) {
|
|
1273
|
+
console.error("Error: Invalid CLARABIT_API_KEY. Get a valid key from Clarabit workspace settings.");
|
|
1274
|
+
} else {
|
|
1275
|
+
console.error(`Error: Cannot reach Clarabit at ${baseUrl}. Check CLARABIT_URL. (${err.message})`);
|
|
1276
|
+
}
|
|
1277
|
+
process.exit(1);
|
|
1278
|
+
}
|
|
1279
|
+
var server = new McpServer({
|
|
1280
|
+
name: "clarabit",
|
|
1281
|
+
version: "2.0.0"
|
|
1282
|
+
});
|
|
1283
|
+
registerBootstrapTool(server, client);
|
|
1284
|
+
registerProjectTools(server, client);
|
|
1285
|
+
registerTaskTools(server, client);
|
|
1286
|
+
registerClaimTools(server, client);
|
|
1287
|
+
registerCommentTools(server, client);
|
|
1288
|
+
registerTimeTools(server, client);
|
|
1289
|
+
registerPromptTools(server, client);
|
|
1290
|
+
registerPageTools(server, client);
|
|
1291
|
+
registerPageCommentTools(server, client);
|
|
1292
|
+
registerContextTools(server, client);
|
|
1293
|
+
registerSessionTools(server, client);
|
|
1294
|
+
registerProjectContextResource(server, client);
|
|
1295
|
+
registerTaskSpecResource(server, client);
|
|
1296
|
+
registerContextEntriesResource(server, client);
|
|
1297
|
+
registerStartTaskPrompt(server);
|
|
1298
|
+
registerSubmitTaskPrompt(server);
|
|
1299
|
+
var transport = new StdioServerTransport();
|
|
1300
|
+
await server.connect(transport);
|