@tugudush/jira-mcp
v1.1.1
Published
A focused, Jira-only Model Context Protocol server with bitbucket-mcp-grade developer ergonomics. Works today — no OAuth admin gating required.
Maintainers
Readme
Jira MCP Server
A focused, Jira-only Model Context Protocol (MCP) server with bitbucket-mcp-grade developer ergonomics. Talks to the Jira Cloud REST API v3 directly using an Atlassian email + API token — no OAuth dance, no admin gating, works today.
🎯 36 tools (35 read + 1 scoped issue text update) · ✅ Read-only by default · 🏗️ Modern TS 6 + ESM · 📦 TOON / JSON / text output formats
Status: ✅ v1.0.0 released — see CHANGELOG.md and docs/plan.md for the full roadmap and competitors.md for the research that informed it.
Why?
The official Atlassian MCP (Rovo) requires twg:* OAuth scopes that need tenant-admin approval — and the legacy SSE method is being deprecated on June 30, 2026. This server is a drop-in alternative: same MCP UX, none of the gating. Talk to Jira directly with email + API token.
Requirements
- Node.js ≥ 20.0.0 (see
.nvmrc) - A Jira Cloud site (e.g.
https://your-domain.atlassian.net) - An API token is required. Create a classic (unscoped) API token at https://id.atlassian.com/manage-profile/security/api-tokens:
- Important: Click "Create API token" (do NOT use "Create API token with scopes", as scoped tokens can fail to authorize various Jira platform endpoints correctly).
- Make sure your Atlassian account has active read permissions for the target Jira project. The optional issue text update also requires Jira edit permission, and this server still refuses the update unless the authenticated account is the issue reporter or assignee.
Installation
Option 1 — npm global install (recommended)
npm install -g @tugudush/jira-mcpUpdate:
npm update -g @tugudush/jira-mcpOption 2 — npx (no global install)
npx -y @tugudush/jira-mcpOption 3 — build from source
git clone https://github.com/tugudush/jira-mcp.git
cd jira-mcp
npm install
npm run build
node dist/index.jsConfiguration
VS Code / GitHub Copilot — .vscode/mcp.json
After global install:
{
"servers": {
"jira-mcp": {
"type": "stdio",
"command": "jira-mcp",
"env": {
"JIRA_BASE_URL": "https://your-domain.atlassian.net",
"JIRA_EMAIL": "[email protected]",
"JIRA_API_TOKEN": "your-api-token",
},
},
},
}Or via npx (no global install):
{
"mcpServers": {
"jira-mcp": {
"command": "npx",
"args": ["-y", "@tugudush/jira-mcp"],
"env": {
"JIRA_BASE_URL": "https://your-domain.atlassian.net",
"JIRA_EMAIL": "[email protected]",
"JIRA_API_TOKEN": "your-api-token",
},
},
},
}Or from a local build (after npm install && npm run build):
{
"servers": {
"jira-mcp": {
"type": "stdio",
"command": "node",
"args": ["/path/to/jira-mcp/dist/index.js"],
"env": {
"JIRA_BASE_URL": "https://your-domain.atlassian.net",
"JIRA_EMAIL": "[email protected]",
"JIRA_API_TOKEN": "your-api-token",
},
},
},
}Tip: paste the actual values in the
envblock. If you don't want to commit secrets, keep.vscode/mcp.jsonin your global gitignore.
Environment Variables
| Variable | Required | Default | Description |
| -------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| JIRA_BASE_URL | Yes | — | Your Jira Cloud site URL (no trailing slash) |
| JIRA_EMAIL | Yes | — | Atlassian account email |
| JIRA_API_TOKEN | Yes | — | API token from https://id.atlassian.com/manage-profile/security/api-tokens |
| JIRA_ALLOW_ISSUE_UPDATES | No | false | Set to true to enable only jira_update_issue_text; updates still require reporter/assignee identity match |
| JIRA_DEBUG | No | false | Verbose stderr logging (URLs + status codes only — no bodies, no headers) |
| JIRA_DEFAULT_FORMAT | No | text | Default output format: text / json / toon |
| JIRA_REQUEST_TIMEOUT_MS | No | 30000 | Per-request timeout in milliseconds |
No secrets are ever logged. JIRA_DEBUG=true only logs URLs and status codes.
Usage Examples
Once jira-mcp is wired into your MCP client (Copilot, Cursor, Claude Desktop,
etc.), the model can call any of the 36 tools. A few typical invocations:
Search issues by JQL
Tool: jira_search_issues
Args:
jql: "project = ENG AND status != Done ORDER BY updated DESC"
maxResults: 10
output_format: toonUse the new
/rest/api/3/search/jqlendpoint (POST). The older/rest/api/3/searchis deprecated and not exposed by this server.
Fetch one issue
Tool: jira_get_issue
Args:
issueIdOrKey: "ENG-1234"
fields: ["summary", "status", "assignee", "reporter", "priority", "updated"]
output_format: textExtract image attachments from an issue
Tool: jira_get_issue
Args:
issueIdOrKey: "ENG-1234"
include_attachments: true
output_format: textWhen include_attachments: true, jira_get_issue auto-requests the
attachment field, downloads every image attachment (PNG/JPEG/GIF/WebP),
and returns them as MCP image content blocks alongside the text summary.
Defaults to false (no download, no context cost). See
Image Attachments for limits and caveats.
List agile boards and their sprints
Tool: jira_list_boards
Args:
projectKeyOrId: "ENG"
output_format: textTool: jira_get_board_sprints
Args:
boardId: 42
state: "active"Filter a response with JMESPath
Every tool accepts a filter parameter (JMESPath expression) to project the
response down to just the fields you need. Example:
Tool: jira_search_issues
Args:
jql: "project = ENG ORDER BY updated DESC"
maxResults: 20
filter: "issues[].{key: key, summary: fields.summary, status: fields.status.name}"
output_format: jsonSee jmespath.org for the full expression syntax.
Enabling Writes — jira_update_issue_text
Opt-in. Off by default. The 35 read tools are always available. This single scoped write tool requires two safeguards:
JIRA_ALLOW_ISSUE_UPDATES=truein the MCPenvblock, and- The authenticated Atlassian account is the reporter or assignee of the target issue.
What it can change
Only an issue's title (summary) and/or plain-text description. It
cannot change status, assignee, priority, labels, comments, worklogs,
sprints, links, or any other field. The handler converts your plain text
description to Atlassian Document Format (ADF) and PUTs to
/rest/api/3/issue/{key}.
Step 1 — set the env var
In .vscode/mcp.json (or the equivalent in your MCP client):
{
"servers": {
"jira-mcp": {
"type": "stdio",
"command": "jira-mcp",
"env": {
"JIRA_BASE_URL": "https://your-domain.atlassian.net",
"JIRA_EMAIL": "[email protected]",
"JIRA_API_TOKEN": "your-api-token",
"JIRA_ALLOW_ISSUE_UPDATES": "true",
},
},
},
}Step 2 — confirm the identity match
Open the target Jira issue and check the Reporter and Assignee fields.
The email in JIRA_EMAIL must match one of them. The server reads
/rest/api/3/myself first and compares the account IDs.
Step 3 — call the tool
Tool: jira_update_issue_text
Args:
issueIdOrKey: "ENG-1234"
title: "Refactor onboarding wizard to async stepper"
description: "Step 1: profile, Step 2: workspace, Step 3: invite teammates."
output_format: textWhat you see when it works
Updated ENG-1234 successfully.What you see when it doesn't
| Condition | Tool response |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| JIRA_ALLOW_ISSUE_UPDATES not set / "false" | IssueUpdateDisabledError: Issue text updates are disabled. To enable, set JIRA_ALLOW_ISSUE_UPDATES=true. |
| Account is neither reporter nor assignee | IssueUpdatePermissionError: Only the issue reporter or assignee may update the title/description. |
| Both title and description are omitted | Zod validation: Either title or description must be provided. |
| Jira returns 401 / 403 | AuthenticationError or ForbiddenError — check token + email |
| Jira returns 429 | RateLimitError — server retries with exponential backoff |
Caveats
- Token scope. Use a classic (unscoped) API token. Scoped tokens created via "Create API token with scopes" can fail to authorize various Jira endpoints correctly — including the issue update endpoint.
- Plain text only. The
descriptionargument is plain text. Multi-line strings are accepted; markdown is not converted. If you need rich formatting, file an issue and we'll add anadf_jsonargument in v1.1. - No idempotency token. A second call with the same args immediately after
the first will succeed again and bump the
updatedtimestamp. Jira Cloud does not supportIf-Matchon this endpoint.
Image Attachments
Opt-in. Off by default. Setting
include_attachments: trueonjira_get_issuecauses the server to download every image attachment on the issue and return them as MCPimagecontent blocks alongside the regular text summary. Image-capable MCP clients (Copilot, Claude Desktop, Cursor, etc.) will render them inline so the model can actually see the mockup, screenshot, or error capture.
What counts as "an image"
Only the following MIME types are downloaded:
image/pngimage/jpegimage/gifimage/webp
PDFs, zips, logs, Office docs, etc. are listed in the text output but not fetched — the model has no way to consume them as images.
Safety limits
| Limit | Default | What happens when exceeded |
| ---------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Attachments per issue | 10 | additional ones are skipped with skipped: per-issue attachment cap reached |
| Per-attachment size | 2 MB | oversized attachments are skipped with skipped: too large (per-attachment cap) |
| Total extracted per call | 5 MB | once the running total crosses the budget, attachments are skipped with skipped: would exceed total extraction budget |
| Download failure | (per attachment) | the failure is recorded as skipped: download failed and the rest of the batch continues |
Limits are per call (each jira_get_issue invocation starts fresh). The
default of include_attachments: false performs zero downloads and emits zero
image blocks — behavior is unchanged for callers who don't opt in.
Output format gates
Image content blocks are only emitted when both of the following are true:
output_formatistext(the default), and- no JMESPath
filteris supplied.
json and toon are structured-data modes; embedding base64 would balloon
the response. A JMESPath filter means the consumer asked for a projected
slice of the response — images have no data-side representation, so they
are skipped.
Example output (text mode, two PNGs)
ENG-1234 — Add onboarding wizard stepper
- Status: In Progress
- Assignee: Jerome Gomez
- …
- Attachments (2):
- mockup-step-1.png (148 KB, image/png)
- mockup-step-2.png (164 KB, image/png)
[Extracted 2 image(s) into the response]
— ATTACHMENT EXTRACTION REPORT —
- mockup-step-1.png (image/png) — extracted
- mockup-step-2.png (image/png) — extractedThe two PNGs are returned as MCP image content blocks (one block each),
which the MCP client renders inline.
Skipped attachments are never silent
Every attachment on the issue gets a line in the text report — either
extracted or one of the skip reasons listed above. You can always tell
exactly which attachments made it into the response and why the others
didn't.
Caveats
- Read-only. The download is a GET; this server still has no write surface for uploads.
- Not cached. Each call re-downloads attachments. Repeated calls on the same issue will refetch the same images — see the open question in docs/features/image-extraction.md §5 for the in-memory LRU idea (out of scope for v1.1).
- Base64 inflation. Binary → base64 adds ~33%. A 2 MB PNG becomes
~2.67 MB of string data in the LLM context. If your context is tight,
call
jira_get_issueonce withinclude_attachments: falsefirst to read the description, then again withinclude_attachments: trueonly when the model actually needs to see the images. - Not available on
jira_search_jql— onlyjira_get_issueaccepts the flag. Extracting images for every issue in a JQL result would blow the context window for any non-trivial search.
Every tool accepts two optional parameters:
| Parameter | Values | Effect |
| --------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| output_format | text / json / toon | Default text (human-readable markdown); json is pretty-printed; toon saves 30–60% of tokens for lists of > 5 items |
| filter | JMESPath expression | Projects the response down to just the fields you need |
Set JIRA_DEFAULT_FORMAT=toon in env to make TOON the default for every
tool. The model can still override per-call with output_format=....
Project-level AGENTS.md defaults
Want the model to stop rediscovering your project key on every session? Drop
docs/AGENTS.md.example into your repo's AGENTS.md
and edit the YOURPROJ placeholder. See
docs/agents-guide.md for the full how-to. Pattern lifted
from the official atlassian/atlassian-mcp-server (UX4).
Features
- Issue search with JQL via the new
/search/jqlendpoint (POST) - Issue detail — transitions, changelog, comments, worklogs, watchers, links
- Opt-in image attachment extraction on
jira_get_issueviainclude_attachments: true— downloads PNG/JPEG/GIF/WebP attachments and returns them as MCPimagecontent blocks (10 / 2 MB / 5 MB defaults) - Project listing, components, versions, statuses
- Agile boards, sprints, backlog issues
- Users (current user, search, assignable)
- Saved filters, dashboards, workflows, fields + issue types
- Opt-in scoped issue text update behind
JIRA_ALLOW_ISSUE_UPDATES=true: update only issue title (summary) and description, only as reporter or assignee - TOON output format (30–60% token savings) with
text/jsonfallback - JMESPath
filterparameter on every tool - Response truncation with raw file logging when payloads exceed ~10k tokens (aashari pattern)
- Project-level
AGENTS.mddefaults so the model doesn't re-discover your project every session (atlassian/Rovo pattern) - Per-tool
descriptionstrings teach usage — embed deprecation notes, parameter hints, and gotchas inline (aashari pattern) - Retry with exponential backoff on transient errors (
429/5xx/ network cuts) - Configurable per-request timeout via
JIRA_REQUEST_TIMEOUT_MS(default 30 s)
Development
See docs/plan.md for the full phased implementation plan and CHANGELOG.md for the release history.
git clone https://github.com/tugudush/jira-mcp.git
cd jira-mcp
npm install
npm run dev # tsx watch src/index.tsFor workspace-level testing inside VS Code/GitHub Copilot, a local configuration is available at .vscode/mcp.json (gitignored). You can configure your credentials there to load either:
jira-mcp-dev— Runs directly from TS source viatsx(convenient for active development).jira-mcp-dist— Runs the compiled production code from thedist/directory.
You can export the jira-mcp-dist server's env block to your shell with:
node scripts/dump-mcp-env.cjs > .mcp-env.sh && source .mcp-env.shScripts
| Script | What it does |
| ----------------------- | -------------------------------------------------------------------------------------- |
| npm run dev | Watch mode via tsx |
| npm run build | prebuild (scripts/sync-version.ts) → tsc → dist/ |
| npm run type-check | tsc --noEmit |
| npm run lint | ESLint (flat config + sonarjs), max-warnings 0 |
| npm run format | Prettier write |
| npm run format:check | Prettier check (no modifications, exits non-zero on diff) |
| npm test | Vitest (single run, 96+ tests) |
| npm run test:watch | Vitest watch mode |
| npm run test:coverage | Vitest with v8 coverage |
| npm run ltfb | lint → type-check → format → build |
| npm run phase5:smoke | Run the stdio smoke driver against a sandbox tenant |
| npm run image:smoke | Manual image-extraction smoke against SSP-2313 (or any issue with image attachments) |
| npm start | Boot the built server (node dist/index.js) |
No automated CI is configured for this repo. The maintainer runs
npm run ltfb && npm testlocally on Node 20 and Node 22 before merging each PR. If you fork the repo and want CI, see the GitHub Actions Node.js workflow template.
Troubleshooting
"Authentication failed" / 401 errors
- Check that
JIRA_EMAILmatches the email on the Atlassian account that created the API token. - Check that
JIRA_API_TOKENis a classic (unscoped) token from https://id.atlassian.com/manage-profile/security/api-tokens. Scoped tokens created via the newer "Create API token with scopes" flow can fail authorization on various Jira platform endpoints. - Make sure the token has not been revoked.
"Forbidden" / 403 errors
- The Atlassian account must have read permission to the Jira project being queried. Ask your Jira admin to grant access, or use a different account.
"Not Found" / 404 errors
- Verify
JIRA_BASE_URLmatches your Jira Cloud site URL exactly (no trailing slash,https://, nothttp://). - Verify the issue key exists (
PROJ-1234format — must be uppercase project key + dash + number).
"Issue text updates are disabled"
- Set
JIRA_ALLOW_ISSUE_UPDATES=truein the MCPenvblock and restart the MCP client. See Enabling Writes.
"Only the issue reporter or assignee may update the title/description"
- The Atlassian account you're authenticated as is neither the reporter nor
the assignee on the target issue. Either:
- have the reporter/assignee make the change, or
- have a Jira admin change the issue's reporter/assignee to you, or
- use a different Atlassian account whose email matches the reporter or assignee on the issue.
Tool never returns / times out
- Default per-request timeout is 30 s (override with
JIRA_REQUEST_TIMEOUT_MS). - Set
JIRA_DEBUG=trueto see URL + status code lines on stderr. - Jira Cloud's rate limit is ~10 req/s per tenant. The server retries with exponential backoff on 429/5xx. If you're seeing frequent timeouts, slow down the model's request rate.
"Cannot find module '@modelcontextprotocol/sdk/...'" after install
- Run
npm cito do a clean install frompackage-lock.json. If the issue persists, ensure your Node version is ≥ 20 (node --version).
Server shows Starting → Running → Stopped with no output (global install / npx)
You started the server via the global bin ("command": "jira-mcp") or
npx, and the VS Code MCP panel goes Starting → Running → Stopped —
with no [server stderr] lines in the Output channel. The
initialize handshake either times out or the process exits immediately.
This affected @tugudush/[email protected] only: an entry-point guard in
src/index.ts only started the server when process.argv[1] string-matched
the module path, which fails when Node resolves a symlinked/junctioned
install (nvm-for-windows, the npx cache, Homebrew, Linux /usr/local/bin/…,
etc.). The same code runs fine when invoked directly from a non-symlinked
source build.
Upgrade to
≥ 1.0.1— the entry-point guard was rewritten to userealpathSync()on both sides, which is symlink- and slash-agnostic. After upgrading, both"command": "jira-mcp"and thenpxconfig work on every platform.Immediate workaround without upgrading: run the server from a local source build (a real path, NOT through any symlink/junction). Clone, build, and point
.vscode/mcp.jsonat the local entry:{ "servers": { "jira-mcp": { "type": "stdio", "command": "node", "args": ["C:/path/to/cloned/jira-mcp/dist/index.js"], "env": { "JIRA_BASE_URL": "…", "JIRA_EMAIL": "…", "JIRA_API_TOKEN": "…", }, }, }, }See
docs/bugs/unable-to-start-via-global-npm/findings.mdfor the full investigation.
Local development in this repo
- The
phase5:smokescript runs an end-to-end round-trip (jira_get_current_user→jira_get_issue→jira_update_issue_text→jira_get_issue) against the builtdist/index.js. It needs a Jira Cloud sandbox tenant. SetJIRA_ALLOW_ISSUE_UPDATES=trueand a test issue key (defaultKAN-1) in your environment before running it. The script is manual — it is not part of any automated pipeline. - The
image:smokescript callsjira_get_issuewithinclude_attachments: trueagainst a real issue that has image attachments (defaultSSP-2313) and prints the resulting MCP content blocks (text + eachimageblock'smimeTypeand base64 length). Useful for verifying the Image Attachments feature against your own tenant before tagging a release. The script is also manual — it is not part of any automated pipeline.
Security
- Read-only by default. Only the scoped issue text update tool can mutate
Jira, and only after
JIRA_ALLOW_ISSUE_UPDATES=trueand an identity check. - No broad write mode. Method-locked to
PUT; path-locked to/rest/api/3/issue/{key}. Even if a bug tried to call the write helper incorrectly, the API layer rejects it. - No secrets logged.
JIRA_DEBUG=truelogs URLs + status codes only. - No startup network calls. The server is fully offline until a tool fires.
- See
docs/plan.md§11 Security Posture for the full threat model.
License
MIT — see LICENSE.
