@elixium.ai/mcp-server 0.2.1 → 0.2.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/dist/index.js +65 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +10,14 @@ const API_URL = process.env.ELIXIUM_API_URL || "https://elixium.ai/api";
|
|
|
10
10
|
const BOARD_SLUG = process.env.ELIXIUM_BOARD_SLUG;
|
|
11
11
|
const LANE_STYLE_ENV = process.env.ELIXIUM_LANE_STYLE;
|
|
12
12
|
const USER_EMAIL = process.env.ELIXIUM_USER_EMAIL; // Optional: Override requester email for stories
|
|
13
|
+
// UUID v4 format validation — prevents 500s from partial/truncated IDs
|
|
14
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
15
|
+
function assertUUID(value, label) {
|
|
16
|
+
if (!UUID_RE.test(value)) {
|
|
17
|
+
throw new Error(`Invalid ${label}: "${value}" is not a valid UUID. ` +
|
|
18
|
+
`Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (full UUID from list_stories/list_epics).`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
13
21
|
const CLI_ARGS = process.argv.slice(2);
|
|
14
22
|
const hasArg = (flag) => CLI_ARGS.includes(flag);
|
|
15
23
|
const getArgValue = (flag) => {
|
|
@@ -604,6 +612,20 @@ const createServer = () => {
|
|
|
604
612
|
required: ["storyId", "testPlan"],
|
|
605
613
|
},
|
|
606
614
|
},
|
|
615
|
+
{
|
|
616
|
+
name: "approve_tests",
|
|
617
|
+
description: "Approve a proposed test plan so implementation can proceed. Transitions workflow from tests_proposed to tests_approved. This is the human approval gate in the TDD workflow.",
|
|
618
|
+
inputSchema: {
|
|
619
|
+
type: "object",
|
|
620
|
+
properties: {
|
|
621
|
+
storyId: {
|
|
622
|
+
type: "string",
|
|
623
|
+
description: "ID of the story whose test plan to approve",
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
required: ["storyId"],
|
|
627
|
+
},
|
|
628
|
+
},
|
|
607
629
|
{
|
|
608
630
|
name: "submit_for_review",
|
|
609
631
|
description: "Submit implementation for human review. Only works if tests are approved. Sets state to finished.",
|
|
@@ -836,6 +858,7 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
836
858
|
case "record_learning": {
|
|
837
859
|
const args = request.params.arguments;
|
|
838
860
|
const { storyId, outcome_summary } = args;
|
|
861
|
+
assertUUID(storyId, "storyId");
|
|
839
862
|
const response = await client.patch(`/stories/${storyId}`, {
|
|
840
863
|
outcome_summary,
|
|
841
864
|
});
|
|
@@ -851,6 +874,7 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
851
874
|
if (!storyId) {
|
|
852
875
|
throw new Error("storyId is required");
|
|
853
876
|
}
|
|
877
|
+
assertUUID(storyId, "storyId");
|
|
854
878
|
// Guardrail: Block AI from setting accepted/rejected states (human-in-the-loop)
|
|
855
879
|
const blockedStates = ["accepted", "rejected"];
|
|
856
880
|
if (state && blockedStates.includes(state.toLowerCase())) {
|
|
@@ -904,6 +928,7 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
904
928
|
if (!epicId) {
|
|
905
929
|
throw new Error("epicId is required");
|
|
906
930
|
}
|
|
931
|
+
assertUUID(epicId, "epicId");
|
|
907
932
|
const payload = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined));
|
|
908
933
|
const response = await client.patch(`/epics/${epicId}`, payload);
|
|
909
934
|
return {
|
|
@@ -915,6 +940,7 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
915
940
|
case "prepare_implementation": {
|
|
916
941
|
const args = request.params.arguments;
|
|
917
942
|
const { storyId } = args;
|
|
943
|
+
assertUUID(storyId, "storyId");
|
|
918
944
|
const storyResponse = await client.get(`/stories/${storyId}`);
|
|
919
945
|
const story = storyResponse.data;
|
|
920
946
|
// Validation & Guardrails
|
|
@@ -959,6 +985,7 @@ Here’s the smallest change that will validate it:
|
|
|
959
985
|
if (!storyId) {
|
|
960
986
|
throw new Error("storyId is required");
|
|
961
987
|
}
|
|
988
|
+
assertUUID(storyId, "storyId");
|
|
962
989
|
const response = await client.post(`/stories/${storyId}/start`, {
|
|
963
990
|
branchPrefix: branchPrefix || "feat",
|
|
964
991
|
trunkBased,
|
|
@@ -1041,6 +1068,7 @@ ${result.workflow_reminder}
|
|
|
1041
1068
|
if (!storyId) {
|
|
1042
1069
|
throw new Error("storyId is required");
|
|
1043
1070
|
}
|
|
1071
|
+
assertUUID(storyId, "storyId");
|
|
1044
1072
|
if (!testPlan) {
|
|
1045
1073
|
throw new Error("testPlan is required");
|
|
1046
1074
|
}
|
|
@@ -1062,6 +1090,30 @@ ${(result.test_file_paths || []).map((p) => `- \`${p}\``).join("\n") || "No test
|
|
|
1062
1090
|
${result.message}
|
|
1063
1091
|
|
|
1064
1092
|
> 🛑 **BLOCKED:** Implementation cannot proceed until a human approves this test plan.
|
|
1093
|
+
`;
|
|
1094
|
+
return {
|
|
1095
|
+
content: [{ type: "text", text: formattedResult.trim() }],
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
case "approve_tests": {
|
|
1099
|
+
const args = request.params.arguments;
|
|
1100
|
+
const { storyId } = args;
|
|
1101
|
+
if (!storyId) {
|
|
1102
|
+
throw new Error("storyId is required");
|
|
1103
|
+
}
|
|
1104
|
+
assertUUID(storyId, "storyId");
|
|
1105
|
+
const response = await client.post(`/stories/${storyId}/approve-tests`);
|
|
1106
|
+
const result = response.data;
|
|
1107
|
+
const formattedResult = `
|
|
1108
|
+
# Test Plan Approved
|
|
1109
|
+
|
|
1110
|
+
**Story ID:** ${storyId}
|
|
1111
|
+
**Workflow Stage:** ${result.workflow_stage}
|
|
1112
|
+
|
|
1113
|
+
## Status
|
|
1114
|
+
${result.message}
|
|
1115
|
+
|
|
1116
|
+
> ✅ **Approved:** Agent may now proceed with implementation.
|
|
1065
1117
|
`;
|
|
1066
1118
|
return {
|
|
1067
1119
|
content: [{ type: "text", text: formattedResult.trim() }],
|
|
@@ -1073,6 +1125,7 @@ ${result.message}
|
|
|
1073
1125
|
if (!storyId) {
|
|
1074
1126
|
throw new Error("storyId is required");
|
|
1075
1127
|
}
|
|
1128
|
+
assertUUID(storyId, "storyId");
|
|
1076
1129
|
const response = await client.post(`/stories/${storyId}/submit-review`, {
|
|
1077
1130
|
commitHash,
|
|
1078
1131
|
testResults,
|
|
@@ -1131,6 +1184,7 @@ ${result.next_step}
|
|
|
1131
1184
|
let { provider } = args;
|
|
1132
1185
|
if (!storyId)
|
|
1133
1186
|
throw new Error("storyId is required");
|
|
1187
|
+
assertUUID(storyId, "storyId");
|
|
1134
1188
|
// Default provider from infrastructure profile if not explicitly provided
|
|
1135
1189
|
if (!provider) {
|
|
1136
1190
|
const costConfig = await fetchFeatureConfig();
|
|
@@ -1200,6 +1254,7 @@ ${(estimate.assumptions || []).map((a) => `- ${a}`).join("\n")}
|
|
|
1200
1254
|
const { epicId } = args;
|
|
1201
1255
|
if (!epicId)
|
|
1202
1256
|
throw new Error("epicId is required");
|
|
1257
|
+
assertUUID(epicId, "epicId");
|
|
1203
1258
|
// Fetch all stories and filter by epicId
|
|
1204
1259
|
const allStories = await fetchStories();
|
|
1205
1260
|
const epicStories = allStories.filter((s) => s.epicId === epicId);
|
|
@@ -1302,11 +1357,20 @@ ${unestimated.length === 0
|
|
|
1302
1357
|
if (error.response) {
|
|
1303
1358
|
console.error("Response data:", error.response.data);
|
|
1304
1359
|
}
|
|
1360
|
+
let errorText = `Error: ${error.message}`;
|
|
1361
|
+
if (error.response?.status) {
|
|
1362
|
+
errorText += ` (HTTP ${error.response.status})`;
|
|
1363
|
+
}
|
|
1364
|
+
if (error.response?.data) {
|
|
1365
|
+
const data = error.response.data;
|
|
1366
|
+
const detail = typeof data === "string" ? data : JSON.stringify(data);
|
|
1367
|
+
errorText += `\nDetails: ${detail}`;
|
|
1368
|
+
}
|
|
1305
1369
|
return {
|
|
1306
1370
|
content: [
|
|
1307
1371
|
{
|
|
1308
1372
|
type: "text",
|
|
1309
|
-
text:
|
|
1373
|
+
text: errorText,
|
|
1310
1374
|
},
|
|
1311
1375
|
],
|
|
1312
1376
|
isError: true,
|