๐Ÿข a2atlassian

Agent-to-Atlassian

Give AI agents access to Jira and Confluence. Save credentials once, work from anywhere.

Jira + Confluence · read-only by default · pre-configured connections · compact TSV output

PyPI Python License CI MCP Registry

Quick Start · MCP Tools · Security · Comparison · Setup

--- ``` Agent: "What's the status of PROJ-42? Add a comment with the progress update." โ†“ a2atlassian โ†’ get issue, add comment, transition to In Progress โ†“ Agent: "Done โ€” PROJ-42 updated and moved to In Progress." ``` ## Why a2atlassian? Existing Atlassian MCP servers (Rovo, sooperset) require Docker, `.env` files, and `mcp-remote` bridges. They dump 72 tools into agent context and have [known quirks](docs/) that silently fail. a2atlassian fixes all of that: - **No Docker** โ€” `pip install a2atlassian` and you're done - **Pre-configured connections** โ€” define projects in `.mcp.json` with `--register`, agent works immediately - **Read-only by default** โ€” write access is opt-in per connection - **Connection scoping** โ€” `--scope` limits which projects an agent can see - **Compact output** โ€” TSV for lists (30-60% fewer tokens), JSON for single entities - **Dynamic tool loading** โ€” MCP clients that support deferred tools (e.g., Claude Code) load tools on demand, keeping context lean - **Error enrichment** โ€” bad field names get suggestions, JQL typos get corrections, quirks get auto-fixed - **Secrets stay in env** โ€” `${ATLASSIAN_TOKEN}` in configs, expanded only at runtime > **Scope today:** full Jira surface (issues, comments, sprints, boards, worklogs, links, versions, fields, watchers, projects) and Confluence core (pages CRUD, search, metadata-only writes). ## Quick Start ```bash # Recommended โ€” installs globally as a CLI tool uv tool install a2atlassian # Or with pip pip install a2atlassian ``` ### As an MCP Server (recommended) **Claude Code** (with pre-configured connection): ```bash claude mcp add -s user a2atlassian -- uvx --from a2atlassian a2atlassian-mcp \ --register myproject https://mysite.atlassian.net user@company.com '${ATLASSIAN_TOKEN}' ``` **Claude Code** (minimal โ€” agent calls `login` on demand): ```bash claude mcp add -s user a2atlassian -- uvx --from a2atlassian a2atlassian-mcp ``` **Claude Desktop / Cursor / any MCP client** (`.mcp.json`): ```json { "mcpServers": { "a2atlassian": { "command": "uvx", "args": [ "--from", "a2atlassian", "a2atlassian-mcp", "--register", "myproject", "https://mysite.atlassian.net", "user@company.com", "${ATLASSIAN_TOKEN}" ], "env": { "ATLASSIAN_TOKEN": "your-api-token-here" } } } } ``` **Multiple projects:** ```json { "args": [ "--from", "a2atlassian", "a2atlassian-mcp", "--register", "myproject", "https://mysite.atlassian.net", "user@a.com", "${TOKEN_A}", "--register", "personal", "https://personal.atlassian.net", "user@b.com", "${TOKEN_B}" ] } ``` **Scoped connections** (limit agent to specific saved projects): ```json { "args": ["--from", "a2atlassian", "a2atlassian-mcp", "--scope", "myproject"] } ``` `--register` creates ephemeral in-memory connections (process lifetime, no files written). `--scope` filters which saved connections are visible. Both limit blast radius. ### As a CLI ```bash # Save a connection (validates by calling /myself) a2atlassian login -c myproject \ --url https://mysite.atlassian.net \ --email user@company.com \ --token "$ATLASSIAN_TOKEN" # Same, pulling the token from 1Password via `op` a2atlassian login -c myproject \ --url https://mysite.atlassian.net \ --email user@company.com \ --token "op://Personal/Atlassian/token" # Enable writes a2atlassian login -c myproject \ --url https://mysite.atlassian.net \ --email user@company.com \ --token "$ATLASSIAN_TOKEN" \ --no-read-only # List / remove connections a2atlassian connections a2atlassian logout -c myproject ``` Tokens accept three forms: literal value, `${ENV_VAR}` reference, or `op://vault/item/field` (resolved via the 1Password CLI at runtime). ## MCP Tools ### Connection Management | Tool | Description | |------|-------------| | `login` | Save a connection โ€” validates by calling /myself first | | `logout` | Remove a saved connection | | `list_connections` | List connections (no secrets exposed) | ### Jira โ€” Read | Tool | Description | |------|-------------| | `jira_get_issue` | Get issue by key โ€” full fields, status, assignee | | `jira_search` | Search by JQL with pagination โ€” compact TSV output by default | | `jira_search_count` | Count-only JQL โ€” cheap pre-check for "is this going to be huge?" | | `jira_search_fields` | Discover custom-field IDs by name | | `jira_get_field_options` | List allowed values for a select / multi-select field | | `jira_get_comments` | Get all comments for an issue | | `jira_get_worklogs` | Get all worklogs for an issue | | `jira_get_transitions` | Discover available status transitions | | `jira_get_link_types` | List available issue-link types | | `jira_get_watchers` | List watchers for an issue | | `jira_get_projects` | List projects accessible to the connection | | `jira_get_project_metadata` | Fetch creation metadata (issue types, required fields) | | `jira_get_user_profile` | Resolve an email/accountId to a full user profile | | `jira_get_boards` | List agile boards in a project | | `jira_get_board_issues` | Issues on a board (paginated) | | `jira_get_sprints` | List sprints on a board | | `jira_get_sprint_issues` | Issues in a sprint (paginated) | ### Jira โ€” Write (requires read-write connection) | Tool | Description | |------|-------------| | `jira_create_issue` | Create a new issue | | `jira_update_issue` | Update fields on an existing issue | | `jira_delete_issue` | Delete an issue | | `jira_transition_issue` | Move issue to a new status | | `jira_add_comment` | Add comment (wiki markup, API v2) | | `jira_edit_comment` | Update existing comment | | `jira_add_worklog` | Log time on an issue | | `jira_create_issue_link` | Link two issues | | `jira_remove_issue_link` | Remove an issue link | | `jira_set_watchers` | Replace the watcher set on an issue | | `jira_create_sprint` | Create a sprint on a board | | `jira_update_sprint` | Update sprint state / dates | | `jira_add_issues_to_sprint` | Move issues into a sprint | | `jira_create_version` | Create a project version | ### Confluence โ€” Read | Tool | Description | |------|-------------| | `confluence_get_page` | Fetch a page by id (body storage, version, space) | | `confluence_get_page_children` | List direct children of a page (paginated) | | `confluence_search` | CQL search; minimal per-match rows | ### Confluence โ€” Write (requires read-write connection) | Tool | Description | |------|-------------| | `confluence_upsert_pages` | Batch create-or-update with preserve-on-omit body semantics + per-page status + partial-failure shape | | `confluence_set_page_properties` | Metadata-only write (page_width, emoji, labels) โ€” physically cannot touch body or title | ### Output Formats All tools accept a `format` parameter: | Format | Default for | Description | |--------|-------------|-------------| | `toon` | Lists (search, comments) | TSV with header โ€” shape once, data many. 30-60% fewer tokens than JSON | | `json` | Single entities (get_issue) | Standard JSON with metadata envelope | List responses use a compact TSV-style format (header row + tab-separated values) inspired by [TOON](https://toonformat.dev). This is the same approach a2db uses โ€” column names appear once, then just values. For a 50-issue search result, this typically saves **40-60% of tokens** compared to JSON. **TSV example (search results):** ``` # search (23 results, 50ms, truncated: False) key summary assignee status PROJ-142 Fix auth timeout Alice Smith In Progress PROJ-141 Add search filters Bob Jones To Do ``` **JSON example (single issue):** ```json { "data": {"key": "PROJ-142", "fields": {"summary": "Fix auth timeout", ...}}, "count": 1, "truncated": false, "time_ms": 85 } ``` ### Error Enrichment When something fails, a2atlassian tells the agent what to do: ``` Field 'asignee' does not exist Did you mean: assignee? ``` ``` Connection 'myproject' is read-only. Run: a2atlassian login -p myproject --read-only false ``` **Quirks handled automatically:** - Assignee requires display name (not `712020:` account IDs) โ€” auto-detected with hint - Parent field must be plain string โ€” `{"key": "PROJ-14"}` normalized to `"PROJ-14"` silently - Issue type conversion not supported via API โ€” clear Jira UI instructions provided ## Security ### Read-Only by Default Every connection starts read-only. Write tools check the connection flag before executing: ``` Connection 'myproject' is read-only. Re-run 'a2atlassian login -p myproject --read-only false' to enable writes. ``` The human operator controls write access โ€” not the agent. ### Credential Storage Connections saved via `login` go to `~/.config/a2atlassian/connections/` as TOML files: - **File permissions:** `0600` (owner read/write only) - **`${ATLASSIAN_TOKEN}` syntax** โ€” env var references stored literally, expanded at runtime - **No secrets in output** โ€” `list_connections` shows project name, URL, and mode โ€” never tokens - **Ephemeral mode** โ€” `--register` keeps credentials in memory only, never written to disk ### Connection Scoping Use `--scope` to limit which saved connections a specific MCP instance can access: ```bash # Project config โ€” only myproject visible, even if other connections are saved uvx --from a2atlassian a2atlassian-mcp --scope myproject ``` Project-level MCP configs (`.claude/mcp.json`) override global configs โ€” each repo sees only its own connections. ### Rate Limiting Built-in retry with exponential backoff for Atlassian's rate limits (429) and transient server errors (500). Two retries at 1s and 3s intervals before surfacing the error. ## Comparison | Feature | a2atlassian | Rovo (official) | sooperset/mcp-atlassian | |---------|-------------|-----------------|------------------------| | **Setup** | `pip install` | OAuth + Docker | Docker + .env + mcp-remote | | **Tools in context** | ~35 (loaded on demand) | ~72 | ~72 | | **Connection management** | TOML + `--register` + `--scope` | Per-session OAuth | .env file | | **Multi-project** | Yes (scoped) | No | One .env per setup | | **Read-only default** | Yes (per-connection) | No | No | | **Output format** | TSV + JSON | JSON | JSON | | **Error enrichment** | Field suggestions, quirk fixes | Generic errors | Generic errors | | **Quirk handling** | Auto-fix (assignee, parent) | Documented workarounds | Documented workarounds | | **Rate limiting** | Built-in retry | No | No | | **CLI** | Yes | No | No | | **License** | Apache 2.0 | Proprietary | MIT | ## Roadmap **Shipped:** Jira full surface (v0.3.0) ยท Confluence core + markdown-to-storage with full CommonMark + GFM fidelity (v0.4.0, v0.5.2) ยท 1Password `op://` token refs (v0.5.1) ยท metadata-only Confluence writes + preserve-on-omit body semantics (v0.5.2). **Next:** `confluence_delete_page`, Confluence comments + attachments, Confluence integration-test path. Backlog in [`TODO.md`](TODO.md). ## Setup by Environment ### Local (macOS / Linux) ```bash # Recommended uv tool install a2atlassian # Or with pip pip install a2atlassian # CLI a2atlassian login -p myproject --url https://mysite.atlassian.net --email me@co.com --token "$TOKEN" # Or add as MCP server (see Quick Start) ``` ### CI / Automation ```bash uv tool install a2atlassian # Pre-configured โ€” no login needed uvx --from a2atlassian a2atlassian-mcp --register ci https://mysite.atlassian.net ci-user@co.com "${CI_ATLASSIAN_TOKEN}" ``` ## Development ```bash make bootstrap # Install deps + pnpm + git hooks make check # Lint + test + coverage-diff + security (full gate) make test # Tests with coverage make lint # agent-harness + jscpd + actionlint (never modifies files) make fix # Auto-fix + lint make similar # Advisory: report similarly-named functions/classes ``` Linters: `ruff` + `ty` (via agent-harness), `yamllint`, `jscpd` (copy-paste detection via pnpm), `actionlint` (GitHub Actions workflows). Pre-commit hooks run `agent-harness fix` + lint on every commit. Install `pnpm` and `actionlint` via `brew install pnpm actionlint`. ## License Apache 2.0 ---

๐Ÿข Agent-first Atlassian access since 2025.

Built by Denis Tomilin