Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@
{
"name": "bitwarden-atlassian-tools",
"source": "./plugins/bitwarden-atlassian-tools",
"version": "2.4.0",
"description": "Read-only Atlassian access via MCP server with deep Jira issue research skill, JQL search, Confluence pages, CQL search, and attachments"
"version": "2.5.0",
"description": "Atlassian access via MCP server with deep Jira issue research skill, JQL search, Confluence pages, CQL search, attachments, and opt-in Jira write tools"
},
{
"name": "bitwarden-security-engineer",
Expand Down
3 changes: 3 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.2",
"words": [
"accum",
"acli",
"adf",
"ADRs",
"AKIA",
Expand All @@ -27,6 +28,7 @@
"CODEOWNERS",
"Confluence",
"CQL",
"createmeta",
"customfield",
"cvss",
"Dashlane",
Expand Down Expand Up @@ -66,6 +68,7 @@
"issueIdOrKey",
"issuelinks",
"issuetype",
"issuetypes",
"Jira",
"JQL",
"keyserver",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A curated collection of plugins for AI-assisted development at Bitwarden. Enable
| [bitwarden-ai-telemetry](plugins/bitwarden-ai-telemetry/) | 1.1.0 | Claude Code hooks emitting metadata-only AI-usage telemetry (identity, git-linkage, MCP) via OTLP |
| [bitwarden-tech-lead](plugins/bitwarden-tech-lead/) | 3.0.0 | Tech lead for technical planning, architecture coherence, and surfacing patterns to Technical Strategy Ideas |
| [bitwarden-shepherd](plugins/bitwarden-shepherd/) | 1.0.1 | Champion of a technical strategy β€” shepherds a TSI through evaluation into the funnel, then through to adoption |
| [bitwarden-atlassian-tools](plugins/bitwarden-atlassian-tools/) | 2.4.0 | Read-only Atlassian access via MCP server with deep Jira issue research skill |
| [bitwarden-atlassian-tools](plugins/bitwarden-atlassian-tools/) | 2.5.0 | Atlassian access via MCP server with deep Jira issue research skill and opt-in Jira write tools |
| [bitwarden-code-review](plugins/bitwarden-code-review/) | 1.13.1 | Autonomous code review agent following Bitwarden engineering standards with GitHub integration |
| [bitwarden-delivery-tools](plugins/bitwarden-delivery-tools/) | 2.4.0 | Delivery lifecycle skills: initiative funnel navigation, work transitions, tech breakdowns and task decomposition, commits, PRs, preflight, labeling |
| [bitwarden-designer](plugins/bitwarden-designer/) | 0.1.0 | Product designer persona: Code of Conduct and 30/60/90 critique, critique facilitation; dispatches into bitwarden-design-tools |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "bitwarden-atlassian-tools",
"version": "2.4.0",
"description": "Read-only Atlassian access via MCP server with deep Jira issue research skill, JQL search, Confluence pages, CQL search, and attachments",
"version": "2.5.0",
"description": "Atlassian access via MCP server with deep Jira issue research skill, JQL search, Confluence pages, CQL search, attachments, and opt-in Jira write tools",
"author": {
"name": "Bitwarden"
},
"keywords": ["atlassian", "jira", "confluence", "mcp", "read-only", "cql"]
"keywords": ["atlassian", "jira", "confluence", "mcp", "cql"]
}
1 change: 1 addition & 0 deletions plugins/bitwarden-atlassian-tools/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"ATLASSIAN_CLOUD_ID": "${ATLASSIAN_CLOUD_ID}",
"ATLASSIAN_EMAIL": "${ATLASSIAN_EMAIL}",
"ATLASSIAN_JIRA_READ_ONLY_TOKEN": "${ATLASSIAN_JIRA_READ_ONLY_TOKEN}",
"ATLASSIAN_JIRA_WRITE_TOKEN": "${ATLASSIAN_JIRA_WRITE_TOKEN}",
Comment thread
SaintPatrck marked this conversation as resolved.
"ATLASSIAN_CONFLUENCE_READ_ONLY_TOKEN": "${ATLASSIAN_CONFLUENCE_READ_ONLY_TOKEN}"
}
}
Expand Down
9 changes: 9 additions & 0 deletions plugins/bitwarden-atlassian-tools/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to the Bitwarden Atlassian Tools plugin will be documented i
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.5.0] - 2026-08-03

