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.
- jira2mcp-0.1.0/PKG-INFO +135 -0
- jira2mcp-0.1.0/README.md +121 -0
- jira2mcp-0.1.0/pyproject.toml +33 -0
- jira2mcp-0.1.0/src/jira2mcp/__init__.py +32 -0
- jira2mcp-0.1.0/src/jira2mcp/adf.py +167 -0
- jira2mcp-0.1.0/src/jira2mcp/formatters.py +323 -0
- jira2mcp-0.1.0/src/jira2mcp/models.py +218 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/__init__.py +22 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/add_link.py +80 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/attachment.py +135 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/comment.py +54 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/comments.py +93 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/create.py +91 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/delete_link.py +54 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/edit.py +89 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/fields.py +137 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/jql_syntax_prompt.py +172 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/link_types_resource.py +29 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/projects.py +74 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/read.py +74 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/search.py +81 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/server.py +5 -0
- jira2mcp-0.1.0/src/jira2mcp/tools/users.py +62 -0
- jira2mcp-0.1.0/src/jira2mcp/utils.py +77 -0
jira2mcp-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
jira2mcp-0.1.0/README.md
ADDED
|
@@ -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)"
|