jira2mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.3
2
+ Name: jira2mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server exposing Jira Cloud tools via FastMCP
5
+ Author: en-ver
6
+ Requires-Dist: fastmcp>=3.1.0
7
+ Requires-Dist: jira2py==0.4.0
8
+ Requires-Dist: marklassian>=0.1.0
9
+ Requires-Dist: pathvalidate>=3.3.1
10
+ Requires-Dist: pyadf>=0.3.0
11
+ Requires-Dist: pydantic>=2.12.5
12
+ Requires-Python: >=3.13
13
+ Description-Content-Type: text/markdown
14
+
15
+ # jira2mcp
16
+
17
+ MCP server for Jira Cloud — gives AI assistants like Claude the ability to read, create, edit, search, and comment on Jira issues.
18
+
19
+ Built with [FastMCP](https://github.com/jlowin/fastmcp) and [jira2py](https://pypi.org/project/jira2py/).
20
+
21
+ ## Setup
22
+
23
+ ### 1. Get your Jira credentials
24
+
25
+ You need three values from your Jira Cloud instance:
26
+
27
+ | Variable | Description |
28
+ |---|---|
29
+ | `JIRA_URL` | Your Jira instance URL (e.g. `https://yourcompany.atlassian.net`) |
30
+ | `JIRA_USER` | Your Jira account email |
31
+ | `JIRA_API_TOKEN` | API token from [id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens) |
32
+
33
+ ### 2. Install uv (if not already installed)
34
+
35
+ `uvx` is part of [uv](https://docs.astral.sh/uv/), a fast Python package manager:
36
+
37
+ ```bash
38
+ # macOS / Linux
39
+ curl -LsSf https://astral.sh/uv/install.sh | sh
40
+
41
+ # Windows
42
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
43
+ ```
44
+
45
+ ### 3. Add to Claude Code
46
+
47
+ ```bash
48
+ claude mcp add jira -- uvx jira2mcp
49
+ ```
50
+
51
+ ### 4. Configure credentials
52
+
53
+ **Option A: Shell environment variables**
54
+
55
+ Export them in your shell profile (`~/.bashrc`, `~/.zshrc`, etc.):
56
+
57
+ ```bash
58
+ export JIRA_URL="https://yourcompany.atlassian.net"
59
+ export JIRA_USER="you@company.com"
60
+ export JIRA_API_TOKEN="your-api-token"
61
+ ```
62
+
63
+ The MCP configuration stays minimal:
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "jira": {
69
+ "command": "uvx",
70
+ "args": ["jira2mcp"]
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ **Option B: Inline in MCP configuration**
77
+
78
+ If you prefer not to set global environment variables, provide them directly in the `env` section:
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "jira": {
84
+ "command": "uvx",
85
+ "args": ["jira2mcp"],
86
+ "env": {
87
+ "JIRA_URL": "https://yourcompany.atlassian.net",
88
+ "JIRA_USER": "you@company.com",
89
+ "JIRA_API_TOKEN": "your-api-token"
90
+ }
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ ## Tools
97
+
98
+ | Tool | Description |
99
+ |---|---|
100
+ | `jira_read` | Read a Jira issue by key with full details |
101
+ | `jira_search` | Search issues using JQL |
102
+ | `jira_create` | Create a new issue |
103
+ | `jira_edit` | Update an existing issue |
104
+ | `jira_comment` | Add a comment to an issue |
105
+ | `jira_comments` | List comments with pagination |
106
+ | `jira_fields` | Get field metadata for create/edit screens |
107
+ | `jira_projects` | List accessible projects |
108
+ | `jira_users` | Search users by name or email |
109
+ | `jira_attachment` | Download an attachment |
110
+ | `jira_add_link` | Create a link between two issues |
111
+ | `jira_delete_link` | Delete an issue link |
112
+
113
+ ## Resources
114
+
115
+ | Resource | Description |
116
+ |---|---|
117
+ | `data://jira/link-types` | Available issue link types in your Jira instance |
118
+
119
+ ## Prompts
120
+
121
+ | Prompt | Description |
122
+ |---|---|
123
+ | `jql_syntax` | JQL syntax reference for building search queries |
124
+
125
+ ## Key features
126
+
127
+ - **Markdown in, Markdown out** — write descriptions and comments in Markdown; they're auto-converted to Atlassian Document Format (ADF). ADF fields from Jira are converted back to Markdown.
128
+ - **Field discovery** — use `jira_fields` to discover required and available fields before creating or editing issues.
129
+ - **User lookup** — use `jira_users` to resolve display names to account IDs for assignment.
130
+ - **Extra fields** — request additional fields on `jira_read` beyond the standard set; rich-text fields are auto-converted.
131
+ - **Link management** — read the `data://jira/link-types` resource to discover available link types, then create or delete links between issues.
132
+
133
+ ## License
134
+
135
+ MIT
@@ -0,0 +1,121 @@
1
+ # jira2mcp
2
+
3
+ MCP server for Jira Cloud — gives AI assistants like Claude the ability to read, create, edit, search, and comment on Jira issues.
4
+
5
+ Built with [FastMCP](https://github.com/jlowin/fastmcp) and [jira2py](https://pypi.org/project/jira2py/).
6
+
7
+ ## Setup
8
+
9
+ ### 1. Get your Jira credentials
10
+
11
+ You need three values from your Jira Cloud instance:
12
+
13
+ | Variable | Description |
14
+ |---|---|
15
+ | `JIRA_URL` | Your Jira instance URL (e.g. `https://yourcompany.atlassian.net`) |
16
+ | `JIRA_USER` | Your Jira account email |
17
+ | `JIRA_API_TOKEN` | API token from [id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens) |
18
+
19
+ ### 2. Install uv (if not already installed)
20
+
21
+ `uvx` is part of [uv](https://docs.astral.sh/uv/), a fast Python package manager:
22
+
23
+ ```bash
24
+ # macOS / Linux
25
+ curl -LsSf https://astral.sh/uv/install.sh | sh
26
+
27
+ # Windows
28
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
29
+ ```
30
+
31
+ ### 3. Add to Claude Code
32
+
33
+ ```bash
34
+ claude mcp add jira -- uvx jira2mcp
35
+ ```
36
+
37
+ ### 4. Configure credentials
38
+
39
+ **Option A: Shell environment variables**
40
+
41
+ Export them in your shell profile (`~/.bashrc`, `~/.zshrc`, etc.):
42
+
43
+ ```bash
44
+ export JIRA_URL="https://yourcompany.atlassian.net"
45
+ export JIRA_USER="you@company.com"
46
+ export JIRA_API_TOKEN="your-api-token"
47
+ ```
48
+
49
+ The MCP configuration stays minimal:
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "jira": {
55
+ "command": "uvx",
56
+ "args": ["jira2mcp"]
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ **Option B: Inline in MCP configuration**
63
+
64
+ If you prefer not to set global environment variables, provide them directly in the `env` section:
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "jira": {
70
+ "command": "uvx",
71
+ "args": ["jira2mcp"],
72
+ "env": {
73
+ "JIRA_URL": "https://yourcompany.atlassian.net",
74
+ "JIRA_USER": "you@company.com",
75
+ "JIRA_API_TOKEN": "your-api-token"
76
+ }
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ ## Tools
83
+
84
+ | Tool | Description |
85
+ |---|---|
86
+ | `jira_read` | Read a Jira issue by key with full details |
87
+ | `jira_search` | Search issues using JQL |
88
+ | `jira_create` | Create a new issue |
89
+ | `jira_edit` | Update an existing issue |
90
+ | `jira_comment` | Add a comment to an issue |
91
+ | `jira_comments` | List comments with pagination |
92
+ | `jira_fields` | Get field metadata for create/edit screens |
93
+ | `jira_projects` | List accessible projects |
94
+ | `jira_users` | Search users by name or email |
95
+ | `jira_attachment` | Download an attachment |
96
+ | `jira_add_link` | Create a link between two issues |
97
+ | `jira_delete_link` | Delete an issue link |
98
+
99
+ ## Resources
100
+
101
+ | Resource | Description |
102
+ |---|---|
103
+ | `data://jira/link-types` | Available issue link types in your Jira instance |
104
+
105
+ ## Prompts
106
+
107
+ | Prompt | Description |
108
+ |---|---|
109
+ | `jql_syntax` | JQL syntax reference for building search queries |
110
+
111
+ ## Key features
112
+
113
+ - **Markdown in, Markdown out** — write descriptions and comments in Markdown; they're auto-converted to Atlassian Document Format (ADF). ADF fields from Jira are converted back to Markdown.
114
+ - **Field discovery** — use `jira_fields` to discover required and available fields before creating or editing issues.
115
+ - **User lookup** — use `jira_users` to resolve display names to account IDs for assignment.
116
+ - **Extra fields** — request additional fields on `jira_read` beyond the standard set; rich-text fields are auto-converted.
117
+ - **Link management** — read the `data://jira/link-types` resource to discover available link types, then create or delete links between issues.
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "jira2mcp"
3
+ version = "0.1.0"
4
+ description = "MCP server exposing Jira Cloud tools via FastMCP"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "en-ver" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "fastmcp>=3.1.0",
12
+ "jira2py==0.4.0",
13
+ "marklassian>=0.1.0",
14
+ "pathvalidate>=3.3.1",
15
+ "pyadf>=0.3.0",
16
+ "pydantic>=2.12.5",
17
+ ]
18
+
19
+ [project.scripts]
20
+ jira2mcp = "jira2mcp:main"
21
+
22
+ [build-system]
23
+ requires = ["uv_build>=0.10.4,<0.11.0"]
24
+ build-backend = "uv_build"
25
+
26
+ [tool.ruff.lint]
27
+ select = ["E4", "E7", "E9", "F", "I", "UP"]
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "ruff>=0.15.4",
32
+ "ty>=0.0.20",
33
+ ]
@@ -0,0 +1,32 @@
1
+ """Jira MCP Server — Model Context Protocol server for Jira Cloud."""
2
+
3
+ from importlib.metadata import version as _pkg_version
4
+
5
+ from fastmcp import FastMCP
6
+
7
+ from .tools import tools
8
+
9
+ mcp = FastMCP(
10
+ "jira2mcp",
11
+ version=_pkg_version("jira2mcp"),
12
+ instructions=(
13
+ "Jira Cloud integration server. All tools are prefixed with 'jira_'.\n"
14
+ "Key workflows:\n"
15
+ "- Before creating: use jira_fields with project_key + issue_type to discover required fields\n"
16
+ "- Before assigning: use jira_users to look up account IDs\n"
17
+ "- Descriptions and comments accept markdown (auto-converted to ADF)\n"
18
+ "- Use the extra_fields parameter on jira_read to request additional fields by ID\n"
19
+ "- Extra fields are returned with display names; rich-text fields are auto-converted from ADF to markdown\n"
20
+ "- Read the data://jira/link-types resource before creating issue links\n"
21
+ "- Use jira_comments to read comments (jira_read only shows the count)\n"
22
+ "- Use the jql_syntax prompt for JQL syntax reference when building search queries"
23
+ ),
24
+ mask_error_details=True,
25
+ on_duplicate="error",
26
+ )
27
+ mcp.mount(tools, namespace="jira")
28
+
29
+
30
+ def main() -> None:
31
+ """Entry point for the MCP server."""
32
+ mcp.run(transport="stdio")
@@ -0,0 +1,167 @@
1
+ """ADF (Atlassian Document Format) conversion utilities.
2
+
3
+ Uses pyadf for ADF→Markdown (reading from Jira)
4
+ and marklassian for Markdown→ADF (writing to Jira).
5
+ """
6
+
7
+ import logging
8
+ from typing import Any
9
+
10
+ from marklassian import AdfDocument, AdfNode
11
+ from marklassian import markdown_to_adf as _markdown_to_adf
12
+ from pyadf import Document
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def adf_to_markdown(adf: Any) -> str:
18
+ """Convert ADF JSON to Markdown.
19
+
20
+ Args:
21
+ adf: ADF document as a dictionary, or None.
22
+
23
+ Returns:
24
+ Markdown string, or "(none)" if input is empty/invalid.
25
+ """
26
+ if not adf or not isinstance(adf, dict):
27
+ return "(none)"
28
+
29
+ try:
30
+ doc = Document(adf)
31
+ md = doc.to_markdown()
32
+ return md.strip() if md else "(none)"
33
+ except Exception:
34
+ logger.exception("ADF to Markdown conversion failed, using plain text fallback")
35
+ return _extract_text_fallback(adf)
36
+
37
+
38
+ def markdown_to_adf(markdown: str) -> AdfDocument:
39
+ """Convert Markdown to ADF JSON.
40
+
41
+ Args:
42
+ markdown: Markdown string.
43
+
44
+ Returns:
45
+ ADF document as a TypedDict.
46
+ """
47
+ if not markdown or not markdown.strip():
48
+ return AdfDocument(type="doc", version=1, content=[])
49
+
50
+ try:
51
+ return _markdown_to_adf(markdown)
52
+ except Exception:
53
+ logger.exception("Markdown to ADF conversion failed, wrapping as plain text")
54
+ return AdfDocument(
55
+ type="doc",
56
+ version=1,
57
+ content=[
58
+ AdfNode(
59
+ type="paragraph",
60
+ content=[AdfNode(type="text", text=markdown)],
61
+ )
62
+ ],
63
+ )
64
+
65
+
66
+ # --- ADF field detection ---
67
+
68
+ # System fields known to use ADF format
69
+ _ADF_SYSTEM_FIELDS: set[str] = {"description", "environment"}
70
+
71
+ # Custom field schema suffix indicating ADF (rich-text paragraph)
72
+ _ADF_CUSTOM_SUFFIX = ":textarea"
73
+
74
+
75
+ def is_adf_value(value: Any) -> bool:
76
+ """Check if a value looks like an ADF document.
77
+
78
+ ADF documents are dicts with ``{"type": "doc", "version": 1, "content": [...]}``.
79
+ """
80
+ return isinstance(value, dict) and value.get("type") == "doc"
81
+
82
+
83
+ def is_adf_field(field_id: str, custom_schema: str = "") -> bool:
84
+ """Check if a field is known to use ADF format.
85
+
86
+ Detection is based on:
87
+ - Known system fields (description, environment)
88
+ - Custom field schema containing ':textarea'
89
+ """
90
+ if field_id in _ADF_SYSTEM_FIELDS:
91
+ return True
92
+ if custom_schema and _ADF_CUSTOM_SUFFIX in custom_schema:
93
+ return True
94
+ return False
95
+
96
+
97
+ def convert_adf_values(data: dict[str, Any]) -> dict[str, Any]:
98
+ """Convert any ADF values in a dict to markdown strings.
99
+
100
+ Iterates over all values and converts those that look like ADF documents.
101
+ Non-ADF values are left unchanged.
102
+ """
103
+ result = {}
104
+ for key, value in data.items():
105
+ if is_adf_value(value):
106
+ result[key] = adf_to_markdown(value)
107
+ else:
108
+ result[key] = value
109
+ return result
110
+
111
+
112
+ def detect_adf_field_ids(fields_metadata: list[dict[str, Any]]) -> set[str]:
113
+ """Build a set of field IDs that use ADF format.
114
+
115
+ Examines field metadata from the Jira fields API and identifies
116
+ fields that use ADF based on known system fields and custom textarea schema.
117
+
118
+ Args:
119
+ fields_metadata: List of field dicts from ``api.fields.get_fields()``.
120
+
121
+ Returns:
122
+ Set of field IDs that expect ADF format.
123
+ """
124
+ adf_ids = set(_ADF_SYSTEM_FIELDS)
125
+ for field in fields_metadata:
126
+ field_id = field.get("id", "")
127
+ schema = field.get("schema", {}) or {}
128
+ custom = schema.get("custom", "")
129
+ if custom and _ADF_CUSTOM_SUFFIX in custom:
130
+ adf_ids.add(field_id)
131
+ return adf_ids
132
+
133
+
134
+ def convert_markdown_fields(
135
+ fields: dict[str, Any],
136
+ adf_field_ids: set[str],
137
+ ) -> dict[str, Any]:
138
+ """Convert markdown string values to ADF for known ADF fields.
139
+
140
+ Only converts values that are plain strings and whose field ID
141
+ is in the provided set of ADF field IDs.
142
+ """
143
+ result = {}
144
+ for key, value in fields.items():
145
+ if key in adf_field_ids and isinstance(value, str):
146
+ result[key] = markdown_to_adf(value)
147
+ else:
148
+ result[key] = value
149
+ return result
150
+
151
+
152
+ def _extract_text_fallback(adf: dict[str, Any]) -> str:
153
+ """Extract plain text from ADF as a last resort."""
154
+ texts: list[str] = []
155
+
156
+ def walk(node: Any) -> None:
157
+ if isinstance(node, dict):
158
+ if node.get("type") == "text" and "text" in node:
159
+ texts.append(node["text"])
160
+ for child in node.get("content", []):
161
+ walk(child)
162
+ elif isinstance(node, list):
163
+ for item in node:
164
+ walk(item)
165
+
166
+ walk(adf)
167
+ return " ".join(texts).strip() or "(none)"