@elixium.ai/mcp-server 0.3.2 → 0.3.6
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 +517 -7
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -200,12 +200,16 @@ const fetchFeatureConfig = async () => {
|
|
|
200
200
|
learningLoop: true,
|
|
201
201
|
tddWorkflow: true,
|
|
202
202
|
aiTools: true,
|
|
203
|
+
teamDecisions: false,
|
|
204
|
+
ragKnowledgeBase: false,
|
|
203
205
|
},
|
|
204
206
|
source: {
|
|
205
207
|
balancedTeam: "error-fallback",
|
|
206
208
|
learningLoop: "error-fallback",
|
|
207
209
|
tddWorkflow: "error-fallback",
|
|
208
210
|
aiTools: "error-fallback",
|
|
211
|
+
teamDecisions: "error-fallback",
|
|
212
|
+
ragKnowledgeBase: "error-fallback",
|
|
209
213
|
},
|
|
210
214
|
};
|
|
211
215
|
}
|
|
@@ -219,6 +223,10 @@ const isLearningLoopEnabled = async () => {
|
|
|
219
223
|
const config = await fetchFeatureConfig();
|
|
220
224
|
return config.features.learningLoop;
|
|
221
225
|
};
|
|
226
|
+
const isTeamDecisionsEnabled = async () => {
|
|
227
|
+
const config = await fetchFeatureConfig();
|
|
228
|
+
return config.features.teamDecisions;
|
|
229
|
+
};
|
|
222
230
|
const isAiToolsEnabled = async () => {
|
|
223
231
|
const config = await fetchFeatureConfig();
|
|
224
232
|
return config.features.aiTools;
|
|
@@ -423,6 +431,8 @@ const createServer = () => {
|
|
|
423
431
|
const featureConfig = await fetchFeatureConfig();
|
|
424
432
|
const tddEnabled = featureConfig.features.tddWorkflow;
|
|
425
433
|
const learningLoopEnabled = featureConfig.features.learningLoop;
|
|
434
|
+
const teamDecisionsEnabled = featureConfig.features.teamDecisions;
|
|
435
|
+
const ragKnowledgeBaseEnabled = featureConfig.features.ragKnowledgeBase;
|
|
426
436
|
const baseTools = [
|
|
427
437
|
{
|
|
428
438
|
name: "get_feature_config",
|
|
@@ -434,10 +444,34 @@ const createServer = () => {
|
|
|
434
444
|
},
|
|
435
445
|
{
|
|
436
446
|
name: "list_stories",
|
|
437
|
-
description: "List
|
|
447
|
+
description: "List stories on the Elixium board. Returns compact summaries (id, title, lane, points, owners, epic, state). Use lane filter to reduce results. Defaults to 25 stories max.",
|
|
438
448
|
inputSchema: {
|
|
439
449
|
type: "object",
|
|
440
|
-
properties: {
|
|
450
|
+
properties: {
|
|
451
|
+
lane: {
|
|
452
|
+
type: "string",
|
|
453
|
+
description: "Filter by lane (case-insensitive). Omit to list all lanes.",
|
|
454
|
+
enum: ["Current", "Backlog", "Icebox", "Done"],
|
|
455
|
+
},
|
|
456
|
+
limit: {
|
|
457
|
+
type: "number",
|
|
458
|
+
description: "Max stories to return (default 25, max 100)",
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
name: "get_story",
|
|
465
|
+
description: "Get full details for a single story by UUID. Returns all fields including description, acceptance criteria, tasks, labels, metrics, and workflow state.",
|
|
466
|
+
inputSchema: {
|
|
467
|
+
type: "object",
|
|
468
|
+
properties: {
|
|
469
|
+
id: {
|
|
470
|
+
type: "string",
|
|
471
|
+
description: "The full UUID of the story (from list_stories output)",
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
required: ["id"],
|
|
441
475
|
},
|
|
442
476
|
},
|
|
443
477
|
{
|
|
@@ -477,6 +511,14 @@ const createServer = () => {
|
|
|
477
511
|
type: "string",
|
|
478
512
|
description: "Email of the person requesting this story. Defaults to ELIXIUM_USER_EMAIL env var or API key owner.",
|
|
479
513
|
},
|
|
514
|
+
epicId: {
|
|
515
|
+
type: "string",
|
|
516
|
+
description: "Epic ID to link this story to (optional). Use list_epics to find epic IDs.",
|
|
517
|
+
},
|
|
518
|
+
testPlan: {
|
|
519
|
+
type: "string",
|
|
520
|
+
description: "Markdown test plan describing test strategy (optional). Sets the test_plan field without changing workflow_stage.",
|
|
521
|
+
},
|
|
480
522
|
},
|
|
481
523
|
required: ["title"],
|
|
482
524
|
},
|
|
@@ -595,6 +637,18 @@ const createServer = () => {
|
|
|
595
637
|
type: "string",
|
|
596
638
|
description: "Acceptance criteria in Given/When/Then format",
|
|
597
639
|
},
|
|
640
|
+
sortOrder: {
|
|
641
|
+
type: "number",
|
|
642
|
+
description: "Sort order within the lane (lower = higher priority)",
|
|
643
|
+
},
|
|
644
|
+
epicId: {
|
|
645
|
+
type: "string",
|
|
646
|
+
description: "Epic ID to link this story to. Set to empty string to unlink.",
|
|
647
|
+
},
|
|
648
|
+
testPlan: {
|
|
649
|
+
type: "string",
|
|
650
|
+
description: "Markdown test plan. Updates test_plan field without changing workflow_stage.",
|
|
651
|
+
},
|
|
598
652
|
},
|
|
599
653
|
required: ["storyId"],
|
|
600
654
|
},
|
|
@@ -658,6 +712,25 @@ const createServer = () => {
|
|
|
658
712
|
required: ["epicId"],
|
|
659
713
|
},
|
|
660
714
|
},
|
|
715
|
+
{
|
|
716
|
+
name: "prioritize_epic",
|
|
717
|
+
description: "AI-powered dependency analysis and story prioritization for an epic. Analyzes stories, proposes optimal execution order based on dependencies, and optionally applies the reorder. Uses the same server-side endpoint as the UI.",
|
|
718
|
+
inputSchema: {
|
|
719
|
+
type: "object",
|
|
720
|
+
properties: {
|
|
721
|
+
epicId: {
|
|
722
|
+
type: "string",
|
|
723
|
+
description: "ID of the epic to prioritize",
|
|
724
|
+
},
|
|
725
|
+
mode: {
|
|
726
|
+
type: "string",
|
|
727
|
+
description: "analyze (preview proposed order) or apply (commit the reorder)",
|
|
728
|
+
enum: ["analyze", "apply"],
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
required: ["epicId"],
|
|
732
|
+
},
|
|
733
|
+
},
|
|
661
734
|
];
|
|
662
735
|
const tddTools = tddEnabled ? [
|
|
663
736
|
// TDD Workflow Tools
|
|
@@ -725,6 +798,20 @@ const createServer = () => {
|
|
|
725
798
|
required: ["storyId"],
|
|
726
799
|
},
|
|
727
800
|
},
|
|
801
|
+
{
|
|
802
|
+
name: "get_test_plan",
|
|
803
|
+
description: "Retrieve and display the full TDD test plan for a story. Shows the plan text, workflow stage, approval status, and test file paths.",
|
|
804
|
+
inputSchema: {
|
|
805
|
+
type: "object",
|
|
806
|
+
properties: {
|
|
807
|
+
storyId: {
|
|
808
|
+
type: "string",
|
|
809
|
+
description: "ID of the story whose test plan to display",
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
required: ["storyId"],
|
|
813
|
+
},
|
|
814
|
+
},
|
|
728
815
|
{
|
|
729
816
|
name: "submit_for_review",
|
|
730
817
|
description: "Submit implementation for human review. Only works if tests are approved. Sets state to finished.",
|
|
@@ -802,14 +889,92 @@ const createServer = () => {
|
|
|
802
889
|
},
|
|
803
890
|
},
|
|
804
891
|
] : [];
|
|
805
|
-
|
|
892
|
+
// Team Decisions tools (conditional on feature flag)
|
|
893
|
+
const teamDecisionsTools = teamDecisionsEnabled ? [
|
|
894
|
+
{
|
|
895
|
+
name: "record_decision",
|
|
896
|
+
description: "Record a team decision, meeting outcome, or architectural choice. These are shared across all team members' AI sessions.",
|
|
897
|
+
inputSchema: {
|
|
898
|
+
type: "object",
|
|
899
|
+
properties: {
|
|
900
|
+
title: { type: "string", description: "Short title for the decision" },
|
|
901
|
+
content: { type: "string", description: "Full description of the decision, context, and rationale" },
|
|
902
|
+
category: {
|
|
903
|
+
type: "string",
|
|
904
|
+
description: "Category (e.g., architecture, meeting-note, convention, tooling, decision, general)",
|
|
905
|
+
},
|
|
906
|
+
tags: {
|
|
907
|
+
type: "array",
|
|
908
|
+
items: { type: "string" },
|
|
909
|
+
description: "Tags for filtering (e.g., ['auth', 'self-hosted', 'phase-2'])",
|
|
910
|
+
},
|
|
911
|
+
storyId: { type: "string", description: "Optional story UUID to link this decision to" },
|
|
912
|
+
epicId: { type: "string", description: "Optional epic UUID to link this decision to" },
|
|
913
|
+
},
|
|
914
|
+
required: ["title", "content"],
|
|
915
|
+
},
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
name: "list_decisions",
|
|
919
|
+
description: "List recent team decisions. Filter by category, tag, or date. Use this to check what the team has decided recently.",
|
|
920
|
+
inputSchema: {
|
|
921
|
+
type: "object",
|
|
922
|
+
properties: {
|
|
923
|
+
category: { type: "string", description: "Filter by category" },
|
|
924
|
+
tag: { type: "string", description: "Filter by tag" },
|
|
925
|
+
since: { type: "string", description: "ISO date string — only return decisions after this date" },
|
|
926
|
+
limit: { type: "number", description: "Max results to return (default 20)" },
|
|
927
|
+
},
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
name: "search_decisions",
|
|
932
|
+
description: "Full-text search across all team decisions. Use this when looking for decisions related to a specific topic or keyword.",
|
|
933
|
+
inputSchema: {
|
|
934
|
+
type: "object",
|
|
935
|
+
properties: {
|
|
936
|
+
query: { type: "string", description: "Search query (keywords)" },
|
|
937
|
+
category: { type: "string", description: "Optional category filter" },
|
|
938
|
+
limit: { type: "number", description: "Max results to return (default 10)" },
|
|
939
|
+
},
|
|
940
|
+
required: ["query"],
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
] : [];
|
|
944
|
+
// RAG Knowledge Base tools (conditional on feature flag)
|
|
945
|
+
const ragTools = ragKnowledgeBaseEnabled ? [
|
|
946
|
+
{
|
|
947
|
+
name: "search_knowledge",
|
|
948
|
+
description: "Semantic search across team knowledge base (documents, wikis, runbooks, etc). Returns relevant chunks with source attribution.",
|
|
949
|
+
inputSchema: {
|
|
950
|
+
type: "object",
|
|
951
|
+
properties: {
|
|
952
|
+
query: { type: "string", description: "Search query (natural language)" },
|
|
953
|
+
category: { type: "string", description: "Optional source type filter (e.g., wiki, runbook, adr, confluence)" },
|
|
954
|
+
limit: { type: "number", description: "Max results to return (default 10)" },
|
|
955
|
+
},
|
|
956
|
+
required: ["query"],
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
name: "list_knowledge_sources",
|
|
961
|
+
description: "List all knowledge sources indexed in the RAG knowledge base, grouped by source type.",
|
|
962
|
+
inputSchema: {
|
|
963
|
+
type: "object",
|
|
964
|
+
properties: {
|
|
965
|
+
source_type: { type: "string", description: "Optional filter by source type" },
|
|
966
|
+
},
|
|
967
|
+
},
|
|
968
|
+
},
|
|
969
|
+
] : [];
|
|
970
|
+
return { tools: [...baseTools, ...tddTools, ...learningLoopTools, ...teamDecisionsTools, ...ragTools] };
|
|
806
971
|
});
|
|
807
972
|
// Handle Requests
|
|
808
973
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
809
974
|
try {
|
|
810
975
|
const toolName = request.params.name;
|
|
811
976
|
// Check TDD workflow tools
|
|
812
|
-
const tddWorkflowTools = ["start_story", "propose_test_plan", "submit_for_review", "review_pr"];
|
|
977
|
+
const tddWorkflowTools = ["start_story", "propose_test_plan", "get_test_plan", "submit_for_review", "review_pr"];
|
|
813
978
|
if (tddWorkflowTools.includes(toolName)) {
|
|
814
979
|
const enabled = await isTddWorkflowEnabled();
|
|
815
980
|
if (!enabled) {
|
|
@@ -844,6 +1009,42 @@ const createServer = () => {
|
|
|
844
1009
|
};
|
|
845
1010
|
}
|
|
846
1011
|
}
|
|
1012
|
+
// Check Team Decisions tools
|
|
1013
|
+
const teamDecisionsToolNames = ["record_decision", "list_decisions", "search_decisions"];
|
|
1014
|
+
if (teamDecisionsToolNames.includes(toolName)) {
|
|
1015
|
+
const enabled = await isTeamDecisionsEnabled();
|
|
1016
|
+
if (!enabled) {
|
|
1017
|
+
return {
|
|
1018
|
+
content: [{
|
|
1019
|
+
type: "text",
|
|
1020
|
+
text: JSON.stringify({
|
|
1021
|
+
error: "Team Decisions is disabled",
|
|
1022
|
+
message: "The Team Decisions feature is currently disabled for this board. This feature enables shared institutional memory across all team members' AI sessions.",
|
|
1023
|
+
help: "Team Decisions can be enabled in workspace settings or per-board settings."
|
|
1024
|
+
}, null, 2)
|
|
1025
|
+
}],
|
|
1026
|
+
isError: true,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
// Check RAG Knowledge Base tools
|
|
1031
|
+
const ragToolNames = ["search_knowledge", "list_knowledge_sources"];
|
|
1032
|
+
if (ragToolNames.includes(toolName)) {
|
|
1033
|
+
const config = await fetchFeatureConfig();
|
|
1034
|
+
if (!config.features.ragKnowledgeBase) {
|
|
1035
|
+
return {
|
|
1036
|
+
content: [{
|
|
1037
|
+
type: "text",
|
|
1038
|
+
text: JSON.stringify({
|
|
1039
|
+
error: "RAG Knowledge Base is disabled",
|
|
1040
|
+
message: "The RAG Knowledge Base feature is currently disabled for this board. This feature enables semantic search across team knowledge sources.",
|
|
1041
|
+
help: "RAG Knowledge Base can be enabled in workspace settings or per-board settings."
|
|
1042
|
+
}, null, 2)
|
|
1043
|
+
}],
|
|
1044
|
+
isError: true,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
847
1048
|
switch (toolName) {
|
|
848
1049
|
case "get_feature_config": {
|
|
849
1050
|
const config = await fetchFeatureConfig();
|
|
@@ -855,6 +1056,8 @@ ${config.features.balancedTeam ? "✅" : "❌"} **Balanced Team** - Design URLs,
|
|
|
855
1056
|
${config.features.learningLoop ? "✅" : "❌"} **Learning Loop** - Hypothesis tracking, risk profiles, confidence scores
|
|
856
1057
|
${config.features.tddWorkflow ? "✅" : "❌"} **TDD Workflow** - Test-driven development enforcement
|
|
857
1058
|
${config.features.aiTools ? "✅" : "❌"} **AI Tools** - AI-powered suggestions and analysis
|
|
1059
|
+
${config.features.teamDecisions ? "✅" : "❌"} **Team Decisions** - Shared institutional memory across AI sessions
|
|
1060
|
+
${config.features.ragKnowledgeBase ? "✅" : "❌"} **RAG Knowledge Base** - Semantic search across team knowledge sources
|
|
858
1061
|
|
|
859
1062
|
## Team Profile
|
|
860
1063
|
${config.teamProfile ? `
|
|
@@ -870,6 +1073,8 @@ ${config.teamProfile ? `
|
|
|
870
1073
|
- Learning Loop: ${config.source.learningLoop}
|
|
871
1074
|
- TDD Workflow: ${config.source.tddWorkflow}
|
|
872
1075
|
- AI Tools: ${config.source.aiTools}
|
|
1076
|
+
- Team Decisions: ${config.source.teamDecisions}
|
|
1077
|
+
- RAG Knowledge Base: ${config.source.ragKnowledgeBase}
|
|
873
1078
|
|
|
874
1079
|
## Branching Strategy Defaults
|
|
875
1080
|
${config.branchingDefaults ? `- Trunk-Based: ${config.branchingDefaults.trunkBased ? "Yes" : "No"}
|
|
@@ -890,10 +1095,49 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
890
1095
|
};
|
|
891
1096
|
}
|
|
892
1097
|
case "list_stories": {
|
|
893
|
-
const
|
|
1098
|
+
const args = request.params.arguments;
|
|
1099
|
+
let stories = await fetchStories();
|
|
1100
|
+
// Filter by lane if specified
|
|
1101
|
+
if (args?.lane) {
|
|
1102
|
+
const targetLane = args.lane.toLowerCase();
|
|
1103
|
+
stories = stories.filter((s) => (s.lane || "").toLowerCase() === targetLane);
|
|
1104
|
+
}
|
|
1105
|
+
// Apply limit (default 25, max 100)
|
|
1106
|
+
const limit = Math.min(Math.max(args?.limit || 25, 1), 100);
|
|
1107
|
+
const total = stories.length;
|
|
1108
|
+
stories = stories.slice(0, limit);
|
|
1109
|
+
// Return compact summaries to fit LLM context
|
|
1110
|
+
const compact = stories.map((s) => ({
|
|
1111
|
+
id: s.id,
|
|
1112
|
+
title: s.title,
|
|
1113
|
+
lane: s.lane,
|
|
1114
|
+
state: s.state,
|
|
1115
|
+
points: s.points,
|
|
1116
|
+
owners: s.owners,
|
|
1117
|
+
labels: s.labels,
|
|
1118
|
+
epicId: s.epicId,
|
|
1119
|
+
storyType: s.storyType || s.story_type,
|
|
1120
|
+
createdAt: s.createdAt || s.created_at,
|
|
1121
|
+
}));
|
|
1122
|
+
const result = {
|
|
1123
|
+
total,
|
|
1124
|
+
showing: compact.length,
|
|
1125
|
+
...(args?.lane ? { lane: args.lane } : {}),
|
|
1126
|
+
stories: compact,
|
|
1127
|
+
};
|
|
1128
|
+
return {
|
|
1129
|
+
content: [
|
|
1130
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
1131
|
+
],
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
case "get_story": {
|
|
1135
|
+
const args = request.params.arguments;
|
|
1136
|
+
assertUUID(args.id, "story id");
|
|
1137
|
+
const response = await client.get(`/stories/${args.id}`);
|
|
894
1138
|
return {
|
|
895
1139
|
content: [
|
|
896
|
-
{ type: "text", text: JSON.stringify(
|
|
1140
|
+
{ type: "text", text: JSON.stringify(response.data, null, 2) },
|
|
897
1141
|
],
|
|
898
1142
|
};
|
|
899
1143
|
}
|
|
@@ -1088,6 +1332,32 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
1088
1332
|
fetchBoardSettings(),
|
|
1089
1333
|
]);
|
|
1090
1334
|
const story = storyResponse.data;
|
|
1335
|
+
// Auto-search team decisions if feature flag is enabled (graceful degradation)
|
|
1336
|
+
let relevantDecisions = [];
|
|
1337
|
+
if (teamConfig.features.teamDecisions) {
|
|
1338
|
+
try {
|
|
1339
|
+
const decisionsResponse = await client.get("/decisions/search", {
|
|
1340
|
+
params: { q: story.title, limit: 5 },
|
|
1341
|
+
});
|
|
1342
|
+
relevantDecisions = decisionsResponse.data?.decisions || [];
|
|
1343
|
+
}
|
|
1344
|
+
catch {
|
|
1345
|
+
// Silently omit — decisions search failure should not break the brief
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
// Auto-search knowledge base if feature flag is enabled (graceful degradation)
|
|
1349
|
+
let relatedKnowledge = [];
|
|
1350
|
+
if (teamConfig.features.ragKnowledgeBase) {
|
|
1351
|
+
try {
|
|
1352
|
+
const knowledgeResponse = await client.get("/knowledge/search", {
|
|
1353
|
+
params: { q: story.title, limit: 5 },
|
|
1354
|
+
});
|
|
1355
|
+
relatedKnowledge = knowledgeResponse.data?.results || [];
|
|
1356
|
+
}
|
|
1357
|
+
catch {
|
|
1358
|
+
// Silently omit — knowledge search failure should not break the brief
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1091
1361
|
// Validation & Guardrails
|
|
1092
1362
|
const storyLane = typeof story.lane === "string" ? story.lane.trim().toLowerCase() : "";
|
|
1093
1363
|
const statusWarning = storyLane !== "current"
|
|
@@ -1114,6 +1384,24 @@ ${config.infrastructureProfile?.provider ? `- Provider: ${config.infrastructureP
|
|
|
1114
1384
|
r.concerns.map((c) => `- **${c.severity}:** ${c.description}`).join("\n") + "\n";
|
|
1115
1385
|
}
|
|
1116
1386
|
}
|
|
1387
|
+
// Format relevant team decisions section (only if matches exist)
|
|
1388
|
+
let decisionsSection = "";
|
|
1389
|
+
if (relevantDecisions.length > 0) {
|
|
1390
|
+
const formatted = relevantDecisions.map((d) => {
|
|
1391
|
+
const date = d.created_at ? new Date(d.created_at).toISOString().split("T")[0] : "unknown";
|
|
1392
|
+
return `- [${date}] **${d.title}** (${d.category || "general"})\n ${d.content}`;
|
|
1393
|
+
}).join("\n");
|
|
1394
|
+
decisionsSection = `\n## Relevant Team Decisions\n${formatted}\n`;
|
|
1395
|
+
}
|
|
1396
|
+
// Format related knowledge section (only if matches exist)
|
|
1397
|
+
let knowledgeSection = "";
|
|
1398
|
+
if (relatedKnowledge.length > 0) {
|
|
1399
|
+
const formatted = relatedKnowledge.map((k) => {
|
|
1400
|
+
const relevance = k.relevance ? ` (${(k.relevance * 100).toFixed(0)}% match)` : "";
|
|
1401
|
+
return `- **${k.source_title}** [${k.source_type}]${relevance}\n ${k.content}`;
|
|
1402
|
+
}).join("\n");
|
|
1403
|
+
knowledgeSection = `\n## Related Knowledge\n${formatted}\n`;
|
|
1404
|
+
}
|
|
1117
1405
|
const formattedBrief = `
|
|
1118
1406
|
# Implementation Brief: ${story.title}
|
|
1119
1407
|
|
|
@@ -1126,7 +1414,7 @@ ${acceptanceCriteria}
|
|
|
1126
1414
|
## Assumptions
|
|
1127
1415
|
Here are the assumptions I think we’re testing:
|
|
1128
1416
|
${assumptions}
|
|
1129
|
-
|
|
1417
|
+
${decisionsSection}${knowledgeSection}
|
|
1130
1418
|
${formatTeamContext(teamConfig)}
|
|
1131
1419
|
${formatDorDodSection(boardSettings, story.dorChecklist, story.dodChecklist)}
|
|
1132
1420
|
## Proposal
|
|
@@ -1234,6 +1522,56 @@ ${dorWarnings}`;
|
|
|
1234
1522
|
content: [{ type: "text", text: formattedResult.trim() }],
|
|
1235
1523
|
};
|
|
1236
1524
|
}
|
|
1525
|
+
case "get_test_plan": {
|
|
1526
|
+
const args = request.params.arguments;
|
|
1527
|
+
const { storyId } = args;
|
|
1528
|
+
if (!storyId) {
|
|
1529
|
+
throw new Error("storyId is required");
|
|
1530
|
+
}
|
|
1531
|
+
assertUUID(storyId, "storyId");
|
|
1532
|
+
const response = await client.get(`/stories/${storyId}`);
|
|
1533
|
+
const story = response.data;
|
|
1534
|
+
const testPlan = story.test_plan || story.testPlan;
|
|
1535
|
+
const workflowStage = story.workflow_stage || story.workflowStage;
|
|
1536
|
+
const testFilePaths = story.test_file_paths || story.testFilePaths || [];
|
|
1537
|
+
const feedback = story.test_plan_feedback || story.testPlanFeedback;
|
|
1538
|
+
const title = story.title || "Untitled Story";
|
|
1539
|
+
if (!testPlan) {
|
|
1540
|
+
return {
|
|
1541
|
+
content: [
|
|
1542
|
+
{
|
|
1543
|
+
type: "text",
|
|
1544
|
+
text: `# No Test Plan\n\n**Story:** ${title}\n**Story ID:** ${storyId}\n**Workflow Stage:** ${workflowStage || "none"}\n\nThis story does not have a test plan yet. Use \`propose_test_plan\` to submit one.`,
|
|
1545
|
+
},
|
|
1546
|
+
],
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
const stageLabel = workflowStage === "tests_approved"
|
|
1550
|
+
? "Approved"
|
|
1551
|
+
: workflowStage === "tests_proposed"
|
|
1552
|
+
? "Proposed — Awaiting Approval"
|
|
1553
|
+
: workflowStage === "tests_revision_requested"
|
|
1554
|
+
? "Revision Requested"
|
|
1555
|
+
: workflowStage || "unknown";
|
|
1556
|
+
const feedbackSection = feedback
|
|
1557
|
+
? `\n## Revision Feedback\n> ${feedback.split("\n").join("\n> ")}\n`
|
|
1558
|
+
: "";
|
|
1559
|
+
const formattedResult = `# Test Plan: ${title}
|
|
1560
|
+
|
|
1561
|
+
**Story ID:** ${storyId}
|
|
1562
|
+
**Workflow Stage:** ${workflowStage}
|
|
1563
|
+
**Status:** ${stageLabel}
|
|
1564
|
+
|
|
1565
|
+
## Test Plan
|
|
1566
|
+
${testPlan}
|
|
1567
|
+
${feedbackSection}
|
|
1568
|
+
## Test Files
|
|
1569
|
+
${testFilePaths.length > 0 ? testFilePaths.map((p) => `- \`${p}\``).join("\n") : "No test files specified"}
|
|
1570
|
+
`;
|
|
1571
|
+
return {
|
|
1572
|
+
content: [{ type: "text", text: formattedResult.trim() }],
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1237
1575
|
case "propose_test_plan": {
|
|
1238
1576
|
const args = request.params.arguments;
|
|
1239
1577
|
const { storyId, testPlan, testFilePaths } = args;
|
|
@@ -1540,6 +1878,49 @@ ${categoryBreakdown}
|
|
|
1540
1878
|
${unestimated.length === 0
|
|
1541
1879
|
? "All stories have been estimated!"
|
|
1542
1880
|
: unestimated.map((s) => `- ${s.title} (${s.id})`).join("\n")}
|
|
1881
|
+
`;
|
|
1882
|
+
return {
|
|
1883
|
+
content: [{ type: "text", text: formattedResult.trim() }],
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
case "prioritize_epic": {
|
|
1887
|
+
const args = request.params.arguments;
|
|
1888
|
+
const { epicId, mode = "analyze" } = args;
|
|
1889
|
+
if (!epicId)
|
|
1890
|
+
throw new Error("epicId is required");
|
|
1891
|
+
assertUUID(epicId, "epicId");
|
|
1892
|
+
const res = await client.post(`/prioritize-epic/${epicId}`, { mode });
|
|
1893
|
+
if (mode === "apply") {
|
|
1894
|
+
return {
|
|
1895
|
+
content: [{
|
|
1896
|
+
type: "text",
|
|
1897
|
+
text: `Prioritization applied to epic. ${res.data.updatedCount} stories reordered.`,
|
|
1898
|
+
}],
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
// Analyze mode — format the preview
|
|
1902
|
+
const data = res.data;
|
|
1903
|
+
const orderList = (data.proposedOrder || [])
|
|
1904
|
+
.map((item, i) => `${i + 1}. **${item.storyTitle}** — ${item.reason}`)
|
|
1905
|
+
.join("\n");
|
|
1906
|
+
const depList = (data.dependencies || [])
|
|
1907
|
+
.map((dep) => `- ${dep.storyTitle} is blocked by ${dep.blockedByStoryTitle}: ${dep.reason}`)
|
|
1908
|
+
.join("\n");
|
|
1909
|
+
const circularWarning = data.circularDependencies?.length > 0
|
|
1910
|
+
? `\n## Circular Dependencies Detected\n${data.circularDependencies.map((cd) => `- ${cd.description}`).join("\n")}\n`
|
|
1911
|
+
: "";
|
|
1912
|
+
const formattedResult = `
|
|
1913
|
+
# AI Prioritization Preview
|
|
1914
|
+
|
|
1915
|
+
${data.summary}
|
|
1916
|
+
${circularWarning}
|
|
1917
|
+
## Proposed Order (do first → do last)
|
|
1918
|
+
${orderList}
|
|
1919
|
+
|
|
1920
|
+
## Dependencies
|
|
1921
|
+
${depList || "No dependencies detected."}
|
|
1922
|
+
|
|
1923
|
+
> To apply this order, call \`prioritize_epic\` with mode: "apply"
|
|
1543
1924
|
`;
|
|
1544
1925
|
return {
|
|
1545
1926
|
content: [{ type: "text", text: formattedResult.trim() }],
|
|
@@ -1580,6 +1961,135 @@ ${unestimated.length === 0
|
|
|
1580
1961
|
content: [{ type: "text", text: sections.join("\n") }],
|
|
1581
1962
|
};
|
|
1582
1963
|
}
|
|
1964
|
+
// Team Decisions Handlers
|
|
1965
|
+
case "record_decision": {
|
|
1966
|
+
const args = request.params.arguments;
|
|
1967
|
+
const { title, content, category, tags, storyId, epicId } = args;
|
|
1968
|
+
if (!title || !content) {
|
|
1969
|
+
throw new Error("title and content are required");
|
|
1970
|
+
}
|
|
1971
|
+
if (storyId)
|
|
1972
|
+
assertUUID(storyId, "storyId");
|
|
1973
|
+
if (epicId)
|
|
1974
|
+
assertUUID(epicId, "epicId");
|
|
1975
|
+
const response = await client.post("/decisions", {
|
|
1976
|
+
title,
|
|
1977
|
+
content,
|
|
1978
|
+
category,
|
|
1979
|
+
tags,
|
|
1980
|
+
storyId,
|
|
1981
|
+
epicId,
|
|
1982
|
+
});
|
|
1983
|
+
const decision = response.data;
|
|
1984
|
+
const confirmText = `✅ Decision recorded: "${decision.title || title}"
|
|
1985
|
+
- **Category:** ${decision.category || category || "general"}
|
|
1986
|
+
- **Tags:** ${(decision.tags || tags || []).join(", ") || "none"}
|
|
1987
|
+
- **ID:** ${decision.id}`;
|
|
1988
|
+
return {
|
|
1989
|
+
content: [{ type: "text", text: confirmText }],
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
case "list_decisions": {
|
|
1993
|
+
const args = request.params.arguments;
|
|
1994
|
+
const { category, tag, since, limit } = args || {};
|
|
1995
|
+
const params = {};
|
|
1996
|
+
if (category)
|
|
1997
|
+
params.category = category;
|
|
1998
|
+
if (tag)
|
|
1999
|
+
params.tag = tag;
|
|
2000
|
+
if (since)
|
|
2001
|
+
params.since = since;
|
|
2002
|
+
if (limit)
|
|
2003
|
+
params.limit = String(limit);
|
|
2004
|
+
const response = await client.get("/decisions", { params });
|
|
2005
|
+
const { decisions, total } = response.data;
|
|
2006
|
+
if (!decisions || decisions.length === 0) {
|
|
2007
|
+
return {
|
|
2008
|
+
content: [{ type: "text", text: "No team decisions found matching the filters." }],
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
const formatted = decisions.map((d) => {
|
|
2012
|
+
const date = d.created_at ? new Date(d.created_at).toISOString().split("T")[0] : "unknown";
|
|
2013
|
+
const tagStr = d.tags?.length ? ` [${d.tags.join(", ")}]` : "";
|
|
2014
|
+
return `### [${date}] ${d.title} (${d.category || "general"})${tagStr}\n${d.content}`;
|
|
2015
|
+
}).join("\n\n---\n\n");
|
|
2016
|
+
const header = `# Team Decisions (${decisions.length} of ${total})\n\n`;
|
|
2017
|
+
return {
|
|
2018
|
+
content: [{ type: "text", text: header + formatted }],
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
case "search_decisions": {
|
|
2022
|
+
const args = request.params.arguments;
|
|
2023
|
+
const { query, category, limit } = args || {};
|
|
2024
|
+
if (!query) {
|
|
2025
|
+
throw new Error("query is required");
|
|
2026
|
+
}
|
|
2027
|
+
const params = { q: query };
|
|
2028
|
+
if (category)
|
|
2029
|
+
params.category = category;
|
|
2030
|
+
if (limit)
|
|
2031
|
+
params.limit = String(limit);
|
|
2032
|
+
const response = await client.get("/decisions/search", { params });
|
|
2033
|
+
const { decisions } = response.data;
|
|
2034
|
+
if (!decisions || decisions.length === 0) {
|
|
2035
|
+
return {
|
|
2036
|
+
content: [{ type: "text", text: `No team decisions found matching "${query}".` }],
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
const formatted = decisions.map((d) => {
|
|
2040
|
+
const date = d.created_at ? new Date(d.created_at).toISOString().split("T")[0] : "unknown";
|
|
2041
|
+
const tagStr = d.tags?.length ? ` [${d.tags.join(", ")}]` : "";
|
|
2042
|
+
return `### [${date}] ${d.title} (${d.category || "general"})${tagStr}\n${d.content}`;
|
|
2043
|
+
}).join("\n\n---\n\n");
|
|
2044
|
+
const header = `# Search Results for "${query}" (${decisions.length} matches)\n\n`;
|
|
2045
|
+
return {
|
|
2046
|
+
content: [{ type: "text", text: header + formatted }],
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
case "search_knowledge": {
|
|
2050
|
+
const args = request.params.arguments;
|
|
2051
|
+
const { query, category, limit } = args || {};
|
|
2052
|
+
if (!query) {
|
|
2053
|
+
throw new Error("query is required");
|
|
2054
|
+
}
|
|
2055
|
+
const params = { q: query };
|
|
2056
|
+
if (category)
|
|
2057
|
+
params.category = category;
|
|
2058
|
+
if (limit)
|
|
2059
|
+
params.limit = String(limit);
|
|
2060
|
+
const response = await client.get("/knowledge/search", { params });
|
|
2061
|
+
const { results } = response.data;
|
|
2062
|
+
if (!results || results.length === 0) {
|
|
2063
|
+
return {
|
|
2064
|
+
content: [{ type: "text", text: `No knowledge results found matching "${query}".` }],
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
const formatted = results.map((r) => {
|
|
2068
|
+
const relevance = r.relevance ? `(${(r.relevance * 100).toFixed(1)}% relevant)` : "";
|
|
2069
|
+
return `### ${r.source_title} [${r.source_type}] ${relevance}\n${r.content}`;
|
|
2070
|
+
}).join("\n\n---\n\n");
|
|
2071
|
+
const header = `# Knowledge Search Results for "${query}" (${results.length} matches)\n\n`;
|
|
2072
|
+
return {
|
|
2073
|
+
content: [{ type: "text", text: header + formatted }],
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
case "list_knowledge_sources": {
|
|
2077
|
+
const args = request.params.arguments;
|
|
2078
|
+
const params = {};
|
|
2079
|
+
if (args?.source_type)
|
|
2080
|
+
params.source_type = args.source_type;
|
|
2081
|
+
const response = await client.get("/knowledge/sources", { params });
|
|
2082
|
+
const { sources } = response.data;
|
|
2083
|
+
if (!sources || sources.length === 0) {
|
|
2084
|
+
return {
|
|
2085
|
+
content: [{ type: "text", text: "No knowledge sources are currently indexed." }],
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
const formatted = sources.map((s) => `- **${s.source_title}** (${s.source_type}) — ${s.chunk_count || 0} chunks`).join("\n");
|
|
2089
|
+
return {
|
|
2090
|
+
content: [{ type: "text", text: `# Knowledge Sources\n\n${formatted}` }],
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
1583
2093
|
default:
|
|
1584
2094
|
throw new Error("Unknown tool");
|
|
1585
2095
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elixium.ai/mcp-server",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP Server for Elixium.ai",
|
|
6
|
-
"mcpName": "io.github.
|
|
6
|
+
"mcpName": "io.github.IndirectTek/mcp-server",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|