@elixium.ai/mcp-server 0.1.9 → 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 +676 -289
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -164,19 +164,91 @@ const resolveBoardId = async () => {
164
164
  cachedBoardId = match.id;
165
165
  return cachedBoardId;
166
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
+ };
167
222
  const fetchStories = async () => {
168
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.
169
229
  const response = await client.get("/stories", {
170
- params: boardId ? { boardId } : undefined,
230
+ params: boardId && !isMainBoard ? { boardId } : undefined,
171
231
  });
172
- 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;
173
239
  };
174
240
  const fetchEpics = async () => {
175
241
  const boardId = await resolveBoardId();
242
+ const slug = normalizeBoardSlug(BOARD_SLUG);
243
+ const isMainBoard = slug === "main";
176
244
  const response = await client.get("/epics", {
177
- params: boardId ? { boardId } : undefined,
245
+ params: boardId && !isMainBoard ? { boardId } : undefined,
178
246
  });
179
- 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;
180
252
  };
181
253
  let cachedLaneStyle = null;
182
254
  const detectLaneStyle = async () => {
@@ -214,7 +286,15 @@ const normalizeLane = async (lane) => {
214
286
  const buildIterationContext = (stories, user = null) => {
215
287
  const currentIteration = stories.filter((story) => normalizeLaneForComparison(story?.lane) === "current");
216
288
  const backlog = stories.filter((story) => normalizeLaneForComparison(story?.lane) === "backlog");
217
- 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 };
218
298
  };
219
299
  const createServer = () => {
220
300
  const server = new Server({
@@ -227,302 +307,432 @@ const createServer = () => {
227
307
  });
228
308
  // Define Tools
229
309
  server.setRequestHandler(ListToolsRequestSchema, async () => {
230
- return {
231
- tools: [
232
- {
233
- name: "list_stories",
234
- description: "List all stories on the Elixium board",
235
- inputSchema: {
236
- type: "object",
237
- properties: {},
238
- },
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: {},
239
320
  },
240
- {
241
- name: "create_story",
242
- description: "Create a new story on the Elixium board",
243
- inputSchema: {
244
- type: "object",
245
- properties: {
246
- title: { type: "string", description: "Title of the story" },
247
- description: {
248
- type: "string",
249
- description: "Description of the story",
250
- },
251
- acceptanceCriteria: {
252
- type: "string",
253
- description: "Acceptance criteria in Given/When/Then format",
254
- },
255
- lane: {
256
- type: "string",
257
- description: "Lane to add the story to (case-insensitive)",
258
- enum: [
259
- "Backlog",
260
- "Icebox",
261
- "Current",
262
- "Done",
263
- "BACKLOG",
264
- "ICEBOX",
265
- "CURRENT",
266
- "DONE",
267
- ],
268
- },
269
- points: {
270
- type: "number",
271
- description: "Points (0, 1, 2, 3, 5, 8)",
272
- },
273
- requester: {
274
- type: "string",
275
- description: "Email of the person requesting this story. Defaults to ELIXIUM_USER_EMAIL env var or API key owner.",
276
- },
277
- },
278
- required: ["title"],
279
- },
321
+ },
322
+ {
323
+ name: "list_stories",
324
+ description: "List all stories on the Elixium board",
325
+ inputSchema: {
326
+ type: "object",
327
+ properties: {},
280
328
  },
281
- {
282
- name: "get_iteration_context",
283
- description: "Get the current iteration context (Current + Backlog) for AI planning",
284
- inputSchema: {
285
- type: "object",
286
- 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
+ },
287
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: {},
288
393
  },
289
- {
290
- name: "create_hypothesis",
291
- description: "Create a new assumption/hypothesis in the Icebox",
292
- inputSchema: {
293
- type: "object",
294
- properties: {
295
- title: { type: "string", description: "The assumption statement" },
296
- hypothesis: { type: "string", description: "Detailed hypothesis" },
297
- confidence_score: {
298
- type: "number",
299
- description: "Initial confidence (1-5)",
300
- minimum: 1,
301
- maximum: 5,
302
- },
303
- },
304
- 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
+ },
305
416
  },
417
+ required: ["title"],
306
418
  },
307
- {
308
- name: "list_objectives",
309
- description: "List objectives for the current workspace",
310
- inputSchema: {
311
- type: "object",
312
- 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
+ },
313
442
  },
443
+ required: ["epicId"],
314
444
  },
315
- {
316
- name: "record_learning",
317
- description: "Record a learning outcome for a completed story",
318
- inputSchema: {
319
- type: "object",
320
- properties: {
321
- storyId: { type: "string", description: "ID of the story" },
322
- outcome_summary: {
323
- type: "string",
324
- description: "What was learned?",
325
- },
326
- },
327
- 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
+ },
328
485
  },
486
+ required: ["storyId"],
329
487
  },
330
- {
331
- name: "list_epics",
332
- description: "List epics for the current board",
333
- inputSchema: {
334
- type: "object",
335
- 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
+ },
336
499
  },
500
+ required: ["storyId"],
337
501
  },
338
- {
339
- name: "create_epic",
340
- description: "Create a new epic for the current board",
341
- inputSchema: {
342
- type: "object",
343
- properties: {
344
- title: { type: "string", description: "Epic title" },
345
- description: { type: "string", description: "Epic description" },
346
- stage: {
347
- type: "string",
348
- description: "Roadmap stage",
349
- enum: ["in_progress", "next", "soon", "someday", "archived"],
350
- },
351
- hypothesis: { type: "string", description: "We believe that... (outcome hypothesis)" },
352
- successMetrics: { type: "string", description: "We'll know it works when... (success criteria)" },
353
- targetDate: { type: "number", description: "Unix timestamp - when to evaluate outcome (not ship date)" },
354
- outcomeStatus: {
355
- type: "string",
356
- description: "Outcome validation status",
357
- enum: ["exploring", "validating", "achieved", "abandoned"],
358
- },
359
- },
360
- 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
+ },
361
522
  },
523
+ required: ["storyId", "provider"],
362
524
  },
363
- {
364
- name: "update_epic",
365
- description: "Update an epic (title, description, stage, or outcome fields)",
366
- inputSchema: {
367
- type: "object",
368
- properties: {
369
- epicId: { type: "string", description: "ID of the epic" },
370
- title: { type: "string", description: "Epic title" },
371
- description: { type: "string", description: "Epic description" },
372
- stage: {
373
- type: "string",
374
- description: "Roadmap stage",
375
- enum: ["in_progress", "next", "soon", "someday", "archived"],
376
- },
377
- hypothesis: { type: "string", description: "We believe that... (outcome hypothesis)" },
378
- successMetrics: { type: "string", description: "We'll know it works when... (success criteria)" },
379
- targetDate: { type: "number", description: "Unix timestamp - when to evaluate outcome (not ship date)" },
380
- outcomeStatus: {
381
- type: "string",
382
- description: "Outcome validation status",
383
- enum: ["exploring", "validating", "achieved", "abandoned"],
384
- },
385
- },
386
- 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
+ },
387
536
  },
537
+ required: ["epicId"],
388
538
  },
389
- {
390
- name: "update_story",
391
- description: "Update fields on an existing story",
392
- inputSchema: {
393
- type: "object",
394
- properties: {
395
- storyId: { type: "string", description: "ID of the story" },
396
- title: { type: "string", description: "Updated title" },
397
- description: { type: "string", description: "Updated description" },
398
- lane: {
399
- type: "string",
400
- description: "Lane to move the story to (case-insensitive)",
401
- enum: [
402
- "Backlog",
403
- "Icebox",
404
- "Current",
405
- "Done",
406
- "BACKLOG",
407
- "ICEBOX",
408
- "CURRENT",
409
- "DONE",
410
- ],
411
- },
412
- points: {
413
- type: "number",
414
- description: "Updated points (0, 1, 2, 3, 5, 8)",
415
- },
416
- state: {
417
- type: "string",
418
- description: "Story state (unstarted, started, finished, delivered, accepted, rejected)",
419
- },
420
- outcome_summary: {
421
- type: "string",
422
- description: "Learning outcome summary",
423
- },
424
- acceptanceCriteria: {
425
- type: "string",
426
- description: "Acceptance criteria in Given/When/Then format",
427
- },
428
- },
429
- 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
+ },
430
566
  },
567
+ required: ["storyId"],
431
568
  },
432
- {
433
- name: "prepare_implementation",
434
- description: "Fetch all context for a story and format an implementation brief",
435
- inputSchema: {
436
- type: "object",
437
- properties: {
438
- storyId: {
439
- type: "string",
440
- description: "ID of the story to prepare",
441
- },
442
- },
443
- 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
+ },
444
589
  },
590
+ required: ["storyId", "testPlan"],
445
591
  },
