# zephyr-scale-mcp [Health: Active]

**Category:** 🏢 Workplace & Productivity  
**Repository:** https://github.com/vilaabo/zephyr-scale-mcp  
**GitHub Stars:** 0  
**Views:** 0  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/zephyr-scale-mcp

## Description
Zephyr Scale on self-hosted Jira Server/DC: test cases, cycles, executions, plans and attachments

## Claude Desktop Quick Installation
Install path detected from listing signals. Uses `npx` (confidence: high):

```json
"mcpServers": {
  "zephyr-scale-mcp": {
    "command": "npx",
    "args": ["-y","zephyr-scale-mcp"]
  }
}
```

## Documentation & README

# zephyr-scale-mcp

**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`.

![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)

npm: [`zephyr-scale-mcp`](https://www.npmjs.com/package/zephyr-scale-mcp) · MCP Registry: `io.github.vilaabo/zephyr-scale-mcp` · [Русская версия →](https://github.com/vilaabo/zephyr-scale-mcp/blob/HEAD/README.ru.md)

> ⚠️ **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.

## Why this exists

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

- **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.
- **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`.
- **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.
- **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.
- **1181 unit and contract tests** (vitest + msw, no network) plus a gated end-to-end scenario against a real instance.

## Quick start

Requirements: 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.

Nothing to clone — the package is on npm. **Claude Code**:

```bash
claude mcp add zephyr-scale \
  --env JIRA_BASE_URL=https://jira.example.com \
  --env JIRA_PAT=<personal access token> \
  --env ZEPHYR_DEFAULT_PROJECT_KEY=PROJ \
  --env ZEPHYR_ALLOW_INTERNAL_API=true \
  -- npx -y zephyr-scale-mcp
```

**Claude Desktop / any MCP client** (`claude_desktop_config.json`, `.mcp.json`):

```json
{
  "mcpServers": {
    "zephyr-scale": {
      "command": "npx",
      "args": ["-y", "zephyr-scale-mcp"],
      "env": {
        "JIRA_BASE_URL": "https://jira.example.com",
        "JIRA_PAT": "<personal access token>",
        "ZEPHYR_DEFAULT_PROJECT_KEY": "PROJ",
        "ZEPHYR_ALLOW_INTERNAL_API": "true"
      }
    }
  }
}
```

<details>
<summary>Running from source instead</summary>

```bash
git clone https://github.com/vilaabo/zephyr-scale-mcp.git
cd zephyr-scale-mcp
npm install
npm run build        # -> dist/index.js
```

Then point the client at the build: `"command": "node", "args": ["/path/to/zephyr-scale-mcp/dist/index.js"]`.

</details>

> **`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.

Then 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`.

## What you can ask your agent to do

- *"Create the folder `/Regression/Payments` and add step-by-step test cases for the checkout flow described in this document."*
- *"Find every Draft case in `/Regression`, review them, and set the ready ones to Approved."*
- *"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."*
- *"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.
- *"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.
- *"Add two steps to PROJ-T55 after step 3."* — existing steps and their ids survive.
- *"Recreate cycle PROJ-R7 with three more cases, keep the results, and delete the original."*

## Configuration

| Variable | Required | Default | Purpose |
|---|---|---|---|
| `JIRA_BASE_URL` | yes | — | Jira base URL without a trailing `/`, e.g. `https://jira.example.com` |
| `JIRA_AUTH` | no | `pat` | `pat` \| `basic` |
| `JIRA_PAT` | with `pat` | — | Jira Server/DC Personal Access Token |
| `JIRA_USERNAME`, `JIRA_PASSWORD` | with `basic` | — | Basic-auth credentials |
| `JIRA_TIMEOUT_MS` | no | `30000` | Per-request timeout |
| `JIRA_MAX_RETRIES` | no | `2` | Retries for GET and for any `429`/`503`, honoring `Retry-After`, otherwise exponential backoff with jitter |
| `JIRA_TLS_REJECT_UNAUTHORIZED` | no | `true` | `false` accepts self-signed certificates — this disables TLS verification process-wide and prints a warning to stderr |
| `ZEPHYR_DEFAULT_PROJECT_KEY` | no | — | Used whenever a tool is called without `projectKey` |
| `ZEPHYR_READONLY` | no | `false` | `true` makes every write tool refuse with an error; read tools keep working |
| `ZEPHYR_ALLOW_INTERNAL_API` | no | `false` | `true` registers the 12 UNOFFICIAL tools backed by the internal `/rest/tests/1.0` API |
| `ZEPHYR_LOG_LEVEL` | no | `info` | `debug` \| `info` \| `warn` \| `error` |

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

