Skip to content

Commit e425a96

Browse files
authored
[MCP Server] Add run_ci_checks tool (#233416)
1 parent bc54229 commit e425a96

File tree

3 files changed

+268
-0
lines changed

3 files changed

+268
-0
lines changed

src/platform/packages/shared/kbn-mcp-dev-server/src/server/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { listKibanaPackagesTool } from '../tools/list_packages';
1616
import { generateKibanaPackageTool } from '../tools/generate_package';
1717
import { listKibanaTeamsTool } from '../tools/list_teams';
1818
import { runUnitTestsTool } from '../tools/run_unit_tests';
19+
import { runCiChecksTool } from '../tools/run_ci_checks';
1920
import { codeSearchTool } from '../tools/code_search';
2021
import { getDistinctValuesTool } from '../tools/get_distinct_values';
2122
import { findUsagesTool } from '../tools/find_usages';
@@ -27,6 +28,7 @@ run(async () => {
2728
addTool(server, generateKibanaPackageTool);
2829
addTool(server, listKibanaTeamsTool);
2930
addTool(server, runUnitTestsTool);
31+
addTool(server, runCiChecksTool);
3032
addTool(server, codeSearchTool);
3133
addTool(server, getDistinctValuesTool);
3234
addTool(server, findUsagesTool);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { runCiChecksTool } from './run_ci_checks';
11+
12+
describe('runCiChecksTool', () => {
13+
it('should have the correct name', () => {
14+
expect(runCiChecksTool.name).toBe('run_ci_checks');
15+
});
16+
17+
it('should have a description', () => {
18+
expect(runCiChecksTool.description).toBeTruthy();
19+
expect(typeof runCiChecksTool.description).toBe('string');
20+
});
21+
22+
it('should have an input schema', () => {
23+
expect(runCiChecksTool.inputSchema).toBeDefined();
24+
});
25+
26+
it('should have a handler function', () => {
27+
expect(runCiChecksTool.handler).toBeDefined();
28+
expect(typeof runCiChecksTool.handler).toBe('function');
29+
});
30+
31+
it('should have default values for optional parameters', () => {
32+
const schema = runCiChecksTool.inputSchema;
33+
const defaults = schema.parse({});
34+
35+
expect(defaults.checks).toEqual([
36+
'build',
37+
'quick_checks',
38+
'checks',
39+
'type_check',
40+
'linting_with_types',
41+
'linting',
42+
'oas_snapshot',
43+
]);
44+
expect(defaults.parallel).toBe(true);
45+
});
46+
47+
it('should accept custom checks', () => {
48+
const schema = runCiChecksTool.inputSchema;
49+
const customChecks = schema.parse({
50+
checks: ['build', 'type_check'],
51+
});
52+
53+
expect(customChecks.checks).toEqual(['build', 'type_check']);
54+
expect(customChecks.parallel).toBe(true);
55+
});
56+
57+
it('should accept custom parallel setting', () => {
58+
const schema = runCiChecksTool.inputSchema;
59+
const sequential = schema.parse({
60+
parallel: false,
61+
});
62+
63+
expect(sequential.parallel).toBe(false);
64+
});
65+
});
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { z } from '@kbn/zod';
11+
import execa from 'execa';
12+
import { REPO_ROOT } from '@kbn/repo-info';
13+
14+
import type { ToolDefinition } from '../types';
15+
16+
const runCiChecksInputSchema = z.object({
17+
checks: z
18+
.array(
19+
z.enum([
20+
'build',
21+
'quick_checks',
22+
'checks',
23+
'type_check',
24+
'linting_with_types',
25+
'linting',
26+
'oas_snapshot',
27+
])
28+
)
29+
.optional()
30+
.default([
31+
'build',
32+
'quick_checks',
33+
'checks',
34+
'type_check',
35+
'linting_with_types',
36+
'linting',
37+
'oas_snapshot',
38+
])
39+
.describe('Specific CI checks to run. Defaults to all checks.'),
40+
parallel: z
41+
.boolean()
42+
.optional()
43+
.default(true)
44+
.describe('Whether to run checks in parallel. Defaults to true.'),
45+
});
46+
47+
interface CheckResult {
48+
check: string;
49+
status: 'passed' | 'failed';
50+
error?: string;
51+
duration?: number;
52+
}
53+
54+
interface CiChecksResult {
55+
success: boolean;
56+
results: CheckResult[];
57+
totalDuration: number;
58+
}
59+
60+
const CI_CHECKS = {
61+
build: {
62+
name: 'Build Kibana Distribution',
63+
command: 'node --no-experimental-require-module scripts/build_kibana_platform_plugins',
64+
description: 'Build Kibana platform plugins',
65+
},
66+
quick_checks: {
67+
name: 'Quick Checks',
68+
command: 'yarn quick-checks',
69+
description: 'Run quick validation checks',
70+
},
71+
linting: {
72+
name: 'Linting',
73+
command: 'yarn lint',
74+
description: 'Run all linting checks (ESLint and Stylelint)',
75+
},
76+
type_check: {
77+
name: 'Type Check',
78+
command: 'yarn test:type_check',
79+
description: 'Run TypeScript type checking',
80+
},
81+
linting_with_types: {
82+
name: 'Linting (with types)',
83+
command: 'node --no-experimental-require-module scripts/eslint_with_types',
84+
description: 'Run ESLint with type checking',
85+
},
86+
oas_snapshot: {
87+
name: 'OAS Snapshot',
88+
command: 'node --no-experimental-require-module scripts/validate_oas_docs',
89+
description: 'Validate OpenAPI documentation',
90+
},
91+
};
92+
93+
async function runSingleCheck(checkKey: string): Promise<CheckResult> {
94+
const check = CI_CHECKS[checkKey as keyof typeof CI_CHECKS];
95+
if (!check) {
96+
return {
97+
check: checkKey,
98+
status: 'failed',
99+
error: `Unknown check: ${checkKey}`,
100+
};
101+
}
102+
103+
const startTime = Date.now();
104+
105+
try {
106+
await execa.command(check.command, {
107+
cwd: REPO_ROOT,
108+
stdio: 'pipe',
109+
timeout: 900000, // 15 minutes timeout
110+
});
111+
112+
const duration = Date.now() - startTime;
113+
return {
114+
check: checkKey,
115+
status: 'passed',
116+
duration,
117+
};
118+
} catch (error: any) {
119+
const duration = Date.now() - startTime;
120+
return {
121+
check: checkKey,
122+
status: 'failed',
123+
error: error.message || 'Unknown error',
124+
duration,
125+
};
126+
}
127+
}
128+
129+
async function runCiChecks(input: z.infer<typeof runCiChecksInputSchema>): Promise<CiChecksResult> {
130+
const { checks, parallel } = input;
131+
const startTime = Date.now();
132+
133+
// Filter to only run requested checks
134+
const checksToRun = checks.filter((check) => CI_CHECKS[check as keyof typeof CI_CHECKS]);
135+
136+
if (checksToRun.length === 0) {
137+
return {
138+
success: false,
139+
results: [],
140+
totalDuration: 0,
141+
};
142+
}
143+
144+
let results: CheckResult[];
145+
146+
if (parallel) {
147+
// Run all checks in parallel
148+
const checkPromises = checksToRun.map((check) => runSingleCheck(check));
149+
const settledResults = await Promise.allSettled(checkPromises);
150+
results = settledResults.map((result, index) => {
151+
if (result.status === 'fulfilled') {
152+
return result.value;
153+
} else {
154+
// This shouldn't happen since runSingleCheck never rejects, but handle it just in case
155+
return {
156+
check: checksToRun[index],
157+
status: 'failed' as const,
158+
error: result.reason?.message || 'Unknown error',
159+
duration: 0,
160+
};
161+
}
162+
});
163+
} else {
164+
// Run checks sequentially
165+
results = [];
166+
for (const check of checksToRun) {
167+
const result = await runSingleCheck(check);
168+
results.push(result);
169+
170+
// If a check fails and we're running sequentially, we could optionally stop here
171+
// For now, continue with all checks
172+
}
173+
}
174+
175+
const totalDuration = Date.now() - startTime;
176+
const success = results.every((result) => result.status === 'passed');
177+
178+
return {
179+
success,
180+
results,
181+
totalDuration,
182+
};
183+
}
184+
185+
export const runCiChecksTool: ToolDefinition<typeof runCiChecksInputSchema> = {
186+
name: 'run_ci_checks',
187+
description:
188+
'Run CI checks similar to the Buildkite pipeline including build, quick checks, type checking, linting, and OAS snapshot validation',
189+
inputSchema: runCiChecksInputSchema,
190+
handler: async (input) => {
191+
const result = await runCiChecks(input);
192+
return {
193+
content: [
194+
{
195+
type: 'text',
196+
text: JSON.stringify(result, null, 2),
197+
},
198+
],
199+
};
200+
},
201+
};

0 commit comments

Comments
 (0)