jira-mcp
by Mr-pixle
README.md
# jira-mcp
[](https://github.com/Mr-pixle/jira-mcp/actions/workflows/ci.yml)
[](https://www.python.org/downloads/)
[](https://modelcontextprotocol.io/)
[](LICENSE)
**An MCP server for Jira Data Center.** Lets Claude Desktop, Claude Code, or any
MCP-compatible client search, analyse and manage Jira through its official REST
API — with a permission policy, a preview step, and an audit trail in front of
every change.
**[فارسی](README.fa.md)** · 83 tools · 6 prompts · 6 skills · 579 tests
```
You: Is the current sprint going to make it?
Claude: Sprint 14 is at risk. 18 of 24 issues done (32/45 points) with 3 days
left, but two issues have been blocked for six days. At the current
rate it projects to 84% completion.
You: Close it, then
Claude: PREVIEW (nothing changed): would close Sprint 14; 6 issues are not
done and will be moved out: OPS-14, OPS-22, OPS-31...
```
## Why this exists
Jira's REST API answers questions like *"what issues are in sprint 87"*. People
ask questions like *"is the sprint going to make it?"*. This server closes that
gap, and tries hard not to lie while doing it:
- **Missing data is reported, not zeroed.** No Story Points field configured?
The report says "unavailable" rather than showing `0.0`, which would read as
*the team completed nothing*.
- **Every number carries its definition.** "Velocity" means different things at
different companies, so each response says how it was computed —
`Report.explain_metrics` gives the full rule and its caveat.
- **Heuristics name their evidence.** Jira has no native "blocked" concept, so
detection checks three signals and always reports which one matched. A team
that records blockers in comments legitimately shows zero, and is told so.
- **Destructive changes preview first**, reading real state rather than
restating your request.
## Status
Complete and in use. Version 1.0.0 — see the [changelog](CHANGELOG.md).
## Design principles
| Principle | What it means here |
|---|---|
| REST API only | No direct database access, ever. Upgrade-safe by construction. |
| Domain tools | Tools are named `Issue.search`, not `GET /rest/api/2/search`. |
| One response shape | Every tool returns `success / message / data / warnings / next_actions / meta`. |
| Permission guard | Each tool declares `read` / `write` / `admin`; policy decides what is even visible. |
| Preview before change | Destructive and bulk tools return a dry-run first; the real change needs `confirm=true`. |
| Multi-instance | Production, staging and dev switch by config, not by code. |
| Audited | Every write is appended to a JSON Lines audit log with secrets redacted. |
| Tested | Every tool has unit tests against a mocked Jira — no live server needed. |
## Quick start
You need Python 3.10+ and a Jira Data Center [Personal Access
Token](https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html)
(Jira 8.14+).
```bash
git clone https://github.com/Mr-pixle/jira-mcp
cd jira-mcp
uv venv && uv pip install -e ".[dev]"
cp .env.example .env # set JIRA_PROD_PAT
cp config/instances.yaml.example config/instances.yaml
$EDITOR config/instances.yaml # set your Jira base_url
uv run jira-mcp --check # validates config and credentials, contacts nothing
```
Then run it — or let your client run it for you:
```bash
uv run jira-mcp # stdio, for a local client
uv run jira-mcp --transport http --port 8000 # HTTP, for a shared server
```
> `.env` and `config/instances.yaml` are git-ignored. The first holds your
> token, the second your internal hostname.
### Connect Claude Desktop / Claude Code (stdio)
```json
{
"mcpServers": {
"jira": {
"command": "uv",
"args": ["run", "--directory", "/opt/jira-mcp", "jira-mcp"],
"env": { "JIRA_PROD_PAT": "your-token-here" }
}
}
}
```
Then ask: *"Check the Jira connection"* → the client calls `jira_system_health`.
## Available tools
83 tools — 57 read, 26 write. Full details in
[`docs/Tool-Reference.md`](docs/Tool-Reference.md) (generated from the registry).
| Domain | Read | Write |
|---|---|---|
| **System** | `health`, `whoami`, `list_instances`, `list_tools` | — |
| **Issues** | `search` (JQL), `get`, `get_transitions`, `get_changelog`, `get_link_types` | `create`, `update`, `transition`, `assign`, `link` |
| **Fields** | `list` — find custom field IDs like Story Points | — |
| **Projects** | `list`, `get`, `get_components`, `get_versions`, `get_issue_types` | `create_version`, `release_version` |
| **Users** | `search`, `get`, `find_assignable` | — |
| **Boards** | `list`, `get`, `get_configuration`, `get_backlog`, `get_epics` | — |
| **Sprints** | `list`, `get`, `get_issues` | `create`, `start`, `close`, `move_issues`, `move_to_backlog` |
| **Comments** | `list` | `add`, `update`, `delete` |
| **Worklogs** | `list` | `add`, `update`, `delete` |
| **Attachments** | `list`, `get_text` | `upload`, `delete` |
| **Filters** | `list_favourites`, `get`, `run` | — |
| **Dashboards** | `list`, `get` | — |
| **Bulk** | — | `transition`, `assign`, `comment`, `update_labels`, `update_field`, `move_to_sprint` |
| **Reports** | `sprint_health`, `velocity`, `blocked_issues`, `overdue_issues`, `workload_balance`, `release_readiness`, `team_performance`, `executive_summary`, `explain_metrics` | — |
| **eazyBI** | `health`, `export_report`, `get_export_url`, `generate_report_definition`, `explain_report`, plus 8 experimental metadata tools | — |
Regenerate the reference after adding tools:
```bash
uv run jira-mcp --generate-docs # writes docs/Tool-Reference.md
```
### Things you can ask for today
> Which issues in OPS have been in progress for more than five days?
> Show me the current sprint on the platform board and how it's tracking.
> Create a bug in OPS about the Safari login failure and assign it to Ali.
> Move OPS-123 to Done with resolution Fixed and log 3 hours against it.
> Close Sprint 14 — but tell me what's unfinished first.
> What does the error log attached to OPS-123 say?
> Is the current sprint going to make it, and why not?
> What's our velocity over the last six sprints — improving or declining?
> Give me a status roll-up across OPS, MOB and WEB.
> Is the work spread fairly this sprint?
> How exactly do you calculate velocity?
## Preview before change
Destructive and high-impact tools return a **preview** on the first call and
change nothing. The preview shows real before/after values read from Jira, not a
restatement of the request:
```
You: Close sprint 14
Claude: [calls jira_sprint_close]
PREVIEW (nothing changed): would close sprint 'Sprint 14' (id=87);
3 issues are not done and will be moved out: OPS-2, OPS-3, OPS-7
You: Go ahead
Claude: [calls jira_sprint_close with confirm=true]
Closed sprint 'Sprint 14'.
```
Which tools preview is a policy decision in `config/permissions.yaml`, not a
property of the tool — routine single-issue edits run in one step by default,
while `Sprint.close`, `Sprint.start`, `*.delete` and all bulk operations preview.
The audit log records previews and real changes separately.
## Bulk operations
Jira Data Center exposes no REST API for bulk edits, so `Bulk.*` means N
individual requests. Three things follow from that, and the engine handles all
three:
**Blast radius.** Issues are selected by explicit keys or by JQL. A JQL matching
more than `max_issues` (default 50, ceiling 200) is **refused outright** rather
than truncated — a half-applied bulk change is worse than none.
**Partial failure is normal.** Ten issues succeed, one is in a status the
transition does not allow, one is in a project the account cannot write to. Every
result reports `succeeded_keys` and a `failed` list with per-issue reasons, and
`next_actions` hands back the exact retry call:
```
Bulk.transition: 8 succeeded, 2 failed (OPS-14, OPS-22).
→ To retry only those, call Bulk.transition again with
issue_keys=['OPS-14', 'OPS-22']
```
Set `stop_on_error=true` when the issues must all change together — the run then
goes sequentially and halts at the first failure, leaving the rest untouched.
**Notifications.** Single-issue edits notify watchers. Bulk edits default to
`notify_users=false`: relabelling 80 issues should not send 80 emails.
`Bulk.comment` is the exception — Jira always notifies on a new comment, and the
preview says so.
Requests run five at a time: fast enough for 100 issues, polite enough to stay
clear of Jira's rate limiter.
## Reports and the semantic layer
The report tools answer questions people ask — *"is the sprint going to make
it?"* — rather than questions the API answers. Three ideas make that work.
**Custom fields are discovered, not configured.** "Story Points" is
`customfield_10004` on one Jira and `customfield_10026` on another. The resolver
prefers the board's own estimation field — the field the team's velocity chart
already uses, so the report agrees with what they see in Jira — and falls back to
matching the field catalogue by name.
**Missing data is reported, not zeroed.** If no estimation field exists,
points-based sections say "unavailable" instead of showing `0.0`, which would
read as *the team completed nothing*. `Report.velocity` refuses outright and
points you at the count-based alternative.
**Every number carries its definition.** "Velocity" and "blocked" mean different
things at different companies, so each response includes a `methodology` block,
and `Report.explain_metrics` returns the full rule and its caveat — in English or
Persian:
```
You: ولاسیتی رو چطور حساب میکنی؟
Claude: [jira_report_explain_metrics term="velocity"]
Computed as: for each closed sprint, the sum of the estimation field on
issues whose status category is 'done' at the time of reading...
Caveat: counts points as they stand now, not as at sprint close.
```
Jira has no native "blocked" concept, so `Report.blocked_issues` checks three
signals — the Flagged field, the status name, then labels — and always reports
which one matched. A team that records blockers only in comments will
legitimately show zero, and the response says so rather than implying nothing is
stuck.
Each report documents its API cost in its description, since several make one
request per issue.
## eazyBI
**What eazyBI actually offers.** It publishes exactly one supported REST
endpoint — running a saved report. Its support team has
[stated publicly](https://community.eazybi.com/t/rest-apis-for-easybi-reports-or-dashboards/11655)
that there is no REST API for creating or publishing reports and dashboards, on
any deployment. The tools here are shaped around that reality rather than
around what would be convenient.
| Layer | Tools | Reliability |
|---|---|---|
| **Stable** | `export_report`, `get_export_url` | The [documented endpoint](https://docs.eazybi.com/eazybi/set-up-and-administer/customization/report-results-export-api) |
| **Experimental** | `list_accounts`, `list_reports`, `get_report_definition`, `list_dashboards`, `get_dashboard`, `list_cubes`, `list_dimensions`, `list_measures` | Internal UI routes — usually work on Data Center, degrade to `supported: false` where they do not |
| **Local** | `generate_report_definition`, `explain_report` | No eazyBI call at all |
**Start with `EazyBI.health`.** It probes what works on your installation and
reports it: whether the add-on responds at `{jira}/plugins/servlet/eazybi`,
which credential it accepts, whether the metadata routes are open, and — the
part you need before anything else — what account IDs exist.
**Credentials are reused, not duplicated.** The add-on sits behind Jira's own
auth, so the Jira PAT is tried first. eazyBI's docs specify Basic auth, so a PAT
may be rejected; the client then falls back to separate credentials if you have
configured them, and remembers which one worked. Nothing to set up in the happy
case.
**Creating reports is a two-step process, honestly.** Since eazyBI has no API
for it, `generate_report_definition` designs the report and produces the JSON
document you paste into eazyBI's *Import report definition* dialog. Five
templates cover the common shapes. Member names are instance-specific, so
reading a similar report first with `get_report_definition` turns a plausible
definition into a valid one.
**On 401/403 from an internal route**, the error says what it cannot know: the
route may be closed to API callers, or the credentials may be wrong, and the
response cannot distinguish them. It then names the experiment that resolves it
— run `export_report` with a known ID.
### Context management
Jira responses are large — a single issue can be 30 KB of JSON. Two mechanisms
keep results usable:
- **`detail`** — `compact` (default) returns the triage fields; `full` adds
description, components, links and time tracking.
- **Page size caps** — `max_results` is capped at 100 with a default of 25, and
the requested fields are trimmed at the Jira end rather than after the fact.
## Prompts and skills
**Prompts** appear in the client's menu as ready-made requests:
`sprint_report`, `executive_report`, `risk_report`, `release_report`,
`workload_report`, `team_performance`.
**Skills** ([`skills/`](skills/)) are multi-step workflows with decision points —
the difference being that a prompt has a known answer shape, while a skill's
next step depends on what the last one found:
| Skill | Writes? |
|---|---|
| `sprint-review` — classify unfinished work, propose decisions | Only if asked |
| `weekly-status` — a status update for a manager | No |
| `triage-backlog` — fill in missing assignee, priority, estimate | Behind a preview |
| `stale-work-cleanup` — find and clear work that stopped moving | Behind a preview |
| `release-checklist` — readiness, notes, version closeout | Behind a preview |
| `onboard-project` — map an unfamiliar project's conventions | No |
## Deployment
```bash
sudo ./deploy/install.sh # systemd service, hardened, loopback-bound
```
Then TLS via [`deploy/nginx.conf`](deploy/nginx.conf). Full instructions in the
[Deployment Guide](docs/Deployment-Guide.md).
## Development
```bash
uv run pytest # 579 tests, mocked Jira, offline
uv run pytest --live # also run tests marked 'live'
uv run python tests/integration/acceptance.py # every capability over real MCP
uv run ruff check . # lint
uv run mypy src # type check
uv run jira-mcp --generate-docs # regenerate the tool reference
```
## Security notes
- Secrets live **only** in environment variables; `instances.yaml` stores variable *names*.
- HTTP transport binds to `127.0.0.1` by default. Set `JIRA_MCP_HTTP_TOKEN` and put a
TLS-terminating reverse proxy in front of it before exposing it to a network.
- Use a dedicated Jira service account with the minimum permissions it needs — not an admin.
- `logs/` and `.env` are git-ignored. Never commit them.
## Documentation
| Document | Language |
|---|---|
| [Architecture](docs/Architecture.md) | English |
| [Configuration Guide](docs/Configuration-Guide.md) | English |
| [Deployment Guide](docs/Deployment-Guide.md) | English |
| [Tool Reference](docs/Tool-Reference.md) | English (generated) |
| [Skills](skills/README.md) | English |
| [SECURITY](SECURITY.md) — threat model and credential handling | English |
| [CONTRIBUTING](CONTRIBUTING.md) | English |
| [CHANGELOG](CHANGELOG.md) | English |
| [README فارسی](README.fa.md) | فارسی |
| [راهنمای نصب و راهاندازی](docs/fa/راهنمای-نصب.md) | فارسی |
| [راهنمای پیکربندی](docs/fa/راهنمای-پیکربندی.md) | فارسی |
| [راهنمای استفاده](docs/fa/راهنمای-استفاده.md) | فارسی |
## Acknowledgements
Design informed by three prior projects:
[`ultimate-guitar/eazybi-mcp`](https://github.com/ultimate-guitar/eazybi-mcp),
whose research into eazyBI's API surface shaped this project's eazyBI domain
entirely;
[`atlassian/atlassian-mcp-server`](https://github.com/atlassian/atlassian-mcp-server),
whose practice of shipping skills alongside an MCP server is adopted here; and
[`edrich13/mcp-jira-server`](https://github.com/edrich13/mcp-jira-server).
## License
MIT
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues