@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.
- package/dist/src/server.js +48 -1
- package/dist/src/tools/agent.d.ts +2 -2
- package/dist/src/tools/agent.js +31 -3
- package/dist/src/tools/architecture.js +16 -2
- package/dist/src/tools/cross-check.js +32 -3
- package/dist/src/tools/docs.js +6 -4
- package/dist/src/tools/entropy.d.ts +9 -0
- package/dist/src/tools/entropy.js +25 -6
- package/dist/src/tools/feedback.js +6 -5
- package/dist/src/tools/generate-slash-commands.js +3 -2
- package/dist/src/tools/graph.js +1 -8
- package/dist/src/tools/init.js +4 -2
- package/dist/src/tools/interaction.d.ts +130 -0
- package/dist/src/tools/interaction.js +235 -0
- package/dist/src/tools/linter.js +6 -2
- package/dist/src/tools/performance.js +7 -6
- package/dist/src/tools/persona.js +2 -1
- package/dist/src/tools/phase-gate.js +2 -2
- package/dist/src/tools/review-pipeline.d.ts +63 -0
- package/dist/src/tools/review-pipeline.js +114 -0
- package/dist/src/tools/roadmap.d.ts +82 -0
- package/dist/src/tools/roadmap.js +390 -0
- package/dist/src/tools/security.js +2 -1
- package/dist/src/tools/skill.js +3 -2
- package/dist/src/tools/state.js +4 -4
- package/dist/src/tools/validate.d.ts +7 -0
- package/dist/src/tools/validate.js +16 -1
- package/dist/src/utils/sanitize-path.d.ts +5 -0
- package/dist/src/utils/sanitize-path.js +12 -0
- package/package.json +7 -7
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { resultToMcpResponse } from '../utils/result-adapter.js';
|
|
4
|
+
import { sanitizePath } from '../utils/sanitize-path.js';
|
|
5
|
+
export const manageRoadmapDefinition = {
|
|
6
|
+
name: 'manage_roadmap',
|
|
7
|
+
description: 'Manage the project roadmap: show, add, update, remove, sync features, or query by filter. Reads and writes docs/roadmap.md.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
path: { type: 'string', description: 'Path to project root' },
|
|
12
|
+
action: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
enum: ['show', 'add', 'update', 'remove', 'query', 'sync'],
|
|
15
|
+
description: 'Action to perform',
|
|
16
|
+
},
|
|
17
|
+
feature: { type: 'string', description: 'Feature name (required for add, update, remove)' },
|
|
18
|
+
milestone: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Milestone name (required for add; optional filter for show)',
|
|
21
|
+
},
|
|
22
|
+
status: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
enum: ['backlog', 'planned', 'in-progress', 'done', 'blocked'],
|
|
25
|
+
description: 'Feature status (required for add; optional for update; optional filter for show)',
|
|
26
|
+
},
|
|
27
|
+
summary: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Feature summary (required for add; optional for update)',
|
|
30
|
+
},
|
|
31
|
+
spec: { type: 'string', description: 'Spec file path (optional for add/update)' },
|
|
32
|
+
plans: {
|
|
33
|
+
type: 'array',
|
|
34
|
+
items: { type: 'string' },
|
|
35
|
+
description: 'Plan file paths (optional for add/update)',
|
|
36
|
+
},
|
|
37
|
+
blocked_by: {
|
|
38
|
+
type: 'array',
|
|
39
|
+
items: { type: 'string' },
|
|
40
|
+
description: 'Blocking feature names (optional for add/update)',
|
|
41
|
+
},
|
|
42
|
+
filter: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: 'Query filter: "blocked", "in-progress", "done", "planned", "backlog", or "milestone:<name>" (required for query)',
|
|
45
|
+
},
|
|
46
|
+
apply: {
|
|
47
|
+
type: 'boolean',
|
|
48
|
+
description: 'For sync action: apply proposed changes (default: false, preview only)',
|
|
49
|
+
},
|
|
50
|
+
force_sync: {
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
description: 'For sync action: override human-always-wins rule',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
required: ['path', 'action'],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
function roadmapPath(projectRoot) {
|
|
59
|
+
return path.join(projectRoot, 'docs', 'roadmap.md');
|
|
60
|
+
}
|
|
61
|
+
function readRoadmapFile(projectRoot) {
|
|
62
|
+
const filePath = roadmapPath(projectRoot);
|
|
63
|
+
try {
|
|
64
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function writeRoadmapFile(projectRoot, content) {
|
|
71
|
+
const filePath = roadmapPath(projectRoot);
|
|
72
|
+
const dir = path.dirname(filePath);
|
|
73
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
74
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
75
|
+
}
|
|
76
|
+
export async function handleManageRoadmap(input) {
|
|
77
|
+
try {
|
|
78
|
+
const { parseRoadmap, serializeRoadmap, syncRoadmap } = await import('@harness-engineering/core');
|
|
79
|
+
const { Ok } = await import('@harness-engineering/types');
|
|
80
|
+
const projectPath = sanitizePath(input.path);
|
|
81
|
+
switch (input.action) {
|
|
82
|
+
case 'show': {
|
|
83
|
+
const raw = readRoadmapFile(projectPath);
|
|
84
|
+
if (raw === null) {
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: 'Error: docs/roadmap.md not found. Create a roadmap first.',
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const result = parseRoadmap(raw);
|
|
96
|
+
if (!result.ok)
|
|
97
|
+
return resultToMcpResponse(result);
|
|
98
|
+
let roadmap = result.value;
|
|
99
|
+
// Apply milestone filter
|
|
100
|
+
if (input.milestone) {
|
|
101
|
+
const milestoneFilter = input.milestone;
|
|
102
|
+
roadmap = {
|
|
103
|
+
...roadmap,
|
|
104
|
+
milestones: roadmap.milestones.filter((m) => m.name.toLowerCase() === milestoneFilter.toLowerCase()),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Apply status filter
|
|
108
|
+
if (input.status) {
|
|
109
|
+
const statusFilter = input.status;
|
|
110
|
+
roadmap = {
|
|
111
|
+
...roadmap,
|
|
112
|
+
milestones: roadmap.milestones
|
|
113
|
+
.map((m) => ({
|
|
114
|
+
...m,
|
|
115
|
+
features: m.features.filter((f) => f.status === statusFilter),
|
|
116
|
+
}))
|
|
117
|
+
.filter((m) => m.features.length > 0),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return resultToMcpResponse(Ok(roadmap));
|
|
121
|
+
}
|
|
122
|
+
case 'add': {
|
|
123
|
+
if (!input.feature) {
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: 'text', text: 'Error: feature is required for add action' }],
|
|
126
|
+
isError: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (!input.milestone) {
|
|
130
|
+
return {
|
|
131
|
+
content: [
|
|
132
|
+
{ type: 'text', text: 'Error: milestone is required for add action' },
|
|
133
|
+
],
|
|
134
|
+
isError: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (!input.status) {
|
|
138
|
+
return {
|
|
139
|
+
content: [{ type: 'text', text: 'Error: status is required for add action' }],
|
|
140
|
+
isError: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (!input.summary) {
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: 'text', text: 'Error: summary is required for add action' }],
|
|
146
|
+
isError: true,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const raw = readRoadmapFile(projectPath);
|
|
150
|
+
if (raw === null) {
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: 'text',
|
|
155
|
+
text: 'Error: docs/roadmap.md not found. Create a roadmap first.',
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
isError: true,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const result = parseRoadmap(raw);
|
|
162
|
+
if (!result.ok)
|
|
163
|
+
return resultToMcpResponse(result);
|
|
164
|
+
const roadmap = result.value;
|
|
165
|
+
const milestone = roadmap.milestones.find((m) => m.name.toLowerCase() === input.milestone.toLowerCase());
|
|
166
|
+
if (!milestone) {
|
|
167
|
+
return {
|
|
168
|
+
content: [
|
|
169
|
+
{ type: 'text', text: `Error: milestone "${input.milestone}" not found` },
|
|
170
|
+
],
|
|
171
|
+
isError: true,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
milestone.features.push({
|
|
175
|
+
name: input.feature,
|
|
176
|
+
status: input.status,
|
|
177
|
+
spec: input.spec ?? null,
|
|
178
|
+
plans: input.plans ?? [],
|
|
179
|
+
blockedBy: input.blocked_by ?? [],
|
|
180
|
+
summary: input.summary,
|
|
181
|
+
});
|
|
182
|
+
// Update last_manual_edit timestamp
|
|
183
|
+
roadmap.frontmatter.lastManualEdit = new Date().toISOString();
|
|
184
|
+
writeRoadmapFile(projectPath, serializeRoadmap(roadmap));
|
|
185
|
+
return resultToMcpResponse(Ok(roadmap));
|
|
186
|
+
}
|
|
187
|
+
case 'update': {
|
|
188
|
+
if (!input.feature) {
|
|
189
|
+
return {
|
|
190
|
+
content: [
|
|
191
|
+
{ type: 'text', text: 'Error: feature is required for update action' },
|
|
192
|
+
],
|
|
193
|
+
isError: true,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const raw = readRoadmapFile(projectPath);
|
|
197
|
+
if (raw === null) {
|
|
198
|
+
return {
|
|
199
|
+
content: [
|
|
200
|
+
{
|
|
201
|
+
type: 'text',
|
|
202
|
+
text: 'Error: docs/roadmap.md not found. Create a roadmap first.',
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
isError: true,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const result = parseRoadmap(raw);
|
|
209
|
+
if (!result.ok)
|
|
210
|
+
return resultToMcpResponse(result);
|
|
211
|
+
const roadmap = result.value;
|
|
212
|
+
let found = false;
|
|
213
|
+
for (const m of roadmap.milestones) {
|
|
214
|
+
const feature = m.features.find((f) => f.name.toLowerCase() === input.feature.toLowerCase());
|
|
215
|
+
if (feature) {
|
|
216
|
+
if (input.status)
|
|
217
|
+
feature.status = input.status;
|
|
218
|
+
if (input.summary !== undefined)
|
|
219
|
+
feature.summary = input.summary;
|
|
220
|
+
if (input.spec !== undefined)
|
|
221
|
+
feature.spec = input.spec || null;
|
|
222
|
+
if (input.plans !== undefined)
|
|
223
|
+
feature.plans = input.plans;
|
|
224
|
+
if (input.blocked_by !== undefined)
|
|
225
|
+
feature.blockedBy = input.blocked_by;
|
|
226
|
+
found = true;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!found) {
|
|
231
|
+
return {
|
|
232
|
+
content: [
|
|
233
|
+
{ type: 'text', text: `Error: feature "${input.feature}" not found` },
|
|
234
|
+
],
|
|
235
|
+
isError: true,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
roadmap.frontmatter.lastManualEdit = new Date().toISOString();
|
|
239
|
+
writeRoadmapFile(projectPath, serializeRoadmap(roadmap));
|
|
240
|
+
return resultToMcpResponse(Ok(roadmap));
|
|
241
|
+
}
|
|
242
|
+
case 'remove': {
|
|
243
|
+
if (!input.feature) {
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{ type: 'text', text: 'Error: feature is required for remove action' },
|
|
247
|
+
],
|
|
248
|
+
isError: true,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const raw = readRoadmapFile(projectPath);
|
|
252
|
+
if (raw === null) {
|
|
253
|
+
return {
|
|
254
|
+
content: [
|
|
255
|
+
{
|
|
256
|
+
type: 'text',
|
|
257
|
+
text: 'Error: docs/roadmap.md not found. Create a roadmap first.',
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
isError: true,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const result = parseRoadmap(raw);
|
|
264
|
+
if (!result.ok)
|
|
265
|
+
return resultToMcpResponse(result);
|
|
266
|
+
const roadmap = result.value;
|
|
267
|
+
let found = false;
|
|
268
|
+
for (const m of roadmap.milestones) {
|
|
269
|
+
const idx = m.features.findIndex((f) => f.name.toLowerCase() === input.feature.toLowerCase());
|
|
270
|
+
if (idx !== -1) {
|
|
271
|
+
m.features.splice(idx, 1);
|
|
272
|
+
found = true;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (!found) {
|
|
277
|
+
return {
|
|
278
|
+
content: [
|
|
279
|
+
{ type: 'text', text: `Error: feature "${input.feature}" not found` },
|
|
280
|
+
],
|
|
281
|
+
isError: true,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
roadmap.frontmatter.lastManualEdit = new Date().toISOString();
|
|
285
|
+
writeRoadmapFile(projectPath, serializeRoadmap(roadmap));
|
|
286
|
+
return resultToMcpResponse(Ok(roadmap));
|
|
287
|
+
}
|
|
288
|
+
case 'query': {
|
|
289
|
+
if (!input.filter) {
|
|
290
|
+
return {
|
|
291
|
+
content: [
|
|
292
|
+
{ type: 'text', text: 'Error: filter is required for query action' },
|
|
293
|
+
],
|
|
294
|
+
isError: true,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const raw = readRoadmapFile(projectPath);
|
|
298
|
+
if (raw === null) {
|
|
299
|
+
return {
|
|
300
|
+
content: [
|
|
301
|
+
{
|
|
302
|
+
type: 'text',
|
|
303
|
+
text: 'Error: docs/roadmap.md not found. Create a roadmap first.',
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
isError: true,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const result = parseRoadmap(raw);
|
|
310
|
+
if (!result.ok)
|
|
311
|
+
return resultToMcpResponse(result);
|
|
312
|
+
const roadmap = result.value;
|
|
313
|
+
const allFeatures = roadmap.milestones.flatMap((m) => m.features.map((f) => ({ ...f, milestone: m.name })));
|
|
314
|
+
const filter = input.filter.toLowerCase();
|
|
315
|
+
let filtered;
|
|
316
|
+
if (filter.startsWith('milestone:')) {
|
|
317
|
+
const milestoneName = filter.slice('milestone:'.length).trim();
|
|
318
|
+
filtered = allFeatures.filter((f) => f.milestone.toLowerCase().includes(milestoneName));
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
// Treat filter as a status value
|
|
322
|
+
filtered = allFeatures.filter((f) => f.status === filter);
|
|
323
|
+
}
|
|
324
|
+
return resultToMcpResponse(Ok(filtered));
|
|
325
|
+
}
|
|
326
|
+
case 'sync': {
|
|
327
|
+
const raw = readRoadmapFile(projectPath);
|
|
328
|
+
if (raw === null) {
|
|
329
|
+
return {
|
|
330
|
+
content: [
|
|
331
|
+
{
|
|
332
|
+
type: 'text',
|
|
333
|
+
text: 'Error: docs/roadmap.md not found. Create a roadmap first.',
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
isError: true,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const result = parseRoadmap(raw);
|
|
340
|
+
if (!result.ok)
|
|
341
|
+
return resultToMcpResponse(result);
|
|
342
|
+
const roadmap = result.value;
|
|
343
|
+
const syncResult = syncRoadmap({
|
|
344
|
+
projectPath,
|
|
345
|
+
roadmap,
|
|
346
|
+
forceSync: input.force_sync ?? false,
|
|
347
|
+
});
|
|
348
|
+
if (!syncResult.ok)
|
|
349
|
+
return resultToMcpResponse(syncResult);
|
|
350
|
+
const changes = syncResult.value;
|
|
351
|
+
if (changes.length === 0) {
|
|
352
|
+
return resultToMcpResponse(Ok({ changes: [], message: 'Roadmap is up to date.' }));
|
|
353
|
+
}
|
|
354
|
+
if (input.apply) {
|
|
355
|
+
// Apply changes to roadmap
|
|
356
|
+
for (const change of changes) {
|
|
357
|
+
for (const m of roadmap.milestones) {
|
|
358
|
+
const feature = m.features.find((f) => f.name.toLowerCase() === change.feature.toLowerCase());
|
|
359
|
+
if (feature) {
|
|
360
|
+
feature.status = change.to;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
roadmap.frontmatter.lastSynced = new Date().toISOString();
|
|
366
|
+
writeRoadmapFile(projectPath, serializeRoadmap(roadmap));
|
|
367
|
+
return resultToMcpResponse(Ok({ changes, applied: true, roadmap }));
|
|
368
|
+
}
|
|
369
|
+
return resultToMcpResponse(Ok({ changes, applied: false }));
|
|
370
|
+
}
|
|
371
|
+
default: {
|
|
372
|
+
return {
|
|
373
|
+
content: [{ type: 'text', text: `Error: unknown action` }],
|
|
374
|
+
isError: true,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
return {
|
|
381
|
+
content: [
|
|
382
|
+
{
|
|
383
|
+
type: 'text',
|
|
384
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
isError: true,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
+
import { sanitizePath } from '../utils/sanitize-path.js';
|
|
2
3
|
// ============ run_security_scan ============
|
|
3
4
|
export const runSecurityScanDefinition = {
|
|
4
5
|
name: 'run_security_scan',
|
|
@@ -23,7 +24,7 @@ export const runSecurityScanDefinition = {
|
|
|
23
24
|
export async function handleRunSecurityScan(input) {
|
|
24
25
|
try {
|
|
25
26
|
const core = await import('@harness-engineering/core');
|
|
26
|
-
const projectRoot =
|
|
27
|
+
const projectRoot = sanitizePath(input.path);
|
|
27
28
|
// Load config from project
|
|
28
29
|
let configData = {};
|
|
29
30
|
try {
|
package/dist/src/tools/skill.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from 'path';
|
|
|
3
3
|
import { Ok, Err } from '@harness-engineering/core';
|
|
4
4
|
import { resultToMcpResponse } from '../utils/result-adapter.js';
|
|
5
5
|
import { resolveSkillsDir } from '../utils/paths.js';
|
|
6
|
+
import { sanitizePath } from '../utils/sanitize-path.js';
|
|
6
7
|
export const runSkillDefinition = {
|
|
7
8
|
name: 'run_skill',
|
|
8
9
|
description: 'Load and return the content of a skill (SKILL.md), optionally with project state context',
|
|
@@ -41,7 +42,7 @@ export async function handleRunSkill(input) {
|
|
|
41
42
|
let content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
42
43
|
// Optionally inject project state context
|
|
43
44
|
if (input.path) {
|
|
44
|
-
const projectPath =
|
|
45
|
+
const projectPath = sanitizePath(input.path);
|
|
45
46
|
const stateFile = path.join(projectPath, '.harness', 'state.json');
|
|
46
47
|
if (fs.existsSync(stateFile)) {
|
|
47
48
|
const stateContent = fs.readFileSync(stateFile, 'utf-8');
|
|
@@ -82,7 +83,7 @@ export async function handleCreateSkill(input) {
|
|
|
82
83
|
name: input.name,
|
|
83
84
|
description: input.description,
|
|
84
85
|
cognitiveMode: input.cognitiveMode ?? 'constructive-architect',
|
|
85
|
-
outputDir:
|
|
86
|
+
outputDir: sanitizePath(input.path),
|
|
86
87
|
});
|
|
87
88
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
88
89
|
}
|
package/dist/src/tools/state.js
CHANGED
|
@@ -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
|
// ── manage_state ──────────────────────────────────────────────────────
|
|
5
5
|
export const manageStateDefinition = {
|
|
6
6
|
name: 'manage_state',
|
|
@@ -30,7 +30,7 @@ export const manageStateDefinition = {
|
|
|
30
30
|
export async function handleManageState(input) {
|
|
31
31
|
try {
|
|
32
32
|
const { loadState, saveState, appendLearning, appendFailure, archiveFailures, runMechanicalGate, DEFAULT_STATE, } = await import('@harness-engineering/core');
|
|
33
|
-
const projectPath =
|
|
33
|
+
const projectPath = sanitizePath(input.path);
|
|
34
34
|
switch (input.action) {
|
|
35
35
|
case 'show': {
|
|
36
36
|
const result = await loadState(projectPath, input.stream);
|
|
@@ -136,7 +136,7 @@ export const manageHandoffDefinition = {
|
|
|
136
136
|
export async function handleManageHandoff(input) {
|
|
137
137
|
try {
|
|
138
138
|
const { saveHandoff, loadHandoff } = await import('@harness-engineering/core');
|
|
139
|
-
const projectPath =
|
|
139
|
+
const projectPath = sanitizePath(input.path);
|
|
140
140
|
switch (input.action) {
|
|
141
141
|
case 'save': {
|
|
142
142
|
if (!input.handoff) {
|
|
@@ -189,7 +189,7 @@ export const listStreamsDefinition = {
|
|
|
189
189
|
export async function handleListStreams(input) {
|
|
190
190
|
try {
|
|
191
191
|
const { listStreams, loadStreamIndex } = await import('@harness-engineering/core');
|
|
192
|
-
const projectPath =
|
|
192
|
+
const projectPath = sanitizePath(input.path);
|
|
193
193
|
const indexResult = await loadStreamIndex(projectPath);
|
|
194
194
|
const streamsResult = await listStreams(projectPath);
|
|
195
195
|
if (!streamsResult.ok)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import { resolveProjectConfig } from '../utils/config-resolver.js';
|
|
3
|
+
import { sanitizePath } from '../utils/sanitize-path.js';
|
|
3
4
|
export const validateToolDefinition = {
|
|
4
5
|
name: 'validate_project',
|
|
5
6
|
description: 'Run all validation checks on a harness engineering project',
|
|
@@ -12,7 +13,21 @@ export const validateToolDefinition = {
|
|
|
12
13
|
},
|
|
13
14
|
};
|
|
14
15
|
export async function handleValidateProject(input) {
|
|
15
|
-
|
|
16
|
+
let projectPath;
|
|
17
|
+
try {
|
|
18
|
+
projectPath = sanitizePath(input.path);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return {
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
isError: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
16
31
|
const errors = [];
|
|
17
32
|
const checks = {
|
|
18
33
|
config: 'fail',
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
/**
|
|
3
|
+
* Validates and resolves an input path, rejecting filesystem root
|
|
4
|
+
* to prevent accidental broad filesystem access via MCP tools.
|
|
5
|
+
*/
|
|
6
|
+
export function sanitizePath(inputPath) {
|
|
7
|
+
const resolved = path.resolve(inputPath);
|
|
8
|
+
if (resolved === '/' || resolved === path.parse(resolved).root) {
|
|
9
|
+
throw new Error('Invalid project path: cannot use filesystem root');
|
|
10
|
+
}
|
|
11
|
+
return resolved;
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-engineering/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "MCP server for Harness Engineering toolkit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
"zod": "^3.22.0",
|
|
24
24
|
"yaml": "^2.3.0",
|
|
25
25
|
"handlebars": "^4.7.0",
|
|
26
|
-
"@harness-engineering/core": "0.
|
|
27
|
-
"@harness-engineering/graph": "0.2.
|
|
28
|
-
"@harness-engineering/
|
|
29
|
-
"@harness-engineering/
|
|
30
|
-
"@harness-engineering/
|
|
26
|
+
"@harness-engineering/core": "0.9.1",
|
|
27
|
+
"@harness-engineering/graph": "0.2.3",
|
|
28
|
+
"@harness-engineering/cli": "1.8.1",
|
|
29
|
+
"@harness-engineering/linter-gen": "0.1.2",
|
|
30
|
+
"@harness-engineering/types": "0.2.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^22.0.0",
|
|
@@ -53,6 +53,6 @@
|
|
|
53
53
|
"test:watch": "vitest",
|
|
54
54
|
"lint": "eslint src",
|
|
55
55
|
"typecheck": "tsc --noEmit",
|
|
56
|
-
"clean": "
|
|
56
|
+
"clean": "node ../../scripts/clean.mjs dist"
|
|
57
57
|
}
|
|
58
58
|
}
|