@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.
Files changed (3) hide show
  1. package/README.md +32 -0
  2. package/dist/index.js +447 -369
  3. 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 server = new Server({
198
- name: "elixium-mcp-server",
199
- version: "0.1.0",
200
- }, {
201
- capabilities: {
202
- tools: {},
203
- },
204
- });
205
- // Define Tools
206
- server.setRequestHandler(ListToolsRequestSchema, async () => {
207
- return {
208
- tools: [
209
- {
210
- name: "list_stories",
211
- description: "List all stories on the Elixium board",
212
- inputSchema: {
213
- type: "object",
214
- properties: {},
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
- name: "create_story",
219
- description: "Create a new story on the Elixium board",
220
- inputSchema: {
221
- type: "object",
222
- properties: {
223
- title: { type: "string", description: "Title of the story" },
224
- description: {
225
- type: "string",
226
- description: "Description of the story",
227
- },
228
- acceptanceCriteria: {
229
- type: "string",
230
- description: "Acceptance criteria in Given/When/Then format",
231
- },
232
- lane: {
233
- type: "string",
234
- description: "Lane to add the story to (case-insensitive)",
235
- enum: [
236
- "Backlog",
237
- "Icebox",
238
- "Current",
239
- "Done",
240
- "BACKLOG",
241
- "ICEBOX",
242
- "CURRENT",
243
- "DONE",
244
- ],
245
- },
246
- points: {
247
- type: "number",
248
- description: "Points (0, 1, 2, 3, 5, 8)",
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
- name: "get_iteration_context",
256
- description: "Get the current iteration context (Current + Backlog) for AI planning",
257
- inputSchema: {
258
- type: "object",
259
- properties: {},
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
- name: "create_hypothesis",
264
- description: "Create a new assumption/hypothesis in the Icebox",
265
- inputSchema: {
266
- type: "object",
267
- properties: {
268
- title: { type: "string", description: "The assumption statement" },
269
- hypothesis: { type: "string", description: "Detailed hypothesis" },
270
- confidence_score: {
271
- type: "number",
272
- description: "Initial confidence (1-5)",
273
- minimum: 1,
274
- maximum: 5,
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
- name: "list_objectives",
282
- description: "List objectives for the current workspace",
283
- inputSchema: {
284
- type: "object",
285
- properties: {},
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
- name: "record_learning",
290
- description: "Record a learning outcome for a completed story",
291
- inputSchema: {
292
- type: "object",
293
- properties: {
294
- storyId: { type: "string", description: "ID of the story" },
295
- outcome_summary: {
296
- type: "string",
297
- description: "What was learned?",
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
- name: "list_epics",
305
- description: "List epics for the current board",
306
- inputSchema: {
307
- type: "object",
308
- properties: {},
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
- name: "create_epic",
313
- description: "Create a new epic for the current board",
314
- inputSchema: {
315
- type: "object",
316
- properties: {
317
- title: { type: "string", description: "Epic title" },
318
- description: { type: "string", description: "Epic description" },
319
- stage: {
320
- type: "string",
321
- description: "Roadmap stage",
322
- enum: ["in_progress", "next", "soon", "someday", "archived"],
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
- name: "update_epic",
330
- description: "Update an epic (title, description, or stage)",
331
- inputSchema: {
332
- type: "object",
333
- properties: {
334
- epicId: { type: "string", description: "ID of the epic" },
335
- title: { type: "string", description: "Epic title" },
336
- description: { type: "string", description: "Epic description" },
337
- stage: {
338
- type: "string",
339
- description: "Roadmap stage",
340
- enum: ["in_progress", "next", "soon", "someday", "archived"],
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
- name: "update_story",
348
- description: "Update fields on an existing story",
349
- inputSchema: {
350
- type: "object",
351
- properties: {
352
- storyId: { type: "string", description: "ID of the story" },
353
- title: { type: "string", description: "Updated title" },
354
- description: { type: "string", description: "Updated description" },
355
- lane: {
356
- type: "string",
357
- description: "Lane to move the story to (case-insensitive)",
358
- enum: [
359
- "Backlog",
360
- "Icebox",
361
- "Current",
362
- "Done",
363
- "BACKLOG",
364
- "ICEBOX",
365
- "CURRENT",
366
- "DONE",
367
- ],
368
- },
369
- points: {
370
- type: "number",
371
- description: "Updated points (0, 1, 2, 3, 5, 8)",
372
- },
373
- state: {
374
- type: "string",
375
- description: "Story state (unstarted, started, finished, delivered, accepted, rejected)",
376
- },
377
- outcome_summary: {
378
- type: "string",
379
- description: "Learning outcome summary",
380
- },
381
- acceptanceCriteria: {
382
- type: "string",
383
- description: "Acceptance criteria in Given/When/Then format",
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
- name: "prepare_implementation",
391
- description: "Fetch all context for a story and format an implementation brief",
392
- inputSchema: {
393
- type: "object",
394
- properties: {
395
- storyId: {
396
- type: "string",
397
- description: "ID of the story to prepare",
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
- // Handle Requests
407
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
408
- try {
409
- switch (request.params.name) {
410
- case "list_stories": {
411
- const stories = await fetchStories();
412
- return {
413
- content: [
414
- { type: "text", text: JSON.stringify(stories, null, 2) },
415
- ],
416
- };
417
- }
418
- case "create_story": {
419
- const args = request.params.arguments;
420
- const normalizedLane = await normalizeLane(args.lane);
421
- const boardId = await resolveBoardId();
422
- const response = await client.post("/stories", {
423
- ...args,
424
- lane: normalizedLane,
425
- ...(boardId ? { boardId } : {}),
426
- });
427
- return {
428
- content: [
429
- { type: "text", text: JSON.stringify(response.data, null, 2) },
430
- ],
431
- };
432
- }
433
- case "get_iteration_context": {
434
- const boardId = await resolveBoardId();
435
- let contextData = null;
436
- if (!boardId) {
437
- try {
438
- const response = await client.get("/context");
439
- contextData = response.data;
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
- catch {
442
- // Fall back to /stories if /context is unavailable.
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
- const currentIteration = contextData?.currentIteration;
446
- const backlog = contextData?.backlog;
447
- const hasContextData = Array.isArray(currentIteration) &&
448
- Array.isArray(backlog) &&
449
- (currentIteration.length > 0 || backlog.length > 0);
450
- const context = hasContextData
451
- ? contextData
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
- // Guardrail: Block AI from setting accepted/rejected states (human-in-the-loop)
495
- const blockedStates = ["accepted", "rejected"];
496
- if (state && blockedStates.includes(state.toLowerCase())) {
497
- throw new Error(`Cannot set state to "${state}". Acceptance decisions require human review.`);
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
- const normalizedLane = await normalizeLane(lane);
500
- const payload = Object.fromEntries(Object.entries({
501
- ...rest,
502
- ...(state ? { state } : {}),
503
- ...(normalizedLane ? { lane: normalizedLane } : {}),
504
- }).filter(([, value]) => value !== undefined));
505
- const response = await client.patch(`/stories/${storyId}`, payload);
506
- return {
507
- content: [
508
- { type: "text", text: JSON.stringify(response.data, null, 2) },
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
- const payload = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined));
548
- const response = await client.patch(`/epics/${epicId}`, payload);
549
- return {
550
- content: [
551
- { type: "text", text: JSON.stringify(response.data, null, 2) },
552
- ],
553
- };
554
- }
555
- case "prepare_implementation": {
556
- const args = request.params.arguments;
557
- const { storyId } = args;
558
- const storyResponse = await client.get(`/stories/${storyId}`);
559
- const story = storyResponse.data;
560
- // Validation & Guardrails
561
- const storyLane = typeof story.lane === "string" ? story.lane.trim().toLowerCase() : "";
562
- const statusWarning = storyLane !== "current"
563
- ? `\n> [!WARNING]\n> This story is currently in the **${story.lane}** lane. It should ideally be in **Current** before starting implementation.\n`
564
- : "";
565
- const acceptanceCriteria = story.acceptance_criteria ||
566
- story.acceptanceCriteria ||
567
- story.description ||
568
- "No specific AC provided.";
569
- const assumptions = story.learning_goals ||
570
- story.learning_goal ||
571
- story.learningGoals ||
572
- story.hypothesis ||
573
- "No specific learning goals identified.";
574
- const formattedBrief = `
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
- return {
592
- content: [{ type: "text", text: formattedBrief.trim() }],
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
- default:
596
- throw new Error("Unknown tool");
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
- catch (error) {
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
- return {
605
- content: [
606
- {
607
- type: "text",
608
- text: `Error: ${error.message}`,
609
- },
610
- ],
611
- isError: true,
612
- };
613
- }
614
- });
615
- const transport = new StdioServerTransport();
616
- await server.connect(transport);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elixium.ai/mcp-server",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "MCP Server for Elixium.ai",
6
6
  "publishConfig": {