Configuration is validated at startup: all problems are reported at once and the process exits with a non-zero code rather than starting half-configured.

## Tools

54 tools. The 42 public ones are always registered; the 12 in the last group only with `ZEPHYR_ALLOW_INTERNAL_API=true`.

<details>
<summary><b>Test cases</b> — 13 tools</summary>

| Tool | What it does |
|---|---|
| `create_test_case` | Create a case with a `STEP_BY_STEP` / `PLAIN_TEXT` / `BDD` script, parameters, custom fields, Call-to-Test steps |
| `get_test_case` | Read a case, optionally narrowed by `fields`; step ids come back here |
| `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`) |
| `update_test_case` | Partial update; `testScript.steps` is synchronized by id (see limitation 7) |
| `add_test_steps` | Insert steps at a position without losing the existing ones — read, merge by id, write back |
| `set_test_script` | Replace the whole script or change its format; destructive by design |
| `clone_test_case` | Copy a case inside its project with fresh step ids; links, attachments and history are not copied |
| `move_test_cases_to_folder` | Bulk-move by explicit keys or by source folder; a failing case does not abort the rest |
| `delete_test_case` | Permanent delete of the case, its script and its history |
| `create_test_cases_bulk` | Create many cases in one call, with a per-case fallback on broken bulk endpoints |
| `link_issues_to_test_cases` | Bulk-link cases to Jira issues (additive) |
| `get_test_cases_linked_to_issue` | Reverse lookup: issue → cases |
| `get_issue_test_coverage` | Traceability report: issue → linked cases → latest execution of each |

</details>

<details>
<summary><b>Test cycles (test runs)</b> — 7 tools</summary>

| Tool | What it does |
|---|---|
| `create_test_run` | Create a cycle with its complete item list — optionally with each item's execution result in the same call |
| `get_test_run` | Read a cycle including its items |
| `search_test_runs` | TQL search — for runs only `projectKey` and `folder` are searchable |
| `delete_test_run` | Permanent delete of the cycle and all its results |
| `get_test_run_results` | Page through the executions of a cycle, with the legacy flat-endpoint fallback |
| `get_test_run_summary` | Last execution per item: `byStatus` counts verbatim, `executionProgressPct`, `passRatePct` when a literal `Pass` status exists |
| `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 |

</details>

<details>
<summary><b>Executions (test results)</b> — 4 tools</summary>

| Tool | What it does |
|---|---|
| `create_test_result` | Append a new execution to a run item, including per-step `scriptResults` |
| `update_last_test_result` | Partial update of the most recent execution of an item |
| `create_test_results_bulk` | Many executions for one cycle in a single call |
| `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 |

Items that exist several times in a run (per environment or per assignee) are disambiguated with `matchEnvironment` / `matchUserKey`, sent as query parameters.

</details>

<details>
<summary><b>Test plans</b> — 5 tools</summary>

| Tool | What it does |
|---|---|
| `create_test_plan` | Create a plan; returns `{ key }`, e.g. `PROJ-P123` |
| `get_test_plan` | Read a plan with its linked runs and issues |
| `update_test_plan` | Partial update |
| `delete_test_plan` | Permanent delete |
| `search_test_plans` | TQL search; the searchable field set varies by Zephyr Scale version |

</details>

<details>
<summary><b>Folders</b> — 2 tools</summary>

| Tool | What it does |
|---|---|
| `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 |
| `rename_folder` | Rename one folder segment by its numeric id (and optionally set its custom fields) |

</details>

<details>
<summary><b>Attachments</b> — 4 tools</summary>

| Tool | What it does |
|---|---|
| `upload_attachment` | Attach a local file to a case, a case step, a cycle, a result or a result step (multipart) |
| `list_attachments` | List the attachments of any of those targets; each record carries the id and url the other tools need |
| `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` |
| `delete_attachment` | Permanently delete one attachment by numeric id |

</details>

<details>
<summary><b>Automation</b> — 3 tools</summary>

| Tool | What it does |
|---|---|
| `upload_automation_results` | Publish a ZIP of results in Zephyr's custom JSON format; always creates a new cycle |
| `upload_cucumber_results` | Publish a ZIP of Cucumber JSON reports; scenarios are matched by their `@TestCaseKey=PROJ-T1` tag |
| `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 |

