@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 +84 -0
- package/dist/index.js +449 -23
- package/dist/test-prepare.js +55 -0
- package/package.json +7 -2
- package/src/index.ts +0 -151
- package/tsconfig.json +0 -14
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 (
|
|
55
|
-
enum: [
|
|
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: "
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
}
|