@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
|
@@ -155,22 +155,43 @@ export class ClaudeAgentRunner {
|
|
|
155
155
|
tempDir = await mkdtemp(join(tmpdir(), 'claude-prompt-'));
|
|
156
156
|
tempFile = join(tempDir, 'prompt.txt');
|
|
157
157
|
await writeFile(tempFile, prompt, 'utf8');
|
|
158
|
+
// Write system prompt to temp file if provided (Claude-only feature)
|
|
159
|
+
// Uses --system-prompt (full replacement) instead of --append-system-prompt
|
|
160
|
+
// to prevent the default Claude Code system prompt from competing with
|
|
161
|
+
// our output format instructions (e.g., YAML-only output).
|
|
162
|
+
let systemPromptArg = '';
|
|
163
|
+
if (options.systemPrompt) {
|
|
164
|
+
const systemPromptFile = join(tempDir, 'system-prompt.txt');
|
|
165
|
+
await writeFile(systemPromptFile, options.systemPrompt, 'utf8');
|
|
166
|
+
systemPromptArg = `--system-prompt "$(cat '${systemPromptFile}')"`;
|
|
167
|
+
}
|
|
158
168
|
// Build command with temp file
|
|
159
169
|
const flags = this.config.flags.join(' ');
|
|
160
170
|
const modelArg = options.model ? `${this.config.modelFlag} ${options.model}` : '';
|
|
161
|
-
const
|
|
171
|
+
const extraFlags = options.flags ? options.flags.join(' ') : '';
|
|
172
|
+
const command = `${this.config.command} ${flags} ${modelArg} ${extraFlags} ${systemPromptArg} < "${tempFile}"`.replace(/\s+/g, ' ').trim();
|
|
162
173
|
// Log the command being executed
|
|
163
174
|
this.logger.info({
|
|
164
175
|
promptLength: prompt.length,
|
|
165
176
|
tempFile,
|
|
166
177
|
}, 'Executing command with temp file');
|
|
167
178
|
// Use exec instead of spawn for better shell compatibility
|
|
168
|
-
|
|
169
|
-
|
|
179
|
+
// Note: setting CLAUDECODE to undefined in a spread does NOT remove it —
|
|
180
|
+
// Node coerces undefined to the string "undefined", which is still truthy.
|
|
181
|
+
// We must delete the key from the env object to fully unset it.
|
|
182
|
+
const env = { ...process.env };
|
|
183
|
+
delete env.CLAUDECODE;
|
|
184
|
+
const execOptions = {
|
|
185
|
+
env,
|
|
170
186
|
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
171
187
|
shell: process.env.SHELL || '/bin/bash',
|
|
172
188
|
timeout,
|
|
173
|
-
}
|
|
189
|
+
};
|
|
190
|
+
if (options.cwd) {
|
|
191
|
+
execOptions.cwd = options.cwd;
|
|
192
|
+
this.logger.info({ cwd: options.cwd }, 'Setting working directory for agent');
|
|
193
|
+
}
|
|
194
|
+
const { stderr, stdout } = await execAsync(command, execOptions);
|
|
174
195
|
stdoutData = stdout;
|
|
175
196
|
stderrData = stderr;
|
|
176
197
|
const duration = Date.now() - startTime;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* All commands use this service to get correct directories for epics, stories, and PRD files.
|
|
6
6
|
*/
|
|
7
7
|
import type pino from 'pino';
|
|
8
|
+
import type { ReviewConfig } from '../validation/config-validator.js';
|
|
8
9
|
import { FileManager } from './file-manager.js';
|
|
9
10
|
/**
|
|
10
11
|
* PathResolver service for resolving and validating file paths
|
|
@@ -118,6 +119,15 @@ export declare class PathResolver {
|
|
|
118
119
|
* // Returns: '/path/to/project/docs/qa/stories'
|
|
119
120
|
*/
|
|
120
121
|
getQaStoryDir(): string;
|
|
122
|
+
/**
|
|
123
|
+
* Get the review configuration section from core-config.yaml
|
|
124
|
+
*
|
|
125
|
+
* Returns the parsed review configuration if present, or undefined if not.
|
|
126
|
+
* The review section contains scanner, severity, self-heal, and path-rule settings.
|
|
127
|
+
*
|
|
128
|
+
* @returns Review configuration object or undefined
|
|
129
|
+
*/
|
|
130
|
+
getReviewConfig(): ReviewConfig | undefined;
|
|
121
131
|
/**
|
|
122
132
|
* Get the story directory path
|
|
123
133
|
*
|
|
@@ -154,6 +154,18 @@ export class PathResolver {
|
|
|
154
154
|
this.logger.debug('Getting QA story directory: %s', paths.qaStoryDir);
|
|
155
155
|
return paths.qaStoryDir;
|
|
156
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Get the review configuration section from core-config.yaml
|
|
159
|
+
*
|
|
160
|
+
* Returns the parsed review configuration if present, or undefined if not.
|
|
161
|
+
* The review section contains scanner, severity, self-heal, and path-rule settings.
|
|
162
|
+
*
|
|
163
|
+
* @returns Review configuration object or undefined
|
|
164
|
+
*/
|
|
165
|
+
getReviewConfig() {
|
|
166
|
+
const config = this.loadConfig();
|
|
167
|
+
return config.review;
|
|
168
|
+
}
|
|
157
169
|
/**
|
|
158
170
|
* Get the story directory path
|
|
159
171
|
*
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Configuration Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages the active MCP gateway configuration at ~/.bmad/mcp/config.yaml.
|
|
5
|
+
* Provides methods to add/remove servers, apply presets, and query config state.
|
|
6
|
+
*/
|
|
7
|
+
import type { IConfigManager, McpServerConfig, ServerTemplate } from '../../mcp/types.js';
|
|
8
|
+
export declare class McpConfigManager implements IConfigManager {
|
|
9
|
+
private config;
|
|
10
|
+
private readonly configPath;
|
|
11
|
+
constructor(configPath?: string);
|
|
12
|
+
/**
|
|
13
|
+
* Add a server template to the active configuration
|
|
14
|
+
*/
|
|
15
|
+
addServer(template: ServerTemplate): void;
|
|
16
|
+
/**
|
|
17
|
+
* Apply a named preset to the active configuration
|
|
18
|
+
*/
|
|
19
|
+
applyPreset(presetName: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Get all configured servers with their enabled/disabled status
|
|
22
|
+
*/
|
|
23
|
+
getEnabledServers(): McpServerConfig[];
|
|
24
|
+
/**
|
|
25
|
+
* Get the configured gateway URL
|
|
26
|
+
*/
|
|
27
|
+
getGatewayUrl(): string;
|
|
28
|
+
/**
|
|
29
|
+
* Get the name of the currently active preset
|
|
30
|
+
*/
|
|
31
|
+
getPresetName(): string;
|
|
32
|
+
/**
|
|
33
|
+
* Remove a server from the active configuration
|
|
34
|
+
*
|
|
35
|
+
* @returns true if server was found and removed, false if not found
|
|
36
|
+
*/
|
|
37
|
+
removeServer(name: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Load the active config from disk
|
|
40
|
+
*/
|
|
41
|
+
private loadConfig;
|
|
42
|
+
/**
|
|
43
|
+
* Load a preset YAML file
|
|
44
|
+
*/
|
|
45
|
+
private loadPreset;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve preset inheritance chain (e.g., full extends research extends minimal)
|
|
48
|
+
*/
|
|
49
|
+
private resolvePresetInheritance;
|
|
50
|
+
/**
|
|
51
|
+
* Save the active config to disk
|
|
52
|
+
*/
|
|
53
|
+
private saveConfig;
|
|
54
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Configuration Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages the active MCP gateway configuration at ~/.bmad/mcp/config.yaml.
|
|
5
|
+
* Provides methods to add/remove servers, apply presets, and query config state.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { dirname, join, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import yaml from 'js-yaml';
|
|
12
|
+
import { createLogger } from '../../utils/logger.js';
|
|
13
|
+
const logger = createLogger({ namespace: 'services:mcp:config-manager' });
|
|
14
|
+
const CONFIG_DIR = join(homedir(), '.bmad', 'mcp');
|
|
15
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.yaml');
|
|
16
|
+
export class McpConfigManager {
|
|
17
|
+
config;
|
|
18
|
+
configPath;
|
|
19
|
+
constructor(configPath) {
|
|
20
|
+
this.configPath = configPath || CONFIG_FILE;
|
|
21
|
+
this.config = this.loadConfig();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Add a server template to the active configuration
|
|
25
|
+
*/
|
|
26
|
+
addServer(template) {
|
|
27
|
+
logger.info('Adding server: %s', template.name);
|
|
28
|
+
this.config.servers[template.name] = template;
|
|
29
|
+
this.saveConfig();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Apply a named preset to the active configuration
|
|
33
|
+
*/
|
|
34
|
+
applyPreset(presetName) {
|
|
35
|
+
logger.info('Applying preset: %s', presetName);
|
|
36
|
+
const validPresets = ['minimal', 'research', 'full'];
|
|
37
|
+
if (!validPresets.includes(presetName)) {
|
|
38
|
+
throw new Error(`Invalid preset '${presetName}'. Available presets: ${validPresets.join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
const presetConfig = this.loadPreset(presetName);
|
|
41
|
+
this.config = {
|
|
42
|
+
preset: presetName,
|
|
43
|
+
servers: {},
|
|
44
|
+
};
|
|
45
|
+
// Resolve preset inheritance
|
|
46
|
+
const resolvedServers = this.resolvePresetInheritance(presetConfig);
|
|
47
|
+
for (const [name, serverConfig] of Object.entries(resolvedServers)) {
|
|
48
|
+
this.config.servers[name] = { ...serverConfig, name };
|
|
49
|
+
}
|
|
50
|
+
this.saveConfig();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get all configured servers with their enabled/disabled status
|
|
54
|
+
*/
|
|
55
|
+
getEnabledServers() {
|
|
56
|
+
return Object.values(this.config.servers).map((server) => ({
|
|
57
|
+
apiKeyRequired: server.api_key_required,
|
|
58
|
+
enabled: server.enabled,
|
|
59
|
+
name: server.name,
|
|
60
|
+
useCase: server.use_case,
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get the configured gateway URL
|
|
65
|
+
*/
|
|
66
|
+
getGatewayUrl() {
|
|
67
|
+
return 'http://localhost:8080';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get the name of the currently active preset
|
|
71
|
+
*/
|
|
72
|
+
getPresetName() {
|
|
73
|
+
return this.config.preset || 'custom';
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Remove a server from the active configuration
|
|
77
|
+
*
|
|
78
|
+
* @returns true if server was found and removed, false if not found
|
|
79
|
+
*/
|
|
80
|
+
removeServer(name) {
|
|
81
|
+
logger.info('Removing server: %s', name);
|
|
82
|
+
if (this.config.servers[name]) {
|
|
83
|
+
delete this.config.servers[name];
|
|
84
|
+
this.saveConfig();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Load the active config from disk
|
|
91
|
+
*/
|
|
92
|
+
loadConfig() {
|
|
93
|
+
try {
|
|
94
|
+
if (existsSync(this.configPath)) {
|
|
95
|
+
const content = readFileSync(this.configPath, 'utf8');
|
|
96
|
+
const parsed = yaml.load(content);
|
|
97
|
+
if (parsed && typeof parsed === 'object') {
|
|
98
|
+
return {
|
|
99
|
+
preset: parsed.preset,
|
|
100
|
+
servers: parsed.servers || {},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
logger.warn('Failed to load config from %s: %s', this.configPath, error.message);
|
|
107
|
+
}
|
|
108
|
+
return { servers: {} };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Load a preset YAML file
|
|
112
|
+
*/
|
|
113
|
+
loadPreset(presetName) {
|
|
114
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
115
|
+
const presetPath = resolve(currentDir, '..', '..', 'mcp', 'presets', `${presetName}.yaml`);
|
|
116
|
+
if (!existsSync(presetPath)) {
|
|
117
|
+
throw new Error(`Preset file not found: ${presetPath}`);
|
|
118
|
+
}
|
|
119
|
+
const content = readFileSync(presetPath, 'utf8');
|
|
120
|
+
return yaml.load(content);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolve preset inheritance chain (e.g., full extends research extends minimal)
|
|
124
|
+
*/
|
|
125
|
+
resolvePresetInheritance(presetConfig) {
|
|
126
|
+
let servers = {};
|
|
127
|
+
if (presetConfig.extends) {
|
|
128
|
+
const parentConfig = this.loadPreset(presetConfig.extends);
|
|
129
|
+
servers = this.resolvePresetInheritance(parentConfig);
|
|
130
|
+
}
|
|
131
|
+
// Merge current preset servers (overrides parent)
|
|
132
|
+
return { ...servers, ...presetConfig.servers };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Save the active config to disk
|
|
136
|
+
*/
|
|
137
|
+
saveConfig() {
|
|
138
|
+
const dir = dirname(this.configPath);
|
|
139
|
+
if (!existsSync(dir)) {
|
|
140
|
+
mkdirSync(dir, { recursive: true });
|
|
141
|
+
}
|
|
142
|
+
const content = yaml.dump(this.config, { sortKeys: true });
|
|
143
|
+
writeFileSync(this.configPath, content, 'utf8');
|
|
144
|
+
logger.info('Config saved to %s', this.configPath);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Context Injector
|
|
3
|
+
*
|
|
4
|
+
* Provides MCP tool discovery context to headless pipeline agents.
|
|
5
|
+
* Returns formatted instructions that are prepended to agent prompts,
|
|
6
|
+
* enabling agents to discover and use external MCP tools.
|
|
7
|
+
*
|
|
8
|
+
* Interface consumed by WorkflowOrchestrator (Story 3.3).
|
|
9
|
+
* Concrete implementation provided by Story 3.1.
|
|
10
|
+
*/
|
|
11
|
+
import type { IConfigManager, IHealthChecker } from '../../mcp/types.js';
|
|
12
|
+
/**
|
|
13
|
+
* A single MCP tool entry describing a server and its available tools
|
|
14
|
+
*/
|
|
15
|
+
export interface McpToolEntry {
|
|
16
|
+
description: string;
|
|
17
|
+
name: string;
|
|
18
|
+
tools: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* MCP context returned by the injector for a specific agent/phase combination
|
|
22
|
+
*/
|
|
23
|
+
export interface McpContext {
|
|
24
|
+
/** Formatted tool descriptions from healthy MCP servers */
|
|
25
|
+
availableTools: McpToolEntry[];
|
|
26
|
+
/** The text to prepend to the agent prompt — empty string means no injection */
|
|
27
|
+
instructions: string;
|
|
28
|
+
}
|
|
29
|
+
/** Valid workflow phase identifiers for MCP context injection */
|
|
30
|
+
export type McpPhase = 'dev' | 'epic' | 'qa' | 'review' | 'story';
|
|
31
|
+
/**
|
|
32
|
+
* Interface for injecting MCP tool context into agent prompts
|
|
33
|
+
*
|
|
34
|
+
* Implementations handle: phase-server mapping, health checks,
|
|
35
|
+
* 500-token budget enforcement, and graceful degradation.
|
|
36
|
+
*/
|
|
37
|
+
export interface McpContextInjector {
|
|
38
|
+
/**
|
|
39
|
+
* Get MCP context for a specific agent type and workflow phase
|
|
40
|
+
*
|
|
41
|
+
* @param agentType - The type of agent being spawned (e.g., 'architect', 'sm', 'dev', 'qa')
|
|
42
|
+
* @param phase - The workflow phase (epic, story, dev, review, qa)
|
|
43
|
+
* @returns MCP context with instructions to prepend (empty instructions = no injection)
|
|
44
|
+
*/
|
|
45
|
+
getContextForAgent(agentType: string, phase: McpPhase): Promise<McpContext>;
|
|
46
|
+
}
|
|
47
|
+
/** Maximum token budget for MCP instructions (NFR3) */
|
|
48
|
+
export declare const MAX_TOKEN_BUDGET = 500;
|
|
49
|
+
/**
|
|
50
|
+
* McpContextInjectorImpl generates phase-aware MCP instructions for agent prompts.
|
|
51
|
+
*
|
|
52
|
+
* - Queries health status of configured servers
|
|
53
|
+
* - Filters to servers relevant for the workflow phase
|
|
54
|
+
* - Generates compact instructions under 500-token budget
|
|
55
|
+
* - Gracefully degrades: never throws, returns empty context on any error
|
|
56
|
+
*/
|
|
57
|
+
export declare class McpContextInjectorImpl implements McpContextInjector {
|
|
58
|
+
private readonly configManager;
|
|
59
|
+
private readonly healthChecker;
|
|
60
|
+
constructor(configManager: IConfigManager, healthChecker: IHealthChecker);
|
|
61
|
+
/**
|
|
62
|
+
* Get MCP context for a specific agent type and workflow phase
|
|
63
|
+
*
|
|
64
|
+
* @param agentType - The type of agent being spawned
|
|
65
|
+
* @param phase - The workflow phase
|
|
66
|
+
* @returns MCP context with instructions (empty on any error)
|
|
67
|
+
*/
|
|
68
|
+
getContextForAgent(agentType: string, phase: McpPhase): Promise<McpContext>;
|
|
69
|
+
/**
|
|
70
|
+
* Build compact MCP instructions for agent prompt injection
|
|
71
|
+
*
|
|
72
|
+
* @param tools - Available tool entries
|
|
73
|
+
* @param gatewayUrl - MCP gateway URL
|
|
74
|
+
* @returns Formatted instructions string (empty if no tools)
|
|
75
|
+
*/
|
|
76
|
+
private buildMcpInstructions;
|
|
77
|
+
/**
|
|
78
|
+
* Filter servers to those allowed for the given workflow phase
|
|
79
|
+
*
|
|
80
|
+
* @param servers - Available healthy servers
|
|
81
|
+
* @param phase - Workflow phase
|
|
82
|
+
* @returns Servers relevant to the phase
|
|
83
|
+
*/
|
|
84
|
+
private selectServersForPhase;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Approximate token count using chars/4 heuristic
|
|
88
|
+
*
|
|
89
|
+
* @param text - Text to count tokens for
|
|
90
|
+
* @returns Approximate token count
|
|
91
|
+
*/
|
|
92
|
+
export declare function countTokens(text: string): number;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Context Injector
|
|
3
|
+
*
|
|
4
|
+
* Provides MCP tool discovery context to headless pipeline agents.
|
|
5
|
+
* Returns formatted instructions that are prepended to agent prompts,
|
|
6
|
+
* enabling agents to discover and use external MCP tools.
|
|
7
|
+
*
|
|
8
|
+
* Interface consumed by WorkflowOrchestrator (Story 3.3).
|
|
9
|
+
* Concrete implementation provided by Story 3.1.
|
|
10
|
+
*/
|
|
11
|
+
import { createLogger } from '../../utils/logger.js';
|
|
12
|
+
const logger = createLogger({ namespace: 'services:mcp:context-injector' });
|
|
13
|
+
// --- Constants ---
|
|
14
|
+
/** Maximum token budget for MCP instructions (NFR3) */
|
|
15
|
+
export const MAX_TOKEN_BUDGET = 500;
|
|
16
|
+
/** Empty context returned on any error or when no servers are available */
|
|
17
|
+
const EMPTY_CONTEXT = { availableTools: [], instructions: '' };
|
|
18
|
+
/**
|
|
19
|
+
* Phase-to-server mapping. Defines which MCP servers are relevant per workflow phase.
|
|
20
|
+
* Not user-configurable — fixed per architecture decision.
|
|
21
|
+
*/
|
|
22
|
+
const PHASE_SERVER_MAP = {
|
|
23
|
+
dev: ['context7', 'playwright'],
|
|
24
|
+
epic: ['context7', 'exa'],
|
|
25
|
+
qa: ['context7', 'playwright'],
|
|
26
|
+
review: ['context7'],
|
|
27
|
+
story: ['context7'],
|
|
28
|
+
};
|
|
29
|
+
/** Default servers for unrecognized phases */
|
|
30
|
+
const DEFAULT_SERVERS = ['context7'];
|
|
31
|
+
/**
|
|
32
|
+
* Known tool lists per server (used for instructions generation).
|
|
33
|
+
* Maps server name → representative tool names.
|
|
34
|
+
*/
|
|
35
|
+
const SERVER_TOOLS = {
|
|
36
|
+
context7: ['resolve-library-id', 'get-library-docs'],
|
|
37
|
+
exa: ['web_search_exa', 'research_paper_search', 'company_research', 'competitor_finder'],
|
|
38
|
+
playwright: ['puppeteer_navigate', 'puppeteer_screenshot', 'puppeteer_click', 'puppeteer_fill'],
|
|
39
|
+
};
|
|
40
|
+
/** Use-case descriptions per server */
|
|
41
|
+
const SERVER_USE_CASES = {
|
|
42
|
+
context7: 'Library documentation lookup (current API references)',
|
|
43
|
+
exa: 'Web search, competitive analysis, academic research',
|
|
44
|
+
playwright: 'Browser automation, screenshots, E2E testing',
|
|
45
|
+
};
|
|
46
|
+
/** Tool selection priority text — must be included in all non-empty instructions */
|
|
47
|
+
const TOOL_PRIORITY_TEXT = `ALWAYS use native tools for local operations:
|
|
48
|
+
Read files → Read tool, Write files → Write/Edit, Search → Grep/Glob, Run commands → Bash
|
|
49
|
+
ONLY use MCP tools for external operations.`;
|
|
50
|
+
// --- Implementation ---
|
|
51
|
+
/**
|
|
52
|
+
* McpContextInjectorImpl generates phase-aware MCP instructions for agent prompts.
|
|
53
|
+
*
|
|
54
|
+
* - Queries health status of configured servers
|
|
55
|
+
* - Filters to servers relevant for the workflow phase
|
|
56
|
+
* - Generates compact instructions under 500-token budget
|
|
57
|
+
* - Gracefully degrades: never throws, returns empty context on any error
|
|
58
|
+
*/
|
|
59
|
+
export class McpContextInjectorImpl {
|
|
60
|
+
configManager;
|
|
61
|
+
healthChecker;
|
|
62
|
+
constructor(configManager, healthChecker) {
|
|
63
|
+
this.configManager = configManager;
|
|
64
|
+
this.healthChecker = healthChecker;
|
|
65
|
+
logger.debug('McpContextInjectorImpl initialized');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get MCP context for a specific agent type and workflow phase
|
|
69
|
+
*
|
|
70
|
+
* @param agentType - The type of agent being spawned
|
|
71
|
+
* @param phase - The workflow phase
|
|
72
|
+
* @returns MCP context with instructions (empty on any error)
|
|
73
|
+
*/
|
|
74
|
+
async getContextForAgent(agentType, phase) {
|
|
75
|
+
try {
|
|
76
|
+
logger.debug('Getting context for agent=%s phase=%s', agentType, phase);
|
|
77
|
+
// 1. Get configured servers
|
|
78
|
+
const configuredServers = this.configManager.getEnabledServers();
|
|
79
|
+
// 2. Check health of all servers
|
|
80
|
+
const healthReport = await this.healthChecker.checkAll();
|
|
81
|
+
// 3. Filter to only healthy servers
|
|
82
|
+
const healthyServerNames = new Set(healthReport.servers
|
|
83
|
+
.filter((s) => s.status === 'healthy')
|
|
84
|
+
.map((s) => s.name));
|
|
85
|
+
const healthyServers = configuredServers.filter((s) => s.enabled && healthyServerNames.has(s.name));
|
|
86
|
+
// 4. Apply phase-server mapping
|
|
87
|
+
const phaseServers = this.selectServersForPhase(healthyServers, phase);
|
|
88
|
+
if (phaseServers.length === 0) {
|
|
89
|
+
logger.debug('No healthy servers available for phase=%s', phase);
|
|
90
|
+
return EMPTY_CONTEXT;
|
|
91
|
+
}
|
|
92
|
+
// 5. Build tool entries
|
|
93
|
+
const availableTools = phaseServers.map((server) => ({
|
|
94
|
+
description: SERVER_USE_CASES[server.name] || server.useCase || server.name,
|
|
95
|
+
name: server.name,
|
|
96
|
+
tools: SERVER_TOOLS[server.name] || [],
|
|
97
|
+
}));
|
|
98
|
+
// 6. Build instructions with token budget enforcement
|
|
99
|
+
const gatewayUrl = this.configManager.getGatewayUrl();
|
|
100
|
+
const instructions = this.buildMcpInstructions(availableTools, gatewayUrl);
|
|
101
|
+
logger.debug('Context generated: %d servers, %d tokens', availableTools.length, countTokens(instructions));
|
|
102
|
+
return { availableTools, instructions };
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
logger.warn('Failed to get MCP context (returning empty): %s', error.message);
|
|
106
|
+
return EMPTY_CONTEXT;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Build compact MCP instructions for agent prompt injection
|
|
111
|
+
*
|
|
112
|
+
* @param tools - Available tool entries
|
|
113
|
+
* @param gatewayUrl - MCP gateway URL
|
|
114
|
+
* @returns Formatted instructions string (empty if no tools)
|
|
115
|
+
*/
|
|
116
|
+
buildMcpInstructions(tools, gatewayUrl) {
|
|
117
|
+
if (tools.length === 0) {
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
let serverLines = tools.map((t) => `- **${t.name}**: ${t.description} [${t.tools.join(', ')}]`);
|
|
121
|
+
let instructions = assembleInstructions(serverLines, gatewayUrl);
|
|
122
|
+
// Enforce token budget — drop lowest-priority servers if over budget
|
|
123
|
+
while (countTokens(instructions) > MAX_TOKEN_BUDGET && serverLines.length > 1) {
|
|
124
|
+
serverLines = serverLines.slice(0, -1);
|
|
125
|
+
instructions = assembleInstructions(serverLines, gatewayUrl);
|
|
126
|
+
}
|
|
127
|
+
// If still over budget with one server, truncate
|
|
128
|
+
if (countTokens(instructions) > MAX_TOKEN_BUDGET && serverLines.length === 1) {
|
|
129
|
+
const maxChars = MAX_TOKEN_BUDGET * 4 - 100;
|
|
130
|
+
instructions = instructions.slice(0, maxChars);
|
|
131
|
+
}
|
|
132
|
+
return instructions;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Filter servers to those allowed for the given workflow phase
|
|
136
|
+
*
|
|
137
|
+
* @param servers - Available healthy servers
|
|
138
|
+
* @param phase - Workflow phase
|
|
139
|
+
* @returns Servers relevant to the phase
|
|
140
|
+
*/
|
|
141
|
+
selectServersForPhase(servers, phase) {
|
|
142
|
+
const allowedNames = PHASE_SERVER_MAP[phase] || DEFAULT_SERVERS;
|
|
143
|
+
const allowedSet = new Set(allowedNames);
|
|
144
|
+
return servers.filter((s) => allowedSet.has(s.name));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// --- Utility functions (exported for testing) ---
|
|
148
|
+
/**
|
|
149
|
+
* Approximate token count using chars/4 heuristic
|
|
150
|
+
*
|
|
151
|
+
* @param text - Text to count tokens for
|
|
152
|
+
* @returns Approximate token count
|
|
153
|
+
*/
|
|
154
|
+
export function countTokens(text) {
|
|
155
|
+
return Math.ceil(text.length / 4);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Assemble the full instructions string from server lines
|
|
159
|
+
*/
|
|
160
|
+
function assembleInstructions(serverLines, gatewayUrl) {
|
|
161
|
+
return `## Available MCP Tools
|
|
162
|
+
|
|
163
|
+
${serverLines.join('\n')}
|
|
164
|
+
|
|
165
|
+
Gateway: ${gatewayUrl}
|
|
166
|
+
|
|
167
|
+
${TOOL_PRIORITY_TEXT}`;
|
|
168
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Credential Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages API key credentials for MCP servers.
|
|
5
|
+
* Resolution chain: environment variable > stored credential file.
|
|
6
|
+
* Stores credentials at ~/.bmad/credentials/.
|
|
7
|
+
*/
|
|
8
|
+
import type { CredentialEntry, ICredentialManager } from '../../mcp/types.js';
|
|
9
|
+
export declare class McpCredentialManager implements ICredentialManager {
|
|
10
|
+
private readonly credentialsPath;
|
|
11
|
+
private store;
|
|
12
|
+
constructor(credentialsPath?: string);
|
|
13
|
+
/**
|
|
14
|
+
* Get a credential value from the stored file (not env vars).
|
|
15
|
+
*
|
|
16
|
+
* @returns The stored credential value, or null if not found
|
|
17
|
+
*/
|
|
18
|
+
get(key: string): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* List all known credential entries with their configuration status
|
|
21
|
+
*/
|
|
22
|
+
list(): CredentialEntry[];
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a credential key value.
|
|
25
|
+
* Resolution chain: environment variable > stored credential file.
|
|
26
|
+
*
|
|
27
|
+
* @returns The credential value, or null if not found anywhere
|
|
28
|
+
*/
|
|
29
|
+
resolve(key: string): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Remove a credential from the store.
|
|
32
|
+
*
|
|
33
|
+
* @returns true if the key was found and removed, false if not found
|
|
34
|
+
*/
|
|
35
|
+
remove(key: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Store a credential key-value pair
|
|
38
|
+
*/
|
|
39
|
+
set(key: string, value: string): void;
|
|
40
|
+
/**
|
|
41
|
+
* Load the credential store from disk
|
|
42
|
+
*/
|
|
43
|
+
private loadStore;
|
|
44
|
+
/**
|
|
45
|
+
* Save the credential store to disk
|
|
46
|
+
*/
|
|
47
|
+
private saveStore;
|
|
48
|
+
}
|