@elixium.ai/mcp-server 0.1.4 → 0.1.6
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 +32 -0
- package/dist/index.js +447 -369
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,6 +53,38 @@ Replace:
|
|
|
53
53
|
- `<YOUR_API_KEY>` with the API key from your administrator
|
|
54
54
|
- `<YOUR_TENANT>` with your workspace subdomain (e.g., `acme` for `acme.elixium.ai`)
|
|
55
55
|
|
|
56
|
+
> [!NOTE]
|
|
57
|
+
> Different IDEs and MCP clients expect different top-level keys and file paths.
|
|
58
|
+
> Use the setup that matches your IDE:
|
|
59
|
+
> - **VS Code + Elixium Companion**: `.vscode/mcp.json` with `mcpServers`
|
|
60
|
+
> - **Cursor**: `.cursor/mcp.json` with `mcpServers`
|
|
61
|
+
> - **Cline (VS Code extension)**: `cline_mcp_settings.json` with `mcpServers`
|
|
62
|
+
> - **Continue**: `.continue/config.yaml` (or `.continue/mcpServers/*.json`) with `mcpServers`
|
|
63
|
+
> - **VS Code native MCP**: `.vscode/mcp.json` with `servers`
|
|
64
|
+
|
|
65
|
+
### Shared Daemon (SSE)
|
|
66
|
+
If you want a single MCP server shared by multiple clients (VS Code, Codex, etc),
|
|
67
|
+
run the server in SSE mode and point each client to the same `url`.
|
|
68
|
+
|
|
69
|
+
Start the daemon:
|
|
70
|
+
```bash
|
|
71
|
+
elixium-mcp-server --sse --host 127.0.0.1 --port 7357
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Client config example:
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"elixium": {
|
|
79
|
+
"transport": "sse",
|
|
80
|
+
"url": "http://127.0.0.1:7357/sse"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For VS Code auto-start and multi-client setup, see `docs/ide/README.md`.
|
|
87
|
+
|
|
56
88
|
### Multi-MCP Example (Stripe + Elixium)
|
|
57
89
|
If you're using multiple MCP servers, combine them in the same config:
|
|
58
90
|
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
4
5
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
6
|
import axios from "axios";
|
|
7
|
+
import * as http from "node:http";
|
|
6
8
|
const API_KEY = process.env.ELIXIUM_API_KEY;
|
|
7
9
|
const API_URL = process.env.ELIXIUM_API_URL || "https://elixium.ai/api";
|
|
8
10
|
const BOARD_SLUG = process.env.ELIXIUM_BOARD_SLUG;
|
|
9
11
|
const LANE_STYLE_ENV = process.env.ELIXIUM_LANE_STYLE;
|
|
12
|
+
const CLI_ARGS = process.argv.slice(2);
|
|
13
|
+
const hasArg = (flag) => CLI_ARGS.includes(flag);
|
|
14
|
+
const getArgValue = (flag) => {
|
|
15
|
+
const index = CLI_ARGS.indexOf(flag);
|
|
16
|
+
if (index === -1)
|
|
17
|
+
return null;
|
|
18
|
+
const value = CLI_ARGS[index + 1];
|
|
19
|
+
return value && !value.startsWith("--") ? value : null;
|
|
20
|
+
};
|
|
21
|
+
const ensurePath = (value, fallback) => {
|
|
22
|
+
const trimmed = value?.trim();
|
|
23
|
+
if (!trimmed)
|
|
24
|
+
return fallback;
|
|
25
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
26
|
+
};
|
|
27
|
+
const resolvePort = (value, fallback) => {
|
|
28
|
+
const parsed = value ? Number(value) : NaN;
|
|
29
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
30
|
+
};
|
|
31
|
+
const USE_SSE = hasArg("--sse") || process.env.ELIXIUM_MCP_TRANSPORT === "sse";
|
|
32
|
+
const SSE_PORT = resolvePort(getArgValue("--port") ?? process.env.ELIXIUM_MCP_PORT ?? null, 7357);
|
|
33
|
+
const SSE_HOST = getArgValue("--host") ?? process.env.ELIXIUM_MCP_HOST ?? "127.0.0.1";
|
|
34
|
+
const SSE_PATH = ensurePath(getArgValue("--sse-path") ?? process.env.ELIXIUM_MCP_SSE_PATH ?? "/sse", "/sse");
|
|
35
|
+
const MESSAGE_PATH = ensurePath(getArgValue("--message-path") ??
|
|
36
|
+
process.env.ELIXIUM_MCP_MESSAGE_PATH ??
|
|
37
|
+
"/message", "/message");
|
|
10
38
|
import * as fs from "fs";
|
|
11
39
|
import * as path from "path";
|
|
12
40
|
import { fileURLToPath } from "url";
|
|
@@ -194,384 +222,385 @@ const buildIterationContext = (stories, user = null) => {
|
|
|
194
222
|
const backlog = stories.filter((story) => normalizeLaneForComparison(story?.lane) === "backlog");
|
|
195
223
|
return { currentIteration, backlog, user };
|
|
196
224
|
};
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
225
|
+
const createServer = () => {
|
|
226
|
+
const server = new Server({
|
|
227
|
+
name: "elixium-mcp-server",
|
|
228
|
+
version: "0.1.0",
|
|
229
|
+
}, {
|
|
230
|
+
capabilities: {
|
|
231
|
+
tools: {},
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
// Define Tools
|
|
235
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
236
|
+
return {
|
|
237
|
+
tools: [
|
|
238
|
+
{
|
|
239
|
+
name: "list_stories",
|
|
240
|
+
description: "List all stories on the Elixium board",
|
|
241
|
+
inputSchema: {
|
|
242
|
+
type: "object",
|
|
243
|
+
properties: {},
|
|
244
|
+
},
|
|
215
245
|
},
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
246
|
+
{
|
|
247
|
+
name: "create_story",
|
|
248
|
+
description: "Create a new story on the Elixium board",
|
|
249
|
+
inputSchema: {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: {
|
|
252
|
+
title: { type: "string", description: "Title of the story" },
|
|
253
|
+
description: {
|
|
254
|
+
type: "string",
|
|
255
|
+
description: "Description of the story",
|
|
256
|
+
},
|
|
257
|
+
acceptanceCriteria: {
|
|
258
|
+
type: "string",
|
|
259
|
+
description: "Acceptance criteria in Given/When/Then format",
|
|
260
|
+
},
|
|
261
|
+
lane: {
|
|
262
|
+
type: "string",
|
|
263
|
+
description: "Lane to add the story to (case-insensitive)",
|
|
264
|
+
enum: [
|
|
265
|
+
"Backlog",
|
|
266
|
+
"Icebox",
|
|
267
|
+
"Current",
|
|
268
|
+
"Done",
|
|
269
|
+
"BACKLOG",
|
|
270
|
+
"ICEBOX",
|
|
271
|
+
"CURRENT",
|
|
272
|
+
"DONE",
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
points: {
|
|
276
|
+
type: "number",
|
|
277
|
+
description: "Points (0, 1, 2, 3, 5, 8)",
|
|
278
|
+
},
|
|
249
279
|
},
|
|
280
|
+
required: ["title"],
|
|
250
281
|
},
|
|
251
|
-
required: ["title"],
|
|
252
282
|
},
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
283
|
+
{
|
|
284
|
+
name: "get_iteration_context",
|
|
285
|
+
description: "Get the current iteration context (Current + Backlog) for AI planning",
|
|
286
|
+
inputSchema: {
|
|
287
|
+
type: "object",
|
|
288
|
+
properties: {},
|
|
289
|
+
},
|
|
260
290
|
},
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
291
|
+
{
|
|
292
|
+
name: "create_hypothesis",
|
|
293
|
+
description: "Create a new assumption/hypothesis in the Icebox",
|
|
294
|
+
inputSchema: {
|
|
295
|
+
type: "object",
|
|
296
|
+
properties: {
|
|
297
|
+
title: { type: "string", description: "The assumption statement" },
|
|
298
|
+
hypothesis: { type: "string", description: "Detailed hypothesis" },
|
|
299
|
+
confidence_score: {
|
|
300
|
+
type: "number",
|
|
301
|
+
description: "Initial confidence (1-5)",
|
|
302
|
+
minimum: 1,
|
|
303
|
+
maximum: 5,
|
|
304
|
+
},
|
|
275
305
|
},
|
|
306
|
+
required: ["title", "hypothesis", "confidence_score"],
|
|
276
307
|
},
|
|
277
|
-
required: ["title", "hypothesis", "confidence_score"],
|
|
278
308
|
},
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
309
|
+
{
|
|
310
|
+
name: "list_objectives",
|
|
311
|
+
description: "List objectives for the current workspace",
|
|
312
|
+
inputSchema: {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: {},
|
|
315
|
+
},
|
|
286
316
|
},
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
317
|
+
{
|
|
318
|
+
name: "record_learning",
|
|
319
|
+
description: "Record a learning outcome for a completed story",
|
|
320
|
+
inputSchema: {
|
|
321
|
+
type: "object",
|
|
322
|
+
properties: {
|
|
323
|
+
storyId: { type: "string", description: "ID of the story" },
|
|
324
|
+
outcome_summary: {
|
|
325
|
+
type: "string",
|
|
326
|
+
description: "What was learned?",
|
|
327
|
+
},
|
|
298
328
|
},
|
|
329
|
+
required: ["storyId", "outcome_summary"],
|
|
299
330
|
},
|
|
300
|
-
required: ["storyId", "outcome_summary"],
|
|
301
331
|
},
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
332
|
+
{
|
|
333
|
+
name: "list_epics",
|
|
334
|
+
description: "List epics for the current board",
|
|
335
|
+
inputSchema: {
|
|
336
|
+
type: "object",
|
|
337
|
+
properties: {},
|
|
338
|
+
},
|
|
309
339
|
},
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
340
|
+
{
|
|
341
|
+
name: "create_epic",
|
|
342
|
+
description: "Create a new epic for the current board",
|
|
343
|
+
inputSchema: {
|
|
344
|
+
type: "object",
|
|
345
|
+
properties: {
|
|
346
|
+
title: { type: "string", description: "Epic title" },
|
|
347
|
+
description: { type: "string", description: "Epic description" },
|
|
348
|
+
stage: {
|
|
349
|
+
type: "string",
|
|
350
|
+
description: "Roadmap stage",
|
|
351
|
+
enum: ["in_progress", "next", "soon", "someday", "archived"],
|
|
352
|
+
},
|
|
323
353
|
},
|
|
354
|
+
required: ["title"],
|
|
324
355
|
},
|
|
325
|
-
required: ["title"],
|
|
326
356
|
},
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
357
|
+
{
|
|
358
|
+
name: "update_epic",
|
|
359
|
+
description: "Update an epic (title, description, or stage)",
|
|
360
|
+
inputSchema: {
|
|
361
|
+
type: "object",
|
|
362
|
+
properties: {
|
|
363
|
+
epicId: { type: "string", description: "ID of the epic" },
|
|
364
|
+
title: { type: "string", description: "Epic title" },
|
|
365
|
+
description: { type: "string", description: "Epic description" },
|
|
366
|
+
stage: {
|
|
367
|
+
type: "string",
|
|
368
|
+
description: "Roadmap stage",
|
|
369
|
+
enum: ["in_progress", "next", "soon", "someday", "archived"],
|
|
370
|
+
},
|
|
341
371
|
},
|
|
372
|
+
required: ["epicId"],
|
|
342
373
|
},
|
|
343
|
-
required: ["epicId"],
|
|
344
374
|
},
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
375
|
+
{
|
|
376
|
+
name: "update_story",
|
|
377
|
+
description: "Update fields on an existing story",
|
|
378
|
+
inputSchema: {
|
|
379
|
+
type: "object",
|
|
380
|
+
properties: {
|
|
381
|
+
storyId: { type: "string", description: "ID of the story" },
|
|
382
|
+
title: { type: "string", description: "Updated title" },
|
|
383
|
+
description: { type: "string", description: "Updated description" },
|
|
384
|
+
lane: {
|
|
385
|
+
type: "string",
|
|
386
|
+
description: "Lane to move the story to (case-insensitive)",
|
|
387
|
+
enum: [
|
|
388
|
+
"Backlog",
|
|
389
|
+
"Icebox",
|
|
390
|
+
"Current",
|
|
391
|
+
"Done",
|
|
392
|
+
"BACKLOG",
|
|
393
|
+
"ICEBOX",
|
|
394
|
+
"CURRENT",
|
|
395
|
+
"DONE",
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
points: {
|
|
399
|
+
type: "number",
|
|
400
|
+
description: "Updated points (0, 1, 2, 3, 5, 8)",
|
|
401
|
+
},
|
|
402
|
+
state: {
|
|
403
|
+
type: "string",
|
|
404
|
+
description: "Story state (unstarted, started, finished, delivered, accepted, rejected)",
|
|
405
|
+
},
|
|
406
|
+
outcome_summary: {
|
|
407
|
+
type: "string",
|
|
408
|
+
description: "Learning outcome summary",
|
|
409
|
+
},
|
|
410
|
+
acceptanceCriteria: {
|
|
411
|
+
type: "string",
|
|
412
|
+
description: "Acceptance criteria in Given/When/Then format",
|
|
413
|
+
},
|
|
384
414
|
},
|
|
415
|
+
required: ["storyId"],
|
|
385
416
|
},
|
|
386
|
-
required: ["storyId"],
|
|
387
417
|
},
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
418
|
+
{
|
|
419
|
+
name: "prepare_implementation",
|
|
420
|
+
description: "Fetch all context for a story and format an implementation brief",
|
|
421
|
+
inputSchema: {
|
|
422
|
+
type: "object",
|
|
423
|
+
properties: {
|
|
424
|
+
storyId: {
|
|
425
|
+
type: "string",
|
|
426
|
+
description: "ID of the story to prepare",
|
|
427
|
+
},
|
|
398
428
|
},
|
|
429
|
+
required: ["storyId"],
|
|
399
430
|
},
|
|
400
|
-
required: ["storyId"],
|
|
401
431
|
},
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
432
|
+
],
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
// Handle Requests
|
|
436
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
437
|
+
try {
|
|
438
|
+
switch (request.params.name) {
|
|
439
|
+
case "list_stories": {
|
|
440
|
+
const stories = await fetchStories();
|
|
441
|
+
return {
|
|
442
|
+
content: [
|
|
443
|
+
{ type: "text", text: JSON.stringify(stories, null, 2) },
|
|
444
|
+
],
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
case "create_story": {
|
|
448
|
+
const args = request.params.arguments;
|
|
449
|
+
const normalizedLane = await normalizeLane(args.lane);
|
|
450
|
+
const boardId = await resolveBoardId();
|
|
451
|
+
const response = await client.post("/stories", {
|
|
452
|
+
...args,
|
|
453
|
+
lane: normalizedLane,
|
|
454
|
+
...(boardId ? { boardId } : {}),
|
|
455
|
+
});
|
|
456
|
+
return {
|
|
457
|
+
content: [
|
|
458
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
459
|
+
],
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
case "get_iteration_context": {
|
|
463
|
+
const boardId = await resolveBoardId();
|
|
464
|
+
let contextData = null;
|
|
465
|
+
if (!boardId) {
|
|
466
|
+
try {
|
|
467
|
+
const response = await client.get("/context");
|
|
468
|
+
contextData = response.data;
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// Fall back to /stories if /context is unavailable.
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const currentIteration = contextData?.currentIteration;
|
|
475
|
+
const backlog = contextData?.backlog;
|
|
476
|
+
const hasContextData = Array.isArray(currentIteration) &&
|
|
477
|
+
Array.isArray(backlog) &&
|
|
478
|
+
(currentIteration.length > 0 || backlog.length > 0);
|
|
479
|
+
const context = hasContextData
|
|
480
|
+
? contextData
|
|
481
|
+
: buildIterationContext(await fetchStories(), contextData?.user ?? null);
|
|
482
|
+
return {
|
|
483
|
+
content: [
|
|
484
|
+
{ type: "text", text: JSON.stringify(context, null, 2) },
|
|
485
|
+
],
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
case "create_hypothesis": {
|
|
489
|
+
const args = request.params.arguments;
|
|
490
|
+
const normalizedLane = await normalizeLane("Icebox");
|
|
491
|
+
const boardId = await resolveBoardId();
|
|
492
|
+
// Enforce Icebox lane
|
|
493
|
+
const payload = {
|
|
494
|
+
...args,
|
|
495
|
+
lane: normalizedLane, // Map to lane style expected by API
|
|
496
|
+
...(boardId ? { boardId } : {}),
|
|
497
|
+
};
|
|
498
|
+
const response = await client.post("/stories", payload);
|
|
499
|
+
return {
|
|
500
|
+
content: [
|
|
501
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
502
|
+
],
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
case "record_learning": {
|
|
506
|
+
const args = request.params.arguments;
|
|
507
|
+
const { storyId, outcome_summary } = args;
|
|
508
|
+
const response = await client.patch(`/stories/${storyId}`, {
|
|
509
|
+
outcome_summary,
|
|
510
|
+
});
|
|
511
|
+
return {
|
|
512
|
+
content: [
|
|
513
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
514
|
+
],
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
case "update_story": {
|
|
518
|
+
const args = request.params.arguments;
|
|
519
|
+
const { storyId, lane, state, ...rest } = args;
|
|
520
|
+
if (!storyId) {
|
|
521
|
+
throw new Error("storyId is required");
|
|
440
522
|
}
|
|
441
|
-
|
|
442
|
-
|
|
523
|
+
// Guardrail: Block AI from setting accepted/rejected states (human-in-the-loop)
|
|
524
|
+
const blockedStates = ["accepted", "rejected"];
|
|
525
|
+
if (state && blockedStates.includes(state.toLowerCase())) {
|
|
526
|
+
throw new Error(`Cannot set state to "${state}". Acceptance decisions require human review.`);
|
|
443
527
|
}
|
|
528
|
+
const normalizedLane = await normalizeLane(lane);
|
|
529
|
+
const payload = Object.fromEntries(Object.entries({
|
|
530
|
+
...rest,
|
|
531
|
+
...(state ? { state } : {}),
|
|
532
|
+
...(normalizedLane ? { lane: normalizedLane } : {}),
|
|
533
|
+
}).filter(([, value]) => value !== undefined));
|
|
534
|
+
const response = await client.patch(`/stories/${storyId}`, payload);
|
|
535
|
+
return {
|
|
536
|
+
content: [
|
|
537
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
538
|
+
],
|
|
539
|
+
};
|
|
444
540
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
: buildIterationContext(await fetchStories(), contextData?.user ?? null);
|
|
453
|
-
return {
|
|
454
|
-
content: [
|
|
455
|
-
{ type: "text", text: JSON.stringify(context, null, 2) },
|
|
456
|
-
],
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
case "create_hypothesis": {
|
|
460
|
-
const args = request.params.arguments;
|
|
461
|
-
const normalizedLane = await normalizeLane("Icebox");
|
|
462
|
-
const boardId = await resolveBoardId();
|
|
463
|
-
// Enforce Icebox lane
|
|
464
|
-
const payload = {
|
|
465
|
-
...args,
|
|
466
|
-
lane: normalizedLane, // Map to lane style expected by API
|
|
467
|
-
...(boardId ? { boardId } : {}),
|
|
468
|
-
};
|
|
469
|
-
const response = await client.post("/stories", payload);
|
|
470
|
-
return {
|
|
471
|
-
content: [
|
|
472
|
-
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
473
|
-
],
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
case "record_learning": {
|
|
477
|
-
const args = request.params.arguments;
|
|
478
|
-
const { storyId, outcome_summary } = args;
|
|
479
|
-
const response = await client.patch(`/stories/${storyId}`, {
|
|
480
|
-
outcome_summary,
|
|
481
|
-
});
|
|
482
|
-
return {
|
|
483
|
-
content: [
|
|
484
|
-
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
485
|
-
],
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
case "update_story": {
|
|
489
|
-
const args = request.params.arguments;
|
|
490
|
-
const { storyId, lane, state, ...rest } = args;
|
|
491
|
-
if (!storyId) {
|
|
492
|
-
throw new Error("storyId is required");
|
|
541
|
+
case "list_objectives": {
|
|
542
|
+
const response = await client.get("/strategy/objectives");
|
|
543
|
+
return {
|
|
544
|
+
content: [
|
|
545
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
546
|
+
],
|
|
547
|
+
};
|
|
493
548
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
549
|
+
case "list_epics": {
|
|
550
|
+
const epics = await fetchEpics();
|
|
551
|
+
return {
|
|
552
|
+
content: [
|
|
553
|
+
{ type: "text", text: JSON.stringify(epics, null, 2) },
|
|
554
|
+
],
|
|
555
|
+
};
|
|
498
556
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}
|
|
512
|
-
case "list_objectives": {
|
|
513
|
-
const response = await client.get("/strategy/objectives");
|
|
514
|
-
return {
|
|
515
|
-
content: [
|
|
516
|
-
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
517
|
-
],
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
case "list_epics": {
|
|
521
|
-
const epics = await fetchEpics();
|
|
522
|
-
return {
|
|
523
|
-
content: [
|
|
524
|
-
{ type: "text", text: JSON.stringify(epics, null, 2) },
|
|
525
|
-
],
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
case "create_epic": {
|
|
529
|
-
const args = request.params.arguments;
|
|
530
|
-
const boardId = await resolveBoardId();
|
|
531
|
-
const response = await client.post("/epics", {
|
|
532
|
-
...args,
|
|
533
|
-
...(boardId ? { boardId } : {}),
|
|
534
|
-
});
|
|
535
|
-
return {
|
|
536
|
-
content: [
|
|
537
|
-
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
538
|
-
],
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
case "update_epic": {
|
|
542
|
-
const args = request.params.arguments;
|
|
543
|
-
const { epicId, ...rest } = args;
|
|
544
|
-
if (!epicId) {
|
|
545
|
-
throw new Error("epicId is required");
|
|
557
|
+
case "create_epic": {
|
|
558
|
+
const args = request.params.arguments;
|
|
559
|
+
const boardId = await resolveBoardId();
|
|
560
|
+
const response = await client.post("/epics", {
|
|
561
|
+
...args,
|
|
562
|
+
...(boardId ? { boardId } : {}),
|
|
563
|
+
});
|
|
564
|
+
return {
|
|
565
|
+
content: [
|
|
566
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
567
|
+
],
|
|
568
|
+
};
|
|
546
569
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
story.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
story.
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
570
|
+
case "update_epic": {
|
|
571
|
+
const args = request.params.arguments;
|
|
572
|
+
const { epicId, ...rest } = args;
|
|
573
|
+
if (!epicId) {
|
|
574
|
+
throw new Error("epicId is required");
|
|
575
|
+
}
|
|
576
|
+
const payload = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined));
|
|
577
|
+
const response = await client.patch(`/epics/${epicId}`, payload);
|
|
578
|
+
return {
|
|
579
|
+
content: [
|
|
580
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
581
|
+
],
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
case "prepare_implementation": {
|
|
585
|
+
const args = request.params.arguments;
|
|
586
|
+
const { storyId } = args;
|
|
587
|
+
const storyResponse = await client.get(`/stories/${storyId}`);
|
|
588
|
+
const story = storyResponse.data;
|
|
589
|
+
// Validation & Guardrails
|
|
590
|
+
const storyLane = typeof story.lane === "string" ? story.lane.trim().toLowerCase() : "";
|
|
591
|
+
const statusWarning = storyLane !== "current"
|
|
592
|
+
? `\n> [!WARNING]\n> This story is currently in the **${story.lane}** lane. It should ideally be in **Current** before starting implementation.\n`
|
|
593
|
+
: "";
|
|
594
|
+
const acceptanceCriteria = story.acceptance_criteria ||
|
|
595
|
+
story.acceptanceCriteria ||
|
|
596
|
+
story.description ||
|
|
597
|
+
"No specific AC provided.";
|
|
598
|
+
const assumptions = story.learning_goals ||
|
|
599
|
+
story.learning_goal ||
|
|
600
|
+
story.learningGoals ||
|
|
601
|
+
story.hypothesis ||
|
|
602
|
+
"No specific learning goals identified.";
|
|
603
|
+
const formattedBrief = `
|
|
575
604
|
# Implementation Brief: ${story.title}
|
|
576
605
|
|
|
577
606
|
${statusWarning}
|
|
@@ -588,29 +617,78 @@ ${assumptions}
|
|
|
588
617
|
Here’s the smallest change that will validate it:
|
|
589
618
|
[Agent should fill this in based on the context above]
|
|
590
619
|
`;
|
|
591
|
-
|
|
592
|
-
|
|
620
|
+
return {
|
|
621
|
+
content: [{ type: "text", text: formattedBrief.trim() }],
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
default:
|
|
625
|
+
throw new Error("Unknown tool");
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
console.error("Error executing tool:", error.message);
|
|
630
|
+
if (error.response) {
|
|
631
|
+
console.error("Response data:", error.response.data);
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
content: [
|
|
635
|
+
{
|
|
636
|
+
type: "text",
|
|
637
|
+
text: `Error: ${error.message}`,
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
isError: true,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
return server;
|
|
645
|
+
};
|
|
646
|
+
const startSseServer = async () => {
|
|
647
|
+
const sessions = new Map();
|
|
648
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
649
|
+
try {
|
|
650
|
+
const reqUrl = req.url ?? "";
|
|
651
|
+
const url = new URL(reqUrl, `http://${req.headers.host ?? SSE_HOST}`);
|
|
652
|
+
if (req.method === "GET" && url.pathname === SSE_PATH) {
|
|
653
|
+
const transport = new SSEServerTransport(MESSAGE_PATH, res);
|
|
654
|
+
const server = createServer();
|
|
655
|
+
const sessionId = transport.sessionId;
|
|
656
|
+
sessions.set(sessionId, { server, transport });
|
|
657
|
+
transport.onclose = async () => {
|
|
658
|
+
sessions.delete(sessionId);
|
|
659
|
+
await server.close().catch(() => undefined);
|
|
593
660
|
};
|
|
661
|
+
await server.connect(transport);
|
|
662
|
+
return;
|
|
594
663
|
}
|
|
595
|
-
|
|
596
|
-
|
|
664
|
+
if (req.method === "POST" && url.pathname === MESSAGE_PATH) {
|
|
665
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
666
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
667
|
+
res.writeHead(404).end("Session not found");
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const { transport } = sessions.get(sessionId);
|
|
671
|
+
await transport.handlePostMessage(req, res);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
res.writeHead(404).end("Not found");
|
|
597
675
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
console.error("Error executing tool:", error.message);
|
|
601
|
-
if (error.response) {
|
|
602
|
-
console.error("Response data:", error.response.data);
|
|
676
|
+
catch (error) {
|
|
677
|
+
res.writeHead(500).end(error?.message ?? "Internal server error");
|
|
603
678
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
679
|
+
});
|
|
680
|
+
httpServer.listen(SSE_PORT, SSE_HOST, () => {
|
|
681
|
+
console.log(`Elixium MCP server listening on http://${SSE_HOST}:${SSE_PORT}${SSE_PATH}`);
|
|
682
|
+
});
|
|
683
|
+
};
|
|
684
|
+
const startStdioServer = async () => {
|
|
685
|
+
const server = createServer();
|
|
686
|
+
const transport = new StdioServerTransport();
|
|
687
|
+
await server.connect(transport);
|
|
688
|
+
};
|
|
689
|
+
if (USE_SSE) {
|
|
690
|
+
await startSseServer();
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
await startStdioServer();
|
|
694
|
+
}
|