Atlassian Attachments
MCP server for downloading, uploading, and deleting Jira Cloud attachments by ID.
Open source Open in the app JSON README (API)
About
MCP server for downloading, uploading, and deleting Jira Cloud attachments by ID.
Details
- Kind
- MCP servers
- Topic
- Productivity
- Publisher
- alyiox
- Origin
- official
- Category
- ferramentas
- Transport
- local
- Version
- 0.3.0
- Last push
- 2026-08-24T05:43:15Z
- Repository state
- ativo
- Language
- Python
- License
- MIT
- Added
- 2026-08-29 03:02:24
- Updated
- 2026-08-29 03:02:24
- Origin id
io.github.alyiox/mcp-atlassian-attachments
README
# MCP Atlassian Attachments
[](https://github.com/alyiox/mcp-atlassian-attachments/actions/workflows/ci.yml)
[](https://pypi.org/project/mcp-atlassian-attachments/)
[](https://www.python.org/downloads/)
[](LICENSE)
<!-- mcp-name: io.github.alyiox/mcp-atlassian-attachments -->
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for downloading Jira Cloud attachments by attachment ID to local disk, and uploading local files as attachments to a Jira issue.
> **Supplement to the official Atlassian MCP server.**
> The [official Atlassian MCP server](https://mcp.atlassian.com/v1/mcp) covers search, issue management, and content operations but does not move attachment files between Jira and local disk. This server fills that gap.
**Requirements:** Python 3.13+, an Atlassian Cloud account, and an API token with at least the `read:jira-work` scope. Uploading also needs `write:jira-work`.
## Authentication
Scoped tokens are recommended to limit access to exactly the permissions needed.
> **Note:** The granular `read:attachment:jira` scope is not sufficient — Jira's attachment metadata endpoint (`/rest/api/3/attachment/{id}`) requires `read:jira-work` to resolve issue-level permissions. A classic (unscoped) API token also works.
### Create an API token
1. Go to [id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
2. Click **Create API token**
3. Choose **"Create API token with scopes"** and select `read:jira-work` (add `write:jira-work` to upload), **or** choose **"Classic API token"** for full access
4. Copy the generated token
## Quick start
The fastest way to try the server is with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). Set the three required environment variables and run:
**From the published package** (no clone needed):
```bash
ATLASSIAN_SITE_URL=https://yourorg.atlassian.net \
ATLASSIAN_EMAIL=your.email@example.com \
ATLASSIAN_API_TOKEN=your-api-token \
npx -y @modelcontextprotocol/inspector uvx mcp-atlassian-attachments
```
**From a local clone:**
```bash
ATLASSIAN_SITE_URL=https://yourorg.atlassian.net \
ATLASSIAN_EMAIL=your.email@example.com \
ATLASSIAN_API_TOKEN=your-api-token \
npx -y @modelcontextprotocol/inspector uv run mcp-atlassian-attachments
```
## Configuration
Set environment variables or create a config file. Environment variables take priority.
**Environment variables:**
```bash
export ATLASSIAN_SITE_URL="https://yourorg.atlassian.net"
export ATLASSIAN_EMAIL="your.email@example.com"
export ATLASSIAN_API_TOKEN="your-api-token"
```
**Config file** (`~/.config/mcp-atlassian-attachments/config.json`):
```json
{
"site_url": "https://yourorg.atlassian.net",
"email": "your.email@example.com",
"api_token": "your-api-token"
}
```
## Tools
| Tool | Description | Required params |
|------|-------------|-----------------|
| **`download_jira_attachment`** | Download a Jira attachment by ID. | `attachment_id`, `output_dir` |
| **`upload_jira_attachment`** | Upload a local file as an attachment on a Jira issue. | `issue_key`, `file_path` |
| **`get_jira_attachment_reference`** | Resolve an existing attachment into an ADF media node. | `attachment_id` |
| **`delete_jira_attachment`** | Permanently delete an attachment by ID. | `attachment_id` |
### `download_jira_attachment`
| Parameter | Type | Description |
|-----------|------|-------------|
| `attachment_id` | string | Jira attachment ID |
| `output_dir` | string | Local directory for the saved file. Created automatically if it does not exist. |
| `filename` | string (optional) | Override filename. Uses metadata filename when omitted. |
| `overwrite` | bool (optional) | Replace an existing file. Defaults to `false`. |
Returns:
```json
{
"product": "jira",
"attachmentId": "439535",
"filename": "screenshot.png",
"mimeType": "image/png",
"size": 496724,
"path": "/your/output/dir/screenshot.png",
"attachmentUrl": "https://yourorg.atlassian.net/rest/api/3/attachment/content/439535"
}
```
### `upload_jira_attachment`
| Parameter | Type | Description |
|-----------|------|-------------|
| `issue_key` | string | Jira issue key or ID, for example `PROJ-123` |
| `file_path` | string | Path to the local file to upload. Must exist. |
| `filename` | string (optional) | Name to store in Jira. Uses the local filename when omitted. |
Returns:
```json
{
"product": "jira",
"issueKey": "PROJ-123",
"attachmentId": "600100",
"filename": "report.png",
"mimeType": "image/png",
"size": 24581,
"path": "/your/local/dir/report.png",
"attachmentUrl": "https://yourorg.atlassian.net/rest/api/3/attachment/content/600100",
"mediaId": "6df94659-caa7-4561-bbe3-6384ec534bba"
}
```
Each call adds a new attachment; Jira does not replace a same-named file. Uploads larger than the site attachment size limit fail with a `413`.
### `get_jira_attachment_reference`
| Parameter | Type | Description |
|-----------|------|-------------|
| `attachment_id` | string | Jira attachment ID |
Read-only. Returns `mediaId` for an attachment that is **already** on an issue:
```json
{
"product": "jira",
"attachmentId": "439535",
"filename": "screenshot.png",
"mimeType": "image/png",
"size": 496724,
"attachmentUrl": "https://yourorg.atlassian.net/rest/api/3/attachment/content/439535",
"mediaId": "6df94659-caa7-4561-bbe3-6384ec534bba"
}
```
After an upload you do **not** need this tool — `upload_jira_attachment` already returns `mediaId`, so embedding costs no extra call.
### `delete_jira_attachment`
| Parameter | Type | Description |
|-----------|------|-------------|
| `attachment_id` | string | Jira attachment ID to delete permanently |
Needs `write:jira-work`. Metadata is read before the delete, so the result reports what was removed and a missing ID fails without attempting anything:
```json
{
"product": "jira",
"attachmentId": "481718",
"filename": "delete-me.txt",
"mimeType": "text/plain",
"size": 31,
"deleted": true
}
```
> **There is no undo.** Jira deletes the file outright, and a second call to the same ID fails with `404`.
>
> **Deleting does not clean up references.** If the attachment was embedded in a description or comment, the ADF media node stays exactly where it was and becomes a dangling reference — verified against a live issue. Remove the node yourself if you delete a file that was referenced.
## Referencing an attachment in a description or comment
Uploading a file attaches it, but nothing appears inline in the description or a comment. Rendering it requires an [ADF](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/) media node — and that node identifies the file by its **Media Services UUID**, not by the numeric attachment ID:
```json
{
"type": "mediaSingle",
"attrs": { "layout": "center" },
"content": [
{
"type": "media",
"attrs": {
"type": "file",
"id": "6df94659-caa7-4561-bbe3-6384ec534bba",
"collection": "",
"alt": "report.png"
}
}
]
}
```
No Jira attachment metadata endpoint exposes that UUID, which is why this server resolves it: `GET /rest/api/3/attachment/content/{id}` answers with a `303` to `https://api.media.atlassian.com/file/{mediaId}/binary`, so reading the `Location` header without following the redirect yields the UUID at no download cost. The UUID exists as soon as the file is attached — it does not depend on the attachment being referenced anywhere.
Both tools therefore return `mediaId`. Writing the node into the issue is deliberately left to a Jira content tool such as the official Atlassian MCP server, whose `addCommentToJiraIssue` and `editJiraIssue` accept `contentFormat: "adf"` — this server moves files and does not edit issue content:
1. `upload_jira_attachment` → take `mediaId` from the result
2. Post a comment with an ADF body wrapping a media node built from it:
```json
{
"version": 1,
"type": "doc",
"content": [
{ "type": "paragraph", "content": [{ "type": "text", "text": "Latest run:" }] },
{ "type": "mediaSingle", "attrs": { "layout": "center" }, "content": [ "...the media node..." ] }
]
}
```
For a description, read the existing ADF, append the node, and write the whole document back — `PUT /rest/api/3/issue/{key}` replaces the field rather than appending to it.
Notes:
- A bare `media` node is not rendered; it must be wrapped in `mediaSingle` (single file), `mediaGroup` (file-card list), or `mediaInline` (inline chip).
- `collection` is `""` for Jira issue attachments.
- Optional `width`/`height` on the media node set the intrinsic pixel size, and are only honoured inside `mediaSingle`.
- `mediaId` is best-effort: if the redirect cannot be read it comes back `null`, and the upload itself still succeeds.
### Simpler alternative: v2 wiki markup
The older v2 endpoints still accept wiki markup and convert it to ADF server-side, resolving the attachment **by filename** — so no media UUID is needed:
```bash
POST /rest/api/2/issue/PROJ-123/comment
{ "body": "See attached: !report.png|thumbnail!" }
```
Jira stores that as a proper `media` node with the correct UUID filled in. Confirmed working on Jira Cloud as of August 2026.
Two behaviours to know about, both verified against a live issue:
- **Dimensions are a fixed placeholder, not the real ones.** The conversion always writes `width: 200, height: 183` regardless of the image — a 64x48 and an 8x8 PNG both came back as 200x183. `!file|width=300!` sets the width but leaves height at 183, so the aspect ratio is wrong unless you write the ADF yourself.
- **Duplicate filenames resolve to the oldest.** With two attachments both named `ambiguous.png`, `!ambiguous.png!` resolved to the *first* one uploaded. Re-uploading under the same name to "update" an image will keep showing the old file.
Prefer `mediaId` when you want to be explicit: it is unambiguous with duplicate names, it lets you set true dimensions, it works with v3/ADF (which is what the official Atlassian MCP server writes), and it does not depend on a legacy path Atlassian may eventually retire.
## MCP host configuration
Add the following to your MCP host's config file. The JSON is the same for Cursor (`.cursor/mcp.json`), Claude Desktop (`claude_desktop_config.json`), and Claude Code (`.claude.json`).
```json
{
"mcpServers": {
"atlassian": {
"command": "uvx",
"args": ["mcp-atlassian-attachments"],
"env": {
"ATLASSIAN_SITE_URL": "https://yourorg.atlassian.net",
"ATLASSIAN_EMAIL": "your.email@example.com",
"ATLASSIAN_API_TOKEN": "your-api-token"
}
}
}
}
```
## Security
- `ATLASSIAN_API_TOKEN` is never logged or included in error messages.
- The computed `Authorization` header is never exposed in tool output or errors.
- File writes are confined to the provided `output_dir`.
- Filenames are sanitized to prevent path traversal, both on download and on the name sent to Jira.
- Uploads read exactly the file at `file_path` and send nothing else.
## Tests
```bash
uv run pytest tests/ -v
```
## Roadmap
- **`download_confluence_attachment_tool`** — Confluence Cloud uses a different API (`/wiki/api/v2/`) and a different identifier model. Planned for a future release.
## License
MIT. See [LICENSE](LICENSE).