Skip to content
AI Agent How-To

Home › MCP server setup guides

MCP Server for Jira: Search and Create Issues from Claude or Cursor

Intermediate ~6 min read Last tested October 2026

Updated 2026-10-15

There are two routes to Jira over MCP:

  1. Atlassian’s official remote MCP server (part of Atlassian’s Rovo offering), which you add to a supported client and authorize with OAuth in the browser. Easiest if your client and Atlassian plan support it; check Atlassian’s current docs for the endpoint.
  2. A small self-hosted server using a Jira API token, which works with any MCP client and gives you full control over what the agent can do. That’s what this guide builds.

Prerequisites

Steps

1. Create the project

uv init jira-mcp && cd jira-mcp
uv add "mcp[cli]" httpx

2. Write the server

# server.py
import os, httpx
from mcp.server.fastmcp import FastMCP

BASE = os.environ["JIRA_URL"].rstrip("/")              # https://your-site.atlassian.net
AUTH = (os.environ["JIRA_EMAIL"], os.environ["JIRA_API_TOKEN"])
mcp = FastMCP("jira")

def api(method: str, path: str, **kw):
    r = httpx.request(method, f"{BASE}/rest/api/3{path}", auth=AUTH,
                      headers={"Accept": "application/json"}, timeout=20, **kw)
    r.raise_for_status()
    return r.json() if r.content else {}

@mcp.tool()
def search_issues(jql: str, max_results: int = 20) -> str:
    """Search Jira with JQL, e.g. 'project = WEB AND status != Done ORDER BY updated DESC'."""
    data = api("GET", "/search/jql", params={"jql": jql, "maxResults": max_results,
                                               "fields": "summary,status,assignee,priority"})
    lines = []
    for i in data.get("issues", []):
        f = i["fields"]
        who = (f.get("assignee") or {}).get("displayName", "unassigned")
        lines.append(f'{i["key"]} [{f["status"]["name"]}] {f["summary"]} ({who})')
    return "\n".join(lines) or "no issues"

@mcp.tool()
def get_issue(key: str) -> str:
    """Get one issue's summary, status and description as text."""
    f = api("GET", f"/issue/{key}", params={"fields": "summary,status,description"})["fields"]
    def text(node):
        if not node: return ""
        if node.get("type") == "text": return node.get("text", "")
        return "".join(text(c) for c in node.get("content", [])) + ("\n" if node.get("type") == "paragraph" else "")
    return f'{key}: {f["summary"]}\nStatus: {f["status"]["name"]}\n\n{text(f.get("description"))}'

@mcp.tool()
def create_issue(project_key: str, summary: str, description: str, issue_type: str = "Task") -> str:
    """Create a Jira issue and return its key."""
    body = {"fields": {
        "project": {"key": project_key}, "summary": summary, "issuetype": {"name": issue_type},
        "description": {"type": "doc", "version": 1, "content": [
            {"type": "paragraph", "content": [{"type": "text", "text": description}]}]}}}
    return api("POST", "/issue", json=body)["key"]

if __name__ == "__main__":
    mcp.run()

Jira Cloud API v3 uses Atlassian Document Format (ADF) for descriptions, which is why create_issue wraps the text in a doc node. Search uses the /rest/api/3/search/jql endpoint, which replaced the older /rest/api/3/search.

3. Register it

{
  "mcpServers": {
    "jira": {
      "command": "uv",
      "args": ["--directory", "/ABSOLUTE/PATH/jira-mcp", "run", "server.py"],
      "env": {
        "JIRA_URL": "https://your-site.atlassian.net",
        "JIRA_EMAIL": "[email protected]",
        "JIRA_API_TOKEN": "your-token"
      }
    }
  }
}

4. Try it

“List my open bugs in project WEB, then create a task to add tests for the top one.”

Common errors

401 Unauthorized: wrong email/token pair, or you used your password instead of an API token.

400 with "Operation value must be an Atlassian Document": you sent a plain string as description. Use ADF as shown.

Search returns an error saying the endpoint was removed: you are calling the removed /rest/api/3/search endpoint. Switch to /search/jql.

issuetype: Specify a valid issue type: issue type names vary by project (“Story”, “Bug”, “Task”). Ask the agent to search an existing issue first, or list types via /issue/createmeta.