@harness-engineering/mcp-server 0.5.3 → 0.6.1

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.
@@ -0,0 +1,235 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { sanitizePath } from '../utils/sanitize-path.js';
3
+ export const emitInteractionDefinition = {
4
+ name: 'emit_interaction',
5
+ description: 'Emit a structured interaction (question, confirmation, or phase transition) for round-trip communication with the user',
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ path: { type: 'string', description: 'Path to project root' },
10
+ type: {
11
+ type: 'string',
12
+ enum: ['question', 'confirmation', 'transition'],
13
+ description: 'Type of interaction',
14
+ },
15
+ stream: {
16
+ type: 'string',
17
+ description: 'State stream for recording (auto-resolves from branch if omitted)',
18
+ },
19
+ question: {
20
+ type: 'object',
21
+ description: 'Question payload (required when type is question)',
22
+ properties: {
23
+ text: { type: 'string', description: 'The question text' },
24
+ options: {
25
+ type: 'array',
26
+ items: { type: 'string' },
27
+ description: 'Multiple choice options (omit for free-form)',
28
+ },
29
+ default: { type: 'string', description: 'Default answer' },
30
+ },
31
+ required: ['text'],
32
+ },
33
+ confirmation: {
34
+ type: 'object',
35
+ description: 'Confirmation payload (required when type is confirmation)',
36
+ properties: {
37
+ text: { type: 'string', description: 'What to confirm' },
38
+ context: {
39
+ type: 'string',
40
+ description: 'Why confirmation is needed',
41
+ },
42
+ },
43
+ required: ['text', 'context'],
44
+ },
45
+ transition: {
46
+ type: 'object',
47
+ description: 'Transition payload (required when type is transition)',
48
+ properties: {
49
+ completedPhase: {
50
+ type: 'string',
51
+ description: 'Phase that was completed',
52
+ },
53
+ suggestedNext: {
54
+ type: 'string',
55
+ description: 'Suggested next phase',
56
+ },
57
+ reason: {
58
+ type: 'string',
59
+ description: 'Why the transition is happening',
60
+ },
61
+ artifacts: {
62
+ type: 'array',
63
+ items: { type: 'string' },
64
+ description: 'File paths produced during the completed phase',
65
+ },
66
+ requiresConfirmation: {
67
+ type: 'boolean',
68
+ description: 'true = wait for user confirmation, false = proceed immediately',
69
+ },
70
+ summary: {
71
+ type: 'string',
72
+ description: '1-2 sentence rich summary with key metrics',
73
+ },
74
+ },
75
+ required: [
76
+ 'completedPhase',
77
+ 'suggestedNext',
78
+ 'reason',
79
+ 'artifacts',
80
+ 'requiresConfirmation',
81
+ 'summary',
82
+ ],
83
+ },
84
+ },
85
+ required: ['path', 'type'],
86
+ },
87
+ };
88
+ export async function handleEmitInteraction(input) {
89
+ try {
90
+ const projectPath = sanitizePath(input.path);
91
+ const id = randomUUID();
92
+ switch (input.type) {
93
+ case 'question': {
94
+ if (!input.question) {
95
+ return {
96
+ content: [
97
+ {
98
+ type: 'text',
99
+ text: 'Error: question payload is required when type is question',
100
+ },
101
+ ],
102
+ isError: true,
103
+ };
104
+ }
105
+ const { text, options, default: defaultAnswer } = input.question;
106
+ let prompt = text;
107
+ if (options && options.length > 0) {
108
+ prompt +=
109
+ '\n\nOptions:\n' +
110
+ options.map((o, i) => ` ${String.fromCharCode(65 + i)}) ${o}`).join('\n');
111
+ }
112
+ if (defaultAnswer) {
113
+ prompt += `\n\nDefault: ${defaultAnswer}`;
114
+ }
115
+ // Record in state decisions array
116
+ await recordInteraction(projectPath, id, 'question', text, input.stream);
117
+ return {
118
+ content: [{ type: 'text', text: JSON.stringify({ id, prompt }) }],
119
+ };
120
+ }
121
+ case 'confirmation': {
122
+ if (!input.confirmation) {
123
+ return {
124
+ content: [
125
+ {
126
+ type: 'text',
127
+ text: 'Error: confirmation payload is required when type is confirmation',
128
+ },
129
+ ],
130
+ isError: true,
131
+ };
132
+ }
133
+ const { text, context } = input.confirmation;
134
+ const prompt = `${text}\n\nContext: ${context}\n\nProceed? (yes/no)`;
135
+ await recordInteraction(projectPath, id, 'confirmation', text, input.stream);
136
+ return {
137
+ content: [{ type: 'text', text: JSON.stringify({ id, prompt }) }],
138
+ };
139
+ }
140
+ case 'transition': {
141
+ if (!input.transition) {
142
+ return {
143
+ content: [
144
+ {
145
+ type: 'text',
146
+ text: 'Error: transition payload is required when type is transition',
147
+ },
148
+ ],
149
+ isError: true,
150
+ };
151
+ }
152
+ const { completedPhase, suggestedNext, reason, artifacts, requiresConfirmation, summary } = input.transition;
153
+ const prompt = `Phase "${completedPhase}" complete. ${reason}\n\n` +
154
+ `${summary}\n\n` +
155
+ `Artifacts produced:\n${artifacts.map((a) => ` - ${a}`).join('\n')}\n\n` +
156
+ (requiresConfirmation
157
+ ? `Suggested next: "${suggestedNext}". Proceed?`
158
+ : `Proceeding to ${suggestedNext}...`);
159
+ // Write handoff
160
+ try {
161
+ const { saveHandoff } = await import('@harness-engineering/core');
162
+ await saveHandoff(projectPath, {
163
+ timestamp: new Date().toISOString(),
164
+ fromSkill: 'emit_interaction',
165
+ phase: completedPhase,
166
+ summary: reason,
167
+ completed: [completedPhase],
168
+ pending: [suggestedNext],
169
+ concerns: [],
170
+ decisions: [],
171
+ blockers: [],
172
+ contextKeywords: [],
173
+ }, input.stream);
174
+ }
175
+ catch {
176
+ // Handoff write failure is non-fatal
177
+ }
178
+ await recordInteraction(projectPath, id, 'transition', `${completedPhase} -> ${suggestedNext}`, input.stream);
179
+ const responsePayload = { id, prompt, handoffWritten: true };
180
+ if (!requiresConfirmation) {
181
+ responsePayload.autoTransition = true;
182
+ responsePayload.nextAction = `Invoke harness-${suggestedNext} skill now`;
183
+ }
184
+ return {
185
+ content: [
186
+ {
187
+ type: 'text',
188
+ text: JSON.stringify(responsePayload),
189
+ },
190
+ ],
191
+ };
192
+ }
193
+ default: {
194
+ return {
195
+ content: [
196
+ {
197
+ type: 'text',
198
+ text: `Error: unknown interaction type: ${String(input.type)}`,
199
+ },
200
+ ],
201
+ isError: true,
202
+ };
203
+ }
204
+ }
205
+ }
206
+ catch (error) {
207
+ return {
208
+ content: [
209
+ {
210
+ type: 'text',
211
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
212
+ },
213
+ ],
214
+ isError: true,
215
+ };
216
+ }
217
+ }
218
+ async function recordInteraction(projectPath, id, type, decision, stream) {
219
+ try {
220
+ const { loadState, saveState } = await import('@harness-engineering/core');
221
+ const stateResult = await loadState(projectPath, stream);
222
+ if (stateResult.ok) {
223
+ const state = stateResult.value;
224
+ state.decisions.push({
225
+ date: new Date().toISOString(),
226
+ decision: `[${type}:${id}] ${decision}`,
227
+ context: 'pending user response',
228
+ });
229
+ await saveState(projectPath, state, stream);
230
+ }
231
+ }
232
+ catch {
233
+ // State recording failure is non-fatal
234
+ }
235
+ }
@@ -1,3 +1,4 @@
1
+ import { sanitizePath } from '../utils/sanitize-path.js';
1
2
  export const generateLinterDefinition = {
2
3
  name: 'generate_linter',
3
4
  description: 'Generate an ESLint rule from YAML configuration',
@@ -13,7 +14,10 @@ export const generateLinterDefinition = {
13
14
  export async function handleGenerateLinter(input) {
14
15
  try {
15
16
  const { generate } = await import('@harness-engineering/linter-gen');
16
- const result = await generate({ configPath: input.configPath, outputDir: input.outputDir });
17
+ const result = await generate({
18
+ configPath: sanitizePath(input.configPath),
19
+ outputDir: input.outputDir ? sanitizePath(input.outputDir) : undefined,
20
+ });
17
21
  if ('success' in result && result.success) {
18
22
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
19
23
  }
@@ -45,7 +49,7 @@ export const validateLinterConfigDefinition = {
45
49
  export async function handleValidateLinterConfig(input) {
46
50
  try {
47
51
  const { validate } = await import('@harness-engineering/linter-gen');
48
- const result = await validate({ configPath: input.configPath });
52
+ const result = await validate({ configPath: sanitizePath(input.configPath) });
49
53
  if ('success' in result && result.success) {
50
54
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
51
55
  }
@@ -1,6 +1,6 @@
1
- import * as path from 'path';
2
1
  import { Ok } from '@harness-engineering/core';
3
2
  import { resultToMcpResponse } from '../utils/result-adapter.js';
3
+ import { sanitizePath } from '../utils/sanitize-path.js';
4
4
  export const checkPerformanceDefinition = {
5
5
  name: 'check_performance',
6
6
  description: 'Run performance checks: structural complexity, coupling metrics, and size budgets',
@@ -21,8 +21,9 @@ export async function handleCheckPerformance(input) {
21
21
  try {
22
22
  const { EntropyAnalyzer } = await import('@harness-engineering/core');
23
23
  const typeFilter = input.type ?? 'all';
24
+ const projectPath = sanitizePath(input.path);
24
25
  const analyzer = new EntropyAnalyzer({
25
- rootDir: path.resolve(input.path),
26
+ rootDir: projectPath,
26
27
  analyze: {
27
28
  complexity: typeFilter === 'all' || typeFilter === 'structural',
28
29
  coupling: typeFilter === 'all' || typeFilter === 'coupling',
@@ -33,7 +34,7 @@ export async function handleCheckPerformance(input) {
33
34
  let graphOptions;
34
35
  try {
35
36
  const { loadGraphStore } = await import('../utils/graph-loader.js');
36
- const store = await loadGraphStore(path.resolve(input.path));
37
+ const store = await loadGraphStore(projectPath);
37
38
  if (store) {
38
39
  const { GraphComplexityAdapter, GraphCouplingAdapter } = await import('@harness-engineering/graph');
39
40
  const complexityAdapter = new GraphComplexityAdapter(store);
@@ -77,7 +78,7 @@ export const getPerfBaselinesDefinition = {
77
78
  export async function handleGetPerfBaselines(input) {
78
79
  try {
79
80
  const { BaselineManager } = await import('@harness-engineering/core');
80
- const manager = new BaselineManager(path.resolve(input.path));
81
+ const manager = new BaselineManager(sanitizePath(input.path));
81
82
  const baselines = manager.load();
82
83
  return resultToMcpResponse(Ok(baselines ?? { version: 1, updatedAt: '', updatedFrom: '', benchmarks: {} }));
83
84
  }
@@ -125,7 +126,7 @@ export const updatePerfBaselinesDefinition = {
125
126
  export async function handleUpdatePerfBaselines(input) {
126
127
  try {
127
128
  const { BaselineManager } = await import('@harness-engineering/core');
128
- const manager = new BaselineManager(path.resolve(input.path));
129
+ const manager = new BaselineManager(sanitizePath(input.path));
129
130
  manager.save(input.results, input.commitHash);
130
131
  const updated = manager.load();
131
132
  return resultToMcpResponse(Ok(updated));
@@ -157,7 +158,7 @@ export const getCriticalPathsDefinition = {
157
158
  export async function handleGetCriticalPaths(input) {
158
159
  try {
159
160
  const { CriticalPathResolver } = await import('@harness-engineering/core');
160
- const resolver = new CriticalPathResolver(path.resolve(input.path));
161
+ const resolver = new CriticalPathResolver(sanitizePath(input.path));
161
162
  const result = await resolver.resolve();
162
163
  return resultToMcpResponse(Ok(result));
163
164
  }
@@ -2,6 +2,7 @@ import * as path from 'path';
2
2
  import { Ok, Err } from '@harness-engineering/core';
3
3
  import { resultToMcpResponse } from '../utils/result-adapter.js';
4
4
  import { resolvePersonasDir } from '../utils/paths.js';
5
+ import { sanitizePath } from '../utils/sanitize-path.js';
5
6
  export const listPersonasDefinition = {
6
7
  name: 'list_personas',
7
8
  description: 'List available agent personas',
@@ -100,7 +101,7 @@ export async function handleRunPersona(input) {
100
101
  const personaResult = loadPersona(filePath);
101
102
  if (!personaResult.ok)
102
103
  return resultToMcpResponse(personaResult);
103
- const projectPath = input.path ? path.resolve(input.path) : process.cwd();
104
+ const projectPath = input.path ? sanitizePath(input.path) : process.cwd();
104
105
  const trigger = (input.trigger ?? 'auto');
105
106
  const { ALLOWED_PERSONA_COMMANDS } = await import('@harness-engineering/cli');
106
107
  const commandExecutor = async (command) => {
@@ -1,4 +1,4 @@
1
- import * as path from 'path';
1
+ import { sanitizePath } from '../utils/sanitize-path.js';
2
2
  export const checkPhaseGateDefinition = {
3
3
  name: 'check_phase_gate',
4
4
  description: 'Verify implementation-to-spec mappings: checks that each implementation file has a corresponding spec document',
@@ -13,7 +13,7 @@ export const checkPhaseGateDefinition = {
13
13
  export async function handleCheckPhaseGate(input) {
14
14
  try {
15
15
  const { runCheckPhaseGate } = await import('@harness-engineering/cli');
16
- const result = await runCheckPhaseGate({ cwd: path.resolve(input.path) });
16
+ const result = await runCheckPhaseGate({ cwd: sanitizePath(input.path) });
17
17
  if (result.ok) {
18
18
  return { content: [{ type: 'text', text: JSON.stringify(result.value) }] };
19
19
  }
@@ -0,0 +1,63 @@
1
+ export declare const runCodeReviewDefinition: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ path: {
8
+ type: string;
9
+ description: string;
10
+ };
11
+ diff: {
12
+ type: string;
13
+ description: string;
14
+ };
15
+ commitMessage: {
16
+ type: string;
17
+ description: string;
18
+ };
19
+ comment: {
20
+ type: string;
21
+ description: string;
22
+ };
23
+ ci: {
24
+ type: string;
25
+ description: string;
26
+ };
27
+ deep: {
28
+ type: string;
29
+ description: string;
30
+ };
31
+ noMechanical: {
32
+ type: string;
33
+ description: string;
34
+ };
35
+ prNumber: {
36
+ type: string;
37
+ description: string;
38
+ };
39
+ repo: {
40
+ type: string;
41
+ description: string;
42
+ };
43
+ };
44
+ required: string[];
45
+ };
46
+ };
47
+ export declare function handleRunCodeReview(input: {
48
+ path: string;
49
+ diff: string;
50
+ commitMessage?: string;
51
+ comment?: boolean;
52
+ ci?: boolean;
53
+ deep?: boolean;
54
+ noMechanical?: boolean;
55
+ prNumber?: number;
56
+ repo?: string;
57
+ }): Promise<{
58
+ content: {
59
+ type: "text";
60
+ text: string;
61
+ }[];
62
+ isError: boolean;
63
+ }>;
@@ -0,0 +1,114 @@
1
+ import { sanitizePath } from '../utils/sanitize-path.js';
2
+ // ============ run_code_review ============
3
+ export const runCodeReviewDefinition = {
4
+ name: 'run_code_review',
5
+ description: 'Run the unified 7-phase code review pipeline: gate, mechanical checks, context scoping, parallel agents, validation, deduplication, and output.',
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ path: { type: 'string', description: 'Path to project root' },
10
+ diff: { type: 'string', description: 'Git diff string to review' },
11
+ commitMessage: {
12
+ type: 'string',
13
+ description: 'Most recent commit message (for change-type detection)',
14
+ },
15
+ comment: {
16
+ type: 'boolean',
17
+ description: 'Post inline comments to GitHub PR (requires prNumber and repo)',
18
+ },
19
+ ci: {
20
+ type: 'boolean',
21
+ description: 'Enable eligibility gate and non-interactive output',
22
+ },
23
+ deep: {
24
+ type: 'boolean',
25
+ description: 'Add threat modeling pass to security agent',
26
+ },
27
+ noMechanical: {
28
+ type: 'boolean',
29
+ description: 'Skip mechanical checks (useful if already run)',
30
+ },
31
+ prNumber: {
32
+ type: 'number',
33
+ description: 'PR number (required for --comment and CI gate)',
34
+ },
35
+ repo: {
36
+ type: 'string',
37
+ description: 'Repository in owner/repo format (required for --comment)',
38
+ },
39
+ },
40
+ required: ['path', 'diff'],
41
+ },
42
+ };
43
+ export async function handleRunCodeReview(input) {
44
+ try {
45
+ const { parseDiff, runReviewPipeline } = await import('@harness-engineering/core');
46
+ const parseResult = parseDiff(input.diff);
47
+ if (!parseResult.ok) {
48
+ return {
49
+ content: [
50
+ {
51
+ type: 'text',
52
+ text: `Error parsing diff: ${parseResult.error.message}`,
53
+ },
54
+ ],
55
+ isError: true,
56
+ };
57
+ }
58
+ const codeChanges = parseResult.value;
59
+ const projectRoot = sanitizePath(input.path);
60
+ // Build DiffInfo from parsed diff
61
+ const diffInfo = {
62
+ changedFiles: codeChanges.files.map((f) => f.path),
63
+ newFiles: codeChanges.files
64
+ .filter((f) => f.status === 'added')
65
+ .map((f) => f.path),
66
+ deletedFiles: codeChanges.files
67
+ .filter((f) => f.status === 'deleted')
68
+ .map((f) => f.path),
69
+ totalDiffLines: input.diff.split('\n').length,
70
+ fileDiffs: new Map(codeChanges.files.map((f) => [f.path, f.diff ?? ''])),
71
+ };
72
+ const result = await runReviewPipeline({
73
+ projectRoot,
74
+ diff: diffInfo,
75
+ commitMessage: input.commitMessage ?? '',
76
+ flags: {
77
+ comment: input.comment ?? false,
78
+ ci: input.ci ?? false,
79
+ deep: input.deep ?? false,
80
+ noMechanical: input.noMechanical ?? false,
81
+ },
82
+ ...(input.repo != null ? { repo: input.repo } : {}),
83
+ });
84
+ return {
85
+ content: [
86
+ {
87
+ type: 'text',
88
+ text: JSON.stringify({
89
+ skipped: result.skipped,
90
+ skipReason: result.skipReason,
91
+ stoppedByMechanical: result.stoppedByMechanical,
92
+ assessment: result.assessment,
93
+ findingCount: result.findings.length,
94
+ terminalOutput: result.terminalOutput,
95
+ githubCommentCount: result.githubComments.length,
96
+ exitCode: result.exitCode,
97
+ }, null, 2),
98
+ },
99
+ ],
100
+ isError: false,
101
+ };
102
+ }
103
+ catch (error) {
104
+ return {
105
+ content: [
106
+ {
107
+ type: 'text',
108
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
109
+ },
110
+ ],
111
+ isError: true,
112
+ };
113
+ }
114
+ }
@@ -0,0 +1,82 @@
1
+ export declare const manageRoadmapDefinition: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ path: {
8
+ type: string;
9
+ description: string;
10
+ };
11
+ action: {
12
+ type: string;
13
+ enum: string[];
14
+ description: string;
15
+ };
16
+ feature: {
17
+ type: string;
18
+ description: string;
19
+ };
20
+ milestone: {
21
+ type: string;
22
+ description: string;
23
+ };
24
+ status: {
25
+ type: string;
26
+ enum: string[];
27
+ description: string;
28
+ };
29
+ summary: {
30
+ type: string;
31
+ description: string;
32
+ };
33
+ spec: {
34
+ type: string;
35
+ description: string;
36
+ };
37
+ plans: {
38
+ type: string;
39
+ items: {
40
+ type: string;
41
+ };
42
+ description: string;
43
+ };
44
+ blocked_by: {
45
+ type: string;
46
+ items: {
47
+ type: string;
48
+ };
49
+ description: string;
50
+ };
51
+ filter: {
52
+ type: string;
53
+ description: string;
54
+ };
55
+ apply: {
56
+ type: string;
57
+ description: string;
58
+ };
59
+ force_sync: {
60
+ type: string;
61
+ description: string;
62
+ };
63
+ };
64
+ required: string[];
65
+ };
66
+ };
67
+ interface ManageRoadmapInput {
68
+ path: string;
69
+ action: 'show' | 'add' | 'update' | 'remove' | 'query' | 'sync';
70
+ feature?: string;
71
+ milestone?: string;
72
+ status?: 'backlog' | 'planned' | 'in-progress' | 'done' | 'blocked';
73
+ summary?: string;
74
+ spec?: string;
75
+ plans?: string[];
76
+ blocked_by?: string[];
77
+ filter?: string;
78
+ apply?: boolean;
79
+ force_sync?: boolean;
80
+ }
81
+ export declare function handleManageRoadmap(input: ManageRoadmapInput): Promise<import("../utils/result-adapter.js").McpToolResponse>;
82
+ export {};