</details>

<details>
<summary><b>Service and diagnostics</b> — 4 tools</summary>

| Tool | What it does |
|---|---|
| `health_check` | Jira reachability, credentials, and whether the Zephyr plugin answers |
| `list_environments` | The project's environments — the exact case-sensitive names results reference |
| `create_environment` | Create an environment in the project |
| `find_jira_user` | Resolve the Jira **user key** (`JIRAUSER10000`) that `owner` / `executedBy` / `assignedTo` require |

</details>

<details>
<summary><b>UNOFFICIAL — internal API, opt-in</b> — 12 tools</summary>

Registered **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.

| Tool | What it does |
|---|---|
| `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 |
| `add_test_cases_to_run` | Append cases to an existing cycle in place; the key and the existing results survive |
| `remove_test_cases_from_run` | Remove items from an existing cycle in place — their whole execution history dies with them |
| `reorder_test_run_items` | Reorder the items of an existing cycle; a cycle already in the requested order makes no write at all |
| `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 |
| `link_test_run_to_plan` | Associate an existing cycle with a test plan after creation (the public API accepts `testPlanKey` only at creation) |
| `delete_test_results` | Delete individual executions by numeric id. The **last** execution of an item cannot be deleted |
| `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 |
| `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 |
| `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 |
| `get_custom_field_definitions` | Custom field definitions per entity type: names, types, required flags, options |
| `delete_folder` | Delete a folder by numeric id. What happens to a non-empty folder is version-specific — empty it first |

</details>

## Working around API v1

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

1. **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).
2. **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.
3. **`owner` / `executedBy` / `assignedTo` take a Jira user key** (`JIRAUSER10000`), never a username or an e-mail. `find_jira_user` resolves it.
4. **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`.
5. **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.
6. **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.
7. **`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.
8. **Deprecated fields are intentionally not accepted**: use `issueLinks` instead of `issueKey`, `actualEndDate` instead of `executionDate`, `executedBy` instead of `userKey`.

## Quirks of older plugin builds

All of the following was observed live on a real legacy Zephyr Scale Server instance and is covered by tests.

- **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.
- **`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.
- **An overall `status` sent together with `scriptResults` is ignored.** Send the step results first, then set the overall status with `update_last_test_result`.
- **`POST /testcase/link-issues` may answer 500.** Link through `update_test_case` with `issueLinks` instead.
- **`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.
- **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.
- **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`.
- **`download_feature_files` requires the `tql` query parameter** — the API rejects the call without it.
- **The last execution of a run item cannot be deleted.** `delete_test_results` rejects it; remove the item instead.
- **Custom fields may be absent entirely** — the definitions endpoint then returns `[]`.

## Development

```bash
npm run typecheck    # tsc --noEmit, strict
npm test             # 1181 unit + contract tests (vitest + msw), no network
npm run build        # tsup -> dist/index.js
npm run smoke        # 13 end-to-end tests, ZEPHYR_E2E=1, real instance required
```

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

```text
src/
├── index.ts             # bootstrap: config, tool registration, stdio transport
├── config.ts            # environment validation
├── http.ts              # fetch wrapper: auth, timeouts, retries, error normalization,
│                        #   multipart and binary bodies
├── schemas.ts           # shared parameter schemas, field shapes, recurring description constants
├── toolkit.ts           # defineTool(): strict zod input, read-only guard,
│                        #   JSON / isError response shaping, shared helpers
├── internal.ts          # the UNOFFICIAL /rest/tests/1.0 layer: key -> id resolution,
│                        #   run-item plumbing, status resolution, error hints
├── runResults.ts        # paged run results with the legacy fallback
├── log.ts               # leveled logger, stderr only
└── tools/               # one module per tool family: testCases, testRuns, testResults,
                         #   testPlans, folders, attachments, automation, misc,
                         #   runMaintenance, resultsMaintenance, internalRefs
test/                    # one test module per source module + negative contract tests
                         #   and the gated smoke scenario
```

## Versioning and changelog

Version 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](https://github.com/vilaabo/zephyr-scale-mcp/blob/HEAD/CHANGELOG.md).

## License

[MIT](https://github.com/vilaabo/zephyr-scale-mcp/blob/HEAD/LICENSE)

