Skip to content

Replace acli with the official Atlassian Remote MCP Server for Jira (#522)#524

Merged
dhilgaertner merged 2 commits into
mainfrom
feature/crow-522-jira-mcp
Jun 18, 2026
Merged

Replace acli with the official Atlassian Remote MCP Server for Jira (#522)#524
dhilgaertner merged 2 commits into
mainfrom
feature/crow-522-jira-mcp

Conversation

@dhilgaertner

Copy link
Copy Markdown
Contributor

Closes #522.

What

acli cannot set a Jira assignee (at create or after) and its transitions are unreliable — so every ticket Crow filed landed unassigned. This routes the agent-side Jira flow (create-with-assignee, assign/reassign, transition, fetch, link) through the official Atlassian Remote MCP Server (https://mcp.atlassian.com/v1/mcp) instead.

Scope (agreed with operator): session-side only. Crow's in-app Swift JiraTaskBackend (issue-board polling + auto-complete) keeps using acli — that path works. A configurable Crow↔Jira state-mapping UI is split out as #523.

How

  • Model/resolverAppConfig.atlassianMCP (endpoint / email / op:// token) + AtlassianMCPResolver builds Authorization: Basic base64(email:token), mirroring GatewayResolver.
  • Settings → Automation — new "Atlassian MCP (Jira)" section, same op:// secret handling as the AI-gateway config.
  • InjectionClaudeHookConfigWriter.writeAtlassianMcpConfig writes a project-root .mcp.json (type: http, Authorization via ${ATLASSIAN_MCP_AUTHORIZATION}) and pre-trusts it via enabledMcpjsonServers; the resolved credential lives only in the 0600 settings.local.json env block. Wired at launch for Jira-task worktrees and the Manager/cron (devRoot). Mirrored in setup.sh for the scaffolding path.
  • github_ops fixsetup.sh no longer runs GitHub issue auto-assign/project-status against Jira tickets (kills the recurring auto-assign failed no-op).
  • Skills + launcher prompt/crow-create-ticket, /crow-workspace, /crow-batch-workspace and the Claude launcher now use the MCP tools (getJiraIssue, createJiraIssue with assignee, transitionJiraIssue, lookupJiraAccountId); acli kept only as a fallback note. Bundled settings.json allows mcp__atlassian.
  • Template resync — the release-fallback templates (crow-workspace-setup.sh.template + the SKILL.md templates) were stale (predated the gateway + Jira features); resynced to their live sources so this ships in release builds.
  • Docsconfiguration.md + automation.md, including the one-time org-admin enablement of API-token auth for the Rovo MCP Server and personal-API-token setup.

Operator prerequisite

An Atlassian org admin must enable API-token auth for the Rovo MCP Server, and you create a personal API token at id.atlassian.com — then enter email + token (as an op:// ref) in Settings → Automation. Without the org-admin step the headless calls 401.

Tests

  • New AtlassianMCPResolverTests (8) — Basic-auth build, op:// resolution, empty/half/failed handling, AppConfig round-trip.
  • Updated the Jira launcher-prompt test to assert MCP not acli.
  • Green: root 234, CrowCore 285, CrowClaude 41, AttributionSkillTests 20 (template byte-identity guards). make app clean.

Verify end-to-end

From a Crow-launched Jira session (e.g. SecurityScorecard/MAXX): create a MAXX ticket with an assignee in one step via MCP, reassign, fetch, and transition — headless, no browser prompt, no manual UI assignment. Confirm the worktree has .mcp.json + settings.local.json (enabledMcpjsonServers:["atlassian"], 0600) and setup.sh logs no auto-assign failed.

🤖 Generated with Claude Code

…w) (#522)

acli cannot set a Jira assignee (at create or after) and its transitions are
unreliable, so every ticket Crow filed landed unassigned. Route the agent-side
Jira flow (create-with-assignee, assign/reassign, transition, fetch, link)
through the official Atlassian Remote MCP Server instead. Crow's in-app
issue-board polling + auto-complete keep using acli (that path works); only the
agent flow moved.

- Model: AppConfig.atlassianMCP (endpoint/email/op:// token) + AtlassianMCPResolver
  builds `Authorization: Basic base64(email:token)`, mirroring GatewayResolver.
- Settings → Automation: "Atlassian MCP (Jira)" section, same op:// secret rules
  as the AI-gateway config.
- Injection: ClaudeHookConfigWriter.writeAtlassianMcpConfig writes a project-root
  .mcp.json (http server, Authorization via ${env}) + pre-trusts it via
  enabledMcpjsonServers; resolved credential lives only in the 0600
  settings.local.json env block. Wired at session launch for Jira-task worktrees
  and the Manager/cron (devRoot).
- setup.sh: resolve task provider + Atlassian MCP from config.json, write
  .mcp.json/settings.local.json for Jira workspaces, git-exclude .mcp.json, and
  stop running GitHub issue auto-assign/project-status against Jira tickets (the
  `auto-assign failed` no-op).
- Skills + Claude launcher prompt migrated to the MCP tools (getJiraIssue,
  createJiraIssue with assignee, transitionJiraIssue, ...), acli kept only as a
  fallback note. Bundled settings.json allows mcp__atlassian.
- Resynced the stale release-fallback templates (setup.sh + SKILL.md) to their
  live sources so the feature ships in release builds.
- Docs: configuration.md + automation.md (incl. one-time org-admin enablement +
  personal-API-token setup). Follow-up #523 tracks a configurable
  Crow↔Jira state-mapping UI.

🐦‍⬛ Generated with Claude Code, orchestrated by Crow

Co-Authored-By: Claude <noreply@anthropic.com>
Crow-Session: 9104A714-C9DE-4922-9C95-91F3A2749E37
@dhilgaertner dhilgaertner requested a review from dgershman as a code owner June 18, 2026 17:29

@dgershman dgershman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code & Security Review

Critical Issues

None.

Security Review

Strengths:

  • Secret never lands in .mcp.json — the Authorization header is a ${ATLASSIAN_MCP_AUTHORIZATION} env reference; the resolved Basic credential lives only in settings.local.json env.
  • settings.local.json is chmod'd 0o600 in both the Swift writer (ClaudeHookConfigWriter.writeAtlassianMcpConfig) and the bash mirror (skills/crow-workspace/setup.sh), matching ConfigStore's config.json policy.
  • op:// references are resolved at launch via the injected resolveSecret (default GatewayResolver.opRead), so plaintext tokens never need to be persisted when the user uses 1Password.
  • AtlassianMCPResolver correctly returns nil when secret resolution fails — no broken-credential injection (AtlassianMCPResolver.swift:54-59).
  • Resolved secrets are never logged (NSLog calls only report failure modes; the bash mirror likewise never echoes WS_MCP_AUTH/token).
  • Bash posix_quote (setup.sh:94) is correctly used on every value that lands on the launch line; the MCP credential is never put on the launch line at all — it flows through settings.local.json only.
  • .mcp.json and .claude/settings.local.json are both added to .git/info/exclude so the secret-bearing files can't be committed accidentally.
  • Resolver tests (8) cover plaintext/op:///empty/half-filled/failed-op-read/blank-endpoint paths plus AppConfig round-trip — all pass locally.

Concerns:
None blocking. The endpoint field is user-configurable with no scheme validation, but that's self-targeted risk (the user is choosing where their own Basic auth header is sent) — acceptable.

Code Quality

🟡 Yellow — writeAtlassianMcpConfig cleanup leaves a stale atlassian entry in a multi-server .mcp.json (Packages/CrowClaude/Sources/CrowClaude/ClaudeHookConfigWriter.swift:222-232).

When resolved == nil (MCP toggled off, or session relaunched after task provider switched away from Jira), the teardown only deletes .mcp.json if it contains exactly one server (atlassian):

if  let servers = parsed[\"mcpServers\"] as? [String: Any],
   servers.count == 1, servers[atlassianMcpServerName] != nil {
    try? FileManager.default.removeItem(atPath: mcpPath)
}

If the user added another MCP server to .mcp.json themselves, our atlassian entry stays behind — but the ATLASSIAN_MCP_AUTHORIZATION env var it references is removed from settings.local.json in the same call. On next launch, Claude Code will attempt to expand the missing env var and the dangling server will fail/warn.

Contrast with the env and enabledMcpjsonServers paths a few lines up (:189-202), which correctly remove just our keys and only delete the parent collection when it becomes empty. The .mcp.json path should do the same: remove servers[atlassianMcpServerName], then either rewrite .mcp.json (when servers is non-empty) or delete it.

The bash mirror in setup.sh has the same coverage gap (no teardown for .mcp.json when a workspace's taskProvider flips off jira), but since setup.sh runs at workspace creation, it matters less than the Swift writer that runs on every session launch.

🟢 Green — minor:

  • AtlassianMCPConfig.isEmpty returns true only when both email and tokenRef are blank, even though both are required for Basic auth. The semantics are intentional (the resolver's per-field guards do the real validation, and isEmpty exists to short-circuit logging for the fully-unconfigured case), and the comment on the property documents it — but a name like isFullyUnset would have less surprise. Not worth changing.
  • workspaceAtlassianMCPResolved(for:) re-runs op read once per worktree per session launch (SessionService.swift:829-836). For multi-worktree sessions this is N calls instead of 1. Each op read is ~10–100ms, so not pressing, but consider caching at the session level next to managerGatewayResolved().
  • Sources/Crow/App/Scaffolder.swift:488-489 adds mcp__atlassian and mcp__atlassian__* to agentsAllowed — both are needed because Claude Code matches the bare server name plus per-tool names. Correct.

Tests

  • swift test --filter AtlassianMCPResolverTests → 8 passed locally.
  • ClaudeLauncherTests.generatePromptWithJiraTaskAndGitHubCode updated to assert the MCP prompt path (getJiraIssue / PROJ-7) and that acli jira workitem is gone — good guard against regression.
  • No test coverage for ClaudeHookConfigWriter.writeAtlassianMcpConfig itself (file write, 0600 perms, the teardown bug above, the multi-server merge path). Worth adding when the Yellow fix lands.

Summary Table

Color Meaning Verdict effect
Red Must fix Request changes
Yellow Should fix Request changes
Green Consider Approve allowed

Recommendation: Request Changes — driven by [0 Red, 1 Yellow, 3 Green] findings. The Yellow is a contained cleanup bug in writeAtlassianMcpConfig's teardown path; fixing it (remove our servers entry, rewrite/delete based on remainder) lands in the same round trip. No security or correctness blockers in the new credential flow.


🐦‍⬛ Reviewed by Crow via Claude Code

 review)

Addresses dgershman's Yellow finding: writeAtlassianMcpConfig's teardown
(resolved == nil) only deleted .mcp.json when it held exactly one server, so a
user-authored multi-server file kept a dangling `atlassian` entry after its
${ATLASSIAN_MCP_AUTHORIZATION} env var was removed — Claude Code would warn on
the missing expansion next launch.

Now mirror the env/enabledMcpjsonServers handling: remove only our server key,
rewrite .mcp.json when other servers remain, delete it only when nothing of ours
or theirs is left.

Adds ClaudeHookConfigWriterMCPTests covering write (server/env/0600), user-server
preservation on write, teardown removing only our entry while keeping the user's,
and full-file deletion when only ours remained.

🐦‍⬛ Generated with Claude Code, orchestrated by Crow

Co-Authored-By: Claude <noreply@anthropic.com>
Crow-Session: 9104A714-C9DE-4922-9C95-91F3A2749E37
@dhilgaertner

Copy link
Copy Markdown
Contributor Author

Thanks @dgershman — addressed the Yellow finding in 4dd4b4e.

🟡 writeAtlassianMcpConfig teardown — fixed. The teardown now mirrors the env/enabledMcpjsonServers handling: it removes only our atlassian server key, rewrites .mcp.json when other (user-authored) servers remain, and deletes the file only when nothing of ours or theirs is left. No more dangling entry referencing a cleared env var. Added ClaudeHookConfigWriterMCPTests (4) covering write + 0600, user-server preservation on write, teardown-keeps-user-server, and full-file deletion when only ours remained.

On the three Green items — none changed, reasoning below:

  • .mcp.json teardown in setup.sh — left as-is intentionally. setup.sh runs once at worktree creation, but ClaudeHookConfigWriter.writeAtlassianMcpConfig runs on every session launch (resolved-or-nil), so it's the authoritative teardown — a worktree whose workspace later flips off Jira gets cleaned on its next launch regardless of setup.sh. Duplicating the strip logic in bash would just be a second code path to keep in sync. (The bash write path already preserves user servers — jq only sets .mcpServers.atlassian.)
  • isEmpty naming — agreed it's intentional (short-circuits logging for the fully-unset case; the resolver's per-field guards do real validation), so leaving it per your "not worth changing."
  • op read cachingworkspaceAtlassianMCPResolved(for:) resolves against appState.primaryWorktree(for:) once per session launch (same cadence as workspaceGatewayResolved), not once per worktree — so it's a single op read per launch, matching the existing gateway pattern. Will fold MCP into any session-level resolve cache if/when we add one for the gateway.

Green light to re-review.

@dhilgaertner dhilgaertner requested a review from dgershman June 18, 2026 17:40

@dgershman dgershman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code & Security Review (round 2)

Critical Issues

None.

Security Review

Strengths:

  • Yellow finding from round 1 fully addressed in 4dd4b4ewriteAtlassianMcpConfig's .mcp.json teardown (Packages/CrowClaude/Sources/CrowClaude/ClaudeHookConfigWriter.swift:229-250) now mirrors the env / enabledMcpjsonServers handling: removes only the atlassian server key, rewrites .mcp.json when other (user-authored) servers remain, and deletes the file only when nothing of ours or theirs is left. No more dangling entry referencing a cleared ${ATLASSIAN_MCP_AUTHORIZATION}.
  • Early-return guard at :235 (servers[atlassianMcpServerName] != nil) avoids touching a .mcp.json that was never ours.
  • 0600 perms on settings.local.json, env-only secret transport, and op:// resolution semantics from round 1 are unchanged — still good.

Concerns:
None.

Code Quality

  • The teardown logic is now structurally consistent with the env and enabledMcpjsonServers paths immediately above it — same shape, same nil/empty cascade. Easy to reason about.
  • Reasonable rationale in the PR comment for not duplicating the strip in setup.sh (Swift writer is authoritative, runs on every launch) — agree.

Tests

  • swift test --filter ClaudeHookConfigWriterMCPTests → 4 new tests pass locally:
    • writeAtlassianMcpConfigWritesServerEnvAndTrust — server/env/trust shape + 0600 perm assertion.
    • writeAtlassianMcpConfigPreservesUserServerOnWrite — write path doesn't clobber my-server.
    • teardownRemovesOnlyOurServerAndKeepsUserServer — direct regression guard for the round-1 Yellow.
    • teardownDeletesMcpJsonWhenOnlyOursRemained — confirms full-file delete when nothing remains.
  • swift test on CrowClaude → 45/45 green.
  • swift test --filter AtlassianMCPResolverTests on CrowCore → 8/8 green.

Summary Table

Color Meaning Verdict effect
Red Must fix Request changes
Yellow Should fix Request changes
Green Consider Approve allowed

Recommendation: Approve — driven by [0 Red, 0 Yellow, 0 Green] findings. Round-1 Yellow is resolved with matching test coverage; round-1 Green items are acknowledged with clear reasoning for leaving them.


🐦‍⬛ Reviewed by Crow via Claude Code

@dhilgaertner dhilgaertner added the crow:merge Crow auto-merge on green label Jun 18, 2026
@dhilgaertner dhilgaertner merged commit 7061aaa into main Jun 18, 2026
2 checks passed
@dhilgaertner dhilgaertner deleted the feature/crow-522-jira-mcp branch June 18, 2026 17:47
dhilgaertner added a commit that referenced this pull request Jun 18, 2026
…#525)

Closes #523. Follow-up to #522/#524 (the Atlassian MCP migration),
rebased onto it.

## Problem
Crow's Crow→Jira status mapping was **hardcoded** in
`JiraTaskBackend.jiraStatusName(for:)` (`.ready` → "To Do"; everything
else used the `TicketStatus` raw value). Jira workflow status names are
configurable per project, so a project that renames a status (e.g. "In
Development" instead of "In Progress") made Crow's transitions silently
fail.

## What
A **per-workspace** `jiraStatusMap` (Backlog / Ready / In Progress / In
Review / Done → concrete Jira status names) persisted in `config.json`,
edited in **Settings → Workspaces → Jira Status Mapping**, consulted by
**both** status surfaces with a fallback to today's hardcoded defaults
when unset.

## How
- **Model** — `WorkspaceInfo.jiraStatusMap` + `JiraConfig.statusMap`.
The default table moves to `TicketStatus.defaultJiraStatusName`
(CrowCore) as one source of truth shared by the UI (placeholders) and
the backend (fallback).
- **In-app `acli` path** — `JiraTaskBackend.setTaskStatus` consults the
map; `IssueTracker.markInReview` resolves the matching workspace's map
from the ticket's project key / site (acli is single-site, so only the
map needs threading).
- **Agent MCP path** — `ClaudeLauncher` embeds a `## Jira Status
Mapping` block in Jira-session prompts, and the `/crow-workspace` skill
(+ synced template) tells the agent to consult `jiraStatusMap` from
`config.json` before `transitionJiraIssue`.
- **Settings UI** — one field per pipeline status (placeholder = current
default; blank = use default). **Bonus:** a "Fetch from Jira" button
populates per-row dropdowns from the live workflow via `GET
/rest/api/3/project/{key}/statuses` (new `JiraStatusFetcher`, reusing
#524's Atlassian email+token Basic auth). Degrades to free-text when no
credential / site / project key is set. (`acli` has no list-transitions
command and the in-app UI can't reach the agent MCP, so REST is the only
viable in-app fetch.)
- **Docs** — `docs/configuration.md` Jira section documents
`jiraStatusMap` shape, fallback behavior, and the fetch affordance.

## Tests
- `JiraTaskBackendTests` — default table, map override, blank-entry
fallback, `setTaskStatus` uses the mapped `--status`.
- `AppConfigTests` — `jiraStatusMap` round-trip + nil-when-absent
(back-compat).
- `ClaudeLauncherTests` — mapping block present with a map, absent
without one, never leaks on non-Jira sessions.
- New `JiraStatusFetcherTests` — URL build, parse/dedupe, Basic-auth
header, HTTP-error surface.
- Green: CrowCore 295+, CrowProvider 50, CrowClaude 48, root 145, skill
byte-identity guards 20. `make app` clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
dhilgaertner added a commit that referenced this pull request Jun 19, 2026
…#528) (#531)

Closes #528. Partially reverts #522/#524.

## What

The Atlassian Remote MCP Server (`mcp.atlassian.com`, server
`atlassian`, HTTP Basic via `ATLASSIAN_MCP_AUTHORIZATION`) added in
#522/#524 never worked in our setup. This routes all agent-side Jira
tooling through the **`jira` MCP** (`sooperset/mcp-atlassian`, Docker
stdio) that is already configured **globally** in `~/.claude.json` and
confirmed working.

## Decisions (called out per the ticket)

- **Injection vs global → global.** The `jira` server lives in
`~/.claude.json`'s top-level `mcpServers`, so it's auto-loaded and
trusted in **every** Claude Code session (worktrees, Manager, cron).
Crow injects nothing — no per-session `.mcp.json`, no
`enabledMcpjsonServers`.
- **Secret handling.** No Crow-side secret for the agent flow (the
global server owns `JIRA_URL`/`JIRA_USERNAME`/`JIRA_API_TOKEN`). The one
exception: the in-app **"Fetch from Jira"** status-map button (#523)
runs in the Swift app process, which *cannot* use the MCP — it keeps a
small `Jira (status fetch)` credential (`username` + `op://` token)
under Settings → Automation, used only to call Jira REST directly.

## Changes

- **setup.sh + bundled template:** removed `resolve_atlassian_mcp_env`,
the `settings.local.json` `ATLASSIAN_MCP_AUTHORIZATION` +
`enabledMcpjsonServers` injection, `write_mcp_json`, and the `.mcp.json`
git-exclude.
- **App:** deleted `ClaudeHookConfigWriter.writeAtlassianMcpConfig`, the
`SessionService` injection sites + resolver helpers, and the injection
test. Renamed `AtlassianMCPConfig` → `JiraCredential` and
`AtlassianMCPResolver` → `JiraCredentialResolver`;
`AppConfig.atlassianMCP` → `jiraCredential` with backward-compat decode
of the old block. Settings section `Atlassian MCP` → `Jira (status
fetch)`. Scaffolder allowlist `mcp__atlassian*` → `mcp__jira*`.
- **Skills + docs:** rewrote the Jira sections to the verified `jira_*`
tools (`jira_get_issue` / `jira_create_issue` / `jira_update_issue` /
`jira_transition_issue` + `jira_get_transitions` / `jira_add_comment` /
`jira_get_user_profile`), dropped the `cloudId` /
`getAccessibleAtlassianResources` step, and documented the two-step
transition flow (`jira_get_transitions` → `transition_id` →
`jira_transition_issue`).
- Removed the stale `atlassian` entry from the live devRoot `.mcp.json`
+ `settings.local.json`.

## Verification

- `make app` builds; `make test` green for all touched packages
(CrowCore, CrowClaude, CrowUI, root — incl. the new
`JiraCredentialResolverTests` and the skill/template byte-identical sync
tests). The only failing suite is `CrowGit` `RebaseTests`, a
pre-existing sandbox env issue (no `git config user.email`/`user.name`
in the temp test repos) unrelated to this change.
- `rg` sweep confirms no remaining `mcp.atlassian.com` /
`ATLASSIAN_MCP_AUTHORIZATION` / `atlassian` MCP server / old tool names
(`getJiraIssue`, etc.) in setup.sh, skills, app, or `.mcp.json` — except
the intentional backward-compat migration decoder + its test fixture.

## Coordination

Land **before #529** (status-transition audit) and alongside **#523**
(jiraStatusMap) — the transition tool is now `jira_transition_issue`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

crow:merge Crow auto-merge on green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace acli with the official Atlassian Remote MCP Server for Jira (fixes assignment + headless automation)

2 participants