@elixium.ai/mcp-server 0.2.2 → 0.3.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.
Files changed (2) hide show
  1. package/dist/index.js +249 -17
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -227,6 +227,105 @@ const getTeamProfile = async () => {
227
227
  const config = await fetchFeatureConfig();
228
228
  return config.teamProfile;
229
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
+ };
230
329
  const fetchStories = async () => {
231
330
  const boardId = await resolveBoardId();
232
331
  const slug = normalizeBoardSlug(BOARD_SLUG);
@@ -652,6 +751,20 @@ const createServer = () => {
652
751
  required: ["storyId"],
653
752
  },
654
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
+ },
655
768
  ] : [];
656
769
  // Learning Loop tools (conditional on feature flag)
657
770
  const learningLoopTools = learningLoopEnabled ? [
@@ -696,7 +809,7 @@ const createServer = () => {
696
809
  try {
697
810
  const toolName = request.params.name;
698
811
  // Check TDD workflow tools
699
- const tddWorkflowTools = ["start_story", "propose_test_plan", "submit_for_review"];
812
+ const tddWorkflowTools = ["start_story", "propose_test_plan", "submit_for_review", "review_pr"];
700
813
  if (tddWorkflowTools.includes(toolName)) {
701
814
  const enabled = await isTddWorkflowEnabled();
702
815
  if (!enabled) {
@@ -886,11 +999,38 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
886
999
  ...(state ? { state } : {}),
887
1000
  ...(normalizedLane ? { lane: normalizedLane } : {}),
888
1001
  }).filter(([, value]) => value !== undefined));
889
- 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
+ }
890
1032
  return {
891
- content: [
892
- { type: "text", text: JSON.stringify(response.data, null, 2) },
893
- ],
1033
+ content: [{ type: "text", text: resultText }],
894
1034
  };
895
1035
  }
896
1036
  case "list_objectives": {
@@ -941,7 +1081,12 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
941
1081
  const args = request.params.arguments;
942
1082
  const { storyId } = args;
943
1083
  assertUUID(storyId, "storyId");
944
- const storyResponse = await client.get(`/stories/${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
+ ]);
945
1090
  const story = storyResponse.data;
946
1091
  // Validation & Guardrails
947
1092
  const storyLane = typeof story.lane === "string" ? story.lane.trim().toLowerCase() : "";
@@ -957,11 +1102,23 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
957
1102
  story.learningGoals ||
958
1103
  story.hypothesis ||
959
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
+ }
960
1117
  const formattedBrief = `
961
1118
  # Implementation Brief: ${story.title}
962
1119
 
963
1120
  ${statusWarning}
964
-
1121
+ ${revisionFeedbackSection}${aiReviewSection}
965
1122
  ## Acceptance Criteria
966
1123
  Here’s the acceptance criteria I’m going to satisfy:
967
1124
  ${acceptanceCriteria}
@@ -970,6 +1127,8 @@ ${acceptanceCriteria}
970
1127
  Here are the assumptions I think we’re testing:
971
1128
  ${assumptions}
972
1129
 
1130
+ ${formatTeamContext(teamConfig)}
1131
+ ${formatDorDodSection(boardSettings, story.dorChecklist, story.dodChecklist)}
973
1132
  ## Proposal
974
1133
  Here’s the smallest change that will validate it:
975
1134
  [Agent should fill this in based on the context above]
@@ -986,12 +1145,19 @@ Here’s the smallest change that will validate it:
986
1145
  throw new Error("storyId is required");
987
1146
  }
988
1147
  assertUUID(storyId, "storyId");
989
- const response = await client.post(`/stories/${storyId}/start`, {
990
- branchPrefix: branchPrefix || "feat",
991
- trunkBased,
992
- autoMerge,
993
- });
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
+ ]);
994
1158
  const result = response.data;
1159
+ const teamContext = formatTeamContext(teamConfig);
1160
+ const dorWarnings = formatDorWarnings(boardSettings, result.dorChecklist);
995
1161
  const isTrunk = result.trunkBased;
