@elixium.ai/mcp-server 0.2.1 → 0.3.0

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.
Files changed (2) hide show
  1. package/dist/index.js +314 -18
  2. 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) => {
@@ -219,6 +227,105 @@ const getTeamProfile = async () => {
219
227
  const config = await fetchFeatureConfig();
220
228
  return config.teamProfile;
221
229
  };
230
+ const formatTeamContext = (config) => {
231
+ const features = config.features;
232
+ const tp = config.teamProfile;
233
+ const bd = config.branchingDefaults;
234
+ const infra = config.infrastructureProfile;
235
+ return `
236
+ ## Team Context
237
+
238
+ ### Enabled Features
239
+ ${features.balancedTeam ? "✅" : "❌"} Balanced Team
240
+ ${features.learningLoop ? "✅" : "❌"} Learning Loop
241
+ ${features.tddWorkflow ? "✅" : "❌"} TDD Workflow
242
+ ${features.aiTools ? "✅" : "❌"} AI Tools
243
+
244
+ ### Team Profile
245
+ ${tp ? `- Team Size: ${tp.teamSize || "Not set"}
246
+ - Has Designer: ${tp.hasDesigner ? "Yes" : "No"}
247
+ - Has Product Manager: ${tp.hasProductManager ? "Yes" : "No"}
248
+ - Has QA: ${tp.hasQA ? "Yes" : "No"}
249
+ - Development Approach: ${tp.developmentApproach || "Not set"}` : "Team profile not configured. Set up in Settings → Profile."}
250
+
251
+ ### Branching Strategy
252
+ - Trunk-Based: ${bd?.trunkBased ? "Yes" : "No"}
253
+ - Auto-Merge: ${bd?.autoMerge ? "Yes" : "No"}
254
+ - Source: ${bd?.source || "default"}
255
+ ${infra?.provider ? `
256
+ ### Infrastructure
257
+ - Provider: ${infra.provider.toUpperCase()}
258
+ - Regions: ${infra.regions?.join(", ") || "Not set"}
259
+ - Compliance: ${infra.complianceFrameworks?.map((f) => f.toUpperCase()).join(", ") || "None"}` : ""}
260
+ `.trim();
261
+ };
262
+ const fetchBoardSettings = async () => {
263
+ try {
264
+ const boardId = await resolveBoardId();
265
+ if (!boardId)
266
+ return undefined;
267
+ const response = await client.get("/boards");
268
+ const boards = Array.isArray(response.data) ? response.data : [];
269
+ const board = boards.find((b) => b.id === boardId);
270
+ return board?.settings;
271
+ }
272
+ catch {
273
+ return undefined;
274
+ }
275
+ };
276
+ const formatDorDodSection = (boardSettings, dorChecklist, dodChecklist) => {
277
+ if (!boardSettings)
278
+ return "";
279
+ const sections = [];
280
+ const dorConfig = boardSettings.definitionOfReady;
281
+ if (dorConfig?.enabled && dorConfig.items?.length > 0) {
282
+ const cl = dorChecklist || {};
283
+ const lines = dorConfig.items.map((item) => {
284
+ const met = cl[item.id] === true;
285
+ return `- ${met ? "✅" : "❌"} ${item.label}`;
286
+ });
287
+ const unmetItems = dorConfig.items.filter((item) => cl[item.id] !== true);
288
+ const met = dorConfig.items.length - unmetItems.length;
289
+ sections.push(`### Definition of Ready (${met}/${dorConfig.items.length})`);
290
+ sections.push(lines.join("\n"));
291
+ if (unmetItems.length > 0) {
292
+ const unmetLabels = unmetItems.map((i) => i.label).join(", ");
293
+ sections.push(`\n> **DoR unmet:** ${unmetLabels}`);
294
+ }
295
+ }
296
+ const dodConfig = boardSettings.definitionOfDone;
297
+ if (dodConfig?.enabled && dodConfig.items?.length > 0) {
298
+ const cl = dodChecklist || {};
299
+ const lines = dodConfig.items.map((item) => {
300
+ const met = cl[item.id] === true;
301
+ return `- ${met ? "✅" : "❌"} ${item.label}`;
302
+ });
303
+ const unmetItems = dodConfig.items.filter((item) => cl[item.id] !== true);
304
+ const met = dodConfig.items.length - unmetItems.length;
305
+ sections.push(`### Definition of Done (${met}/${dodConfig.items.length})`);
306
+ sections.push(lines.join("\n"));
307
+ if (unmetItems.length > 0) {
308
+ const unmetLabels = unmetItems.map((i) => i.label).join(", ");
309
+ sections.push(`\n> **DoD unmet:** ${unmetLabels}`);
310
+ }
311
+ }
312
+ if (sections.length === 0)
313
+ return "";
314
+ return `\n## Checklists\n${sections.join("\n")}\n`;
315
+ };
316
+ const formatDorWarnings = (boardSettings, dorChecklist) => {
317
+ if (!boardSettings)
318
+ return "";
319
+ const dorConfig = boardSettings.definitionOfReady;
320
+ if (!dorConfig?.enabled || dorConfig.enforcement !== "warn")
321
+ return "";
322
+ const cl = dorChecklist || {};
323
+ const unmetItems = (dorConfig.items || []).filter((item) => cl[item.id] !== true);
324
+ if (unmetItems.length === 0)
325
+ return "";
326
+ const warningLines = unmetItems.map((i) => `- ⚠️ DoR not met: ${i.label}`).join("\n");
327
+ return `\n## ⚠️ DoR Warnings\n${warningLines}\n\nConsider addressing these before starting implementation.\n`;
328
+ };
222
329
  const fetchStories = async () => {
223
330
  const boardId = await resolveBoardId();
224
331
  const slug = normalizeBoardSlug(BOARD_SLUG);
@@ -604,6 +711,20 @@ const createServer = () => {
604
711
  required: ["storyId", "testPlan"],
605
712
  },
606
713
  },
714
+ {
715
+ name: "approve_tests",
716
+ 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.",
717
+ inputSchema: {
718
+ type: "object",
719
+ properties: {
720
+ storyId: {
721
+ type: "string",
722
+ description: "ID of the story whose test plan to approve",
723
+ },
724
+ },
725
+ required: ["storyId"],
726
+ },
727
+ },
607
728
  {
608
729
  name: "submit_for_review",
609
730
  description: "Submit implementation for human review. Only works if tests are approved. Sets state to finished.",
@@ -630,6 +751,20 @@ const createServer = () => {
630
751
  required: ["storyId"],
631
752
  },
632
753
  },
754
+ {
755
+ name: "review_pr",
756
+ description: "Run AI review on a story's PR diff against acceptance criteria and DoD. Fetches diff, runs AI validation, posts findings as GitHub PR comment, and stores results on the story.",
757
+ inputSchema: {
758
+ type: "object",
759
+ properties: {
760
+ storyId: {
761
+ type: "string",
762
+ description: "ID of the story to review",
763
+ },
764
+ },
765
+ required: ["storyId"],
766
+ },
767
+ },
633
768
  ] : [];
