npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

jira-fetch

v0.6.0

Published

Fetch Jira Cloud issues into Markdown files with frontmatter and attachments — a CLI and MCP server for AI agents.

Readme

jira-fetch

CI npm license: MIT

Fetch Jira Cloud issues into Markdown files with YAML frontmatter, attachments and all — from the terminal, or over MCP so an AI agent reads Jira through a config file it does not control.

This project was written entirely by AI. Every line was produced by Claude — tested, but never human-reviewed. Read it before you trust it with credentials.

jira-fetch DN-1243 --out tmp
# tmp/DN-1243.md
# tmp/.DN-1243/screenshot_01.png

The document is a plain Markdown file: metadata in the frontmatter, a heading that links back to the ticket, the description as Markdown, then every comment after a --- rule. The frontmatter carries only what the ticket actually has — a key you do not see is a value Jira did not have.

Attachments are downloaded next to it and linked relatively, so it stays readable offline. Only the ones actually embedded in the description or a comment get a link in the body; the rest are still listed in the frontmatter's assets, the complete index of what was fetched.

Install

npm install -g jira-fetch

Works on macOS, Linux and Windows, x64 and arm64.

npm update -g jira-fetch    # upgrade
npx jira-fetch DN-1243      # run it once, installing nothing

Configuration

Your credentials and your filters live in one YAML file per project, in your own configuration directory, outside every repository:

| | | | ------------ | ------------------------------------------------------------------------------------ | | macOS, Linux | ~/.config/jira-fetch/<project-path>.yml | | Windows | %APPDATA%\jira-fetch\<project-path>.yml (that is %USERPROFILE%\AppData\Roaming\) |

The file is named after the git repository root, so you never pick it and there is nothing to pass: no environment variables, no flags, no config file in the project. jira-fetch therefore runs only inside a repository. The MCP server is why it works this way.

jira-fetch setup          # credentials, output folder, people
jira-fetch filters        # which tickets are fetched
jira-fetch config-file    # print its path, whether or not it exists yet

setup asks for your site, your account email and an API token — including where to create one — and checks them against Jira before writing anything. Once they work, it shows a form with the rest of the settings and their current values, so you change what you care about and leave the rest.

To edit the file by hand afterwards:

$EDITOR "$(jira-fetch config-file)"

A minimal file:

project: /home/you/code/thing # the repository this configuration is for
baseUrl: https://your-site.atlassian.net
email: [email protected]
token: ATATT3xFfGF0...

What your Jira site contains

Filters are easier to write when you can see what there is to filter on, so jira-fetch keeps a copy of your projects' labels, fields and the values they accept, issue types, statuses, components, versions, sprints and people:

jira-fetch cache DN SUP     # read those projects; they are remembered for next time
jira-fetch cache            # read whatever has gone out of date
jira-fetch cache --show     # what is cached, and how old
jira-fetch cache --clear    # delete it

jira-fetch filters does this for you before it offers anything, so there is nothing to run first — the command above is for looking at what was read, or for reading a project you have not filtered on yet.

| | | | ------------ | ------------------------------------ | | macOS, Linux | ~/.cache/jira-fetch/<hash>/ | | Windows | %APPDATA%\jira-fetch\cache\<hash>\ |

It refreshes itself as it ages, so there is nothing to run on a schedule. Anything it could not read — a project you cannot create issues in, a site with no Agile boards, a token without permission for some of it — is recorded as unreadable rather than as empty, and --show says which. Nothing in there is anything a fresh read cannot produce again, so deleting it is always safe.

Usage

jira-fetch <ISSUE-KEY>...          fetch one or more issues by key
jira-fetch --jql "<JQL>"           fetch every issue matching a query
jira-fetch mcp                     serve the same pipeline over MCP (see below)
jira-fetch setup                   configure this project, interactively
jira-fetch filters                 choose which tickets are fetched
jira-fetch config-file             print the path of this project's config file
jira-fetch cache <KEY>...          read what your Jira projects contain

  -o, --out <dir>      output directory (default: current directory)
  -n, --dry-run        report what would be fetched and filtered; write nothing
  -v, --verbose        per-issue progress and filter decisions on stderr

Exit codes: 0 success · 1 runtime error · 2 usage or config error · 3 nothing written because every issue was excluded by a filter. An interactive menu left with Ctrl+C exits 130.

Filters

Filters decide which tickets are fetched at all, and which comments make it into the document. jira-fetch filters builds them by picking from what your site actually has — its labels, statuses, components, versions, sprints, people, and the values each custom field accepts — so you are choosing from a list rather than typing from memory. docs/config-example.yml shows every option in one file.

# yaml-language-server: $schema=https://raw.githubusercontent.com/casaper/jira-fetch/main/schema/jira-fetch.schema.json
filters:
  exclude:
    - project: [SUP]
    - labels: [wontfix]
    - field:
        Team: [Platform]
    - title:
        matches: '^spike:'
        flags: i
    - reporter: [null]
  comments:
    exclude:
      - author: [Automation for Jira]
  • Every predicate in a rule must match (AND); rules in a list are OR'd. So the example drops a ticket in project SUP, or labelled wontfix, or …
  • include works the same way: if it is non-empty, a ticket must match one of its rules to be fetched. Exclude beats include.
  • null means "absent" — an anonymous portal reporter, an unassigned issue, an unset field. It cannot collide with someone actually named "anonymous".
  • field reaches every field, built-in ones included, spelled exactly as Jira spells it: field: {Status: [Done, Cancelled]} and field: {Issue Type: [Bug]} are how you filter by status and by type, and there is no separate predicate for either. A raw customfield_10101 works in place of a name — and is what jira-fetch filters writes, showing you the name on screen, so do not be surprised to find customfield_10101: where you picked "Team". A name is resolved through the cached field list; an id is matched before it, so a rule that names one keeps meaning the same field whatever happens to that cache.
  • A field name that does not resolve stops the run with exit code 2, before any issue is fetched — as does one that resolves to two fields, since Jira lets two custom fields share a name. The error names both ids so you can pick one.
  • tags is an alias for labels.
  • Comment filters drop comments, never the ticket, and are exclude-only.

