@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.
- package/dist/index.js +143 -9
- 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 {
|
|
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"
|
|
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,
|
|
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:
|
|
1373
|
+
text: errorText,
|
|
1240
1374
|
},
|
|
1241
1375
|
],
|
|
1242
1376
|
isError: true,
|