@rishibhushan/jenkins-mcp-server 1.0.6 ā 1.1.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.
- package/README.md +584 -18
- package/package.json +1 -1
- package/requirements.txt +1 -0
- package/src/jenkins_mcp_server/__init__.py +19 -11
- package/src/jenkins_mcp_server/cache.py +310 -0
- package/src/jenkins_mcp_server/config.py +54 -1
- package/src/jenkins_mcp_server/jenkins_client.py +69 -80
- package/src/jenkins_mcp_server/metrics.py +358 -0
- package/src/jenkins_mcp_server/server.py +1015 -108
|
@@ -7,9 +7,11 @@ Provides MCP protocol handlers for Jenkins operations including:
|
|
|
7
7
|
- Tools (Jenkins operations)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
import asyncio
|
|
10
11
|
import json
|
|
11
12
|
import logging
|
|
12
13
|
import sys
|
|
14
|
+
import time
|
|
13
15
|
from typing import Optional
|
|
14
16
|
|
|
15
17
|
import mcp.server.stdio
|
|
@@ -18,8 +20,10 @@ from mcp.server import NotificationOptions, Server
|
|
|
18
20
|
from mcp.server.models import InitializationOptions
|
|
19
21
|
from pydantic import AnyUrl
|
|
20
22
|
|
|
23
|
+
from .cache import get_cache_manager
|
|
21
24
|
from .config import JenkinsSettings, get_default_settings
|
|
22
|
-
from .jenkins_client import get_jenkins_client
|
|
25
|
+
from .jenkins_client import get_jenkins_client
|
|
26
|
+
from .metrics import get_metrics_collector, record_tool_execution
|
|
23
27
|
|
|
24
28
|
# Configure logging
|
|
25
29
|
logger = logging.getLogger(__name__)
|
|
@@ -30,11 +34,17 @@ server = Server("jenkins-mcp-server")
|
|
|
30
34
|
# Settings storage (injected by main)
|
|
31
35
|
_jenkins_settings: Optional[JenkinsSettings] = None
|
|
32
36
|
|
|
37
|
+
# Client connection cache (Quick Win #3)
|
|
38
|
+
_jenkins_client_cache = None
|
|
39
|
+
_client_cache_lock = asyncio.Lock()
|
|
40
|
+
|
|
33
41
|
|
|
34
42
|
def set_jenkins_settings(settings: JenkinsSettings) -> None:
|
|
35
43
|
"""Set Jenkins settings for the server (called from __init__.py)"""
|
|
36
|
-
global _jenkins_settings
|
|
44
|
+
global _jenkins_settings, _jenkins_client_cache
|
|
37
45
|
_jenkins_settings = settings
|
|
46
|
+
# Clear cache when settings change
|
|
47
|
+
_jenkins_client_cache = None
|
|
38
48
|
|
|
39
49
|
|
|
40
50
|
def get_settings() -> JenkinsSettings:
|
|
@@ -45,6 +55,64 @@ def get_settings() -> JenkinsSettings:
|
|
|
45
55
|
return _jenkins_settings
|
|
46
56
|
|
|
47
57
|
|
|
58
|
+
async def get_cached_jenkins_client(settings: JenkinsSettings):
|
|
59
|
+
"""
|
|
60
|
+
Get or create cached Jenkins client (Quick Win #3: Client Caching)
|
|
61
|
+
Reuses the same client connection across tool calls for better performance.
|
|
62
|
+
"""
|
|
63
|
+
global _jenkins_client_cache
|
|
64
|
+
|
|
65
|
+
async with _client_cache_lock:
|
|
66
|
+
if _jenkins_client_cache is None:
|
|
67
|
+
logger.info("Creating new Jenkins client connection")
|
|
68
|
+
_jenkins_client_cache = get_jenkins_client(settings)
|
|
69
|
+
return _jenkins_client_cache
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Input Validation Helpers (Quick Win #4)
|
|
73
|
+
|
|
74
|
+
def validate_job_name(job_name: any) -> str:
|
|
75
|
+
"""Validate job name parameter"""
|
|
76
|
+
if not job_name:
|
|
77
|
+
raise ValueError("Missing required argument: job_name")
|
|
78
|
+
if not isinstance(job_name, str):
|
|
79
|
+
raise ValueError(f"job_name must be a string, got {type(job_name).__name__}")
|
|
80
|
+
if not job_name.strip():
|
|
81
|
+
raise ValueError("job_name cannot be empty or whitespace")
|
|
82
|
+
return job_name.strip()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def validate_build_number(build_number: any) -> int:
|
|
86
|
+
"""Validate build number parameter"""
|
|
87
|
+
if build_number is None:
|
|
88
|
+
raise ValueError("Missing required argument: build_number")
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
num = int(build_number)
|
|
92
|
+
except (ValueError, TypeError):
|
|
93
|
+
raise ValueError(f"build_number must be an integer, got: {build_number}")
|
|
94
|
+
|
|
95
|
+
if num < 0:
|
|
96
|
+
raise ValueError(f"build_number must be non-negative, got: {num}")
|
|
97
|
+
|
|
98
|
+
return num
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def validate_config_xml(config_xml: any) -> str:
|
|
102
|
+
"""Validate XML configuration parameter"""
|
|
103
|
+
if not config_xml:
|
|
104
|
+
raise ValueError("Missing required argument: config_xml")
|
|
105
|
+
if not isinstance(config_xml, str):
|
|
106
|
+
raise ValueError(f"config_xml must be a string, got {type(config_xml).__name__}")
|
|
107
|
+
|
|
108
|
+
# Basic XML validation
|
|
109
|
+
xml_str = config_xml.strip()
|
|
110
|
+
if not xml_str.startswith('<'):
|
|
111
|
+
raise ValueError("config_xml must be valid XML (should start with '<')")
|
|
112
|
+
|
|
113
|
+
return xml_str
|
|
114
|
+
|
|
115
|
+
|
|
48
116
|
# ==================== Resources ====================
|
|
49
117
|
|
|
50
118
|
@server.list_resources()
|
|
@@ -314,6 +382,91 @@ async def _prompt_analyze_build_logs(arguments: dict[str, str]) -> types.GetProm
|
|
|
314
382
|
)
|
|
315
383
|
|
|
316
384
|
|
|
385
|
+
async def _tool_trigger_multiple_builds(client, args):
|
|
386
|
+
"""Trigger builds for multiple jobs at once"""
|
|
387
|
+
job_names = args.get("job_names", [])
|
|
388
|
+
parameters = args.get("parameters", {})
|
|
389
|
+
wait_for_start = args.get("wait_for_start", False)
|
|
390
|
+
|
|
391
|
+
# Validation
|
|
392
|
+
if not job_names:
|
|
393
|
+
raise ValueError("No job names provided")
|
|
394
|
+
|
|
395
|
+
if not isinstance(job_names, list):
|
|
396
|
+
raise ValueError("job_names must be an array")
|
|
397
|
+
|
|
398
|
+
if len(job_names) > 20:
|
|
399
|
+
raise ValueError("Maximum 20 jobs can be triggered at once")
|
|
400
|
+
|
|
401
|
+
# Validate each job name
|
|
402
|
+
validated_jobs = []
|
|
403
|
+
for job_name in job_names:
|
|
404
|
+
try:
|
|
405
|
+
validated = validate_job_name(job_name)
|
|
406
|
+
validated_jobs.append(validated)
|
|
407
|
+
except ValueError as e:
|
|
408
|
+
return [types.TextContent(
|
|
409
|
+
type="text",
|
|
410
|
+
text=f"ā Invalid job name '{job_name}': {str(e)}"
|
|
411
|
+
)]
|
|
412
|
+
|
|
413
|
+
# Validate parameters if provided
|
|
414
|
+
if parameters and not isinstance(parameters, dict):
|
|
415
|
+
raise ValueError(f"parameters must be a dictionary, got {type(parameters).__name__}")
|
|
416
|
+
|
|
417
|
+
# Trigger all builds
|
|
418
|
+
results = []
|
|
419
|
+
for job_name in validated_jobs:
|
|
420
|
+
try:
|
|
421
|
+
result = client.build_job(
|
|
422
|
+
job_name,
|
|
423
|
+
parameters,
|
|
424
|
+
wait_for_start=wait_for_start,
|
|
425
|
+
timeout=10 # Shorter timeout for batch
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
results.append({
|
|
429
|
+
"job": job_name,
|
|
430
|
+
"status": "triggered",
|
|
431
|
+
"queue_id": result.get('queue_id'),
|
|
432
|
+
"build_number": result.get('build_number') if wait_for_start else None
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
logger.info(f"Triggered build for {job_name}")
|
|
436
|
+
|
|
437
|
+
except Exception as e:
|
|
438
|
+
results.append({
|
|
439
|
+
"job": job_name,
|
|
440
|
+
"status": "failed",
|
|
441
|
+
"error": str(e)
|
|
442
|
+
})
|
|
443
|
+
logger.error(f"Failed to trigger {job_name}: {e}")
|
|
444
|
+
|
|
445
|
+
# Build summary
|
|
446
|
+
successful = [r for r in results if r["status"] == "triggered"]
|
|
447
|
+
failed = [r for r in results if r["status"] == "failed"]
|
|
448
|
+
|
|
449
|
+
summary = {
|
|
450
|
+
"total": len(job_names),
|
|
451
|
+
"successful": len(successful),
|
|
452
|
+
"failed": len(failed),
|
|
453
|
+
"results": results
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
# Invalidate cache since jobs were triggered
|
|
457
|
+
cache_manager = get_cache_manager()
|
|
458
|
+
await cache_manager.invalidate_pattern("jobs_list:")
|
|
459
|
+
|
|
460
|
+
emoji = "ā
" if len(failed) == 0 else "ā ļø"
|
|
461
|
+
message = f"{emoji} Batch Build Trigger Complete\n\n"
|
|
462
|
+
message += f"Total Jobs: {len(job_names)}\n"
|
|
463
|
+
message += f"Successful: {len(successful)}\n"
|
|
464
|
+
message += f"Failed: {len(failed)}\n\n"
|
|
465
|
+
message += f"Details:\n{json.dumps(results, indent=2)}"
|
|
466
|
+
|
|
467
|
+
return [types.TextContent(type="text", text=message)]
|
|
468
|
+
|
|
469
|
+
|
|
317
470
|
# ==================== Tools ====================
|
|
318
471
|
|
|
319
472
|
@server.list_tools()
|
|
@@ -343,8 +496,14 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
343
496
|
inputSchema={
|
|
344
497
|
"type": "object",
|
|
345
498
|
"properties": {
|
|
346
|
-
"job_name": {
|
|
347
|
-
|
|
499
|
+
"job_name": {
|
|
500
|
+
"type": "string",
|
|
501
|
+
"description": "Name of the Jenkins job"
|
|
502
|
+
},
|
|
503
|
+
"build_number": {
|
|
504
|
+
"type": "integer",
|
|
505
|
+
"description": "Build number to stop"
|
|
506
|
+
},
|
|
348
507
|
},
|
|
349
508
|
"required": ["job_name", "build_number"],
|
|
350
509
|
},
|
|
@@ -353,13 +512,18 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
353
512
|
# Job Information
|
|
354
513
|
types.Tool(
|
|
355
514
|
name="list-jobs",
|
|
356
|
-
description="List all Jenkins jobs with optional filtering",
|
|
515
|
+
description="List all Jenkins jobs with optional filtering and caching",
|
|
357
516
|
inputSchema={
|
|
358
517
|
"type": "object",
|
|
359
518
|
"properties": {
|
|
360
519
|
"filter": {
|
|
361
520
|
"type": "string",
|
|
362
521
|
"description": "Filter jobs by name (case-insensitive partial match)"
|
|
522
|
+
},
|
|
523
|
+
"use_cache": {
|
|
524
|
+
"type": "boolean",
|
|
525
|
+
"description": "Use cached results if available (default: true)",
|
|
526
|
+
"default": True
|
|
363
527
|
}
|
|
364
528
|
}
|
|
365
529
|
},
|
|
@@ -369,7 +533,19 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
369
533
|
description="Get detailed information about a Jenkins job",
|
|
370
534
|
inputSchema={
|
|
371
535
|
"type": "object",
|
|
372
|
-
"properties": {
|
|
536
|
+
"properties": {
|
|
537
|
+
"job_name": {
|
|
538
|
+
"type": "string",
|
|
539
|
+
"description": "Name of the Jenkins job"
|
|
540
|
+
},
|
|
541
|
+
"max_recent_builds": {
|
|
542
|
+
"type": "integer",
|
|
543
|
+
"description": "Maximum number of recent builds to fetch (0-10, default: 3). Set to 0 to skip build history.",
|
|
544
|
+
"default": 3,
|
|
545
|
+
"minimum": 0,
|
|
546
|
+
"maximum": 10
|
|
547
|
+
}
|
|
548
|
+
},
|
|
373
549
|
"required": ["job_name"],
|
|
374
550
|
},
|
|
375
551
|
),
|
|
@@ -381,8 +557,14 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
381
557
|
inputSchema={
|
|
382
558
|
"type": "object",
|
|
383
559
|
"properties": {
|
|
384
|
-
"job_name": {
|
|
385
|
-
|
|
560
|
+
"job_name": {
|
|
561
|
+
"type": "string",
|
|
562
|
+
"description": "Name of the Jenkins job"
|
|
563
|
+
},
|
|
564
|
+
"build_number": {
|
|
565
|
+
"type": "integer",
|
|
566
|
+
"description": "Build number to get information about"
|
|
567
|
+
},
|
|
386
568
|
},
|
|
387
569
|
"required": ["job_name", "build_number"],
|
|
388
570
|
},
|
|
@@ -393,8 +575,26 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
393
575
|
inputSchema={
|
|
394
576
|
"type": "object",
|
|
395
577
|
"properties": {
|
|
396
|
-
"job_name": {
|
|
397
|
-
|
|
578
|
+
"job_name": {
|
|
579
|
+
"type": "string",
|
|
580
|
+
"description": "Name of the Jenkins job"
|
|
581
|
+
},
|
|
582
|
+
"build_number": {
|
|
583
|
+
"type": "integer",
|
|
584
|
+
"description": "Build number to get console output from"
|
|
585
|
+
},
|
|
586
|
+
"max_lines": {
|
|
587
|
+
"type": "integer",
|
|
588
|
+
"description": "Maximum number of lines to return (default: 1000, max: 10000)",
|
|
589
|
+
"default": 1000,
|
|
590
|
+
"minimum": 10,
|
|
591
|
+
"maximum": 10000
|
|
592
|
+
},
|
|
593
|
+
"tail_only": {
|
|
594
|
+
"type": "boolean",
|
|
595
|
+
"description": "If true, return last N lines instead of first N lines (default: false)",
|
|
596
|
+
"default": False
|
|
597
|
+
},
|
|
398
598
|
},
|
|
399
599
|
"required": ["job_name", "build_number"],
|
|
400
600
|
},
|
|
@@ -404,7 +604,12 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
404
604
|
description="Get the last build number for a job",
|
|
405
605
|
inputSchema={
|
|
406
606
|
"type": "object",
|
|
407
|
-
"properties": {
|
|
607
|
+
"properties": {
|
|
608
|
+
"job_name": {
|
|
609
|
+
"type": "string",
|
|
610
|
+
"description": "Name of the Jenkins job"
|
|
611
|
+
}
|
|
612
|
+
},
|
|
408
613
|
"required": ["job_name"],
|
|
409
614
|
},
|
|
410
615
|
),
|
|
@@ -413,7 +618,12 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
413
618
|
description="Get the timestamp of the last build",
|
|
414
619
|
inputSchema={
|
|
415
620
|
"type": "object",
|
|
416
|
-
"properties": {
|
|
621
|
+
"properties": {
|
|
622
|
+
"job_name": {
|
|
623
|
+
"type": "string",
|
|
624
|
+
"description": "Name of the Jenkins job"
|
|
625
|
+
}
|
|
626
|
+
},
|
|
417
627
|
"required": ["job_name"],
|
|
418
628
|
},
|
|
419
629
|
),
|
|
@@ -425,8 +635,14 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
425
635
|
inputSchema={
|
|
426
636
|
"type": "object",
|
|
427
637
|
"properties": {
|
|
428
|
-
"job_name": {
|
|
429
|
-
|
|
638
|
+
"job_name": {
|
|
639
|
+
"type": "string",
|
|
640
|
+
"description": "Name for the new Jenkins job"
|
|
641
|
+
},
|
|
642
|
+
"config_xml": {
|
|
643
|
+
"type": "string",
|
|
644
|
+
"description": "Job configuration in XML format"
|
|
645
|
+
},
|
|
430
646
|
},
|
|
431
647
|
"required": ["job_name", "config_xml"],
|
|
432
648
|
},
|
|
@@ -437,8 +653,14 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
437
653
|
inputSchema={
|
|
438
654
|
"type": "object",
|
|
439
655
|
"properties": {
|
|
440
|
-
"new_job_name": {
|
|
441
|
-
|
|
656
|
+
"new_job_name": {
|
|
657
|
+
"type": "string",
|
|
658
|
+
"description": "Name for the new job to be created"
|
|
659
|
+
},
|
|
660
|
+
"source_job_name": {
|
|
661
|
+
"type": "string",
|
|
662
|
+
"description": "Name of the existing job to copy from"
|
|
663
|
+
},
|
|
442
664
|
},
|
|
443
665
|
"required": ["new_job_name", "source_job_name"],
|
|
444
666
|
},
|
|
@@ -449,9 +671,19 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
449
671
|
inputSchema={
|
|
450
672
|
"type": "object",
|
|
451
673
|
"properties": {
|
|
452
|
-
"job_name": {
|
|
453
|
-
|
|
454
|
-
|
|
674
|
+
"job_name": {
|
|
675
|
+
"type": "string",
|
|
676
|
+
"description": "Name for the new Jenkins job"
|
|
677
|
+
},
|
|
678
|
+
"config_data": {
|
|
679
|
+
"type": "object",
|
|
680
|
+
"description": "Job configuration as structured data (will be converted to XML)"
|
|
681
|
+
},
|
|
682
|
+
"root_tag": {
|
|
683
|
+
"type": "string",
|
|
684
|
+
"default": "project",
|
|
685
|
+
"description": "Root XML tag for the configuration (default: 'project')"
|
|
686
|
+
},
|
|
455
687
|
},
|
|
456
688
|
"required": ["job_name", "config_data"],
|
|
457
689
|
},
|
|
@@ -461,7 +693,12 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
461
693
|
description="Delete an existing Jenkins job",
|
|
462
694
|
inputSchema={
|
|
463
695
|
"type": "object",
|
|
464
|
-
"properties": {
|
|
696
|
+
"properties": {
|
|
697
|
+
"job_name": {
|
|
698
|
+
"type": "string",
|
|
699
|
+
"description": "Name of the Jenkins job to delete"
|
|
700
|
+
}
|
|
701
|
+
},
|
|
465
702
|
"required": ["job_name"],
|
|
466
703
|
},
|
|
467
704
|
),
|
|
@@ -470,7 +707,12 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
470
707
|
description="Enable a disabled Jenkins job",
|
|
471
708
|
inputSchema={
|
|
472
709
|
"type": "object",
|
|
473
|
-
"properties": {
|
|
710
|
+
"properties": {
|
|
711
|
+
"job_name": {
|
|
712
|
+
"type": "string",
|
|
713
|
+
"description": "Name of the Jenkins job to enable"
|
|
714
|
+
}
|
|
715
|
+
},
|
|
474
716
|
"required": ["job_name"],
|
|
475
717
|
},
|
|
476
718
|
),
|
|
@@ -479,7 +721,12 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
479
721
|
description="Disable a Jenkins job",
|
|
480
722
|
inputSchema={
|
|
481
723
|
"type": "object",
|
|
482
|
-
"properties": {
|
|
724
|
+
"properties": {
|
|
725
|
+
"job_name": {
|
|
726
|
+
"type": "string",
|
|
727
|
+
"description": "Name of the Jenkins job to disable"
|
|
728
|
+
}
|
|
729
|
+
},
|
|
483
730
|
"required": ["job_name"],
|
|
484
731
|
},
|
|
485
732
|
),
|
|
@@ -489,8 +736,14 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
489
736
|
inputSchema={
|
|
490
737
|
"type": "object",
|
|
491
738
|
"properties": {
|
|
492
|
-
"job_name": {
|
|
493
|
-
|
|
739
|
+
"job_name": {
|
|
740
|
+
"type": "string",
|
|
741
|
+
"description": "Current name of the Jenkins job"
|
|
742
|
+
},
|
|
743
|
+
"new_name": {
|
|
744
|
+
"type": "string",
|
|
745
|
+
"description": "New name for the Jenkins job"
|
|
746
|
+
},
|
|
494
747
|
},
|
|
495
748
|
"required": ["job_name", "new_name"],
|
|
496
749
|
},
|
|
@@ -502,7 +755,12 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
502
755
|
description="Get the configuration XML for a job",
|
|
503
756
|
inputSchema={
|
|
504
757
|
"type": "object",
|
|
505
|
-
"properties": {
|
|
758
|
+
"properties": {
|
|
759
|
+
"job_name": {
|
|
760
|
+
"type": "string",
|
|
761
|
+
"description": "Name of the Jenkins job"
|
|
762
|
+
}
|
|
763
|
+
},
|
|
506
764
|
"required": ["job_name"],
|
|
507
765
|
},
|
|
508
766
|
),
|
|
@@ -512,8 +770,14 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
512
770
|
inputSchema={
|
|
513
771
|
"type": "object",
|
|
514
772
|
"properties": {
|
|
515
|
-
"job_name": {
|
|
516
|
-
|
|
773
|
+
"job_name": {
|
|
774
|
+
"type": "string",
|
|
775
|
+
"description": "Name of the Jenkins job to update"
|
|
776
|
+
},
|
|
777
|
+
"config_xml": {
|
|
778
|
+
"type": "string",
|
|
779
|
+
"description": "New configuration in XML format"
|
|
780
|
+
},
|
|
517
781
|
},
|
|
518
782
|
"required": ["job_name", "config_xml"],
|
|
519
783
|
},
|
|
@@ -535,10 +799,102 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
535
799
|
description="Get information about a specific Jenkins node",
|
|
536
800
|
inputSchema={
|
|
537
801
|
"type": "object",
|
|
538
|
-
"properties": {
|
|
802
|
+
"properties": {
|
|
803
|
+
"node_name": {
|
|
804
|
+
"type": "string",
|
|
805
|
+
"description": "Name of the Jenkins node/agent"
|
|
806
|
+
}
|
|
807
|
+
},
|
|
539
808
|
"required": ["node_name"],
|
|
540
809
|
},
|
|
541
810
|
),
|
|
811
|
+
types.Tool(
|
|
812
|
+
name="trigger-multiple-builds",
|
|
813
|
+
description="Trigger builds for multiple jobs at once",
|
|
814
|
+
inputSchema={
|
|
815
|
+
"type": "object",
|
|
816
|
+
"properties": {
|
|
817
|
+
"job_names": {
|
|
818
|
+
"type": "array",
|
|
819
|
+
"items": {"type": "string"},
|
|
820
|
+
"description": "List of job names to trigger",
|
|
821
|
+
"minItems": 1,
|
|
822
|
+
"maxItems": 20
|
|
823
|
+
},
|
|
824
|
+
"parameters": {
|
|
825
|
+
"type": "object",
|
|
826
|
+
"description": "Common parameters for all builds (optional)",
|
|
827
|
+
"additionalProperties": {"type": ["string", "number", "boolean"]},
|
|
828
|
+
},
|
|
829
|
+
"wait_for_start": {
|
|
830
|
+
"type": "boolean",
|
|
831
|
+
"description": "Wait for all builds to start (default: false)",
|
|
832
|
+
"default": False
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
"required": ["job_names"],
|
|
836
|
+
},
|
|
837
|
+
),
|
|
838
|
+
|
|
839
|
+
# Cache tools
|
|
840
|
+
types.Tool(
|
|
841
|
+
name="get-cache-stats",
|
|
842
|
+
description="Get cache statistics and information",
|
|
843
|
+
inputSchema={"type": "object", "properties": {}},
|
|
844
|
+
),
|
|
845
|
+
types.Tool(
|
|
846
|
+
name="clear-cache",
|
|
847
|
+
description="Clear all cached data",
|
|
848
|
+
inputSchema={"type": "object", "properties": {}},
|
|
849
|
+
),
|
|
850
|
+
|
|
851
|
+
# Health Check (Quick Win #1)
|
|
852
|
+
types.Tool(
|
|
853
|
+
name="health-check",
|
|
854
|
+
description="Check Jenkins server health and connection status. Useful for troubleshooting connectivity issues.",
|
|
855
|
+
inputSchema={"type": "object", "properties": {}},
|
|
856
|
+
),
|
|
857
|
+
|
|
858
|
+
# Get metrics
|
|
859
|
+
types.Tool(
|
|
860
|
+
name="get-metrics",
|
|
861
|
+
description="Get usage metrics and performance statistics",
|
|
862
|
+
inputSchema={
|
|
863
|
+
"type": "object",
|
|
864
|
+
"properties": {
|
|
865
|
+
"tool_name": {
|
|
866
|
+
"type": "string",
|
|
867
|
+
"description": "Specific tool name (optional, returns all if not specified)"
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
),
|
|
872
|
+
|
|
873
|
+
types.Tool(
|
|
874
|
+
name="configure-webhook",
|
|
875
|
+
description="Configure webhook notifications for Jenkins events (requires Jenkins plugin)",
|
|
876
|
+
inputSchema={
|
|
877
|
+
"type": "object",
|
|
878
|
+
"properties": {
|
|
879
|
+
"job_name": {
|
|
880
|
+
"type": "string",
|
|
881
|
+
"description": "Job to configure webhook for"
|
|
882
|
+
},
|
|
883
|
+
"webhook_url": {
|
|
884
|
+
"type": "string",
|
|
885
|
+
"description": "URL to receive webhook notifications"
|
|
886
|
+
},
|
|
887
|
+
"events": {
|
|
888
|
+
"type": "array",
|
|
889
|
+
"items": {
|
|
890
|
+
"enum": ["build_started", "build_completed", "build_failed", "build_success"]
|
|
891
|
+
},
|
|
892
|
+
"description": "Events to trigger webhook"
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
"required": ["job_name", "webhook_url", "events"]
|
|
896
|
+
},
|
|
897
|
+
),
|
|
542
898
|
]
|
|
543
899
|
|
|
544
900
|
logger.info(f"Registered {len(tools)} Jenkins tools")
|
|
@@ -550,11 +906,15 @@ async def handle_call_tool(
|
|
|
550
906
|
name: str,
|
|
551
907
|
arguments: dict | None
|
|
552
908
|
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
|
553
|
-
"""Handle tool execution requests"""
|
|
909
|
+
"""Handle tool execution requests with improved error handling and metrics tracking"""
|
|
554
910
|
arguments = arguments or {}
|
|
911
|
+
start_time = time.time()
|
|
912
|
+
success = False
|
|
913
|
+
error_message = None
|
|
555
914
|
|
|
556
915
|
try:
|
|
557
|
-
client
|
|
916
|
+
# Use cached client for better performance
|
|
917
|
+
client = await get_cached_jenkins_client(get_settings())
|
|
558
918
|
|
|
559
919
|
# Route to appropriate handler
|
|
560
920
|
handlers = {
|
|
@@ -589,22 +949,163 @@ async def handle_call_tool(
|
|
|
589
949
|
"get-queue-info": _tool_get_queue_info,
|
|
590
950
|
"list-nodes": _tool_list_nodes,
|
|
591
951
|
"get-node-info": _tool_get_node_info,
|
|
952
|
+
|
|
953
|
+
# Health check (Quick Win #1)
|
|
954
|
+
"health-check": _tool_health_check,
|
|
955
|
+
|
|
956
|
+
# NEW: Medium Priority
|
|
957
|
+
"trigger-multiple-builds": _tool_trigger_multiple_builds,
|
|
958
|
+
"get-cache-stats": _tool_get_cache_stats,
|
|
959
|
+
"clear-cache": _tool_clear_cache,
|
|
960
|
+
|
|
961
|
+
# NEW: Low Priority
|
|
962
|
+
"get-metrics": _tool_get_metrics,
|
|
963
|
+
"configure-webhook": _tool_configure_webhook,
|
|
592
964
|
}
|
|
593
965
|
|
|
594
966
|
handler = handlers.get(name)
|
|
595
967
|
if not handler:
|
|
596
968
|
raise ValueError(f"Unknown tool: {name}")
|
|
597
969
|
|
|
598
|
-
|
|
970
|
+
result = await handler(client, arguments)
|
|
971
|
+
success = True
|
|
972
|
+
return result
|
|
599
973
|
|
|
600
|
-
|
|
601
|
-
|
|
974
|
+
# Better error messages with troubleshooting steps (Quick Win #2)
|
|
975
|
+
except ValueError as e:
|
|
976
|
+
# Validation errors - user's fault
|
|
977
|
+
logger.warning(f"Validation error in {name}: {e}")
|
|
602
978
|
return [
|
|
603
979
|
types.TextContent(
|
|
604
980
|
type="text",
|
|
605
|
-
text=f"
|
|
981
|
+
text=f"ā Invalid input for {name}: {str(e)}\n\n"
|
|
982
|
+
f"š” Please check the parameter values and try again."
|
|
606
983
|
)
|
|
607
984
|
]
|
|
985
|
+
except ImportError:
|
|
986
|
+
# Missing requests library
|
|
987
|
+
import_error = (
|
|
988
|
+
f"ā ļø Missing required library 'requests'.\n\n"
|
|
989
|
+
f"To fix this, run:\n"
|
|
990
|
+
f"pip install requests"
|
|
991
|
+
)
|
|
992
|
+
logger.error(f"Import error in {name}: requests library not found")
|
|
993
|
+
return [types.TextContent(type="text", text=import_error)]
|
|
994
|
+
except Exception as e:
|
|
995
|
+
# Check for common requests exceptions
|
|
996
|
+
error_type = type(e).__name__
|
|
997
|
+
error_message = str(e)
|
|
998
|
+
|
|
999
|
+
# Timeout errors
|
|
1000
|
+
if 'timeout' in error_message.lower() or error_type == 'Timeout':
|
|
1001
|
+
logger.error(f"Timeout error in {name}: {e}")
|
|
1002
|
+
return [
|
|
1003
|
+
types.TextContent(
|
|
1004
|
+
type="text",
|
|
1005
|
+
text=f"ā±ļø Timeout connecting to Jenkins.\n\n"
|
|
1006
|
+
f"Troubleshooting steps:\n"
|
|
1007
|
+
f"1. Check Jenkins server is running\n"
|
|
1008
|
+
f"2. Verify URL is correct: {get_settings().url}\n"
|
|
1009
|
+
f"3. Ensure network/VPN connection is active\n"
|
|
1010
|
+
f"4. Check firewall settings\n\n"
|
|
1011
|
+
f"Error: {str(e)}"
|
|
1012
|
+
)
|
|
1013
|
+
]
|
|
1014
|
+
|
|
1015
|
+
# Connection errors
|
|
1016
|
+
elif 'connection' in error_message.lower() or error_type in ['ConnectionError', 'ConnectionRefusedError']:
|
|
1017
|
+
logger.error(f"Connection error in {name}: {e}")
|
|
1018
|
+
return [
|
|
1019
|
+
types.TextContent(
|
|
1020
|
+
type="text",
|
|
1021
|
+
text=f"š Cannot connect to Jenkins at {get_settings().url}\n\n"
|
|
1022
|
+
f"Troubleshooting steps:\n"
|
|
1023
|
+
f"1. Verify Jenkins server is accessible\n"
|
|
1024
|
+
f"2. Check port is correct (usually 8080)\n"
|
|
1025
|
+
f"3. Ensure firewall allows connection\n"
|
|
1026
|
+
f"4. Test with: curl {get_settings().url}/api/json\n\n"
|
|
1027
|
+
f"Error: {str(e)}"
|
|
1028
|
+
)
|
|
1029
|
+
]
|
|
1030
|
+
|
|
1031
|
+
# Authentication errors (401)
|
|
1032
|
+
elif '401' in error_message or 'unauthorized' in error_message.lower():
|
|
1033
|
+
logger.error(f"Authentication error in {name}: {e}")
|
|
1034
|
+
return [
|
|
1035
|
+
types.TextContent(
|
|
1036
|
+
type="text",
|
|
1037
|
+
text=f"š Authentication failed.\n\n"
|
|
1038
|
+
f"Troubleshooting steps:\n"
|
|
1039
|
+
f"1. Verify username is correct: {get_settings().username}\n"
|
|
1040
|
+
f"2. Check API token is valid (not expired)\n"
|
|
1041
|
+
f"3. Generate new token in Jenkins:\n"
|
|
1042
|
+
f" - Go to Jenkins ā Your Name ā Configure\n"
|
|
1043
|
+
f" - Click 'Add new Token' under API Token section\n"
|
|
1044
|
+
f"4. Update .env file with new token\n\n"
|
|
1045
|
+
f"Error: {str(e)}"
|
|
1046
|
+
)
|
|
1047
|
+
]
|
|
1048
|
+
|
|
1049
|
+
# Permission errors (403)
|
|
1050
|
+
elif '403' in error_message or 'forbidden' in error_message.lower():
|
|
1051
|
+
logger.error(f"Permission error in {name}: {e}")
|
|
1052
|
+
return [
|
|
1053
|
+
types.TextContent(
|
|
1054
|
+
type="text",
|
|
1055
|
+
text=f"š« Permission denied.\n\n"
|
|
1056
|
+
f"Troubleshooting steps:\n"
|
|
1057
|
+
f"1. Check user has permission to access Jenkins\n"
|
|
1058
|
+
f"2. Verify user has permission for this operation\n"
|
|
1059
|
+
f"3. Contact Jenkins admin to grant necessary permissions\n\n"
|
|
1060
|
+
f"User: {get_settings().username}\n"
|
|
1061
|
+
f"Operation: {name}\n"
|
|
1062
|
+
f"Error: {str(e)}"
|
|
1063
|
+
)
|
|
1064
|
+
]
|
|
1065
|
+
|
|
1066
|
+
# Not found errors (404)
|
|
1067
|
+
elif '404' in error_message or 'not found' in error_message.lower():
|
|
1068
|
+
logger.error(f"Not found error in {name}: {e}")
|
|
1069
|
+
return [
|
|
1070
|
+
types.TextContent(
|
|
1071
|
+
type="text",
|
|
1072
|
+
text=f"ā Resource not found.\n\n"
|
|
1073
|
+
f"Troubleshooting steps:\n"
|
|
1074
|
+
f"1. Check job/resource name is correct (case-sensitive)\n"
|
|
1075
|
+
f"2. Verify resource exists in Jenkins\n"
|
|
1076
|
+
f"3. Ensure user has permission to view the resource\n"
|
|
1077
|
+
f"4. Try listing all jobs with 'list-jobs' tool\n\n"
|
|
1078
|
+
f"Error: {str(e)}"
|
|
1079
|
+
)
|
|
1080
|
+
]
|
|
1081
|
+
|
|
1082
|
+
# Generic error with some context
|
|
1083
|
+
else:
|
|
1084
|
+
logger.error(f"Tool execution failed for {name}: {e}", exc_info=True)
|
|
1085
|
+
return [
|
|
1086
|
+
types.TextContent(
|
|
1087
|
+
type="text",
|
|
1088
|
+
text=f"ā Error executing {name}\n\n"
|
|
1089
|
+
f"Error type: {error_type}\n"
|
|
1090
|
+
f"Error message: {str(e)}\n\n"
|
|
1091
|
+
f"š” Troubleshooting tips:\n"
|
|
1092
|
+
f"1. Run 'health-check' tool to verify connection\n"
|
|
1093
|
+
f"2. Check Jenkins logs for more details\n"
|
|
1094
|
+
f"3. Verify all parameters are correct\n"
|
|
1095
|
+
f"4. Try the operation manually in Jenkins UI"
|
|
1096
|
+
)
|
|
1097
|
+
]
|
|
1098
|
+
|
|
1099
|
+
finally:
|
|
1100
|
+
# Record metrics
|
|
1101
|
+
execution_time_ms = (time.time() - start_time) * 1000
|
|
1102
|
+
await record_tool_execution(
|
|
1103
|
+
tool_name=name,
|
|
1104
|
+
execution_time_ms=execution_time_ms,
|
|
1105
|
+
success=success,
|
|
1106
|
+
error_message=error_message,
|
|
1107
|
+
args={"tool": name} # Don't log full args for privacy
|
|
1108
|
+
)
|
|
608
1109
|
|
|
609
1110
|
|
|
610
1111
|
# ==================== Tool Handlers ====================
|
|
@@ -613,11 +1114,12 @@ async def handle_call_tool(
|
|
|
613
1114
|
|
|
614
1115
|
async def _tool_trigger_build(client, args):
|
|
615
1116
|
"""Trigger a Jenkins build"""
|
|
616
|
-
|
|
1117
|
+
# Input validation (Quick Win #4)
|
|
1118
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
617
1119
|
parameters = args.get("parameters", {})
|
|
618
1120
|
|
|
619
|
-
if not
|
|
620
|
-
raise ValueError("
|
|
1121
|
+
if parameters and not isinstance(parameters, dict):
|
|
1122
|
+
raise ValueError(f"parameters must be a dictionary, got {type(parameters).__name__}")
|
|
621
1123
|
|
|
622
1124
|
result = client.build_job(job_name, parameters)
|
|
623
1125
|
|
|
@@ -634,11 +1136,9 @@ async def _tool_trigger_build(client, args):
|
|
|
634
1136
|
|
|
635
1137
|
async def _tool_stop_build(client, args):
|
|
636
1138
|
"""Stop a running build"""
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if not job_name or build_number is None:
|
|
641
|
-
raise ValueError("Missing required arguments: job_name and build_number")
|
|
1139
|
+
# Input validation (Quick Win #4)
|
|
1140
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
1141
|
+
build_number = validate_build_number(args.get("build_number"))
|
|
642
1142
|
|
|
643
1143
|
client.stop_build(job_name, build_number)
|
|
644
1144
|
|
|
@@ -653,9 +1153,26 @@ async def _tool_stop_build(client, args):
|
|
|
653
1153
|
# Job Information
|
|
654
1154
|
|
|
655
1155
|
async def _tool_list_jobs(client, args):
|
|
656
|
-
"""List all Jenkins jobs with optional filtering"""
|
|
657
|
-
jobs = client.get_jobs()
|
|
1156
|
+
"""List all Jenkins jobs with optional filtering and caching"""
|
|
658
1157
|
filter_text = args.get("filter", "").strip()
|
|
1158
|
+
use_cache = args.get("use_cache", True) # cache control
|
|
1159
|
+
|
|
1160
|
+
# Try cache first (if enabled)
|
|
1161
|
+
cache_key = f"jobs_list:{filter_text or 'all'}"
|
|
1162
|
+
if use_cache:
|
|
1163
|
+
cache_manager = get_cache_manager()
|
|
1164
|
+
cached_jobs = await cache_manager.get(cache_key)
|
|
1165
|
+
if cached_jobs is not None:
|
|
1166
|
+
logger.debug(f"Using cached job list ({len(cached_jobs)} jobs)")
|
|
1167
|
+
return [
|
|
1168
|
+
types.TextContent(
|
|
1169
|
+
type="text",
|
|
1170
|
+
text=f"Jenkins Jobs (cached) ({len(cached_jobs)} total):\n\n{json.dumps(cached_jobs, indent=2)}"
|
|
1171
|
+
)
|
|
1172
|
+
]
|
|
1173
|
+
|
|
1174
|
+
# Fetch from Jenkins
|
|
1175
|
+
jobs = client.get_jobs()
|
|
659
1176
|
|
|
660
1177
|
# Apply filter if provided
|
|
661
1178
|
if filter_text:
|
|
@@ -674,6 +1191,10 @@ async def _tool_list_jobs(client, args):
|
|
|
674
1191
|
for job in jobs
|
|
675
1192
|
]
|
|
676
1193
|
|
|
1194
|
+
# Cache the result
|
|
1195
|
+
if use_cache:
|
|
1196
|
+
await cache_manager.set(cache_key, jobs_info, ttl_seconds=30)
|
|
1197
|
+
|
|
677
1198
|
# Build response message
|
|
678
1199
|
if filter_text:
|
|
679
1200
|
message = f"Jenkins Jobs matching '{filter_text}' ({len(jobs_info)} found):\n\n{json.dumps(jobs_info, indent=2)}"
|
|
@@ -690,10 +1211,19 @@ async def _tool_list_jobs(client, args):
|
|
|
690
1211
|
|
|
691
1212
|
async def _tool_get_job_details(client, args):
|
|
692
1213
|
"""Get detailed job information"""
|
|
693
|
-
|
|
1214
|
+
# Input validation
|
|
1215
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
694
1216
|
|
|
695
|
-
|
|
696
|
-
|
|
1217
|
+
# Configurable number of recent builds to fetch (Critical Issue #3)
|
|
1218
|
+
max_recent_builds = args.get("max_recent_builds", 3)
|
|
1219
|
+
try:
|
|
1220
|
+
max_recent_builds = int(max_recent_builds)
|
|
1221
|
+
if max_recent_builds < 0:
|
|
1222
|
+
max_recent_builds = 0
|
|
1223
|
+
elif max_recent_builds > 10:
|
|
1224
|
+
max_recent_builds = 10 # Cap at 10 to prevent excessive API calls
|
|
1225
|
+
except (ValueError, TypeError):
|
|
1226
|
+
max_recent_builds = 3 # Default to 3
|
|
697
1227
|
|
|
698
1228
|
job_info = client.get_job_info(job_name)
|
|
699
1229
|
|
|
@@ -707,10 +1237,14 @@ async def _tool_get_job_details(client, args):
|
|
|
707
1237
|
"lastFailedBuild": job_info.get("lastFailedBuild", {}),
|
|
708
1238
|
}
|
|
709
1239
|
|
|
710
|
-
# Add recent builds
|
|
711
|
-
if "builds" in job_info:
|
|
1240
|
+
# Add recent builds (optimized to reduce API calls)
|
|
1241
|
+
if max_recent_builds > 0 and "builds" in job_info:
|
|
712
1242
|
recent_builds = []
|
|
713
|
-
|
|
1243
|
+
builds_to_fetch = job_info["builds"][:max_recent_builds]
|
|
1244
|
+
|
|
1245
|
+
logger.info(f"Fetching {len(builds_to_fetch)} recent builds for '{job_name}'")
|
|
1246
|
+
|
|
1247
|
+
for build in builds_to_fetch:
|
|
714
1248
|
try:
|
|
715
1249
|
build_info = client.get_build_info(job_name, build["number"])
|
|
716
1250
|
recent_builds.append({
|
|
@@ -723,6 +1257,7 @@ async def _tool_get_job_details(client, args):
|
|
|
723
1257
|
logger.warning(f"Could not fetch build {build['number']}: {e}")
|
|
724
1258
|
|
|
725
1259
|
details["recentBuilds"] = recent_builds
|
|
1260
|
+
details["recentBuildsCount"] = len(recent_builds)
|
|
726
1261
|
|
|
727
1262
|
# Notify of resource changes
|
|
728
1263
|
try:
|
|
@@ -742,11 +1277,9 @@ async def _tool_get_job_details(client, args):
|
|
|
742
1277
|
|
|
743
1278
|
async def _tool_get_build_info(client, args):
|
|
744
1279
|
"""Get build information"""
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
if not job_name or build_number is None:
|
|
749
|
-
raise ValueError("Missing required arguments: job_name and build_number")
|
|
1280
|
+
# Input validation (Quick Win #4)
|
|
1281
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
1282
|
+
build_number = validate_build_number(args.get("build_number"))
|
|
750
1283
|
|
|
751
1284
|
build_info = client.get_build_info(job_name, build_number)
|
|
752
1285
|
|
|
@@ -779,33 +1312,78 @@ async def _tool_get_build_info(client, args):
|
|
|
779
1312
|
|
|
780
1313
|
|
|
781
1314
|
async def _tool_get_build_console(client, args):
|
|
782
|
-
"""Get build console output"""
|
|
783
|
-
|
|
784
|
-
|
|
1315
|
+
"""Get build console output with improved truncation (High Priority Issue #5)"""
|
|
1316
|
+
# Input validation (Quick Win #4)
|
|
1317
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
1318
|
+
build_number = validate_build_number(args.get("build_number"))
|
|
785
1319
|
|
|
786
|
-
|
|
787
|
-
|
|
1320
|
+
# Get configurable parameters with defaults from settings
|
|
1321
|
+
settings = get_settings()
|
|
1322
|
+
max_lines = args.get("max_lines", settings.console_max_lines)
|
|
1323
|
+
tail_only = args.get("tail_only", False)
|
|
788
1324
|
|
|
1325
|
+
# Validate max_lines
|
|
1326
|
+
try:
|
|
1327
|
+
max_lines = int(max_lines)
|
|
1328
|
+
if max_lines < 10:
|
|
1329
|
+
max_lines = 10
|
|
1330
|
+
elif max_lines > 10000:
|
|
1331
|
+
max_lines = 10000
|
|
1332
|
+
except (ValueError, TypeError):
|
|
1333
|
+
max_lines = settings.console_max_lines
|
|
1334
|
+
|
|
1335
|
+
# Validate tail_only
|
|
1336
|
+
if not isinstance(tail_only, bool):
|
|
1337
|
+
tail_only = str(tail_only).lower() in ('true', '1', 'yes')
|
|
1338
|
+
|
|
1339
|
+
# Get console output
|
|
789
1340
|
console_output = client.get_build_console_output(job_name, build_number)
|
|
790
1341
|
|
|
791
|
-
#
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1342
|
+
# Split into lines for better handling
|
|
1343
|
+
all_lines = console_output.split('\n')
|
|
1344
|
+
total_lines = len(all_lines)
|
|
1345
|
+
|
|
1346
|
+
# Determine what to show
|
|
1347
|
+
prefix = ""
|
|
1348
|
+
if total_lines <= max_lines:
|
|
1349
|
+
# No truncation needed
|
|
1350
|
+
output_lines = all_lines
|
|
1351
|
+
prefix = f"[Complete output: {total_lines} lines]\n\n"
|
|
1352
|
+
elif tail_only:
|
|
1353
|
+
# Show last N lines
|
|
1354
|
+
output_lines = all_lines[-max_lines:]
|
|
1355
|
+
truncated_lines = total_lines - max_lines
|
|
1356
|
+
prefix = f"[Showing last {max_lines} of {total_lines} lines - {truncated_lines} earlier lines omitted]\n\n"
|
|
1357
|
+
else:
|
|
1358
|
+
# Show first N lines
|
|
1359
|
+
output_lines = all_lines[:max_lines]
|
|
1360
|
+
truncated_lines = total_lines - max_lines
|
|
1361
|
+
prefix = f"[Showing first {max_lines} of {total_lines} lines - {truncated_lines} later lines truncated]\n\n"
|
|
1362
|
+
|
|
1363
|
+
# Reconstruct output
|
|
1364
|
+
final_output = '\n'.join(output_lines)
|
|
1365
|
+
|
|
1366
|
+
# Add helpful note if truncated
|
|
1367
|
+
if total_lines > max_lines:
|
|
1368
|
+
if tail_only:
|
|
1369
|
+
suffix = f"\n\nš” Tip: Use max_lines parameter to see more lines (current: {max_lines}, max: 10000)"
|
|
1370
|
+
else:
|
|
1371
|
+
suffix = f"\n\nš” Tip: Set tail_only=true to see last {max_lines} lines, or increase max_lines (current: {max_lines}, max: 10000)"
|
|
1372
|
+
else:
|
|
1373
|
+
suffix = ""
|
|
795
1374
|
|
|
796
1375
|
return [
|
|
797
1376
|
types.TextContent(
|
|
798
1377
|
type="text",
|
|
799
|
-
text=f"Console output for {job_name} #{build_number}:\n\n```\n{
|
|
1378
|
+
text=f"{prefix}Console output for {job_name} #{build_number}:\n\n```\n{final_output}\n```{suffix}"
|
|
800
1379
|
)
|
|
801
1380
|
]
|
|
802
1381
|
|
|
803
1382
|
|
|
804
1383
|
async def _tool_get_last_build_number(client, args):
|
|
805
1384
|
"""Get last build number"""
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
raise ValueError("Missing required argument: job_name")
|
|
1385
|
+
# Input validation
|
|
1386
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
809
1387
|
|
|
810
1388
|
num = client.get_last_build_number(job_name)
|
|
811
1389
|
return [types.TextContent(type="text", text=f"Last build number for '{job_name}': {num}")]
|
|
@@ -813,9 +1391,8 @@ async def _tool_get_last_build_number(client, args):
|
|
|
813
1391
|
|
|
814
1392
|
async def _tool_get_last_build_timestamp(client, args):
|
|
815
1393
|
"""Get last build timestamp"""
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
raise ValueError("Missing required argument: job_name")
|
|
1394
|
+
# Input validation
|
|
1395
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
819
1396
|
|
|
820
1397
|
ts = client.get_last_build_timestamp(job_name)
|
|
821
1398
|
return [types.TextContent(type="text", text=f"Last build timestamp for '{job_name}': {ts}")]
|
|
@@ -825,11 +1402,9 @@ async def _tool_get_last_build_timestamp(client, args):
|
|
|
825
1402
|
|
|
826
1403
|
async def _tool_create_job(client, args):
|
|
827
1404
|
"""Create a new job"""
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if not job_name or not config_xml:
|
|
832
|
-
raise ValueError("Missing required arguments: job_name and config_xml")
|
|
1405
|
+
# Input validation (Quick Win #4)
|
|
1406
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
1407
|
+
config_xml = validate_config_xml(args.get("config_xml"))
|
|
833
1408
|
|
|
834
1409
|
client.create_job(job_name, config_xml)
|
|
835
1410
|
return [types.TextContent(type="text", text=f"Successfully created job '{job_name}'")]
|
|
@@ -837,11 +1412,9 @@ async def _tool_create_job(client, args):
|
|
|
837
1412
|
|
|
838
1413
|
async def _tool_create_job_from_copy(client, args):
|
|
839
1414
|
"""Create job from copy"""
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
if not new_job_name or not source_job_name:
|
|
844
|
-
raise ValueError("Missing required arguments: new_job_name and source_job_name")
|
|
1415
|
+
# Input validation
|
|
1416
|
+
new_job_name = validate_job_name(args.get("new_job_name"))
|
|
1417
|
+
source_job_name = validate_job_name(args.get("source_job_name"))
|
|
845
1418
|
|
|
846
1419
|
client.create_job_from_copy(new_job_name, source_job_name)
|
|
847
1420
|
return [types.TextContent(type="text", text=f"Successfully created job '{new_job_name}' from '{source_job_name}'")]
|
|
@@ -849,12 +1422,15 @@ async def _tool_create_job_from_copy(client, args):
|
|
|
849
1422
|
|
|
850
1423
|
async def _tool_create_job_from_data(client, args):
|
|
851
1424
|
"""Create job from data"""
|
|
852
|
-
|
|
1425
|
+
# Input validation
|
|
1426
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
853
1427
|
config_data = args.get("config_data")
|
|
854
1428
|
root_tag = args.get("root_tag", "project")
|
|
855
1429
|
|
|
856
|
-
if
|
|
857
|
-
raise ValueError("Missing required
|
|
1430
|
+
if config_data is None:
|
|
1431
|
+
raise ValueError("Missing required argument: config_data")
|
|
1432
|
+
if not isinstance(config_data, dict):
|
|
1433
|
+
raise ValueError(f"config_data must be a dictionary, got {type(config_data).__name__}")
|
|
858
1434
|
|
|
859
1435
|
client.create_job_from_dict(job_name, config_data, root_tag)
|
|
860
1436
|
return [types.TextContent(type="text", text=f"Successfully created job '{job_name}' from data")]
|
|
@@ -862,9 +1438,8 @@ async def _tool_create_job_from_data(client, args):
|
|
|
862
1438
|
|
|
863
1439
|
async def _tool_delete_job(client, args):
|
|
864
1440
|
"""Delete a job"""
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
raise ValueError("Missing required argument: job_name")
|
|
1441
|
+
# Input validation
|
|
1442
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
868
1443
|
|
|
869
1444
|
client.delete_job(job_name)
|
|
870
1445
|
return [types.TextContent(type="text", text=f"Successfully deleted job '{job_name}'")]
|
|
@@ -872,9 +1447,8 @@ async def _tool_delete_job(client, args):
|
|
|
872
1447
|
|
|
873
1448
|
async def _tool_enable_job(client, args):
|
|
874
1449
|
"""Enable a job"""
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
raise ValueError("Missing required argument: job_name")
|
|
1450
|
+
# Input validation
|
|
1451
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
878
1452
|
|
|
879
1453
|
client.enable_job(job_name)
|
|
880
1454
|
return [types.TextContent(type="text", text=f"Successfully enabled job '{job_name}'")]
|
|
@@ -882,9 +1456,8 @@ async def _tool_enable_job(client, args):
|
|
|
882
1456
|
|
|
883
1457
|
async def _tool_disable_job(client, args):
|
|
884
1458
|
"""Disable a job"""
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
raise ValueError("Missing required argument: job_name")
|
|
1459
|
+
# Input validation
|
|
1460
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
888
1461
|
|
|
889
1462
|
client.disable_job(job_name)
|
|
890
1463
|
return [types.TextContent(type="text", text=f"Successfully disabled job '{job_name}'")]
|
|
@@ -892,11 +1465,9 @@ async def _tool_disable_job(client, args):
|
|
|
892
1465
|
|
|
893
1466
|
async def _tool_rename_job(client, args):
|
|
894
1467
|
"""Rename a job"""
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
if not job_name or not new_name:
|
|
899
|
-
raise ValueError("Missing required arguments: job_name and new_name")
|
|
1468
|
+
# Input validation
|
|
1469
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
1470
|
+
new_name = validate_job_name(args.get("new_name"))
|
|
900
1471
|
|
|
901
1472
|
client.rename_job(job_name, new_name)
|
|
902
1473
|
return [types.TextContent(type="text", text=f"Successfully renamed job '{job_name}' to '{new_name}'")]
|
|
@@ -906,9 +1477,8 @@ async def _tool_rename_job(client, args):
|
|
|
906
1477
|
|
|
907
1478
|
async def _tool_get_job_config(client, args):
|
|
908
1479
|
"""Get job configuration"""
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
raise ValueError("Missing required argument: job_name")
|
|
1480
|
+
# Input validation
|
|
1481
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
912
1482
|
|
|
913
1483
|
config = client.get_job_config(job_name)
|
|
914
1484
|
return [types.TextContent(type="text", text=config)]
|
|
@@ -916,11 +1486,9 @@ async def _tool_get_job_config(client, args):
|
|
|
916
1486
|
|
|
917
1487
|
async def _tool_update_job_config(client, args):
|
|
918
1488
|
"""Update job configuration"""
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
if not job_name or not config_xml:
|
|
923
|
-
raise ValueError("Missing required arguments: job_name and config_xml")
|
|
1489
|
+
# Input validation
|
|
1490
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
1491
|
+
config_xml = validate_config_xml(args.get("config_xml"))
|
|
924
1492
|
|
|
925
1493
|
client.update_job_config(job_name, config_xml)
|
|
926
1494
|
return [types.TextContent(type="text", text=f"Successfully updated config for job '{job_name}'")]
|
|
@@ -978,10 +1546,13 @@ async def _tool_list_nodes(client, args):
|
|
|
978
1546
|
|
|
979
1547
|
async def _tool_get_node_info(client, args):
|
|
980
1548
|
"""Get node information"""
|
|
1549
|
+
# Input validation
|
|
981
1550
|
node_name = args.get("node_name")
|
|
982
|
-
|
|
983
1551
|
if not node_name:
|
|
984
1552
|
raise ValueError("Missing required argument: node_name")
|
|
1553
|
+
if not isinstance(node_name, str):
|
|
1554
|
+
raise ValueError(f"node_name must be a string, got {type(node_name).__name__}")
|
|
1555
|
+
node_name = node_name.strip()
|
|
985
1556
|
|
|
986
1557
|
node_info = client.get_node_info(node_name)
|
|
987
1558
|
|
|
@@ -1002,6 +1573,342 @@ async def _tool_get_node_info(client, args):
|
|
|
1002
1573
|
]
|
|
1003
1574
|
|
|
1004
1575
|
|
|
1576
|
+
async def _tool_get_cache_stats(client, args):
|
|
1577
|
+
"""Get cache statistics"""
|
|
1578
|
+
cache_manager = get_cache_manager()
|
|
1579
|
+
stats = cache_manager.get_stats()
|
|
1580
|
+
cache_info = await cache_manager.get_cache_info()
|
|
1581
|
+
|
|
1582
|
+
report = f"""
|
|
1583
|
+
š Cache Statistics
|
|
1584
|
+
|
|
1585
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1586
|
+
OVERVIEW
|
|
1587
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1588
|
+
Cache Size: {stats['size']} entries
|
|
1589
|
+
Total Requests: {stats['total_requests']}
|
|
1590
|
+
Cache Hits: {stats['hits']}
|
|
1591
|
+
Cache Misses: {stats['misses']}
|
|
1592
|
+
Hit Rate: {stats['hit_rate_percent']}%
|
|
1593
|
+
Evictions: {stats['evictions']}
|
|
1594
|
+
|
|
1595
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1596
|
+
CACHED ENTRIES
|
|
1597
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1598
|
+
"""
|
|
1599
|
+
|
|
1600
|
+
for entry in cache_info['entries']:
|
|
1601
|
+
status = "ā
Valid" if not entry['is_expired'] else "ā Expired"
|
|
1602
|
+
report += f"\n{entry['key']}\n"
|
|
1603
|
+
report += f" Status: {status}\n"
|
|
1604
|
+
report += f" Age: {entry['age_seconds']}s\n"
|
|
1605
|
+
report += f" TTL: {entry['ttl_seconds']}s\n"
|
|
1606
|
+
report += f" Expires in: {entry['expires_in_seconds']}s\n"
|
|
1607
|
+
|
|
1608
|
+
return [types.TextContent(type="text", text=report.strip())]
|
|
1609
|
+
|
|
1610
|
+
|
|
1611
|
+
async def _tool_clear_cache(client, args):
|
|
1612
|
+
"""Clear all cached data"""
|
|
1613
|
+
cache_manager = get_cache_manager()
|
|
1614
|
+
cleared = await cache_manager.clear()
|
|
1615
|
+
|
|
1616
|
+
return [types.TextContent(
|
|
1617
|
+
type="text",
|
|
1618
|
+
text=f"ā
Cache cleared: {cleared} entries removed"
|
|
1619
|
+
)]
|
|
1620
|
+
|
|
1621
|
+
|
|
1622
|
+
# Health Check Tool (Quick Win #1)
|
|
1623
|
+
|
|
1624
|
+
async def _tool_health_check(client, args):
|
|
1625
|
+
"""
|
|
1626
|
+
Check Jenkins server health and connection status.
|
|
1627
|
+
Provides detailed diagnostics for troubleshooting.
|
|
1628
|
+
"""
|
|
1629
|
+
checks = {
|
|
1630
|
+
"server_reachable": False,
|
|
1631
|
+
"authentication_valid": False,
|
|
1632
|
+
"api_responsive": False,
|
|
1633
|
+
"server_version": None,
|
|
1634
|
+
"server_url": get_settings().url,
|
|
1635
|
+
"username": get_settings().username,
|
|
1636
|
+
"response_time_ms": None,
|
|
1637
|
+
"timestamp": None
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
status_emoji = "ā"
|
|
1641
|
+
status_text = "Unhealthy"
|
|
1642
|
+
error_details = None
|
|
1643
|
+
|
|
1644
|
+
try:
|
|
1645
|
+
import datetime
|
|
1646
|
+
start_time = time.time()
|
|
1647
|
+
checks["timestamp"] = datetime.datetime.now().isoformat()
|
|
1648
|
+
|
|
1649
|
+
# Test 1: Basic connectivity
|
|
1650
|
+
try:
|
|
1651
|
+
# Try to get user info (tests auth + connectivity)
|
|
1652
|
+
user_info = client.get_whoami()
|
|
1653
|
+
checks["server_reachable"] = True
|
|
1654
|
+
checks["authentication_valid"] = True
|
|
1655
|
+
|
|
1656
|
+
# Get version info
|
|
1657
|
+
try:
|
|
1658
|
+
version = client.get_version()
|
|
1659
|
+
checks["server_version"] = version
|
|
1660
|
+
checks["api_responsive"] = True
|
|
1661
|
+
except Exception as ve:
|
|
1662
|
+
logger.warning(f"Could not get version: {ve}")
|
|
1663
|
+
checks["api_responsive"] = False
|
|
1664
|
+
|
|
1665
|
+
# Calculate response time
|
|
1666
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
1667
|
+
checks["response_time_ms"] = round(elapsed_ms, 2)
|
|
1668
|
+
|
|
1669
|
+
# Determine overall status
|
|
1670
|
+
if checks["api_responsive"]:
|
|
1671
|
+
status_emoji = "ā
"
|
|
1672
|
+
status_text = "Healthy"
|
|
1673
|
+
if elapsed_ms > 2000:
|
|
1674
|
+
status_text = "Healthy (Slow)"
|
|
1675
|
+
status_emoji = "ā ļø"
|
|
1676
|
+
elif checks["authentication_valid"]:
|
|
1677
|
+
status_emoji = "ā ļø"
|
|
1678
|
+
status_text = "Partially Healthy (API issues)"
|
|
1679
|
+
|
|
1680
|
+
except Exception as conn_error:
|
|
1681
|
+
error_details = str(conn_error)
|
|
1682
|
+
error_type = type(conn_error).__name__
|
|
1683
|
+
|
|
1684
|
+
# Classify the error
|
|
1685
|
+
if 'timeout' in error_details.lower():
|
|
1686
|
+
status_text = "Timeout - Server not responding"
|
|
1687
|
+
checks["server_reachable"] = False
|
|
1688
|
+
elif '401' in error_details or 'unauthorized' in error_details.lower():
|
|
1689
|
+
status_text = "Authentication Failed"
|
|
1690
|
+
checks["server_reachable"] = True
|
|
1691
|
+
checks["authentication_valid"] = False
|
|
1692
|
+
elif 'connection' in error_details.lower():
|
|
1693
|
+
status_text = "Connection Failed - Server unreachable"
|
|
1694
|
+
checks["server_reachable"] = False
|
|
1695
|
+
else:
|
|
1696
|
+
status_text = f"Error: {error_type}"
|
|
1697
|
+
|
|
1698
|
+
except Exception as e:
|
|
1699
|
+
error_details = str(e)
|
|
1700
|
+
status_text = f"Health check failed: {type(e).__name__}"
|
|
1701
|
+
logger.error(f"Health check error: {e}", exc_info=True)
|
|
1702
|
+
|
|
1703
|
+
# Build detailed report
|
|
1704
|
+
report = f"""
|
|
1705
|
+
{status_emoji} Jenkins Health Check: {status_text}
|
|
1706
|
+
|
|
1707
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1708
|
+
CONNECTION STATUS
|
|
1709
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1710
|
+
Server URL: {checks['server_url']}
|
|
1711
|
+
Username: {checks['username']}
|
|
1712
|
+
Server Reachable: {'ā
Yes' if checks['server_reachable'] else 'ā No'}
|
|
1713
|
+
Authentication: {'ā
Valid' if checks['authentication_valid'] else 'ā Failed'}
|
|
1714
|
+
API Responsive: {'ā
Yes' if checks['api_responsive'] else 'ā No'}
|
|
1715
|
+
|
|
1716
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1717
|
+
SERVER DETAILS
|
|
1718
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1719
|
+
Jenkins Version: {checks['server_version'] or 'Unknown'}
|
|
1720
|
+
Response Time: {checks['response_time_ms']}ms
|
|
1721
|
+
Checked At: {checks['timestamp']}
|
|
1722
|
+
"""
|
|
1723
|
+
|
|
1724
|
+
if error_details:
|
|
1725
|
+
report += f"""
|
|
1726
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1727
|
+
ERROR DETAILS
|
|
1728
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1729
|
+
{error_details}
|
|
1730
|
+
"""
|
|
1731
|
+
|
|
1732
|
+
# Add troubleshooting tips if unhealthy
|
|
1733
|
+
if status_emoji == "ā":
|
|
1734
|
+
report += """
|
|
1735
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1736
|
+
TROUBLESHOOTING STEPS
|
|
1737
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1738
|
+
"""
|
|
1739
|
+
if not checks['server_reachable']:
|
|
1740
|
+
report += """
|
|
1741
|
+
š Server Not Reachable:
|
|
1742
|
+
1. Verify Jenkins is running
|
|
1743
|
+
2. Check the URL is correct
|
|
1744
|
+
3. Test with: curl {url}/api/json
|
|
1745
|
+
4. Check firewall/VPN settings
|
|
1746
|
+
5. Verify network connectivity
|
|
1747
|
+
""".format(url=checks['server_url'])
|
|
1748
|
+
|
|
1749
|
+
if checks['server_reachable'] and not checks['authentication_valid']:
|
|
1750
|
+
report += """
|
|
1751
|
+
š Authentication Failed:
|
|
1752
|
+
1. Verify username is correct
|
|
1753
|
+
2. Check API token is valid
|
|
1754
|
+
3. Generate new token:
|
|
1755
|
+
- Jenkins ā Your Name ā Configure
|
|
1756
|
+
- API Token section ā Add new Token
|
|
1757
|
+
4. Update .env file with new token
|
|
1758
|
+
"""
|
|
1759
|
+
|
|
1760
|
+
if checks['server_reachable'] and checks['authentication_valid'] and not checks['api_responsive']:
|
|
1761
|
+
report += """
|
|
1762
|
+
ā ļø API Not Responsive:
|
|
1763
|
+
1. Check Jenkins server logs
|
|
1764
|
+
2. Verify Jenkins is not overloaded
|
|
1765
|
+
3. Check for Jenkins plugin issues
|
|
1766
|
+
4. Restart Jenkins if needed
|
|
1767
|
+
"""
|
|
1768
|
+
|
|
1769
|
+
report += "\nš” Tip: Run this health-check regularly to monitor your Jenkins connection."
|
|
1770
|
+
|
|
1771
|
+
return [types.TextContent(type="text", text=report.strip())]
|
|
1772
|
+
|
|
1773
|
+
|
|
1774
|
+
async def _tool_get_metrics(client, args):
|
|
1775
|
+
"""Get usage metrics"""
|
|
1776
|
+
tool_name = args.get("tool_name")
|
|
1777
|
+
|
|
1778
|
+
metrics_collector = get_metrics_collector()
|
|
1779
|
+
|
|
1780
|
+
if tool_name:
|
|
1781
|
+
# Get specific tool metrics
|
|
1782
|
+
stats = await metrics_collector.get_tool_stats(tool_name)
|
|
1783
|
+
|
|
1784
|
+
report = f"""
|
|
1785
|
+
š Metrics for '{tool_name}'
|
|
1786
|
+
|
|
1787
|
+
{json.dumps(stats, indent=2)}
|
|
1788
|
+
"""
|
|
1789
|
+
else:
|
|
1790
|
+
# Get overall summary
|
|
1791
|
+
summary = await metrics_collector.get_summary()
|
|
1792
|
+
tool_stats = await metrics_collector.get_tool_stats()
|
|
1793
|
+
|
|
1794
|
+
report = f"""
|
|
1795
|
+
š Jenkins MCP Server Metrics
|
|
1796
|
+
|
|
1797
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1798
|
+
SUMMARY
|
|
1799
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1800
|
+
Uptime: {summary['uptime_human']}
|
|
1801
|
+
Total Executions: {summary['total_executions']}
|
|
1802
|
+
Successful: {summary['successful_executions']}
|
|
1803
|
+
Failed: {summary['failed_executions']}
|
|
1804
|
+
Success Rate: {summary['success_rate_percent']}%
|
|
1805
|
+
Avg Execution Time: {summary['avg_execution_time_ms']}ms
|
|
1806
|
+
Unique Tools Used: {summary['unique_tools_used']}
|
|
1807
|
+
Most Used Tool: {summary['most_used_tool']}
|
|
1808
|
+
Slowest Tool: {summary['slowest_tool']}
|
|
1809
|
+
|
|
1810
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1811
|
+
PER-TOOL STATISTICS
|
|
1812
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1813
|
+
{json.dumps(tool_stats, indent=2)}
|
|
1814
|
+
"""
|
|
1815
|
+
|
|
1816
|
+
return [types.TextContent(type="text", text=report.strip())]
|
|
1817
|
+
|
|
1818
|
+
|
|
1819
|
+
async def _tool_configure_webhook(client, args):
|
|
1820
|
+
"""Configure webhook for Jenkins job"""
|
|
1821
|
+
job_name = validate_job_name(args.get("job_name"))
|
|
1822
|
+
webhook_url = args.get("webhook_url")
|
|
1823
|
+
events = args.get("events", [])
|
|
1824
|
+
|
|
1825
|
+
if not webhook_url:
|
|
1826
|
+
raise ValueError("webhook_url is required")
|
|
1827
|
+
|
|
1828
|
+
if not events:
|
|
1829
|
+
raise ValueError("At least one event must be specified")
|
|
1830
|
+
|
|
1831
|
+
# Get current job config
|
|
1832
|
+
config_xml = client.get_job_config(job_name)
|
|
1833
|
+
|
|
1834
|
+
# Add webhook notification (this is simplified - actual implementation
|
|
1835
|
+
# depends on Jenkins plugin configuration)
|
|
1836
|
+
import xml.etree.ElementTree as ET
|
|
1837
|
+
|
|
1838
|
+
try:
|
|
1839
|
+
root = ET.fromstring(config_xml)
|
|
1840
|
+
|
|
1841
|
+
# Add or update properties section
|
|
1842
|
+
properties = root.find('properties')
|
|
1843
|
+
if properties is None:
|
|
1844
|
+
properties = ET.SubElement(root, 'properties')
|
|
1845
|
+
|
|
1846
|
+
# Add webhook trigger configuration
|
|
1847
|
+
# (Actual XML structure depends on the webhook plugin used)
|
|
1848
|
+
webhook_config = f"""
|
|
1849
|
+
<!-- Webhook Configuration -->
|
|
1850
|
+
<!-- Events: {', '.join(events)} -->
|
|
1851
|
+
<!-- URL: {webhook_url} -->
|
|
1852
|
+
"""
|
|
1853
|
+
|
|
1854
|
+
# Note: This is a placeholder. Real implementation would need
|
|
1855
|
+
# to configure the actual webhook plugin XML structure
|
|
1856
|
+
|
|
1857
|
+
updated_xml = ET.tostring(root, encoding='unicode')
|
|
1858
|
+
|
|
1859
|
+
# Update job
|
|
1860
|
+
client.update_job_config(job_name, updated_xml)
|
|
1861
|
+
|
|
1862
|
+
return [types.TextContent(
|
|
1863
|
+
type="text",
|
|
1864
|
+
text=f"ā
Webhook configured for '{job_name}'\n\n"
|
|
1865
|
+
f"URL: {webhook_url}\n"
|
|
1866
|
+
f"Events: {', '.join(events)}\n\n"
|
|
1867
|
+
f"ā ļø Note: Requires Generic Webhook Trigger plugin in Jenkins"
|
|
1868
|
+
)]
|
|
1869
|
+
|
|
1870
|
+
except Exception as e:
|
|
1871
|
+
return [types.TextContent(
|
|
1872
|
+
type="text",
|
|
1873
|
+
text=f"ā Failed to configure webhook: {str(e)}\n\n"
|
|
1874
|
+
f"Make sure the Generic Webhook Trigger plugin is installed in Jenkins."
|
|
1875
|
+
)]
|
|
1876
|
+
|
|
1877
|
+
|
|
1878
|
+
# Note: MCP protocol may not support streaming yet. This is prepared for future use.
|
|
1879
|
+
# Example: For trigger-multiple-builds
|
|
1880
|
+
async def _tool_trigger_multiple_builds_with_progress(client, args):
|
|
1881
|
+
"""Trigger builds with progress updates"""
|
|
1882
|
+
job_names = args.get("job_names", [])
|
|
1883
|
+
|
|
1884
|
+
# Initial message
|
|
1885
|
+
yield types.TextContent(
|
|
1886
|
+
type="text",
|
|
1887
|
+
text=f"š Starting batch build trigger for {len(job_names)} jobs..."
|
|
1888
|
+
)
|
|
1889
|
+
|
|
1890
|
+
results = []
|
|
1891
|
+
for i, job_name in enumerate(job_names, 1):
|
|
1892
|
+
# Progress update
|
|
1893
|
+
yield types.TextContent(
|
|
1894
|
+
type="text",
|
|
1895
|
+
text=f"ā³ [{i}/{len(job_names)}] Triggering {job_name}..."
|
|
1896
|
+
)
|
|
1897
|
+
|
|
1898
|
+
try:
|
|
1899
|
+
result = client.build_job(job_name)
|
|
1900
|
+
results.append({"job": job_name, "status": "success"})
|
|
1901
|
+
except Exception as e:
|
|
1902
|
+
results.append({"job": job_name, "status": "failed", "error": str(e)})
|
|
1903
|
+
|
|
1904
|
+
# Final summary
|
|
1905
|
+
successful = len([r for r in results if r["status"] == "success"])
|
|
1906
|
+
yield types.TextContent(
|
|
1907
|
+
type="text",
|
|
1908
|
+
text=f"ā
Complete: {successful}/{len(job_names)} builds triggered successfully"
|
|
1909
|
+
)
|
|
1910
|
+
|
|
1911
|
+
|
|
1005
1912
|
# ==================== Main Server Entry Point ====================
|
|
1006
1913
|
|
|
1007
1914
|
async def main():
|
|
@@ -1034,4 +1941,4 @@ async def main():
|
|
|
1034
1941
|
logger.info("Server stopped by user")
|
|
1035
1942
|
except Exception as e:
|
|
1036
1943
|
logger.error(f"Server error: {e}", exc_info=True)
|
|
1037
|
-
sys.exit(1)
|
|
1944
|
+
sys.exit(1)
|