@alexsarrell/jenkins-mcp-server 1.0.0

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,110 @@
1
+ import { z } from "zod";
2
+ import { formatQueue, ok, error } from "../utils/formatters.js";
3
+ export function registerDiscoveryTools(client, register) {
4
+ // 16. searchBuildLogs
5
+ register("searchBuildLogs", "Search build logs for a text pattern across recent builds. Like grep for Jenkins logs. Searches the last N builds (default: 5) and returns matching lines with context.", z.object({
6
+ jobPath: z.string().describe("Full job path"),
7
+ pattern: z.string().describe("Text pattern to search for (case-insensitive substring match)"),
8
+ buildNumber: z.number().optional().describe("Search a specific build number only"),
9
+ lastN: z.number().optional().default(5).describe("Number of recent builds to search (default: 5, max: 20)"),
10
+ }), async (args) => {
11
+ const jobPath = args.jobPath;
12
+ const pattern = args.pattern;
13
+ const buildNumber = args.buildNumber;
14
+ const lastN = Math.min(args.lastN || 5, 20);
15
+ try {
16
+ const patternLower = pattern.toLowerCase();
17
+ const results = [];
18
+ if (buildNumber) {
19
+ // Search specific build
20
+ const matches = await searchBuildLog(client, jobPath, buildNumber, patternLower);
21
+ if (matches.length > 0) {
22
+ results.push(`Build #${buildNumber} (${matches.length} matches):`);
23
+ results.push(...matches.map((m) => ` L${m.line}: ${m.text}`));
24
+ }
25
+ }
26
+ else {
27
+ // Get recent builds
28
+ const data = await client.get(jobPath, "/api/json", {
29
+ tree: "builds[number]{0," + lastN + "}",
30
+ });
31
+ const jobData = data;
32
+ const builds = jobData.builds || [];
33
+ for (const build of builds) {
34
+ const matches = await searchBuildLog(client, jobPath, build.number, patternLower);
35
+ if (matches.length > 0) {
36
+ results.push(`Build #${build.number} (${matches.length} matches):`);
37
+ results.push(...matches.slice(0, 20).map((m) => ` L${m.line}: ${m.text}`));
38
+ if (matches.length > 20) {
39
+ results.push(` ... and ${matches.length - 20} more matches`);
40
+ }
41
+ results.push("");
42
+ }
43
+ }
44
+ }
45
+ if (results.length === 0) {
46
+ return ok(`No matches found for "${pattern}" in ${buildNumber ? `build #${buildNumber}` : `last ${lastN} builds`}.`);
47
+ }
48
+ return ok(`Search results for "${pattern}":\n\n${results.join("\n")}`);
49
+ }
50
+ catch (e) {
51
+ return handleError(e);
52
+ }
53
+ });
54
+ // 17. getQueue
55
+ register("getQueue", "View the Jenkins build queue - shows jobs waiting to be built, why they're waiting, and if they're stuck.", z.object({}), async () => {
56
+ try {
57
+ const data = await client.getAbsolute("/queue/api/json", {
58
+ tree: "items[id,task[name,url],why,buildableStartMilliseconds,stuck,blocked]",
59
+ });
60
+ const queue = data;
61
+ return ok(formatQueue(queue.items || []));
62
+ }
63
+ catch (e) {
64
+ return handleError(e);
65
+ }
66
+ });
67
+ // 18. enableDisableJob
68
+ register("enableDisableJob", "Enable or disable a Jenkins job. Disabled jobs cannot be triggered.", z.object({
69
+ jobPath: z.string().describe("Full job path"),
70
+ enabled: z.boolean().describe("true to enable, false to disable"),
71
+ }), async (args) => {
72
+ const jobPath = args.jobPath;
73
+ const enabled = args.enabled;
74
+ try {
75
+ const action = enabled ? "/enable" : "/disable";
76
+ await client.post(jobPath, action);
77
+ return ok(`Job ${jobPath} ${enabled ? "enabled" : "disabled"} successfully.`);
78
+ }
79
+ catch (e) {
80
+ return handleError(e);
81
+ }
82
+ });
83
+ }
84
+ async function searchBuildLog(client, jobPath, buildNumber, patternLower) {
85
+ try {
86
+ const logText = await client.getRaw(jobPath, `/${buildNumber}/consoleText`);
87
+ const lines = logText.split("\n");
88
+ const matches = [];
89
+ for (let i = 0; i < lines.length; i++) {
90
+ if (lines[i].toLowerCase().includes(patternLower)) {
91
+ matches.push({
92
+ line: i + 1,
93
+ text: lines[i].substring(0, 200), // Truncate long lines
94
+ });
95
+ }
96
+ }
97
+ return matches;
98
+ }
99
+ catch {
100
+ return [];
101
+ }
102
+ }
103
+ function handleError(e) {
104
+ if (e && typeof e === "object" && "errorCode" in e) {
105
+ const je = e;
106
+ return error(`[${je.errorCode}] ${je.message}`);
107
+ }
108
+ const msg = e instanceof Error ? e.message : String(e);
109
+ return error(msg);
110
+ }
@@ -0,0 +1,4 @@
1
+ import { z } from "zod";
2
+ import type { JenkinsClient } from "../jenkins-client.js";
3
+ import type { ToolResult } from "../types.js";
4
+ export declare function registerJobTools(client: JenkinsClient, register: (name: string, description: string, schema: z.ZodType, handler: (args: Record<string, unknown>) => Promise<ToolResult>) => void): void;
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ import { formatJobList, formatJobDetail, ok, error, truncateText } from "../utils/formatters.js";
3
+ export function registerJobTools(client, register) {
4
+ // 1. getJobs
5
+ register("getJobs", "List Jenkins jobs at root level or in a specific folder. Returns job names, status, and last build info. For multibranch pipelines, lists branches as sub-jobs.", z.object({
6
+ folder: z.string().optional().describe("Folder path (e.g., 'my-folder' or 'folder/subfolder'). Omit for root level."),
7
+ limit: z.number().optional().default(50).describe("Maximum number of jobs to return (default: 50)"),
8
+ }), async (args) => {
9
+ const folder = args.folder;
10
+ const limit = args.limit || 50;
11
+ try {
12
+ const tree = "jobs[name,url,color,description,fullName,lastBuild[number,result,timestamp]]";
13
+ let data;
14
+ if (folder) {
15
+ data = await client.get(folder, "/api/json", { tree });
16
+ }
17
+ else {
18
+ data = await client.getAbsolute("/api/json", { tree });
19
+ }
20
+ const result = data;
21
+ const jobs = (result.jobs || []).slice(0, limit);
22
+ return ok(formatJobList(jobs));
23
+ }
24
+ catch (e) {
25
+ return handleError(e);
26
+ }
27
+ });
28
+ // 2. getJob
29
+ register("getJob", "Get detailed information about a specific Jenkins job including status, health, parameter definitions, and last build info. For multibranch pipelines, also lists branches.", z.object({
30
+ jobPath: z.string().describe("Full job path (e.g., 'my-folder/my-job' or 'pipeline/main'). For branches with slashes, use :: separator: 'pipeline::feature/branch'"),
31
+ }), async (args) => {
32
+ const jobPath = args.jobPath;
33
+ try {
34
+ const tree = "name,fullName,url,color,description,buildable,healthReport[description,score],lastBuild[number,result,timestamp,url],lastSuccessfulBuild[number,url],lastFailedBuild[number,url],property[parameterDefinitions[name,type,description,defaultParameterValue[value]]],jobs[name,color,url,fullName]";
35
+ const data = await client.get(jobPath, "/api/json", { tree });
36
+ return ok(formatJobDetail(data));
37
+ }
38
+ catch (e) {
39
+ return handleError(e);
40
+ }
41
+ });
42
+ // 3. getJobConfig
43
+ register("getJobConfig", "Get the XML configuration of a Jenkins job (config.xml). Useful for understanding job setup, viewing pipeline definitions, or preparing edits.", z.object({
44
+ jobPath: z.string().describe("Full job path (e.g., 'my-folder/my-job')"),
45
+ }), async (args) => {
46
+ const jobPath = args.jobPath;
47
+ try {
48
+ const xml = await client.getRaw(jobPath, "/config.xml");
49
+ return ok(truncateText(xml));
50
+ }
51
+ catch (e) {
52
+ return handleError(e);
53
+ }
54
+ });
55
+ // 4. updateJobConfig
56
+ register("updateJobConfig", "Update a Jenkins job's XML configuration. Send the complete config.xml content. Use getJobConfig first to read the current config, modify it, then submit.", z.object({
57
+ jobPath: z.string().describe("Full job path (e.g., 'my-folder/my-job')"),
58
+ configXml: z.string().describe("Complete XML configuration for the job"),
59
+ }), async (args) => {
60
+ const jobPath = args.jobPath;
61
+ const configXml = args.configXml;
62
+ try {
63
+ await client.post(jobPath, "/config.xml", configXml, "application/xml");
64
+ return ok(`Job config updated successfully: ${jobPath}`);
65
+ }
66
+ catch (e) {
67
+ return handleError(e);
68
+ }
69
+ });
70
+ }
71
+ function handleError(e) {
72
+ if (e && typeof e === "object" && "errorCode" in e) {
73
+ const je = e;
74
+ return error(`[${je.errorCode}] ${je.message}`);
75
+ }
76
+ const msg = e instanceof Error ? e.message : String(e);
77
+ return error(msg);
78
+ }
@@ -0,0 +1,4 @@
1
+ import { z } from "zod";
2
+ import type { JenkinsClient } from "../jenkins-client.js";
3
+ import type { ToolResult } from "../types.js";
4
+ export declare function registerPipelineTools(client: JenkinsClient, register: (name: string, description: string, schema: z.ZodType, handler: (args: Record<string, unknown>) => Promise<ToolResult>) => void): void;
@@ -0,0 +1,208 @@
1
+ import { z } from "zod";
2
+ import { formatStages, formatDuration, ok, error, truncateText } from "../utils/formatters.js";
3
+ export function registerPipelineTools(client, register) {
4
+ // 11. getPipelineStages
5
+ register("getPipelineStages", "Get pipeline stages overview for a build - shows stage names, status, and duration. Works with Pipeline (Workflow) jobs. Useful for understanding which stage failed.", z.object({
6
+ jobPath: z.string().describe("Full job path"),
7
+ buildNumber: z.number().optional().describe("Build number (default: last build)"),
8
+ }), async (args) => {
9
+ const jobPath = args.jobPath;
10
+ const buildNumber = args.buildNumber;
11
+ const num = buildNumber ?? "lastBuild";
12
+ try {
13
+ const data = await client.get(jobPath, `/${num}/wfapi/describe`);
14
+ const run = data;
15
+ if (!run.stages || run.stages.length === 0) {
16
+ return ok("No pipeline stages found. This may not be a Pipeline job, or the build hasn't started stages yet.");
17
+ }
18
+ return ok(formatStages(run.stages));
19
+ }
20
+ catch (e) {
21
+ return handleError(e, "This endpoint requires a Pipeline (Workflow) job. Classic Freestyle jobs don't have stages.");
22
+ }
23
+ });
24
+ // 12. getStageLog
25
+ register("getStageLog", "Get the console log for a specific pipeline stage. Provide the stage name as it appears in getPipelineStages output.", z.object({
26
+ jobPath: z.string().describe("Full job path"),
27
+ buildNumber: z.number().describe("Build number"),
28
+ stageName: z.string().describe("Stage name (as shown in getPipelineStages)"),
29
+ }), async (args) => {
30
+ const jobPath = args.jobPath;
31
+ const buildNumber = args.buildNumber;
32
+ const stageName = args.stageName;
33
+ try {
34
+ // First, get the pipeline description to find the stage ID
35
+ const data = await client.get(jobPath, `/${buildNumber}/wfapi/describe`);
36
+ const run = data;
37
+ const stage = run.stages?.find((s) => s.name.toLowerCase() === stageName.toLowerCase());
38
+ if (!stage) {
39
+ const available = run.stages?.map((s) => s.name).join(", ") || "none";
40
+ return error(`Stage "${stageName}" not found.`, `Available stages: ${available}`);
41
+ }
42
+ // Get the stage log using the node ID
43
+ const logData = await client.getRaw(jobPath, `/${buildNumber}/execution/node/${stage.id}/wfapi/log`);
44
+ // The wfapi/log endpoint returns JSON with a text field
45
+ let logText;
46
+ try {
47
+ const parsed = JSON.parse(logData);
48
+ logText = parsed.text;
49
+ }
50
+ catch {
51
+ // If it's not JSON, use raw text
52
+ logText = logData;
53
+ }
54
+ // Strip HTML tags that Jenkins sometimes includes
55
+ logText = logText.replace(/<[^>]*>/g, "");
56
+ return ok(truncateText(`--- Stage Log: ${stage.name} (${stage.status}, ${formatDuration(stage.durationMillis)}) ---\n\n${logText}`));
57
+ }
58
+ catch (e) {
59
+ return handleError(e);
60
+ }
61
+ });
62
+ // 13. getPipelineScript
63
+ register("getPipelineScript", "Get the Jenkinsfile/pipeline script used in a build. Fetches from the replay page. Useful for reviewing or modifying the script before replaying.", z.object({
64
+ jobPath: z.string().describe("Full job path"),
65
+ buildNumber: z.number().describe("Build number"),
66
+ }), async (args) => {
67
+ const jobPath = args.jobPath;
68
+ const buildNumber = args.buildNumber;
69
+ try {
70
+ const html = await client.getRaw(jobPath, `/${buildNumber}/replay`);
71
+ // Parse the main script from the replay page
72
+ // The replay page has a <textarea> with class "ace-editor" or name "_.mainScript"
73
+ const mainScriptMatch = html.match(/name="_.mainScript"[^>]*>([\s\S]*?)<\/textarea>/);
74
+ const scripts = [];
75
+ if (mainScriptMatch) {
76
+ // Decode HTML entities
77
+ const script = decodeHtmlEntities(mainScriptMatch[1]);
78
+ scripts.push({ name: "Jenkinsfile (main)", content: script });
79
+ }
80
+ // Also look for loaded library scripts
81
+ const scriptBlocks = html.matchAll(/name="_.([^"]+)"[^>]*class="[^"]*jenkins-readonly-crumb[^"]*"[^>]*>([\s\S]*?)<\/textarea>/g);
82
+ for (const match of scriptBlocks) {
83
+ if (match[1] !== "mainScript") {
84
+ scripts.push({
85
+ name: match[1],
86
+ content: decodeHtmlEntities(match[2]),
87
+ });
88
+ }
89
+ }
90
+ // Try alternative pattern if nothing found
91
+ if (scripts.length === 0) {
92
+ // Try finding any textarea with script content
93
+ const textareaMatches = html.matchAll(/<textarea[^>]*name="([^"]*)"[^>]*>([\s\S]*?)<\/textarea>/g);
94
+ for (const match of textareaMatches) {
95
+ const name = match[1].replace("_.", "");
96
+ if (name && match[2].trim()) {
97
+ scripts.push({
98
+ name,
99
+ content: decodeHtmlEntities(match[2]),
100
+ });
101
+ }
102
+ }
103
+ }
104
+ if (scripts.length === 0) {
105
+ return error("Could not extract pipeline script from replay page.", "The build may not be a Pipeline job, or replay may not be available.");
106
+ }
107
+ const lines = [];
108
+ for (const s of scripts) {
109
+ lines.push(`=== ${s.name} ===`);
110
+ lines.push(s.content);
111
+ lines.push("");
112
+ }
113
+ return ok(truncateText(lines.join("\n")));
114
+ }
115
+ catch (e) {
116
+ return handleError(e, "Replay page may not be available. Ensure the build is a Pipeline job.");
117
+ }
118
+ });
119
+ // 14. replayBuild
120
+ register("replayBuild", "Replay a pipeline build with optional script modifications. If no script is provided, replays with the same script. Use getPipelineScript first to get the current script, modify it, then replay.", z.object({
121
+ jobPath: z.string().describe("Full job path"),
122
+ buildNumber: z.number().describe("Build number to replay"),
123
+ mainScript: z.string().optional().describe("Modified Jenkinsfile content. If omitted, replays with the original script."),
124
+ }), async (args) => {
125
+ const jobPath = args.jobPath;
126
+ const buildNumber = args.buildNumber;
127
+ let mainScript = args.mainScript;
128
+ try {
129
+ // If no script provided, fetch current one
130
+ if (!mainScript) {
131
+ const html = await client.getRaw(jobPath, `/${buildNumber}/replay`);
132
+ const match = html.match(/name="_.mainScript"[^>]*>([\s\S]*?)<\/textarea>/);
133
+ if (match) {
134
+ mainScript = decodeHtmlEntities(match[1]);
135
+ }
136
+ else {
137
+ return error("Could not extract current pipeline script for replay.", "Try providing the script explicitly via the mainScript parameter.");
138
+ }
139
+ }
140
+ // Submit replay
141
+ const formData = new URLSearchParams();
142
+ formData.set("mainScript", mainScript);
143
+ formData.set("json", JSON.stringify({ mainScript }));
144
+ const result = await client.postForm(jobPath, `/${buildNumber}/replay/run`, formData);
145
+ // The response redirects to the new build page
146
+ const location = result.response.headers.get("location");
147
+ if (location) {
148
+ return ok(`Replay triggered successfully.\nNew build: ${location}`);
149
+ }
150
+ return ok(`Replay triggered for ${jobPath} build #${buildNumber}.`);
151
+ }
152
+ catch (e) {
153
+ return handleError(e, "Replay requires Pipeline job type and appropriate permissions.");
154
+ }
155
+ });
156
+ // 15. restartFromStage
157
+ register("restartFromStage", "Restart a pipeline from a specific stage. Requires the Declarative Pipeline plugin with 'Restart from Stage' support. Only works with top-level stages in Declarative pipelines and completed builds.", z.object({
158
+ jobPath: z.string().describe("Full job path"),
159
+ buildNumber: z.number().describe("Build number"),
160
+ stageName: z.string().describe("Stage name to restart from"),
161
+ }), async (args) => {
162
+ const jobPath = args.jobPath;
163
+ const buildNumber = args.buildNumber;
164
+ const stageName = args.stageName;
165
+ try {
166
+ // First verify the stage exists
167
+ const data = await client.get(jobPath, `/${buildNumber}/wfapi/describe`);
168
+ const run = data;
169
+ const stage = run.stages?.find((s) => s.name.toLowerCase() === stageName.toLowerCase());
170
+ if (!stage) {
171
+ const available = run.stages?.map((s) => s.name).join(", ") || "none";
172
+ return error(`Stage "${stageName}" not found.`, `Available stages: ${available}`);
173
+ }
174
+ // Try the restart endpoint
175
+ // This uses the Pipeline: Stage Step plugin's restart functionality
176
+ const formData = new URLSearchParams();
177
+ formData.set("stageName", stage.name);
178
+ formData.set("json", JSON.stringify({ stageName: stage.name }));
179
+ const result = await client.postForm(jobPath, `/${buildNumber}/restart/restart`, formData);
180
+ const location = result.response.headers.get("location");
181
+ if (location) {
182
+ return ok(`Restart from stage "${stage.name}" triggered.\nNew build: ${location}`);
183
+ }
184
+ return ok(`Restart from stage "${stage.name}" triggered for ${jobPath} build #${buildNumber}.`);
185
+ }
186
+ catch (e) {
187
+ return handleError(e, "Restart from stage requires: (1) Declarative Pipeline plugin, (2) a completed build, (3) a top-level stage. Not all Jenkins installations support this.");
188
+ }
189
+ });
190
+ }
191
+ function decodeHtmlEntities(text) {
192
+ return text
193
+ .replace(/&amp;/g, "&")
194
+ .replace(/&lt;/g, "<")
195
+ .replace(/&gt;/g, ">")
196
+ .replace(/&quot;/g, '"')
197
+ .replace(/&#39;/g, "'")
198
+ .replace(/&#x27;/g, "'")
199
+ .replace(/&#x2F;/g, "/");
200
+ }
201
+ function handleError(e, suggestion) {
202
+ if (e && typeof e === "object" && "errorCode" in e) {
203
+ const je = e;
204
+ return error(`[${je.errorCode}] ${je.message}`, suggestion);
205
+ }
206
+ const msg = e instanceof Error ? e.message : String(e);
207
+ return error(msg, suggestion);
208
+ }
@@ -0,0 +1,143 @@
1
+ export interface JenkinsConfig {
2
+ url: string;
3
+ user: string;
4
+ token: string;
5
+ }
6
+ export interface JenkinsJob {
7
+ _class: string;
8
+ name: string;
9
+ url: string;
10
+ color: string;
11
+ description: string | null;
12
+ fullName: string;
13
+ buildable?: boolean;
14
+ healthReport?: Array<{
15
+ description: string;
16
+ score: number;
17
+ }>;
18
+ lastBuild?: {
19
+ number: number;
20
+ result: string | null;
21
+ timestamp: number;
22
+ url: string;
23
+ };
24
+ lastSuccessfulBuild?: {
25
+ number: number;
26
+ url: string;
27
+ };
28
+ lastFailedBuild?: {
29
+ number: number;
30
+ url: string;
31
+ };
32
+ jobs?: JenkinsJob[];
33
+ property?: Array<{
34
+ _class: string;
35
+ parameterDefinitions?: Array<{
36
+ name: string;
37
+ type: string;
38
+ description: string;
39
+ defaultParameterValue?: {
40
+ value: string;
41
+ };
42
+ }>;
43
+ }>;
44
+ }
45
+ export interface JenkinsBuild {
46
+ _class: string;
47
+ number: number;
48
+ url: string;
49
+ result: string | null;
50
+ building: boolean;
51
+ duration: number;
52
+ estimatedDuration: number;
53
+ timestamp: number;
54
+ displayName: string;
55
+ description: string | null;
56
+ fullDisplayName: string;
57
+ actions?: Array<{
58
+ _class: string;
59
+ causes?: Array<{
60
+ shortDescription: string;
61
+ userName?: string;
62
+ }>;
63
+ }>;
64
+ artifacts?: BuildArtifact[];
65
+ changeSets?: Array<{
66
+ items: Array<{
67
+ msg: string;
68
+ author: {
69
+ fullName: string;
70
+ };
71
+ commitId: string;
72
+ }>;
73
+ }>;
74
+ }
75
+ export interface BuildArtifact {
76
+ displayPath: string;
77
+ fileName: string;
78
+ relativePath: string;
79
+ }
80
+ export interface PipelineStage {
81
+ id: string;
82
+ name: string;
83
+ status: string;
84
+ startTimeMillis: number;
85
+ durationMillis: number;
86
+ pauseDurationMillis: number;
87
+ stageFlowNodes?: Array<{
88
+ id: string;
89
+ name: string;
90
+ status: {
91
+ result: string;
92
+ };
93
+ }>;
94
+ }
95
+ export interface PipelineRun {
96
+ id: string;
97
+ name: string;
98
+ status: string;
99
+ startTimeMillis: number;
100
+ durationMillis: number;
101
+ stages: PipelineStage[];
102
+ }
103
+ export interface TestResult {
104
+ failCount: number;
105
+ passCount: number;
106
+ skipCount: number;
107
+ totalCount: number;
108
+ suites?: Array<{
109
+ name: string;
110
+ cases: Array<{
111
+ className: string;
112
+ name: string;
113
+ status: string;
114
+ duration: number;
115
+ errorDetails?: string;
116
+ errorStackTrace?: string;
117
+ }>;
118
+ }>;
119
+ }
120
+ export interface QueueItem {
121
+ id: number;
122
+ task: {
123
+ name: string;
124
+ url: string;
125
+ };
126
+ why: string | null;
127
+ buildableStartMilliseconds: number;
128
+ stuck: boolean;
129
+ blocked: boolean;
130
+ }
131
+ export interface JenkinsError {
132
+ statusCode: number;
133
+ message: string;
134
+ errorCode: string;
135
+ }
136
+ export interface ToolResult {
137
+ [key: string]: unknown;
138
+ content: Array<{
139
+ type: "text";
140
+ text: string;
141
+ }>;
142
+ isError?: boolean;
143
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { JenkinsJob, JenkinsBuild, PipelineStage, QueueItem, ToolResult } from "../types.js";
2
+ export declare function truncateText(text: string, maxBytes?: number): string;
3
+ export declare function formatJobStatus(color: string): string;
4
+ export declare function formatTimestamp(ts: number): string;
5
+ export declare function formatDuration(ms: number): string;
6
+ export declare function formatJobList(jobs: JenkinsJob[]): string;
7
+ export declare function formatJobDetail(job: JenkinsJob): string;
8
+ export declare function formatBuild(build: JenkinsBuild): string;
9
+ export declare function formatStages(stages: PipelineStage[]): string;
10
+ export declare function formatQueue(items: QueueItem[]): string;
11
+ export declare function ok(text: string): ToolResult;
12
+ export declare function error(message: string, suggestion?: string): ToolResult;