{
  "markdown": "# MCP Atlassian Attachments\n\n[![CI](https://github.com/alyiox/mcp-atlassian-attachments/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/alyiox/mcp-atlassian-attachments/actions/workflows/ci.yml)\n[![PyPI](https://img.shields.io/pypi/v/mcp-atlassian-attachments.svg)](https://pypi.org/project/mcp-atlassian-attachments/)\n[![Python 3.13+](https://img.shields.io/badge/python-3.13%2B-blue.svg)](https://www.python.org/downloads/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n\n<!-- mcp-name: io.github.alyiox/mcp-atlassian-attachments -->\n\nA [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.\n\n> **Supplement to the official Atlassian MCP server.**\n> 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.\n\n**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`.\n\n## Authentication\n\nScoped tokens are recommended to limit access to exactly the permissions needed.\n\n> **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.\n\n### Create an API token\n\n1. Go to [id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens)\n2. Click **Create API token**\n3. 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\n4. Copy the generated token\n\n## Quick start\n\nThe fastest way to try the server is with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). Set the three required environment variables and run:\n\n**From the published package** (no clone needed):\n\n```bash\nATLASSIAN_SITE_URL=https://yourorg.atlassian.net \\\nATLASSIAN_EMAIL=your.email@example.com \\\nATLASSIAN_API_TOKEN=your-api-token \\\nnpx -y @modelcontextprotocol/inspector uvx mcp-atlassian-attachments\n```\n\n**From a local clone:**\n\n```bash\nATLASSIAN_SITE_URL=https://yourorg.atlassian.net \\\nATLASSIAN_EMAIL=your.email@example.com \\\nATLASSIAN_API_TOKEN=your-api-token \\\nnpx -y @modelcontextprotocol/inspector uv run mcp-atlassian-attachments\n```\n\n## Configuration\n\nSet environment variables or create a config file. Environment variables take priority.\n\n**Environment variables:**\n\n```bash\nexport ATLASSIAN_SITE_URL=\"https://yourorg.atlassian.net\"\nexport ATLASSIAN_EMAIL=\"your.email@example.com\"\nexport ATLASSIAN_API_TOKEN=\"your-api-token\"\n```\n\n**Config file** (`~/.config/mcp-atlassian-attachments/config.json`):\n\n```json\n{\n  \"site_url\": \"https://yourorg.atlassian.net\",\n  \"email\": \"your.email@example.com\",\n  \"api_token\": \"your-api-token\"\n}\n```\n\n## Tools\n\n| Tool | Description | Required params |\n|------|-------------|-----------------|\n| **`download_jira_attachment`** | Download a Jira attachment by ID. | `attachment_id`, `output_dir` |\n| **`upload_jira_attachment`** | Upload a local file as an attachment on a Jira issue. | `issue_key`, `file_path` |\n| **`get_jira_attachment_reference`** | Resolve an existing attachment into an ADF media node. | `attachment_id` |\n| **`delete_jira_attachment`** | Permanently delete an attachment by ID. | `attachment_id` |\n\n### `download_jira_attachment`\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `attachment_id` | string | Jira attachment ID |\n| `output_dir` | string | Local directory for the saved file. Created automatically if it does not exist. |\n| `filename` | string (optional) | Override filename. Uses metadata filename when omitted. |\n| `overwrite` | bool (optional) | Replace an existing file. Defaults to `false`. |\n\nReturns:\n\n```json\n{\n  \"product\": \"jira\",\n  \"attachmentId\": \"439535\",\n  \"filename\": \"screenshot.png\",\n  \"mimeType\": \"image/png\",\n  \"size\": 496724,\n  \"path\": \"/your/output/dir/screenshot.png\",\n  \"attachmentUrl\": \"https://yourorg.atlassian.net/rest/api/3/attachment/content/439535\"\n}\n```\n\n### `upload_jira_attachment`\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `issue_key` | string | Jira issue key or ID, for example `PROJ-123` |\n| `file_path` | string | Path to the local file to upload. Must exist. |\n| `filename` | string (optional) | Name to store in Jira. Uses the local filename when omitted. |\n\nReturns:\n\n```json\n{\n  \"product\": \"jira\",\n  \"issueKey\": \"PROJ-123\",\n  \"attachmentId\": \"600100\",\n  \"filename\": \"report.png\",\n  \"mimeType\": \"image/png\",\n  \"size\": 24581,\n  \"path\": \"/your/local/dir/report.png\",\n  \"attachmentUrl\": \"https://yourorg.atlassian.net/rest/api/3/attachment/content/600100\",\n  \"mediaId\": \"6df94659-caa7-4561-bbe3-6384ec534bba\"\n}\n```\n\nEach 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`.\n\n### `get_jira_attachment_reference`\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `attachment_id` | string | Jira attachment ID |\n\nRead-only. Returns `mediaId` for an attachment that is **already** on an issue:\n\n```json\n{\n  \"product\": \"jira\",\n  \"attachmentId\": \"439535\",\n  \"filename\": \"screenshot.png\",\n  \"mimeType\": \"image/png\",\n  \"size\": 496724,\n  \"attachmentUrl\": \"https://yourorg.atlassian.net/rest/api/3/attachment/content/439535\",\n  \"mediaId\": \"6df94659-caa7-4561-bbe3-6384ec534bba\"\n}\n```\n\nAfter an upload you do **not** need this tool — `upload_jira_attachment` already returns `mediaId`, so embedding costs no extra call.\n\n### `delete_jira_attachment`\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `attachment_id` | string | Jira attachment ID to delete permanently |\n\nNeeds `write:jira-work`. Metadata is read before the delete, so the result reports what was removed and a missing ID fails without attempting anything:\n\n```json\n{\n  \"product\": \"jira\",\n  \"attachmentId\": \"481718\",\n  \"filename\": \"delete-me.txt\",\n  \"mimeType\": \"text/plain\",\n  \"size\": 31,\n  \"deleted\": true\n}\n```\n\n> **There is no undo.** Jira deletes the file outright, and a second call to the same ID fails with `404`.\n>\n> **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.\n\n## Referencing an attachment in a description or comment\n\nUploading 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:\n\n```json\n{\n  \"type\": \"mediaSingle\",\n  \"attrs\": { \"layout\": \"center\" },\n  \"content\": [\n    {\n      \"type\": \"media\",\n      \"attrs\": {\n        \"type\": \"file\",\n        \"id\": \"6df94659-caa7-4561-bbe3-6384ec534bba\",\n        \"collection\": \"\",\n        \"alt\": \"report.png\"\n      }\n    }\n  ]\n}\n```\n\nNo 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.\n\nBoth 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:\n\n1. `upload_jira_attachment` → take `mediaId` from the result\n2. Post a comment with an ADF body wrapping a media node built from it:\n\n```json\n{\n  \"version\": 1,\n  \"type\": \"doc\",\n  \"content\": [\n    { \"type\": \"paragraph\", \"content\": [{ \"type\": \"text\", \"text\": \"Latest run:\" }] },\n    { \"type\": \"mediaSingle\", \"attrs\": { \"layout\": \"center\" }, \"content\": [ \"...the media node...\" ] }\n  ]\n}\n```\n\nFor 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.\n\nNotes:\n\n- A bare `media` node is not rendered; it must be wrapped in `mediaSingle` (single file), `mediaGroup` (file-card list), or `mediaInline` (inline chip).\n- `collection` is `\"\"` for Jira issue attachments.\n- Optional `width`/`height` on the media node set the intrinsic pixel size, and are only honoured inside `mediaSingle`.\n- `mediaId` is best-effort: if the redirect cannot be read it comes back `null`, and the upload itself still succeeds.\n\n### Simpler alternative: v2 wiki markup\n\nThe 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:\n\n```bash\nPOST /rest/api/2/issue/PROJ-123/comment\n{ \"body\": \"See attached: !report.png|thumbnail!\" }\n```\n\nJira stores that as a proper `media` node with the correct UUID filled in. Confirmed working on Jira Cloud as of August 2026.\n\nTwo behaviours to know about, both verified against a live issue:\n\n- **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.\n- **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.\n\nPrefer `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.\n\n## MCP host configuration\n\nAdd 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`).\n\n```json\n{\n  \"mcpServers\": {\n    \"atlassian\": {\n      \"command\": \"uvx\",\n      \"args\": [\"mcp-atlassian-attachments\"],\n      \"env\": {\n        \"ATLASSIAN_SITE_URL\": \"https://yourorg.atlassian.net\",\n        \"ATLASSIAN_EMAIL\": \"your.email@example.com\",\n        \"ATLASSIAN_API_TOKEN\": \"your-api-token\"\n      }\n    }\n  }\n}\n```\n\n## Security\n\n- `ATLASSIAN_API_TOKEN` is never logged or included in error messages.\n- The computed `Authorization` header is never exposed in tool output or errors.\n- File writes are confined to the provided `output_dir`.\n- Filenames are sanitized to prevent path traversal, both on download and on the name sent to Jira.\n- Uploads read exactly the file at `file_path` and send nothing else.\n\n## Tests\n\n```bash\nuv run pytest tests/ -v\n```\n\n## Roadmap\n\n- **`download_confluence_attachment_tool`** — Confluence Cloud uses a different API (`/wiki/api/v2/`) and a different identifier model. Planned for a future release.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n",
  "bytes": 11679,
  "sha": "b5cde3b60106e74979bde8b4313e61c5ad05751ebe01fcbf888adda2ecf7ea3e",
  "repo_slug": "alyiox/mcp-atlassian-attachments",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_alyiox_mcp_atlassian_attachmen_76eb6707/readme"
}