{
  "markdown": "# zephyr-scale-mcp\n\n**MCP server for Zephyr Scale on self-hosted Jira Server / Data Center** (formerly TM4J). It gives an AI agent — Claude Code, Claude Desktop, Cursor, any [MCP](https://modelcontextprotocol.io) client — 54 tools over your test management: test cases, folders, test cycles, executions, test plans, attachments and automation imports, through the Zephyr Scale REST API v1 at `{JIRA_BASE_URL}/rest/atm/1.0`.\n\n![Node](https://img.shields.io/badge/node-%E2%89%A520-brightgreen) ![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue) ![Tests](https://img.shields.io/badge/tests-1181%20passing-brightgreen) ![License](https://img.shields.io/badge/license-MIT-blue) ![API](https://img.shields.io/badge/Zephyr%20Scale-Server%2FDC%20v1-orange)\n\nnpm: [`zephyr-scale-mcp`](https://www.npmjs.com/package/zephyr-scale-mcp) · MCP Registry: `io.github.vilaabo/zephyr-scale-mcp` · [Русская версия →](README.ru.md)\n\n> ⚠️ **Scope: Server / Data Center only.** Zephyr Scale **Cloud** (API v2) and Zephyr **Squad** are different APIs and are out of scope — this server will not work against them.\n\n## Why this exists\n\nMost Zephyr MCP servers target the Cloud API. If your Jira is on-premise you get the v1 API instead, and v1 has teeth: test runs are immutable after creation, folders cannot be listed, statuses are case-sensitive internal names, BDD scripts reject a `Feature:` header, and older plugin builds are missing whole endpoints. This server encodes that knowledge instead of leaving the agent to discover it one 400 at a time.\n\n- **54 tools** (42 public + 12 opt-in internal-API tools), each with a description that states the endpoint, the constraints a caller cannot guess, and the exact response shape.\n- **Composite tools for the API's blind spots** — `add_test_steps` merges steps by id (read → merge → write) so nothing is silently deleted; `recreate_test_run_with_items` works around run immutability and can carry the last results over; `get_issue_test_coverage`, `get_test_run_summary`, `clone_test_case`, `move_test_cases_to_folder`.\n- **Degradation on older builds** — automatic fallback when the paginated results endpoint is absent, and a per-item fallback when the bulk create endpoint is broken. Both were found on a real legacy instance, not in a changelog.\n- **Plumbing that fails loudly** — strict zod input validation, `Retry-After`-aware retries with exponential backoff, error messages carrying actionable hints, read-only mode, secrets kept out of logs and tool output.\n- **1181 unit and contract tests** (vitest + msw, no network) plus a gated end-to-end scenario against a real instance.\n\n## Quick start\n\nRequirements: Node.js >= 20, Jira Server/DC with the Zephyr Scale plugin, and a [Personal Access Token](https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html) (Jira 8.14+) or a username/password pair.\n\nNothing to clone — the package is on npm. **Claude Code**:\n\n```bash\nclaude mcp add zephyr-scale \\\n  --env JIRA_BASE_URL=https://jira.example.com \\\n  --env JIRA_PAT=<personal access token> \\\n  --env ZEPHYR_DEFAULT_PROJECT_KEY=PROJ \\\n  --env ZEPHYR_ALLOW_INTERNAL_API=true \\\n  -- npx -y zephyr-scale-mcp\n```\n\n**Claude Desktop / any MCP client** (`claude_desktop_config.json`, `.mcp.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"zephyr-scale\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"zephyr-scale-mcp\"],\n      \"env\": {\n        \"JIRA_BASE_URL\": \"https://jira.example.com\",\n        \"JIRA_PAT\": \"<personal access token>\",\n        \"ZEPHYR_DEFAULT_PROJECT_KEY\": \"PROJ\",\n        \"ZEPHYR_ALLOW_INTERNAL_API\": \"true\"\n      }\n    }\n  }\n}\n```\n\n<details>\n<summary>Running from source instead</summary>\n\n```bash\ngit clone https://github.com/vilaabo/zephyr-scale-mcp.git\ncd zephyr-scale-mcp\nnpm install\nnpm run build        # -> dist/index.js\n```\n\nThen point the client at the build: `\"command\": \"node\", \"args\": [\"/path/to/zephyr-scale-mcp/dist/index.js\"]`.\n\n</details>\n\n> **`ZEPHYR_ALLOW_INTERNAL_API=true` is optional and recommended.** It registers 12 extra tools that reach the things the public API cannot do at all: editing a test run in place — renaming or moving it (`update_test_run`) and adding or removing cases without changing its key (`add_test_cases_to_run`, `remove_test_cases_from_run`) — plus listing the folder tree (`get_folder_tree`), deleting folders (`delete_folder`), editing older executions (`update_test_result_by_id`) and reading the exact status names the API silently expects (`get_status_options`). These call the same undocumented `/rest/tests/1.0` endpoints the Jira UI itself uses; the vendor does not support them and they may differ or be absent on another Zephyr Scale version. Leave the flag off if that trade-off is not acceptable — the other 42 tools are unaffected.\n\nThen ask the agent to run `health_check`. It verifies connectivity and credentials via `GET /rest/api/2/myself` and, when `ZEPHYR_DEFAULT_PROJECT_KEY` is set, that the Zephyr plugin answers on `/rest/atm/1.0`.\n\n## What you can ask your agent to do\n\n- *\"Create the folder `/Regression/Payments` and add step-by-step test cases for the checkout flow described in this document.\"*\n- *\"Find every Draft case in `/Regression`, review them, and set the ready ones to Approved.\"*\n- *\"Create a cycle for sprint 42 with all smoke cases, then record the results from this report — step by step where the script has steps.\"*\n- *\"Which test cases cover PROJ-123, and when did each of them last pass?\"* — traceability from the issue to its cases and their latest executions.\n- *\"Take this ZIP of Cucumber JSON reports and publish it as a new cycle in PROJ.\"* — automation import; scenarios are matched to BDD cases by their `@TestCaseKey=PROJ-T1` tag.\n- *\"Add two steps to PROJ-T55 after step 3.\"* — existing steps and their ids survive.\n- *\"Recreate cycle PROJ-R7 with three more cases, keep the results, and delete the original.\"*\n\n## Configuration\n\n| Variable | Required | Default | Purpose |\n|---|---|---|---|\n| `JIRA_BASE_URL` | yes | — | Jira base URL without a trailing `/`, e.g. `https://jira.example.com` |\n| `JIRA_AUTH` | no | `pat` | `pat` \\| `basic` |\n| `JIRA_PAT` | with `pat` | — | Jira Server/DC Personal Access Token |\n| `JIRA_USERNAME`, `JIRA_PASSWORD` | with `basic` | — | Basic-auth credentials |\n| `JIRA_TIMEOUT_MS` | no | `30000` | Per-request timeout |\n| `JIRA_MAX_RETRIES` | no | `2` | Retries for GET and for any `429`/`503`, honoring `Retry-After`, otherwise exponential backoff with jitter |\n| `JIRA_TLS_REJECT_UNAUTHORIZED` | no | `true` | `false` accepts self-signed certificates — this disables TLS verification process-wide and prints a warning to stderr |\n| `ZEPHYR_DEFAULT_PROJECT_KEY` | no | — | Used whenever a tool is called without `projectKey` |\n| `ZEPHYR_READONLY` | no | `false` | `true` makes every write tool refuse with an error; read tools keep working |\n| `ZEPHYR_ALLOW_INTERNAL_API` | no | `false` | `true` registers the 12 UNOFFICIAL tools backed by the internal `/rest/tests/1.0` API |\n| `ZEPHYR_LOG_LEVEL` | no | `info` | `debug` \\| `info` \\| `warn` \\| `error` |\n\nTwo guarantees the tests cover: `JIRA_PAT` and `JIRA_PASSWORD` never appear in logs, tool output or error messages — in any encoding they can take on the way out: raw, JSON-escaped, or the base64 basic-auth token (error text carries the method and path only — never the query string, which may contain data) — and **stdout is reserved for the MCP protocol**, every log line goes to stderr. One deliberate exception: a secret shorter than six characters is left alone, because redacting it would corrupt unrelated output while protecting nothing; the server warns about it at startup.\n\nConfiguration is validated at startup: all problems are reported at once and the process exits with a non-zero code rather than starting half-configured.\n\n## Tools\n\n54 tools. The 42 public ones are always registered; the 12 in the last group only with `ZEPHYR_ALLOW_INTERNAL_API=true`.\n\n<details>\n<summary><b>Test cases</b> — 13 tools</summary>\n\n| Tool | What it does |\n|---|---|\n| `create_test_case` | Create a case with a `STEP_BY_STEP` / `PLAIN_TEXT` / `BDD` script, parameters, custom fields, Call-to-Test steps |\n| `get_test_case` | Read a case, optionally narrowed by `fields`; step ids come back here |\n| `search_test_cases` | TQL search with pagination; a query longer than 1500 characters is sent as `POST /testcase/search` (which supports only `projectKey`, `key`, `name`) |\n| `update_test_case` | Partial update; `testScript.steps` is synchronized by id (see limitation 7) |\n| `add_test_steps` | Insert steps at a position without losing the existing ones — read, merge by id, write back |\n| `set_test_script` | Replace the whole script or change its format; destructive by design |\n| `clone_test_case` | Copy a case inside its project with fresh step ids; links, attachments and history are not copied |\n| `move_test_cases_to_folder` | Bulk-move by explicit keys or by source folder; a failing case does not abort the rest |\n| `delete_test_case` | Permanent delete of the case, its script and its history |\n| `create_test_cases_bulk` | Create many cases in one call, with a per-case fallback on broken bulk endpoints |\n| `link_issues_to_test_cases` | Bulk-link cases to Jira issues (additive) |\n| `get_test_cases_linked_to_issue` | Reverse lookup: issue → cases |\n| `get_issue_test_coverage` | Traceability report: issue → linked cases → latest execution of each |\n\n</details>\n\n<details>\n<summary><b>Test cycles (test runs)</b> — 7 tools</summary>\n\n| Tool | What it does |\n|---|---|\n| `create_test_run` | Create a cycle with its complete item list — optionally with each item's execution result in the same call |\n| `get_test_run` | Read a cycle including its items |\n| `search_test_runs` | TQL search — for runs only `projectKey` and `folder` are searchable |\n| `delete_test_run` | Permanent delete of the cycle and all its results |\n| `get_test_run_results` | Page through the executions of a cycle, with the legacy flat-endpoint fallback |\n| `get_test_run_summary` | Last execution per item: `byStatus` counts verbatim, `executionProgressPct`, `passRatePct` when a literal `Pass` status exists |\n| `recreate_test_run_with_items` | The public workaround for run immutability: rebuild under a **new** key with cases added or removed, optionally carrying the last results over and deleting the original |\n\n</details>\n\n<details>\n<summary><b>Executions (test results)</b> — 4 tools</summary>\n\n| Tool | What it does |\n|---|---|\n| `create_test_result` | Append a new execution to a run item, including per-step `scriptResults` |\n| `update_last_test_result` | Partial update of the most recent execution of an item |\n| `create_test_results_bulk` | Many executions for one cycle in a single call |\n| `get_latest_result_for_test_case` | The execution of a case with the greatest stored execution date, across all cycles — not necessarily the one recorded last |\n\nItems that exist several times in a run (per environment or per assignee) are disambiguated with `matchEnvironment` / `matchUserKey`, sent as query parameters.\n\n</details>\n\n<details>\n<summary><b>Test plans</b> — 5 tools</summary>\n\n| Tool | What it does |\n|---|---|\n| `create_test_plan` | Create a plan; returns `{ key }`, e.g. `PROJ-P123` |\n| `get_test_plan` | Read a plan with its linked runs and issues |\n| `update_test_plan` | Partial update |\n| `delete_test_plan` | Permanent delete |\n| `search_test_plans` | TQL search; the searchable field set varies by Zephyr Scale version |\n\n</details>\n\n<details>\n<summary><b>Folders</b> — 2 tools</summary>\n\n| Tool | What it does |\n|---|---|\n| `create_folder` | Create a case / plan / cycle folder from a full path; with `recursive` (default `true`) a 400 on the full path triggers creating each parent prefix and one retry — `403`, `409` and `5xx` propagate untouched |\n| `rename_folder` | Rename one folder segment by its numeric id (and optionally set its custom fields) |\n\n</details>\n\n<details>\n<summary><b>Attachments</b> — 4 tools</summary>\n\n| Tool | What it does |\n|---|---|\n| `upload_attachment` | Attach a local file to a case, a case step, a cycle, a result or a result step (multipart) |\n| `list_attachments` | List the attachments of any of those targets; each record carries the id and url the other tools need |\n| `download_attachment` | Save an attachment by id or by the url `list_attachments` returned — that url must be on the configured Jira host. The only public tool that reads from the internal API: attachment content is served by `/rest/tests/1.0/attachment/{id}`, which exists regardless of `ZEPHYR_ALLOW_INTERNAL_API` |\n| `delete_attachment` | Permanently delete one attachment by numeric id |\n\n</details>\n\n<details>\n<summary><b>Automation</b> — 3 tools</summary>\n\n| Tool | What it does |\n|---|---|\n| `upload_automation_results` | Publish a ZIP of results in Zephyr's custom JSON format; always creates a new cycle |\n| `upload_cucumber_results` | Publish a ZIP of Cucumber JSON reports; scenarios are matched by their `@TestCaseKey=PROJ-T1` tag |\n| `download_feature_files` | Export BDD cases as a ZIP of `.feature` files; `tql` is required and uses the `testCase.`-prefixed dialect. The archive is written only after its `PK` signature is verified, so an HTML login page served with HTTP 200 fails instead of leaving a corrupt file |\n\n</details>\n\n<details>\n<summary><b>Service and diagnostics</b> — 4 tools</summary>\n\n| Tool | What it does |\n|---|---|\n| `health_check` | Jira reachability, credentials, and whether the Zephyr plugin answers |\n| `list_environments` | The project's environments — the exact case-sensitive names results reference |\n| `create_environment` | Create an environment in the project |\n| `find_jira_user` | Resolve the Jira **user key** (`JIRAUSER10000`) that `owner` / `executedBy` / `assignedTo` require |\n\n</details>\n\n<details>\n<summary><b>UNOFFICIAL — internal API, opt-in</b> — 12 tools</summary>\n\nRegistered **only** with `ZEPHYR_ALLOW_INTERNAL_API=true`. These call `/rest/tests/1.0`, the undocumented API behind the Jira UI. **The vendor does not support it**: endpoints may differ or be absent on another Zephyr Scale version, and a `404`/`405` from one of these tools means exactly that. Every request shape below was either captured from the Jira UI's own traffic or verified live against a real instance — never guessed. Errors from this layer carry a hint saying so.\n\n| Tool | What it does |\n|---|---|\n| `update_test_run` | Rename a cycle, move it to another folder (by numeric folder id) or change its planned dates **in place**, keeping its key, items and results. No `PUT /testrun` exists in the public API |\n| `add_test_cases_to_run` | Append cases to an existing cycle in place; the key and the existing results survive |\n| `remove_test_cases_from_run` | Remove items from an existing cycle in place — their whole execution history dies with them |\n| `reorder_test_run_items` | Reorder the items of an existing cycle; a cycle already in the requested order makes no write at all |\n| `link_issues_to_test_run` | Link Jira issues to an existing cycle — the public API rejects an `issueLinks` field on runs outright, so this is the only way |\n| `link_test_run_to_plan` | Associate an existing cycle with a test plan after creation (the public API accepts `testPlanKey` only at creation) |\n| `delete_test_results` | Delete individual executions by numeric id. The **last** execution of an item cannot be deleted |\n| `update_test_result_by_id` | Edit any execution in an item's history, including older ones; `status` is resolved from its case-sensitive name to the internal id |\n| `get_folder_tree` | The full folder tree of a project with the numeric ids `rename_folder`, `delete_folder` and `update_test_run` need. Each entity type has its own tree |\n| `get_status_options` | The exact internal names of the project's execution statuses, case statuses or priorities — the values the public API silently ignores when they are wrong |\n| `get_custom_field_definitions` | Custom field definitions per entity type: names, types, required flags, options |\n| `delete_folder` | Delete a folder by numeric id. What happens to a non-empty folder is version-specific — empty it first |\n\n</details>\n\n## Working around API v1\n\nThese are the constraints the server is built around. Every tool description repeats the ones relevant to it, so the agent sees them at call time.\n\n1. **Test runs are immutable.** There is no `PUT /testrun`: a run cannot be renamed, moved, or have cases added or removed. Its items are fixed at creation and the run status is derived from item statuses. Escape hatches: `recreate_test_run_with_items` (public, produces a **new** key) or the internal `update_test_run` / `add_test_cases_to_run` / `remove_test_cases_from_run` (same key).\n2. **Folders are never created implicitly.** `create_test_case`, `create_test_run` and `create_test_plan` fail with 400 on an unknown folder. Folders also cannot be listed through the public API, and renaming needs the numeric id returned by `create_folder` — or `get_folder_tree` with the internal API enabled.\n3. **`owner` / `executedBy` / `assignedTo` take a Jira user key** (`JIRAUSER10000`), never a username or an e-mail. `find_jira_user` resolves it.\n4. **TQL is strict**: spaces around operators are mandatory, string values go in double quotes, `AND` is the only connector (no `OR`), and folder paths start with `/`. Test runs are searchable by `projectKey` and `folder` only. For single/multi-choice custom fields `=` does not work — use `IN`.\n5. **Statuses, priorities and environments are case-sensitive internal names.** Built-in statuses are shown localized in the UI (the label you see for `Draft` may be a translation) while custom ones use their literal names — `get_status_options` shows what the API actually expects. A wrong execution status is silently ignored by the public API.\n6. **A BDD script is the scenario body only** — bare `Given` / `When` / `Then` / `And` / `But` lines. Text wrapped in `Feature:` / `Scenario:` is rejected with `400 Invalid BDD Script`; the wrapper is generated on export.\n7. **`STEP_BY_STEP` steps are synchronized by id on `PUT`**: a step without an id is created, a step with an id is updated, and every stored step missing from the list is **deleted**. Always send the complete final list with ids carried over from `get_test_case` — or let `add_test_steps` do the read-merge-write.\n8. **Deprecated fields are intentionally not accepted**: use `issueLinks` instead of `issueKey`, `actualEndDate` instead of `executionDate`, `executedBy` instead of `userKey`.\n\n## Quirks of older plugin builds\n\nAll of the following was observed live on a real legacy Zephyr Scale Server instance and is covered by tests.\n\n- **Cycle keys may use the `-C` prefix** (`PROJ-C34`) instead of `-R`. Every run-key parameter takes the key as-is, so pass whatever your instance shows.\n- **`GET /testrun/{key}/testresults/page` may not exist.** `get_test_run_results` and `get_test_run_summary` fall back to the deprecated flat endpoint and paginate client-side, adding a `note` to the response. A run that genuinely does not exist still surfaces as a 404.\n- **An overall `status` sent together with `scriptResults` is ignored.** Send the step results first, then set the overall status with `update_last_test_result`.\n- **`POST /testcase/link-issues` may answer 500.** Link through `update_test_case` with `issueLinks` instead.\n- **`POST /testcase/bulk` may answer HTTP 500 with an empty body** for any payload while single creation works. `create_test_cases_bulk` falls back to creating the cases one by one — on any `5xx`, and on a JSON `404` (which means \"no such endpoint on this build\", unlike the HTML `404` Jira serves when the plugin is absent) — and reports which ones succeeded ([#1](https://github.com/vilaabo/zephyr-scale-mcp/issues/1)). Every other `4xx` is a payload error and is not retried.\n- **Posting a result for a case that is not among the run's items silently adds it to the run** on this build; other builds reject the call with 400/404. The result tools document both.\n- **The custom automation-results format is validated strictly.** `{\"version\": 1, \"executions\": [{\"source\", \"result\", \"testCase\": {\"key\"}}]}` works; extra per-execution fields such as `executionTime` are rejected with `Invalid Custom Format JSON file`. Cucumber JSON reports work as-is when the scenario carries `@TestCaseKey=PROJ-T1`.\n- **`download_feature_files` requires the `tql` query parameter** — the API rejects the call without it.\n- **The last execution of a run item cannot be deleted.** `delete_test_results` rejects it; remove the item instead.\n- **Custom fields may be absent entirely** — the definitions endpoint then returns `[]`.\n\n## Development\n\n```bash\nnpm run typecheck    # tsc --noEmit, strict\nnpm test             # 1181 unit + contract tests (vitest + msw), no network\nnpm run build        # tsup -> dist/index.js\nnpm run smoke        # 13 end-to-end tests, ZEPHYR_E2E=1, real instance required\n```\n\nThe smoke scenario is skipped unless `ZEPHYR_E2E=1`. It needs a real `JIRA_BASE_URL`, credentials and a **dedicated** `ZEPHYR_DEFAULT_PROJECT_KEY` — it creates and deletes real entities and leaves `/mcp-smoke-*` folders behind, since the public API cannot delete folders.\n\n```text\nsrc/\n├── index.ts             # bootstrap: config, tool registration, stdio transport\n├── config.ts            # environment validation\n├── http.ts              # fetch wrapper: auth, timeouts, retries, error normalization,\n│                        #   multipart and binary bodies\n├── schemas.ts           # shared parameter schemas, field shapes, recurring description constants\n├── toolkit.ts           # defineTool(): strict zod input, read-only guard,\n│                        #   JSON / isError response shaping, shared helpers\n├── internal.ts          # the UNOFFICIAL /rest/tests/1.0 layer: key -> id resolution,\n│                        #   run-item plumbing, status resolution, error hints\n├── runResults.ts        # paged run results with the legacy fallback\n├── log.ts               # leveled logger, stderr only\n└── tools/               # one module per tool family: testCases, testRuns, testResults,\n                         #   testPlans, folders, attachments, automation, misc,\n                         #   runMaintenance, resultsMaintenance, internalRefs\ntest/                    # one test module per source module + negative contract tests\n                         #   and the gated smoke scenario\n```\n\n## Versioning and changelog\n\nVersion 1.0.0. Tool names, parameter names and response shapes are a public contract from this release on and change only in a major version. See [CHANGELOG.md](CHANGELOG.md).\n\n## License\n\n[MIT](LICENSE)\n",
  "bytes": 22774,
  "sha": "758a76219b13b60c88f5f2d5b20421a69938d2f7633238412920ee196173e62e",
  "repo_slug": "vilaabo/zephyr-scale-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_vilaabo_zephyr_scale_mcp_0d8ba8c5/readme"
}