@hyperdrive.bot/bmad-workflow 1.0.18 → 1.0.19

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 (98) hide show
  1. package/dist/commands/config/show.js +8 -2
  2. package/dist/commands/decompose.js +26 -5
  3. package/dist/commands/epics/create.d.ts +1 -0
  4. package/dist/commands/mcp/add.d.ts +16 -0
  5. package/dist/commands/mcp/add.js +77 -0
  6. package/dist/commands/mcp/credential/get.d.ts +14 -0
  7. package/dist/commands/mcp/credential/get.js +35 -0
  8. package/dist/commands/mcp/credential/list.d.ts +17 -0
  9. package/dist/commands/mcp/credential/list.js +67 -0
  10. package/dist/commands/mcp/credential/remove.d.ts +18 -0
  11. package/dist/commands/mcp/credential/remove.js +84 -0
  12. package/dist/commands/mcp/credential/set.d.ts +16 -0
  13. package/dist/commands/mcp/credential/set.js +41 -0
  14. package/dist/commands/mcp/credential/validate.d.ts +12 -0
  15. package/dist/commands/mcp/credential/validate.js +150 -0
  16. package/dist/commands/mcp/list.d.ts +17 -0
  17. package/dist/commands/mcp/list.js +80 -0
  18. package/dist/commands/mcp/logs.d.ts +15 -0
  19. package/dist/commands/mcp/logs.js +64 -0
  20. package/dist/commands/mcp/preset.d.ts +15 -0
  21. package/dist/commands/mcp/preset.js +84 -0
  22. package/dist/commands/mcp/remove.d.ts +14 -0
  23. package/dist/commands/mcp/remove.js +36 -0
  24. package/dist/commands/mcp/start.d.ts +12 -0
  25. package/dist/commands/mcp/start.js +80 -0
  26. package/dist/commands/mcp/status.d.ts +30 -0
  27. package/dist/commands/mcp/status.js +180 -0
  28. package/dist/commands/mcp/stop.d.ts +12 -0
  29. package/dist/commands/mcp/stop.js +47 -0
  30. package/dist/commands/stories/create.d.ts +1 -0
  31. package/dist/commands/stories/develop.d.ts +1 -0
  32. package/dist/commands/stories/qa.js +5 -2
  33. package/dist/commands/stories/review.d.ts +124 -0
  34. package/dist/commands/stories/review.js +516 -0
  35. package/dist/commands/workflow.d.ts +8 -0
  36. package/dist/commands/workflow.js +110 -2
  37. package/dist/mcp/types.d.ts +99 -0
  38. package/dist/mcp/types.js +7 -0
  39. package/dist/mcp/utils/docker-utils.d.ts +56 -0
  40. package/dist/mcp/utils/docker-utils.js +108 -0
  41. package/dist/mcp/utils/template-loader.d.ts +21 -0
  42. package/dist/mcp/utils/template-loader.js +60 -0
  43. package/dist/models/agent-options.d.ts +10 -1
  44. package/dist/models/workflow-config.d.ts +77 -0
  45. package/dist/models/workflow-result.d.ts +7 -0
  46. package/dist/services/agents/claude-agent-runner.js +19 -3
  47. package/dist/services/file-system/path-resolver.d.ts +10 -0
  48. package/dist/services/file-system/path-resolver.js +12 -0
  49. package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
  50. package/dist/services/mcp/mcp-config-manager.js +146 -0
  51. package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
  52. package/dist/services/mcp/mcp-context-injector.js +168 -0
  53. package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
  54. package/dist/services/mcp/mcp-credential-manager.js +124 -0
  55. package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
  56. package/dist/services/mcp/mcp-health-checker.js +162 -0
  57. package/dist/services/mcp/types/health-types.d.ts +31 -0
  58. package/dist/services/mcp/types/health-types.js +7 -0
  59. package/dist/services/orchestration/dependency-graph-executor.js +1 -1
  60. package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
  61. package/dist/services/orchestration/task-decomposition-service.js +90 -36
  62. package/dist/services/orchestration/workflow-orchestrator.d.ts +54 -2
  63. package/dist/services/orchestration/workflow-orchestrator.js +303 -17
  64. package/dist/services/review/ai-review-scanner.d.ts +66 -0
  65. package/dist/services/review/ai-review-scanner.js +142 -0
  66. package/dist/services/review/coderabbit-scanner.d.ts +25 -0
  67. package/dist/services/review/coderabbit-scanner.js +31 -0
  68. package/dist/services/review/index.d.ts +20 -0
  69. package/dist/services/review/index.js +15 -0
  70. package/dist/services/review/lint-scanner.d.ts +46 -0
  71. package/dist/services/review/lint-scanner.js +172 -0
  72. package/dist/services/review/review-config.d.ts +62 -0
  73. package/dist/services/review/review-config.js +91 -0
  74. package/dist/services/review/review-phase-executor.d.ts +69 -0
  75. package/dist/services/review/review-phase-executor.js +152 -0
  76. package/dist/services/review/review-queue.d.ts +98 -0
  77. package/dist/services/review/review-queue.js +174 -0
  78. package/dist/services/review/review-reporter.d.ts +94 -0
  79. package/dist/services/review/review-reporter.js +386 -0
  80. package/dist/services/review/scanner-factory.d.ts +42 -0
  81. package/dist/services/review/scanner-factory.js +60 -0
  82. package/dist/services/review/self-heal-loop.d.ts +58 -0
  83. package/dist/services/review/self-heal-loop.js +132 -0
  84. package/dist/services/review/severity-classifier.d.ts +17 -0
  85. package/dist/services/review/severity-classifier.js +314 -0
  86. package/dist/services/review/tech-debt-tracker.d.ts +52 -0
  87. package/dist/services/review/tech-debt-tracker.js +245 -0
  88. package/dist/services/review/types.d.ts +93 -0
  89. package/dist/services/review/types.js +23 -0
  90. package/dist/services/validation/config-validator.d.ts +84 -0
  91. package/dist/services/validation/config-validator.js +78 -0
  92. package/dist/utils/credential-utils.d.ts +14 -0
  93. package/dist/utils/credential-utils.js +19 -0
  94. package/dist/utils/duration.d.ts +41 -0
  95. package/dist/utils/duration.js +89 -0
  96. package/dist/utils/shared-flags.d.ts +1 -0
  97. package/dist/utils/shared-flags.js +11 -2
  98. package/package.json +4 -2
@@ -0,0 +1,150 @@
1
+ /**
2
+ * MCP Credential Validate Command
3
+ *
4
+ * Validates all credentials required by enabled MCP servers by resolving them
5
+ * via the credential resolution chain and testing against live health checks.
6
+ */
7
+ import { Command } from '@oclif/core';
8
+ import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ import { loadServerTemplate } from '../../../mcp/utils/template-loader.js';
11
+ import { McpConfigManager } from '../../../services/mcp/mcp-config-manager.js';
12
+ import { McpCredentialManager } from '../../../services/mcp/mcp-credential-manager.js';
13
+ import { McpHealthChecker } from '../../../services/mcp/mcp-health-checker.js';
14
+ import { createLogger } from '../../../utils/logger.js';
15
+ const logger = createLogger({ namespace: 'commands:mcp:credential:validate' });
16
+ export default class CredentialValidate extends Command {
17
+ static description = 'Validate credentials for all enabled MCP servers requiring API keys';
18
+ static examples = [
19
+ '<%= config.bin %> mcp credential validate',
20
+ ];
21
+ async run() {
22
+ logger.info('Validating credentials for enabled servers');
23
+ const configManager = new McpConfigManager();
24
+ const credentialManager = new McpCredentialManager();
25
+ const enabledServers = configManager.getEnabledServers();
26
+ // Filter to servers that require API keys
27
+ const serversNeedingKeys = enabledServers.filter((s) => s.apiKeyRequired && s.enabled);
28
+ if (serversNeedingKeys.length === 0) {
29
+ this.log(chalk.dim('No enabled servers require API keys.'));
30
+ return;
31
+ }
32
+ this.log(chalk.bold('\nValidating MCP Server Credentials\n'));
33
+ let hasFailures = false;
34
+ const results = [];
35
+ for (const server of serversNeedingKeys) {
36
+ // Load the full template to get env var names
37
+ let template;
38
+ try {
39
+ template = loadServerTemplate(server.name);
40
+ }
41
+ catch {
42
+ results.push({
43
+ credentialKey: '?',
44
+ error: 'Template not found',
45
+ latency: null,
46
+ serverName: server.name,
47
+ status: 'fail',
48
+ });
49
+ hasFailures = true;
50
+ continue;
51
+ }
52
+ const envKeys = template.env ? Object.keys(template.env) : [];
53
+ if (envKeys.length === 0)
54
+ continue;
55
+ for (const envKey of envKeys) {
56
+ const resolved = credentialManager.resolve(envKey);
57
+ if (!resolved) {
58
+ results.push({
59
+ credentialKey: envKey,
60
+ error: 'Credential not configured',
61
+ latency: null,
62
+ serverName: server.name,
63
+ status: 'missing',
64
+ });
65
+ hasFailures = true;
66
+ continue;
67
+ }
68
+ // Credential resolves — test with health check
69
+ const spinner = ora({ text: `Checking ${server.name} (${envKey})...`, stream: process.stderr }).start();
70
+ try {
71
+ const healthChecker = new McpHealthChecker(configManager);
72
+ const serverWithHealth = {
73
+ enabled: true,
74
+ healthCheck: {
75
+ command: template.health_check.command,
76
+ method: template.health_check.method,
77
+ timeout: template.health_check.timeout,
78
+ },
79
+ name: server.name,
80
+ useCase: server.useCase,
81
+ };
82
+ const healthResult = await healthChecker.checkServer(serverWithHealth);
83
+ spinner.stop();
84
+ if (healthResult.status === 'healthy') {
85
+ results.push({
86
+ credentialKey: envKey,
87
+ error: null,
88
+ latency: healthResult.latency,
89
+ serverName: server.name,
90
+ status: 'pass',
91
+ });
92
+ }
93
+ else {
94
+ results.push({
95
+ credentialKey: envKey,
96
+ error: healthResult.error || `Status: ${healthResult.status}`,
97
+ latency: healthResult.latency,
98
+ serverName: server.name,
99
+ status: 'fail',
100
+ });
101
+ hasFailures = true;
102
+ }
103
+ }
104
+ catch (error) {
105
+ spinner.stop();
106
+ results.push({
107
+ credentialKey: envKey,
108
+ error: error.message,
109
+ latency: null,
110
+ serverName: server.name,
111
+ status: 'fail',
112
+ });
113
+ hasFailures = true;
114
+ }
115
+ }
116
+ }
117
+ // Display results
118
+ this.log(`${chalk.dim('Server'.padEnd(20))} ${chalk.dim('Key'.padEnd(20))} ${chalk.dim('Status'.padEnd(12))} ${chalk.dim('Latency')}`);
119
+ this.log(chalk.dim('─'.repeat(70)));
120
+ for (const r of results) {
121
+ let statusStr;
122
+ switch (r.status) {
123
+ case 'pass': {
124
+ statusStr = chalk.green('PASS');
125
+ break;
126
+ }
127
+ case 'missing': {
128
+ statusStr = chalk.yellow('MISSING');
129
+ break;
130
+ }
131
+ case 'fail': {
132
+ statusStr = chalk.red('FAIL');
133
+ break;
134
+ }
135
+ }
136
+ const latencyStr = r.latency !== null ? `${r.latency}ms` : '-';
137
+ this.log(`${r.serverName.padEnd(20)} ${r.credentialKey.padEnd(20)} ${statusStr.padEnd(22)} ${latencyStr}`);
138
+ if (r.error) {
139
+ this.log(chalk.dim(` ${r.error}`));
140
+ }
141
+ }
142
+ this.log('');
143
+ if (hasFailures) {
144
+ this.error(chalk.red('Credential validation failed. Fix missing or failing credentials above.'), { exit: 1 });
145
+ }
146
+ else {
147
+ this.log(chalk.green('All credential validations passed.'));
148
+ }
149
+ }
150
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * MCP List Command
3
+ *
4
+ * Displays all configured and available MCP servers with their status,
5
+ * API key requirements, and use case descriptions.
6
+ */
7
+ import { Command } from '@oclif/core';
8
+ import type { McpServerConfig } from '../../mcp/types.js';
9
+ /**
10
+ * Render the server list output
11
+ */
12
+ export declare function renderServerList(presetName: string, enabledServers: McpServerConfig[], availableTemplates: string[]): string;
13
+ export default class McpList extends Command {
14
+ static description: string;
15
+ static examples: string[];
16
+ run(): Promise<void>;
17
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * MCP List Command
3
+ *
4
+ * Displays all configured and available MCP servers with their status,
5
+ * API key requirements, and use case descriptions.
6
+ */
7
+ import { Command } from '@oclif/core';
8
+ import chalk from 'chalk';
9
+ import { listAvailableTemplates } from '../../mcp/utils/template-loader.js';
10
+ import { McpConfigManager } from '../../services/mcp/mcp-config-manager.js';
11
+ import { createLogger } from '../../utils/logger.js';
12
+ const logger = createLogger({ namespace: 'commands:mcp:list' });
13
+ /**
14
+ * Render the server list output
15
+ */
16
+ export function renderServerList(presetName, enabledServers, availableTemplates) {
17
+ const lines = [];
18
+ // Header with current preset
19
+ lines.push(chalk.bold('MCP Server Configuration'));
20
+ lines.push(chalk.dim('========================'));
21
+ lines.push(`Active preset: ${chalk.cyan(presetName)}`);
22
+ lines.push('');
23
+ // Build a map of configured servers
24
+ const configuredMap = new Map();
25
+ for (const server of enabledServers) {
26
+ configuredMap.set(server.name, server);
27
+ }
28
+ // Combine: configured servers + available-but-not-configured
29
+ const allServerNames = new Set([
30
+ ...enabledServers.map((s) => s.name),
31
+ ...availableTemplates,
32
+ ]);
33
+ if (allServerNames.size === 0) {
34
+ lines.push(chalk.dim(' No servers configured or available'));
35
+ return lines.join('\n');
36
+ }
37
+ // Table header
38
+ const maxNameLen = Math.max(...[...allServerNames].map((n) => n.length), 6);
39
+ const header = ` ${'Name'.padEnd(maxNameLen)} ${'Status'.padEnd(10)} ${'API Key'.padEnd(8)} Use Case`;
40
+ lines.push(chalk.bold(header));
41
+ lines.push(chalk.dim(` ${'─'.repeat(maxNameLen)} ${'─'.repeat(10)} ${'─'.repeat(8)} ${'─'.repeat(40)}`));
42
+ // Sort by name
43
+ const sortedNames = [...allServerNames].sort();
44
+ for (const name of sortedNames) {
45
+ const configured = configuredMap.get(name);
46
+ const paddedName = name.padEnd(maxNameLen);
47
+ if (configured) {
48
+ const status = configured.enabled
49
+ ? chalk.green('enabled'.padEnd(10))
50
+ : chalk.yellow('disabled'.padEnd(10));
51
+ const apiKey = configured.apiKeyRequired
52
+ ? chalk.yellow('yes'.padEnd(8))
53
+ : chalk.dim('no'.padEnd(8));
54
+ const useCase = configured.useCase || '';
55
+ lines.push(` ${paddedName} ${status} ${apiKey} ${useCase}`);
56
+ }
57
+ else {
58
+ const status = chalk.dim('available'.padEnd(10));
59
+ const apiKey = chalk.dim('—'.padEnd(8));
60
+ const useCase = chalk.dim('(not configured — run mcp add)');
61
+ lines.push(` ${paddedName} ${status} ${apiKey} ${useCase}`);
62
+ }
63
+ }
64
+ return lines.join('\n');
65
+ }
66
+ export default class McpList extends Command {
67
+ static description = 'List all configured and available MCP servers';
68
+ static examples = [
69
+ '<%= config.bin %> mcp list',
70
+ ];
71
+ async run() {
72
+ logger.info('Listing MCP servers');
73
+ const configManager = new McpConfigManager();
74
+ const enabledServers = configManager.getEnabledServers();
75
+ const presetName = configManager.getPresetName();
76
+ const availableTemplates = listAvailableTemplates();
77
+ const output = renderServerList(presetName, enabledServers, availableTemplates);
78
+ this.log(output);
79
+ }
80
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * MCP Logs Command
3
+ *
4
+ * Streams gateway container logs via docker compose logs,
5
+ * supporting a --follow flag for live tailing.
6
+ */
7
+ import { Command } from '@oclif/core';
8
+ export default class McpLogs extends Command {
9
+ static description: string;
10
+ static examples: string[];
11
+ static flags: {
12
+ follow: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ };
14
+ run(): Promise<void>;
15
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * MCP Logs Command
3
+ *
4
+ * Streams gateway container logs via docker compose logs,
5
+ * supporting a --follow flag for live tailing.
6
+ */
7
+ import { spawn } from 'node:child_process';
8
+ import { Command, Flags } from '@oclif/core';
9
+ import { dockerComposeFileExists, formatDockerError, getDockerComposeFilePath, isDockerAvailable, } from '../../mcp/utils/docker-utils.js';
10
+ import { createLogger } from '../../utils/logger.js';
11
+ const logger = createLogger({ namespace: 'commands:mcp:logs' });
12
+ export default class McpLogs extends Command {
13
+ static description = 'View MCP gateway container logs';
14
+ static examples = [
15
+ '<%= config.bin %> mcp logs',
16
+ '<%= config.bin %> mcp logs --follow',
17
+ '<%= config.bin %> mcp logs -f',
18
+ ];
19
+ static flags = {
20
+ follow: Flags.boolean({
21
+ char: 'f',
22
+ default: false,
23
+ description: 'Follow log output (live tail)',
24
+ }),
25
+ };
26
+ async run() {
27
+ const { flags } = await this.parse(McpLogs);
28
+ logger.info('Viewing MCP gateway logs (follow=%s)', flags.follow);
29
+ // 1. Check Docker availability
30
+ const dockerCheck = isDockerAvailable();
31
+ if (!dockerCheck.available) {
32
+ this.error(formatDockerError(dockerCheck), { exit: 1 });
33
+ return;
34
+ }
35
+ // 2. Verify compose file exists
36
+ const composePath = getDockerComposeFilePath();
37
+ if (!dockerComposeFileExists()) {
38
+ this.error(`Docker Compose file not found at: ${composePath}\nNo logs available — the gateway has not been initialized.`, { exit: 1 });
39
+ return;
40
+ }
41
+ // 3. Build command args
42
+ const args = ['compose', '-f', composePath, 'logs'];
43
+ if (flags.follow) {
44
+ args.push('--follow');
45
+ }
46
+ // 4. Spawn docker compose logs and pipe output
47
+ const child = spawn('docker', args, {
48
+ stdio: ['ignore', 'inherit', 'inherit'],
49
+ });
50
+ await new Promise((resolve, reject) => {
51
+ child.on('close', (code) => {
52
+ if (code === 0 || code === null) {
53
+ resolve();
54
+ }
55
+ else {
56
+ reject(new Error(`docker compose logs exited with code ${code}`));
57
+ }
58
+ });
59
+ child.on('error', (err) => {
60
+ reject(err);
61
+ });
62
+ });
63
+ }
64
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * MCP Preset Command
3
+ *
4
+ * Switches the active MCP gateway configuration to a named preset
5
+ * (minimal, research, full) and displays the resulting server list.
6
+ */
7
+ import { Command } from '@oclif/core';
8
+ export default class McpPreset extends Command {
9
+ static args: {
10
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
11
+ };
12
+ static description: string;
13
+ static examples: string[];
14
+ run(): Promise<void>;
15
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * MCP Preset Command
3
+ *
4
+ * Switches the active MCP gateway configuration to a named preset
5
+ * (minimal, research, full) and displays the resulting server list.
6
+ */
7
+ import { Args, Command } from '@oclif/core';
8
+ import chalk from 'chalk';
9
+ import { McpConfigManager } from '../../services/mcp/mcp-config-manager.js';
10
+ import { McpCredentialManager } from '../../services/mcp/mcp-credential-manager.js';
11
+ import { createLogger } from '../../utils/logger.js';
12
+ const logger = createLogger({ namespace: 'commands:mcp:preset' });
13
+ const VALID_PRESETS = ['minimal', 'research', 'full'];
14
+ export default class McpPreset extends Command {
15
+ static args = {
16
+ name: Args.string({
17
+ description: 'Preset name to apply (minimal, research, full)',
18
+ required: true,
19
+ }),
20
+ };
21
+ static description = 'Switch the active MCP configuration to a named preset';
22
+ static examples = [
23
+ '<%= config.bin %> mcp preset minimal',
24
+ '<%= config.bin %> mcp preset research',
25
+ '<%= config.bin %> mcp preset full',
26
+ ];
27
+ async run() {
28
+ const { args } = await this.parse(McpPreset);
29
+ const presetName = args.name;
30
+ logger.info('Switching to preset: %s', presetName);
31
+ // Validate preset name
32
+ if (!VALID_PRESETS.includes(presetName)) {
33
+ this.error(`Invalid preset '${presetName}'.\n\nAvailable presets: ${VALID_PRESETS.join(', ')}`, { exit: 1 });
34
+ return;
35
+ }
36
+ // Apply preset
37
+ const configManager = new McpConfigManager();
38
+ configManager.applyPreset(presetName);
39
+ this.log(chalk.green(`Switched to '${presetName}' preset`));
40
+ this.log('');
41
+ // Display resulting server list
42
+ const servers = configManager.getEnabledServers();
43
+ this.log(chalk.bold('Configured servers:'));
44
+ if (servers.length === 0) {
45
+ this.log(chalk.dim(' No servers in this preset'));
46
+ }
47
+ else {
48
+ for (const server of servers) {
49
+ const status = server.enabled ? chalk.green('enabled') : chalk.yellow('disabled');
50
+ const apiKey = server.apiKeyRequired ? chalk.dim(' (API key required)') : '';
51
+ this.log(` ${server.name}: ${status}${apiKey}`);
52
+ }
53
+ }
54
+ // Check for missing credentials
55
+ const credentialManager = new McpCredentialManager();
56
+ const missingCredentials = [];
57
+ for (const server of servers) {
58
+ if (server.apiKeyRequired) {
59
+ // Extract env var name from the server template in config
60
+ const enabledServerConfigs = configManager.getEnabledServers();
61
+ const serverConfig = enabledServerConfigs.find((s) => s.name === server.name);
62
+ if (serverConfig) {
63
+ // We need to check the env keys — the simple check is by known mapping
64
+ const knownEnvKeys = {
65
+ apify: 'APIFY_TOKEN',
66
+ exa: 'EXA_API_KEY',
67
+ };
68
+ const envKey = knownEnvKeys[server.name];
69
+ if (envKey && !credentialManager.resolve(envKey)) {
70
+ missingCredentials.push(envKey);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ if (missingCredentials.length > 0) {
76
+ this.log('');
77
+ this.log(chalk.yellow('Missing credentials:'));
78
+ for (const key of missingCredentials) {
79
+ this.log(chalk.yellow(` - ${key}`));
80
+ }
81
+ this.log(chalk.dim('\nSet credentials with: bmad-workflow mcp add <server> (will prompt for key)'));
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * MCP Remove Command
3
+ *
4
+ * Removes an MCP server from the active gateway configuration.
5
+ */
6
+ import { Command } from '@oclif/core';
7
+ export default class McpRemove extends Command {
8
+ static args: {
9
+ server: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
10
+ };
11
+ static description: string;
12
+ static examples: string[];
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * MCP Remove Command
3
+ *
4
+ * Removes an MCP server from the active gateway configuration.
5
+ */
6
+ import { Args, Command } from '@oclif/core';
7
+ import chalk from 'chalk';
8
+ import { McpConfigManager } from '../../services/mcp/mcp-config-manager.js';
9
+ import { createLogger } from '../../utils/logger.js';
10
+ const logger = createLogger({ namespace: 'commands:mcp:remove' });
11
+ export default class McpRemove extends Command {
12
+ static args = {
13
+ server: Args.string({
14
+ description: 'Name of the MCP server to remove',
15
+ required: true,
16
+ }),
17
+ };
18
+ static description = 'Remove an MCP server from the active gateway configuration';
19
+ static examples = [
20
+ '<%= config.bin %> mcp remove exa',
21
+ '<%= config.bin %> mcp remove apify',
22
+ ];
23
+ async run() {
24
+ const { args } = await this.parse(McpRemove);
25
+ const serverName = args.server;
26
+ logger.info('Removing MCP server: %s', serverName);
27
+ const configManager = new McpConfigManager();
28
+ const removed = configManager.removeServer(serverName);
29
+ if (removed) {
30
+ this.log(chalk.green(`Server '${serverName}' removed from configuration`));
31
+ }
32
+ else {
33
+ this.log(chalk.yellow(`Server '${serverName}' is not configured`));
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * MCP Start Command
3
+ *
4
+ * Launches the Docker MCP gateway via docker compose and polls the
5
+ * /health endpoint until healthy or 30s timeout is exceeded.
6
+ */
7
+ import { Command } from '@oclif/core';
8
+ export default class McpStart extends Command {
9
+ static description: string;
10
+ static examples: string[];
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * MCP Start Command
3
+ *
4
+ * Launches the Docker MCP gateway via docker compose and polls the
5
+ * /health endpoint until healthy or 30s timeout is exceeded.
6
+ */
7
+ import { execSync } from 'node:child_process';
8
+ import { Command } from '@oclif/core';
9
+ import chalk from 'chalk';
10
+ import { DEFAULT_GATEWAY_URL, HEALTH_CHECK_INTERVAL_MS, HEALTH_CHECK_TIMEOUT_MS, dockerComposeFileExists, formatDockerError, getDockerComposeFilePath, isDockerAvailable, } from '../../mcp/utils/docker-utils.js';
11
+ import { createLogger } from '../../utils/logger.js';
12
+ const logger = createLogger({ namespace: 'commands:mcp:start' });
13
+ export default class McpStart extends Command {
14
+ static description = 'Start the Docker MCP gateway and wait for it to become healthy';
15
+ static examples = [
16
+ '<%= config.bin %> mcp start',
17
+ ];
18
+ async run() {
19
+ logger.info('Starting MCP gateway');
20
+ // 1. Check Docker availability
21
+ const dockerCheck = isDockerAvailable();
22
+ if (!dockerCheck.available) {
23
+ this.error(formatDockerError(dockerCheck), { exit: 1 });
24
+ return;
25
+ }
26
+ // 2. Verify compose file exists
27
+ const composePath = getDockerComposeFilePath();
28
+ if (!dockerComposeFileExists()) {
29
+ this.error(`Docker Compose file not found at: ${composePath}\nRun 'bmad-workflow mcp init' first to generate the gateway configuration.`, { exit: 1 });
30
+ return;
31
+ }
32
+ // 3. Launch gateway
33
+ this.log(chalk.dim('Starting MCP gateway containers...'));
34
+ try {
35
+ execSync(`docker compose -f "${composePath}" up -d`, { stdio: 'pipe', timeout: 60_000 });
36
+ }
37
+ catch (error) {
38
+ const err = error;
39
+ const stderr = err.stderr ? err.stderr.toString() : err.message;
40
+ logger.error('Failed to start gateway: %s', stderr);
41
+ this.error(`Failed to start MCP gateway:\n${stderr}`, { exit: 1 });
42
+ return;
43
+ }
44
+ // 4. Health check polling
45
+ this.log(chalk.dim('Waiting for gateway to become healthy...'));
46
+ const healthUrl = `${DEFAULT_GATEWAY_URL}/health`;
47
+ const startTime = Date.now();
48
+ while (Date.now() - startTime < HEALTH_CHECK_TIMEOUT_MS) {
49
+ try {
50
+ const response = await fetch(healthUrl, { signal: AbortSignal.timeout(5000) });
51
+ if (response.ok) {
52
+ const elapsed = Date.now() - startTime;
53
+ this.log('');
54
+ this.log(chalk.green.bold(`✓ MCP Gateway started successfully (${elapsed}ms)`));
55
+ this.log(chalk.dim(` Gateway URL: ${DEFAULT_GATEWAY_URL}`));
56
+ this.log(chalk.dim(` Health: ${healthUrl}`));
57
+ logger.info('Gateway healthy after %dms', elapsed);
58
+ return;
59
+ }
60
+ }
61
+ catch {
62
+ // Health endpoint not ready yet — keep polling
63
+ }
64
+ await new Promise((resolve) => {
65
+ setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS);
66
+ });
67
+ }
68
+ // 5. Timeout
69
+ logger.warn('Health check timed out after %dms', HEALTH_CHECK_TIMEOUT_MS);
70
+ this.error([
71
+ `Gateway health check timed out after ${HEALTH_CHECK_TIMEOUT_MS / 1000}s.`,
72
+ '',
73
+ 'Troubleshooting:',
74
+ ` 1. Check container logs: bmad-workflow mcp logs`,
75
+ ` 2. Verify port 8080 is free: lsof -i :8080`,
76
+ ` 3. Check Docker status: docker ps`,
77
+ ` 4. Restart Docker daemon and try again`,
78
+ ].join('\n'), { exit: 1 });
79
+ }
80
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * MCP Status Command
3
+ *
4
+ * Displays a structured dashboard showing gateway health, per-server health
5
+ * with latency, credential status, and current active preset name.
6
+ */
7
+ import { Command } from '@oclif/core';
8
+ import type { CredentialEntry, HealthReport } from '../../mcp/types.js';
9
+ /**
10
+ * Render the status dashboard to the terminal
11
+ */
12
+ declare function renderDashboard(healthReport: HealthReport, presetName: string, credentials: CredentialEntry[]): string;
13
+ export default class McpStatus extends Command {
14
+ static description: string;
15
+ static examples: string[];
16
+ run(): Promise<void>;
17
+ /**
18
+ * Get credentials status — delegates to McpCredentialManager when available
19
+ */
20
+ private getCredentials;
21
+ /**
22
+ * Gather health report — delegates to McpHealthChecker when available
23
+ */
24
+ private getHealthReport;
25
+ /**
26
+ * Get current preset name — delegates to McpConfigManager when available
27
+ */
28
+ private getPresetName;
29
+ }
30
+ export { renderDashboard };