ct-jsm-ops-mcp-server
CloudThinker MCP server for Jira Service Management Operations — alerts, on-call schedules, and an idempotent alert lifecycle, served over stdio.
Reads JSM_* env at startup, resolves the site's cloudId, proves the
credential can actually read Operations, then serves tools. There is no
connect or login tool: the first tool call works.
Why this exists
Atlassian's own Rovo MCP server exposes only four JSM ops tools
(getJsmOpsAlerts, getJsmOpsScheduleInfo, getJsmOpsTeamInfo,
updateJsmOpsAlert), and those are reachable only under API-token auth, not
OAuth — which is the auth CloudThinker's atlassian connection uses. That
surface also has no alert timeline, no responder management, and no assign or
escalate. So this is a from-scratch wrapper over the JSM ops REST API
(https://api.atlassian.com/jsm/ops/api/{cloudId}/v1) plus the Jira platform
API for the incident issue itself.
Environment
| Env var | Required | Default | Purpose |
|---|---|---|---|
JSM_SITE_URL |
yes | — | Site origin, e.g. https://acme.atlassian.net. Project and board URLs are accepted and normalized back to the origin. |
JSM_SITE_EMAIL |
yes | — | The Atlassian account the API token belongs to. |
JSM_API_TOKEN |
yes | — | Atlassian API token from id.atlassian.com. Sent as Authorization: Basic base64(email:token). |
LOG_LEVEL |
no | info |
Also mirrored into FASTMCP_LOG_LEVEL. |
cloudId is not an input. It is resolved from JSM_SITE_URL via
/_edge/tenant_info; it is a UUID the user has no reason to know.
Startup preflight
The server exits non-zero rather than starting when Operations is not usable, so the CloudThinker connection test reports the real reason instead of letting the failure surface mid-incident:
| Condition | Exit | Reported as |
|---|---|---|
| Missing env var | 2 | which var is unset |
Unparseable JSM_SITE_URL |
2 | expected form |
Site or cloudId unreachable |
1 | check the site URL |
| 401 on the ops probe | 1 | API token, not a password, not a Rovo token |
| 403 on the ops probe | 1 | Operations not usable: plan lacks it, or the account cannot open it |
| 404 on the ops probe | 1 | that id does not exist on this site |
A JSM site without Operations authenticates perfectly well and then answers
every ops path with 403 {"code": 40301, "message": "Account does not have access to Opsgenie."} — verified against a live site. Auth success alone proves
nothing, so the probe makes one real ops read (/alerts and /schedules, one
record each).
The 403 does not distinguish "the plan has no Operations" from "this user cannot open Operations", so neither does the error. It names both and quotes the provider's own message.
Tools
Read-only (8): list_alerts, get_alert, get_alert_timeline,
list_schedules, get_on_call, get_team, get_jsm_incident,
get_action_status.
Idempotent writes (4): acknowledge_alert, assign_alert,
add_alert_responder, add_alert_note.
Destructive writes (2): close_alert, escalate_alert.
Tool annotations are load-bearing. CloudThinker reads readOnlyHint and
destructiveHint off the MCP definition to route human approval, so
close_alert and escalate_alert — the two actions CloudThinker cannot undo —
always reach the approval gate.
The write contract
Every write runs the same three steps, so retrying is safe by construction:
read alert -> decide -> post -> poll requestId to settled
Alert actions are processed asynchronously: the POST returns only a
requestId, so "the action worked" is knowable only by polling
/alerts/requests/{id}. Each write returns one of:
| status | meaning |
|---|---|
applied |
the action ran and the provider confirmed it |
noop_already_applied |
the alert was already in the target state; nothing was sent |
conflict |
the alert moved somewhere the action does not apply from; nothing was sent, a recovery path is returned |
pending |
accepted but still queued; poll get_action_status, do not re-send |
failed |
the provider rejected it, or the alert was not found |
Idempotency is read from the provider, not from local bookkeeping, so it stays correct when the same action arrives from two paths or after a restart. The server never forces: a closed alert is never reopened.
escalate_alert is the exception with no provider-side idempotency handle —
no alert field says "already escalated to X" — so only the "still open" check
applies, and a replay pages someone twice. That is why it is destructive.
Local checks
uv sync
uv run pytest -q # pure helpers + lifecycle, offline
uv run python tests/smoke_listtools.py # tool names + annotations, no network
Live-site verification required before publishing
These are doc-derived and must be confirmed against a real JSM site with Operations. Fixtures built from documentation prove code against the wrong schema.
- Response envelopes.
{"values": [...], "links": {...}}is assumed for/alertsand/schedules.as_valuesalso accepts a bare array anddata/named alternates — record which one is real. - Alert logs and notes shapes. The note body key is assumed to be
notewithtextas a fallback.add_alert_notededupe correctness depends on this exact key. /teams/{id}. The team endpoint's path and response shape are the least documented of the set; confirm before trustingget_team.- Action request bodies.
{"owner": {...}}for assign,{"responder": {"id","type"}}for responders,{"escalation": {"id"}}for escalate,{"note": ...}for acknowledge/close/notes. - Request-status envelope.
isSuccessis assumed, flat or underdata. If a queued request instead 404s for longer than the poll budget, tuneACTION_POLL_ATTEMPTS. listToolsshows exactly the 14 tools and nothing else.
License
Apache-2.0.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ct_jsm_ops_mcp_server-0.1.0.tar.gz.
File metadata
- Download URL: ct_jsm_ops_mcp_server-0.1.0.tar.gz
- Upload date:
- Size: 139.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72b8c6a8737a523d970a3a6591ba352b6539dcdb024b561a093a7523f31ad784
|
|
| MD5 |
2e5bdd5119c126460744203e5302c2ec
|
|
| BLAKE2b-256 |
9186d2ea72b1083a4adce395e17ff3f2011e2a4dc9c968d5022a7f156692fc96
|
File details
Details for the file ct_jsm_ops_mcp_server-0.1.0-py3-none-any.whl.
File metadata
- Download URL: ct_jsm_ops_mcp_server-0.1.0-py3-none-any.whl
- Upload date:
- Size: 24.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
100927eee475e1ea71c2f73ed96afcb75d1f6f9c94d422a58310c97aaf04bc0b
|
|
| MD5 |
887c7048da3b6467ad8acb01bd81536c
|
|
| BLAKE2b-256 |
38c8730693955b0d22a5b3af86ccd068412acf165a98e35586483162017f5c98
|