634
769
  // Learning Loop tools (conditional on feature flag)
635
770
  const learningLoopTools = learningLoopEnabled ? [
@@ -674,7 +809,7 @@ const createServer = () => {
674
809
  try {
675
810
  const toolName = request.params.name;
676
811
  // Check TDD workflow tools
677
- const tddWorkflowTools = ["start_story", "propose_test_plan", "submit_for_review"];
812
+ const tddWorkflowTools = ["start_story", "propose_test_plan", "submit_for_review", "review_pr"];
678
813
  if (tddWorkflowTools.includes(toolName)) {
679
814
  const enabled = await isTddWorkflowEnabled();
680
815
  if (!enabled) {
@@ -836,6 +971,7 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
836
971
  case "record_learning": {
837
972
  const args = request.params.arguments;
838
973
  const { storyId, outcome_summary } = args;
974
+ assertUUID(storyId, "storyId");
839
975
  const response = await client.patch(`/stories/${storyId}`, {
840
976
  outcome_summary,
841
977
  });
@@ -851,6 +987,7 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
851
987
  if (!storyId) {
852
988
  throw new Error("storyId is required");
853
989
  }
990
+ assertUUID(storyId, "storyId");
854
991
  // Guardrail: Block AI from setting accepted/rejected states (human-in-the-loop)
855
992
  const blockedStates = ["accepted", "rejected"];
856
993
  if (state && blockedStates.includes(state.toLowerCase())) {
@@ -862,11 +999,38 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
862
999
  ...(state ? { state } : {}),
863
1000
  ...(normalizedLane ? { lane: normalizedLane } : {}),
864
1001
  }).filter(([, value]) => value !== undefined));
865
- const response = await client.patch(`/stories/${storyId}`, payload);
1002
+ let response;
1003
+ try {
1004
+ response = await client.patch(`/stories/${storyId}`, payload);
1005
+ }
1006
+ catch (err) {
1007
+ // DoR/DoD enforcement: surface 422 with unmet criteria to agent
1008
+ if (err.response?.status === 422 && err.response?.data?.unmetCriteria) {
1009
+ const errorMsg = err.response.data.error || "Checklist not met";
1010
+ const unmet = err.response.data.unmetCriteria;
1011
+ const unmetList = unmet.map((c) => `- ❌ ${c.label}`).join("\n");
1012
+ const isDod = errorMsg.includes("Done");
1013
+ const checklistField = isDod ? "dodChecklist" : "dorChecklist";
1014
+ return {
1015
+ content: [
1016
+ {
1017
+ type: "text",
1018
+ text: `# ⛔ ${errorMsg}\n\nCannot move story. The following criteria are unmet:\n\n${unmetList}\n\n**Action:** Update the story's \`${checklistField}\` to mark items as met, or ask the team to review the requirements in Board Settings.`,
1019
+ },
1020
+ ],
1021
+ };
1022
+ }
1023
+ throw err;
1024
+ }
1025
+ const data = response.data;
1026
+ let resultText = JSON.stringify(data, null, 2);
1027
+ // Surface DoR warnings to agent
1028
+ if (data.warnings && data.warnings.length > 0) {
1029
+ const warningList = data.warnings.map((w) => `- ⚠️ ${w}`).join("\n");
1030
+ resultText += `\n\n---\n# ⚠️ DoR Warnings\n\nStory moved to Current, but the following DoR items are unmet:\n\n${warningList}\n\nConsider addressing these before starting implementation.`;
1031
+ }
866
1032
  return {
867
- content: [
868
- { type: "text", text: JSON.stringify(response.data, null, 2) },
869
- ],
1033
+ content: [{ type: "text", text: resultText }],
870
1034
  };
871
1035
  }
872
1036
  case "list_objectives": {
@@ -904,6 +1068,7 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
904
1068
  if (!epicId) {
905
1069
  throw new Error("epicId is required");
906
1070
  }
1071
+ assertUUID(epicId, "epicId");
907
1072
  const payload = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined));
908
1073
  const response = await client.patch(`/epics/${epicId}`, payload);
909
1074
  return {
@@ -915,7 +1080,13 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
915
1080
  case "prepare_implementation": {
916
1081
  const args = request.params.arguments;
917
1082
  const { storyId } = args;
918
- const storyResponse = await client.get(`/stories/${storyId}`);
1083
+ assertUUID(storyId, "storyId");
1084
+ // Fetch story, team context, and board settings in parallel
1085
+ const [storyResponse, teamConfig, boardSettings] = await Promise.all([
1086
+ client.get(`/stories/${storyId}`),
1087
+ fetchFeatureConfig(),
1088
+ fetchBoardSettings(),
1089
+ ]);
919
1090
  const story = storyResponse.data;
920
1091
  // Validation & Guardrails
921
1092
  const storyLane = typeof story.lane === "string" ? story.lane.trim().toLowerCase() : "";
@@ -931,11 +1102,23 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
931
1102
  story.learningGoals ||
932
1103
  story.hypothesis ||
933
1104
  "No specific learning goals identified.";
1105
+ const revisionFeedbackSection = story.test_plan_feedback
1106
+ ? `\n## Test Plan Revision Feedback\n> ${story.test_plan_feedback.split("\n").join("\n> ")}\n\n> **Action:** Revise your test plan based on this feedback and call \`propose_test_plan\` again.\n`
1107
+ : "";
1108
+ let aiReviewSection = "";
1109
+ if (story.ai_review) {
1110
+ const r = story.ai_review;
1111
+ aiReviewSection = `\n## Previous AI Review\n> ${r.summary || "Review completed."}\n`;
1112
+ if (r.concerns?.length > 0) {
1113
+ aiReviewSection += "\n**Concerns to address:**\n" +
1114
+ r.concerns.map((c) => `- **${c.severity}:** ${c.description}`).join("\n") + "\n";
1115
+ }
1116
+ }
934
1117
  const formattedBrief = `
935
1118
  # Implementation Brief: ${story.title}
936
1119
 
937
1120
  ${statusWarning}
938
-
1121
+ ${revisionFeedbackSection}${aiReviewSection}
939
1122
  ## Acceptance Criteria
940
1123
  Here’s the acceptance criteria I’m going to satisfy:
941
1124
  ${acceptanceCriteria}
@@ -944,6 +1127,8 @@ ${acceptanceCriteria}
944
1127
  Here are the assumptions I think we’re testing:
945
1128
  ${assumptions}
946
1129
 
1130
+ ${formatTeamContext(teamConfig)}
1131
+ ${formatDorDodSection(boardSettings, story.dorChecklist, story.dodChecklist)}
947
1132
  ## Proposal
948
1133
  Here’s the smallest change that will validate it:
949
1134
  [Agent should fill this in based on the context above]
@@ -959,12 +1144,20 @@ Here’s the smallest change that will validate it:
959
1144
  if (!storyId) {
960
1145
  throw new Error("storyId is required");
961
1146
  }
962
- const response = await client.post(`/stories/${storyId}/start`, {
963
- branchPrefix: branchPrefix || "feat",
964
- trunkBased,
965
- autoMerge,
966
- });
1147
+ assertUUID(storyId, "storyId");
1148
+ // Fetch story start, team config, and board settings in parallel
1149
+ const [response, teamConfig, boardSettings] = await Promise.all([
1150
+ client.post(`/stories/${storyId}/start`, {
1151
+ branchPrefix: branchPrefix || "feat",
1152
+ trunkBased,
1153
+ autoMerge,
1154
+ }),
1155
+ fetchFeatureConfig(),
1156
+ fetchBoardSettings(),
1157
+ ]);
967
1158
  const result = response.data;
1159
+ const teamContext = formatTeamContext(teamConfig);
1160
+ const dorWarnings = formatDorWarnings(boardSettings, result.dorChecklist);
968
1161
  const isTrunk = result.trunkBased;
969
1162
  const isAutoMerge = result.autoMerge;
970
1163
  let formattedResult;
@@ -989,7 +1182,9 @@ ${result.acceptance_criteria || "No specific AC provided."}
989
1182
 
990
1183
  > **Feature Flag:** Wrap new behavior with \`${result.featureFlagName}\` for safe isolation.
991
1184
  > **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
992
- `;
1185
+
1186
+ ${teamContext}
1187
+ ${dorWarnings}`;
993
1188
  }
994
1189
  else if (isTrunk && isAutoMerge) {
995
1190
  formattedResult = `
@@ -1013,7 +1208,9 @@ ${result.acceptance_criteria || "No specific AC provided."}
1013
1208
 
1014
1209
  > **Feature Flag:** Wrap new behavior with \`${result.featureFlagName}\` for safe isolation.
1015
1210
  > **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
1016
- `;
1211
+
1212
+ ${teamContext}
1213
+ ${dorWarnings}`;
1017
1214
  }
1018
1215
  else {
1019
1216
  formattedResult = `
@@ -1029,7 +1226,9 @@ ${result.acceptance_criteria || "No specific AC provided."}
1029
1226
  ${result.workflow_reminder}
1030
1227
 
1031
1228
  > **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
1032
- `;
1229
+
1230
+ ${teamContext}
1231
+ ${dorWarnings}`;
1033
1232
  }
1034
1233
  return {
1035
1234
  content: [{ type: "text", text: formattedResult.trim() }],
@@ -1041,20 +1240,33 @@ ${result.workflow_reminder}
1041
1240
  if (!storyId) {
1042
1241
  throw new Error("storyId is required");
1043
1242
  }
1243
+ assertUUID(storyId, "storyId");
1044
1244
  if (!testPlan) {
1045
1245
  throw new Error("testPlan is required");
1046
1246
  }
1247
+ // Fetch story to check for previous revision feedback
1248
+ let previousFeedback = null;
1249
+ try {
1250
+ const storyResponse = await client.get(`/stories/${storyId}`);
1251
+ previousFeedback = storyResponse.data?.test_plan_feedback || null;
1252
+ }
1253
+ catch (_) {
1254
+ // Non-blocking — proceed without feedback context
1255
+ }
1047
1256
  const response = await client.post(`/stories/${storyId}/propose-tests`, {
1048
1257
  testPlan,
1049
1258
  testFilePaths,
1050
1259
  });
1051
1260
  const result = response.data;
1261
+ const feedbackSection = previousFeedback
1262
+ ? `\n## Previous Revision Feedback\n> ${previousFeedback.split("\n").join("\n> ")}\n`
1263
+ : "";
1052
1264
  const formattedResult = `
1053
1265
  # Test Plan Proposed
1054
1266
 
1055
1267
  **Story ID:** ${storyId}
1056
1268
  **Workflow Stage:** ${result.workflow_stage}
1057
-
1269
+ ${feedbackSection}
1058
1270
  ## Test Files
1059
1271
  ${(result.test_file_paths || []).map((p) => `- \`${p}\``).join("\n") || "No test files specified"}
1060
1272
 
@@ -1062,6 +1274,30 @@ ${(result.test_file_paths || []).map((p) => `- \`${p}\``).join("\n") || "No test
1062
1274
  ${result.message}
1063
1275
 
1064
1276
  > 🛑 **BLOCKED:** Implementation cannot proceed until a human approves this test plan.
1277
+ `;
1278
+ return {
1279
+ content: [{ type: "text", text: formattedResult.trim() }],
1280
+ };
1281
+ }
1282
+ case "approve_tests": {
1283
+ const args = request.params.arguments;
1284
+ const { storyId } = args;
1285
+ if (!storyId) {
1286
+ throw new Error("storyId is required");
1287
+ }
1288
+ assertUUID(storyId, "storyId");
1289
+ const response = await client.post(`/stories/${storyId}/approve-tests`);
1290
+ const result = response.data;
1291
+ const formattedResult = `
1292
+ # Test Plan Approved
1293
+
1294
+ **Story ID:** ${storyId}
1295
+ **Workflow Stage:** ${result.workflow_stage}
1296
+
1297
+ ## Status
1298
+ ${result.message}
1299
+
1300
+ > ✅ **Approved:** Agent may now proceed with implementation.
1065
1301
  `;
1066
1302
  return {
1067
1303
  content: [{ type: "text", text: formattedResult.trim() }],
@@ -1073,6 +1309,7 @@ ${result.message}
1073
1309
  if (!storyId) {
1074
1310
  throw new Error("storyId is required");
1075
1311
  }
1312
+ assertUUID(storyId, "storyId");
1076
1313
  const response = await client.post(`/stories/${storyId}/submit-review`, {
1077
1314
  commitHash,
1078
1315
  testResults,
@@ -1098,6 +1335,14 @@ git branch -d ${instr.sourceBranch}
1098
1335
  \`\`\`
1099
1336
  `;
1100
1337
  }
1338
+ // Format PR section if PR was created or failed
1339
+ let prSection = "";
1340
+ if (result.pr_url && result.pr_number) {
1341
+ prSection = `\n## Pull Request\n**PR:** [#${result.pr_number}](${result.pr_url})\n**Status:** ${result.pr_state || "open"}\n> DoD checklist and acceptance criteria included in PR body.\n`;
1342
+ }
1343
+ else if (result.pr_error) {
1344
+ prSection = `\n## Pull Request\n> PR creation failed: ${result.pr_error}\n> Workflow proceeded successfully — create PR manually if needed.\n`;
1345
+ }
1101
1346
  const formattedResult = `
1102
1347
  # Implementation Submitted for Review
1103
1348
 
@@ -1113,7 +1358,7 @@ ${result.featureFlagName ? `**Feature Flag:** \`${result.featureFlagName}\`` : "
1113
1358
 
1114
1359
  ## Commits
1115
1360
  ${(result.commit_hashes || []).map((h) => `- \`${h}\``).join("\n") || "No commits recorded"}
1116
- ${autoMergeSection}
1361
+ ${autoMergeSection}${prSection}
1117
1362
  ## Next Step
1118
1363
  ${result.next_step}
1119
1364
 
@@ -1125,12 +1370,53 @@ ${result.next_step}
1125
1370
  content: [{ type: "text", text: formattedResult.trim() }],
1126
1371
  };
1127
1372
  }