446
- // TDD Workflow Tools
447
- {
448
- name: "start_story",
449
- 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).",
450
- inputSchema: {
451
- type: "object",
452
- properties: {
453
- storyId: {
454
- type: "string",
455
- description: "ID of the story to start",
456
- },
457
- branchPrefix: {
458
- type: "string",
459
- description: "Branch prefix (feat, fix, chore) - ignored if trunkBased=true",
460
- enum: ["feat", "fix", "chore"],
461
- },
462
- trunkBased: {
463
- type: "boolean",
464
- description: "If true, skip branch creation and commit directly to main. Recommended for small, well-tested changes.",
465
- },
466
- },
467
- 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
+ },
468
615
  },
616
+ required: ["storyId"],
469
617
  },
470
- {
471
- name: "propose_test_plan",
472
- description: "Submit a test plan for human review. Sets workflow_stage to tests_proposed. Implementation is BLOCKED until human approves.",
473
- inputSchema: {
474
- type: "object",
475
- properties: {
476
- storyId: {
477
- type: "string",
478
- description: "ID of the story",
479
- },
480
- testPlan: {
481
- type: "string",
482
- description: "Markdown test plan describing test strategy",
483
- },
484
- testFilePaths: {
485
- type: "array",
486
- items: { type: "string" },
487
- description: "Paths to created test files",
488
- },
489
- },
490
- 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
+ },
491
636
  },
