jira-mcp
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| JIRA_PAT | No | Jira Personal Access Token. Only available on Jira 8.14+; when set, requests are sent with Bearer authentication instead of Basic Auth. | |
| JIRA_AUTH | No | Authentication scheme: 'basic' or 'pat'. Inferred from the other variables if not set — 'pat' when JIRA_PAT is set, otherwise 'basic'. | basic |
| JIRA_BASE_URL | No | Base URL of the Jira Server / Data Center instance (e.g. https://jira.corp.com). Required. | |
| JIRA_PASSWORD | No | Jira password for Basic Auth. Use this (together with JIRA_USERNAME) on Jira 8.5.7 and older. Note: Basic Auth is unavailable for accounts that use SSO/Crowd without a local password, or when CAPTCHA is enabled. | |
| JIRA_USERNAME | No | Jira username for Basic Auth. Use this (together with JIRA_PASSWORD) on Jira 8.5.7 and older, which have no Personal Access Token support. | |
| JIRA_READ_ONLY | No | Global read-only switch. When enabled it constrains both the core Jira tools and the Zephyr tools. | false |
| ZEPHYR_ENABLED | No | Set to 'false' to disable the Zephyr Scale tools entirely. | true |
| JIRA_SSL_VERIFY | No | Alias for JIRA_TLS_REJECT_UNAUTHORIZED. Set to 'false' to accept self-signed TLS certificates. | true |
| JIRA_TIMEOUT_MS | No | Timeout in milliseconds for a single HTTP request. | 30000 |
| JIRA_CONFIG_FILE | No | Path to an alias configuration file, which may point outside the project directory. | <project>/jira.config.json |
| JIRA_MAX_RETRIES | No | Number of retries for 429/502/503/504 responses and network errors. Honors the Retry-After header. | 2 |
| ZEPHYR_ALLOW_INTERNAL_API | No | When enabled, exposes 12 additional Zephyr tools that use the internal /rest/tests/1.0 API. | false |
| ZEPHYR_DEFAULT_PROJECT_KEY | No | Default project key used by the Zephyr tools. | |
| JIRA_TLS_REJECT_UNAUTHORIZED | No | Set to 'false' to accept self-signed TLS certificates. | true |
Instructions
Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.
This server publishes no instructions, or was last inspected before Glama recorded them.
Capabilities
Features and capabilities supported by this server
Protocol revision2025-11-25
| Capability | Details |
|---|---|
| tools | {
"listChanged": true
} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| jira_describe_createA | Call before creating an issue. Returns the fields writable for this project/type, which are required, their value shapes and allowed values |
| jira_describe_editA | Call before updating an issue. Returns the fields editable on this issue right now (already filtered by workflow/screen) |
| jira_get_issueA | Get an issue. Returns all fields by default, custom fields included. Use expand to pull extra sections in the same call (e.g. changelog, renderedFields, transitions) |
| jira_search_issuesC | Search issues with JQL |
| jira_create_issueA | Create an issue. Call jira_describe_create first for the field template; keys may be field ids or business aliases |
| jira_update_issueA | Update an issue. Call jira_describe_edit first; use |
| jira_delete_issueC | Delete an issue |
| jira_assign_issueC | Assign an issue |
| jira_get_transitionsA | List the transitions currently available for this issue |
| jira_transition_issueC | Apply a workflow transition |
| jira_list_commentsC | List the comments of an issue |
| jira_add_commentB | Add a comment (Server takes wiki markup, not Markdown) |
| jira_update_commentD | Update a comment |
| jira_delete_commentC | Delete a comment |
| jira_list_worklogsC | List the worklogs of an issue |
| jira_add_worklogC | Log work against an issue |
| jira_update_worklogC | Update a worklog |
| jira_delete_worklogC | Delete a worklog |
| jira_list_attachmentsA | List the attachments of an issue (id and download url included) |
| jira_get_attachment_metaB | Get attachment metadata (filename/size/author/download url) |
| jira_upload_attachmentC | Upload a local file as an issue attachment (multipart) |
| jira_delete_attachmentC | Delete an attachment |
| jira_list_projectsC | List projects |
| jira_get_projectC | Get project details |
| jira_get_componentsC | List the components of a project |
| jira_get_versionsB | List the versions of a project |
| jira_get_statusesC | List the statuses of a project |
| jira_get_current_userB | Get the authenticated user |
| jira_get_userC | Get a user by username |
| jira_search_usersB | Search users (Server matches on username, not accountId) |
| jira_search_assignableA | Search users assignable to an issue |
| jira_get_link_typesA | List issue link types. Use the returned |
| jira_link_issuesA | Link two issues. |
| jira_delete_linkA | Delete an issue link by its id (find ids in the issue's issuelinks field) |
| jira_get_remote_linksA | List the remote links (e.g. Confluence pages) attached to an issue |
| jira_create_remote_linkC | Attach a remote link (e.g. a Confluence page) to an issue |
| jira_get_watchersA | List the watchers of an issue, and whether the authenticated user is watching |
| jira_add_watcherA | Add a user as a watcher of an issue |
| jira_remove_watcherC | Remove a user from the watchers of an issue |
| jira_get_fieldsA | List every field, plugin-provided ones included. Use it to obtain field ids |
| jira_get_issue_typesB | List issue types |
| jira_get_prioritiesC | List priorities |
| jira_server_infoA | Get Jira version information (useful to confirm the Server version and deployment type) |
| jira_requestA | Raw Jira REST call. The escape hatch for plugin modules that have no dedicated tool yet |
| jira_list_pluginsA | List installed UPM plugins when the account may read /rest/plugins/1.0. If UPM is unavailable, falls back to inferring plugin keys from custom field schemas and reports why each UPM path failed |
| jira_list_boardsC | List boards |
| jira_list_sprintsC | List the sprints of a board |
| jira_get_sprintC | Get a sprint by id |
| jira_list_backlogC | List the backlog of a board |
| jira_get_board_issuesC | List the issues on a board |
| jira_get_sprint_issuesC | List the issues in a sprint |
| jira_add_issues_to_sprintC | Move issues into a sprint |
| jira_move_issues_to_backlogC | Move issues to the backlog |
| jira_scriptrunner_runC | Run a ScriptRunner custom endpoint |
| jira_jsm_queuesC | List Jira Service Management queues |
| create_test_caseA | Create a test case (POST /testcase). The folder MUST already exist — the API never creates folders implicitly (use create_folder first). status, priority, component and custom field names must match the ones configured on the instance and are case-sensitive: an unknown or wrong-case value is rejected with 400 and nothing is created (unlike EXECUTION statuses, which the API silently ignores). owner: Jira user key (e.g. 'JIRAUSER10000'), NOT a username or e-mail — resolve it with find_jira_user. estimatedTime is in milliseconds. name is limited to 255 characters — a longer one is refused locally, because the API answers it with an opaque bodyless HTTP 500. testScript is STEP_BY_STEP with steps, or PLAIN_TEXT/BDD with text; a step carrying testCaseKey is a "Call to Test" that inlines another case. BDD text is stored verbatim and must contain Gherkin step lines only — a "Feature:"/"Scenario:" header is rejected with 400 "Invalid BDD Script". Omitting testScript does NOT leave the case script-less: the server attaches an empty PLAIN_TEXT script, which add_test_steps treats as "no script yet". Returns { key, url }. |
| get_test_caseA | Read one test case (GET /testcase/{testCaseKey}). A STEP_BY_STEP script comes back with a numeric id on every step; those ids are what update_test_case and set_test_script match on, so read them before editing steps by hand (add_test_steps does it for you). Returns the test case object as the API sends it, restricted to fields when given. |
| search_test_casesA | Search test cases with a TQL query (GET /testcase/search). A query longer than 1500 characters is sent as POST /testcase/search instead, which supports ONLY the fields projectKey, key and name and at most 2500 values per IN list. That POST endpoint is missing or broken on some Zephyr Scale Server builds, so prefer staying under 1500 characters (split a long IN list across calls); when a POST search fails the error says which transport was used and why. Unknown VALUES are validated inconsistently: an unknown status, priority, component or projectKey is rejected with 400, while an unknown label in an IN list and an unknown key inside key IN (…) are silently skipped and just shrink the result set — a typo there is indistinguishable from no match. isLast is a heuristic, so an exactly full page always reports isLast false even when it is the last one: stop when values is empty. TQL quick reference:
Returns { startAt, maxResults, count, isLast, values }; isLast is the heuristic count < maxResults. Paginate with startAt (default 0) and maxResults (default 50; the API server-side default is 200). |
| update_test_caseA | Update a test case (PUT /testcase/{testCaseKey}). PARTIAL: only the fields passed are changed and omitted fields keep their values, so never send empty placeholders. projectKey cannot be changed. issueLinks REPLACES the whole link set instead of adding to it — sending issueLinks: ["PROJ-1"] to a case already linked to PROJ-2 silently unlinks PROJ-2, so read the current links with get_test_case and send the complete final list (link_issues_to_test_cases is the additive alternative). testScript.steps is synchronized BY ID — a step without an id is created, a step with an id is updated, and every stored step whose id is missing from the list is DELETED; always send the complete final list, carrying over the ids from get_test_case. To only add steps use add_test_steps, which does that read-merge-write safely. A name longer than 255 characters is refused locally: the API stores the first 255 characters and still reports success. A step whose "Call to Test" points at its own case is refused locally too: the API answers 2xx to such a write and stores NOTHING, throwing away the other steps of the same request with it. When (and only when) testScript is passed, the tool reads the case back afterwards (one extra GET) and compares the STORED script with the one sent — its type, the step count for STEP_BY_STEP, and that a non-empty text survived for PLAIN_TEXT/BDD (the text itself is not compared byte-for-byte); an update without testScript costs no extra request. The folder MUST already exist — the API never creates folders implicitly (use create_folder first). Returns { key, url }; when the stand accepted the write and kept the old script, the answer also carries storedType, storedSteps and a warning saying what is really stored, and a warning alone when the read-back itself failed. Only the script is verified: the other fields of a partial update are not read back. |
| add_test_stepsA | Insert steps into a STEP_BY_STEP script without losing the existing ones (GET then PUT /testcase/{testCaseKey}): reads the current steps, keeps their ids, splices the new ones in and writes the whole list back — needed because PUT deletes every step missing from the list it receives. The stored steps are ordered by their authoritative |
| set_test_scriptA | Replace a test case's whole script or change its format (PUT /testcase/{testCaseKey} with a full testScript). DESTRUCTIVE: switching STEP_BY_STEP to PLAIN_TEXT or BDD irreversibly deletes all steps, and a STEP_BY_STEP replacement deletes every stored step whose id is absent from steps. text is required for PLAIN_TEXT and BDD, steps for STEP_BY_STEP — the pairing is validated locally, before any request — but steps: [] passes that check and DELETES every stored step, leaving an empty STEP_BY_STEP script. BDD text is stored verbatim and must contain Gherkin step lines only, no "Feature:"/"Scenario:" header (400 "Invalid BDD Script"). A step whose "Call to Test" points at its own case is refused locally: the API answers 2xx to such a write and stores NOTHING. After the write the tool reads the case back (a second GET) and compares the STORED script with the one sent — its type, the step count for STEP_BY_STEP, and that a non-empty text survived for PLAIN_TEXT/BDD (the text itself is not compared byte-for-byte). Returns { key, url }; when the stand accepted the write and kept the old script, the answer also carries storedType, storedSteps and a warning saying what is really stored, and a warning alone when the read-back itself failed. |
| delete_test_caseA | Permanently delete a test case (DELETE /testcase/{testCaseKey}). Irreversible: the case, its script and its execution history cannot be restored through the API. Inbound references are NOT checked: a case that other cases invoke as a "Call to Test" step is deleted anyway and those steps keep pointing at a key that no longer resolves. Returns { deleted: true, key }. |
| create_test_cases_bulkA | Create several test cases in one request (POST /testcase/bulk). Each item takes the same fields as create_test_case; an item without its own projectKey uses the shared projectKey, then ZEPHYR_DEFAULT_PROJECT_KEY. The folder MUST already exist — the API never creates folders implicitly (use create_folder first). Some Server builds ship a broken bulk endpoint (any 5xx, or a JSON 404 — as opposed to the plugin-not-installed HTML 404) while single creation works: the tool then falls back to POST /testcase per item so partial progress survives — on such a build the fallback runs on every call and the bulk shape is never returned. A 4xx from the bulk endpoint is a payload error and is NOT retried. Returns [{ key, url }] on the bulk path, or { note, created: [{ key, url }], failed?: [{ index, name, error }] } when the fallback ran — also when SOME items failed, so always read failed[]. created[] is in input order but carries no index, and failed[] is omitted entirely when every item succeeded; index is the position in the testCases array. When EVERY item fails nothing was created and the call FAILS, with one line per item naming its index, its name and its error, so the payload can be fixed in one pass. A name longer than 255 characters is rejected locally, naming the item. |
| link_issues_to_test_casesA | Link Jira issues to test cases in bulk (POST /testcase/link-issues). One entry links one test case to one issue; repeat a testCaseKey across entries to link it to several issues. At most 2500 UNIQUE test case keys per call — checked locally, before any request. Additive: it only creates links, never removes existing ones. KNOWN ISSUE: on many Server/DC builds this endpoint answers HTTP 500 with an empty body even for a single valid pair (verified live on such a stand); link through update_test_case (or create_test_case) with the issueLinks field instead — that field REPLACES the case's whole link set, so send the complete final list. Returns the API payload, or { linked: } when the API answers with an empty body (the usual case). |
| get_test_cases_linked_to_issueA | List the test cases linked to a Jira issue (GET /issuelink/{issueKey}/testcases) — traceability from a requirement or bug to its tests. Create such links with link_issues_to_test_cases, or with the issueLinks field of create_test_case. Use get_issue_test_coverage instead to also see the latest execution of each case. Returns the API array of test case objects, one entry per LINK: a case linked to the issue twice appears twice, and the order is not stable between calls — de-duplicate by key before counting. Entries carry the API fields, including lastTestResultStatus — a DENORMALIZED value that is absent while the case has never been executed and is reset to "Not Executed" (not cleared) when the test run holding its executions is deleted, so it can read "Not Executed" for a case that really ran Pass. get_issue_test_coverage resolves the execution live and reports lastResult null in that situation; prefer it when the distinction matters. |
| move_test_cases_to_folderA | Move test cases into another folder — one partial PUT /testcase/{key} per case that changes only the folder field. Select the cases with EITHER testCaseKeys OR fromFolder (resolved by GET /testcase/search on folder = ""): exactly one of the two, checked before any request. fromFolder matches that folder EXACTLY — cases in its subfolders are not included, and an existing but empty fromFolder moves nothing. The folder MUST already exist — the API never creates folders implicitly (use create_folder first). The TARGET folder is NOT checked before the requests: a path that does not exist still issues one PUT per case, every one of them fails with 400 and the call resolves with movedCount 0 — always read failed[], since the returned folder only echoes what was asked for. Folder paths are case-sensitive and the root "/" is not a valid target (400 "The value / was not found for field folder"), so a case cannot be moved out of all folders here. Duplicate testCaseKeys are de-duplicated: each case is moved once and movedCount counts distinct cases. maxCases caps both selection modes. A failing case does not abort the rest — it is reported in failed. Only the folder field is written (script, step ids, version, labels, status, priority, owner, objective and precondition are preserved) and moving is reversible (move them back the same way). De-duplication is by EXACT string: keys are neither trimmed nor upper-cased, so "PROJ-T1" and "proj-t1" are two candidates and the second one simply fails with 404 on this case-sensitive API. movedCount counts successful writes, so a case already sitting in the target folder counts as moved. maxCases is applied AFTER the duplicates are removed, and note appears only when something needs explaining (cap truncation, ignored duplicates) — a clean full move and an empty fromFolder both return no note. A fromFolder path that does not exist is different from an empty one: the underlying search fails with 400 "Value(s) not found for field folder". Each failed[] entry is { key, error }. Returns { folder, movedCount, moved, failed?, note? }. |
| clone_test_caseA | Copy a test case inside its own project (GET /testcase/{testCaseKey}, then POST /testcase). Copies name, objective, precondition, folder, status, priority, component, owner, estimatedTime, labels, custom fields, parameters and — unless includeScript is false — the script; step ids are dropped so the copy owns its steps. Issue links, attachments and execution history are NOT copied. name defaults to " (copy)" and folder to the source folder. The folder MUST already exist — the API never creates folders implicitly (use create_folder first). A test case name is limited to 255 CHARACTERS, not bytes (the API rejects a longer one with an opaque HTTP 500): an explicit longer name is refused locally before any request, and the default name is shortened to fit — the copied source name is cut on a whole-character boundary (an astral character is dropped rather than split), the " (copy)" marker is kept, and note reports both lengths. An explicitly empty name ("") is sent as-is and rejected with 400 "The field name is required." — omit the parameter to get the default. Names are not deduplicated: cloning twice gives two cases with the same name, and cloning a copy gives "… (copy) (copy)" — unless the source name is already at the limit, where the shortening cuts exactly the previous " (copy)" off and the copy ends up named identically to its source (note says so; pass an explicit name to tell them apart). With includeScript=false the copy has no steps, but the API still reports an empty PLAIN_TEXT testScript — that is what every script-less case looks like here. Returns { key, url, sourceKey, note? }. |
| get_issue_test_coverageA | Traceability report for a Jira issue: every linked test case with its latest execution (GET /issuelink/{issueKey}/testcases, then GET /testcase/{key} and GET /testcase/{key}/testresult/latest per case). Costs up to 2 requests per case, so maxCases (integer 1..200, default 50) caps the volume. totalLinked counts LINKS — the API returns one entry per link, so a case linked to the issue twice is counted twice — while cases[] holds one row per DISTINCT case, expanded once. The order the API supplies is not stable between calls, so which cases survive the maxCases cut may vary; the cap is applied AFTER duplicate links are collapsed, so it counts DISTINCT cases. lastResult is { status, environment?, actualEndDate?, executedBy?, comment? } with the absent keys OMITTED, and it carries no execution id/key (use get_latest_result_for_test_case when you need the id). It is the most recently CREATED execution, not the one with the greatest actualEndDate, so a back-dated execution still wins and the date shown may be older than that of a suppressed one. lastResult null means no latest execution could be resolved — never executed, or its test run was deleted — not "untested" by itself; with includeLastResults=false (it defaults to true) the key is absent entirely. Fault-tolerant: a case that cannot be read still appears with its key. An issue with no linked cases returns totalLinked 0 and an empty cases[]; an issue key that does not exist raises 404 (Jira resolves issue keys case-insensitively, and issueKey is echoed back exactly as passed). note is present only when something was truncated or collapsed. Returns { issueKey, totalLinked, returned, note?, cases: [{ key, name, status, lastResult? }] }. |
| create_folderA | Create a folder for test cases, test plans or test runs / test cycles (POST /folder). name is the FULL path from the root, not a single segment, and every segment must be non-empty and not blank — "/" alone, "/A//B", a trailing "/" and a whitespace-only segment ("/A/ ") are rejected before any HTTP call because the API would create a permanently nameless folder from them. Spaces around a real name are legal and are NOT trimmed. The two rules surface differently: a missing leading "/" is caught by the input schema (an MCP input-validation error), while empty or blank segments are caught by the tool itself (a plain "Invalid folder path ..." message). The other tools never create folders implicitly: create_test_case, create_test_run and create_test_plan fail with 400 on an unknown folder. Not idempotent: an existing path fails with 400 "The folder already exists" and no retry is attempted. With recursive=true (the default) any OTHER 400 on the full path triggers the fallback — every parent prefix is created from the root and the full path is retried once; 403, 409 and 5xx propagate unchanged, so a permission problem is never mistaken for a missing parent. On builds where POST /folder already creates missing ancestors itself that fallback never fires — the reference build is one of them: a two-level-deep new path succeeds even with recursive=false, so recursive is effectively a no-op there. Each folder type has its own tree, so the same path must be created once per type. The public Server/DC API v1 cannot LIST folders, so keep the numeric id returned by create_folder — rename_folder and delete_folder need it (otherwise it can only be found in the Jira UI, or with get_folder_tree when the internal API is enabled). Returns { id, name, type } — id is the id of the LAST segment only, so ancestors created along the way have ids this call never reports (find them with get_folder_tree). |
| rename_folderA | Rename a folder and/or set its custom fields (PUT /folder/{folderId}). name replaces the name of that ONE folder segment — it is not a path, so it cannot move the folder to another parent, and an empty or whitespace-only name, or "/" or "" in it, is rejected before any HTTP call. The rename changes the full path of this folder and of every folder below it, so paths held elsewhere (the folder argument of create_test_case / create_test_run, TQL folder filters) must be updated afterwards. The API does NOT enforce sibling-name uniqueness here (verified live): renaming a folder to the name of an existing sibling succeeds and leaves two siblings with one name — an ambiguous path — even though create_folder rejects that same path with 400 "already exists". The public Server/DC API v1 cannot LIST folders, so keep the numeric id returned by create_folder — rename_folder and delete_folder need it (otherwise it can only be found in the Jira UI, or with get_folder_tree when the internal API is enabled). Returns { id, name }, where name is the new SINGLE segment — unlike create_folder, which echoes the full path. |
| create_test_runA | Create a test run / test cycle (POST /testrun). Pass the COMPLETE list of test cases in |
| get_test_runA | Read one test run / test cycle including its items (GET /testrun/{key}). API v1 limitation: a test run is IMMUTABLE — there is no PUT /testrun, so it cannot be renamed, moved or have cases added/removed through the public API; its items are fixed at creation and the run status is derived from item statuses. Escape hatches: recreate_test_run_with_items (public, new key) or the internal-API tools update_test_run / add_test_cases_to_run / remove_test_cases_from_run (same key, require ZEPHYR_ALLOW_INTERNAL_API=true). Use get_test_run_results for the executions of the run and get_test_run_summary for status counts. items[].id is the id of that item's LATEST EXECUTION, not a stable item identifier: it changes with every new result, and it is a different id space from remove_test_cases_from_run's removedItemIds. Returns the run object as the API stores it (key, name, status, owner, folder, items, …), or only the requested |
| search_test_runsA | Search test runs / test cycles with TQL (GET /testrun/search). For runs TQL accepts ONLY the fields projectKey and folder — name, status or dates are NOT searchable, and there is no full-text search; read a candidate run with get_test_run instead. A folder clause matches that folder AND its subfolders: folder = "/A" also returns the runs in /A/B. TQL quick reference:
|
| delete_test_runA | Permanently delete a test run / test cycle together with ALL its execution results, its attachments and its test-plan links (DELETE /testrun/{key}). Cannot be undone. API v1 limitation: a test run is IMMUTABLE — there is no PUT /testrun, so it cannot be renamed, moved or have cases added/removed through the public API; its items are fixed at creation and the run status is derived from item statuses. Escape hatches: recreate_test_run_with_items (public, new key) or the internal-API tools update_test_run / add_test_cases_to_run / remove_test_cases_from_run (same key, require ZEPHYR_ALLOW_INTERNAL_API=true). So delete + create_test_run with the full desired |
| get_test_run_resultsA | Page through the execution results of a test run / test cycle (GET /testrun/{key}/testresults/page). An item can have several executions; onlyLastExecutions=true keeps only the most recent one per item, so it never returns more values than the run has items. Creating a run already seeds one 'Not Executed' execution per item (that execution IS the item's last one), so with onlyLastExecutions=false — the default — |
| get_test_run_summaryA | Aggregated execution summary of a test run / test cycle: composite read-only call of GET /testrun/{key} plus every page of its results (with the flat-endpoint fallback of get_test_run_results). Counts the LAST execution of each item — the run object names them, so latestResults and executed never exceed itemCount — grouping by status name verbatim in |
| create_test_resultA | Record a NEW execution of a run item (POST /testrun/{runKey}/testcase/{caseKey}/testresult). Appends to the execution history of that item — to amend the newest execution instead, use update_last_test_result. Only the fields you pass are sent, and the execution keeps the project default for everything you omit. The test case should already be an item of the run; if it is not, the behavior is VERSION-SPECIFIC — some Server builds silently ADD it to the run as a new item (verified live: testCaseCount grows; the new item's POSITION in items[] is not the head and not the tail — it landed second of three and second of four in two separate runs, so do not rely on where it appears), others reject the call with 400/404. Default statuses: 'Not Executed', 'In Progress', 'Pass', 'Fail', 'Blocked' — case-sensitive internal names; instances may define custom ones. scriptResults carry per-step outcomes of a STEP_BY_STEP script as { index (0-based), status, comment? }. An overall |
| update_last_test_resultA | Amend the LAST (most recent) execution of a run item (PUT /testrun/{runKey}/testcase/{caseKey}/testresult). The endpoint REPLACES the execution instead of patching it: fields missing from the body are reset (verified live — an omitted status falls back to the project default 'Not Executed', executedBy becomes null, actualEndDate/executionDate jump to the server time; only comment, environment, executionTime, actualStartDate and scriptResults are kept by the API itself). To stop a comment edit from wiping the verdict, this tool therefore first READS the run item's current execution (one extra GET, two requests on builds without /testresults/page) and re-sends what you did not pass: status, executedBy, assignedTo, environment, comment, executionTime, actualStartDate, actualEndDate, iteration, version — exactly as the API returned them, never invented. So omitting a field means "keep it", not "clear it"; a value the API does not return cannot be preserved; and if the pre-read fails or the item has no execution yet, only your fields are sent. Older executions are unreachable here — record a new one with create_test_result, or edit any execution by id with update_test_result_by_id (internal API, when enabled). The test case should already be an item of the run; if it is not, the behavior is VERSION-SPECIFIC — some Server builds silently ADD it to the run as a new item (verified live: testCaseCount grows; the new item's POSITION in items[] is not the head and not the tail — it landed second of three and second of four in two separate runs, so do not rely on where it appears), others reject the call with 400/404. Default statuses: 'Not Executed', 'In Progress', 'Pass', 'Fail', 'Blocked' — case-sensitive internal names; instances may define custom ones. scriptResults carry per-step outcomes of a STEP_BY_STEP script as { index (0-based), status, comment? }. An overall |
| create_test_results_bulkA | Record NEW executions for several items of ONE test run in a single call (POST /testrun/{runKey}/testresults). The body is the |
| get_latest_result_for_test_caseA | Read ONE execution of a test case across ALL test runs (GET /testcase/{key}/testresult/latest). WHICH one is the API's choice and it is not the plain "newest": measured live with three executions of one case, it returned the most recently CREATED one (highest id) even though another carried a later execution date, and back-dating the winner did not dislodge it — so neither "latest by date" nor "latest by date you set" is a safe reading. Treat the answer as "an execution the API considers current" and, whenever the specific execution matters, read the run with get_test_run_results instead. Answers 404 when the case has never been executed. Returns the execution object as the API stores it (id, testCaseKey, status, environment, executedBy, scriptResults, …). Read-back quirks seen live: executionDate mirrors actualEndDate, executedBy is duplicated as userKey (both absent when there is no executor), every result created through the API carries automated: true, issueLinks come back as traceLinks, and a case with no script still returns one stepless scriptResults entry. |
| create_test_planA | Create a test plan (POST /testplan). folder must be a TEST_PLAN folder (create_folder with type TEST_PLAN) — The folder MUST already exist — the API never creates folders implicitly (use create_folder first). status is a case-sensitive internal name. owner: Jira user key (e.g. 'JIRAUSER10000'), NOT a username or e-mail — resolve it with find_jira_user. Returns { key } (e.g. "PROJ-P123") — no UI url, because the test plan page has no stable address across Zephyr Scale versions. |
| get_test_planA | Read a test plan by key (GET /testplan/{testPlanKey}). The payload embeds the linked test runs and Jira issues when the plan has any; narrow it with fields. Returns the test plan object as the API sends it. |
| update_test_planA | Update a test plan (PUT /testplan/{testPlanKey}). PARTIAL update: only the fields you pass are written and omitted fields keep their current value, so never send empty placeholders — they overwrite real data. projectKey cannot be changed. folder must be a TEST_PLAN folder (create_folder with type TEST_PLAN) — The folder MUST already exist — the API never creates folders implicitly (use create_folder first). status is a case-sensitive internal name. owner: Jira user key (e.g. 'JIRAUSER10000'), NOT a username or e-mail — resolve it with find_jira_user. Returns { key }. |
| delete_test_planA | Permanently delete a test plan (DELETE /testplan/{testPlanKey}). Irreversible — there is no trash and no undo. NOT idempotent and it never confirms a deletion it did not perform: an unknown, mistyped or already-deleted key answers 404 and this tool fails instead of returning { deleted: true } (verified live). The key is not reused afterwards — the next create_test_plan gets a fresh number. Does NOT cascade to the test runs linked to the plan (verified live): the runs survive with their names, testCaseCount and status intact, only the plan↔run trace links die. Delete the runs separately with delete_test_run if that is what you meant. Returns { deleted: true, key }. |
| search_test_plansA | Search test plans with a TQL query (GET /testplan/search). For test plans the searchable fields include projectKey, folder, name, status, key, owner and labels (verified live) — the exact set varies by Zephyr Scale version, and an unsupported field fails with 400 "Unrecognized field: ". folder matches EXACTLY: plans in a subfolder of the given path are NOT returned. A folder path that does not exist is not an error here — it comes back as an empty page (count 0), unlike search_test_cases and search_test_runs, which answer 400 for the same path. TQL quick reference:
Returns { startAt, maxResults, count, isLast, values }; isLast is the heuristic count < maxResults. Paginate with startAt (default 0) and maxResults (default 50; the API server-side default is 200). |
| upload_attachmentA | Attach a local file to a test case, test run (cycle), test result, or to one step of a case or result (POST multipart/form-data /testcase/{key}[/step/{i}]/attachments, /testrun/{key}/attachments, /testresult/{id}[/step/{i}]/attachments). Addressing: 'test_case' needs testCaseKey, 'test_run' needs testRunKey, 'test_result' needs testResultId (numeric); an identifier that does not match the target is rejected. stepIndex is accepted for 'test_case' and 'test_result' only — API v1 has no per-step attachments endpoint for runs. filePath is read from the disk of the machine running this MCP server, not from the caller. Uploads are not idempotent: calling twice creates two attachments. A bogus testResultId is rejected here with 404 even though list_attachments answers [] for it (verified live). Returns the attachment metadata the API reports — on the reference build always a bare { id }, with neither the file name nor the size echoed back, so verifying an upload costs a list_attachments call — or { uploaded: true, fileName, size } when the API answers with an empty body. |
| list_attachmentsA | List the attachments of a test case, test run (cycle), test result, or of one step of a case or result (GET /testcase/{key}[/step/{i}]/attachments, /testrun/{key}/attachments, /testresult/{id}[/step/{i}]/attachments). Addressing: 'test_case' needs testCaseKey, 'test_run' needs testRunKey, 'test_result' needs testResultId (numeric); an identifier that does not match the target is rejected. stepIndex is accepted for 'test_case' and 'test_result' only — API v1 has no per-step attachments endpoint for runs. These endpoints take no pagination and no fields projection — the full list always comes back. Step attachments are aggregated ASYMMETRICALLY (verified live): the test-case list EXCLUDES attachments that live on the case steps, while the test-result list INCLUDES them — so enumerating a case's evidence needs one extra call per step, and doing the same on a result double-counts. An out-of-range stepIndex answers 404 (an in-range step with no attachments answers []), and a testResultId that does not exist answers [] rather than 404, unlike a bogus test case or run key. Returns the API's array of attachment records as-is; each record carries the numeric id delete_attachment needs and the url download_attachment accepts. |
| download_attachmentA | Download the content of an attachment to a local file (GET /rest/tests/1.0/attachment/{id}). Address it by attachmentId or by the exact url list_attachments returns — pass exactly one of the two. Attachment content lives outside API v1: /rest/tests/1.0/attachment/{id} is the url the official list endpoint itself hands out, so this tool follows it without requiring ZEPHYR_ALLOW_INTERNAL_API. A supplied url must be on the configured Jira host — credentials are never sent to another host. A query string on the url is sent as request parameters rather than kept in the path, so it is never echoed in an error message (error messages carry the method and the path only, because a query string can carry a token). outputPath is written on the machine running this MCP server and its parent directory must already exist; a failed download writes nothing. Jira answers a url that is not attachment content (a login redirect, an unknown path) with HTTP 200 and an HTML PAGE, so a response whose body starts with or is refused and nothing is written — { savedTo, bytes } therefore means the bytes really came from the attachment endpoint. The refusal names the page it caught (its , or the page's first readable text when it has none) so a login redirect, an error page and a wrong host can be told apart. Set allowHtml: true for an attachment that genuinely is an HTML file. Markup that is not a page (XML, SVG) is never affected. Returns { savedTo, bytes }. |
| delete_attachmentA | Permanently delete one attachment by its numeric id (DELETE /attachments/{id}). Irreversible, and there is no bulk form — one call per attachment. Ids come from list_attachments or from an upload_attachment response; the entity the attachment belongs to is not needed (ids are global, not per-entity). The DELETE itself answers 2xx even for an id that never existed or was already deleted, so the id is verified FIRST with GET /rest/tests/1.0/attachment/{id} (the one endpoint that 404s for a missing attachment): an unknown id fails without deleting anything, at the cost of downloading the attachment's FULL content first — deleting a large attachment transfers the whole file before removing it. Returns { deleted: true, id, existenceVerified: true }, or existenceVerified: false plus a note when that pre-check itself could not answer (the delete is still attempted, so success then does not prove the id existed). |
| upload_automation_resultsA | Publish automated execution results from a local ZIP archive (POST multipart/form-data /automation/execution/{projectKey}). The archive must hold JSON files in Zephyr's custom results format: {"version": 1, "executions": [{"source", "result", "testCase": {"key"}}]}. Validation is strict only at the TOP level of an execution: an extra sibling of source/result such as executionTime is rejected with 400 "Invalid Custom Format JSON file", while an extra field inside "testCase" is silently accepted; "version" is not validated at all and "source" is optional (and readable back through no endpoint). Each execution's "result" is a status name — Default statuses: 'Not Executed', 'In Progress', 'Pass', 'Fail', 'Blocked' — case-sensitive internal names; instances may define custom ones. WARNING (verified live): an UNRECOGNIZED result value is NOT rejected — "PASS", "pass", "FAIL", "" and free text are all stored as Blocked with HTTP 200, so a single typo turns a green suite into a Blocked cycle silently; only an absent "result" key errors (400 "Test Result Status is required"). Test case keys are case-sensitive and must exist: one bad key rejects the whole archive and creates no partial cycle. Two executions of the SAME case become two separate run items, and several JSON files in one ZIP are merged into one cycle. A 400 "Invalid ZIP file" also means a structurally valid archive that contains no JSON at all. Always creates a NEW test cycle (test run) — it never appends to an existing one — and returns the API description of that cycle unchanged, or { uploaded: true } for an empty body. |
| upload_cucumber_resultsA | Publish Cucumber execution results from a local ZIP archive (POST multipart/form-data /automation/execution/cucumber/{projectKey}). The archive must hold the output of Cucumber's built-in json formatter (one or more .json report files). Every scenario must carry a @TestCaseKey=PROJ-T1 tag naming the BDD test case it reports on — that tag is how the server matches a scenario to an existing test case. Always creates a NEW test cycle (test run) — it never appends to an existing one — and returns the API description of that cycle unchanged, or { uploaded: true } for an empty body. |
| download_feature_filesA | Export BDD test cases as Gherkin .feature files packed in a ZIP archive (GET /automation/testcases). tql is REQUIRED — the API rejects the call without it — and this endpoint uses the testCase.-prefixed TQL dialect, which supports ONLY the fields testCase.key, testCase.projectKey and testCase.name (= and IN) joined by AND: testCase.folder, testCase.status, testCase.priority and testCase.labels are rejected with 400 "Error executing TQL", so a whole folder cannot be exported — select the cases with testCase.key IN (...) instead. Values must be quoted, single or double quotes both work, and spaces around operators are optional here, unlike search_test_cases; lowercase "and", lowercase "testcase." and OR are not accepted, and an OR query answers 200 with an empty body rather than a syntax error. Only cases whose script type is BDD are exported: STEP_BY_STEP and PLAIN_TEXT cases, and keys in an IN list that do not exist, are silently skipped (332 cases yielded 251 .feature files on the reference instance) and the return value does not say which keys were dropped. A query that matches no BDD case — a nonexistent projectKey included — returns HTTP 200 with an EMPTY body, not an empty ZIP, and this tool then reports that the query matched nothing. The archive is flat: one .feature per case, no directories. The server writes "Feature: ", " @TestCaseKey=", " Scenario: ", a blank line, then every stored BDD line prefixed with exactly 8 spaces, so an exported file is only byte-identical to the stored script after that prefix is removed, and it is NOT accepted back by set_test_script / create_test_case (400 "Invalid BDD Script") until the Feature:/@TestCaseKey/Scenario: header is stripped. The archive is written to outputPath only after its 'PK' signature is verified, so an HTML login or error page served with HTTP 200 fails loudly instead of leaving a corrupt file. outputPath's parent directory must already exist, '~' is NOT expanded, and an existing file at outputPath is overwritten without warning on success (a failed call leaves it byte-identical). Reads from Zephyr only, so it stays available in ZEPHYR_READONLY mode. Returns { savedTo, bytes }. |
| recreate_test_run_with_itemsA | Recreate a test run (cycle) under a NEW key with a changed item list, name or folder (GET /testrun/{key} + POST /testrun, plus DELETE /testrun/{key} when deleteOriginal=true). API v1 limitation: a test run is IMMUTABLE — there is no PUT /testrun, so it cannot be renamed, moved or have cases added/removed through the public API; its items are fixed at creation and the run status is derived from item statuses. Escape hatches: recreate_test_run_with_items (public, new key) or the internal-API tools update_test_run / add_test_cases_to_run / remove_test_cases_from_run (same key, require ZEPHYR_ALLOW_INTERNAL_API=true). Items of the new run are: the source items in their original order, minus removeTestCaseKeys, plus addItems appended; of the source items only the planning fields (testCaseKey, environment, assignedTo) are carried over when copyResults is false — all read-only item data is dropped. The new run gets a NEW key and nothing that referenced the old one is updated. Header fields not passed explicitly are inherited from the source run (a JSON null there counts as absent) — EXCEPT testPlanKey, which GET /testrun does not report at all, so the new run starts with no plan association unless you pass it (or re-link with link_test_run_to_plan). A run cannot carry Jira issue links at all: issueLinks is rejected locally, and any value the source run reports is dropped rather than forwarded — link the issues on the test cases instead. removeTestCaseKeys filters the SOURCE items only: a key that also appears in addItems is still added. deleteOriginal runs only after POST /testrun succeeded, so a failed create leaves the source run untouched. copyResults=true carries each kept case's LAST execution over as the initial result of its item, while the item's own environment/assignedTo still win — but GET /testrun reports each item MERGED with its latest execution, so an item whose configured environment/assignedTo were not repeated in that execution has already lost them before this tool reads the run; set them explicitly through addItems when specific values matter. Copied per-step scriptResults are sanitized: this API stores the execution of a case without a STEP_BY_STEP script as an index-less stub, and POST /testrun requires an index on every entry, so entries without a usable index are numbered by position or dropped. removeTestCaseKeys is a SILENT filter (keys that are not items of the run, including nonexistent ones, are ignored), addItems does NOT deduplicate (adding a case that is already an item creates a second item for it, after which test results for that case need matchEnvironment/matchUserKey), and removing every item is allowed and produces a valid run with zero items. The source run survives unless deleteOriginal=true, and is never deleted when creating the new run failed. Returns { key, originalKey, itemCount, copiedResults, deletedOriginal } plus copyResultsNote when result copying hit its page cap. copiedResults counts the KEPT SOURCE items that had a last execution — including the 'Not Executed' execution the server writes for every item at creation, so it is not a count of real executions; addItems entries are never counted, even when they carry a status. |
| list_environmentsA | List the Zephyr Scale environments of a project (GET /environments?projectKey=…). Environments are per-project and are referenced BY NAME (case-sensitive) in test run items and test results, so use this to get the exact spelling. Returns the raw array of environment objects ([{ id, name, description }]); an empty array means the project defines none. |
| create_environmentA | Create a Zephyr Scale environment in a project (POST /environments). The name must be unique within the project — a duplicate is rejected with 400 — and is the string other tools use to reference the environment, so create it with the exact casing you intend to pass to test results. Returns the created environment object as the API sends it. |
| find_jira_userA | Search Jira users (GET /rest/api/2/user/search). Use it to resolve the Jira USER KEY (e.g. "JIRAUSER10000") that the owner / executedBy / assignedTo fields of the other tools require — those fields reject usernames and e-mail addresses. Needs the Jira "Browse users" permission, otherwise Jira answers 403. Returns an array of { key, name, displayName, emailAddress }, empty when nothing matches; emailAddress is absent when Jira hides it. |
| health_checkA | Verify connectivity and credentials (GET /rest/api/2/myself) and, when ZEPHYR_DEFAULT_PROJECT_KEY is configured, whether the Zephyr Scale plugin answers on /rest/atm/1.0 (GET /environments). Any JSON error from the plugin — including 403 for a project without Zephyr — still counts as reachable; only Jira's generic HTML 404 page or a network failure counts as unreachable. The tool itself fails only when Jira does not answer or rejects the credentials. Returns { ok: true, jiraUser, baseUrl, zephyrPluginReachable } — zephyrPluginReachable is omitted when no default project key is set. |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
TDQS
Scored across 97 tools
Most tools target a distinct resource+action, but the attachment trio is duplicated across domains (jira_list_attachments/jira_upload_attachment/jira_delete_attachment vs list_attachments/upload_attachment/delete_attachment), and create_test_case vs create_test_cases_bulk, create_test_result vs create_test_results_bulk, and the run-result readers (get_test_run_results/get_test_run_summary/get_latest_result_for_test_case) overlap in scope. The extremely detailed, quirk-filled descriptions help an agent choose, but the near-duplicate names still risk misselection.
snake_case is used throughout, but the surface splits into a jira_-prefixed Jira family and an unprefixed Zephyr family, so the same conceptual operation (list_attachments, delete_attachment) appears under two conventions. Within families the verb_noun pattern is mostly consistent, with oddities like jira_list_sprints vs jira_get_sprint and noun-first names such as health_check and find_jira_user.
97 tools is an extreme mismatch for a single server, spanning two large products (Jira core plus Zephyr Scale). The set is far past the 50+ threshold where findability collapses, and many capabilities (bulk vs single, run-result readers, raw jira_request) push the count higher without adding distinct value.
Coverage is exceptional: full CRUD for test cases, runs, plans, folders, results, environments and attachments; test-case scripting (steps, script replacement, clone, bulk, move); traceability/coverage reporting; automation-result ingestion; and Jira issue/comment/worklog/watcher/link/board/sprint operations. Documented gaps (run mutability requiring recreation or internal API) are explicit escape hatches rather than dead ends.