The $schema line gives editors autocomplete and inline validation. It is generated from the same Zod definitions the CLI validates against, so the two cannot drift; point it at a local copy if you would rather not fetch it.

People

How much the document says about the people on a ticket is up to you:

people:
  roles: [reporter, assignee, commenter] # who appears at all; [] leaves people out entirely
  fields: [name, email] # what is recorded about them; at least one
  nameFormat: full # or: initials

Those are the defaults, and roles and fields are independent axes. nameFormat: initials writes Kaspar Vollenweider as KV, in the frontmatter and in comment headings alike; it shortens a name and only a name, so a heading falling through to an email address keeps it whole. All of this is presentation only — reporter and assignee filters read the issue itself, so leaving someone out of the document never changes which tickets are fetched.

What "never fetched" really means

Only the project prefix is decided without fetching, since it comes from the issue key. Everything else needs the payload, so the filter runs before comments and attachments — where the cost and every disk write are. A filtered ticket costs one request and writes nothing; --dry-run and -v show it happening.

Restricting JQL

allowJql: false makes --jql fail with exit code 2. It also removes the search_issues tool from the MCP server, so an agent cannot query either. Use it when someone should only fetch tickets by key. It gates the flag, not the requests the tool makes on its own.

MCP server

jira-fetch mcp serves the same pipeline over the Model Context Protocol on stdin/stdout, so an agent — Claude Code, or any other MCP client — can read Jira through this tool.

The reason to want that is not convenience. It is that the agent's access is decided by a config file, not by instructions you give it:

  • It cannot write to Jira, because there is no tool that writes. Not "the agent was told not to" — the capability is absent from tools/list, so there is nothing to talk it into.
  • The filters above decide which issues it may fetch, exactly as they do at the terminal. There is no tool argument, no query and no prompt that reaches past them.
  • JQL is not a way around them. A query only chooses candidate keys; each key then goes through the same filter stages as a key you typed yourself.

Which is cheaper than minting a separate read-only Atlassian token for every class of ticket you want to fence off.

Setting it up with Claude Code

npm install -g jira-fetch
claude mcp add --scope user jira-fetch -- jira-fetch mcp --out docs/jira

Or without installing anything, at the cost of npx resolving the package on each launch:

claude mcp add --scope user jira-fetch -- npx -y jira-fetch mcp --out docs/jira

There is nothing else to pass: the server finds the same config file the CLI would, derived from the repository it is started in. --scope user puts the definition in ~/.claude.json rather than a .mcp.json in the project, which keeps the launch command itself out of the tree the agent edits.

Check it once: -v prints the config file the server resolved, on stderr, where Claude Code shows it as MCP server output. If it is not the file you meant, nothing else here is true of your setup.

What the agent cannot do

  • It cannot rewrite the policy. There is no config file inside the project; the path is computed from the repository root, so it lands outside the tree the agent edits.
  • It cannot reach the credentials. The token is in that one file and nowhere else — not in the environment its shell inherits, and not in any flag it could pass.

But this is not a sandbox. The server runs as you, and so does the agent's shell — cat "$(jira-fetch config-file)" is a command, not a trick. The only hard boundary is on Atlassian's side, so use a token whose account cannot see what you do not want read.

The guarantee is also about what the server will fetch, not what an agent can read. Filters are evaluated at fetch time, so tightening them later does not remove documents already written, and those are readable with the agent's ordinary file tools. Point --out at a directory you can clear if that matters to you.

// ~/.claude/settings.json — one write, every project
{
  "permissions": {
    "deny": [
      "Read(~/.config/jira-fetch/**)",
      "Edit(~/.config/jira-fetch/**)",
      "Read(~/.cache/jira-fetch/**)",
      "Edit(~/.cache/jira-fetch/**)"
    ]
  }
}
// <project>/.claude/settings.local.json
{ "permissions": { "deny": ["Bash(jira-fetch setup:*)"] } }

Both directories go at user scope deliberately: a deny at any scope beats an allow at any other, so a project cannot grant back what they take away. Read also covers Grep, Glob and the file reads Claude Code recognises inside Bash. setup merges them into whatever is already in those files and adds nothing on a second run — and it refuses to run without a terminal, which an agent's shell does not have.

The configuration directory holds your token. The cache beside it holds no credentials, but it does hold every person your projects can assign to, and the field list in it takes part in deciding which field a field: filter means.

These stop the well-behaved path and are worth having for that, but they do not reach a script that opens either file itself, and an agent with a shell can edit the settings files too.

The tools

| Tool | What it does | | --------------- | ------------------------------------------------------------------------ | | fetch_issues | Writes a document for each issue key given, up to 50 | | search_issues | The same, for each issue a JQL query matches, up to limit (default 25) |

search_issues is not offered at all when the config sets allowJql: false — it is missing from tools/list, rather than present and refusing.

Both write into the output directory fixed when the server starts and return a link to each document. No tool takes a path: the agent chooses which issues it wants, never where bytes land. No ticket content travels through the protocol — the agent reads the files.

An issue the config denies is reported as not available (no such issue, or not permitted by this server configuration) and no file is written for it. A nonexistent issue gets the same sentence, on purpose: answering differently would let an agent map your deny-list by probing keys one at a time. Jira's own API already conflates the two.

Contributing

CONTRIBUTING.md