@elixium.ai/mcp-server 0.1.8 → 0.2.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 +685 -299
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -113,16 +113,9 @@ const parseLaneStyle = (value) => {
113
113
  return null;
114
114
  };
115
115
  const inferLaneStyleFromUrl = (apiUrl) => {
116
- try {
117
- const host = new URL(apiUrl).hostname.toLowerCase();
118
- if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".local")) {
119
- return "title";
120
- }
121
- }
122
- catch {
123
- // Fall through to default.
124
- }
125
- return "upper";
116
+ // Default to title case as that's what the frontend LaneType enum expects
117
+ // (e.g., "Backlog", "Current", "Done", "Icebox")
118
+ return "title";
126
119
  };
127
120
  const normalizeLaneForComparison = (lane) => {
128
121
  if (typeof lane !== "string")
@@ -171,19 +164,91 @@ const resolveBoardId = async () => {
171
164
  cachedBoardId = match.id;
172
165
  return cachedBoardId;
173
166
  };
167
+ // Feature flag caching
168
+ let cachedFeatureConfig = null;
169
+ const FEATURE_FLAG_CACHE_TTL = 60000; // 1 minute
170
+ const fetchFeatureConfig = async () => {
171
+ const now = Date.now();
172
+ if (cachedFeatureConfig && now - cachedFeatureConfig.fetchedAt < FEATURE_FLAG_CACHE_TTL) {
173
+ const { fetchedAt, ...config } = cachedFeatureConfig;
174
+ return config;
175
+ }
176
+ try {
177
+ const boardId = await resolveBoardId();
178
+ const response = await client.get("/config/features", {
179
+ params: boardId ? { boardId } : undefined,
180
+ });
181
+ cachedFeatureConfig = {
182
+ ...response.data,
183
+ fetchedAt: now,
184
+ };
185
+ return response.data;
186
+ }
187
+ catch (error) {
188
+ console.error("Failed to fetch feature config, defaulting to all enabled:", error);
189
+ return {
190
+ features: {
191
+ balancedTeam: true,
192
+ learningLoop: true,
193
+ tddWorkflow: true,
194
+ aiTools: true,
195
+ },
196
+ source: {
197
+ balancedTeam: "error-fallback",
198
+ learningLoop: "error-fallback",
199
+ tddWorkflow: "error-fallback",
200
+ aiTools: "error-fallback",
201
+ },
202
+ };
203
+ }
204
+ };
205
+ // Helper functions to check individual features
206
+ const isTddWorkflowEnabled = async () => {
207
+ const config = await fetchFeatureConfig();
208
+ return config.features.tddWorkflow;
209
+ };
210
+ const isLearningLoopEnabled = async () => {
211
+ const config = await fetchFeatureConfig();
212
+ return config.features.learningLoop;
213
+ };
214
+ const isAiToolsEnabled = async () => {
215
+ const config = await fetchFeatureConfig();
216
+ return config.features.aiTools;
217
+ };
218
+ const getTeamProfile = async () => {
219
+ const config = await fetchFeatureConfig();
220
+ return config.teamProfile;
221
+ };
174
222
  const fetchStories = async () => {
175
223
  const boardId = await resolveBoardId();
224
+ const slug = normalizeBoardSlug(BOARD_SLUG);
225
+ const isMainBoard = slug === "main";
226
+ // For the "main" board, fetch ALL stories (no boardId filter) then filter
227
+ // client-side. This matches the frontend behavior: legacy stories have
228
+ // boardId: null and must be included on the main board.
176
229
  const response = await client.get("/stories", {
177
- params: boardId ? { boardId } : undefined,
230
+ params: boardId && !isMainBoard ? { boardId } : undefined,
178
231
  });
179
- return extractStories(response.data);
232
+ let stories = extractStories(response.data);
233
+ // Client-side filter for main board: include stories matching boardId OR
234
+ // with no boardId (legacy stories created before multi-board support)
235
+ if (boardId && isMainBoard) {
236
+ stories = stories.filter((s) => s.boardId === boardId || !s.boardId);
237
+ }
238
+ return stories;
180
239
  };
181
240
  const fetchEpics = async () => {
182
241
  const boardId = await resolveBoardId();
242
+ const slug = normalizeBoardSlug(BOARD_SLUG);
243
+ const isMainBoard = slug === "main";
183
244
  const response = await client.get("/epics", {
184
- params: boardId ? { boardId } : undefined,
245
+ params: boardId && !isMainBoard ? { boardId } : undefined,
185
246
  });
186
- return extractEpics(response.data);
247
+ let epics = extractEpics(response.data);
248
+ if (boardId && isMainBoard) {
249
+ epics = epics.filter((e) => e.boardId === boardId || !e.boardId);
250
+ }
251
+ return epics;
187
252
  };
188
253
  let cachedLaneStyle = null;
189
254
  const detectLaneStyle = async () => {
@@ -221,7 +286,15 @@ const normalizeLane = async (lane) => {
221
286
  const buildIterationContext = (stories, user = null) => {
222
287
  const currentIteration = stories.filter((story) => normalizeLaneForComparison(story?.lane) === "current");
223
288
  const backlog = stories.filter((story) => normalizeLaneForComparison(story?.lane) === "backlog");
224
- return { currentIteration, backlog, user };
289
+ // Cost summary across current iteration
290
+ const iterationStories = [...currentIteration, ...backlog];
291
+ const withEstimates = iterationStories.filter((s) => s.costEstimate);
292
+ const costSummary = {
293
+ totalMonthlyCost: withEstimates.reduce((sum, s) => sum + (s.costEstimate?.totalMonthlyCost || 0), 0),
294
+ estimatedCount: withEstimates.length,
295
+ unestimatedCount: iterationStories.length - withEstimates.length,
296
+ };
297
+ return { currentIteration, backlog, user, costSummary };
225
298
  };
226
299
  const createServer = () => {
227
300
  const server = new Server({
@@ -234,302 +307,432 @@ const createServer = () => {
234
307
  });
235
308
  // Define Tools
236
309
  server.setRequestHandler(ListToolsRequestSchema, async () => {
237
- return {
238
- tools: [
239
- {
240
- name: "list_stories",
241
- description: "List all stories on the Elixium board",
242
- inputSchema: {
243
- type: "object",
244
- properties: {},
245
- },
310
+ const featureConfig = await fetchFeatureConfig();
311
+ const tddEnabled = featureConfig.features.tddWorkflow;
312
+ const learningLoopEnabled = featureConfig.features.learningLoop;
313
+ const baseTools = [
314
+ {
315
+ name: "get_feature_config",
316
+ description: "Get the feature configuration for this board/workspace. Returns enabled features, team profile, and smart defaults context.",
317
+ inputSchema: {
318
+ type: "object",
319
+ properties: {},
246
320
  },
247
- {
248
- name: "create_story",
249
- description: "Create a new story on the Elixium board",
250
- inputSchema: {
251
- type: "object",
252
- properties: {
253
- title: { type: "string", description: "Title of the story" },
254
- description: {
255
- type: "string",
256
- description: "Description of the story",
257
- },
258
- acceptanceCriteria: {
259
- type: "string",
260
- description: "Acceptance criteria in Given/When/Then format",
261
- },
262
- lane: {
263
- type: "string",
264
- description: "Lane to add the story to (case-insensitive)",
265
- enum: [
266
- "Backlog",
267
- "Icebox",
268
- "Current",
269
- "Done",
270
- "BACKLOG",
271
- "ICEBOX",
272
- "CURRENT",
273
- "DONE",
274
- ],
275
- },
276
- points: {
277
- type: "number",
278
- description: "Points (0, 1, 2, 3, 5, 8)",
279
- },
280
- requester: {
281
- type: "string",
282
- description: "Email of the person requesting this story. Defaults to ELIXIUM_USER_EMAIL env var or API key owner.",
283
- },
284
- },
285
- required: ["title"],
286
- },
321
+ },
322
+ {
323
+ name: "list_stories",
324
+ description: "List all stories on the Elixium board",
325
+ inputSchema: {
326
+ type: "object",
327
+ properties: {},
287
328
  },
288
- {
289
- name: "get_iteration_context",
290
- description: "Get the current iteration context (Current + Backlog) for AI planning",
291
- inputSchema: {
292
- type: "object",
293
- properties: {},
329
+ },
330
+ {
331
+ name: "create_story",
332
+ description: "Create a new story on the Elixium board",
333
+ inputSchema: {
334
+ type: "object",
335
+ properties: {
336
+ title: { type: "string", description: "Title of the story" },
337
+ description: {
338
+ type: "string",
339
+ description: "Description of the story",
340
+ },
341
+ acceptanceCriteria: {
342
+ type: "string",
343
+ description: "Acceptance criteria in Given/When/Then format",
344
+ },
345
+ lane: {
346
+ type: "string",
347
+ description: "Lane to add the story to (case-insensitive)",
348
+ enum: [
349
+ "Backlog",
350
+ "Icebox",
351
+ "Current",
352
+ "Done",
353
+ "BACKLOG",
354
+ "ICEBOX",
355
+ "CURRENT",
356
+ "DONE",
357
+ ],
358
+ },
359
+ points: {
360
+ type: "number",
361
+ description: "Points (0, 1, 2, 3, 5, 8)",
362
+ },
363
+ requester: {
364
+ type: "string",
365
+ description: "Email of the person requesting this story. Defaults to ELIXIUM_USER_EMAIL env var or API key owner.",
366
+ },
294
367
  },
368
+ required: ["title"],
369
+ },
370
+ },
371
+ {
372
+ name: "get_iteration_context",
373
+ description: "Get the current iteration context (Current + Backlog) for AI planning",
374
+ inputSchema: {
375
+ type: "object",
376
+ properties: {},
377
+ },
378
+ },
379
+ {
380
+ name: "list_objectives",
381
+ description: "List objectives for the current workspace",
382
+ inputSchema: {
383
+ type: "object",
384
+ properties: {},
385
+ },
386
+ },
387
+ {
388
+ name: "list_epics",
389
+ description: "List epics for the current board",
390
+ inputSchema: {
391
+ type: "object",
392
+ properties: {},
295
393
  },
296
- {
297
- name: "create_hypothesis",
298
- description: "Create a new assumption/hypothesis in the Icebox",
299
- inputSchema: {
300
- type: "object",
301
- properties: {
302
- title: { type: "string", description: "The assumption statement" },
303
- hypothesis: { type: "string", description: "Detailed hypothesis" },
304
- confidence_score: {
305
- type: "number",
306
- description: "Initial confidence (1-5)",
307
- minimum: 1,
308
- maximum: 5,
309
- },
310
- },
311
- required: ["title", "hypothesis", "confidence_score"],
394
+ },
395
+ {
396
+ name: "create_epic",
397
+ description: "Create a new epic for the current board",
398
+ inputSchema: {
399
+ type: "object",
400
+ properties: {
401
+ title: { type: "string", description: "Epic title" },
402
+ description: { type: "string", description: "Epic description" },
403
+ stage: {
404
+ type: "string",
405
+ description: "Roadmap stage",
406
+ enum: ["in_progress", "next", "soon", "someday", "archived"],
407
+ },
408
+ hypothesis: { type: "string", description: "We believe that... (outcome hypothesis)" },
409
+ successMetrics: { type: "string", description: "We'll know it works when... (success criteria)" },
410
+ targetDate: { type: "number", description: "Unix timestamp - when to evaluate outcome (not ship date)" },
411
+ outcomeStatus: {
412
+ type: "string",
413
+ description: "Outcome validation status",
414
+ enum: ["exploring", "validating", "achieved", "abandoned"],
415
+ },
312
416
  },
417
+ required: ["title"],
313
418
  },
314
- {
315
- name: "list_objectives",
316
- description: "List objectives for the current workspace",
317
- inputSchema: {
318
- type: "object",
319
- properties: {},
419
+ },
420
+ {
421
+ name: "update_epic",
422
+ description: "Update an epic (title, description, stage, or outcome fields)",
423
+ inputSchema: {
424
+ type: "object",
425
+ properties: {
426
+ epicId: { type: "string", description: "ID of the epic" },
427
+ title: { type: "string", description: "Epic title" },
428
+ description: { type: "string", description: "Epic description" },
429
+ stage: {
430
+ type: "string",
431
+ description: "Roadmap stage",
432
+ enum: ["in_progress", "next", "soon", "someday", "archived"],
433
+ },
434
+ hypothesis: { type: "string", description: "We believe that... (outcome hypothesis)" },
435
+ successMetrics: { type: "string", description: "We'll know it works when... (success criteria)" },
436
+ targetDate: { type: "number", description: "Unix timestamp - when to evaluate outcome (not ship date)" },
437
+ outcomeStatus: {
438
+ type: "string",
439
+ description: "Outcome validation status",
440
+ enum: ["exploring", "validating", "achieved", "abandoned"],
441
+ },
320
442
  },
443
+ required: ["epicId"],
321
444
  },
322
- {
323
- name: "record_learning",
324
- description: "Record a learning outcome for a completed story",
325
- inputSchema: {
326
- type: "object",
327
- properties: {
328
- storyId: { type: "string", description: "ID of the story" },
329
- outcome_summary: {
330
- type: "string",
331
- description: "What was learned?",
332
- },
333
- },
334
- required: ["storyId", "outcome_summary"],
445
+ },
446
+ {
447
+ name: "update_story",
448
+ description: "Update fields on an existing story",
449
+ inputSchema: {
450
+ type: "object",
451
+ properties: {
452
+ storyId: { type: "string", description: "ID of the story" },
453
+ title: { type: "string", description: "Updated title" },
454
+ description: { type: "string", description: "Updated description" },
455
+ lane: {
456
+ type: "string",
457
+ description: "Lane to move the story to (case-insensitive)",
458
+ enum: [
459
+ "Backlog",
460
+ "Icebox",
461
+ "Current",
462
+ "Done",
463
+ "BACKLOG",
464
+ "ICEBOX",
465
+ "CURRENT",
466
+ "DONE",
467
+ ],
468
+ },
469
+ points: {
470
+ type: "number",
471
+ description: "Updated points (0, 1, 2, 3, 5, 8)",
472
+ },
473
+ state: {
474
+ type: "string",
475
+ description: "Story state (unstarted, started, finished, delivered, accepted, rejected)",
476
+ },
477
+ outcome_summary: {
478
+ type: "string",
479
+ description: "Learning outcome summary",
480
+ },
481
+ acceptanceCriteria: {
482
+ type: "string",
483
+ description: "Acceptance criteria in Given/When/Then format",
484
+ },
335
485
  },
486
+ required: ["storyId"],
336
487
  },
337
- {
338
- name: "list_epics",
339
- description: "List epics for the current board",
340
- inputSchema: {
341
- type: "object",
342
- properties: {},
488
+ },
489
+ {
490
+ name: "prepare_implementation",
491
+ description: "Fetch all context for a story and format an implementation brief",
492
+ inputSchema: {
493
+ type: "object",
494
+ properties: {
495
+ storyId: {
496
+ type: "string",
497
+ description: "ID of the story to prepare",
498
+ },
343
499
  },
500
+ required: ["storyId"],
344
501
  },
345
- {
346
- name: "create_epic",
347
- description: "Create a new epic for the current board",
348
- inputSchema: {
349
- type: "object",
350
- properties: {
351
- title: { type: "string", description: "Epic title" },
352
- description: { type: "string", description: "Epic description" },
353
- stage: {
354
- type: "string",
355
- description: "Roadmap stage",
356
- enum: ["in_progress", "next", "soon", "someday", "archived"],
357
- },
358
- hypothesis: { type: "string", description: "We believe that... (outcome hypothesis)" },
359
- successMetrics: { type: "string", description: "We'll know it works when... (success criteria)" },
360
- targetDate: { type: "number", description: "Unix timestamp - when to evaluate outcome (not ship date)" },
361
- outcomeStatus: {
362
- type: "string",
363
- description: "Outcome validation status",
364
- enum: ["exploring", "validating", "achieved", "abandoned"],
365
- },
366
- },
367
- required: ["title"],
502
+ },
503
+ {
504
+ name: "estimate_cost",
505
+ description: "Generate AI-assisted infrastructure cost estimate for a story. Uses live cloud pricing data when available.",
506
+ inputSchema: {
507
+ type: "object",
508
+ properties: {
509
+ storyId: {
510
+ type: "string",
511
+ description: "ID of the story to estimate cost for",
512
+ },
513
+ provider: {
514
+ type: "string",
515
+ description: "Cloud provider for the estimate",
516
+ enum: ["gcp", "aws", "azure", "self-hosted"],
517
+ },
518
+ constraints: {
519
+ type: "string",
520
+ description: "Optional constraints (e.g., budget limit, region, instance types)",
521
+ },
368
522
  },
523
+ required: ["storyId", "provider"],
369
524
  },
370
- {
371
- name: "update_epic",
372
- description: "Update an epic (title, description, stage, or outcome fields)",
373
- inputSchema: {
374
- type: "object",
375
- properties: {
376
- epicId: { type: "string", description: "ID of the epic" },
377
- title: { type: "string", description: "Epic title" },
378
- description: { type: "string", description: "Epic description" },
379
- stage: {
380
- type: "string",
381
- description: "Roadmap stage",
382
- enum: ["in_progress", "next", "soon", "someday", "archived"],
383
- },
384
- hypothesis: { type: "string", description: "We believe that... (outcome hypothesis)" },
385
- successMetrics: { type: "string", description: "We'll know it works when... (success criteria)" },
386
- targetDate: { type: "number", description: "Unix timestamp - when to evaluate outcome (not ship date)" },
387
- outcomeStatus: {
388
- type: "string",
389
- description: "Outcome validation status",
390
- enum: ["exploring", "validating", "achieved", "abandoned"],
391
- },
392
- },
393
- required: ["epicId"],
525
+ },
526
+ {
527
+ name: "get_epic_cost_rollup",
528
+ description: "Get aggregated infrastructure cost for all stories in an epic. Shows total monthly cost, category breakdown, and estimation coverage.",
529
+ inputSchema: {
530
+ type: "object",
531
+ properties: {
532
+ epicId: {
533
+ type: "string",
534
+ description: "ID of the epic",
535
+ },
394
536
  },
537
+ required: ["epicId"],
395
538
  },
396
- {
397
- name: "update_story",
398
- description: "Update fields on an existing story",
399
- inputSchema: {
400
- type: "object",
401
- properties: {
402
- storyId: { type: "string", description: "ID of the story" },
403
- title: { type: "string", description: "Updated title" },
404
- description: { type: "string", description: "Updated description" },
405
- lane: {
406
- type: "string",
407
- description: "Lane to move the story to (case-insensitive)",
408
- enum: [
409
- "Backlog",
410
- "Icebox",
411
- "Current",
412
- "Done",
413
- "BACKLOG",
414
- "ICEBOX",
415
- "CURRENT",
416
- "DONE",
417
- ],
418
- },
419
- points: {
420
- type: "number",
421
- description: "Updated points (0, 1, 2, 3, 5, 8)",
422
- },
423
- state: {
424
- type: "string",
425
- description: "Story state (unstarted, started, finished, delivered, accepted, rejected)",
426
- },
427
- outcome_summary: {
428
- type: "string",
429
- description: "Learning outcome summary",
430
- },
431
- acceptanceCriteria: {
432
- type: "string",
433
- description: "Acceptance criteria in Given/When/Then format",
434
- },
435
- },
436
- required: ["storyId"],
539
+ },
540
+ ];
541
+ const tddTools = tddEnabled ? [
542
+ // TDD Workflow Tools
543
+ {
544
+ name: "start_story",
545
+ description: "Start TDD workflow for a story. By default creates a feature branch. Set trunkBased=true to work directly on main (recommended for small changes). Set autoMerge=true with trunkBased for short-lived branches that auto-merge on submit.",
546
+ inputSchema: {
547
+ type: "object",
548
+ properties: {
549
+ storyId: {
550
+ type: "string",
551
+ description: "ID of the story to start",
552
+ },
553
+ branchPrefix: {
554
+ type: "string",
555
+ description: "Branch prefix (feat, fix, chore) - ignored if trunkBased=true without autoMerge",
556
+ enum: ["feat", "fix", "chore"],
557
+ },
558
+ trunkBased: {
559
+ type: "boolean",
560
+ description: "If true, use trunk-based development. Without autoMerge, commits directly to main with feature flag. With autoMerge, creates a short-lived branch. Board/workspace defaults apply if not set.",
561
+ },
562
+ autoMerge: {
563
+ type: "boolean",
564
+ description: "If true with trunkBased, creates a short-lived branch that auto-merges to main on submit_for_review. Branch is deleted after merge.",
565
+ },
437
566
  },
567
+ required: ["storyId"],
438
568
  },
439
- {
440
- name: "prepare_implementation",
441
- description: "Fetch all context for a story and format an implementation brief",
442
- inputSchema: {
443
- type: "object",
444
- properties: {
445
- storyId: {
446
- type: "string",
447
- description: "ID of the story to prepare",
448
- },
449
- },
450
- required: ["storyId"],
569
+ },
570
+ {
571
+ name: "propose_test_plan",
572
+ description: "Submit a test plan for human review. Sets workflow_stage to tests_proposed. Implementation is BLOCKED until human approves.",
573
+ inputSchema: {
574
+ type: "object",
575
+ properties: {
576
+ storyId: {
577
+ type: "string",
578
+ description: "ID of the story",
579
+ },
580
+ testPlan: {
581
+ type: "string",
582
+ description: "Markdown test plan describing test strategy",
583
+ },
584
+ testFilePaths: {
585
+ type: "array",
586
+ items: { type: "string" },
587
+ description: "Paths to created test files",
588
+ },
451
589
  },
590
+ required: ["storyId", "testPlan"],
452
591
  },
453
- // TDD Workflow Tools
454
- {
455
- name: "start_story",
456
- description: "Start TDD workflow for a story. By default creates a feature branch. Set trunkBased=true to work directly on main (recommended for small changes).",
457
- inputSchema: {
458
- type: "object",
459
- properties: {
460
- storyId: {
461
- type: "string",
462
- description: "ID of the story to start",
463
- },
464
- branchPrefix: {
465
- type: "string",
466
- description: "Branch prefix (feat, fix, chore) - ignored if trunkBased=true",
467
- enum: ["feat", "fix", "chore"],
468
- },
469
- trunkBased: {
470
- type: "boolean",
471
- description: "If true, skip branch creation and commit directly to main. Recommended for small, well-tested changes.",
472
- },
473
- },
474
- required: ["storyId"],
592
+ },
593
+ {
594
+ name: "submit_for_review",
595
+ description: "Submit implementation for human review. Only works if tests are approved. Sets state to finished.",
596
+ inputSchema: {
597
+ type: "object",
598
+ properties: {
599
+ storyId: {
600
+ type: "string",
601
+ description: "ID of the story",
602
+ },
603
+ commitHash: {
604
+ type: "string",
605
+ description: "Latest commit SHA",
606
+ },
607
+ testResults: {
608
+ type: "string",
609
+ description: "Test output summary",
610
+ },
611
+ implementationNotes: {
612
+ type: "string",
613
+ description: "What was implemented",
614
+ },
475
615
  },
616
+ required: ["storyId"],
476
617
  },
477
- {
478
- name: "propose_test_plan",
479
- description: "Submit a test plan for human review. Sets workflow_stage to tests_proposed. Implementation is BLOCKED until human approves.",
480
- inputSchema: {
481
- type: "object",
482
- properties: {
483
- storyId: {
484
- type: "string",
485
- description: "ID of the story",
486
- },
487
- testPlan: {
488
- type: "string",
489
- description: "Markdown test plan describing test strategy",
490
- },
491
- testFilePaths: {
492
- type: "array",
493
- items: { type: "string" },
494
- description: "Paths to created test files",
495
- },
496
- },
497
- required: ["storyId", "testPlan"],
618
+ },
619
+ ] : [];
620
+ // Learning Loop tools (conditional on feature flag)
621
+ const learningLoopTools = learningLoopEnabled ? [
622
+ {
623
+ name: "create_hypothesis",
624
+ description: "Create a new assumption/hypothesis in the Icebox for validation",
625
+ inputSchema: {
626
+ type: "object",
627
+ properties: {
628
+ title: { type: "string", description: "The assumption statement" },
629
+ hypothesis: { type: "string", description: "Detailed hypothesis" },
630
+ confidence_score: {
631
+ type: "number",
632
+ description: "Initial confidence (1-5)",
633
+ minimum: 1,
634
+ maximum: 5,
635
+ },
498
636
  },
637
+ required: ["title", "hypothesis", "confidence_score"],
499
638
  },
500
- {
501
- name: "submit_for_review",
502
- description: "Submit implementation for human review. Only works if tests are approved. Sets state to finished.",
503
- inputSchema: {
504
- type: "object",
505
- properties: {
506
- storyId: {
507
- type: "string",
508
- description: "ID of the story",
509
- },
510
- commitHash: {
511
- type: "string",
512
- description: "Latest commit SHA",
513
- },
514
- testResults: {
515
- type: "string",
516
- description: "Test output summary",
517
- },
518
- implementationNotes: {
519
- type: "string",
520
- description: "What was implemented",
521
- },
522
- },
523
- required: ["storyId"],
639
+ },
640
+ {
641
+ name: "record_learning",
642
+ description: "Record a learning outcome for a completed story",
643
+ inputSchema: {
644
+ type: "object",
645
+ properties: {
646
+ storyId: { type: "string", description: "ID of the story" },
647
+ outcome_summary: {
648
+ type: "string",
649
+ description: "What was learned?",
650
+ },
524
651
  },
652
+ required: ["storyId", "outcome_summary"],
525
653
  },
526
- ],
527
- };
654
+ },
655
+ ] : [];
656
+ return { tools: [...baseTools, ...tddTools, ...learningLoopTools] };
528
657
  });
529
658
  // Handle Requests
530
659
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
531
660
  try {
532
- switch (request.params.name) {
661
+ const toolName = request.params.name;
662
+ // Check TDD workflow tools
663
+ const tddWorkflowTools = ["start_story", "propose_test_plan", "submit_for_review"];
664
+ if (tddWorkflowTools.includes(toolName)) {
665
+ const enabled = await isTddWorkflowEnabled();
666
+ if (!enabled) {
667
+ return {
668
+ content: [{
669
+ type: "text",
670
+ text: JSON.stringify({
671
+ error: "TDD workflow is disabled",
672
+ message: "The TDD workflow feature is currently disabled for this board. Contact your workspace administrator to enable it.",
673
+ help: "TDD workflow can be enabled in workspace settings or per-board settings."
674
+ }, null, 2)
675
+ }],
676
+ isError: true,
677
+ };
678
+ }
679
+ }
680
+ // Check Learning Loop tools
681
+ const learningLoopTools = ["create_hypothesis", "record_learning"];
682
+ if (learningLoopTools.includes(toolName)) {
683
+ const enabled = await isLearningLoopEnabled();
684
+ if (!enabled) {
685
+ return {
686
+ content: [{
687
+ type: "text",
688
+ text: JSON.stringify({
689
+ error: "Learning Loop is disabled",
690
+ message: "The Learning Loop feature is currently disabled for this board. This feature includes hypothesis tracking and confidence scoring.",
691
+ help: "Learning Loop can be enabled in workspace settings or per-board settings."
692
+ }, null, 2)
693
+ }],
694
+ isError: true,
695
+ };
696
+ }
697
+ }
698
+ switch (toolName) {
699
+ case "get_feature_config": {
700
+ const config = await fetchFeatureConfig();
701
+ const formattedConfig = `
702
+ # Workspace Feature Configuration
703
+
704
+ ## Enabled Features
705
+ ${config.features.balancedTeam ? "✅" : "❌"} **Balanced Team** - Design URLs, Three Amigos checks, OKR linking
706
+ ${config.features.learningLoop ? "✅" : "❌"} **Learning Loop** - Hypothesis tracking, risk profiles, confidence scores
707
+ ${config.features.tddWorkflow ? "✅" : "❌"} **TDD Workflow** - Test-driven development enforcement
708
+ ${config.features.aiTools ? "✅" : "❌"} **AI Tools** - AI-powered suggestions and analysis
709
+
710
+ ## Team Profile
711
+ ${config.teamProfile ? `
712
+ - Team Size: ${config.teamProfile.teamSize || "Not set"}
713
+ - Has Designer: ${config.teamProfile.hasDesigner ? "Yes" : "No"}
714
+ - Has Product Manager: ${config.teamProfile.hasProductManager ? "Yes" : "No"}
715
+ - Has QA: ${config.teamProfile.hasQA ? "Yes" : "No"}
716
+ - Development Approach: ${config.teamProfile.developmentApproach || "Not set"}
717
+ ` : "Team profile not configured"}
718
+
719
+ ## Feature Sources
720
+ - Balanced Team: ${config.source.balancedTeam}
721
+ - Learning Loop: ${config.source.learningLoop}
722
+ - TDD Workflow: ${config.source.tddWorkflow}
723
+ - AI Tools: ${config.source.aiTools}
724
+
725
+ ## Branching Strategy Defaults
726
+ ${config.branchingDefaults ? `- Trunk-Based: ${config.branchingDefaults.trunkBased ? "Yes" : "No"}
727
+ - Auto-Merge: ${config.branchingDefaults.autoMerge ? "Yes" : "No"}
728
+ - Source: ${config.branchingDefaults.source}` : "Not configured (branch-based default)"}
729
+
730
+ > **Tip:** Features can be configured at workspace or board level. Board settings override workspace defaults.
731
+ `;
732
+ return {
733
+ content: [{ type: "text", text: formattedConfig.trim() }],
734
+ };
735
+ }
533
736
  case "list_stories": {
534
737
  const stories = await fetchStories();
535
738
  return {
@@ -542,6 +745,12 @@ const createServer = () => {
542
745
  const args = request.params.arguments;
543
746
  const normalizedLane = await normalizeLane(args.lane);
544
747
  const boardId = await resolveBoardId();
748
+ // Warn if no boardId - stories may not appear on board UI
749
+ if (!boardId) {
750
+ console.warn("Warning: ELIXIUM_BOARD_SLUG is not set or board not found. " +
751
+ "Stories created without boardId may not appear on the board UI. " +
752
+ "Set ELIXIUM_BOARD_SLUG in your MCP configuration.");
753
+ }
545
754
  // Use requester from args, fall back to env var, then let backend use API key owner
546
755
  const requester = args.requester || USER_EMAIL;
547
756
  const response = await client.post("/stories", {
@@ -724,35 +933,68 @@ Here’s the smallest change that will validate it:
724
933
  // TDD Workflow Handlers
725
934
  case "start_story": {
726
935
  const args = request.params.arguments;
727
- const { storyId, branchPrefix, trunkBased } = args;
936
+ const { storyId, branchPrefix, trunkBased, autoMerge } = args;
728
937
  if (!storyId) {
729
938
  throw new Error("storyId is required");
730
939
  }
731
940
  const response = await client.post(`/stories/${storyId}/start`, {
732
941
  branchPrefix: branchPrefix || "feat",
733
- trunkBased: trunkBased || false,
942
+ trunkBased,
943
+ autoMerge,
734
944
  });
735
945
  const result = response.data;
736
- const isTrunk = trunkBased || result.branch === "main";
737
- const formattedResult = isTrunk ? `
946
+ const isTrunk = result.trunkBased;
947
+ const isAutoMerge = result.autoMerge;
948
+ let formattedResult;
949
+ if (isTrunk && !isAutoMerge) {
950
+ formattedResult = `
738
951
  # Story Started (Trunk-Based): ${storyId}
739
952
 
740
- **Mode:** 🚀 Direct to main (trunk-based development)
953
+ **Mode:** Direct to main (trunk-based development)
954
+ **Feature Flag:** \`${result.featureFlagName || "none"}\`
741
955
  **Workflow Stage:** ${result.workflow_stage}
742
956
 
743
957
  ## Acceptance Criteria
744
958
  ${result.acceptance_criteria || "No specific AC provided."}
745
959
 
746
960
  ## Trunk-Based Workflow
747
- 1. Write tests first
748
- 2. Call \`propose_test_plan\` and wait for approval
749
- 3. Implement (make tests pass)
750
- 4. Run \`npm run build\` to verify
751
- 5. Commit directly to main
752
- 6. 🚀 Auto-deploy on green CI
753
-
754
- > ⚠️ **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
755
- ` : `
961
+ 1. Write tests first
962
+ 2. Call \`propose_test_plan\` and wait for approval
963
+ 3. Implement (make tests pass)
964
+ 4. Run \`npm run build\` to verify
965
+ 5. Commit directly to main behind feature flag \`${result.featureFlagName}\`
966
+ 6. Auto-deploy on green CI
967
+
968
+ > **Feature Flag:** Wrap new behavior with \`${result.featureFlagName}\` for safe isolation.
969
+ > **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
970
+ `;
971
+ }
972
+ else if (isTrunk && isAutoMerge) {
973
+ formattedResult = `
974
+ # Story Started (Trunk-Based + Auto-Merge): ${storyId}
975
+
976
+ **Mode:** Short-lived branch with auto-merge to main
977
+ **Branch:** \`${result.branch}\`
978
+ **Feature Flag:** \`${result.featureFlagName || "none"}\`
979
+ **Workflow Stage:** ${result.workflow_stage}
980
+
981
+ ## Acceptance Criteria
982
+ ${result.acceptance_criteria || "No specific AC provided."}
983
+
984
+ ## Auto-Merge Workflow
985
+ 1. Write tests first on branch \`${result.branch}\`
986
+ 2. Call \`propose_test_plan\` and wait for approval
987
+ 3. Implement (make tests pass)
988
+ 4. Run \`npm run build\` to verify
989
+ 5. Call \`submit_for_review\` — branch auto-merges to main if tests pass
990
+ 6. Branch is deleted after merge
991
+
992
+ > **Feature Flag:** Wrap new behavior with \`${result.featureFlagName}\` for safe isolation.
993
+ > **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
994
+ `;
995
+ }
996
+ else {
997
+ formattedResult = `
756
998
  # Story Started: ${storyId}
757
999
 
758
1000
  **Branch:** \`${result.branch}\`
@@ -764,8 +1006,9 @@ ${result.acceptance_criteria || "No specific AC provided."}
764
1006
  ## Next Steps
765
1007
  ${result.workflow_reminder}
766
1008
 
767
- > ⚠️ **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
1009
+ > **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
768
1010
  `;
1011
+ }
769
1012
  return {
770
1013
  content: [{ type: "text", text: formattedResult.trim() }],
771
1014
  };
@@ -814,6 +1057,25 @@ ${result.message}
814
1057
  implementationNotes,
815
1058
  });
816
1059
  const result = response.data;
1060
+ const isAutoMerge = result.autoMerge;
1061
+ let autoMergeSection = "";
1062
+ if (isAutoMerge && result.autoMergeInstructions) {
1063
+ const instr = result.autoMergeInstructions;
1064
+ autoMergeSection = `
1065
+ ## Auto-Merge Instructions
1066
+ **Action:** Merge \`${instr.sourceBranch}\` into \`${instr.targetBranch}\`
1067
+ **Delete branch after merge:** Yes
1068
+
1069
+ Run these git commands:
1070
+ \`\`\`bash
1071
+ git checkout main && git pull
1072
+ git merge ${instr.sourceBranch}
1073
+ git push origin main
1074
+ git push origin --delete ${instr.sourceBranch}
1075
+ git branch -d ${instr.sourceBranch}
1076
+ \`\`\`
1077
+ `;
1078
+ }
817
1079
  const formattedResult = `
818
1080
  # Implementation Submitted for Review
819
1081
 
@@ -821,17 +1083,141 @@ ${result.message}
821
1083
  **Status:** ${result.status}
822
1084
  **Workflow Stage:** ${result.workflow_stage}
823
1085
  **State:** ${result.state}
1086
+ ${result.trunkBased ? `**Mode:** Trunk-based${isAutoMerge ? " + Auto-merge" : ""}` : ""}
1087
+ ${result.featureFlagName ? `**Feature Flag:** \`${result.featureFlagName}\`` : ""}
824
1088
 
825
1089
  ## Branch
826
- \`${result.branch || "No branch recorded"}\`
1090
+ \`${result.branch || "main (trunk-based)"}\`
827
1091
 
828
1092
  ## Commits
829
1093
  ${(result.commit_hashes || []).map((h) => `- \`${h}\``).join("\n") || "No commits recorded"}
830
-
1094
+ ${autoMergeSection}
831
1095
  ## Next Step
832
1096
  ${result.next_step}
833
1097
 
834
- > ✅ **Ready for Review:** Human should review diff and move story to Done lane.
1098
+ > ${isAutoMerge
1099
+ ? "**Auto-Merge:** Execute the git commands above to merge and clean up the branch."
1100
+ : "**Ready for Review:** Human should review diff and move story to Done lane."}
1101
+ `;
1102
+ return {
1103
+ content: [{ type: "text", text: formattedResult.trim() }],
1104
+ };
1105
+ }
1106
+ case "estimate_cost": {
1107
+ const args = request.params.arguments;
1108
+ const { storyId, provider, constraints } = args;
1109
+ if (!storyId)
1110
+ throw new Error("storyId is required");
1111
+ if (!provider)
1112
+ throw new Error("provider is required");
1113
+ if (!["gcp", "aws", "azure", "self-hosted"].includes(provider)) {
1114
+ throw new Error("provider must be one of: gcp, aws, azure, self-hosted");
1115
+ }
1116
+ // Fetch the story to get title and description
1117
+ const storyRes = await client.get(`/stories/${storyId}`);
1118
+ const storyData = storyRes.data;
1119
+ if (!storyData.description) {
1120
+ throw new Error("Story needs a description before cost estimation");
1121
+ }
1122
+ // Call the AI estimate-cost endpoint
1123
+ const estimateRes = await client.post("/ai/estimate-cost", {
1124
+ storyTitle: storyData.title,
1125
+ description: storyData.description,
1126
+ provider,
1127
+ ...(constraints ? { constraints } : {}),
1128
+ });
1129
+ const estimate = estimateRes.data;
1130
+ // Save the estimate to the story
1131
+ await client.patch(`/stories/${storyId}`, {
1132
+ costEstimate: estimate,
1133
+ });
1134
+ // Format result as markdown
1135
+ const componentTable = estimate.components
1136
+ .map((c) => provider === "self-hosted"
1137
+ ? `| ${c.name} | ${c.category} | ${c.resourceUnits || "-"} |`
1138
+ : `| ${c.name} | ${c.category} | $${c.monthlyCost?.toFixed(2)} | ${c.resourceUnits || "-"} |`)
1139
+ .join("\n");
1140
+ const tableHeader = provider === "self-hosted"
1141
+ ? "| Component | Category | Resources |\n|-----------|----------|-----------|\n"
1142
+ : "| Component | Category | Monthly Cost | Resources |\n|-----------|----------|-------------|------------|\n";
1143
+ const formattedResult = `
1144
+ # Cost Estimate: ${storyData.title}
1145
+
1146
+ **Provider:** ${provider.toUpperCase()}
1147
+ **Confidence:** ${estimate.confidence}
1148
+ **Pricing Source:** ${estimate.pricingSource === "live" ? "Live API data" : "AI-estimated"}
1149
+ ${provider !== "self-hosted" ? `**Total Monthly Cost:** $${estimate.totalMonthlyCost?.toLocaleString()}/mo` : "**Mode:** Self-hosted (resource units only)"}
1150
+
1151
+ ## Components
1152
+ ${tableHeader}${componentTable}
1153
+
1154
+ ## Assumptions
1155
+ ${(estimate.assumptions || []).map((a) => `- ${a}`).join("\n")}
1156
+
1157
+ > Estimate saved to story ${storyId}.
1158
+ `;
1159
+ return {
1160
+ content: [{ type: "text", text: formattedResult.trim() }],
1161
+ };
1162
+ }
1163
+ case "get_epic_cost_rollup": {
1164
+ const args = request.params.arguments;
1165
+ const { epicId } = args;
1166
+ if (!epicId)
1167
+ throw new Error("epicId is required");
1168
+ // Fetch all stories and filter by epicId
1169
+ const allStories = await fetchStories();
1170
+ const epicStories = allStories.filter((s) => s.epicId === epicId);
1171
+ // Fetch epic info
1172
+ const epicsRes = await client.get("/epics");
1173
+ const epicsData = Array.isArray(epicsRes.data) ? epicsRes.data : [];
1174
+ const epic = epicsData.find((e) => e.id === epicId);
1175
+ const epicTitle = epic?.title || epicId;
1176
+ const estimated = epicStories.filter((s) => s.costEstimate);
1177
+ const unestimated = epicStories.filter((s) => !s.costEstimate);
1178
+ if (estimated.length === 0) {
1179
+ return {
1180
+ content: [{
1181
+ type: "text",
1182
+ text: `# Cost Rollup: ${epicTitle}\n\nNo stories have cost estimates yet. Use \`estimate_cost\` to estimate individual stories first.\n\n**Stories without estimates:** ${unestimated.length}`,
1183
+ }],
1184
+ };
1185
+ }
1186
+ let totalMonthlyCost = 0;
1187
+ const byCategory = {};
1188
+ const providerCounts = {};
1189
+ estimated.forEach((s) => {
1190
+ const est = s.costEstimate;
1191
+ totalMonthlyCost += est.totalMonthlyCost || 0;
1192
+ providerCounts[est.provider] = (providerCounts[est.provider] || 0) + 1;
1193
+ (est.components || []).forEach((comp) => {
1194
+ byCategory[comp.category] = (byCategory[comp.category] || 0) + (comp.monthlyCost || 0);
1195
+ });
1196
+ });
1197
+ const providers = Object.keys(providerCounts);
1198
+ const providerSummary = providers.length === 1
1199
+ ? providers[0].toUpperCase()
1200
+ : providers.map((p) => `${p.toUpperCase()} (${providerCounts[p]})`).join(", ");
1201
+ const categoryBreakdown = Object.entries(byCategory)
1202
+ .sort(([, a], [, b]) => b - a)
1203
+ .map(([cat, cost]) => `| ${cat} | $${cost.toFixed(2)} |`)
1204
+ .join("\n");
1205
+ const formattedResult = `
1206
+ # Cost Rollup: ${epicTitle}
1207
+
1208
+ **Total Monthly Cost:** $${totalMonthlyCost.toLocaleString(undefined, { minimumFractionDigits: 2 })}/mo
1209
+ **Coverage:** ${estimated.length} of ${epicStories.length} stories estimated
1210
+ **Provider(s):** ${providerSummary}
1211
+
1212
+ ## Cost by Category
1213
+ | Category | Monthly Cost |
1214
+ |----------|-------------|
1215
+ ${categoryBreakdown}
1216
+
1217
+ ## Stories Without Estimates
1218
+ ${unestimated.length === 0
1219
+ ? "All stories have been estimated!"
1220
+ : unestimated.map((s) => `- ${s.title} (${s.id})`).join("\n")}
835
1221
  `;
836
1222
  return {
837
1223
  content: [{ type: "text", text: formattedResult.trim() }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elixium.ai/mcp-server",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "MCP Server for Elixium.ai",
6
6
  "publishConfig": {