### Added

- **`get_create_fields` MCP tool** (read-only) β€” reports a project's creatable issue types, and for a given type every field on the create screen with its field id, required flag, and allowed values. Lets callers discover a project's shape instead of hardcoding it, which matters because Bitwarden's projects differ: PM and SM expose an Acceptance criteria field, QA and VULN do not; VULN has no Story type; PLT's only creatable type is `Platform Initiative`.
- **`create_issue` MCP tool** (write, opt-in) β€” creates a single work item in any project. Defaults to a dry run that returns the exact payload without sending it; a live create requires an explicit `dryRun: false`. Carries no project-specific field knowledge: the issue type is a name Jira resolves, and anything beyond project/type/summary/description/parent/labels is passed through a `fields` object keyed by Jira field id.
- **`link_issues` MCP tool** (write, opt-in) β€” links two work items. For a dependency it takes `blockerKey` and `blockedKey` and applies Jira's inward/outward mapping internally, so the direction cannot be inverted by argument order. Also defaults to a dry run.
- **Optional `ATLASSIAN_JIRA_WRITE_TOKEN`** β€” write capability is opt-in per install. The write tools are always listed and their dry-run paths always work; without this variable, a live write refuses to execute.

## [2.4.0] - 2026-07-24

### Added
Expand Down
73 changes: 61 additions & 12 deletions plugins/bitwarden-atlassian-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

## Overview

Read-only Atlassian access via a custom MCP server providing Jira issue retrieval, JQL search, Confluence page reading, CQL search, and attachment downloads. All operations are read-only β€” the server never creates, updates, or deletes any Atlassian resource.
Atlassian access via a custom MCP server providing Jira issue retrieval, JQL search, Confluence page reading, CQL search, and attachment downloads.

Read access is the default and always available. Jira write access (creating work items and links) is **opt-in per install**: `create_issue` and `link_issues` are always listed and their dry-run preview always works, but without `ATLASSIAN_JIRA_WRITE_TOKEN` a live write refuses to execute. Confluence remains read-only with no write path.

## Installation

Expand All @@ -14,6 +16,10 @@ export ATLASSIAN_CLOUD_ID="your-cloud-id"
export ATLASSIAN_EMAIL="your-email@company.com"
export ATLASSIAN_JIRA_READ_ONLY_TOKEN="your-jira-scoped-token"
export ATLASSIAN_CONFLUENCE_READ_ONLY_TOKEN="your-confluence-scoped-token"

# Optional β€” enables the Jira write tools (create_issue, link_issues).
# Omit to keep this install read-only.
export ATLASSIAN_JIRA_WRITE_TOKEN="your-jira-write-scoped-token"
```

API requests are routed through the Atlassian API gateway (`api.atlassian.com`), which supports both classic and scoped API tokens.
Expand Down Expand Up @@ -67,17 +73,59 @@ The Jira Agile (Software) endpoints behind `list_boards`, `get_sprints`, and `ge

### Jira

| Tool | Purpose |
| ------------------------ | -------------------------------------------------------------------- |
| `get_issue` | Read a Jira issue by key or ID |
| `search_issues` | Search issues using JQL |
| `get_issue_comments` | Get comments for an issue |
| `get_issue_remote_links` | Get remote links for an issue (Confluence pages, PRs, external URLs) |
| `list_projects` | List accessible Jira projects |
| `list_boards` | List Agile boards, optionally filtered by project |
| `get_sprints` | List sprints for a board (filter by active/future/closed) |
| `get_sprint_issues` | List all issues in a sprint |
| `download_attachment` | Download a Jira attachment as Base64 |
| Tool | Purpose |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| `get_issue` | Read a Jira issue by key or ID |
| `search_issues` | Search issues using JQL |
| `get_issue_comments` | Get comments for an issue |
| `get_issue_remote_links` | Get remote links for an issue (Confluence pages, PRs, external URLs) |
| `list_projects` | List accessible Jira projects |
| `list_boards` | List Agile boards, optionally filtered by project |
| `get_sprints` | List sprints for a board (filter by active/future/closed) |
| `get_sprint_issues` | List all issues in a sprint |
| `download_attachment` | Download a Jira attachment as Base64 |
| `get_create_fields` | Report a project's creatable issue types, and a type's create-screen fields with ids, required flags, and allowed values |

