Jira MCP Server
Production-ready Jira MCP server exposing Jira projects, issues, sprints, users, attachments, and worklogs as Model Context Protocol tools, resources, and prompts — to any MCP client (Claude Desktop, Claude Code, Cursor, VS Code) over stdio (local) or HTTP/SSE / Streamable HTTP (remote or team-shared).
┌──────────────────────────────────────────────────────────────────┐
│ MCP Client │
│ Claude Desktop / Claude Code / Cursor / VS Code / custom │
└───────────────┬───────────────────────────────────┬──────────────┘
│ stdio (JSON-RPC over stdin/stdout) │ HTTP (SSE/Streamable)
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ stdio transport │ │ HTTP transport │
│ (default) │ │ /sse /mcp /health │
└───────┬──────────┘ └──────────┬───────────┘
│ same registered server │
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ jira_mcp_server.server.create_server() │
│ 27 tools · 4 resources · 4 prompts (transport-agnostic) │
└───────────────────────────────┬──────────────────────────┘
▼
┌────────────────────────────────┐
│ JiraClient (httpx async) │
│ auth · rate-limit · retry │
└────────────────┬───────────────┘
▼
Jira REST API / Agile API (HTTPS)
Features
- Dual transports — stdio for local integration; HTTP with SSE and
Streamable HTTP for remote/team deployments, switchable via CLI flag or
MCP_TRANSPORT. - Dual Jira auth — Jira Cloud (basic: email + API token) and Jira Data Center / Server (Bearer PAT).
- Security first — HTTPS-only (
JIRA_BASE_URLrefuses plaintext), no credentials in logs (token masking), client-side token-bucket rate limiting, project whitelist enforcement, tool-level CRUD permission control (JIRA_TOOLS),confirmguard on deletion, attachment size limits, optional static Bearer auth for the HTTP transport, CORS control. - Automatic retry + pagination — 429/5xx retry with exponential backoff; list endpoints paginate automatically.
- ADF support — plain-text → Atlassian Document Format conversion for descriptions/comments; ADF → text rendering for reads.
- Type-safe — Pydantic-validated tool schemas and config.
- Docker ready —
Dockerfile+docker-compose.yml.
Requirements
- Python 3.11+ (3.12 recommended).
- A Jira instance:
- Jira Cloud — an API token (email + token for Basic auth), and the user should have at least: Browse projects, Create issues, Edit issues, Manage attachments, Manage worklogs, and (for board/sprint tools) access to the relevant boards.
- Jira Data Center / Server — a Personal Access Token, and a TLS-terminated HTTPS endpoint (the server refuses plaintext).
Token management: create tokens per environment, scope them to the least privilege you need, and rotate them regularly. Never commit
.env. If a token must be granted broad Jira permissions, pair it withJIRA_TOOLS(see Tool permissions) to restrict which of those permissions MCP clients can actually drive.
Installation
1. pip
pip install jira-mcp-server-hopcos
2. uv
uv pip install jira-mcp-server-hopcos
3. From source
git clone https://github.com/your-org/jira-mcp-server.git
cd jira-mcp-server
python -m venv .venv
source .venv/bin/activate # Linux / macOS
pip install -e ".[dev]"
On Windows, use PowerShell instead:
git clone https://github.com/your-org/jira-mcp-server.git
cd jira-mcp-server
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -e ".[dev]"
or (CMD):
git clone https://github.com/your-org/jira-mcp-server.git
cd jira-mcp-server
python -m venv .venv
.\.venv\Scripts\activate.bat
python -m pip install -e ".[dev]"
A successful editable install automatically writes the
jira-mcp-server console entry point — and on Windows the
jira-mcp-server.exe launcher — into the active environment's
Scripts directory. See
Building the jira-mcp-server.exe console script.
4. Docker (HTTP mode)
The Jira credentials are set on the server container (server-side), not on any MCP client:
docker run -p 8080:8080 \
-e JIRA_BASE_URL=https://your-domain.atlassian.net \
-e JIRA_API_TOKEN=your-token \
-e JIRA_USER_EMAIL=your@email.com \
-e JIRA_TOOLS=read,create,update \
jira-mcp-server:latest
See Docker deployment for full details.
Quick start (3 minutes)
stdio mode — Claude Desktop
Configure the server once, server-side, so the token never appears in the
client config (and is never exposed to the model). The server loads its
configuration from a fixed-name config file that sits in the same directory
as the server — so the MCP command stays a bare jira-mcp-server with no
path arguments (nothing to expose to the model) and file values never need
to enter the environment.
Option A — a jira_server.toml (or jira_server.json) next to the server
(the recommended, most discreet choice — secrets never enter the environment):
# jira_server.toml – server-side only, never commit, permissions 0600
base_url = "https://your-domain.atlassian.net"
auth_method = "basic"
user_email = "your-email@example.com"
api_token = "your-api-token" # never in env, never in client config
tools = "read,create,update"
The server auto-detects this file in its working directory on startup.
Option B — a .env KEY=VALUE file next to the server installation:
# .env – server-side configuration (never commit this file)
JIRA_BASE_URL=https://your-domain.atlassian.net
JIRA_AUTH_METHOD=basic
JIRA_USER_EMAIL=your-email@example.com
JIRA_API_TOKEN=your-api-token
JIRA_TOOLS=read,create,update
Then register the server in Claude Desktop's claude_desktop_config.json with
a bare command — no credentials, no env block:
{
"mcpServers": {
"jira": {
"command": "jira-mcp-server",
"args": ["--transport", "stdio"]
}
}
}
Restart Claude Desktop, open a conversation, and try:
"List my Jira projects." → the server calls
jira_list_projects. "Create a Bug in PROJ titled 'Login fails on Safari'." →jira_create_issue.
stdio mode — Cursor / VS Code
The same setup applies — the Jira connection lives in a server-side file
next to the server (jira_server.toml / jira_server.json / .env), and
the editor's MCP configuration references the bare command. The matching
mcpServers block for Cursor (~/.cursor/mcp.json) or the VS Code MCP panel
is identical to the Claude Desktop block above — the server auto-discovers
its own config file, never the editor config.
Why a config file in the server directory? A token set in the MCP client's
envblock (or in shell environment that the client shows) is visible to the AI model running inside the client. A config file next to the server is found by the server automatically, so the launch command has zero arguments and the secret stays out of both the environment and the client config. Because discovery uses a fixed filename, no path needs to be passed to the server either.
HTTP/SSE mode
Start the service on port 8080 (the Jira connection comes from the
server-side .env, exactly as in stdio mode):
jira-mcp-server --transport http --port 8080
# or using env vars
export MCP_TRANSPORT=http
export MCP_HOST=127.0.0.1
export MCP_PORT=8080
export JIRA_TOOLS=read,create,update
jira-mcp-server
Connect from any SSE-capable MCP client:
{
"mcpServers": {
"jira": {
"url": "http://127.0.0.1:8080/sse",
"headers": { "Authorization": "Bearer your-secret-token" }
}
}
}
Check it is up:
curl http://127.0.0.1:8080/health
# {"status":"healthy","server":"jira-mcp-server","version":"0.2.0","transport":"http","jira_configured":true}
To require a client token (recommended for anything beyond localhost), start with:
jira-mcp-server --transport http --port 8080 --auth-mode token --server-token your-secret-token
The token protects who can connect to the MCP server; the Jira credentials (
JIRA_API_TOKEN) still govern what the server can do in Jira.
Client token (HTTP auth)
In HTTP/SSE mode you can require clients to authenticate with a static
Client token before the MCP session is allowed. Think of it as the
server's "front door" password: it decides who may connect, whereas
JIRA_API_TOKEN decides what Jira operations the server may perform.
Create a client token
Any reasonably long random string works (the server compares the presented token by exact string equality). Generate one with secrets, so it cannot be guessed:
# Python is cross-platform and already available (the project requires it).
python -c "import secrets; print(secrets.token_urlsafe(32))"
# e.g. 9xKp2vY6QwE3rT8zU1iA0sD5fG7hJ4kN6lM9oP2qR5uW8
Configure the server side
Enable token auth by supplying the same client token through one of:
# Option A — CLI flag (highest precedence).
jira-mcp-server --transport http --port 8080 \
--auth-mode token --server-token 9xKp2vY6QwE3rT8zU1iA0sD5fG7hJ4kN6lM9oP2qR5uW8
# Option B — environment variables (equivalent; avoids the token in shell history).
export MCP_AUTH_MODE=token
export MCP_SERVER_TOKEN=9xKp2vY6QwE3rT8zU1iA0sD5fG7hJ4kN6lM9oP2qR5uW8
jira-mcp-server --transport http --port 8080
# Option C — docker-compose (environment passthrough, already wired).
environment:
JIRA_TOOLS: ${JIRA_TOOLS:-}
MCP_TRANSPORT: http
MCP_AUTH_MODE: token
MCP_SERVER_TOKEN: ${MCP_SERVER_TOKEN:-}
Set both MCP_AUTH_MODE=token and MCP_SERVER_TOKEN together. Note the
exact behavior when one is missing:
MCP_AUTH_MODE=tokenand noMCP_SERVER_TOKEN→ the server logs a warning at startup and serves unauthenticated (no auth middleware is attached). This is a footgun: if you intend to require a client token, set the token too.MCP_AUTH_MODE=none(or unset) → no client auth at all, regardless ofMCP_SERVER_TOKEN.
Configure the client side
The client sends the same token in an Authorization: Bearer … header:
{
"mcpServers": {
"jira": {
"url": "http://127.0.0.1:8080/sse",
"headers": { "Authorization": "Bearer 9xKp2vY6QwE3rT8zU1iA0sD5fG7hJ4kN6lM9oP2qR5uW8" }
}
}
}
Claude Code uses --header; Claude Desktop and Cursor/VS Code use the
headers map above:
claude mcp add jira --scope project \
--url http://127.0.0.1:8080/sse \
--header "Authorization: Bearer 9xKp2vY6QwE3rT8zU1iA0sD5fG7hJ4kN6lM9oP2qR5uW8"
Sanity-check both sides with curl:
curl -i http://127.0.0.1:8080/health | head -1 # 401 without a token
curl -i -H "Authorization: Bearer 9xKp2vY6QwE3rT8zU1iA0sD5fG7hJ4kN6lM9oP2qR5uW8" \
http://127.0.0.1:8080/health | head -1 # 200 with the token
Rotate the token
Generate a new token (see above), restart the server with it, and update every client config. The two-layer layout (server process holds the Jira credentials, clients only hold the client token) means rotating the client token does not require reissuing Jira tokens.
Connecting to Claude Code
Claude Code registers local MCP servers as stdio commands or remote HTTP URLs.
With the package installed (so jira-mcp-server is on PATH — on Windows
that means D:\develop\Python312\Scripts\jira-mcp-server.exe exists). If the
executable is not on your machine yet, generate it from source first
(see Building the jira-mcp-server.exe console script),
then return here. From the project root:
stdio (recommended for a single user)
Configure the Jira connection on the server process, never in the client:
the stdio command that Claude Code stores and runs must not contain
JIRA_API_TOKEN, the base URL, or even a config-file path — because Claude
Code shows the whole command to the AI.
Step 1 — put a config file next to the server (in the interpreter's
Scripts directory where jira-mcp-server.exe lives, OR where the MCP client
launches the server — both are auto-detected). The server auto-detects a
fixed-named file, so you never pass its location:
# jira_server.toml — server-side only, never commit, permissions 0600.
# Put this in the same Scripts directory as jira-mcp-server.exe (or the server's
# working directory). The server finds it automatically on startup; the MCP
# command stays a bare `jira-mcp-server`.
base_url = "https://your-domain.atlassian.net"
auth_method = "basic"
user_email = "you@email.com"
api_token = "ATATT3YOUR_REAL_TOKEN"
tools = "read,create,update"
(jira_server.json works too, or a KEY=VALUE .env file in the same
directory — see Configuration.)
Step 2 — register the server with a bare command in Claude Code:
# No --config, no --token-file, no -e/--env: the server reads jira_server.toml
# from its own directory automatically. Nothing sensitive (or even a path to
# something sensitive) is stored in .mcp.json.
claude mcp add jira --scope project \
-- jira-mcp-server --transport stdio
The server discovers
jira_server.toml(orjira_server.json/.env) in two places: the working directory it is launched from, and the interpreter'sScriptsdirectory wherejira-mcp-server.exelives. That is why "next to the server" works: no path is ever passed, so nothing to change per client, and you can drop the file next to the exe even if your MCP client starts the server from a different cwd.--token-fileis a manual fallback that mergesKEY=VALUEentries into the process environment.
Notes:
- None of the strategy above puts a secret or a path on the command line.
The MCP client stores only
jira-mcp-server --transport stdio; the token and its location are never shown to the model. - Config file values take precedence over environment variables for the fields the file sets; the environment fills any field the file leaves out.
--token-file/ env vars still work if you prefer them (see Configuration), but the file-next-to-server setup is the least exposed.- Keep credentials off the
claude mcp add …command line entirely. Do NOT pass-e/--env JIRA_API_TOKEN=…. If you must use environment variables, export them in the shell that launchesclaude(not into the MCP config) so the child process inherits them:export JIRA_BASE_URL=https://your-domain.atlassian.net export JIRA_USER_EMAIL=you@email.com export JIRA_API_TOKEN=ATATT3YOUR_REAL_TOKEN export JIRA_AUTH_METHOD=basic export JIRA_TOOLS=read,create,update claude mcp add jira --scope project \ -e JIRA_BASE_URL="$JIRA_BASE_URL" \ -e JIRA_USER_EMAIL="$JIRA_USER_EMAIL" \ -e JIRA_AUTH_METHOD=basic \ -e JIRA_TOOLS="$JIRA_TOOLS" \ -- jira-mcp-server
Exporting keeps the values out of the persisted client config, but they still cross the process boundary into the stdio child.jira_server.tomlnext to the server avoids even that.
Verify:
claude mcp list # jira: Command - ✔ Connected
claude mcp get jira # shows the resolved command; credentials NOT listed
Restart Claude Code (or run /mcp to check connection status) and start a new
session — the jira_* tools will be available.
If the server fails to start with
Jira API Token is required when JIRA_AUTH_METHOD=basic, the most likely cause is thatJIRA_API_TOKENwas not visible to the stdio child process. Fix it by pointing--token-fileat a readable file (or exporting the variables before launchingclaude) — never by adding the token to the client config.
HTTP/SSE (team-shared / multi-client)
# terminal 1: start the service (it holds the Jira credentials)
export JIRA_BASE_URL=https://your-domain.atlassian.net
export JIRA_USER_EMAIL=you@email.com
export JIRA_API_TOKEN=<token>
export JIRA_TOOLS=read,create,update
jira-mcp-server --transport http --host 127.0.0.1 --port 8080 \
--auth-mode token --server-token <client-token>
# terminal 2: register the URL in Claude Code
claude mcp add jira --scope project \
--url http://127.0.0.1:8080/sse \
--header "Authorization: Bearer <client-token>"
This gives two-layer auth: <client-token> guards who may connect to the
MCP server, JIRA_API_TOKEN governs what it can do in Jira. The
<client-token> placeholder is the shared secret you generate yourself
(see Client token (HTTP auth)) — it is not the
Jira API token.
Because the Jira credentials live in the server process (terminal 1), they
never appear in Claude Code's mcp.json/.mcp.json — an advantage of the
HTTP layout when you want to keep JIRA_API_TOKEN out of the client config.
Environment variables
All
JIRA_*variables are configured on the MCP server process — in ajira_server.toml/jira_server.jsonnext to the server (preferred), its environment, its.envfile, or a--token-filefile — never on the MCP client. In stdio mode the command the client stores and runs must not carry these values, or the AI would see the token.Precedence (highest first): auto-discovered config file next to the server (
jira_server.toml/jira_server.jsonin the working directory or in the interpreter'sScriptsdir) → env vars /.env→--token-file. For each option, only the fields it actually sets are applied, so the config file can hold just the secrets while env supplies the rest.
| Variable | Type | Required | Default | Mode | Description | Example |
|---|---|---|---|---|---|---|
JIRA_BASE_URL |
str | yes | –(unset) | all | Jira instance URL (server-side); must be https:// |
https://acme.atlassian.net |
JIRA_AUTH_METHOD |
enum | yes | basic |
all | basic (Cloud) or bearer (Data Center) |
basic |
JIRA_USER_EMAIL |
str | if basic | – | all | Account email for Cloud Basic auth | dev@acme.com |
JIRA_API_TOKEN |
str | yes | – | all | Jira API token (Cloud) or PAT (Server) | ATATT3xxxx… |
JIRA_PROJECT_KEYS |
str | no | – | all | Comma-separated allowlist; reads scoped & writes blocked outside it | ENG,SALES |
JIRA_TOOLS |
str | no | – | all | Tool permission allowlist (read/create/update/delete/write or exact tool names); empty ⇒ all |
read,create,update |
JIRA_SEARCH_ENGINE |
enum | no | jql |
all | Issue search API: jql (default) → enhanced POST /rest/api/3/search/jql; get → GET /rest/api/3/search/jql; auto → alias for jql. The old /rest/api/3/search was removed by Jira Cloud (410) |
get |
JIRA_RATE_LIMIT |
int | no | 100 |
all | Client-side requests/minute (token bucket) | 200 |
JIRA_REQUEST_TIMEOUT |
float | no | 30 |
all | Per-request timeout (s) | 30 |
JIRA_CONNECT_TIMEOUT |
float | no | 10 |
all | Connection-establishment timeout (s) | 10 |
JIRA_TOKEN_FILE |
str | no | – | both | Path to a server-side KEY=VALUE credentials file merged into the process env |
/etc/jira-mcp/secrets.env |
HTTPS_PROXY |
str | no | – | all | Outbound proxy (honored by httpx trust_env) |
https://proxy:8080 |
MCP_TRANSPORT |
enum | no | stdio |
both | stdio, http (SSE), or http-streamable |
http |
MCP_HOST |
str | no | 127.0.0.1 |
http | Bind address (use 127.0.0.1 + reverse proxy in prod) |
0.0.0.0 |
MCP_PORT |
int | no | 8080 |
http | TCP port | 8080 |
MCP_AUTH_MODE |
enum | no | none |
http | none or token |
token |
MCP_SERVER_TOKEN |
str | if token | – | http | Bearer token clients must send | change-me |
MCP_CORS_ORIGINS |
str | no | * |
http | Comma-separated allowed origins | https://app.example.com |
MCP_LOG_LEVEL |
enum | no | INFO |
all | DEBUG/INFO/WARNING/ERROR |
DEBUG |
MCP_LOG_FILE |
str | no | – | all | Log file; empty ⇒ stderr (never stdout) | /var/log/jira-mcp.log |
Server-side config file (TOML or JSON)
The recommended way to configure the Jira connection: put a fixed-name
config file next to the server — jira_server.toml (or jira_server.json)
in the interpreter's Scripts directory (the folder where jira-mcp-server.exe
lives) or in the directory the MCP client launches the server from. The server
auto-discovers it in either place, so the MCP launch command stays a bare
jira-mcp-server with no --config, no path, and no environment variables —
nothing sensitive (or even a path to something sensitive) is ever stored in or
shown to the MCP client.
The file mirrors the JIRA_* variables without the prefix and takes
precedence over them. Only the fields present are applied, so you can put just
the secrets (e.g. api_token) in the file and leave the rest to the
environment:
# jira_server.toml — server-side only, never commit, permissions 0600.
# Put it in the same directory as the server; the server auto-discovers it and
# the MCP command stays a bare `jira-mcp-server`.
base_url = "https://acme.atlassian.net"
auth_method = "basic"
user_email = "dev@acme.com"
api_token = "ATATT3YourSecretToken0001"
project_keys = "ENG,SALES"
tools = "read,create,update"
search_engine = "get" # optional; default "jql" → enhanced POST /rest/api/3/search/jql
rate_limit = 200
JSON works too (flat, or nested under a "jira" key), and tools in the file
is honored exactly like JIRA_TOOLS (it restricts which MCP tools are
exposed). search_engine mirrors JIRA_SEARCH_ENGINE. Values never enter the
environment, so they are invisible to the MCP client and its AI; the file
should be chmod 0600 and never committed.
Copy .env.example → .env (next to the server
installation) for local dev. CLI flags take precedence over environment
variables.
CLI reference
jira-mcp-server [OPTIONS]
Options:
--transport [stdio|http|http-streamable] Transport mode (default: stdio; env MCP_TRANSPORT)
--token-file TEXT Server-side credentials file (KEY=VALUE); JIRA_* entries are
merged into the process env, never into the client command (env JIRA_TOKEN_FILE)
--search-engine [jql|get|auto] Issue search API (default: jql → enhanced POST
/rest/api/3/search/jql; get → GET /rest/api/3/search/jql;
auto → alias for jql; the old /rest/api/3/search was
removed by Jira Cloud, returning 410) (env JIRA_SEARCH_ENGINE)
--host TEXT HTTP bind host (default: 127.0.0.1; env MCP_HOST)
--port INTEGER HTTP bind port (default: 8080; env MCP_PORT)
--auth-mode [none|token] HTTP client auth (default: none; env MCP_AUTH_MODE)
--server-token TEXT Bearer token for client connections (env MCP_SERVER_TOKEN)
--cors-origins TEXT CORS origins, comma-separated (default: *)
--log-level [DEBUG|INFO|WARNING|ERROR] Logging level (env MCP_LOG_LEVEL)
--log-file TEXT Optional log file (env MCP_LOG_FILE)
--version Print version and exit
--help Show help
Transports
| Characteristic | stdio | HTTP/SSE |
|---|---|---|
| Best for | single local user | remote / team-shared / CI |
| Deployment | spawned by the client | standalone service |
| Clients | Claude Desktop, Cursor, VS Code | any MCP SSE/Streamable client |
| Concurrency | one client | many clients |
| Jira credentials | server-side env / .env / --token-file |
server-side env / .env |
| Client auth | none (credentials not offered to client) | Bearer token (MCP_SERVER_TOKEN) |
| Network | local only | network reachable |
| Endpoint | stdin/stdout | /sse + /messages/ (SSE) or /mcp (streamable); /health, / |
Tools
All tools return JSON text. On failure they return
{"isError": true, "content": [{"type": "text", "text": "Jira API Error [403]: …"}]}.
Issues
| Tool | Description | Key params |
|---|---|---|
jira_create_issue |
Create an issue (auto-converts description to ADF) | project_key, summary, issue_type; optional description, priority, assignee_account_id, labels, components, custom_fields, parent_key |
jira_update_issue |
Update fields | issue_key, fields |
jira_get_issue |
Get full issue | issue_key, fields*, expand* |
jira_delete_issue |
Delete (guarded) | issue_key, confirm (must be true), delete_subtasks* |
jira_transition_issue |
Transition by id or status name | issue_key, transition_id*/target_status*, comment*, fields* |
jira_get_transitions |
List available transitions | issue_key |
jira_add_comment |
Add a comment | issue_key, body, visibility*, visibility_value* |
jira_get_comments |
List comments | issue_key, max_results*, start_at* |
jira_link_issues |
Create a link between issues | inward_issue_key, outward_issue_key, link_type, comment* |
jira_get_issue_links |
List links | issue_key |
jira_search_issues |
JQL search (whitelist-scoped) via the enhanced POST /rest/api/3/search/jql (or GET variant when JIRA_SEARCH_ENGINE=get); includes readable fields by default and annotates the effective scope |
jql, max_results*, start_at*, fields*, expand*, fields_by_keys* |
jira_search_issues_jql_only |
Compact JQL search (key/summary/status/assignee) on the enhanced search API | jql, max_results* (default 20) |
Sprints / Boards
| Tool | Description | Key params |
|---|---|---|
jira_list_boards |
List boards | project_key*, board_type* |
jira_list_sprints |
List sprints | board_id, state*, max_results* |
jira_get_sprint_issues |
Issues in a sprint | sprint_id |
jira_move_issues_to_sprint |
Move issues to a sprint | sprint_id, issue_keys |
Projects
| Tool | Description | Key params |
|---|---|---|
jira_list_projects |
List accessible projects | max_results* |
jira_get_project |
Get project detail | project_key |
jira_get_project_versions |
List project versions | project_key |
Users
| Tool | Description | Key params |
|---|---|---|
jira_search_users |
Search users (privacy-filtered) | query, max_results* |
jira_get_myself |
Authenticated user info / debug auth | – |
Attachments
| Tool | Description | Key params |
|---|---|---|
jira_add_attachment |
Upload a file (≤ 10 MiB) | issue_key, file_path |
jira_list_attachments |
List attachments | issue_key |
Worklogs
| Tool | Description | Key params |
|---|---|---|
jira_add_worklog |
Log time | issue_key, time_spent (e.g. 2h 30m), comment*, started* |
jira_get_worklogs |
List worklogs | issue_key |
Example return value
jira_create_issue(project_key="PROJ", summary="Login fails", issue_type="Bug") →
{"issue":{"id":"10004","key":"PROJ-9","self":"https://acme.atlassian.net/rest/api/3/issue/10004"}}
Resources
MCP resources expose read-only context the model can reference:
| URI (template) | Content |
|---|---|
jira://projects |
Static listing of accessible projects (key – name). |
jira://project/{project_key}/meta |
Create-issue metadata: issue types + editable fields. |
jira://issue/{issue_key} |
Live snapshot of a single issue. |
jira://issue/{issue_key}/transitions |
Available workflow transitions. |
Reading jira://project/PROJ/meta, for example, returns JSON such as:
{"projects": [{"key": "PROJ", "issuetypes": [{"name": "Bug", "fields": {"summary": {"required": true}}}]}]}
Prompts
Prompts guide the model through structured workflows:
| Prompt | Description | Variables |
|---|---|---|
create_bug_report |
Draft a structured Bug + create it | project_key, summary, steps_to_reproduce, expected_behavior, actual_behavior, severity |
sprint_review_summary |
Summarize a sprint for review | sprint_id |
triage_issue |
Recommend priority/component/assignee | issue_key |
daily_standup_report |
Group last-24h issue changes by people | project_key, sprint_id (optional) |
Example — after invoking create_bug_report the model will assemble the
details and call jira_create_issue automatically.
Tool permissions (CRUD control)
A Jira API token is often granted broad rights (e.g. create, edit, delete),
but a given deployment may only need a subset of them. Rather than
maintaining a separate token per workflow, the server can restrict which
tools it exposes via JIRA_TOOLS. Disabled tools are never registered, so
clients cannot see them in tools/list — an over-permissioned token can
still only drive the tools you opt in to.
JIRA_TOOLS is a comma-separated allowlist of category keywords and/or exact
tool names. Empty or unset keeps today's behavior (all tools enabled).
| Keyword | Effect |
|---|---|
read |
All read-only tools: jira_get_*, jira_list_*, jira_search_*, jira_get_*_meta. No mutating operations. |
create |
Create issues, add comments / attachments / worklogs, link issues, move issues to sprints. |
update |
Update issue fields and transition issues. |
delete |
jira_delete_issue. |
write |
Shorthand for create,update,delete. |
Examples:
# Read-only deployment (the Jira token may be full-admin; clients can only read).
JIRA_TOOLS=read
# Everything except deletion.
JIRA_TOOLS=read,create,update
# Read + exactly one extra tool.
JIRA_TOOLS=read,jira_add_comment
Notes:
-
Values are case-insensitive; entries must be a known keyword or a real tool name. A typo (e.g.
JIRA_TOOLS=rede) fails startup instead of silently dropping tools. -
The current tool-category mapping is:
Category Tools readjira_get_issue,jira_get_transitions,jira_get_comments,jira_get_issue_links,jira_get_issue_meta,jira_get_project_meta,jira_search_issues,jira_search_issues_jql_only,jira_list_projects,jira_get_project,jira_get_project_versions,jira_list_boards,jira_list_sprints,jira_get_sprint_issues,jira_list_attachments,jira_get_worklogs,jira_search_users,jira_get_myselfcreatejira_create_issue,jira_add_comment,jira_add_attachment,jira_add_worklog,jira_link_issues,jira_move_issues_to_sprintupdatejira_update_issue,jira_transition_issuedeletejira_delete_issue
This layer protects the tool surface, not Jira itself. Authorization also follows
JIRA_PROJECT_KEYS: combine both to scope by operation and by project (e.g.JIRA_TOOLS=read JIRA_PROJECT_KEYS=ENG= read-only on one project).
Security & best practices
- HTTPS only —
JIRA_BASE_URLmust start withhttps://; plaintext URLs are refused at startup. - No hardcoded credentials — everything comes from the server environment or server-side files.
- Server-side credentials only —
JIRA_API_TOKEN(and the base URL / user email) are configured on the MCP server process, never in the MCP client config or on the stdio command. In stdio mode an AI model runs inside the client; a token in the client config would be visible to it. Keep the token in a server-sidejira_server.toml/jira_server.json(preferred),.env, or--token-filenext to the installation. - Config file keeps secrets out of the environment — for maximum
protection, put
api_tokenin the auto-discoveredjira_server.toml/jira_server.json(read by the server only), so the value never appears in any environment variable that a client or process dump could expose, and the MCP launch command needs no path to it. - Token masking — tokens are logged as
ATATT****, never in full. - Never send MCP clients the Jira credentials — the server exposes JQL as MCP tools (so the model never needs to "find the stored credentials" to query Jira); the credentials live only in the server-side file/environment.
- Current search API — issue search uses the enhanced
POST /rest/api/3/search/jqlby default (the only search endpoint Jira Cloud still serves; the old/rest/api/3/searchreturns 410), falling back to its GET variant withJIRA_SEARCH_ENGINE=get. Search requests include a readable default field set so results always carrykey,summary,status, andassignee. - Project whitelist — set
JIRA_PROJECT_KEYS=ENG,SALESto scope all JQL searches and block writes to other projects. - Tool whitelist —
JIRA_TOOLSrestricts which tools are exposed (see Tool permissions); use it to keep an over-permissioned token from driving destructive tools. - Deletion guard —
jira_delete_issuerequiresconfirm=true. - Attachment guard — uploads over 10 MiB are rejected.
- Rate limiting — client-side token bucket (default 100 req/min).
- HTTP transport auth — enable
--auth-mode tokenand a strongMCP_SERVER_TOKENfor anything networked. - Production HTTP — bind
127.0.0.1and put the server behind a reverse proxy (nginx/Caddy) that terminates TLS; restrict the port to the proxy and approved clients; enable request/audit logging. - Rotate tokens — use distinct tokens per environment, rotate quarterly (or on any suspected leak), and remove users from Jira when they leave.
- Least privilege — give the token user only the Jira permissions the workflows need (browse + create/edit specific projects; not Global Admin).
Docker deployment
Dockerfile (included)
FROM python:3.12-slim
WORKDIR /app
COPY pyproject.toml README.md ./
COPY src/ src/
RUN pip install --no-cache-dir .
ENV MCP_TRANSPORT=http MCP_HOST=0.0.0.0 MCP_PORT=8080 JIRA_AUTH_METHOD=basic JIRA_TOOLS=read,create,update
EXPOSE 8080
CMD ["jira-mcp-server", "--transport", "http", "--host", "0.0.0.0", "--port", "8080"]
docker-compose.yml (included)
services:
jira-mcp:
build: .
ports: ["8080:8080"]
environment:
JIRA_BASE_URL: ${JIRA_BASE_URL}
JIRA_API_TOKEN: ${JIRA_API_TOKEN}
JIRA_USER_EMAIL: ${JIRA_USER_EMAIL}
JIRA_PROJECT_KEYS: ${JIRA_PROJECT_KEYS:-}
JIRA_TOOLS: ${JIRA_TOOLS:-}
MCP_TRANSPORT: http
MCP_HOST: 0.0.0.0
MCP_PORT: 8080
MCP_AUTH_MODE: ${MCP_AUTH_MODE:-none}
MCP_SERVER_TOKEN: ${MCP_SERVER_TOKEN:-}
restart: unless-stopped
docker compose up -d # reads .env for JIRA_* / MCP_SERVER_TOKEN
curl http://127.0.0.1:8080/health
Kubernetes (example)
apiVersion: apps/v1
kind: Deployment
metadata: { name: jira-mcp }
spec:
replicas: 2
template:
metadata: { labels: { app: jira-mcp } }
spec:
containers:
- name: jira-mcp
image: your-registry/jira-mcp-server:latest
ports: [{ containerPort: 8080 }]
envFrom: [{ secretRef: { name: jira-mcp-secrets } }]
readinessProbe:
httpGet: { path: /health, port: 8080 }
---
apiVersion: v1
kind: Service
metadata: { name: jira-mcp }
spec:
selector: { app: jira-mcp }
ports: [{ port: 8080, targetPort: 8080 }]
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Jira API Error [401] |
Jira token missing/wrong/expired | Regenerate at the API-token page; re-run |
Jira API Error [403] |
Token lacks permission for the operation | Grant the user the Jira permission or widen API token scope |
Jira API Error [404] |
Wrong issue/project key | Verify the key; check the project exists and is accessible |
Jira API Error [400] |
Invalid field/value for the transition | Read the message; adjust fields |
Jira API Error [429] |
Jira's own rate limit | Slow down; honor retry-after; raise JIRA_RATE_LIMIT |
| Connection timed out | Wrong URL / blocked egress / proxy | Check JIRA_BASE_URL, outbound network, HTTPS_PROXY |
Request validation failed on /sse |
DNS-rebinding protection from the SDK | Use a real Host header; the SDK allows 127.0.0.1/localhost as default. For proxies, pass --host 0.0.0.0 or a configured host |
| SSE drops then reconnects | Idle/long-lived connection timeout on the network | Reconnect is automatic in MCP clients; check proxies for < SSE > 60s timeouts |
401 on the HTTP endpoint |
--auth-mode token wrong/absent Authorization |
Send Authorization: Bearer <MCP_SERVER_TOKEN> |
Connection closed / -32000 in Claude Code stdio |
The server crashed at startup (often JIRA_USER_EMAIL required …): the config file wasn't found, so JIRA_BASE_URL/token weren't set |
Drop jira_server.toml next to jira-mcp-server.exe (in the Python Scripts dir) and restart; run jira-mcp-server --transport stdio manually to see the error |
Diagnosing Jira auth quickly: run
jira-mcp-server --transport stdio
# and in a client, call jira_get_myself — it returns the Jira user the token resolves to.
Rate limiting: the client limiter blocks the calling task instead of
erroring, so batch prompts just run a little slower. Jira's own limits are
upstream of the token; space out CI loops and use max_results liberally.
Development
Add a new tool
- Open
src/jira_mcp_server/tools/issues.py(or the matching module). - Inside the existing
register(registry)function add:
@registry.tool(name="my_new_tool", title="...", description="...")
async def my_new_tool(
ctx: Context,
param1: Annotated[str, Field(description="...")],
optional: Annotated[int | None, Field(default=None, description="...")] = None,
) -> Any:
"""Docstring used as the default description."""
client = get_client()
try:
data = await client.some_endpoint(param1)
return dict_result(data, label="result")
except Exception as exc:
return error_result(exc)
- Add the matching client method in
src/jira_mcp_server/client.py. - Run the tests.
Add a new resource
In src/jira_mcp_server/tools/resources.py, inside register():
@registry.resource("jira://issue/{issue_key}/comments")
def comments_for_issue(issue_key: str) -> str:
return json_dumps(get_client().get_comments_blocking_for_resource(issue_key))
Tests
pytest # full suite
pytest -m "not network" # offline unit + integration tests
pytest tests/test_transport_http.py # HTTP/SSE app tests
The suite uses pytest + pytest-asyncio; HTTP tests run against an
in-process Starlette app via httpx.ASGITransport (no live network).
Style
ruff (lint + format) and mypy are configured in pyproject.toml:
ruff check src tests
ruff format --check src tests
mypy src
Commit messages: conventional-commit style (e.g. feat: add jira_export_issues).
Changes land via PR; each PR must pass lint, type checks, and the test suite.
Building wheel / sdist
Distribution artifacts (wheel for pip install, sdist for source) are
built with build — a build-time tool,
not a runtime dependency, so it is installed into the virtual environment
on demand but never added to pyproject.toml.
Windows — scripts\build.bat
scripts\build.bat
If .venv does not exist it is created; then the script installs build
into it, clears stale build\ / dist\ / *.egg-info, and runs
python -m build. Artifacts land in dist\:
| File | Purpose |
|---|---|
jira_mcp_server-<version>-py3-none-any.whl |
Binary distribution; use directly with pip install |
jira_mcp_server-<version>.tar.gz |
Source distribution (sdist); rebuild the wheel from source |
Windows note: do not delete
.venvfrom inside the script — a live Python holds locks on native.pyd/.dllfiles. Recreate it from a shell that is not using it:rmdir /s /q .venv && python -m venv .venv.
macOS / Linux (steps this script automates)
python -m venv .venv
source .venv/bin/activate
pip install build # build-time tool only; not a project dependency
python -m build # builds wheel + sdist into dist/
What goes into each artifact
- Wheel — the installed package: all
src/jira_mcp_server/**modules, thepy.typedtype marker,LICENSE, entry point, andMETADATA/RECORD. Tests and.env.exampleare not installed (they are dev-only). - sdist — self-contained source:
src/,tests/,pyproject.toml,MANIFEST.in,LICENSE,README.md,py.typed, and.env.example.python -m buildverifies the sdist by rebuilding the wheel from it, so a broken manifest fails the build.
Release checklist (before building)
- Bump
versioninpyproject.tomlandsrc/jira_mcp_server/__init__.py(they must match — the CLI reads the latter, packaging the former). - Update
README.md## Changelog; verifyjira-mcp-server --version. - Run
python -m pytest(all green),ruff check src tests,mypy src. scripts\build.bat(or the macOS/Linux commands above).- Inspect
dist\— exactly one.whland one.tar.gzfor the new version.
Publishing to PyPI
The artifacts in dist\ are ready to upload once they pass the checklist
above. Tags/releases are outside this repo's automation; push a Git tag
(v<version>) and upload:
pip install twine # upload tool; build/publish-time only
twine check dist\*.whl dist\*.tar.gz # verify metadata + long description
twine upload dist\jira_mcp_server-<version>-py3-none-any.whl \
dist\jira_mcp_server-<version>.tar.gz
twine upload prompts for the PyPI API token (or use
TWINE_USERNAME=__token__ TWINE_PASSWORD=<token> for CI). The project
declares its metadata (name, version, description, license,
classifiers, [project.urls]) in pyproject.toml, so no setup.py is
needed.
Version discipline: never upload as
0.2.0twice — PyPI rejects an existing version. Bumppyproject.toml+src/jira_mcp_server/__init__.pyin lockstep and rebuild before every release.
Verify the published package
Test the exact artifact before announcing a release:
python -m venv /tmp/verify && /tmp/verify/bin/python -m pip install \
dist\jira_mcp_server-<version>-py3-none-any.whl
/tmp/verify/bin/jira-mcp-server --version
Building the jira-mcp-server.exe console script
There is no separate "build" step for the executable — on Windows it is
created automatically when you install the project, from the
[project.scripts] entry point in pyproject.toml:
[project.scripts]
jira-mcp-server = "jira_mcp_server.cli:app"
What the entry point produces
pip (via the setuptools backend) generates a small launcher:
- On Windows:
jira-mcp-server.exeinside the Python environment'sScriptsfolder (e.g.D:\develop\Python312\Scripts\jira-mcp-server.exe, or.venv\Scripts\jira-mcp-server.exewhen using a virtual env). - On macOS/Linux: a
jira-mcp-servershell script onPATH.
The launcher is a thin shim that imports jira_mcp_server.cli and calls
app (a Typer command). All real logic lives in src/; the .exe is just
a starter.
Build it from source (Windows)
# 1. Create and activate a virtual environment.
python -m venv .venv
.\.venv\Scripts\Activate.ps1
# 2. Install the project (editable keeps src/ live; plain install copies it).
# Either is fine. "-e" means you can edit src/ and restart the MCP client
# without reinstalling.
python -m pip install -e ".[dev]" # editable, recommended for development
# python -m pip install . # regular install, recommended for a fixed version
# 3. The .exe is now on PATH inside the venv (or the base Scripts dir).
jira-mcp-server --version # -> jira-mcp-server 0.2.0
Verify
python -m jira_mcp_server --version # same behavior as the exe
jira-mcp-server --version # the generated launcher/exe
If jira-mcp-server is not found, ensure the environment's Scripts
directory is on PATH (Python's installer usually adds it). To reinstall
after deleting the launcher, re-run pip install -e ..
Editable vs. regular install
pip install -e .("editable") registers the launcher but points it back at yoursrc/tree. Edit source, restart the MCP client, and the change is picked up without reinstalling — the normal choice while developing.pip install .("regular") copies the package into the environment'ssite-packages; the launcher runs that frozen copy. Use it for a fixed version you do not expect to edit.
Both commands produce the same jira-mcp-server.exe; only where the code
lives differs.
Build without a virtual environment
If you installed Python globally and cannot or do not want a venv, run
pip install -e . directly. The launcher then lands in the base interpreter's
Scripts folder (e.g. D:\develop\Python312\Scripts\jira-mcp-server.exe),
which must be on PATH for MCP clients to find the jira-mcp-server command.
Architecture / how to extend
src/jira_mcp_server/
├── cli.py # typer CLI → transport selection
├── server.py # create_server(): MCPServer + registration
├── config.py # pydantic-settings (JIRA_* env), validated, lazy
├── safety.py # credential masking / normalization
├── permissions.py # JIRA_TOOLS allowlist: CRUD categories → tool sets
├── client.py # JiraClient: httpx async, retry, pagination, scope
├── auth.py # Basic / Bearer header construction
├── errors.py # JiraError hierarchy + status mapping
├── formatters.py # ADF ⇄ plain text
├── validators.py # safe JQL building
├── rate_limiter.py # token bucket
├── middleware.py # HTTP auth / logging / CORS middleware + /health
├── transport/
│ ├── stdio.py # stdio runner
│ ├── http.py # SSE + Streamable HTTP apps (extends SDK app)
│ └── logging.py # stderr/file logging, sensitive filter
└── tools/
├── core.py # ToolRegistry, register_server, shared client
├── serde.py # CallToolResult helpers
├── issues.py # 14 issue tools
├── projects.py # 3 project tools
├── sprints.py # 4 board/sprint tools
├── users.py # 2 user tools
├── attachments.py# 2 attachment tools
├── worklog.py # 2 worklog tools
├── resources.py # 4 MCP resources
└── prompts.py # 4 MCP prompts
Planned / extension points
- Webhook events (the transport layer is decoupled so an HTTP Webhook route can be added without touching the tools).
- Multi-Jira-instance support (the config layer is a single
Settingsobject; a futureMCP_INSTANCEScould create one per instance). - Per-tool granular Prompts / completion metadata.
Changelog
v0.3.0 (2026-08-07)
- Search results now always carry issue keys — the enhanced
/rest/api/3/search/jqlendpoint returns bare{"id": ...}records unless afieldsarray is requested, sojira_search_issueswithout an explicitfieldsused to surface empty keys/summaries (models interpreted this as "no results"). Searches now request a readable default field set (summary,status,assignee) so every result includeskeyand the core fields. - Search results are annotated with the effective project scope — when a
JIRA_PROJECT_KEYSallowlist is configured, search payloads now include the allowed projects (and a note when a query matches nothing), so a model understands when the allowlist — not a bad query — is why a search returns no rows for a project outside the whitelist. - Config file discovery now also checks the interpreter's
Scriptsdirectory —jira_server.toml/jira_server.jsonplaced next tojira-mcp-server.exe(in<python>/Scriptsor<venv>/Scripts) is found even when the MCP client launches the server from a different working directory. This fixes the Windows failure where Claude Code reported "Connection closed / -32000" because the server crashed at startup with no config (the toml was next to the exe, but only the cwd was searched). - Issue search moved off the deprecated endpoint —
jira_search_issues/jira_search_issues_jql_onlynow use the current Jira search API:GET /rest/api/3/searchby default (documented by Atlassian as the primary search endpoint), andPOST /rest/api/3/search/jql(enhanced search, withfields_by_keyssupport) whenJIRA_SEARCH_ENGINE=jql. The deprecatedPOST /rest/api/3/searchis never called, so tools no longer look "legacy" and models have no reason to try to reach past the MCP tools for Jira data. - CLI/config: added
--search-engine(envJIRA_SEARCH_ENGINE; also settable assearch_engineinjira_server.toml/jira_server.json), validated at startup. Startup logs now state which search API is enabled. - Credential guard on search results — issue-search tool output is
sanitized server-side: any key named like a credential (
token,password,credentials,authorization,access_token, …) is stripped, and token-like strings inside nested values are masked. Even if Jira (or a proxy) ever returned such a field, the model would never see it — which also means the model has no need to seek out the server's stored credentials itself. - Docs: README tool table, env-var table, CLI reference, config-file section, and security checklist updated for the search engine and the "use the MCP tools, never reach for stored credentials" note.
v0.2.0 (2026-08-06)
- Credentials are server-side only (
JIRA_API_TOKENout of the MCP client) — the Jira base URL, user email, and API token are no longer accepted as MCP client parameters in stdio mode: passing them through the client config would expose the token to the AI running in the client. The server reads them from its own environment (a.envfile next to the installation) or from a separate--token-file/JIRA_TOKEN_FILEKEY=VALUEfile. All stdio examples in this document now register a credential-free command.JIRA_BASE_URLis resolved server-side only (defaults to unset at construction; the validation still refuses plaintext). - CLI: added
jira-mcp-server --token-file <path>(envJIRA_TOKEN_FILE) which mergesJIRA_*entries into the process environment before settings are built; secrets never appear on the client command line. - Config file next to the server (auto-discovered) — the server now reads
a fixed-name
jira_server.toml/jira_server.jsonin the directory it is launched from, so the whole Jira connection (e.g.api_token,base_url,auth_method) lives in one server-side file with no--config/path on the MCP launch command and without entering any environment variable (visible to MCP clients/AI).toolsthere restricts the MCP tool surface exactly likeJIRA_TOOLS. ExistingJIRA_*env /.env/--token-filepaths keep working alongside. - Tests: test environment is now isolated from real host
.envandJIRA_*/MCP_*variables (added config-file load tests and env isolation intests/conftest.py). - Docs: all documentation is now English-only (previously contained Chinese sections and changelog entries).
- Packaging: added
scripts\build.batandMANIFEST.in, added the missingpy.typedPEP 561 marker, and documented wheel/sdist building and PyPI publishing.python -m buildnow produces a completejira_mcp_server-<version>-py3-none-any.whland.tar.gz(previously the sdist was missingpyproject.tomland the wheel lackedpy.typed).
v0.1.1 (2026-08-06)
- Tool permission control (
JIRA_TOOLS) — restrict which MCP tools are exposed via comma-separated CRUD keywords (read/create/update/delete/write) or exact tool names. Disabled tools are not registered, so clients never see them; typos fail startup. See Tool permissions. - Docs: added the full from-source install guide (Windows
PowerShell/CMD) and how to generate
jira-mcp-server.exelocally; added the Client token (HTTP auth) section covering client-token generation, server-side and client-side configuration, and rotation.
v0.1.0 (2026-08-06)
- Initial release.
Jira MCP Server is not affiliated with, endorsed by, or sponsored by Atlassian. "Jira" is a trademark of Atlassian Pty Ltd.
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 jira_mcp_server_hopcos-0.2.0.tar.gz.
File metadata
- Download URL: jira_mcp_server_hopcos-0.2.0.tar.gz
- Upload date:
- Size: 116.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7e0b080ef2b8e5848c225bfbb00bbe7fc9993e9687468a853722aa04fd38187a
|
|
| MD5 |
9b6c81fb676c1fc19aa6ff8dd1ceb6d9
|
|
| BLAKE2b-256 |
707d7d6e89f5abf8c08850b7e2ea3920141c1fd17e120280621f74199ad5a299
|
File details
Details for the file jira_mcp_server_hopcos-0.2.0-py3-none-any.whl.
File metadata
- Download URL: jira_mcp_server_hopcos-0.2.0-py3-none-any.whl
- Upload date:
- Size: 76.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6fde58bc92fbadab3455adb3c6fd60723ebf951c3ba353b3d96230d3728dfd8b
|
|
| MD5 |
3bb1e8ba3a111fb9a0df17439ae16fee
|
|
| BLAKE2b-256 |
746d4e965af97fdcaf47ee5f270a7886d11f0cf8c9e6f79e446c21b4aae56642
|