637
+ required: ["title", "hypothesis", "confidence_score"],
492
638
  },
493
- {
494
- name: "submit_for_review",
495
- description: "Submit implementation for human review. Only works if tests are approved. Sets state to finished.",
496
- inputSchema: {
497
- type: "object",
498
- properties: {
499
- storyId: {
500
- type: "string",
501
- description: "ID of the story",
502
- },
503
- commitHash: {
504
- type: "string",
505
- description: "Latest commit SHA",
506
- },
507
- testResults: {
508
- type: "string",
509
- description: "Test output summary",
510
- },
511
- implementationNotes: {
512
- type: "string",
513
- description: "What was implemented",
514
- },
515
- },
516
- 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
+ },
517
651
  },
652
+ required: ["storyId", "outcome_summary"],
518
653
  },
519
- ],
520
- };
654
+ },
655
+ ] : [];
656
+ return { tools: [...baseTools, ...tddTools, ...learningLoopTools] };
521
657
  });
522
658
  // Handle Requests
523
659
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
524
660
  try {
525
- 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
+ }
526
736
  case "list_stories": {
527
737
  const stories = await fetchStories();
528
738
  return {
@@ -723,35 +933,68 @@ Here’s the smallest change that will validate it:
723
933
  // TDD Workflow Handlers
724
934
  case "start_story": {
725
935
  const args = request.params.arguments;
726
- const { storyId, branchPrefix, trunkBased } = args;
936
+ const { storyId, branchPrefix, trunkBased, autoMerge } = args;
727
937
  if (!storyId) {
728
938
  throw new Error("storyId is required");
729
939
  }
730
940
  const response = await client.post(`/stories/${storyId}/start`, {
731
941
  branchPrefix: branchPrefix || "feat",
732
- trunkBased: trunkBased || false,
942
+ trunkBased,
943
+ autoMerge,
733
944
  });
734
945
  const result = response.data;
735
- const isTrunk = trunkBased || result.branch === "main";
736
- const formattedResult = isTrunk ? `
946
+ const isTrunk = result.trunkBased;
947
+ const isAutoMerge = result.autoMerge;
948
+ let formattedResult;
949
+ if (isTrunk && !isAutoMerge) {
950
+ formattedResult = `
737
951
  # Story Started (Trunk-Based): ${storyId}
738
952
 
739
- **Mode:** 🚀 Direct to main (trunk-based development)
953
+ **Mode:** Direct to main (trunk-based development)
954
+ **Feature Flag:** \`${result.featureFlagName || "none"}\`
740
955
  **Workflow Stage:** ${result.workflow_stage}
741
956
 
742
957
  ## Acceptance Criteria
743
958
  ${result.acceptance_criteria || "No specific AC provided."}
744
959
 
745
960
  ## Trunk-Based Workflow
746
- 1. Write tests first
747
- 2. Call \`propose_test_plan\` and wait for approval
748
- 3. Implement (make tests pass)
749
- 4. Run \`npm run build\` to verify
750
- 5. Commit directly to main
751
- 6. 🚀 Auto-deploy on green CI
752
-
753
- > ⚠️ **TDD Workflow:** Write tests first, then call \`propose_test_plan\` before implementing.
754
- ` : `
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 = `
755
998
  # Story Started: ${storyId}
756
999
 
757
1000
  **Branch:** \`${result.branch}\`
@@ -763,8 +1006,9 @@ ${result.acceptance_criteria || "No specific AC provided."}
763
1006
  ## Next Steps
764
1007
  ${result.workflow_reminder}
765
1008
 
766
- > ⚠️ **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.
767
1010
  `;
1011
+ }
768
1012
  return {
769
1013
  content: [{ type: "text", text: formattedResult.trim() }],
770
1014
  };
@@ -813,6 +1057,25 @@ ${result.message}
813
1057
  implementationNotes,
814
1058
  });
815
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
+ }
816
1079
  const formattedResult = `
817
1080
  # Implementation Submitted for Review
818
1081
 
@@ -820,17 +1083,141 @@ ${result.message}
820
1083
  **Status:** ${result.status}
821
1084
  **Workflow Stage:** ${result.workflow_stage}
822
1085
  **State:** ${result.state}
1086
+ ${result.trunkBased ? `**Mode:** Trunk-based${isAutoMerge ? " + Auto-merge" : ""}` : ""}
1087
+ ${result.featureFlagName ? `**Feature Flag:** \`${result.featureFlagName}\`` : ""}
823
1088
 
824
1089
  ## Branch
825
- \`${result.branch || "No branch recorded"}\`
1090
+ \`${result.branch || "main (trunk-based)"}\`
826
1091
 
827
1092
  ## Commits
828
1093
  ${(result.commit_hashes || []).map((h) => `- \`${h}\``).join("\n") || "No commits recorded"}
829
-
1094
+ ${autoMergeSection}
830
1095
  ## Next Step
831
1096
  ${result.next_step}
832
1097
 
833
- > ✅ **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")}
834
1221
  `;
835
1222
  return {
836
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.9",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "MCP Server for Elixium.ai",
6
6
  "publishConfig": {