@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 +129 -0
- package/dist/index.js +449 -23
- package/dist/test-prepare.js +55 -0
- package/package.json +8 -3
- package/src/index.ts +0 -151
- package/tsconfig.json +0 -14
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 (
|
|
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,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elixium.ai/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
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": "
|
|
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
|
-
}
|