Agent-facing Jira access layer: MCP server, setup core, and CLI. Provides read and write access to Jira issues through a structured MCP interface.
JAM demonstrates strong tool design with clear naming, comprehensive descriptions, and well-structured schemas. All 5 tools follow verb_noun patterns (jira_search, jira_context, jira_full, jira_write_plan, jira_write_apply). Descriptions are detailed (200-800 chars) and include WHEN to use each tool and what NOT to expect. Input schemas are complete with types and descriptions. Output schemas are documented in descriptions. Error handling is explicit (JAM_WRITE_CONFLICT, JAM_WRITE_PLAN_EXPIRED, JAM_WRITE_VERIFICATION_FAILED). Tool composition is excellent: read tools (search/context/full) are layered by cost/completeness, write tools follow a plan-then-apply pattern preventing accidental mutations. Weaknesses: no tool annotations (readOnlyHint/destructiveHint) visible in schema; descriptions could be more concise for LLM token efficiency; no explicit pagination metadata in jira_search output schema.
Get the dependency and readiness picture for one or more Jira issues. Use for: "can I start this", "what is blocking it", "what should I do first", parent/subtask structure, cross-team dependencies, priority ordering. Returns everything jira_search returns plus issue type, parent, subtasks, issue links (with blocksThisIssue flagged) and the project's whitelisted custom fields. It does NOT return the comment thread - if the question is whether something was agreed, approved, or finished, use jira_full instead. This is the Jira-recorded evidence relevant to readiness, blockers, dependencies and priority - not a readiness verdict. blocksThisIssue reports how Jira words a link; an empty links array means Jira holds no visible link for you, not that nothing blocks the work. Repository and external sources are not evaluated. Every issue carries issueId, Jira's immutable id, alongside key. The key is the current human- and integration-facing locator and Jira can move it to another issue; issueId is the identity. Record issueId when a reference has to survive. statusCategory is Jira's own machine-readable category for the status - read it instead of matching status text, which is workflow-defined and localized. Parent, subtasks and links carry issueId too, wherever Jira supplies one. Pass every key you care about in one call; they are fetched in a single batched round trip. Check meta.complete and meta.missingKeys before drawing conclusions. This is also how a Jira issue key is checked. Asking for an exact key and getting an issue back is a positive resolution: that key names that issue, right now. A key listed in meta.missingKeys resolved to nothing JAM can see - it may not exist, or it may not be visible to this account, and those are indistinguishable from here. Either way it is unusable, and it is NOT evidence that the number is free, unused or reservable. Never synthesize, increment, predict or reserve a Jira issue key; keys are minted by Jira.
Get the complete record for one or more Jira issues, including the description and the full comment thread. Use for final judgements: was this agreed, is the contract settled, was it approved, can it be closed, what did the other team actually answer, what does this issue mean right now. Returns everything jira_context returns - issueId and statusCategory included - plus description and every comment (normalized to plain text). This is the most expensive tool - prefer jira_search for listing and jira_context for readiness, and reach for this one when the answer must not be wrong. Ask for as few keys as possible: with several issues at once the output budget may drop the oldest comments. Always check meta.commentsComplete and meta.complete - if either is false, the thread you are reading is partial and a "yes, it is agreed" answer is not supported. This is the Jira-recorded evidence relevant to agreement, contract, approval and closure. Repository and external sources are not evaluated. Absence of evidence in Jira is not evidence of absence. A complete read with no supporting comments proves only what Jira holds. If the issue references an external canonical source (an MR/PR, a spec or contract document, Confluence, another issue), do not conclude "not agreed", "not approved" or "cannot start": check that source if you can reach it, and otherwise report that Jira alone cannot settle the question and name the source that must be checked. Issues that reference no external source do not warrant an open-ended search.
Tool annotations (readOnlyHint, destructiveHint, idempotentHint) not visible in schema definitions. jira_write_apply is destructive but lacks destructiveHint; jira_search/context/full are read-only but lack readOnlyHint.
jira_search output schema not explicitly documented in source. Description mentions returned fields (key, issueId, summary, status, etc.) but no formal JSON Schema for response structure visible. Pagination metadata (meta.complete) referenced but schema not shown.
jira_write_plan description is very long (1200+ chars) and dense. While comprehensive, it exceeds token-efficient baseline (50-200 chars). LLMs may struggle to extract key intent from verbose docstring.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 67 | 2026-07-28+ | v2 |
Find Jira issues by JQL and get a lightweight list back. Use for: discovery, listing, "what is open", "what is assigned to me", recent changes, picking candidate issues. Returns key, issueId, summary, status, statusCategory, assignee, priority, updated, labels and components only. It deliberately does NOT return description, comments, attachments or links - that keeps listing cheap. Because of that, a jira_search result is NOT complete issue context. Never conclude from it that something is agreed, approved, unblocked, or done. Follow up with jira_context (readiness, blockers, dependencies, priority) or jira_full (agreement, contract, approval, closure). Every issue carries issueId, Jira's immutable id, alongside key. The key is the current human- and integration-facing locator and Jira can move it to another issue; issueId is the identity. Record issueId when a reference has to survive. statusCategory is Jira's own machine-readable category for the status - read it instead of matching status text, which is workflow-defined and localized. Repository and external sources are not evaluated. scope="preview" (default) returns the first page for interactive exploration. scope="complete" walks every page - use it whenever the answer depends on the total count or on seeing every match. Check meta.complete before treating the list as exhaustive.
Apply a plan from jira_write_plan. This changes Jira. Takes a planId and nothing else. The change was decided when the plan was made, so there is no field, payload or override to pass here - that is deliberate, and it is what stops a write happening without the state check that planning did. Before writing, JAM re-reads the issue and compares it to what the plan saw. If it moved, you get JAM_WRITE_CONFLICT and no write happens: call jira_write_plan again against the new state rather than treating the conflict as a transient failure. After writing, JAM reads the issue back and checks the intended result is actually there. Only then does it return "applied". Jira accepting a request is not the same as the issue having changed. Failures worth handling differently: - JAM_WRITE_CONFLICT the issue moved; re-plan - JAM_WRITE_PLAN_EXPIRED the plan aged out; re-plan - JAM_WRITE_VERIFICATION_FAILED Jira accepted it but the issue does not show it; read the issue and tell the user - JAM_WRITE_UNCERTAIN JAM does not know whether it landed; read the issue. Do NOT call this tool again - the write may already have been applied, and applying it twice is a second comment or a second transition. Never report an uncertain or unverified write as done.
Work out how to change one Jira issue, and get back a plan. Changes nothing. This is the first half of every write. Call it, read what it says the issue looks like now and what it would become, then pass the returned planId to jira_write_apply. There is no way to write to Jira without a plan, and a plan cannot be assembled by hand - only jira_write_plan issues one. Operations on an issue that already exists - these need `key`: - comment.add input: { "text": "..." } plain text; JAM converts it, do not send ADF - field.update input: { "summary"?, "priority"?, "labels"?, "components"? } - status.transition input: { "status": "Done" } JAM asks Jira which transitions exist and matches yours - assignee.update input: { "assignee": "..." } a display name or an accountId; JAM resolves it against Jira's own directory Creating an issue - no `key`, because there is no issue yet: - issue.create input: { "issueType": "Task", "summary": "...", "description"?, "priority"?, "labels"?, "components"? } issue.create goes into the project this workspace is bound to; the project is not a parameter. Planning reads Jira's create schema for that project first, so an issue type Jira does not offer, a priority or component outside its allowed values, and a project whose create screen requires a field JAM cannot set are all refused here rather than attempted. `description` is plain text, like a comment. Not settable in this version: assignee, reporter, parent, custom fields, attachments. assignee.update never sends the name you pass. JAM searches Jira's user directory, and assigns only when exactly one user matches your string exactly - an exact display name (case-insensitive) or an accountId. A partial match is Jira reporting a similarity, not identifying a person, so several matches or none come back as a refusal with the candidates attached: name one exactly, or pass their accountId. JAM also checks Jira offers that person as an assignee for this issue, before planning and again before writing, and confirms the result by accountId rather than by name. Unassigning, and setting an assignee while creating, are not in this version. Writes are limited to the Jira project this workspace is bound to; a key from another project is refused rather than attempted. The plan records what the issue looked like when it was made, and expires. If the issue changes in the meantime, jira_write_apply refuses with JAM_WRITE_CONFLICT - re-plan against the new state rather than forcing the old one through. A plan is a statement about what is possible right now, not a promise that it will happen. Nothing is written until jira_write_apply runs.
jira_write_plan 'input' parameter is a generic object with conditional properties (text, status, assignee, issueType, etc.). No explicit documentation of which properties apply to which operations. LLMs may pass invalid combinations.
No explicit error recovery guidance in tool descriptions. jira_write_apply mentions JAM_WRITE_CONFLICT but does not state 'call jira_write_plan again' in the tool description itself, only in the long docstring. Error classification could be more machine-readable.