@elixium.ai/mcp-server 0.1.0 → 0.1.1

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,84 @@
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
+ - **Iteration Context**: Provide the AI with the full context of your Current and Backlog lanes for better planning.
9
+
10
+ ## Setup
11
+
12
+ ### 1. Install
13
+ If you do not have the Elixium codebase, install the MCP server package:
14
+ ```bash
15
+ npm install -g @elixium.ai/mcp-server
16
+ ```
17
+
18
+ Air-gapped/offline install (download the `.tgz` from GitHub Releases first):
19
+ ```bash
20
+ npm install -g /path/to/elixium.ai-mcp-server-0.1.0.tgz
21
+ ```
22
+
23
+ If you are developing from source:
24
+ ```bash
25
+ cd mcp-server
26
+ npm install
27
+ npm run build
28
+ ```
29
+
30
+ ### 2. Authentication (API Key)
31
+ Elixium uses a secure, tenant-scoped API key system for external integrations.
32
+
33
+ 1. **Request a Key**: Ask your Elixium administrator to create an API key in the `sys_api_keys` collection.
34
+ 2. **Scope**: Each key is hardcoded to a specific `tenantId` in the database, ensuring your AI agent only has access to your workspace.
35
+ 3. **Board Selection**: Optionally scope the MCP server to a single board by setting `ELIXIUM_BOARD_SLUG`.
36
+
37
+ ### 3. Configuration
38
+ Add the following to your IDE's MCP configuration (e.g., `mcp_config.json`):
39
+
40
+ ```json
41
+ {
42
+ "mcpServers": {
43
+ "elixium": {
44
+ "command": "elixium-mcp-server",
45
+ "args": [],
46
+ "env": {
47
+ "ELIXIUM_API_KEY": "your_api_key_here",
48
+ "ELIXIUM_API_URL": "https://your-tenant-slug.elixium.ai/api",
49
+ "ELIXIUM_BOARD_SLUG": "main",
50
+ "ELIXIUM_LANE_STYLE": "upper"
51
+ }
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ > [!IMPORTANT]
58
+ > Always use your **Tenant Subdomain** in the `ELIXIUM_API_URL` (e.g., `https://indirecttek.elixium.ai/api`) to ensure the server correctly resolves your workspace context.
59
+ >
60
+ > If you set `ELIXIUM_BOARD_SLUG`, the MCP server will only read/write stories for that board.
61
+ > The server resolves the board slug to a boardId on startup, so the slug must match an existing board.
62
+ >
63
+ > `ELIXIUM_LANE_STYLE` is optional. Use `upper` for APIs that store lanes as `CURRENT/BACKLOG`, or `title` for `Current/Backlog`. The server auto-detects when possible.
64
+ >
65
+ > If you built the MCP server from source, set `command` to `node` and `args`
66
+ > to the absolute path of `dist/index.js`.
67
+
68
+ ## Usage
69
+ Once configured, your AI agent will have access to tools like `list_stories` and `create_story`. You can use these to groom your backlog or generate implementation plans based on real-time board data.
70
+
71
+ ## Maintainers: build a clean release tarball (gh CLI)
72
+ This package uses the `files` list in `package.json` to keep the tarball small.
73
+
74
+ ```bash
75
+ cd mcp-server
76
+ npm install
77
+ npm run build
78
+ npm pack
79
+ ```
80
+
81
+ ```bash
82
+ gh release create mcp-server-v0.1.0 elixium.ai-mcp-server-0.1.0.tgz \
83
+ -t "MCP Server v0.1.0" -n "Air-gapped install artifact"
84
+ ```
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,11 +1,16 @@
1
1
  {
2
2
  "name": "@elixium.ai/mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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
16
  "elixium-mcp-server": "./dist/index.js"
@@ -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
- }