1373
+ case "review_pr": {
1374
+ const args = request.params.arguments;
1375
+ const { storyId } = args;
1376
+ if (!storyId)
1377
+ throw new Error("storyId is required");
1378
+ assertUUID(storyId, "storyId");
1379
+ const reviewResponse = await client.post(`/stories/${storyId}/ai-review`);
1380
+ const reviewResult = reviewResponse.data;
1381
+ const review = reviewResult.ai_review;
1382
+ let acSection = "";
1383
+ if (review?.acCoverage?.length > 0) {
1384
+ acSection = "\n## Acceptance Criteria Coverage\n" +
1385
+ review.acCoverage.map((ac) => {
1386
+ const icon = ac.status === "met" ? "+" : ac.status === "partial" ? "~" : "-";
1387
+ return `[${icon}] **${ac.status.toUpperCase()}:** ${ac.criterion}\n ${ac.evidence}`;
1388
+ }).join("\n");
1389
+ }
1390
+ let dodSection = "";
1391
+ if (review?.dodValidation?.length > 0) {
1392
+ dodSection = "\n\n## Definition of Done Validation\n" +
1393
+ review.dodValidation.map((d) => `[${d.aiVerified ? "x" : " "}] ${d.label} — ${d.note}`).join("\n");
1394
+ }
1395
+ let concernsSection = "";
1396
+ if (review?.concerns?.length > 0) {
1397
+ concernsSection = "\n\n## Concerns\n" +
1398
+ review.concerns.map((c) => `- **${c.severity}:** ${c.description}${c.file ? ` (${c.file}${c.line ? `:${c.line}` : ""})` : ""}`).join("\n");
1399
+ }
1400
+ const formattedReview = `
1401
+ # AI Review — Story ${storyId.substring(0, 8)}
1402
+
1403
+ ${review?.summary || "Review completed."}
1404
+ ${acSection}${dodSection}${concernsSection}
1405
+
1406
+ ---
1407
+ _Generated by Elixium AI Review Intelligence_
1408
+ `.trim();
1409
+ return {
1410
+ content: [{ type: "text", text: formattedReview }],
1411
+ };
1412
+ }
1128
1413
  case "estimate_cost": {
1129
1414
  const args = request.params.arguments;
1130
1415
  const { storyId, constraints } = args;
1131
1416
  let { provider } = args;
1132
1417
  if (!storyId)
1133
1418
  throw new Error("storyId is required");
1419
+ assertUUID(storyId, "storyId");
1134
1420
  // Default provider from infrastructure profile if not explicitly provided
1135
1421
  if (!provider) {
1136
1422
  const costConfig = await fetchFeatureConfig();
@@ -1200,6 +1486,7 @@ ${(estimate.assumptions || []).map((a) => `- ${a}`).join("\n")}
1200
1486
  const { epicId } = args;
1201
1487
  if (!epicId)
1202
1488
  throw new Error("epicId is required");
1489
+ assertUUID(epicId, "epicId");
1203
1490
  // Fetch all stories and filter by epicId
1204
1491
  const allStories = await fetchStories();
1205
1492
  const epicStories = allStories.filter((s) => s.epicId === epicId);
@@ -1302,11 +1589,20 @@ ${unestimated.length === 0
1302
1589
  if (error.response) {
1303
1590
  console.error("Response data:", error.response.data);
1304
1591
  }
1592
+ let errorText = `Error: ${error.message}`;
1593
+ if (error.response?.status) {
1594
+ errorText += ` (HTTP ${error.response.status})`;
1595
+ }
1596
+ if (error.response?.data) {
1597
+ const data = error.response.data;
1598
+ const detail = typeof data === "string" ? data : JSON.stringify(data);
1599
+ errorText += `\nDetails: ${detail}`;
1600
+ }
1305
1601
  return {
1306
1602
  content: [
1307
1603
  {
1308
1604
  type: "text",
1309
- text: `Error: ${error.message}`,
1605
+ text: errorText,
1310
1606
  },
1311
1607
  ],
1312
1608
  isError: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elixium.ai/mcp-server",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "MCP Server for Elixium.ai",
6
6
  "publishConfig": {