@elixium.ai/mcp-server 0.2.0 → 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.
Files changed (2) hide show
  1. package/dist/index.js +143 -9
  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) => {
@@ -283,7 +291,7 @@ const normalizeLane = async (lane) => {
283
291
  const map = style === "upper" ? LANE_UPPER : LANE_TITLE;
284
292
  return map[key] || (style === "upper" ? lane.trim().toUpperCase() : lane.trim());
285
293
  };
286
- const buildIterationContext = (stories, user = null) => {
294
+ const buildIterationContext = (stories, user = null, infraProfile) => {
287
295
  const currentIteration = stories.filter((story) => normalizeLaneForComparison(story?.lane) === "current");
288
296
  const backlog = stories.filter((story) => normalizeLaneForComparison(story?.lane) === "backlog");
289
297
  // Cost summary across current iteration
@@ -294,7 +302,13 @@ const buildIterationContext = (stories, user = null) => {
294
302
  estimatedCount: withEstimates.length,
295
303
  unestimatedCount: iterationStories.length - withEstimates.length,
296
304
  };
297
- return { currentIteration, backlog, user, costSummary };
305
+ return {
306
+ currentIteration,
307
+ backlog,
308
+ user,
309
+ costSummary,
310
+ ...(infraProfile ? { infrastructureProfile: infraProfile } : {}),
311
+ };
298
312
  };
299
313
  const createServer = () => {
300
314
  const server = new Server({
@@ -520,7 +534,15 @@ const createServer = () => {
520
534
  description: "Optional constraints (e.g., budget limit, region, instance types)",
521
535
  },
522
536
  },
523
- required: ["storyId", "provider"],
537
+ required: ["storyId"],
538
+ },
539
+ },
540
+ {
541
+ name: "get_infrastructure_profile",
542
+ description: "Get the workspace infrastructure profile. Returns cloud provider, regions, compliance frameworks, existing services, and constraints. Use this to understand the team's environment before making infrastructure recommendations.",
543
+ inputSchema: {
544
+ type: "object",
545
+ properties: {},
524
546
  },
525
547
  },
526
548
  {
@@ -590,6 +612,20 @@ const createServer = () => {
590
612
  required: ["storyId", "testPlan"],
591
613
  },
592
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
+ },
593
629
  {
594
630
  name: "submit_for_review",
595
631
  description: "Submit implementation for human review. Only works if tests are approved. Sets state to finished.",
@@ -727,6 +763,13 @@ ${config.branchingDefaults ? `- Trunk-Based: ${config.branchingDefaults.trunkBas
727
763
  - Auto-Merge: ${config.branchingDefaults.autoMerge ? "Yes" : "No"}
728
764
  - Source: ${config.branchingDefaults.source}` : "Not configured (branch-based default)"}
729
765
 
766
+ ## Infrastructure Profile
767
+ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureProfile.provider.toUpperCase()}
768
+ - Regions: ${config.infrastructureProfile.regions?.join(", ") || "Not set"}
769
+ - Compliance: ${config.infrastructureProfile.complianceFrameworks?.join(", ").toUpperCase() || "None"}
770
+ - Existing Services: ${config.infrastructureProfile.existingServices?.length || 0}
771
+ > Use \`get_infrastructure_profile\` for full details.` : "Not configured. Set up in Settings → Infrastructure."}
772
+
730
773
  > **Tip:** Features can be configured at workspace or board level. Board settings override workspace defaults.
731
774
  `;
732
775
  return {
@@ -782,9 +825,10 @@ ${config.branchingDefaults ? `- Trunk-Based: ${config.branchingDefaults.trunkBas
782
825
  const hasContextData = Array.isArray(currentIteration) &&
783
826
  Array.isArray(backlog) &&
784
827
  (currentIteration.length > 0 || backlog.length > 0);
828
+ const iterConfig = await fetchFeatureConfig();
785
829
  const context = hasContextData
786
- ? contextData
787
- : buildIterationContext(await fetchStories(), contextData?.user ?? null);
830
+ ? { ...contextData, ...(iterConfig.infrastructureProfile ? { infrastructureProfile: iterConfig.infrastructureProfile } : {}) }
831
+ : buildIterationContext(await fetchStories(), contextData?.user ?? null, iterConfig.infrastructureProfile);
788
832
  return {
789
833
  content: [
790
834
  { type: "text", text: JSON.stringify(context, null, 2) },
@@ -814,6 +858,7 @@ ${config.branchingDefaults ? `- Trunk-Based: ${config.branchingDefaults.trunkBas
814
858
  case "record_learning": {
815
859
  const args = request.params.arguments;
816
860
  const { storyId, outcome_summary } = args;
861
+ assertUUID(storyId, "storyId");
817
862
  const response = await client.patch(`/stories/${storyId}`, {
818
863
  outcome_summary,
819
864
  });
@@ -829,6 +874,7 @@ ${config.branchingDefaults ? `- Trunk-Based: ${config.branchingDefaults.trunkBas
829
874
  if (!storyId) {
830
875
  throw new Error("storyId is required");
831
876
  }
877
+ assertUUID(storyId, "storyId");
832
878
  // Guardrail: Block AI from setting accepted/rejected states (human-in-the-loop)
833
879
  const blockedStates = ["accepted", "rejected"];
834
880
  if (state && blockedStates.includes(state.toLowerCase())) {
@@ -882,6 +928,7 @@ ${config.branchingDefaults ? `- Trunk-Based: ${config.branchingDefaults.trunkBas
882
928
  if (!epicId) {
883
929
  throw new Error("epicId is required");
884
930
  }
931
+ assertUUID(epicId, "epicId");
885
932
  const payload = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined));
886
933
  const response = await client.patch(`/epics/${epicId}`, payload);
887
934
  return {
@@ -893,6 +940,7 @@ ${config.branchingDefaults ? `- Trunk-Based: ${config.branchingDefaults.trunkBas
893
940
  case "prepare_implementation": {
894
941
  const args = request.params.arguments;
895
942
  const { storyId } = args;
943
+ assertUUID(storyId, "storyId");
896
944
  const storyResponse = await client.get(`/stories/${storyId}`);
897
945
  const story = storyResponse.data;
898
946
  // Validation & Guardrails
@@ -937,6 +985,7 @@ Here’s the smallest change that will validate it:
937
985
  if (!storyId) {
938
986
  throw new Error("storyId is required");
939
987
  }
988
+ assertUUID(storyId, "storyId");
940
989
  const response = await client.post(`/stories/${storyId}/start`, {
941
990
  branchPrefix: branchPrefix || "feat",
942
991
  trunkBased,
@@ -1019,6 +1068,7 @@ ${result.workflow_reminder}
1019
1068
  if (!storyId) {
1020
1069
  throw new Error("storyId is required");
1021
1070
  }
1071
+ assertUUID(storyId, "storyId");
1022
1072
  if (!testPlan) {
1023
1073
  throw new Error("testPlan is required");
1024
1074
  }
@@ -1040,6 +1090,30 @@ ${(result.test_file_paths || []).map((p) => `- \`${p}\``).join("\n") || "No test
1040
1090
  ${result.message}
1041
1091
 
1042
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.
1043
1117
  `;
1044
1118
  return {
1045
1119
  content: [{ type: "text", text: formattedResult.trim() }],
@@ -1051,6 +1125,7 @@ ${result.message}
1051
1125
  if (!storyId) {
1052
1126
  throw new Error("storyId is required");
1053
1127
  }
1128
+ assertUUID(storyId, "storyId");
1054
1129
  const response = await client.post(`/stories/${storyId}/submit-review`, {
1055
1130
  commitHash,
1056
1131
  testResults,
@@ -1105,11 +1180,23 @@ ${result.next_step}
1105
1180
  }
1106
1181
  case "estimate_cost": {
1107
1182
  const args = request.params.arguments;
1108
- const { storyId, provider, constraints } = args;
1183
+ const { storyId, constraints } = args;
1184
+ let { provider } = args;
1109
1185
  if (!storyId)
1110
1186
  throw new Error("storyId is required");
1187
+ assertUUID(storyId, "storyId");
1188
+ // Default provider from infrastructure profile if not explicitly provided
1189
+ if (!provider) {
1190
+ const costConfig = await fetchFeatureConfig();
1191
+ const profileProvider = costConfig.infrastructureProfile?.provider;
1192
+ if (profileProvider) {
1193
+ // Map gov variants to commercial for cost estimation API
1194
+ const govMap = { "aws-gov": "aws", "azure-gov": "azure" };
1195
+ provider = govMap[profileProvider] || profileProvider;
1196
+ }
1197
+ }
1111
1198
  if (!provider)
1112
- throw new Error("provider is required");
1199
+ throw new Error("provider is required (set it explicitly or configure an infrastructure profile)");
1113
1200
  if (!["gcp", "aws", "azure", "self-hosted"].includes(provider)) {
1114
1201
  throw new Error("provider must be one of: gcp, aws, azure, self-hosted");
1115
1202
  }
@@ -1119,12 +1206,14 @@ ${result.next_step}
1119
1206
  if (!storyData.description) {
1120
1207
  throw new Error("Story needs a description before cost estimation");
1121
1208
  }
1122
- // Call the AI estimate-cost endpoint
1209
+ // Call the AI estimate-cost endpoint (with epicId/boardId for infra context)
1123
1210
  const estimateRes = await client.post("/ai/estimate-cost", {
1124
1211
  storyTitle: storyData.title,
1125
1212
  description: storyData.description,
1126
1213
  provider,
1127
1214
  ...(constraints ? { constraints } : {}),
1215
+ ...(storyData.epicId ? { epicId: storyData.epicId } : {}),
1216
+ ...(storyData.boardId ? { boardId: storyData.boardId } : {}),
1128
1217
  });
1129
1218
  const estimate = estimateRes.data;
1130
1219
  // Save the estimate to the story
@@ -1165,6 +1254,7 @@ ${(estimate.assumptions || []).map((a) => `- ${a}`).join("\n")}
1165
1254
  const { epicId } = args;
1166
1255
  if (!epicId)
1167
1256
  throw new Error("epicId is required");
1257
+ assertUUID(epicId, "epicId");
1168
1258
  // Fetch all stories and filter by epicId
1169
1259
  const allStories = await fetchStories();
1170
1260
  const epicStories = allStories.filter((s) => s.epicId === epicId);
@@ -1223,6 +1313,41 @@ ${unestimated.length === 0
1223
1313
  content: [{ type: "text", text: formattedResult.trim() }],
1224
1314
  };
1225
1315
  }
1316
+ case "get_infrastructure_profile": {
1317
+ const profileConfig = await fetchFeatureConfig();
1318
+ const profile = profileConfig.infrastructureProfile;
1319
+ if (!profile || !profile.provider) {
1320
+ return {
1321
+ content: [{
1322
+ type: "text",
1323
+ text: "# Infrastructure Profile\n\nNo infrastructure profile configured. Ask the workspace admin to set one up in **Settings → Infrastructure**.\n\nThis profile tells the AI about your cloud environment — provider, regions, compliance frameworks, and existing services — so cost estimates and recommendations are accurate.",
1324
+ }],
1325
+ };
1326
+ }
1327
+ const sections = ["# Workspace Infrastructure Profile\n"];
1328
+ sections.push(`**Cloud Provider:** ${profile.provider.toUpperCase()}`);
1329
+ if (profile.regions?.length) {
1330
+ sections.push(`**Regions:** ${profile.regions.join(", ")}`);
1331
+ }
1332
+ if (profile.complianceFrameworks?.length) {
1333
+ sections.push(`\n## Compliance Frameworks\n${profile.complianceFrameworks.map((f) => `- ${f.toUpperCase()}`).join("\n")}`);
1334
+ }
1335
+ if (profile.existingServices?.length) {
1336
+ sections.push(`\n## Existing Services\n| Service | Category | Specs |\n|---------|----------|-------|\n${profile.existingServices.map((s) => `| ${s.name} | ${s.category} | ${s.specs || "-"} |`).join("\n")}`);
1337
+ }
1338
+ if (profile.networkTopology) {
1339
+ sections.push(`\n## Network Topology\n${profile.networkTopology}`);
1340
+ }
1341
+ if (profile.additionalConstraints) {
1342
+ sections.push(`\n## Additional Constraints\n${profile.additionalConstraints}`);
1343
+ }
1344
+ if (profile.updatedAt) {
1345
+ sections.push(`\n---\n*Last updated: ${new Date(profile.updatedAt).toLocaleDateString()}*`);
1346
+ }
1347
+ return {
1348
+ content: [{ type: "text", text: sections.join("\n") }],
1349
+ };
1350
+ }
1226
1351
  default:
1227
1352
  throw new Error("Unknown tool");
1228
1353
  }
@@ -1232,11 +1357,20 @@ ${unestimated.length === 0
1232
1357
  if (error.response) {
1233
1358
  console.error("Response data:", error.response.data);
1234
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
+ }
1235
1369
  return {
1236
1370
  content: [
1237
1371
  {
1238
1372
  type: "text",
1239
- text: `Error: ${error.message}`,
1373
+ text: errorText,
1240
1374
  },
1241
1375
  ],
1242
1376
  isError: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elixium.ai/mcp-server",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "MCP Server for Elixium.ai",
6
6
  "publishConfig": {