### Jira (write, requires `ATLASSIAN_JIRA_WRITE_TOKEN`)

Both tools default to a dry run that returns the exact payload without sending it. A live write requires an explicit `dryRun: false`. Dry runs need no write token.

| Tool | Purpose |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create_issue` | Create one work item in any project. Carries no project-specific field knowledge: pass anything beyond the common core through `fields`, keyed by field id from `get_create_fields` |
| `link_issues` | Link two work items. For a dependency, takes `blockerKey` and `blockedKey` and maps them onto Jira's inward/outward sides internally so the direction cannot be inverted |

Write tokens need write scopes in addition to the read scopes above:

| Scope | Required for |
| ----------------------------- | --------------------------------------------------------------------------------- |
| `read:issue:jira` | `create_issue` |
| `read:issue:jira-software` | `create_issue` |
| `write:issue:jira` | `create_issue`, `link_issues` |
| `write:issue:jira-software` | `create_issue`, `link_issues` |
| `write:issue-link:jira` | `link_issues` |
| `write:comment:jira` | `create_issue`, `link_issues` (required even though neither tool sends a comment) |
| `write:comment.property:jira` | `create_issue` (required even though it never sends a comment) |
| `write:attachment:jira` | `create_issue` (required even though it never sends an attachment) |

Grant the **whole** set, not a subset β€” a token holding only some of them fails every write with `401 Unauthorized; scope does not match`. For a scoped write token covering both write tools, that's:

```
read:issue:jira
read:issue:jira-software
write:attachment:jira
write:comment.property:jira
write:comment:jira
write:issue-link:jira
write:issue:jira-software
write:issue:jira
```

`get_create_fields` needs no additional scope. It calls the createmeta endpoints, which the existing read-only token already satisfies.

Token scope is separate from Jira project permission. Creating also requires the **Create Issues** permission in the target project, and linking requires **Link Issues**. In a project where the user lacks Create Issues, Jira answers `You cannot create issues in this project`, which `get_create_fields` reports as an ordinary result rather than an error.

A leaked write token permits more than these two tools use: `write:comment:jira`, `write:comment.property:jira`, and `write:attachment:jira` are granted only because Atlassian rejects a narrower scope set, so the token can also add comments and attachments across every project the user can reach. Treat this token as higher-blast-radius than the read-only token and rotate it accordingly.

### Confluence

Expand All @@ -98,6 +146,7 @@ The MCP tools are available as `mcp__bitwarden-atlassian__<tool_name>`. Examples
- Search with JQL: `mcp__bitwarden-atlassian__search_issues` with `jql: "project = PROJ AND status = Open"`
- Read a Confluence page: `mcp__bitwarden-atlassian__get_confluence_page` with `pageId: "123456789"`
- Search Confluence: `mcp__bitwarden-atlassian__search_confluence_cql` with `cql: "space = EN AND text ~ \"search term\""`
- Preview a ticket before creating it: `mcp__bitwarden-atlassian__create_issue` with `project: "PM"`, `issueType: "Story"`, `summary: "Add CSV export to the item list"` β€” omit `dryRun` (defaults to `true`) to get the payload back without creating anything

## Skills

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@bitwarden/atlassian-tools-mcp",
"version": "1.0.0",
"description": "Read-only Jira MCP server for issue retrieval, JQL search, comments, and project discovery",
"description": "Jira MCP server for issue retrieval, JQL search, comments, project discovery, and opt-in issue creation/linking",
"type": "module",
"main": "build/index.js",
"bin": {
Expand All @@ -20,7 +20,6 @@
"jira",
"model-context-protocol",
"atlassian",
"read-only",
"jql"
],
"author": "Bitwarden",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

/**
* Atlassian MCP Server
* Read-only MCP server for Jira and Confluence integration with Claude Code
* MCP server for Jira and Confluence integration with Claude Code.
* Confluence is read-only; Jira read is always on, and Jira write is opt-in
* per install via ATLASSIAN_JIRA_WRITE_TOKEN.
*/

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
Expand Down Expand Up @@ -34,6 +36,25 @@ import listSpaces from "./tools/list-spaces.js";
// Cross-domain tools
import downloadAttachment from "./tools/download-attachment.js";

// Jira write tools
//
// These are registered unconditionally so that their dry-run path, which sends
// no request and needs no write credential, is available on a read-only install.
// Live execution is gated inside each handler on ATLASSIAN_JIRA_WRITE_TOKEN, so
// an install without that token behaves exactly as it does today.
//
// The alternative is gating registration itself on hasJiraWriteToken(), which
// hides the tools entirely from read-only installs. That is arguably the better
// production choice; it is not used here because it would also hide the dry-run
// preview, which should stay available so anyone can evaluate the write tools
// before minting a token.
// get_create_fields is read-only and useful on its own: it answers what a given
// project requires, which is what keeps the write tools free of any hardcoded
// per-project field knowledge.
import getCreateFields from "./tools/get-create-fields.js";
import createIssue from "./tools/create-issue.js";
import linkIssues from "./tools/link-issues.js";

const tools: ToolDefinition[] = [
getIssue,
getIssueComments,
Expand All @@ -50,6 +71,9 @@ const tools: ToolDefinition[] = [
searchConfluenceCql,
listSpaces,
downloadAttachment,
getCreateFields,
createIssue,
linkIssues,
];

async function main() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";

import { loadJiraConfig, hasJiraWriteToken } from "./auth.js";

const ENV_KEYS = [
"ATLASSIAN_CLOUD_ID",
"ATLASSIAN_EMAIL",
"ATLASSIAN_JIRA_READ_ONLY_TOKEN",
"ATLASSIAN_JIRA_WRITE_TOKEN",
] as const;

describe("Jira write-token access mode", () => {
const saved: Record<string, string | undefined> = {};

beforeEach(() => {
for (const key of ENV_KEYS) {
saved[key] = process.env[key];
delete process.env[key];
}
process.env.ATLASSIAN_CLOUD_ID = "test-cloud-id";
process.env.ATLASSIAN_EMAIL = "user@example.com";
process.env.ATLASSIAN_JIRA_READ_ONLY_TOKEN = "read-token";
});

afterEach(() => {
for (const key of ENV_KEYS) {
if (saved[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = saved[key];
}
}
});

it("reports no write token on a read-only install", () => {
expect(hasJiraWriteToken()).toBe(false);
});

it("still loads read config on a read-only install", () => {
expect(loadJiraConfig().apiToken).toBe("read-token");
});

it("defaults to the read token when no mode is given", () => {
process.env.ATLASSIAN_JIRA_WRITE_TOKEN = "write-token";

expect(loadJiraConfig().apiToken).toBe("read-token");
});

it("uses the write token in write mode", () => {
process.env.ATLASSIAN_JIRA_WRITE_TOKEN = "write-token";

expect(hasJiraWriteToken()).toBe(true);
expect(loadJiraConfig("write").apiToken).toBe("write-token");
});

it("throws naming the write variable when write mode has no token", () => {
expect(() => loadJiraConfig("write")).toThrow(/ATLASSIAN_JIRA_WRITE_TOKEN/);
});

it("treats an unexpanded template placeholder as absent", () => {
process.env.ATLASSIAN_JIRA_WRITE_TOKEN = "${ATLASSIAN_JIRA_WRITE_TOKEN}";

expect(hasJiraWriteToken()).toBe(false);
});
});
Loading
Loading