Replace acli with the official Atlassian Remote MCP Server for Jira (#522)#524
Conversation
…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
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
None.
Security Review
Strengths:
- Secret never lands in
.mcp.json— theAuthorizationheader is a${ATLASSIAN_MCP_AUTHORIZATION}env reference; the resolved Basic credential lives only insettings.local.jsonenv. settings.local.jsonis chmod'd0o600in both the Swift writer (ClaudeHookConfigWriter.writeAtlassianMcpConfig) and the bash mirror (skills/crow-workspace/setup.sh), matchingConfigStore'sconfig.jsonpolicy.op://references are resolved at launch via the injectedresolveSecret(defaultGatewayResolver.opRead), so plaintext tokens never need to be persisted when the user uses 1Password.AtlassianMCPResolvercorrectly returnsnilwhen secret resolution fails — no broken-credential injection (AtlassianMCPResolver.swift:54-59).- Resolved secrets are never logged (
NSLogcalls only report failure modes; the bash mirror likewise never echoesWS_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 throughsettings.local.jsononly. .mcp.jsonand.claude/settings.local.jsonare both added to.git/info/excludeso the secret-bearing files can't be committed accidentally.- Resolver tests (8) cover plaintext/
op:///empty/half-filled/failed-op-read/blank-endpoint paths plusAppConfiground-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.isEmptyreturns 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, andisEmptyexists to short-circuit logging for the fully-unconfigured case), and the comment on the property documents it — but a name likeisFullyUnsetwould have less surprise. Not worth changing.workspaceAtlassianMCPResolved(for:)re-runsop readonce per worktree per session launch (SessionService.swift:829-836). For multi-worktree sessions this is N calls instead of 1. Eachop readis ~10–100ms, so not pressing, but consider caching at the session level next tomanagerGatewayResolved().Sources/Crow/App/Scaffolder.swift:488-489addsmcp__atlassianandmcp__atlassian__*toagentsAllowed— 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.generatePromptWithJiraTaskAndGitHubCodeupdated to assert the MCP prompt path (getJiraIssue/PROJ-7) and thatacli jira workitemis gone — good guard against regression.- No test coverage for
ClaudeHookConfigWriter.writeAtlassianMcpConfigitself (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.
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
|
Thanks @dgershman — addressed the Yellow finding in 4dd4b4e. 🟡 On the three Green items — none changed, reasoning below:
Green light to re-review. |
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review (round 2)
Critical Issues
None.
Security Review
Strengths:
- Yellow finding from round 1 fully addressed in 4dd4b4e —
writeAtlassianMcpConfig's.mcp.jsonteardown (Packages/CrowClaude/Sources/CrowClaude/ClaudeHookConfigWriter.swift:229-250) now mirrors theenv/enabledMcpjsonServershandling: removes only theatlassianserver key, rewrites.mcp.jsonwhen 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.jsonthat was never ours. - 0600 perms on
settings.local.json, env-only secret transport, andop://resolution semantics from round 1 are unchanged — still good.
Concerns:
None.
Code Quality
- The teardown logic is now structurally consistent with the
envandenabledMcpjsonServerspaths 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 clobbermy-server.teardownRemovesOnlyOurServerAndKeepsUserServer— direct regression guard for the round-1 Yellow.teardownDeletesMcpJsonWhenOnlyOursRemained— confirms full-file delete when nothing remains.
swift teston CrowClaude → 45/45 green.swift test --filter AtlassianMCPResolverTestson 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.
…#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>
…#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>
Closes #522.
What
aclicannot 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 usingacli— that path works. A configurable Crow↔Jira state-mapping UI is split out as #523.How
AppConfig.atlassianMCP(endpoint / email /op://token) +AtlassianMCPResolverbuildsAuthorization: Basic base64(email:token), mirroringGatewayResolver.op://secret handling as the AI-gateway config.ClaudeHookConfigWriter.writeAtlassianMcpConfigwrites a project-root.mcp.json(type: http, Authorization via${ATLASSIAN_MCP_AUTHORIZATION}) and pre-trusts it viaenabledMcpjsonServers; the resolved credential lives only in the0600settings.local.jsonenv block. Wired at launch for Jira-task worktrees and the Manager/cron (devRoot). Mirrored insetup.shfor the scaffolding path.setup.shno longer runs GitHub issue auto-assign/project-status against Jira tickets (kills the recurringauto-assign failedno-op)./crow-create-ticket,/crow-workspace,/crow-batch-workspaceand the Claude launcher now use the MCP tools (getJiraIssue,createJiraIssuewith assignee,transitionJiraIssue,lookupJiraAccountId);aclikept only as a fallback note. Bundledsettings.jsonallowsmcp__atlassian.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.configuration.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
AtlassianMCPResolverTests(8) — Basic-auth build,op://resolution, empty/half/failed handling, AppConfig round-trip.acli.make appclean.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 noauto-assign failed.🤖 Generated with Claude Code