996
1162
  const isAutoMerge = result.autoMerge;
997
1163
  let formattedResult;
@@ -1016,7 +1182,9 @@ ${result.acceptance_criteria || "No specific AC provided."}
1016
1182
 
1017
1183
  > **Feature Flag:** Wrap new behavior with \`${result.featureFlagName}\` for safe isolation.
1018
1184
  > **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
1019
- `;
1185
+
1186
+ ${teamContext}
1187
+ ${dorWarnings}`;
1020
1188
  }
1021
1189
  else if (isTrunk && isAutoMerge) {
1022
1190
  formattedResult = `
@@ -1040,7 +1208,9 @@ ${result.acceptance_criteria || "No specific AC provided."}
1040
1208
 
1041
1209
  > **Feature Flag:** Wrap new behavior with \`${result.featureFlagName}\` for safe isolation.
1042
1210
  > **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
1043
- `;
1211
+
1212
+ ${teamContext}
1213
+ ${dorWarnings}`;
1044
1214
  }
1045
1215
  else {
1046
1216
  formattedResult = `
@@ -1056,7 +1226,9 @@ ${result.acceptance_criteria || "No specific AC provided."}
1056
1226
  ${result.workflow_reminder}
1057
1227
 
1058
1228
  > **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
1059
- `;
1229
+
1230
+ ${teamContext}
1231
+ ${dorWarnings}`;
1060
1232
  }
1061
1233
  return {
1062
1234
  content: [{ type: "text", text: formattedResult.trim() }],
@@ -1072,17 +1244,29 @@ ${result.workflow_reminder}
1072
1244
  if (!testPlan) {
1073
1245
  throw new Error("testPlan is required");
1074
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
+ }
1075
1256
  const response = await client.post(`/stories/${storyId}/propose-tests`, {
1076
1257
  testPlan,
1077
1258
  testFilePaths,
1078
1259
  });
1079
1260
  const result = response.data;
1261
+ const feedbackSection = previousFeedback
1262
+ ? `\n## Previous Revision Feedback\n> ${previousFeedback.split("\n").join("\n> ")}\n`
1263
+ : "";
1080
1264
  const formattedResult = `
1081
1265
  # Test Plan Proposed
1082
1266
 
1083
1267
  **Story ID:** ${storyId}
1084
1268
  **Workflow Stage:** ${result.workflow_stage}
1085
-
1269
+ ${feedbackSection}
1086
1270
  ## Test Files
1087
1271
  ${(result.test_file_paths || []).map((p) => `- \`${p}\``).join("\n") || "No test files specified"}
1088
1272
 
@@ -1151,6 +1335,14 @@ git branch -d ${instr.sourceBranch}
1151
1335
  \`\`\`
1152
1336
  `;
1153
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
+ }
1154
1346
  const formattedResult = `
1155
1347
  # Implementation Submitted for Review
1156
1348
 
@@ -1166,7 +1358,7 @@ ${result.featureFlagName ? `**Feature Flag:** \`${result.featureFlagName}\`` : "
1166
1358
 
1167
1359
  ## Commits
1168
1360
  ${(result.commit_hashes || []).map((h) => `- \`${h}\``).join("\n") || "No commits recorded"}
1169
- ${autoMergeSection}
1361
+ ${autoMergeSection}${prSection}
1170
1362
  ## Next Step
1171
1363
  ${result.next_step}
1172
1364
 
@@ -1178,6 +1370,46 @@ ${result.next_step}
1178
1370
  content: [{ type: "text", text: formattedResult.trim() }],
1179
1371
  };
1180
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
+ }
1181
1413
  case "estimate_cost": {
1182
1414
  const args = request.params.arguments;
1183
1415
  const { storyId, constraints } = args;
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@elixium.ai/mcp-server",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "MCP Server for Elixium.ai",
6
+ "mcpName": "io.github.elixium-ai/mcp-server",
6
7
  "publishConfig": {
7
8
  "access": "public"
8
9
  },