atl
JSON CLI for Jira and Confluence on Atlassian Cloud (plus Teamwork Graph and Compass), built for AI agents. It calls the REST and GraphQL APIs on api.atlassian.com directly, with its own OAuth client registered through the authorization server of the Atlassian Remote MCP. No runtime dependencies. Design notes and rationale: docs/design.md.
Why
This came out of my hate for the Atlassian MCP, which had all sorts of problems. Instead of constantly battling with it and trying to work out what it can and can't do, I researched the authentication protocol and the API, extracted all of the functions and wrapped them up in a CLI for convenience's sake. It is basically the same thing as the MCP. The only difference is that it's better in every way.
The measurements behind that:
Evaluation
Headless agents answer the same natural-language questions against seeded data on a trial site; answers are checked against the seed's ground truth. Three arms, 3 repetitions per question:
atl: the agent reads the skill (skills/atl/SKILL.md) and runsatl.atl-pre: the same, with the skill preloaded in the system prompt.mcp: the Atlassian Remote MCP v1 endpoint.
All three sets below come from one run (tag r4, product as of commit c8aea8e). Means are per question.
A later run of the two atl arms (tag r5, all 47 questions × 3) added these changes: --grep accepts (?i) and -i, --all is ignored where nothing pages, issue links includes web links, and issue worklogs gained --since/--until/--tz with totals. Across all 141 runs per arm, atl-pre went from 2.56 to 2.17 calls per question and from $0.053 to $0.050, with tool errors down from 31 to 11. atl went from 3.52 to 3.06 calls and from $0.064 to $0.056, with errors down from 32 to 4. On the abyss set, atl-pre went from 4.27 to 2.53 calls and from $0.078 to $0.057. Accuracy was unchanged at 141/141 for atl-pre. atl was correct in every run, but the checker failed one correct n11 answer: a lookalike page's extension sat in a list fragment.
Corporate-nightmare set, n01-n33 (99 runs per arm, writes included):
| atl | atl-pre | mcp | |
|---|---|---|---|
| correct | 99/99 | 99/99 | 98/99 |
| tool calls | 3.32 | 2.31 | 3.99 |
| tool-result tokens | 2,052 | 983 | 8,489 |
| total tokens | 80k | 62k | 128k |
| cost | $0.057 | $0.048 | $0.144 |
| wall time | 19.8 s | 17.2 s | 25.6 s |
The MCP miss is a page-properties question: it has no tool that reads content properties. In the previous run of this set, atl was 99/99, atl-pre 99/99 and mcp 93/99, at $0.068, $0.063 and $0.155 per question.
Strict set, n34-n42 (reads, 24 runs per arm; only the ANSWER lines the prompt asks for are judged):
| atl | atl-pre | mcp | |
|---|---|---|---|
| correct | 24/24 | 24/24 | 23/24 |
| tool calls | 3.25 | 2.29 | 9.17 |
| tool-result tokens | 3,871 | 3,102 | 10,027 |
| total tokens | 84k | 68k | 389k |
| cost | $0.066 | $0.058 | $0.272 |
| wall time | 18.5 s | 16.5 s | 72.2 s |
Abyss set, n43-n47 (15 runs per arm): answers that a default first page or an embedded field hides. These are the last of 240 comments; worklogs beyond the 20 an issue embeds; a write to 27 issues spread over two search pages; 130 child pages; and a four-hop chain from a page through Jira to a remote link.
| atl | atl-pre | mcp | |
|---|---|---|---|
| correct | 15/15 | 15/15 | 12/15 |
| tool calls | 5.13 | 4.27 | 8.27 |
| tool-result tokens | 7,203 | 4,257 | 12,203 |
| total tokens | 131k | 108k | 294k |
| cost | $0.102 | $0.078 | $0.233 |
| wall time | 27.8 s | 25.0 s | 122.5 s |
MCP's abyss misses:
- On the worklog question it retrieved 20 of 45 worklogs and gave no total.
- The 130-child register and the four-hop chain each ran past the 600 s cap once.
The review-and-worklog write (n36) passed 3/3 in every arm, each run with its own transition in the issue history.
Caveats:
- The MCP arm is v1; v2 was not measured.
- Three repetitions make the accuracy numbers directional. The token and cost gaps are large and consistent across all three sets.
- Runs use
--no-session, so tool results that the harness spills toartifact://are unreadable to the agent. This affects the MCP arm on large pages and lists. - Arms ran concurrently against one site. One atl-pre write run waited out an HTTP 429 and is excluded from the wall-time mean.
Method, scenarios and scripts: eval/README.md.
Install
Needs Bun >= 1.4.4 and Node.js on PATH: the atl bin is a Node launcher (#!/usr/bin/env node) that re-executes the CLI under Bun with .env autoloading disabled. Without Node.js, running atl fails with env: node: No such file or directory.
bun add -g @clssck/atl-cli # puts `atl` on PATH (~/.bun/bin); or: npm i -g @clssck/atl-cli
atl skill install # teaches your agents: omp, Claude Code, Codex (~/.agents)
atl auth login # opens the browser; approve, then return
atl doctor # ok: true = ready; otherwise run each `missing[].next`
Upgrade: bun add -g @clssck/atl-cli@latest && atl skill update. atl skill status and atl doctor report a missing or mismatched atl on PATH (atl_not_on_path, atl_version_mismatch) with the command that fixes it, so an agent can set itself up from those.
Setting up through an agent: ask it to "install the atl CLI and its skill". Every step above is a plain command except atl auth login, which needs you to approve in the browser.
From source:
bun install
bun link # `atl` on PATH runs this checkout's src/ directly (no rebuild needed)
atl skill install
bun run build # dist/atl.js: Bun-targeted bundle, run via bin/atl.js
bun run compile # dist/atl: single native binary (--compile --minify --bytecode)
bun run compile:linux # dist/atl-linux
bun run compile:windows # dist/atl.exe
bun run test:platform # pack, install into a temp dir, smoke the installed atl
Releases: dispatch the Release workflow (.github/workflows/release.yml) with a patch/minor/major bump. It prepares one versioned candidate (npm version syncs plugin.json and cuts the [Unreleased] changelog section), runs CI on Linux, macOS and Windows against that exact tarball, publishes it to npm with provenance, then pushes the version commit and tag atomically and creates the GitHub Release. main only takes pull requests with green CI; the release push uses the RELEASE_DEPLOY_KEY deploy key, the ruleset's only bypass. Publishing needs an npm trusted publisher configured for clssck/atl-cli (workflow release.yml).
Auth
atl auth login registers its own OAuth client (RFC 7591 DCR, client_name: atl), runs PKCE S256 with the loopback redirect http://localhost:47923/callback (--port to change) and stores its own tokens. It never touches omp's MCP credential: Atlassian rotates refresh tokens, so sharing one would log the other client out.
The authorize request replicates omp's MCP flow: scopes from the protected-resource metadata, resource=<MCP resource> on authorize, exchange and refresh, prompt=consent, no audience.
- Credentials:
<configDir>/credentials.json(mode 0600). Refresh runs under a cross-process lock (refresh.lock) and re-reads the store before rotating. - Headless:
ATL_ACCESS_TOKEN(never refreshed), oratl auth login --with-token < token.txt. atl auth status | refresh | logout [--revoke].
Sites
The cloudId is never asked for. Site precedence: site in the target URL > --site > ATL_SITE > atl sites --default <host> > the only accessible site. atl sites refreshes the cache from accessible-resources.
Commands
doctor | sites | whoami | api <METHOD> <path> | get <url|ari|key> | formats
auth login | status | refresh | logout
issue get [--comments N] [--grep <regex>] | transitions | links [--chain "<relation>" [--max N]] | tree [--depth N] [--group-by <field>]
issue link-types | comments [--grep <regex>] | changelog | worklogs [--since D --until D [--tz ZONE]] | editmeta | watchers | permissions
issue attachments | attachment add | attachment download [--force] | attachment delete
issue create | update | comment [--edit] | move | log [--edit] | link | remote-link add | convert | watch | unwatch | notify
issue delete | comment-delete | remote-link delete (need --yes)
jql "<query>|<issues URL>" [--group-by <field>] | jql-check <query...> | jql-suggest [--field <field>]
bulk edit | transition | move | delete | status (async bulk tasks; delete needs --yes)
board list | get | issues | sprints [--summary]
project list | types | fields
status | component | priority | resolution | field | filter | dashboard
property get | set (Jira entity properties)
version list | get | related-work list | related-work set
user find "<name>" | get <accountId...>
page get [--version N] [--format md|adf|storage|view] [--grep <regex> [--context N]] | diff | ancestors | tree [--depth 1-5] | children | versions | labels
page operations | properties | property set | restriction | permissions list
page create [--force] | update [--force] | move | copy | restore --version N
page restrict | property delete | permissions add | remove | set (restrict/property delete/remove/set need --yes)
space list | get | personal | pages
attachment list | get (Confluence metadata)
comment list [--kind footer|inline|all] [--replies] | get [--version N] | replies | versions | add | update (--kind footer|inline)
cql "<query>"
search "<text>" [--product confluence|jira] [--type page|issue|...]
graph context | object | add | cypher (Teamwork Graph)
compass components | component | fields | field-create | create | relate
mcp tools [--names | --snapshot | --catalog] | call <tool> (MCP parity tooling, escape hatch)
commands run [--fields NAME,...] [--contains TEXT]
agent contract
atl <command> --help prints usage, flags and examples; atl commands run --fields page.get returns the same as JSON with a flag schema. atl commands run --contains <mcpToolName> finds the command that replaces an MCP tool (each command lists its mcpTools).
Agent skill
skills/atl/ is the companion agent skill: SKILL.md (command table, output and error rules) plus references/ for research, editing and storage-format patterns. Every file in it is embedded in the CLI (dist/atl.js and compiled binaries), so atl installs the skill that matches its own version; no checkout is needed.
atl skill status # per target: missing | current | outdated | modified | unmanaged | linked | newer
atl skill install # every agent home found on this machine; --target omp,claude,agents or --dir <skills-root>
atl skill update # refresh installed copies to this CLI's bundle; missing ones are skipped
atl skill uninstall --yes # remove managed copies
| target | skills root | agent home that install looks for |
|---|---|---|
omp | $PI_CODING_AGENT_DIR or ~/.omp/agent, plus /skills | the same directory |
claude | $CLAUDE_CONFIG_DIR or ~/.claude, plus /skills | the same directory |
agents | ~/.agents/skills (Codex and other Agent Skills hosts; omp also reads it) | ~/.agents or ~/.codex |
dir | --dir <skills-root> | n/a: always installs |
The skill lands in <root>/atl with an .atl-skill.json manifest ({name, version, files: {path: sha256}}). status compares it with the files on disk and the CLI's bundle:
| state | meaning | install / update | uninstall |
|---|---|---|---|
missing | no directory | install / skipped | skipped |
current | manifest version equals the CLI's and the files match the bundle | unchanged | removes |
outdated | untouched copy from a different version or bundle | refreshes | removes |
modified | files edited, added or removed since install | needs --force | needs --force |
unmanaged | directory without a valid manifest | needs --force | refused |
newer | manifest version is greater than the CLI's (another atl installed it) | needs --force | removes |
linked | the directory is a symlink (for example a checkout) | refused | refused |
A refusal with an explicit --target or --dir is a skill_conflict input error (exit 1, nothing changed, next carries the --force command). Without them it is a skipped row with a reason and the other targets proceed. Replacement is atomic (new tree beside the old, two renames, then the old tree is deleted), so files an older bundle shipped disappear and a failed run never leaves a half-written install. --plan returns the same rows without touching disk, and atl doctor lists installed copies under skill and suggests atl skill update when one is outdated. To work on the skill itself, symlink a checkout (ln -s "$PWD/skills/atl" ~/.omp/agent/skills/atl); atl never modifies a link.
Writes
- Bodies come from exactly one of
--body <text>(inline, short text),--file <path>or--file -(stdin). Both isbody_conflict; commands that need a body (page create,comment add,issue comment) fail withbody_requiredwhen neither is given. Markdown is the default and is converted locally to ADF;--body-format adftakes ADF JSON,storageis Confluence only. A--filepath infersadffor.jsonandstoragefor.html/.xhtml/.xml; inline and stdin bodies default to markdown. issue link,issue logandissue moveattach a comment the same way:--comment <text>or--comment-file <path|->, mutually exclusive.--plan(alias--dry-run) prints the exact request (method, URL, body digest and preview) and sends nothing;nextholds the argv that applies it.- Named write commands send once and never retry.
page updateandcomment updateread the current version and send version+1. A 409 conflict exits 3 with a re-read hint. A write that times out or loses its connection exits 3 withwrite_outcome_unknown: the request may have been applied, so read the target back before retrying. atl apiandatl mcp callrequire--yesfor anything that is not a read. Destructive commands (issue delete,issue comment-delete,issue attachment delete,issue remote-link delete,bulk delete,page restrict,page property delete,page permissions remove | set) also require--yes;issue convertneeds it when the issue has children.--planworks without it.page updatealways reads the current body as ADF; a markdown body that would drop inline-comment anchors fails withinline_comment_anchors_dropped(--planlists them asdroppedInlineComments) unless--force.issue attachment downloadnever overwrites (file_exists) without--force.- Storage bodies are checked locally (
src/adf/storage-check.ts).page updatewith a storage body also reads the current body as storage and compares what each contains: macros byac:name, mentions, attachments, page links, links (href,ri:url), dates, tasks, headings (text), layout sections andac:adf-nodetypes, with volatile ids (ac:macro-id,ac:local-id,ri:version-at-save) ignored. Anything the new body lacks warns once on stderr ascontent_removed(never refuses);--planalso returnscheck: {bytes: {current, new}, removed, added}(item to count per category, empty categories absent) next to the plan.page diffaddsinventory: {removed, added}between its two versions, absent when the structure did not change, sopage diff ID --from N --to N+1verifies a write; the default markdown diff fetches both versions' storage for it (4 GETs instead of 2),--format storagereuses what it fetched,--fullis unchanged. page createandpage updaterefuse a body containing<script>,<style>, ordata:/file:URLs inhref/src/ri:urlvalues (ADF: link markhref,media/inlineCard/embedCardurl) withbody_hygiene(exit 1, the hint names each finding) unless--force;--planreports it as a warning instead. CDATA (code macros) and escaped markup do not count; markdown bodies are not checked (raw HTML stays literal text).page update --expect-version <n>is the optimistic-lock guard (the CLI'ssnapshotToken): read the page, edit the body, pass the version you read; if the page is no longer atnthe command fails before writing (also under--plan) withversion_conflict(exit 3,next=page get <page> --format storage).page getreports the number asversion. Drafts: the stored number is reset to 1 on save, so on a draft the check only compares against whatever number the GET reports.- A command that declares its own
--versionflag (page get,page restore) owns it;-Valways prints the CLI version. - Page commands (
page get | tree | children | versions | labels | diff | ancestors | properties | permissions list | restriction | operations,comment list) take an exact page title in place of an id or URL, optionally narrowed with--space KEY: one match resolves, several fail withambiguous_title(id, space key and title per candidate, plus anextcommand each), none withnot_found(exit 2);--type blogpostsearches blog posts.
Identifiers
Page ids, page/blog URLs, tiny links (/wiki/x/...), /display/SPACE/Title links (exact title), viewpage.action?pageId= and draftId= links, pageVersion= (--version wins), focusedCommentId URLs, ARIs, issue keys and issue URLs. A bare id is probed as page, then blog post, unless --type is given. Wrappers (<url>, [x](url), [KAN-1], a trailing :) are stripped. An ARI of another site is site_not_accessible. jql also takes a Jira issues URL.
Output contract
- stdout: exactly one JSON value (compact;
--prettyorATL_PRETTY=1indents). A result over 700 bytes is broken across lines (one list item or object property per line, no indentation) because agent harnesses cut long stdout lines; a line exceeds that only for a single unsplittable string.jq .and any JSON parser read it unchanged. Invisible and bidi characters (zero-width, NBSP, control, tag characters) are escaped as\uXXXX.--help/--versionare the only text outputs. - stderr: JSONL diagnostics, e.g.
{"type":"error","code","category","exitCode","message","hint","next"}; warnings and hints usetype: "warning" | "hint". A warning means the result may be incomplete or suspect:jql_invalid(empty JQL result, Jira reported errors in the query),search_incomplete(a Confluence page came back short twice),fields_unmatched(no--fieldspath matched),inline_comment_anchors_dropped,macros_dropped,content_removed(a storage body lacks macros, links, headings, ... the current page has),body_hygiene. Hints:child_issues_of_empty(useissue tree),replies_truncated,tree_depth_unchecked,empty_search. Never contains tokens. - Reads are compact by default;
--fullreturns raw API objects.--fields a,b.cprojects, per item for lists (usekey, notitems.key, though theitems.prefix is accepted); a numeric segment indexes an array and keys the result by index; a list item no path matches stays{}so counts hold; no match at all warnsfields_unmatched.--output PATHwrites a result'sbody(ordescription) to the file as the original markdown (never the wrapped display lines) and prints the rest of the result with{path, bytes, lines}in its place; results without that text are written as JSON and print{path, bytes}. - Lists return
{items, nextCursor};--limit N,--all(hard cap 1000),--cursor(a malformed cursor isinvalid_cursoron every list). - Bodies render ADF to markdown locally (
--format md, default) and print as an array of lines, so no stdout line exceeds about 700 bytes: the markdown split on\n, a prose line over 600 JSON bytes soft-wrapped at spaces (a markdown soft break, same rendering); fenced code, table rows and headings are never wrapped (an over-long one is hard-split between code points, unmarked).--format adfreturns raw ADF; pages also acceptstorage. ADF is unchanged and the storagebodyis a line array too (--outputwrites the original). page getincludesparent: {id, title}({id, type}when the parent is not a page).--grep <regex>(case-insensitive, matched against the markdown text without backslash escapes) replaces the body with{id, title, parent, version, url, lines, matches:[{from, to?, hits?, text}], moreMatches?}(matches: []is always printed): 1-based line numbers in the rendered body (the lines--outputwrites), ranges split at 20 lines, a long line becomes an excerpt followed by a… N more matches in this linerow, at most 20 ranges withmoreMatchescounting the rest.--context N(needs--grep) adds surrounding lines and merges overlapping ranges. Works with--versionand--format md|storage|view;--format adfand--fullareinvalid_flag_combination.- Every comment command takes
--kind footer|inline.comment addhas no default: a top-level comment is footer and a reply takes its parent's kind (comment_kind_mismatchotherwise).comment get | replies | versionstry footer, then inline, when--kindis omitted.comment listdefaults to footer and also acceptsall(footer then inline, each item taggedkind;--limitapplies per kind and onenextCursorresumes both).comment get --version Nreads an earlier version.comment list --repliesnests replies to full depth (10 levels or 200 requests, thenreplies_truncated);parentCommentIdis the comment replied to.atl get <comment ref>forwards--kindtocomment get. issue links --chain "<relation>"follows the first matching link per issue (up to--max, default 20) and returns{chain:[{key, summary, status, branches?, cycleTo?}], stoppedBy: end|cycle|max};branchesholds the other matching keys, andrelationslists the start issue's relations when none matches.issue linksreturns{items, remote?}: issue links plus the compact remote (web) links;--global-idreturns only that remote link (an empty list with no match).issue worklogs --since YYYY-MM-DD --until YYYY-MM-DD [--tz IANA]keeps worklogs whosestartedfalls on those dates (inclusive) in that zone (default UTC), reads every page (--limit/--cursor/--allareconflicting_flags) and returnstotalSecondsandtotal(39h 55m) over the returned items.--grepaccepts a leading(?i),--grep -i <re>is the same as--grep <re>, and--allon a command without paging is ignored.jql --group-by <field>(a field id, name, localized name or clause name) returns{total, groups:{value: count}}over up to 1000 issues (capped: truewhen cut).issue tree <issue> [--depth N] [--group-by <field>] [--issue-fields ...]lists everything under an epic or issue, sub-tasks and deeper levels included, as{root, total, items, capped?}(each item hasdepthand its parent key), or counts it with--group-by;childIssuesOf()JQL can return nothing for sub-tasks (child_issues_of_empty).--issue-fieldsextras are top-level onjqlandissue getitems. On non-English sites status names are localized; transition names work in either language, JQL may need the namesatl jql-suggest --field statusreturns.searchwith no results writes{"type":"hint","code":"empty_search","next":[...]}to stderr, whosenextholds the equivalentatl cql/atl jqlargv (the search index can lag); an empty Confluence page is retried once. Confluence hits carryspaceKeyandparent.api: GET/HEAD send directly; other methods need--yes, and--planprints the request without sending it. Onlyapi.atlassian.comreceives the bearer token.
Exit codes: 0 ok, 1 input/config/auth (including body_not_utf8 for a non-UTF-8 --file/stdin body), 2 API/internal (every not-found: page_not_found, space_not_found, attachment_not_found, comment_not_found, property_not_found), 3 transient (429/5xx/timeout/409; after a write error write_outcome_unknown, read back before retrying), 4 failed assertion.
Markdown: <u>, <sub> and <sup> read and write underline, subscript and superscript; <strong>, <em> and <del> read as bold, italic and strike and are written only where **, * or ~~ would not re-parse (a mark touching a word or punctuation). A table or task list inside a quote, and a table inside a list item, is placed after its container. Tabs in fenced code are kept; merged table cells repeat their text on read; a mid-paragraph image is inline and a linked image keeps its link. atl formats prints the full mapping.
Coverage and gaps
- MCP v1: every tool in
mcp/tools-v1.json(41 tools) maps to a command;test/parity.test.tsenforces it. - MCP v2: covered directly only (REST/GraphQL with atl's own token, never through the MCP server) for Jira, Confluence, Teamwork Graph and system tools (
mcp/tools-v2.json,mcp/operations-v2.json). Every other in-scope v2 operation is a documented gap inmcp/gaps-v2.jsonwith a reason class:scope-not-grantable: the DCR client only gets the MCP resource's classic scopes; v2*:agent-interfacescopes return 401 on REST/GraphQL.no-public-api: approvals, reactions, public links, Rovo/AI features, export.mcp-internal: discover/execute* tools.- Out-of-scope products are listed in
outOfScopeProducts(assets, jsm, bitbucket, loom, goals, teams, ...).
atl mcp tools | callremain as parity tooling and an explicit escape hatch for anything above; no other command uses them.searchis CQLtext ~plus JQLtext ~, not Rovo search: the GraphQLsearchroot rejects 3LO tokens (OAuthNoSiteInQuery), so semantic ranking and third-party sources are unavailable.- Comala Workflows is a Forge app without a public Cloud API; only
page properties, labels and storage reads (page get --format storage) are possible. graph addsupports Jira issue links (links,blocks) and remote links. Atlas relationships needwrite:project:projects/write:goal:goals, outside the MCP's scopes (missing_scope).graph contextuses Cypher per relationship type;node(id)is not exposed to OAuth clients.
Token limits
- Confluence has no delete scope: pages, comments and attachments are deleted in the UI.
- Confluence attachment downloads are 401;
attachment list | getreturn metadata via CQL. - Page restriction writes (
page restrict,page permissions ...) may be refused by the site's plan. - Board column config and the sprint list are not exposed to OAuth clients;
board sprintsreads the Sprint field of the board's issues.board listonly shows boards you opened recently.
Environment
| variable | effect |
|---|---|
ATL_CONFIG_DIR | config directory (default %APPDATA%\atl on Windows, else $XDG_CONFIG_HOME/atl or ~/.config/atl) |
ATL_SITE | default site host |
ATL_ACCESS_TOKEN | bearer token override |
ATL_TIMEOUT_MS | HTTP timeout (default 30000) |
ATL_PRETTY | 1 or true indents JSON |
ATL_MCP_URL | MCP endpoint for passthrough commands (default https://mcp.atlassian.com/v1/mcp/authv2) |
Develop
bun install --frozen-lockfile
bun run check # tsc, type-aware oxlint (warnings denied), oxfmt --check, verify (plugin version, changelog), bun test
bun run fmt
Repository flow, conventions and the MCP upgrade procedure are in AGENTS.md. CI (.github/workflows/ci.yml) runs typecheck, lint, format, bun run verify, build, tests and the packaged-CLI smoke on pushes to main and on pull requests (Linux), and on Linux, macOS and Windows for releases and manual dispatches.
.omp/mcp.json points omp at the v1 authv2 MCP endpoint for this project (overriding the user-level atlassian server); authorize it with /mcp reauth atlassian.
MCP upstream watch
- .github/workflows/mcp-watch.yml runs daily and compares public MCP signals with
mcp/upstream.json: upstreamserver.json/plugin.json, the MCP registry, protected-resource scopes, a probe for a new/vN/mcpendpoint, and the tool names on the public supported-tools page. When they differ it opens anmcp-upstreamissue.bun scripts/mcp-watch.tsruns the same check locally. bun src/main.ts mcp tools --snapshot --output mcp/tools-v1.jsonrefreshes the v1 tool snapshot (needs a login).- v2 refresh (needs a v2 login in a throwaway config dir; inline env only):
ATL_CONFIG_DIR=$D bun src/main.ts auth login --resource https://mcp.atlassian.com/v2/mcp(a v1 login sees a scope-filtered catalog;--snapshot/--catalogrefuse it withresource_mismatch).- With
ATL_CONFIG_DIR=$D ATL_MCP_URL=https://mcp.atlassian.com/v2/mcp, runmcp tools --snapshot --pretty --output mcp/tools-v2.jsonandmcp tools --catalog --pretty --output mcp/operations-v2.json. bun test test/parity.test.ts; map every new name to a command or add amcp/gaps-v2.jsonentry with probe evidence.rm -rf "$D", thenbun scripts/mcp-watch.ts --write.
Contributing
See CONTRIBUTING.md for the workflow and checks, SECURITY.md for private vulnerability reporting, and the Code of Conduct.
License
MIT (LICENSE); keep the copyright notice / credit clssck.