Skip to main content
Glama

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
JIRA_PATNoJira Personal Access Token. Only available on Jira 8.14+; when set, requests are sent with Bearer authentication instead of Basic Auth.
JIRA_AUTHNoAuthentication scheme: 'basic' or 'pat'. Inferred from the other variables if not set — 'pat' when JIRA_PAT is set, otherwise 'basic'.basic
JIRA_BASE_URLNoBase URL of the Jira Server / Data Center instance (e.g. https://jira.corp.com). Required.
JIRA_PASSWORDNoJira 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_USERNAMENoJira 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_ONLYNoGlobal read-only switch. When enabled it constrains both the core Jira tools and the Zephyr tools.false
ZEPHYR_ENABLEDNoSet to 'false' to disable the Zephyr Scale tools entirely.true
JIRA_SSL_VERIFYNoAlias for JIRA_TLS_REJECT_UNAUTHORIZED. Set to 'false' to accept self-signed TLS certificates.true
JIRA_TIMEOUT_MSNoTimeout in milliseconds for a single HTTP request.30000
JIRA_CONFIG_FILENoPath to an alias configuration file, which may point outside the project directory.<project>/jira.config.json
JIRA_MAX_RETRIESNoNumber of retries for 429/502/503/504 responses and network errors. Honors the Retry-After header.2
ZEPHYR_ALLOW_INTERNAL_APINoWhen enabled, exposes 12 additional Zephyr tools that use the internal /rest/tests/1.0 API.false
ZEPHYR_DEFAULT_PROJECT_KEYNoDefault project key used by the Zephyr tools.
JIRA_TLS_REJECT_UNAUTHORIZEDNoSet 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

CapabilityDetails
tools
{
  "listChanged": true
}

Tools

Functions exposed to the LLM to take actions

NameDescription
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 update for add/remove on multi-value fields

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 name as the link type when linking issues

jira_link_issuesA

Link two issues. type is a link type name from jira_get_link_types. The response carries no id - read the issue's issuelinks field to get it

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:

  • Test case fields: projectKey, key, name, status, priority, component, folder, estimatedTime, labels, owner, issueKeys + custom fields (field name in double quotes).

  • Test run (cycle) fields: ONLY projectKey and folder.

  • Operators: =, >, >=, <, <=, IN; the only logical connector is AND (no OR).

  • Syntax is strict: spaces around operators are mandatory, string values in double quotes. Folder paths start with "/" ("/" is the root). For single/multi-choice custom fields '=' does not work — use IN.

  • Examples: projectKey = "PROJ" AND status = "Draft" AND priority = "High" projectKey = "PROJ" AND folder = "/Regression/Payments" projectKey = "PROJ" AND labels IN ("smoke", "ui") projectKey = "PROJ" AND "My Field" IN ("Value") key IN ("PROJ-T50", "PROJ-T90") projectKey = "PROJ" AND issueKeys IN ("PROJ-5")

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 index before merging, because GET /testcase serves them in an arbitrary array order once a case has been edited; only the insertion changes, existing step ids and their sequence are preserved. position selects the insertion point: "append" (the default), "prepend", or a 0-based index into that ordered sequence, clamped to the step count. Allowed when the current script is STEP_BY_STEP, and when the case has no script CONTENT yet — a case created without a testScript is reported by the API as an empty PLAIN_TEXT script (a stub with no text), and that counts as script-less: a STEP_BY_STEP script is then created. A PLAIN_TEXT or BDD script that really has text is refused; replace it with set_test_script. A step whose "Call to Test" points at its own case is refused too: the API answers 2xx to such a write and stores NOTHING. After the write the tool reads the case back (a second GET) and returns { key, totalSteps } where totalSteps is the count STORED on the case, not the count sent. When the two differ — the stand accepts a write and silently throws it away — the answer also carries stepsSent and a warning saying so; totalSteps is null with a warning when the read-back itself failed.

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 items now — 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). Each item may also carry its execution result (status, executedBy, executionTime, actual dates, per-step scriptResults, …), which imports a run together with its results in one call; afterwards use the test result tools. A run folder is of type TEST_RUN. The folder MUST already exist — the API never creates folders implicitly (use create_folder first). Default statuses: 'Not Executed', 'In Progress', 'Pass', 'Fail', 'Blocked' — case-sensitive internal names; instances may define custom ones. The run's own status is derived by the server from its item statuses and uses a different vocabulary from the execution statuses above ('Not Executed' / 'In Progress' / 'Done'). A run CANNOT be linked to Jira issues: issueLinks is rejected locally because the API has no such field on a run — link the issues on the test cases instead. Returns { key } of the new run.

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 fields.

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:

  • Test case fields: projectKey, key, name, status, priority, component, folder, estimatedTime, labels, owner, issueKeys + custom fields (field name in double quotes).

  • Test run (cycle) fields: ONLY projectKey and folder.

  • Operators: =, >, >=, <, <=, IN; the only logical connector is AND (no OR).

  • Syntax is strict: spaces around operators are mandatory, string values in double quotes. Folder paths start with "/" ("/" is the root). For single/multi-choice custom fields '=' does not work — use IN.

  • Examples: projectKey = "PROJ" AND status = "Draft" AND priority = "High" projectKey = "PROJ" AND folder = "/Regression/Payments" projectKey = "PROJ" AND labels IN ("smoke", "ui") projectKey = "PROJ" AND "My Field" IN ("Value") key IN ("PROJ-T50", "PROJ-T90") projectKey = "PROJ" AND issueKeys IN ("PROJ-5") 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).

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 items is the public way to change a run's name, folder or composition (the new run gets a NEW key). The DELETE itself answers 2xx for a key that never existed, was already deleted or belongs to another entity type (a TEST CASE key was reported deleted while the case stayed intact), so the key is verified FIRST with GET /testrun/{key}: an unknown key fails without deleting anything. Returns { deleted: true, key, 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 run existed).

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 — total starts at the item count, not at 0. Older Zephyr Scale builds have no /page endpoint: the deprecated flat GET /testrun/{key}/testresults is then read and paginated client-side, and onlyLastExecutions is resolved from the run object, whose items[] name the id of each item's last execution; the note in the response says which path produced the values (a run that does not exist still surfaces as a 404). Values come in the order the API returns them, which is neither run-item order nor execution order. Returns { startAt, maxResults, total, count, isLast, values, note? } where total is the size of the set being paged — the number of results on the server, or the post-deduplication count when onlyLastExecutions is true — and isLast is startAt + count >= total.

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 byStatus; nothing is normalized. Default statuses: 'Not Executed', 'In Progress', 'Pass', 'Fail', 'Blocked' — case-sensitive internal names; instances may define custom ones. runStatus is the status of the RUN itself (e.g. 'In Progress', 'Done'), not an execution status. executed counts every counted result whose status is not the literal 'Not Executed'. executionProgressPct = executed/itemCount (executed/latestResults when the run exposes no items), so an item with no counted last execution counts as not executed and note says how many there are. passRatePct is the share of the literal status 'Pass' among executed — 0 when nothing passed, and absent only when executed is 0. Returns { key, name, runStatus, itemCount?, latestResults, executed, executionProgressPct?, byStatus, passRatePct?, note? }.

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 status sent TOGETHER with scriptResults is stored as sent (verified live: 'Blocked' with three 'Pass' steps stored 'Blocked') and is NEVER derived from the step statuses — scriptResults without a status leave the execution at the project default ('Not Executed'), so pass status in the SAME call. Some older builds may instead ignore the overall status: read the result back with get_test_run_results rather than sending a second update_last_test_result, which replaces the whole execution. A scriptResults entry whose index is past the last step of the case is discarded silently (HTTP 200, no error). When the same test case is an item of the run several times (e.g. once per environment or assignee), disambiguate with matchEnvironment / matchUserKey; with no selector the API picks one of them itself — measured live it took the FIRST (lowest-id) twin and left the other untouched, so pass a selector whenever the case appears more than once. Selectors only SELECT an existing item — they never set a value, so pass environment as well if the result should carry it. matchUserKey matches executedBy/userKey, not assignedTo. If nothing matches, this build answers 400 "No test execution found …" or an empty-bodied HTTP 500 — the 500 was first seen with matchEnvironment, but other inputs produce it too, so its cause is undetermined; that empty-bodied 500 has been observed to write the execution and add a duplicate item anyway, so the write may or may not have happened — re-read with get_test_run_results instead of retrying. Returns { id } of the created execution.

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 status sent TOGETHER with scriptResults is stored as sent (verified live: 'Blocked' with three 'Pass' steps stored 'Blocked') and is NEVER derived from the step statuses — scriptResults without a status leave the execution at the project default ('Not Executed'), so pass status in the SAME call. Some older builds may instead ignore the overall status: read the result back with get_test_run_results rather than sending a second update_last_test_result, which replaces the whole execution. A scriptResults entry whose index is past the last step of the case is discarded silently (HTTP 200, no error). When the same test case is an item of the run several times (e.g. once per environment or assignee), disambiguate with matchEnvironment / matchUserKey; with no selector the API picks one of them itself — measured live it took the FIRST (lowest-id) twin and left the other untouched, so pass a selector whenever the case appears more than once. Selectors only SELECT an existing item — they never set a value, so pass environment as well if the result should carry it. matchUserKey matches executedBy/userKey, not assignedTo. If nothing matches, this build answers 400 "No test execution found …" or an empty-bodied HTTP 500 — the 500 was first seen with matchEnvironment, but other inputs produce it too, so its cause is undetermined; that empty-bodied 500 has been observed to write the execution and add a duplicate item anyway, so the write may or may not have happened — re-read with get_test_run_results instead of retrying. Returns the API response ({ id } of the amended execution on the audited build), or { updated: true, testRunKey, testCaseKey } when the API answers with an empty body.

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 results array itself; in each entry only the fields you pass are sent, and the returned ids are positionally aligned with it. 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 status sent TOGETHER with scriptResults is stored as sent (verified live: 'Blocked' with three 'Pass' steps stored 'Blocked') and is NEVER derived from the step statuses — scriptResults without a status leave the execution at the project default ('Not Executed'), so pass status in the SAME call. Some older builds may instead ignore the overall status: read the result back with get_test_run_results rather than sending a second update_last_test_result, which replaces the whole execution. A scriptResults entry whose index is past the last step of the case is discarded silently (HTTP 200, no error). matchEnvironment / matchUserKey apply to the WHOLE batch and only SELECT which existing run item to append to — they never set the created result's environment. NOT ATOMIC, and it does NOT abort at the failing entry: when one entry is rejected (unknown testCaseKey, bad status/iteration/version value, unknown custom field) the call answers an error and returns no ids, yet that entry alone is SKIPPED while EVERY other valid entry is COMMITTED — the entries AFTER the failing one just as much as those before it (verified live twice, with ids: [valid, valid, unknown key, valid] committed entries 1, 2 and 4; [unknown key, valid] committed entry 2). So after an error that names ONE entry the batch may already be fully written except for that entry: re-read the run with get_test_run_results BEFORE resending anything and resend only the entries that are genuinely missing — resending the batch or its tail creates DUPLICATE executions. An error that rejects the payload as a whole (a malformed body, an unknown run key, a batch-wide matchEnvironment/matchUserKey the API cannot resolve) is different: it is refused before any entry is processed, so nothing was committed and nothing needs re-reading. Two entries for the same test case create two independent executions (no upsert). Returns the array of created executions ([{ id }, …]).

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:

  • Test case fields: projectKey, key, name, status, priority, component, folder, estimatedTime, labels, owner, issueKeys + custom fields (field name in double quotes).

  • Test run (cycle) fields: ONLY projectKey and folder.

  • Operators: =, >, >=, <, <=, IN; the only logical connector is AND (no OR).

  • Syntax is strict: spaces around operators are mandatory, string values in double quotes. Folder paths start with "/" ("/" is the root). For single/multi-choice custom fields '=' does not work — use IN.

  • Examples: projectKey = "PROJ" AND status = "Draft" AND priority = "High" projectKey = "PROJ" AND folder = "/Regression/Payments" projectKey = "PROJ" AND labels IN ("smoke", "ui") projectKey = "PROJ" AND "My Field" IN ("Value") key IN ("PROJ-T50", "PROJ-T90") projectKey = "PROJ" AND issueKeys IN ("PROJ-5")

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

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

C2.9/5.0

Scored across 97 tools

Disambiguation3/5

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.

Naming Consistency3/5

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.

Tool Count1/5

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.

Completeness5/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues