@alexsarrell/jenkins-mcp-server 1.0.0 → 1.3.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,66 @@
1
+ import type { ParameterSpec } from "../schemas/parameter.js";
2
+ export interface PipelineSpec {
3
+ type: "pipeline";
4
+ description?: string;
5
+ disabled?: boolean;
6
+ scm: {
7
+ type: "git";
8
+ url: string;
9
+ branches?: string[];
10
+ credentialsId?: string;
11
+ jenkinsfilePath?: string;
12
+ };
13
+ triggers?: {
14
+ cron?: string;
15
+ };
16
+ parameters?: ParameterSpec[];
17
+ buildRetention?: {
18
+ numToKeep?: number;
19
+ daysToKeep?: number;
20
+ };
21
+ }
22
+ export interface MultibranchSpec {
23
+ type: "multibranch";
24
+ description?: string;
25
+ source: {
26
+ type: "git";
27
+ url: string;
28
+ credentialsId?: string;
29
+ };
30
+ jenkinsfilePath?: string;
31
+ orphanedItemStrategy?: {
32
+ numToKeep?: number;
33
+ daysToKeep?: number;
34
+ };
35
+ }
36
+ export interface FolderSpec {
37
+ type: "folder";
38
+ description?: string;
39
+ }
40
+ export type JobSpec = PipelineSpec | MultibranchSpec | FolderSpec;
41
+ export interface JobDescription {
42
+ type: "pipeline" | "multibranch" | "freestyle" | "folder" | "unknown";
43
+ description?: string;
44
+ disabled: boolean;
45
+ concurrentBuilds: boolean;
46
+ scm?: {
47
+ type: "git" | "unknown";
48
+ url?: string;
49
+ branches?: string[];
50
+ credentialsId?: string;
51
+ jenkinsfilePath?: string;
52
+ };
53
+ triggers?: {
54
+ cron?: string;
55
+ scmPolling?: string;
56
+ };
57
+ parameters?: ParameterSpec[];
58
+ buildRetention?: {
59
+ numToKeep?: number;
60
+ daysToKeep?: number;
61
+ };
62
+ unknownXmlElements: string[];
63
+ rawConfigSha: string;
64
+ }
65
+ export declare function parseJobConfig(xml: string): JobDescription;
66
+ export declare function buildJobXml(spec: JobSpec): string;
@@ -0,0 +1,415 @@
1
+ import { XMLParser, XMLBuilder } from "fast-xml-parser";
2
+ import { createHash } from "node:crypto";
3
+ import { mapJenkinsParameter } from "./parameter-mapper.js";
4
+ const parser = new XMLParser({
5
+ ignoreAttributes: false,
6
+ attributeNamePrefix: "@_",
7
+ parseAttributeValue: false,
8
+ parseTagValue: false,
9
+ trimValues: true,
10
+ });
11
+ const KNOWN_ROOTS = {
12
+ "flow-definition": "pipeline",
13
+ "org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject": "multibranch",
14
+ "com.cloudbees.hudson.plugins.folder.Folder": "folder",
15
+ project: "freestyle",
16
+ };
17
+ const PIPELINE_KNOWN = new Set(["description", "keepDependencies", "properties", "triggers", "definition", "disabled"]);
18
+ const MULTIBRANCH_KNOWN = new Set(["description", "disabled", "sources", "factory", "orphanedItemStrategy", "properties", "triggers"]);
19
+ const FREESTYLE_KNOWN = new Set(["description", "disabled", "concurrentBuild", "properties", "triggers"]);
20
+ function asArray(v) {
21
+ if (v === undefined || v === null)
22
+ return [];
23
+ return Array.isArray(v) ? v : [v];
24
+ }
25
+ function asString(v) {
26
+ if (v === undefined || v === null)
27
+ return undefined;
28
+ if (typeof v === "string")
29
+ return v;
30
+ if (typeof v === "object" && v !== null && "#text" in v)
31
+ return String(v["#text"]);
32
+ return String(v);
33
+ }
34
+ function asBoolean(v, fallback) {
35
+ const s = asString(v);
36
+ if (s === "true")
37
+ return true;
38
+ if (s === "false")
39
+ return false;
40
+ return fallback;
41
+ }
42
+ function asNumber(v) {
43
+ const s = asString(v);
44
+ if (s === undefined)
45
+ return undefined;
46
+ const n = Number(s);
47
+ return Number.isFinite(n) ? n : undefined;
48
+ }
49
+ function extractParameters(properties) {
50
+ if (!properties || typeof properties !== "object")
51
+ return undefined;
52
+ const props = properties;
53
+ const paramProp = props["hudson.model.ParametersDefinitionProperty"];
54
+ if (!paramProp || typeof paramProp !== "object")
55
+ return undefined;
56
+ const defsContainer = paramProp.parameterDefinitions;
57
+ if (!defsContainer || typeof defsContainer !== "object")
58
+ return undefined;
59
+ const defs = [];
60
+ for (const [tag, raw] of Object.entries(defsContainer)) {
61
+ for (const item of asArray(raw)) {
62
+ if (typeof item !== "object" || item === null)
63
+ continue;
64
+ const obj = item;
65
+ const name = asString(obj.name);
66
+ if (!name)
67
+ continue;
68
+ const description = asString(obj.description);
69
+ const def = {
70
+ _class: tag,
71
+ name,
72
+ description,
73
+ defaultParameterValue: { value: parseDefaultValue(tag, obj.defaultValue) },
74
+ };
75
+ if (tag === "hudson.model.ChoiceParameterDefinition") {
76
+ const choicesNode = obj.choices;
77
+ const stringList = choicesNode && typeof choicesNode === "object"
78
+ ? choicesNode.a
79
+ : undefined;
80
+ const stringEntries = stringList && typeof stringList === "object"
81
+ ? asArray(stringList.string).map(asString).filter((s) => typeof s === "string")
82
+ : [];
83
+ def.choices = stringEntries;
84
+ }
85
+ defs.push(def);
86
+ }
87
+ }
88
+ return defs.map(mapJenkinsParameter);
89
+ }
90
+ function parseDefaultValue(tag, raw) {
91
+ if (raw === undefined)
92
+ return undefined;
93
+ if (tag === "hudson.model.BooleanParameterDefinition") {
94
+ const s = asString(raw);
95
+ return s === "true";
96
+ }
97
+ return asString(raw);
98
+ }
99
+ function extractRetention(properties) {
100
+ if (!properties || typeof properties !== "object")
101
+ return undefined;
102
+ const props = properties;
103
+ const disc = props["jenkins.model.BuildDiscarderProperty"];
104
+ if (!disc || typeof disc !== "object")
105
+ return undefined;
106
+ const strategy = disc.strategy;
107
+ if (!strategy || typeof strategy !== "object")
108
+ return undefined;
109
+ const s = strategy;
110
+ const out = {};
111
+ const numToKeep = asNumber(s.numToKeep);
112
+ const daysToKeep = asNumber(s.daysToKeep);
113
+ if (numToKeep !== undefined && numToKeep > 0)
114
+ out.numToKeep = numToKeep;
115
+ if (daysToKeep !== undefined && daysToKeep > 0)
116
+ out.daysToKeep = daysToKeep;
117
+ return Object.keys(out).length > 0 ? out : undefined;
118
+ }
119
+ function extractCronTrigger(triggers) {
120
+ if (!triggers || typeof triggers !== "object")
121
+ return undefined;
122
+ const t = triggers["hudson.triggers.TimerTrigger"];
123
+ if (!t || typeof t !== "object")
124
+ return undefined;
125
+ return asString(t.spec);
126
+ }
127
+ function parsePipelineScm(definition) {
128
+ if (!definition || typeof definition !== "object")
129
+ return undefined;
130
+ const def = definition;
131
+ const scm = def.scm;
132
+ const scriptPath = asString(def.scriptPath);
133
+ if (!scm || typeof scm !== "object") {
134
+ return scriptPath ? { type: "unknown", jenkinsfilePath: scriptPath } : undefined;
135
+ }
136
+ const scmCls = scm["@_class"];
137
+ if (scmCls === "hudson.plugins.git.GitSCM") {
138
+ const remote = scm.userRemoteConfigs;
139
+ const config = remote ? remote["hudson.plugins.git.UserRemoteConfig"] : undefined;
140
+ const url = config ? asString(config.url) : undefined;
141
+ const credentialsId = config ? asString(config.credentialsId) : undefined;
142
+ const branchesNode = scm.branches;
143
+ const branches = branchesNode
144
+ ? asArray(branchesNode["hudson.plugins.git.BranchSpec"]).map((b) => asString(b.name)).filter((b) => !!b)
145
+ : undefined;
146
+ const out = { type: "git" };
147
+ if (url)
148
+ out.url = url;
149
+ if (credentialsId)
150
+ out.credentialsId = credentialsId;
151
+ if (branches && branches.length > 0)
152
+ out.branches = branches;
153
+ if (scriptPath)
154
+ out.jenkinsfilePath = scriptPath;
155
+ return out;
156
+ }
157
+ return scriptPath ? { type: "unknown", jenkinsfilePath: scriptPath } : { type: "unknown" };
158
+ }
159
+ function parseMultibranchScm(sources, factory) {
160
+ const factoryScript = factory && typeof factory === "object"
161
+ ? asString(factory.scriptPath)
162
+ : undefined;
163
+ const data = sources && typeof sources === "object"
164
+ ? sources.data
165
+ : undefined;
166
+ const branchSource = data && typeof data === "object"
167
+ ? data["jenkins.branch.BranchSource"]
168
+ : undefined;
169
+ const source = branchSource ? branchSource.source : undefined;
170
+ if (!source) {
171
+ return factoryScript ? { type: "unknown", jenkinsfilePath: factoryScript } : undefined;
172
+ }
173
+ const sourceCls = source["@_class"];
174
+ const out = { type: sourceCls === "jenkins.plugins.git.GitSCMSource" ? "git" : "unknown" };
175
+ const url = asString(source.remote);
176
+ const credentialsId = asString(source.credentialsId);
177
+ if (url)
178
+ out.url = url;
179
+ if (credentialsId)
180
+ out.credentialsId = credentialsId;
181
+ if (factoryScript)
182
+ out.jenkinsfilePath = factoryScript;
183
+ return out;
184
+ }
185
+ function listUnknownChildren(node, known) {
186
+ return Object.keys(node).filter((k) => !k.startsWith("@_") && k !== "?xml" && !known.has(k));
187
+ }
188
+ export function parseJobConfig(xml) {
189
+ const sha = createHash("sha256").update(xml).digest("hex").slice(0, 12);
190
+ const tree = parser.parse(xml);
191
+ const rootEntry = Object.entries(tree).find(([k]) => k !== "?xml");
192
+ const rootName = rootEntry?.[0] ?? "";
193
+ const rootNode = rootEntry?.[1] ?? {};
194
+ const type = KNOWN_ROOTS[rootName] ?? "unknown";
195
+ if (type === "unknown") {
196
+ return { type, disabled: false, concurrentBuilds: false, unknownXmlElements: [rootName], rawConfigSha: sha };
197
+ }
198
+ const description = asString(rootNode.description);
199
+ const disabled = asBoolean(rootNode.disabled, false);
200
+ const concurrentBuilds = type === "freestyle" ? asBoolean(rootNode.concurrentBuild, false) : false;
201
+ const properties = rootNode.properties;
202
+ const parameters = extractParameters(properties);
203
+ const buildRetention = extractRetention(properties);
204
+ const cron = extractCronTrigger(rootNode.triggers);
205
+ let scm;
206
+ let unknownChildren;
207
+ if (type === "pipeline") {
208
+ scm = parsePipelineScm(rootNode.definition);
209
+ unknownChildren = listUnknownChildren(rootNode, PIPELINE_KNOWN);
210
+ }
211
+ else if (type === "multibranch") {
212
+ scm = parseMultibranchScm(rootNode.sources, rootNode.factory);
213
+ unknownChildren = listUnknownChildren(rootNode, MULTIBRANCH_KNOWN);
214
+ }
215
+ else if (type === "freestyle") {
216
+ unknownChildren = listUnknownChildren(rootNode, FREESTYLE_KNOWN);
217
+ }
218
+ else {
219
+ unknownChildren = [];
220
+ }
221
+ const out = {
222
+ type,
223
+ disabled,
224
+ concurrentBuilds,
225
+ unknownXmlElements: unknownChildren,
226
+ rawConfigSha: sha,
227
+ };
228
+ if (description)
229
+ out.description = description;
230
+ if (scm)
231
+ out.scm = scm;
232
+ if (cron)
233
+ out.triggers = { cron };
234
+ if (parameters && parameters.length > 0)
235
+ out.parameters = parameters;
236
+ if (buildRetention)
237
+ out.buildRetention = buildRetention;
238
+ return out;
239
+ }
240
+ // ─── buildJobXml ─────────────────────────────────────────────────────────────
241
+ const builder = new XMLBuilder({
242
+ ignoreAttributes: false,
243
+ attributeNamePrefix: "@_",
244
+ format: true,
245
+ indentBy: " ",
246
+ suppressEmptyNode: false,
247
+ });
248
+ function paramDefXml(p) {
249
+ if (p.type === "string") {
250
+ return {
251
+ tag: "hudson.model.StringParameterDefinition",
252
+ body: { name: p.name, description: p.description ?? "", defaultValue: p.default ?? "", trim: p.trim ?? false },
253
+ };
254
+ }
255
+ if (p.type === "text") {
256
+ return {
257
+ tag: "hudson.model.TextParameterDefinition",
258
+ body: { name: p.name, description: p.description ?? "", defaultValue: p.default ?? "" },
259
+ };
260
+ }
261
+ if (p.type === "boolean") {
262
+ return {
263
+ tag: "hudson.model.BooleanParameterDefinition",
264
+ body: { name: p.name, description: p.description ?? "", defaultValue: String(p.default ?? false) },
265
+ };
266
+ }
267
+ if (p.type === "choice") {
268
+ return {
269
+ tag: "hudson.model.ChoiceParameterDefinition",
270
+ body: {
271
+ name: p.name,
272
+ description: p.description ?? "",
273
+ choices: { "@_class": "java.util.Arrays$ArrayList", a: { "@_class": "string-array", string: p.choices } },
274
+ },
275
+ };
276
+ }
277
+ if (p.type === "password") {
278
+ return {
279
+ tag: "hudson.model.PasswordParameterDefinition",
280
+ body: { name: p.name, description: p.description ?? "", defaultValue: "" },
281
+ };
282
+ }
283
+ if (p.type === "file") {
284
+ return { tag: "hudson.model.FileParameterDefinition", body: { name: p.name, description: p.description ?? "" } };
285
+ }
286
+ if (p.type === "run") {
287
+ return {
288
+ tag: "hudson.model.RunParameterDefinition",
289
+ body: { name: p.name, description: p.description ?? "", projectName: p.projectName ?? "" },
290
+ };
291
+ }
292
+ if (p.type === "credentials") {
293
+ return {
294
+ tag: "com.cloudbees.plugins.credentials.CredentialsParameterDefinition",
295
+ body: { name: p.name, description: p.description ?? "", credentialType: p.credentialType ?? "" },
296
+ };
297
+ }
298
+ // unknown — emit as a string parameter with a marker description
299
+ return {
300
+ tag: "hudson.model.StringParameterDefinition",
301
+ body: { name: p.name, description: `[UNKNOWN ORIGINAL TYPE: ${p.rawType}] ${p.description ?? ""}`, defaultValue: "" },
302
+ };
303
+ }
304
+ function buildPipelineRoot(spec) {
305
+ const properties = {};
306
+ if (spec.parameters && spec.parameters.length > 0) {
307
+ const grouped = {};
308
+ for (const p of spec.parameters) {
309
+ const { tag, body } = paramDefXml(p);
310
+ (grouped[tag] ??= []).push(body);
311
+ }
312
+ properties["hudson.model.ParametersDefinitionProperty"] = { parameterDefinitions: grouped };
313
+ }
314
+ if (spec.buildRetention) {
315
+ properties["jenkins.model.BuildDiscarderProperty"] = {
316
+ strategy: {
317
+ "@_class": "hudson.tasks.LogRotator",
318
+ daysToKeep: String(spec.buildRetention.daysToKeep ?? -1),
319
+ numToKeep: String(spec.buildRetention.numToKeep ?? -1),
320
+ artifactDaysToKeep: "-1",
321
+ artifactNumToKeep: "-1",
322
+ },
323
+ };
324
+ }
325
+ const triggers = {};
326
+ if (spec.triggers?.cron) {
327
+ triggers["hudson.triggers.TimerTrigger"] = { spec: spec.triggers.cron };
328
+ }
329
+ const definition = {
330
+ "@_class": "org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition",
331
+ "@_plugin": "workflow-cps",
332
+ scm: {
333
+ "@_class": "hudson.plugins.git.GitSCM",
334
+ "@_plugin": "git",
335
+ userRemoteConfigs: {
336
+ "hudson.plugins.git.UserRemoteConfig": {
337
+ url: spec.scm.url,
338
+ ...(spec.scm.credentialsId ? { credentialsId: spec.scm.credentialsId } : {}),
339
+ },
340
+ },
341
+ branches: {
342
+ "hudson.plugins.git.BranchSpec": (spec.scm.branches ?? ["*/main"]).map((name) => ({ name })),
343
+ },
344
+ },
345
+ scriptPath: spec.scm.jenkinsfilePath ?? "Jenkinsfile",
346
+ lightweight: "true",
347
+ };
348
+ return {
349
+ "flow-definition": {
350
+ "@_plugin": "workflow-job",
351
+ description: spec.description ?? "",
352
+ keepDependencies: "false",
353
+ ...(Object.keys(properties).length > 0 ? { properties } : { properties: "" }),
354
+ ...(Object.keys(triggers).length > 0 ? { triggers } : {}),
355
+ definition,
356
+ disabled: String(spec.disabled ?? false),
357
+ },
358
+ };
359
+ }
360
+ function buildMultibranchRoot(spec) {
361
+ return {
362
+ "org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject": {
363
+ "@_plugin": "workflow-multibranch",
364
+ description: spec.description ?? "",
365
+ disabled: "false",
366
+ sources: {
367
+ "@_class": "jenkins.branch.MultiBranchProject$BranchSourceList",
368
+ data: {
369
+ "jenkins.branch.BranchSource": {
370
+ source: {
371
+ "@_class": "jenkins.plugins.git.GitSCMSource",
372
+ "@_plugin": "git",
373
+ id: "1",
374
+ remote: spec.source.url,
375
+ ...(spec.source.credentialsId ? { credentialsId: spec.source.credentialsId } : {}),
376
+ },
377
+ },
378
+ },
379
+ },
380
+ factory: {
381
+ "@_class": "org.jenkinsci.plugins.workflow.multibranch.WorkflowBranchProjectFactory",
382
+ scriptPath: spec.jenkinsfilePath ?? "Jenkinsfile",
383
+ },
384
+ ...(spec.orphanedItemStrategy
385
+ ? {
386
+ orphanedItemStrategy: {
387
+ "@_class": "com.cloudbees.hudson.plugins.folder.computed.DefaultOrphanedItemStrategy",
388
+ pruneDeadBranches: "true",
389
+ daysToKeep: String(spec.orphanedItemStrategy.daysToKeep ?? -1),
390
+ numToKeep: String(spec.orphanedItemStrategy.numToKeep ?? -1),
391
+ },
392
+ }
393
+ : {}),
394
+ },
395
+ };
396
+ }
397
+ function buildFolderRoot(spec) {
398
+ return {
399
+ "com.cloudbees.hudson.plugins.folder.Folder": {
400
+ "@_plugin": "cloudbees-folder",
401
+ description: spec.description ?? "",
402
+ },
403
+ };
404
+ }
405
+ export function buildJobXml(spec) {
406
+ let body;
407
+ if (spec.type === "pipeline")
408
+ body = buildPipelineRoot(spec);
409
+ else if (spec.type === "multibranch")
410
+ body = buildMultibranchRoot(spec);
411
+ else
412
+ body = buildFolderRoot(spec);
413
+ const xml = builder.build(body);
414
+ return `<?xml version='1.1' encoding='UTF-8'?>\n${xml}`;
415
+ }
@@ -0,0 +1,20 @@
1
+ export interface GrepOptions {
2
+ pattern: string;
3
+ regex?: boolean;
4
+ before?: number;
5
+ after?: number;
6
+ maxMatches?: number;
7
+ }
8
+ export interface GrepMatch {
9
+ lineNumber: number;
10
+ text: string;
11
+ context: Array<{
12
+ lineNumber: number;
13
+ text: string;
14
+ }>;
15
+ }
16
+ export interface GrepResult {
17
+ matches: GrepMatch[];
18
+ truncated: boolean;
19
+ }
20
+ export declare function grepLog(text: string, opts: GrepOptions): GrepResult;
@@ -0,0 +1,43 @@
1
+ function buildMatcher(pattern, regex) {
2
+ if (regex) {
3
+ let re;
4
+ try {
5
+ re = new RegExp(pattern);
6
+ }
7
+ catch (err) {
8
+ const msg = err instanceof Error ? err.message : String(err);
9
+ throw new Error(`Invalid regex: ${msg}`);
10
+ }
11
+ return (s) => re.test(s);
12
+ }
13
+ const lower = pattern.toLowerCase();
14
+ return (s) => s.toLowerCase().includes(lower);
15
+ }
16
+ export function grepLog(text, opts) {
17
+ const before = opts.before ?? 0;
18
+ const after = opts.after ?? 0;
19
+ const maxMatches = opts.maxMatches ?? 50;
20
+ const matches = [];
21
+ const lines = text.split("\n");
22
+ const matcher = buildMatcher(opts.pattern, opts.regex ?? false);
23
+ for (let i = 0; i < lines.length; i++) {
24
+ if (!matcher(lines[i]))
25
+ continue;
26
+ const ctxStart = Math.max(0, i - before);
27
+ const ctxEnd = Math.min(lines.length - 1, i + after);
28
+ const context = [];
29
+ for (let j = ctxStart; j <= ctxEnd; j++) {
30
+ context.push({ lineNumber: j + 1, text: lines[j] });
31
+ }
32
+ matches.push({ lineNumber: i + 1, text: lines[i], context });
33
+ if (matches.length >= maxMatches) {
34
+ // Check if there are any further matches; if so, signal truncation.
35
+ for (let k = i + 1; k < lines.length; k++) {
36
+ if (matcher(lines[k]))
37
+ return { matches, truncated: true };
38
+ }
39
+ return { matches, truncated: false };
40
+ }
41
+ }
42
+ return { matches, truncated: false };
43
+ }
@@ -0,0 +1,13 @@
1
+ import type { ParameterSpec } from "../schemas/parameter.js";
2
+ export interface JenkinsParameterDefinition {
3
+ _class?: string;
4
+ name: string;
5
+ description?: string;
6
+ defaultParameterValue?: {
7
+ value?: string | boolean | number;
8
+ };
9
+ choices?: string[];
10
+ projectName?: string;
11
+ credentialType?: string;
12
+ }
13
+ export declare function mapJenkinsParameter(p: JenkinsParameterDefinition): ParameterSpec;
@@ -0,0 +1,70 @@
1
+ export function mapJenkinsParameter(p) {
2
+ const cls = p._class || "";
3
+ const last = cls.split(".").pop() || "";
4
+ const desc = p.description || undefined;
5
+ const rawDefault = p.defaultParameterValue?.value;
6
+ if (last === "StringParameterDefinition") {
7
+ const out = { type: "string", name: p.name };
8
+ if (typeof rawDefault === "string" && rawDefault !== "")
9
+ out.default = rawDefault;
10
+ if (desc)
11
+ out.description = desc;
12
+ return out;
13
+ }
14
+ if (last === "TextParameterDefinition") {
15
+ const out = { type: "text", name: p.name };
16
+ if (typeof rawDefault === "string" && rawDefault !== "")
17
+ out.default = rawDefault;
18
+ if (desc)
19
+ out.description = desc;
20
+ return out;
21
+ }
22
+ if (last === "BooleanParameterDefinition") {
23
+ const out = { type: "boolean", name: p.name };
24
+ if (typeof rawDefault === "boolean")
25
+ out.default = rawDefault;
26
+ if (desc)
27
+ out.description = desc;
28
+ return out;
29
+ }
30
+ if (last === "ChoiceParameterDefinition" && p.choices && p.choices.length > 0) {
31
+ const out = { type: "choice", name: p.name, choices: p.choices };
32
+ if (typeof rawDefault === "string")
33
+ out.default = rawDefault;
34
+ if (desc)
35
+ out.description = desc;
36
+ return out;
37
+ }
38
+ if (last === "PasswordParameterDefinition") {
39
+ const out = { type: "password", name: p.name };
40
+ if (desc)
41
+ out.description = desc;
42
+ return out;
43
+ }
44
+ if (last === "FileParameterDefinition") {
45
+ const out = { type: "file", name: p.name };
46
+ if (desc)
47
+ out.description = desc;
48
+ return out;
49
+ }
50
+ if (last === "RunParameterDefinition") {
51
+ const out = { type: "run", name: p.name };
52
+ if (p.projectName)
53
+ out.projectName = p.projectName;
54
+ if (desc)
55
+ out.description = desc;
56
+ return out;
57
+ }
58
+ if (last === "CredentialsParameterDefinition") {
59
+ const out = { type: "credentials", name: p.name };
60
+ if (p.credentialType)
61
+ out.credentialType = p.credentialType;
62
+ if (desc)
63
+ out.description = desc;
64
+ return out;
65
+ }
66
+ const fallback = { type: "unknown", name: p.name, rawType: cls };
67
+ if (desc)
68
+ fallback.description = desc;
69
+ return fallback;
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexsarrell/jenkins-mcp-server",
3
- "version": "1.0.0",
3
+ "version": "1.3.0",
4
4
  "description": "Jenkins MCP server for Claude Code integration",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -8,8 +8,12 @@
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsc",
11
+ "lint": "eslint src/",
12
+ "lint:fix": "eslint src/ --fix",
11
13
  "dev": "tsc --watch",
12
- "start": "node dist/index.js"
14
+ "start": "node dist/index.js",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest"
13
17
  },
14
18
  "type": "module",
15
19
  "files": [
@@ -23,10 +27,16 @@
23
27
  "license": "MIT",
24
28
  "dependencies": {
25
29
  "@modelcontextprotocol/sdk": "^1.27.1",
30
+ "fast-xml-parser": "^4.5.6",
26
31
  "zod": "^4.3.6"
27
32
  },
28
33
  "devDependencies": {
34
+ "@eslint/js": "9.28.0",
29
35
  "@types/node": "^25.5.0",
30
- "typescript": "^5.9.3"
36
+ "@vitest/coverage-v8": "^3.2.4",
37
+ "eslint": "9.28.0",
38
+ "typescript": "^5.9.3",
39
+ "typescript-eslint": "8.57.2",
40
+ "vitest": "^3.2.4"
31
41
  }
32
42
  }