From a445728f5a0b7274d2c52e7519e9858cd2d10829 Mon Sep 17 00:00:00 2001 From: Henrik Koren Date: Mon, 6 Apr 2026 11:10:13 +0300 Subject: [PATCH] docs(jenkins-mcp): add Jenkins MCP integration documentation and tests Add documentation and tests for the Jenkins MCP server integration: - New docs page with installation, configuration, and usage examples - Registration entries in toolsets index and why-holmesgpt pages - Config validation tests for the Jenkins MCP toolset - Unit tests with mocked _get_server_tools to eliminate network calls - Troubleshooting subsections use bold text instead of ### headers Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Signed-off-by: Henrik Koren --- README.md | 1 + docs/data-sources/builtin-toolsets/.nav.yml | 1 + docs/data-sources/builtin-toolsets/index.md | 8 + .../builtin-toolsets/jenkins-mcp.md | 238 ++++++++++++++++++ docs/why-holmesgpt.md | 1 + images/integration_logos/jenkins-icon.png | Bin 0 -> 4859 bytes images/integration_logos/jenkins-icon.svg | 1 + tests/test_mcp_toolset.py | 73 ++++++ 8 files changed, 323 insertions(+) create mode 100644 docs/data-sources/builtin-toolsets/jenkins-mcp.md create mode 100644 images/integration_logos/jenkins-icon.png create mode 100644 images/integration_logos/jenkins-icon.svg diff --git a/README.md b/README.md index 9863244d88..4b77c8b73a 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ HolmesGPT integrates with popular observability and cloud platforms. The followi | [Elasticsearch **Elasticsearch / OpenSearch**](https://holmesgpt.dev/data-sources/builtin-toolsets/elasticsearch/) | Query logs, cluster health, shard and index diagnostics | | [GCP **GCP**](https://holmesgpt.dev/data-sources/builtin-toolsets/gcp/) | Google Cloud Platform resources (MCP) | | [GitHub **GitHub**](https://holmesgpt.dev/data-sources/builtin-toolsets/github-mcp/) | Repositories, issues, and pull requests (MCP) | +| [Jenkins **Jenkins (MCP)**](https://holmesgpt.dev/data-sources/builtin-toolsets/jenkins-mcp/) | Build status, pipeline logs, and job history (MCP) | | [Grafana **Grafana**](https://holmesgpt.dev/data-sources/builtin-toolsets/grafanadashboards/) | Query and analyze dashboard configurations and panels | | [Helm **Helm**](https://holmesgpt.dev/data-sources/builtin-toolsets/helm/) | Release status, chart metadata, and values | | [Internet **Internet**](https://holmesgpt.dev/data-sources/builtin-toolsets/internet/) | Public runbooks, community docs etc | diff --git a/docs/data-sources/builtin-toolsets/.nav.yml b/docs/data-sources/builtin-toolsets/.nav.yml index 50108e28e5..92a69245a0 100644 --- a/docs/data-sources/builtin-toolsets/.nav.yml +++ b/docs/data-sources/builtin-toolsets/.nav.yml @@ -25,6 +25,7 @@ nav: - Helm: helm.md - Inspektor Gadget: inspektor-gadget.md - Internet: internet.md + - Jenkins (MCP): jenkins-mcp.md - Kafka: kafka.md - Kubectl Run: kubectl-run.md - Kubernetes: kubernetes.md diff --git a/docs/data-sources/builtin-toolsets/index.md b/docs/data-sources/builtin-toolsets/index.md index 6cb4138306..24e9338813 100644 --- a/docs/data-sources/builtin-toolsets/index.md +++ b/docs/data-sources/builtin-toolsets/index.md @@ -87,6 +87,14 @@ HolmesGPT includes pre-built integrations for popular monitoring and observabili +### CI/CD + +
+ +- [:simple-jenkins:{ .lg .middle } **Jenkins (MCP)**](jenkins-mcp.md) + +
+ ### Workflow Orchestration
diff --git a/docs/data-sources/builtin-toolsets/jenkins-mcp.md b/docs/data-sources/builtin-toolsets/jenkins-mcp.md new file mode 100644 index 0000000000..5c37e3b072 --- /dev/null +++ b/docs/data-sources/builtin-toolsets/jenkins-mcp.md @@ -0,0 +1,238 @@ +# Jenkins (MCP) + +The Jenkins MCP Server Plugin enables Holmes to interact with your Jenkins CI/CD infrastructure. + +## Prerequisites + +- A running Jenkins instance with the [MCP Server Plugin](https://plugins.jenkins.io/mcp-server/) installed +- A Jenkins API token for authentication +- Network connectivity from Holmes to the Jenkins MCP endpoint + +**Installing the Jenkins MCP Plugin:** + +1. In Jenkins, go to **Manage Jenkins** → **Plugins** → **Available plugins** +2. Search for "MCP Server" and install it +3. Restart Jenkins if required + +**Creating a Jenkins API Token:** + +1. Sign in to Jenkins +2. Click your username in the upper-right corner → **Security** +3. Under **API Token**, click **Add new Token** +4. Enter a descriptive name and click **Generate** +5. Copy the token immediately (it won't be shown again) +6. Click **Save** + +**Encoding credentials for Basic authentication:** + +The Jenkins MCP server uses HTTP Basic authentication. Encode your credentials: + +=== "Linux / macOS" + + ```bash + echo -n "username:api_token" | base64 + ``` + +=== "Windows (PowerShell)" + + ```powershell + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("username:api_token")) + ``` + +Store the encoded credential securely for use in the configuration below. + +## Configuration + +=== "Holmes CLI" + + Add the following to **~/.holmes/config.yaml**. Create the file if it doesn't exist: + + ```yaml + mcp_servers: + jenkins: + description: "Jenkins CI/CD server" + config: + url: "https://your-jenkins-instance/mcp-server/mcp" + mode: streamable-http + headers: + Authorization: "Basic " + verify_ssl: false # Set to true if using valid SSL certificates + icon_url: "https://cdn.simpleicons.org/jenkins/D24939" + llm_instructions: | + When investigating build failures, start with recent build status and then examine console output. + Use pagination for large result sets to avoid token overflow. + ``` + + Replace `` with your encoded `username:api_token`. + + --8<-- "snippets/toolset_refresh_warning.md" + +=== "Holmes Helm Chart" + + **Create Kubernetes Secret:** + + ```bash + # Encode your credentials + JENKINS_AUTH=$(echo -n "username:api_token" | base64) + + # Create the secret + kubectl create secret generic jenkins-credentials \ + --from-literal=token="$JENKINS_AUTH" \ + -n + ``` + + **Configure Helm Values:** + + ```yaml + # values.yaml + additionalEnvVars: + - name: JENKINS_AUTH_TOKEN + valueFrom: + secretKeyRef: + name: jenkins-credentials + key: token + + mcp_servers: + jenkins: + description: "Jenkins CI/CD server" + config: + url: "https://your-jenkins-instance/mcp-server/mcp" + mode: streamable-http + headers: + Authorization: "Basic {{ env.JENKINS_AUTH_TOKEN }}" + verify_ssl: false + icon_url: "https://cdn.simpleicons.org/jenkins/D24939" + llm_instructions: | + When investigating build failures, start with recent build status and then examine console output. + Use pagination for large result sets to avoid token overflow. + ``` + + Then deploy or upgrade your Holmes installation: + + ```bash + helm upgrade --install holmes robusta/holmes -f values.yaml + ``` + +=== "Robusta Helm Chart" + + **Create Kubernetes Secret:** + + ```bash + # Encode your credentials + JENKINS_AUTH=$(echo -n "username:api_token" | base64) + + # Create the secret + kubectl create secret generic jenkins-credentials \ + --from-literal=token="$JENKINS_AUTH" \ + -n + ``` + + **Configure Helm Values:** + + ```yaml + # generated_values.yaml + holmes: + additionalEnvVars: + - name: JENKINS_AUTH_TOKEN + valueFrom: + secretKeyRef: + name: jenkins-credentials + key: token + + mcp_servers: + jenkins: + description: "Jenkins CI/CD server" + config: + url: "https://your-jenkins-instance/mcp-server/mcp" + mode: streamable-http + headers: + Authorization: "Basic {{ env.JENKINS_AUTH_TOKEN }}" + verify_ssl: false + icon_url: "https://cdn.simpleicons.org/jenkins/D24939" + llm_instructions: | + When investigating build failures, start with recent build status and then examine console output. + Use pagination for large result sets to avoid token overflow. + ``` + + Then deploy or upgrade your Robusta installation: + + ```bash + helm upgrade --install robusta robusta/robusta -f generated_values.yaml --set clusterName=YOUR_CLUSTER_NAME + ``` + +!!! warning "MCP endpoint path" + The Jenkins MCP server serves on `/mcp-server/mcp` for Streamable HTTP transport. Other available endpoints: + + - **Streamable HTTP**: `/mcp-server/mcp` (recommended) + - **SSE**: `/mcp-server/sse` + - **Stateless**: `/mcp-server/stateless` + +## Testing the Connection + +```bash +holmes ask "List all Jenkins jobs" +``` + +## Common Use Cases + +**Investigate a failed build:** +```bash +holmes ask "Why did the last build of my-app-pipeline fail?" +``` + +**Check build status:** +```bash +holmes ask "What is the status of the most recent builds for the deploy-production job?" +``` + +**View build logs:** +```bash +holmes ask "Show me the console output from the last failed build of backend-service" +``` + +**Monitor pipeline stages:** +```bash +holmes ask "What stages failed in the latest run of the CI pipeline?" +``` + +**Check queue status:** +```bash +holmes ask "Are there any builds waiting in the Jenkins queue?" +``` + +**Analyze build trends:** +```bash +holmes ask "Show me the build history and success rate for the integration-tests job" +``` + +## Troubleshooting + +**Authentication Errors** + +If you receive 401 or 403 errors: + +1. Verify your API token is valid and not expired +2. Ensure the credentials are properly base64 encoded (username:token format) +3. Check that the Jenkins user has appropriate permissions + +**Connection Issues** + +If Holmes cannot connect to Jenkins: + +1. Verify the Jenkins URL is accessible from the Holmes pod/container +2. Check if SSL certificate verification is causing issues (`verify_ssl: false` for self-signed certs) +3. Ensure the MCP Server plugin is installed and enabled in Jenkins + +**Plugin Not Found** + +If the `/mcp-server/mcp` endpoint returns 404: + +1. Verify the MCP Server plugin is installed in Jenkins +2. Restart Jenkins after plugin installation +3. Check Jenkins system logs for plugin errors + +## Additional Resources + +- [Jenkins MCP Server Plugin](https://plugins.jenkins.io/mcp-server/) +- [Jenkins API Token Documentation](https://www.jenkins.io/doc/book/system-administration/authenticating-scripted-clients/) +- [Model Context Protocol Specification](https://modelcontextprotocol.io/) diff --git a/docs/why-holmesgpt.md b/docs/why-holmesgpt.md index f774452f28..7ede0774af 100644 --- a/docs/why-holmesgpt.md +++ b/docs/why-holmesgpt.md @@ -60,6 +60,7 @@ HolmesGPT ships with read-only integrations for every major observability vendor - **Traces**: Tempo, Datadog, NewRelic - **Dashboards**: Grafana - **Infrastructure**: Kubernetes, Docker, Helm, ArgoCD, OpenShift, Cilium, KubeVela +- **CI/CD**: Jenkins - **Cloud**: AWS RDS, Azure SQL, Azure AKS, GCP - **Databases**: PostgreSQL, MySQL, ClickHouse, MariaDB, SQL Server, MongoDB Atlas - **ITSM**: ServiceNow diff --git a/images/integration_logos/jenkins-icon.png b/images/integration_logos/jenkins-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e48276685989cf9e92716f61b6a08e9a9bf44f89 GIT binary patch literal 4859 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 \ No newline at end of file diff --git a/tests/test_mcp_toolset.py b/tests/test_mcp_toolset.py index 7a95e24a10..99145e03b5 100644 --- a/tests/test_mcp_toolset.py +++ b/tests/test_mcp_toolset.py @@ -2287,3 +2287,76 @@ def test_extra_headers_templates_not_resolved(self): # regular headers SHOULD be resolved by replace_env_vars_values assert config["config"]["headers"]["X-Static"] == "resolved_value" + + +class TestJenkinsMCPConfig: + """Validate that the Jenkins MCP integration config documented in + docs/data-sources/builtin-toolsets/jenkins-mcp.md is accepted by + RemoteMCPToolset and that its fields are preserved correctly. + """ + + _JENKINS_URL = "https://jenkins.example.com/mcp-server/mcp" + _JENKINS_AUTH = "dXNlcjp0b2tlbg==" # base64("user:token") + + def _make_toolset(self) -> RemoteMCPToolset: + """Return a RemoteMCPToolset configured exactly as shown in the Jenkins docs.""" + return RemoteMCPToolset( + name="jenkins", + description="Jenkins CI/CD server", + config={ + "url": self._JENKINS_URL, + "mode": "streamable-http", + "headers": {"Authorization": f"Basic {self._JENKINS_AUTH}"}, + "verify_ssl": False, + }, + ) + + def _stub_get_server_tools(self, monkeypatch, toolset: RemoteMCPToolset) -> None: + """Patch _get_server_tools so prerequisites_callable makes no network calls.""" + + async def _no_op(): + return ListToolsResult(tools=[]) + + monkeypatch.setattr(toolset, "_get_server_tools", _no_op) + + def test_jenkins_config_url_and_mode_parsed( + self, monkeypatch, suppress_migration_warnings + ): + """Documented Jenkins URL and streamable-http mode must be stored verbatim.""" + toolset = self._make_toolset() + self._stub_get_server_tools(monkeypatch, toolset) + toolset.prerequisites_callable(config=toolset.config) + + assert str(toolset._mcp_config.url) == self._JENKINS_URL + assert toolset._mcp_config.mode == MCPMode.STREAMABLE_HTTP + + def test_jenkins_config_auth_header_preserved( + self, monkeypatch, suppress_migration_warnings + ): + """Basic auth header must survive config parsing unchanged.""" + toolset = self._make_toolset() + self._stub_get_server_tools(monkeypatch, toolset) + toolset.prerequisites_callable(config=toolset.config) + + assert toolset._mcp_config.headers is not None + assert toolset._mcp_config.headers.get("Authorization") == ( + f"Basic {self._JENKINS_AUTH}" + ) + + def test_jenkins_config_ssl_verification_disabled( + self, monkeypatch, suppress_migration_warnings + ): + """verify_ssl=False must be reflected in the parsed config.""" + toolset = self._make_toolset() + self._stub_get_server_tools(monkeypatch, toolset) + toolset.prerequisites_callable(config=toolset.config) + + assert toolset._mcp_config.verify_ssl is False + + def test_jenkins_config_missing_url_fails_prerequisites(self): + """A Jenkins toolset with no URL must fail prerequisites with a clear error.""" + toolset = RemoteMCPToolset(name="jenkins", description="Jenkins CI/CD server") + ok, msg = toolset.prerequisites_callable(config=toolset.config) + + assert ok is False + assert msg # error message must be non-empty