Skip to main content
Glama
dittops
by dittops

jenkins-mcp

Two MCP servers, built and deployed from one repo:

Server

Entry point

What it does

jenkins-mcp

jenkins-mcp

Triggers a Jenkins automation job and returns its result

itsm-mcp

itsm-mcp

Updates an ITSM ticket: assigns a group, appends a note

They share nothing at runtime — separate processes, separate credentials, separate config files — but share the same conventions: a JSON file declaring what the server may do, typed errors returned as tool results rather than raised, and one MCP_TRANSPORT switch for stdio vs HTTP. Most of this README covers the Jenkins server; see ITSM server for the other one.

jenkins-mcp

A single Jenkins job dispatches on its action_name build parameter, so the server exposes a registry of actions (fetchos, fetchtop, fetchcpu, …) rather than a registry of jobs. Adding an action is a JSON edit, not a code change.

Related MCP server: Jenkins MCP Server

How a call flows

  1. POST /job/<job>/buildWithParameters?token=… → Jenkins returns a queue URL

  2. Poll the queue item until it is assigned a build number

  3. Poll the build until building == false

  4. Read consoleText and extract the value the job declared at its job_output: marker

Every wait is bounded by a deadline, every response is status-checked, and polling backs off 2s → 10s.

Result extraction

Jobs declare their result in one of two shapes, both matched by the same output_marker:

job_output: 17.9.4a                  # bare marker line
{'job_output': "RedHat-8"}           # printed dict (Python repr or JSON)

For the bare form the rest of that line becomes the tool's output field. Capture stops at the newline: a real console interleaves Jenkins' own epilogue (Build step ... marked build as, Archiving artifacts, [Pipeline] // stage) between the job's output and Finished:, and consuming past the newline would swallow it.

For the dict form the marker's value is parsed out and unquoted, so {'job_output': "RedHat-8"} yields RedHat-8 — not the raw fragment. Both quote styles work, other keys in the dict are ignored, and log prefixes or trailing text on the same line (… INFO {'job_output': "RedHat-8"} took 3.2s) are excluded, since capture is bounded by the dict's own braces. A nested value comes back as JSON for the caller to re-parse. Parsing is literal-only (ast.literal_eval, then json.loads) — nothing in the console is executed.

Matching is case-insensitive and the last marker wins, so a rerun reports its final answer. If no marker is present, output is null and the response says so, with console_tail for context.

For jobs that print block output (pretty-printed JSON, a table) after a bare marker, set "output_multiline": true in the registry. Capture then continues past the first newline, stopping at a blank line, at a Jenkins-looking line, or at the next marker. The dict form needs no such flag — it is already delimited by its braces, spanning as many lines as it likes.

Configuration

Copy .env.example to .env and fill it in — it is read at startup from the working directory (or JENKINS_ENV_FILE). Real environment variables take precedence, so the env block in your MCP client config overrides the file.

Variable

Purpose

JENKINS_URL

Base URL, e.g. http://jenkins.example.com:8080

JENKINS_USER

Jenkins user

JENKINS_API_TOKEN

Jenkins API token (prefer over an account password)

JENKINS_BUILD_TOKEN

Remote-trigger token (?token=)

JENKINS_ACTIONS_CONFIG

Path to actions.json

JENKINS_VERIFY_SSL

Default true

JENKINS_TRUST_ENV

false ignores proxy env vars (default)

JENKINS_TIMEOUT_SECONDS

Hard cap on any wait, default 600

JENKINS_POLL_INTERVAL / JENKINS_POLL_MAX_INTERVAL

Backoff bounds

JENKINS_CONSOLE_TAIL_LINES

Console lines returned, default 0 (no limit)

actions.json

The registry declares the job, the parameters shared by every action, and the supported actions. It is also the allowlist — an action not listed here cannot be triggered.

{
  "job": "AUTOMATION_Trigger_Workflow",
  "output_marker": "job_output:",
  "output_multiline": false,
  "parameters": [
    { "name": "customer_name", "required": true, "description": "Customer identifier" },
    { "name": "ipaddress", "required": true, "description": "Device IP address" }
  ],
  "actions": {
    "fetchos": { "description": "Fetch the OS version running on the device." },
    "fetchcpu": "Fetch current CPU utilisation.",
    "fetchtop": { "description": "Top processes", "action_value": "fetch_top_v2" }
  }
}

Adding an action is a one-line edit — no code change. Notes:

  • An action may declare its own extra parameters, merged over the job-wide ones.

  • action_value sends a different action_name than the alias the model sees.

  • A bare string is shorthand for {"description": …}.

Tools

Tool

Purpose

list_jenkins_actions()

Discover actions and their required parameters

run_jenkins_action(action, parameters, wait=true, timeout_seconds=None)

Trigger and return the result

get_jenkins_build(build_number=None, queue_url=None, wait=false, timeout_seconds=None)

Status/result of a build

get_jenkins_console(build_number, tail_lines_count=0)

Raw console for debugging; 0 returns all of it

wait=false returns as soon as a build number exists, so a long build does not blow past the MCP client's request timeout; poll get_jenkins_build afterwards. Its wait for a build number is separately bounded (15s) rather than using the full build deadline — otherwise a backed-up queue would block it for the very duration the flag exists to avoid. If Jenkins has not assigned a number by then it returns status: "QUEUED" with the queue_url, so the triggered build is never orphaned; pass that URL back to get_jenkins_build to resolve it.

Typical success response:

{
  "ok": true,
  "action": "fetchos",
  "build_number": 412,
  "status": "SUCCESS",
  "url": "http://jenkins.example.com:8080/job/AUTOMATION_Trigger_Workflow/412/",
  "duration_ms": 38210,
  "output": "17.9.4a",
  "console_tail": "…"
}

Errors are returned as {"ok": false, "error": …, "error_type": …} rather than raised, so the model can read and act on them. Network failures become JenkinsConnectionError with a message naming the host — httpx.ConnectTimeout stringifies to "", which would otherwise reach the model as a blank error.

Troubleshooting

error_type

Meaning

ConfigError

A required env var is missing, or actions.json is absent/malformed

JenkinsConnectionError

Host unreachable — wrong JENKINS_URL, or you are off the VPN

JenkinsAPIErrorHTTP 401/403

Bad JENKINS_API_TOKEN, or missing/incorrect JENKINS_BUILD_TOKEN

JenkinsAPIErrorHTTP 404

Job name in actions.json does not match Jenkins

UnknownActionError / ParameterError

Rejected before any network call

Check reachability independently of the server:

nc -z -G 5 jenkins.example.com 8080 && echo reachable || echo unreachable

Guardrails

  • Action allowlist — only registry actions run; unknown names are rejected before any network call.

  • Parameter validation — unknown keys are rejected, not forwarded, so extra build parameters cannot be smuggled in.

  • action_name is server-set — a caller passing it directly is rejected, so the chosen action cannot be spoofed past the allowlist.

  • Credential redaction — userinfo and query strings are stripped from every URL returned or embedded in an error.

  • Console truncation — off by default; the full log is returned. Set JENKINS_CONSOLE_TAIL_LINES (or tail_lines_count) to a positive number to bound context usage. Note that an unbounded log is downloaded whole into memory and may be truncated by the MCP client instead.

  • Console output is untrusted device data; the tool descriptions tell the model not to follow instructions found inside it.

Example client

examples/client_example.py lists the tools and calls one. It works against either transport and is the quickest way to check a deployment. The ITSM server has its own, examples/itsm_client_example.py, which takes the same --url flag.

.venv/bin/python examples/client_example.py

That spawns the server over stdio itself — nothing needs to be running first — and calls the read-only list_jenkins_actions. To test a server already listening over HTTP:

.venv/bin/python examples/client_example.py --url http://127.0.0.1:8000/mcp

To actually start a build, add --trigger. This runs a real Jenkins job against a real device:

.venv/bin/python examples/client_example.py --trigger --action fetchos \
  --param customer_name=acme --param ipaddress=10.0.0.1 --param hostname=sw1 \
  --param username=admin --param device_type=cisco_ios

Add --no-wait to exercise the two-step path (return on start, then poll get_jenkins_build). Exit status is 0 only if the build succeeded, so the script also works as a smoke test in CI.

Transports

MCP_TRANSPORT selects how clients reach the server. The tools are identical either way.

Variable

Default

Purpose

MCP_TRANSPORT

stdio

stdio, streamable-http, or sse

MCP_HOST

127.0.0.1

HTTP bind address

MCP_PORT

8000

HTTP port

MCP_PATH

/mcp

HTTP endpoint path

stdio (default, local)

The client spawns the process and owns its stdin/stdout. No port, nothing on the network. Run by hand and it will sit silently waiting for JSON-RPC on stdin — that is a healthy server, not a hang. This is the right mode for local Claude Code; see the .mcp.json block below.

streamable-http (remote URL)

MCP_TRANSPORT=streamable-http MCP_PORT=8000 .venv/bin/python -m jenkins_mcp.server

Requires the http extra (pip install -e ".[http]"). Serves at http://<host>:<port>/mcp, which is the URL a remote client connects to:

{
  "mcpServers": {
    "jenkins": { "type": "http", "url": "http://your-host:8000/mcp" }
  }
}

Config comes from the process environment (or .env) rather than a client's env block, since the client no longer launches the process.

Securing an HTTP deployment

This server has no authentication of its own. Anyone who can reach the URL can trigger Jenkins jobs against your network devices. It binds 127.0.0.1 by default for that reason.

  • Easiest safe remote access: keep the 127.0.0.1 bind and tunnel — ssh -L 8000:127.0.0.1:8000 user@host.

  • If you must bind 0.0.0.0, put a TLS-terminating authenticating reverse proxy in front of it and firewall the port to known clients. Plain HTTP would also put the traffic on the wire in cleartext.

Container image

docker build -t dittops/jenkins-mcp:0.1.0 .
docker run --rm -p 8000:8000 \
  -e JENKINS_URL=http://jenkins.example.com:8080 \
  -e JENKINS_USER=jenkins-bot \
  -e JENKINS_API_TOKEN=… \
  -e JENKINS_BUILD_TOKEN=… \
  dittops/jenkins-mcp:0.1.0

The image defaults to streamable-http on 0.0.0.0:8000/mcp — a container is reached over the network, and the local 127.0.0.1 default would be unreachable from outside the container. It runs as uid 10001 and works with a read-only root filesystem (--read-only --tmpfs /tmp). actions.json is baked in at /app/actions.json; mount over it, or point JENKINS_ACTIONS_CONFIG elsewhere, to change the registry without rebuilding.

For a stdio client that spawns the container itself:

docker run -i --rm -e MCP_TRANSPORT=stdio --env-file .env dittops/jenkins-mcp:0.1.0

The ITSM server has its own image, built from Dockerfile.itsm out of the same source tree. Same defaults, with itsm.json baked in at /app/itsm.json:

docker build -f Dockerfile.itsm -t dittops/itsm-mcp:0.1.0 .
docker run --rm -p 8001:8000 \
  -e ITSM_URL=https://10.10.146.120 \
  -e ITSM_AUTHTOKEN=… \
  dittops/itsm-mcp:0.1.0

Kubernetes

Two servers, two charts, two images. charts/jenkins-mcp deploys the HTTP transport with the action registry in a ConfigMap and the Jenkins credentials in a Secret; the steps below install it into a jenkins-mcp namespace. charts/itsm-mcp is the same shape for the ITSM server — see Deploying itsm-mcp.

1. Push the image

The cluster has to be able to pull it, so a locally built image is not enough:

docker build -t dittops/jenkins-mcp:0.1.0 .
docker push dittops/jenkins-mcp:0.1.0

For a private registry, add --set imagePullSecrets[0].name=regcred at install time and create that pull secret in the namespace.

2. Create the namespace and the credentials Secret

Keep the credentials out of the Helm release. Values passed with --set are stored in the release secret and land in your shell history; a Secret you create separately does not:

kubectl create namespace jenkins-mcp

kubectl -n jenkins-mcp create secret generic jenkins-creds \
  --from-literal=JENKINS_USER='jenkins-bot' \
  --from-literal=JENKINS_API_TOKEN='<api-token>' \
  --from-literal=JENKINS_BUILD_TOKEN='<build-token>'

Use a Jenkins API token, not an account password. Omit JENKINS_BUILD_TOKEN if the job has no remote-trigger token — the chart treats that key as optional.

The three key names are what the chart reads by default. To reuse a Secret that names them differently, point the chart at its keys instead of renaming anything:

--set jenkins.auth.userKey=username \
--set jenkins.auth.apiTokenKey=api_token \
--set jenkins.auth.buildTokenKey=build_token

To rotate a credential later, update the Secret and restart the pods — the values are read into the environment at startup:

kubectl -n jenkins-mcp create secret generic jenkins-creds \
  --from-literal=JENKINS_USER='jenkins-bot' \
  --from-literal=JENKINS_API_TOKEN='<new-token>' \
  --from-literal=JENKINS_BUILD_TOKEN='<build-token>' \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl -n jenkins-mcp rollout restart deploy/jenkins-mcp

3. Install the chart

helm upgrade --install jenkins-mcp charts/jenkins-mcp \
  --namespace jenkins-mcp \
  --set jenkins.url=http://jenkins.example.com:8080 \
  --set jenkins.auth.existingSecret=jenkins-creds

Add --dry-run=server to validate against the live API without installing. The chart refuses to render without jenkins.url and a credential source, so a half-configured release fails here rather than in CrashLoopBackOff.

Without a pre-made Secret, the chart will render one from values instead:

--set jenkins.auth.user=jenkins-bot \
--set jenkins.auth.apiToken='<api-token>' \
--set jenkins.auth.buildToken='<build-token>'

4. Verify

kubectl -n jenkins-mcp rollout status deploy/jenkins-mcp
kubectl -n jenkins-mcp logs deploy/jenkins-mcp

kubectl -n jenkins-mcp port-forward svc/jenkins-mcp 8000:8000 &
.venv/bin/python examples/client_example.py --url http://127.0.0.1:8000/mcp

That lists the tools and calls the read-only list_jenkins_actions, which exercises config loading and the registry without starting a build. Use the venv's Python: the client needs mcp ≥2.0.0, and a system-wide older mcp fails with too many values to unpack.

In-cluster clients need no port-forward:

{
  "mcpServers": {
    "jenkins": {
      "type": "http",
      "url": "http://jenkins-mcp.jenkins-mcp.svc.cluster.local:8000/mcp"
    }
  }
}

Afterwards

Adding an action is a values edit plus helm upgrade, and the pods restart on the change. Chart values, the existingSecret layout, and the scaling and network caveats are in charts/jenkins-mcp/README.md — the short version is that the Service stays ClusterIP, the Ingress stays off unless something in front of it authenticates, and replicaCount stays at 1 because streamable-http sessions are held in one pod's memory.

helm rollback jenkins-mcp -n jenkins-mcp      # previous revision
helm uninstall jenkins-mcp -n jenkins-mcp     # leaves jenkins-creds in place

Deploying itsm-mcp

Its own image, release, namespace and Secret. The image shares this source tree but is built from Dockerfile.itsm, so the two servers version and roll back independently:

docker build -f Dockerfile.itsm -t dittops/itsm-mcp:0.1.0 .
docker push dittops/itsm-mcp:0.1.0
kubectl create namespace itsm-mcp

kubectl -n itsm-mcp create secret generic itsm-creds \
  --from-literal=ITSM_AUTHTOKEN='…'

helm upgrade --install itsm-mcp charts/itsm-mcp \
  --namespace itsm-mcp \
  --set itsm.url=https://10.10.146.120 \
  --set itsm.auth.existingSecret=itsm-creds

Verify it the same way, with the read-only tool:

kubectl -n itsm-mcp port-forward svc/itsm-mcp 8000:8000
.venv/bin/python examples/itsm_client_example.py --url http://127.0.0.1:8000/mcp

Values, the write policy, and the security caveats are in charts/itsm-mcp/README.md. The one that differs from the Jenkins chart: this server writes, so scope the authtoken to a technician who may only reassign groups and add notes.

ITSM server (itsm-mcp)

Wraps PUT /api/v3/requests/{id} on ManageEngine ServiceDesk Plus. Two things can be written — the assigned group and an appended note — and nothing else.

Why it is not a generic API wrapper

The v3 API takes its whole payload as one input_data JSON string, so anything that reaches that blob lands on the ticket: status, priority, requester, technician. This server never forwards a caller-supplied blob. input_data is assembled in build_update_payload from typed arguments, which is what bounds the write surface to those two fields.

Tools

Tool

Purpose

list_itsm_groups

Commonly used groups, plus the note policy. Call first.

update_itsm_ticket

Assign a group and/or append a note to one ticket.

update_itsm_ticket(request_id, group=None, note=None, show_to_requester=None, mark_first_response=None, add_to_linked_requests=None) — at least one of group / note is required. Any group name is accepted; a name matching the configured list is sent in that list's spelling, so "iccm tools" assigns ICCM Tools, and anything else goes through verbatim.

itsm.json

The write policy. Point at it with ITSM_CONFIG.

{
  "groups": [
    { "name": "ICCM Tools", "description": "ICCM tooling and automation queue." },
    { "name": "ICCM Database", "description": "ICCM database queue." }
  ],
  "notes": {
    "max_length": 5000,
    "defaults": {
      "show_to_requester": true,
      "mark_first_response": false,
      "add_to_linked_requests": true
    },
    "allowed": {
      "show_to_requester": true,
      "add_to_linked_requests": true
    }
  }
}

groups is a hint, not an allowlist. Any group name is accepted and forwarded; ITSM is the authority on which groups exist and rejects one that does not. Listing a group here gives the model something to discover and fixes its spelling. The shorthand ["ICCM Tools", "Network Ops"] works too, and an empty or absent list is fine — nothing is restricted either way.

defaults are what applies when the model says nothing; allowed is a hard gate an operator can close, independent of the defaults.

show_to_requester publishes the note to the customer and add_to_linked_requests copies it onto every linked ticket. Both default to on: the normal case here is a customer-visible update that propagates across the linked set, and an internal-only note is what a caller asks for explicitly with show_to_requester=false. mark_first_response stays off by default, since claiming the SLA first response is rarely what an automated note should do.

Closing a gate without touching defaults turns that flag off rather than leaving a default the gate would reject; spelling out both contradictorily (defaults.X: true with allowed.X: false) is refused at startup instead of failing every note at call time.

Configuration

Variable

Default

Purpose

ITSM_URL

Required. e.g. https://10.10.146.120

ITSM_AUTHTOKEN

Required. ServiceDesk Plus technician authtoken

ITSM_PORTAL_ID

1

Sent as the PORTALID header

ITSM_CONFIG

./itsm.json

Write policy. Explicitly set but missing → startup error

ITSM_VERIFY_SSL

true

See the TLS note below

ITSM_TIMEOUT_SECONDS

30

Per-request timeout

ITSM_TRUST_ENV

false

true honours HTTP(S)_PROXY

TLS. An instance addressed by IP (https://10.10.146.120) normally presents a cert that fails hostname verification. Prefer exporting SSL_CERT_FILE pointing at your internal CA over setting ITSM_VERIFY_SSL=false — the authtoken is a bearer credential and travels on every request.

Behaviour worth knowing

  • The v3 API reports rejected field values inside an HTTP 200, in a response_status envelope. That is checked, so a 200 with status: failed comes back as an error rather than a false success.

  • Redirects are not followed: the authtoken is a header on every request and a followed redirect would hand it to whatever host the redirect names.

  • Notes are stored as HTML by ServiceDesk Plus — prefer plain text.

  • Ticket subjects and statuses echoed back were written by requesters and technicians. They are untrusted data, not instructions.

Register it

{
  "mcpServers": {
    "itsm": {
      "command": "/path/to/jenkins-job-mcp/.venv/bin/itsm-mcp",
      "env": {
        "ITSM_URL": "https://10.10.146.120",
        "ITSM_AUTHTOKEN": "…",
        "ITSM_CONFIG": "/path/to/jenkins-job-mcp/itsm.json"
      }
    }
  }
}

Over HTTP it takes the same MCP_TRANSPORT / MCP_HOST / MCP_PORT / MCP_PATH switches as the Jenkins server, with the same warning: no authentication of its own, so keep it behind an authenticating proxy.

MCP_TRANSPORT=streamable-http MCP_PORT=8001 .venv/bin/itsm-mcp

Try it

examples/itsm_client_example.py spawns the server over stdio itself and calls the read-only list_itsm_groups, so it exercises config loading and the policy without touching a ticket:

.venv/bin/python examples/itsm_client_example.py

Against a server already listening over HTTP, and then for a real write:

.venv/bin/python examples/itsm_client_example.py --url http://127.0.0.1:8001/mcp
.venv/bin/python examples/itsm_client_example.py --update --ticket 2977634 --group "ICCM Tools" --note "Reassigning for triage"

--update modifies the live ticket immediately. The visibility flags (--show-to-requester, --mark-first-response, --add-to-linked-requests) are omitted unless passed, so the server's configured defaults apply.

Deploying it

See Deploying itsm-mcp — same image as the Jenkins server, its own chart at charts/itsm-mcp.

Setup

python3 -m venv .venv && .venv/bin/pip install -e .

This installs both entry points, jenkins-mcp and itsm-mcp.

Run the tests:

PYTHONPATH=src .venv/bin/python -m pytest tests/ -q

Register with Claude Code (.mcp.json), pointing at your real credentials:

{
  "mcpServers": {
    "jenkins": {
      "command": "/path/to/jenkins-mcp/.venv/bin/jenkins-mcp",
      "env": {
        "JENKINS_URL": "http://jenkins.example.com:8080",
        "JENKINS_USER": "jenkins-user",
        "JENKINS_API_TOKEN": "…",
        "JENKINS_BUILD_TOKEN": "your-build-token",
        "JENKINS_ACTIONS_CONFIG": "/path/to/jenkins-mcp/actions.json"
      }
    }
  }
}

Credential note

Use a Jenkins API token, not an account password, and keep it in .env (gitignored) or your MCP client's env block — never in a tracked file.

F
license - not found
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    Enables managing Jenkins operations like listing jobs, triggering builds, and checking build statuses through a configurable MCP server.
    3
    10
    Apache 2.0
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents and external systems to programmatically trigger and monitor Jenkins jobs, retrieve build status and logs via MCP standards.

View all related MCP servers

Related MCP Connectors

  • MCP server for secureFlows: token-free URL builders and integration-linting tools for AI agents.

  • The MCP server for Azure DevOps, bringing the power of Azure DevOps directly to your agents.

  • Personal assistant MCP server with search, execute, packages, jobs, secrets, and integrations.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dittops/jenkins-job-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server