jira-mcp
by skurekjakub
README.md
# jira-mcp
A [Model Context Protocol](https://modelcontextprotocol.io) server for Jira
Cloud. Create issues and subtasks, read issues, search with JQL, walk agile
boards and their swimlanes, move issues through their workflow, comment, and
attach files.
Authenticates with an Atlassian account email and an API token, so it runs
headless — no OAuth, no browser round-trip, no interactive consent.
## Install
Nothing to install. Point your MCP client at the package and let `npx` fetch it:
```json
{
"mcpServers": {
"jira": {
"type": "stdio",
"command": "npx",
"args": ["-y", "github:skurekjakub/jira-mcp#v1.0.0"],
"env": {
"NPM_CONFIG_ALLOW_GIT": "root",
"JIRA_SITE": "your-team.atlassian.net",
"JIRA_CLOUD_ID": "00000000-0000-0000-0000-000000000000",
"JIRA_PROJECT_KEY": "ABC",
"JIRA_EMAIL": "${JIRA_EMAIL}",
"JIRA_API_TOKEN": "${JIRA_API_TOKEN}"
}
}
}
}
```
The build artifact is committed and the runtime dependencies are inlined into
it, so installing clones and links — no build step, no packages fetched, and no
install script to approve.
### `NPM_CONFIG_ALLOW_GIT`
[npm 12 defaults `allow-git` to `none`](https://github.com/npm/cli/commit/caa329568d32587e53f6e098f43b550dd2685034),
so without this npm refuses the install with `EALLOWGIT` — *"Fetching packages
of type git have been disabled"*. Setting it in the server's `env` block scopes
the exemption to this one subprocess, which leaves git fetching disabled for
every other install on the machine. `root` permits it for the package being
installed but not for anything it might depend on; `all` lifts the restriction
entirely and is not needed here. On npm 11 and older the variable is ignored.
### Installing once instead
`npx` re-resolves the git ref on every start, which costs a few seconds and a
network round-trip. To avoid both:
```bash
npm install -g --allow-git=root github:skurekjakub/jira-mcp
```
then use `"command": "jira-mcp"` with no `args` and no `NPM_CONFIG_ALLOW_GIT`.
Upgrading is then a manual re-run of that command.
## Configuration
Every setting comes from the environment. There is no config file, and the
server reads no `.env` — an MCP client passes these in the `env` block above.
| Variable | Required | Description |
| --- | --- | --- |
| `JIRA_SITE` | yes | Site host, e.g. `your-team.atlassian.net`. A scheme and trailing slashes are accepted and stripped |
| `JIRA_CLOUD_ID` | yes | Tenant id — see below |
| `JIRA_PROJECT_KEY` | yes | Project `jira_create_issue` and `jira_create_subtask` write into |
| `JIRA_EMAIL` | yes | Atlassian account email the API token belongs to |
| `JIRA_API_TOKEN` | yes | API token, from [Atlassian account settings](https://id.atlassian.com/manage-profile/security/api-tokens) |
| `JIRA_ATTACHMENTS_DIR` | no | Directory `jira_add_attachment` reads uploads from. Defaults to `/tmp/mcp-attachments` |
| `JIRA_ISSUE_KEY` | no | Pins `jira_add_comment` and `jira_add_attachment` to one issue and drops `issueKey` from their inputs |
Find your cloud id with:
```bash
curl -s https://your-team.atlassian.net/_edge/tenant_info
```
### Keeping the token out of version control
A committed MCP config must not contain the token itself. Claude Code
[expands `${VAR}` references](https://code.claude.com/docs/en/mcp) in `command`,
`args`, `env`, `url`, and `headers`, so the config names a variable and the
value lives elsewhere — a shell profile, or a settings file that is not
committed.
A reference that nothing expands is **not** an error: Claude Code passes the
literal text `${JIRA_API_TOKEN}` through to the server. This server rejects a
value of that shape at startup and names the variable, rather than sending it
to Jira and returning an opaque 401.
## Tools
### `jira_create_issue`
Create an issue in `JIRA_PROJECT_KEY`.
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `summary` | string | yes | Issue title |
| `description` | string \| ADF | yes | Rich text — see [Rich-text bodies](#rich-text-bodies) |
| `issueType` | string | no | e.g. `Task`, `Bug`, `Story` (default `Story`) |
| `labels` | string[] | no | Optional labels |
| `parentKey` | string | no | Link under a parent, e.g. an epic — valid only if the project's issue-type hierarchy allows it |
### `jira_create_subtask`
Create a subtask under an existing issue.
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `parentKey` | string | yes | Parent issue key, e.g. `ABC-3200` |
| `summary` | string | yes | Subtask title |
| `description` | string \| ADF | no | Rich text — see [Rich-text bodies](#rich-text-bodies) |
| `labels` | string[] | no | Optional labels |
### `jira_update_issue`
Edit fields on an existing issue. Only the fields provided change; at least
one is required. Setting `parentKey` re-parents the issue — the way to move a
story from one epic to another.
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `issueKey` | string | yes | Issue key, e.g. `ABC-3316` |
| `summary` | string | no | New summary line |
| `description` | string \| ADF | no | Rich text — see [Rich-text bodies](#rich-text-bodies) |
| `labels` | string[] | no | Replaces the whole label set |
| `parentKey` | string | no | New parent, e.g. the target epic — valid only if the project's issue-type hierarchy allows it |
### Rich-text bodies
`description` on the three tools above takes either shape:
- **A markdown-subset string.** `#`–`######` headings, `-`/`*` bullets,
blank-line paragraphs, inline `` `code` `` and `**bold**`. Anything else —
links, tables, code blocks, nested lists — is flattened to plain text, and
Jira Cloud does not auto-link issue keys or URLs in REST-created text.
- **An Atlassian Document Format document**, `{"type":"doc","version":1,"content":[…]}`,
sent to Jira verbatim. Use it when the body needs a `link` mark, an
`inlineCard`, a `codeBlock`, a `table`, or a nested list. Only the envelope
is validated; Jira reports a malformed node in its 400 response, which the
tool returns as the error result.
### `jira_get_issue`
Fetch an issue's summary, description, type, status, parent, subtasks, issue
links, and fix versions.
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `issueKey` | string | yes | Issue key, e.g. `ABC-3316` |
| `extraFields` | string[] | no | Additional field ids (e.g. `customfield_15000`) returned raw under `extra`, keyed by field id |
### `jira_search_issues`
Search with JQL, paginated.
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `jql` | string | yes | JQL query |
| `maxResults` | number | no | Page size, 1–100 (default 50) |
| `nextPageToken` | string | no | Token from a previous page |
| `includeDescription` | boolean | no | Include each issue's description, flattened to text |
| `extraFields` | string[] | no | Additional field ids returned raw under `extra` |
### `jira_get_board`
Report an agile board's layout — columns, quick filters, and swimlanes, each
with its JQL. Run it before `jira_board_issues` to learn the names that tool
accepts. The board id is the `boards/<id>` segment of a board URL.
Swimlanes come from GreenHopper's internal API; Atlassian publishes no
supported endpoint for them.
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `boardId` | number | yes | Numeric board id, e.g. `1150` |
### `jira_board_issues`
List the issues currently on a board, grouped by swimlane. Reproduces the board
view: the board's own filter, its kanban sub-filter, any quick filters named,
and extra JQL.
Each issue lands in exactly one swimlane — the first, in board order, whose
query matches it. Requesting a subset of swimlanes still evaluates the lanes
above them, so their claim on an issue holds.
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `boardId` | number | yes | Numeric board id |
| `swimlanes` | string[] | no | Swimlane names or ids to return (case-insensitive). Omit for every swimlane |
| `quickFilters` | string[] | no | Quick-filter names or ids, ANDed together |
| `jql` | string | no | Extra JQL ANDed onto the board filter |
| `applySubQuery` | boolean | no | Apply the board's kanban sub-filter (default `true`) |
| `maxIssuesPerSwimlane` | number | no | Per-swimlane cap, 1–1000 (default 200). Each lane reports `matched` and `truncated` |
| `keysOnly` | boolean | no | Return only issue keys per swimlane (default `false`). Rejected together with `extraFields` |
| `extraFields` | string[] | no | Additional field ids returned raw per issue under `extra` |
### `jira_list_transitions`
List the workflow transitions currently available on an issue. Transition names
and ids are workflow-specific — call this before `jira_transition_issue`.
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `issueKey` | string | yes | Issue key |
### `jira_transition_issue`
Move an issue through its workflow. Matches `transitionName` case-insensitively
against the issue's available transitions; fails with the valid list if nothing
matches.
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `issueKey` | string | yes | Issue key |
| `transitionName` | string | yes | Target transition name, e.g. `Done` (case-insensitive) |
| `resolution` | string | no | Resolution to set — only needed if the transition's screen requires one |
| `comment` | string | no | Comment added as part of the transition |
### `jira_add_comment`
Add a comment. The body uses Jira wiki markup (`h3.` for headings, `{{code}}`
for inline code, `{code:lang}…{code}` for blocks, `bq.` for blockquotes). Use
real newlines, not literal `\n` escapes.
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `body` | string | yes | Comment body in Jira wiki markup |
| `issueKey` | string | conditional | Hidden when `JIRA_ISSUE_KEY` is set |
### `jira_add_attachment`
Attach a file. The file must sit in `JIRA_ATTACHMENTS_DIR`; a name escaping that
directory is refused.
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `fileName` | string | yes | Name of a file in the attachments directory |
| `issueKey` | string | conditional | Hidden when `JIRA_ISSUE_KEY` is set |
## Transports
stdio by default. `--transport http --port <n>` serves the same tools over a
stateless Streamable HTTP transport at `/mcp`, with a health check at `/health`.
## Development
```bash
npm install
npm test # vitest
npm run lint # tsc --noEmit
npm run build # webpack -> dist/bundle.js
```
`dist/bundle.js` is committed so installs need no build step. CI rebuilds it on
every push to `main` and commits the result, so it cannot drift from `src/`.
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues