@hyperdrive.bot/bmad-workflow 1.0.17 → 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.
- package/dist/commands/config/show.js +8 -2
- package/dist/commands/decompose.js +26 -5
- package/dist/commands/epics/create.d.ts +1 -0
- package/dist/commands/mcp/add.d.ts +16 -0
- package/dist/commands/mcp/add.js +77 -0
- package/dist/commands/mcp/credential/get.d.ts +14 -0
- package/dist/commands/mcp/credential/get.js +35 -0
- package/dist/commands/mcp/credential/list.d.ts +17 -0
- package/dist/commands/mcp/credential/list.js +67 -0
- package/dist/commands/mcp/credential/remove.d.ts +18 -0
- package/dist/commands/mcp/credential/remove.js +84 -0
- package/dist/commands/mcp/credential/set.d.ts +16 -0
- package/dist/commands/mcp/credential/set.js +41 -0
- package/dist/commands/mcp/credential/validate.d.ts +12 -0
- package/dist/commands/mcp/credential/validate.js +150 -0
- package/dist/commands/mcp/list.d.ts +17 -0
- package/dist/commands/mcp/list.js +80 -0
- package/dist/commands/mcp/logs.d.ts +15 -0
- package/dist/commands/mcp/logs.js +64 -0
- package/dist/commands/mcp/preset.d.ts +15 -0
- package/dist/commands/mcp/preset.js +84 -0
- package/dist/commands/mcp/remove.d.ts +14 -0
- package/dist/commands/mcp/remove.js +36 -0
- package/dist/commands/mcp/start.d.ts +12 -0
- package/dist/commands/mcp/start.js +80 -0
- package/dist/commands/mcp/status.d.ts +30 -0
- package/dist/commands/mcp/status.js +180 -0
- package/dist/commands/mcp/stop.d.ts +12 -0
- package/dist/commands/mcp/stop.js +47 -0
- package/dist/commands/stories/create.d.ts +1 -0
- package/dist/commands/stories/develop.d.ts +1 -0
- package/dist/commands/stories/qa.js +34 -75
- package/dist/commands/stories/review.d.ts +124 -0
- package/dist/commands/stories/review.js +516 -0
- package/dist/commands/workflow.d.ts +89 -0
- package/dist/commands/workflow.js +487 -14
- package/dist/mcp/types.d.ts +99 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/utils/docker-utils.d.ts +56 -0
- package/dist/mcp/utils/docker-utils.js +108 -0
- package/dist/mcp/utils/template-loader.d.ts +21 -0
- package/dist/mcp/utils/template-loader.js +60 -0
- package/dist/models/agent-options.d.ts +10 -1
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/models/workflow-callbacks.d.ts +251 -0
- package/dist/models/workflow-callbacks.js +10 -0
- package/dist/models/workflow-config.d.ts +77 -0
- package/dist/models/workflow-result.d.ts +7 -0
- package/dist/services/WorkflowReporter.d.ts +165 -0
- package/dist/services/WorkflowReporter.js +691 -0
- package/dist/services/agents/claude-agent-runner.js +25 -4
- package/dist/services/file-system/path-resolver.d.ts +10 -0
- package/dist/services/file-system/path-resolver.js +12 -0
- package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
- package/dist/services/mcp/mcp-config-manager.js +146 -0
- package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
- package/dist/services/mcp/mcp-context-injector.js +168 -0
- package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
- package/dist/services/mcp/mcp-credential-manager.js +124 -0
- package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
- package/dist/services/mcp/mcp-health-checker.js +162 -0
- package/dist/services/mcp/types/health-types.d.ts +31 -0
- package/dist/services/mcp/types/health-types.js +7 -0
- package/dist/services/orchestration/dependency-graph-executor.js +1 -1
- package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
- package/dist/services/orchestration/task-decomposition-service.js +90 -36
- package/dist/services/orchestration/workflow-orchestrator.d.ts +87 -3
- package/dist/services/orchestration/workflow-orchestrator.js +1169 -289
- package/dist/services/review/ai-review-scanner.d.ts +66 -0
- package/dist/services/review/ai-review-scanner.js +142 -0
- package/dist/services/review/coderabbit-scanner.d.ts +25 -0
- package/dist/services/review/coderabbit-scanner.js +31 -0
- package/dist/services/review/index.d.ts +20 -0
- package/dist/services/review/index.js +15 -0
- package/dist/services/review/lint-scanner.d.ts +46 -0
- package/dist/services/review/lint-scanner.js +172 -0
- package/dist/services/review/review-config.d.ts +62 -0
- package/dist/services/review/review-config.js +91 -0
- package/dist/services/review/review-phase-executor.d.ts +69 -0
- package/dist/services/review/review-phase-executor.js +152 -0
- package/dist/services/review/review-queue.d.ts +98 -0
- package/dist/services/review/review-queue.js +174 -0
- package/dist/services/review/review-reporter.d.ts +94 -0
- package/dist/services/review/review-reporter.js +386 -0
- package/dist/services/review/scanner-factory.d.ts +42 -0
- package/dist/services/review/scanner-factory.js +60 -0
- package/dist/services/review/self-heal-loop.d.ts +58 -0
- package/dist/services/review/self-heal-loop.js +132 -0
- package/dist/services/review/severity-classifier.d.ts +17 -0
- package/dist/services/review/severity-classifier.js +314 -0
- package/dist/services/review/tech-debt-tracker.d.ts +52 -0
- package/dist/services/review/tech-debt-tracker.js +245 -0
- package/dist/services/review/types.d.ts +93 -0
- package/dist/services/review/types.js +23 -0
- package/dist/services/scaffolding/workflow-session-scaffolder.d.ts +182 -0
- package/dist/services/scaffolding/workflow-session-scaffolder.js +236 -0
- package/dist/services/validation/config-validator.d.ts +84 -0
- package/dist/services/validation/config-validator.js +78 -0
- package/dist/utils/colors.d.ts +10 -10
- package/dist/utils/colors.js +15 -15
- package/dist/utils/credential-utils.d.ts +14 -0
- package/dist/utils/credential-utils.js +19 -0
- package/dist/utils/duration.d.ts +41 -0
- package/dist/utils/duration.js +89 -0
- package/dist/utils/listr2-helpers.d.ts +216 -0
- package/dist/utils/listr2-helpers.js +334 -0
- package/dist/utils/shared-flags.d.ts +1 -0
- package/dist/utils/shared-flags.js +11 -2
- package/package.json +6 -3
|
@@ -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 };
|