@elixium.ai/mcp-server 0.1.0 → 0.1.2

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 ADDED
@@ -0,0 +1,129 @@
1
+ # Elixium MCP Server
2
+
3
+ This server implements the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), allowing AI agents (like Antigravity, Cursor, or Windsurf) to interact directly with your Elixium Board.
4
+
5
+ ## Features
6
+ - **List Stories**: See the current backlog and icebox.
7
+ - **Create Story**: Add new stories directly from your editor.
8
+ - **Update Story**: Move stories between lanes and update fields.
9
+ - **Iteration Context**: Provide the AI with the full context of your Current and Backlog lanes for better planning.
10
+
11
+ ## Quick Start
12
+
13
+ ### 1. Install (Optional)
14
+ If you want to install the package locally:
15
+ ```bash
16
+ npm install -D @elixium.ai/mcp-server@latest
17
+ ```
18
+
19
+ Or install globally:
20
+ ```bash
21
+ npm install -g @elixium.ai/mcp-server@latest
22
+ ```
23
+
24
+ > [!TIP]
25
+ > You can also use `npx` to run the server without installing it (recommended for IDE configurations).
26
+
27
+ ### 2. Get Your API Key
28
+ Contact your Elixium workspace administrator to obtain an API key for your tenant.
29
+
30
+ ### 3. Configure Your IDE
31
+ Add the following to your IDE's MCP configuration file (e.g., `mcp_config.json`):
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "elixium": {
37
+ "command": "npx",
38
+ "args": [
39
+ "-y",
40
+ "@elixium.ai/mcp-server@latest"
41
+ ],
42
+ "env": {
43
+ "ELIXIUM_API_KEY": "<YOUR_API_KEY>",
44
+ "ELIXIUM_API_URL": "https://<YOUR_TENANT>.elixium.ai/api",
45
+ "ELIXIUM_BOARD_SLUG": "main"
46
+ }
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ Replace:
53
+ - `<YOUR_API_KEY>` with the API key from your administrator
54
+ - `<YOUR_TENANT>` with your workspace subdomain (e.g., `acme` for `acme.elixium.ai`)
55
+
56
+ ### Multi-MCP Example (Stripe + Elixium)
57
+ If you're using multiple MCP servers, combine them in the same config:
58
+
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "stripe": {
63
+ "command": "npx",
64
+ "args": ["-y", "@stripe/mcp", "--tools=all", "--api-key=<STRIPE_KEY>"],
65
+ "env": {}
66
+ },
67
+ "elixium": {
68
+ "command": "npx",
69
+ "args": ["-y", "@elixium.ai/mcp-server@latest"],
70
+ "env": {
71
+ "ELIXIUM_API_KEY": "<YOUR_API_KEY>",
72
+ "ELIXIUM_API_URL": "https://<YOUR_TENANT>.elixium.ai/api",
73
+ "ELIXIUM_BOARD_SLUG": "main"
74
+ }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ ## Environment Variables
81
+
82
+ | Variable | Required | Description |
83
+ |----------|----------|-------------|
84
+ | `ELIXIUM_API_KEY` | ✅ Yes | Your tenant-scoped API key |
85
+ | `ELIXIUM_API_URL` | ✅ Yes | Your tenant's API endpoint (e.g., `https://acme.elixium.ai/api`) |
86
+ | `ELIXIUM_BOARD_SLUG` | ⚠️ Recommended | Board slug to scope operations (e.g., `main`) |
87
+ | `ELIXIUM_LANE_STYLE` | Optional | `upper` for `BACKLOG/CURRENT` or `title` for `Backlog/Current` (auto-detected) |
88
+
89
+ > [!IMPORTANT]
90
+ > If you set `ELIXIUM_BOARD_SLUG`, the MCP server will only read/write stories for that board.
91
+ > The server resolves the board slug to a boardId on startup, so the slug must match an existing board.
92
+
93
+ ## Usage
94
+ Once configured, your AI agent will have access to tools like:
95
+ - `list_stories` - View all stories on the board
96
+ - `create_story` - Add new stories with title, description, lane, and points
97
+ - `update_story` - Move stories between lanes or update fields
98
+ - `list_epics` - View epics on the board
99
+ - `get_iteration_context` - Get current iteration and backlog for planning
100
+
101
+ ## Development (Source Build)
102
+ If you're contributing or developing from source:
103
+
104
+ ```bash
105
+ cd mcp-server
106
+ npm install
107
+ npm run build
108
+ ```
109
+
110
+ To use the local build, update your config:
111
+ ```json
112
+ {
113
+ "elixium": {
114
+ "command": "node",
115
+ "args": ["/path/to/mcp-server/dist/index.js"],
116
+ "env": { ... }
117
+ }
118
+ }
119
+ ```
120
+
121
+ ## Maintainers: Release Process
122
+
123
+ ```bash
124
+ cd mcp-server
125
+ npm install
126
+ npm run build
127
+ npm version patch # or minor/major
128
+ npm publish --access public
129
+ ```
package/dist/index.js CHANGED
@@ -5,6 +5,8 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
5
5
  import axios from "axios";
6
6
  const API_KEY = process.env.ELIXIUM_API_KEY;
7
7
  const API_URL = process.env.ELIXIUM_API_URL || "https://elixium.ai/api";
8
+ const BOARD_SLUG = process.env.ELIXIUM_BOARD_SLUG;
9
+ const LANE_STYLE_ENV = process.env.ELIXIUM_LANE_STYLE;
8
10
  if (!API_KEY) {
9
11
  console.error("Error: ELIXIUM_API_KEY environment variable is required");
10
12
  process.exit(1);
@@ -14,8 +16,142 @@ const client = axios.create({
14
16
  headers: {
15
17
  "x-api-key": API_KEY,
16
18
  "Content-Type": "application/json",
19
+ ...(BOARD_SLUG ? { "x-board-slug": BOARD_SLUG } : {}),
17
20
  },
18
21
  });
22
+ const LANE_TITLE = {
23
+ backlog: "Backlog",
24
+ icebox: "Icebox",
25
+ current: "Current",
26
+ done: "Done",
27
+ };
28
+ const LANE_UPPER = {
29
+ backlog: "BACKLOG",
30
+ icebox: "ICEBOX",
31
+ current: "CURRENT",
32
+ done: "DONE",
33
+ };
34
+ const parseLaneStyle = (value) => {
35
+ if (!value)
36
+ return null;
37
+ const normalized = value.trim().toLowerCase();
38
+ if (normalized === "upper")
39
+ return "upper";
40
+ if (normalized === "title")
41
+ return "title";
42
+ return null;
43
+ };
44
+ const inferLaneStyleFromUrl = (apiUrl) => {
45
+ try {
46
+ const host = new URL(apiUrl).hostname.toLowerCase();
47
+ if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".local")) {
48
+ return "title";
49
+ }
50
+ }
51
+ catch {
52
+ // Fall through to default.
53
+ }
54
+ return "upper";
55
+ };
56
+ const normalizeLaneForComparison = (lane) => {
57
+ if (typeof lane !== "string")
58
+ return "";
59
+ return lane.trim().toLowerCase();
60
+ };
61
+ const extractStories = (data) => {
62
+ if (Array.isArray(data))
63
+ return data;
64
+ if (data?.stories && Array.isArray(data.stories))
65
+ return data.stories;
66
+ return [];
67
+ };
68
+ const extractEpics = (data) => {
69
+ if (Array.isArray(data))
70
+ return data;
71
+ if (data?.epics && Array.isArray(data.epics))
72
+ return data.epics;
73
+ return [];
74
+ };
75
+ const normalizeBoardSlug = (value) => {
76
+ if (!value)
77
+ return null;
78
+ const normalized = value.trim().toLowerCase();
79
+ return normalized.length > 0 ? normalized : null;
80
+ };
81
+ let cachedBoardId = null;
82
+ let cachedBoardSlug = null;
83
+ const resolveBoardId = async () => {
84
+ const slug = normalizeBoardSlug(BOARD_SLUG);
85
+ if (!slug)
86
+ return null;
87
+ if (cachedBoardSlug === slug && cachedBoardId)
88
+ return cachedBoardId;
89
+ const response = await client.get("/boards");
90
+ const boards = Array.isArray(response.data) ? response.data : [];
91
+ const match = boards.find((board) => {
92
+ if (typeof board?.slug !== "string")
93
+ return false;
94
+ return board.slug.trim().toLowerCase() === slug;
95
+ });
96
+ if (!match?.id) {
97
+ throw new Error(`Board slug "${BOARD_SLUG}" not found`);
98
+ }
99
+ cachedBoardSlug = slug;
100
+ cachedBoardId = match.id;
101
+ return cachedBoardId;
102
+ };
103
+ const fetchStories = async () => {
104
+ const boardId = await resolveBoardId();
105
+ const response = await client.get("/stories", {
106
+ params: boardId ? { boardId } : undefined,
107
+ });
108
+ return extractStories(response.data);
109
+ };
110
+ const fetchEpics = async () => {
111
+ const boardId = await resolveBoardId();
112
+ const response = await client.get("/epics", {
113
+ params: boardId ? { boardId } : undefined,
114
+ });
115
+ return extractEpics(response.data);
116
+ };
117
+ let cachedLaneStyle = null;
118
+ const detectLaneStyle = async () => {
119
+ const envStyle = parseLaneStyle(LANE_STYLE_ENV);
120
+ if (envStyle)
121
+ return envStyle;
122
+ if (cachedLaneStyle)
123
+ return cachedLaneStyle;
124
+ try {
125
+ const stories = await fetchStories();
126
+ const sampleLane = stories.find((story) => typeof story?.lane === "string")
127
+ ?.lane;
128
+ if (sampleLane) {
129
+ cachedLaneStyle =
130
+ sampleLane.trim() === sampleLane.trim().toUpperCase()
131
+ ? "upper"
132
+ : "title";
133
+ return cachedLaneStyle;
134
+ }
135
+ }
136
+ catch {
137
+ // Fall back to URL-based inference.
138
+ }
139
+ cachedLaneStyle = inferLaneStyleFromUrl(API_URL);
140
+ return cachedLaneStyle;
141
+ };
142
+ const normalizeLane = async (lane) => {
143
+ if (!lane)
144
+ return undefined;
145
+ const style = await detectLaneStyle();
146
+ const key = lane.trim().toLowerCase();
147
+ const map = style === "upper" ? LANE_UPPER : LANE_TITLE;
148
+ return map[key] || (style === "upper" ? lane.trim().toUpperCase() : lane.trim());
149
+ };
150
+ const buildIterationContext = (stories, user = null) => {
151
+ const currentIteration = stories.filter((story) => normalizeLaneForComparison(story?.lane) === "current");
152
+ const backlog = stories.filter((story) => normalizeLaneForComparison(story?.lane) === "backlog");
153
+ return { currentIteration, backlog, user };
154
+ };
19
155
  const server = new Server({
20
156
  name: "elixium-mcp-server",
21
157
  version: "0.1.0",
@@ -24,6 +160,7 @@ const server = new Server({
24
160
  tools: {},
25
161
  },
26
162
  });
163
+ // Define Tools
27
164
  server.setRequestHandler(ListToolsRequestSchema, async () => {
28
165
  return {
29
166
  tools: [
@@ -41,22 +178,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
41
178
  inputSchema: {
42
179
  type: "object",
43
180
  properties: {
44
- title: {
45
- type: "string",
46
- description: "Title of the story",
47
- },
181
+ title: { type: "string", description: "Title of the story" },
48
182
  description: {
49
183
  type: "string",
50
184
  description: "Description of the story",
51
185
  },
52
186
  lane: {
53
187
  type: "string",
54
- description: "Lane to add the story to (BACKLOG, ICEBOX, CURRENT)",
55
- enum: ["BACKLOG", "ICEBOX", "CURRENT"],
188
+ description: "Lane to add the story to (case-insensitive)",
189
+ enum: [
190
+ "Backlog",
191
+ "Icebox",
192
+ "Current",
193
+ "Done",
194
+ "BACKLOG",
195
+ "ICEBOX",
196
+ "CURRENT",
197
+ "DONE",
198
+ ],
56
199
  },
57
200
  points: {
58
201
  type: "number",
59
- description: "Story points (0, 1, 2, 3, 5, 8)",
202
+ description: "Points (0, 1, 2, 3, 5, 8)",
60
203
  },
61
204
  },
62
205
  required: ["title"],
@@ -64,52 +207,335 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
64
207
  },
65
208
  {
66
209
  name: "get_iteration_context",
67
- description: "Get the current iteration context (current stories and backlog) for AI planning",
210
+ description: "Get the current iteration context (Current + Backlog) for AI planning",
211
+ inputSchema: {
212
+ type: "object",
213
+ properties: {},
214
+ },
215
+ },
216
+ {
217
+ name: "create_hypothesis",
218
+ description: "Create a new assumption/hypothesis in the Icebox",
219
+ inputSchema: {
220
+ type: "object",
221
+ properties: {
222
+ title: { type: "string", description: "The assumption statement" },
223
+ hypothesis: { type: "string", description: "Detailed hypothesis" },
224
+ confidence_score: {
225
+ type: "number",
226
+ description: "Initial confidence (1-5)",
227
+ minimum: 1,
228
+ maximum: 5,
229
+ },
230
+ },
231
+ required: ["title", "hypothesis", "confidence_score"],
232
+ },
233
+ },
234
+ {
235
+ name: "list_objectives",
236
+ description: "List objectives for the current workspace",
68
237
  inputSchema: {
69
238
  type: "object",
70
239
  properties: {},
71
240
  },
72
241
  },
242
+ {
243
+ name: "record_learning",
244
+ description: "Record a learning outcome for a completed story",
245
+ inputSchema: {
246
+ type: "object",
247
+ properties: {
248
+ storyId: { type: "string", description: "ID of the story" },
249
+ outcome_summary: {
250
+ type: "string",
251
+ description: "What was learned?",
252
+ },
253
+ },
254
+ required: ["storyId", "outcome_summary"],
255
+ },
256
+ },
257
+ {
258
+ name: "list_epics",
259
+ description: "List epics for the current board",
260
+ inputSchema: {
261
+ type: "object",
262
+ properties: {},
263
+ },
264
+ },
265
+ {
266
+ name: "create_epic",
267
+ description: "Create a new epic for the current board",
268
+ inputSchema: {
269
+ type: "object",
270
+ properties: {
271
+ title: { type: "string", description: "Epic title" },
272
+ description: { type: "string", description: "Epic description" },
273
+ stage: {
274
+ type: "string",
275
+ description: "Roadmap stage",
276
+ enum: ["in_progress", "next", "soon", "someday", "archived"],
277
+ },
278
+ },
279
+ required: ["title"],
280
+ },
281
+ },
282
+ {
283
+ name: "update_epic",
284
+ description: "Update an epic (title, description, or stage)",
285
+ inputSchema: {
286
+ type: "object",
287
+ properties: {
288
+ epicId: { type: "string", description: "ID of the epic" },
289
+ title: { type: "string", description: "Epic title" },
290
+ description: { type: "string", description: "Epic description" },
291
+ stage: {
292
+ type: "string",
293
+ description: "Roadmap stage",
294
+ enum: ["in_progress", "next", "soon", "someday", "archived"],
295
+ },
296
+ },
297
+ required: ["epicId"],
298
+ },
299
+ },
300
+ {
301
+ name: "update_story",
302
+ description: "Update fields on an existing story",
303
+ inputSchema: {
304
+ type: "object",
305
+ properties: {
306
+ storyId: { type: "string", description: "ID of the story" },
307
+ title: { type: "string", description: "Updated title" },
308
+ description: { type: "string", description: "Updated description" },
309
+ lane: {
310
+ type: "string",
311
+ description: "Lane to move the story to (case-insensitive)",
312
+ enum: [
313
+ "Backlog",
314
+ "Icebox",
315
+ "Current",
316
+ "Done",
317
+ "BACKLOG",
318
+ "ICEBOX",
319
+ "CURRENT",
320
+ "DONE",
321
+ ],
322
+ },
323
+ points: {
324
+ type: "number",
325
+ description: "Updated points (0, 1, 2, 3, 5, 8)",
326
+ },
327
+ state: {
328
+ type: "string",
329
+ description: "Story state (unstarted, started, finished, delivered, accepted, rejected)",
330
+ },
331
+ outcome_summary: {
332
+ type: "string",
333
+ description: "Learning outcome summary",
334
+ },
335
+ },
336
+ required: ["storyId"],
337
+ },
338
+ },
339
+ {
340
+ name: "prepare_implementation",
341
+ description: "Fetch all context for a story and format an implementation brief",
342
+ inputSchema: {
343
+ type: "object",
344
+ properties: {
345
+ storyId: {
346
+ type: "string",
347
+ description: "ID of the story to prepare",
348
+ },
349
+ },
350
+ required: ["storyId"],
351
+ },
352
+ },
73
353
  ],
74
354
  };
75
355
  });
356
+ // Handle Requests
76
357
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
77
358
  try {
78
359
  switch (request.params.name) {
79
360
  case "list_stories": {
80
- const response = await client.get("/stories");
361
+ const stories = await fetchStories();
81
362
  return {
82
363
  content: [
83
- {
84
- type: "text",
85
- text: JSON.stringify(response.data, null, 2),
86
- },
364
+ { type: "text", text: JSON.stringify(stories, null, 2) },
87
365
  ],
88
366
  };
89
367
  }
90
368
  case "create_story": {
91
369
  const args = request.params.arguments;
92
- const response = await client.post("/stories", args);
370
+ const normalizedLane = await normalizeLane(args.lane);
371
+ const boardId = await resolveBoardId();
372
+ const response = await client.post("/stories", {
373
+ ...args,
374
+ lane: normalizedLane,
375
+ ...(boardId ? { boardId } : {}),
376
+ });
93
377
  return {
94
378
  content: [
95
- {
96
- type: "text",
97
- text: JSON.stringify(response.data, null, 2),
98
- },
379
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
99
380
  ],
100
381
  };
101
382
  }
102
383
  case "get_iteration_context": {
103
- const response = await client.get("/context");
384
+ const boardId = await resolveBoardId();
385
+ let contextData = null;
386
+ if (!boardId) {
387
+ try {
388
+ const response = await client.get("/context");
389
+ contextData = response.data;
390
+ }
391
+ catch {
392
+ // Fall back to /stories if /context is unavailable.
393
+ }
394
+ }
395
+ const currentIteration = contextData?.currentIteration;
396
+ const backlog = contextData?.backlog;
397
+ const hasContextData = Array.isArray(currentIteration) &&
398
+ Array.isArray(backlog) &&
399
+ (currentIteration.length > 0 || backlog.length > 0);
400
+ const context = hasContextData
401
+ ? contextData
402
+ : buildIterationContext(await fetchStories(), contextData?.user ?? null);
104
403
  return {
105
404
  content: [
106
- {
107
- type: "text",
108
- text: JSON.stringify(response.data, null, 2),
109
- },
405
+ { type: "text", text: JSON.stringify(context, null, 2) },
110
406
  ],
111
407
  };
112
408
  }
409
+ case "create_hypothesis": {
410
+ const args = request.params.arguments;
411
+ const normalizedLane = await normalizeLane("Icebox");
412
+ const boardId = await resolveBoardId();
413
+ // Enforce Icebox lane
414
+ const payload = {
415
+ ...args,
416
+ lane: normalizedLane, // Map to lane style expected by API
417
+ ...(boardId ? { boardId } : {}),
418
+ };
419
+ const response = await client.post("/stories", payload);
420
+ return {
421
+ content: [
422
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
423
+ ],
424
+ };
425
+ }
426
+ case "record_learning": {
427
+ const args = request.params.arguments;
428
+ const { storyId, outcome_summary } = args;
429
+ const response = await client.patch(`/stories/${storyId}`, {
430
+ outcome_summary,
431
+ });
432
+ return {
433
+ content: [
434
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
435
+ ],
436
+ };
437
+ }
438
+ case "update_story": {
439
+ const args = request.params.arguments;
440
+ const { storyId, lane, ...rest } = args;
441
+ if (!storyId) {
442
+ throw new Error("storyId is required");
443
+ }
444
+ const normalizedLane = await normalizeLane(lane);
445
+ const payload = Object.fromEntries(Object.entries({
446
+ ...rest,
447
+ ...(normalizedLane ? { lane: normalizedLane } : {}),
448
+ }).filter(([, value]) => value !== undefined));
449
+ const response = await client.patch(`/stories/${storyId}`, payload);
450
+ return {
451
+ content: [
452
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
453
+ ],
454
+ };
455
+ }
456
+ case "list_objectives": {
457
+ const response = await client.get("/strategy/objectives");
458
+ return {
459
+ content: [
460
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
461
+ ],
462
+ };
463
+ }
464
+ case "list_epics": {
465
+ const epics = await fetchEpics();
466
+ return {
467
+ content: [
468
+ { type: "text", text: JSON.stringify(epics, null, 2) },
469
+ ],
470
+ };
471
+ }
472
+ case "create_epic": {
473
+ const args = request.params.arguments;
474
+ const boardId = await resolveBoardId();
475
+ const response = await client.post("/epics", {
476
+ ...args,
477
+ ...(boardId ? { boardId } : {}),
478
+ });
479
+ return {
480
+ content: [
481
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
482
+ ],
483
+ };
484
+ }
485
+ case "update_epic": {
486
+ const args = request.params.arguments;
487
+ const { epicId, ...rest } = args;
488
+ if (!epicId) {
489
+ throw new Error("epicId is required");
490
+ }
491
+ const payload = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined));
492
+ const response = await client.patch(`/epics/${epicId}`, payload);
493
+ return {
494
+ content: [
495
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
496
+ ],
497
+ };
498
+ }
499
+ case "prepare_implementation": {
500
+ const args = request.params.arguments;
501
+ const { storyId } = args;
502
+ const storyResponse = await client.get(`/stories/${storyId}`);
503
+ const story = storyResponse.data;
504
+ // Validation & Guardrails
505
+ const storyLane = typeof story.lane === "string" ? story.lane.trim().toLowerCase() : "";
506
+ const statusWarning = storyLane !== "current"
507
+ ? `\n> [!WARNING]\n> This story is currently in the **${story.lane}** lane. It should ideally be in **Current** before starting implementation.\n`
508
+ : "";
509
+ const acceptanceCriteria = story.acceptance_criteria ||
510
+ story.acceptanceCriteria ||
511
+ story.description ||
512
+ "No specific AC provided.";
513
+ const assumptions = story.learning_goals ||
514
+ story.learning_goal ||
515
+ story.learningGoals ||
516
+ story.hypothesis ||
517
+ "No specific learning goals identified.";
518
+ const formattedBrief = `
519
+ # Implementation Brief: ${story.title}
520
+
521
+ ${statusWarning}
522
+
523
+ ## Acceptance Criteria
524
+ Here’s the acceptance criteria I’m going to satisfy:
525
+ ${acceptanceCriteria}
526
+
527
+ ## Assumptions
528
+ Here are the assumptions I think we’re testing:
529
+ ${assumptions}
530
+
531
+ ## Proposal
532
+ Here’s the smallest change that will validate it:
533
+ [Agent should fill this in based on the context above]
534
+ `;
535
+ return {
536
+ content: [{ type: "text", text: formattedBrief.trim() }],
537
+ };
538
+ }
113
539
  default:
114
540
  throw new Error("Unknown tool");
115
541
  }
@@ -0,0 +1,55 @@
1
+ import axios from "axios";
2
+ const API_KEY = process.env.ELIXIUM_API_KEY;
3
+ const API_URL = process.env.ELIXIUM_API_URL || "https://elixium.ai/api";
4
+ const BOARD_SLUG = process.env.ELIXIUM_BOARD_SLUG;
5
+ const client = axios.create({
6
+ baseURL: API_URL,
7
+ headers: {
8
+ "x-api-key": API_KEY,
9
+ "Content-Type": "application/json",
10
+ ...(BOARD_SLUG ? { "x-board-slug": BOARD_SLUG } : {}),
11
+ },
12
+ });
13
+ async function testPrepare(storyId) {
14
+ try {
15
+ const storyResponse = await client.get(`/stories/${storyId}`);
16
+ const story = storyResponse.data;
17
+ // Validation & Guardrails
18
+ const storyLane = typeof story.lane === "string" ? story.lane.trim().toLowerCase() : "";
19
+ const statusWarning = storyLane !== "current"
20
+ ? `\n> [!WARNING]\n> This story is currently in the **${story.lane}** lane. It should ideally be in **Current** before starting implementation.\n`
21
+ : "";
22
+ const acceptanceCriteria = story.acceptance_criteria ||
23
+ story.acceptanceCriteria ||
24
+ story.description ||
25
+ "No specific AC provided.";
26
+ const assumptions = story.learning_goals ||
27
+ story.learning_goal ||
28
+ story.learningGoals ||
29
+ story.hypothesis ||
30
+ "No specific learning goals identified.";
31
+ const formattedBrief = `
32
+ # Implementation Brief: ${story.title}
33
+
34
+ ${statusWarning}
35
+
36
+ ## Acceptance Criteria
37
+ Here’s the acceptance criteria I’m going to satisfy:
38
+ ${acceptanceCriteria}
39
+
40
+ ## Assumptions
41
+ Here are the assumptions I think we’re testing:
42
+ ${assumptions}
43
+
44
+ ## Proposal
45
+ Here’s the smallest change that will validate it:
46
+ [Agent should fill this in based on the context above]
47
+ `;
48
+ console.log(formattedBrief.trim());
49
+ }
50
+ catch (error) {
51
+ console.error("Error:", error.message);
52
+ }
53
+ }
54
+ const storyId = process.argv[2] || "ilX5KnrMSNQzhNvoWczh";
55
+ testPrepare(storyId);
package/package.json CHANGED
@@ -1,14 +1,19 @@
1
1
  {
2
2
  "name": "@elixium.ai/mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "MCP Server for Elixium.ai",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "package.json"
13
+ ],
9
14
  "main": "dist/index.js",
10
15
  "bin": {
11
- "elixium-mcp-server": "./dist/index.js"
16
+ "elixium-mcp-server": "dist/index.js"
12
17
  },
13
18
  "scripts": {
14
19
  "build": "tsc",
@@ -25,4 +30,4 @@
25
30
  "ts-node": "^10.9.0",
26
31
  "typescript": "^5.3.0"
27
32
  }
28
- }
33
+ }
package/src/index.ts DELETED
@@ -1,151 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import {
6
- CallToolRequestSchema,
7
- ListToolsRequestSchema,
8
- } from "@modelcontextprotocol/sdk/types.js";
9
- import axios from "axios";
10
- import { z } from "zod";
11
-
12
- const API_KEY = process.env.ELIXIUM_API_KEY;
13
- const API_URL = process.env.ELIXIUM_API_URL || "https://elixium.ai/api";
14
-
15
- if (!API_KEY) {
16
- console.error("Error: ELIXIUM_API_KEY environment variable is required");
17
- process.exit(1);
18
- }
19
-
20
- const client = axios.create({
21
- baseURL: API_URL,
22
- headers: {
23
- "x-api-key": API_KEY,
24
- "Content-Type": "application/json",
25
- },
26
- });
27
-
28
- const server = new Server(
29
- {
30
- name: "elixium-mcp-server",
31
- version: "0.1.0",
32
- },
33
- {
34
- capabilities: {
35
- tools: {},
36
- },
37
- }
38
- );
39
-
40
- server.setRequestHandler(ListToolsRequestSchema, async () => {
41
- return {
42
- tools: [
43
- {
44
- name: "list_stories",
45
- description: "List all stories on the Elixium board",
46
- inputSchema: {
47
- type: "object",
48
- properties: {},
49
- },
50
- },
51
- {
52
- name: "create_story",
53
- description: "Create a new story on the Elixium board",
54
- inputSchema: {
55
- type: "object",
56
- properties: {
57
- title: {
58
- type: "string",
59
- description: "Title of the story",
60
- },
61
- description: {
62
- type: "string",
63
- description: "Description of the story",
64
- },
65
- lane: {
66
- type: "string",
67
- description: "Lane to add the story to (BACKLOG, ICEBOX, CURRENT)",
68
- enum: ["BACKLOG", "ICEBOX", "CURRENT"],
69
- },
70
- points: {
71
- type: "number",
72
- description: "Story points (0, 1, 2, 3, 5, 8)",
73
- },
74
- },
75
- required: ["title"],
76
- },
77
- },
78
- {
79
- name: "get_iteration_context",
80
- description: "Get the current iteration context (current stories and backlog) for AI planning",
81
- inputSchema: {
82
- type: "object",
83
- properties: {},
84
- },
85
- },
86
- ],
87
- };
88
- });
89
-
90
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
91
- try {
92
- switch (request.params.name) {
93
- case "list_stories": {
94
- const response = await client.get("/stories");
95
- return {
96
- content: [
97
- {
98
- type: "text",
99
- text: JSON.stringify(response.data, null, 2),
100
- },
101
- ],
102
- };
103
- }
104
-
105
- case "create_story": {
106
- const args = request.params.arguments as any;
107
- const response = await client.post("/stories", args);
108
- return {
109
- content: [
110
- {
111
- type: "text",
112
- text: JSON.stringify(response.data, null, 2),
113
- },
114
- ],
115
- };
116
- }
117
-
118
- case "get_iteration_context": {
119
- const response = await client.get("/context");
120
- return {
121
- content: [
122
- {
123
- type: "text",
124
- text: JSON.stringify(response.data, null, 2),
125
- },
126
- ],
127
- };
128
- }
129
-
130
- default:
131
- throw new Error("Unknown tool");
132
- }
133
- } catch (error: any) {
134
- console.error("Error executing tool:", error.message);
135
- if (error.response) {
136
- console.error("Response data:", error.response.data);
137
- }
138
- return {
139
- content: [
140
- {
141
- type: "text",
142
- text: `Error: ${error.message}`,
143
- },
144
- ],
145
- isError: true,
146
- };
147
- }
148
- });
149
-
150
- const transport = new StdioServerTransport();
151
- await server.connect(transport);
package/tsconfig.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "Node16",
5
- "moduleResolution": "Node16",
6
- "outDir": "./dist",
7
- "rootDir": "./src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true
12
- },
13
- "include": ["src/**/*"]
14
- }