Skip to main content
Glama
README.md
# tuskr-mcp-server

Implements a Model Context Protocol (MCP) server for the [Tuskr REST API](https://tuskr.app/kb/latest/api)

Built on the FastMCP Python SDK.  
Supports access token authentication.

## Tools

Each tool wraps one Tuskr REST endpoint, or two for `get_test_run_timeline`.
Endpoints are relative to `https://api.tuskr.live/api/tenant/<tenant-id>/`.

| Tool | Method | Endpoint | Purpose |
| --- | --- | --- | --- |
| `list_projects` | `GET` | `project` | Lists projects, filterable by name and by status (`active` / `archived`). |
| `create_project` | `POST` | `project` | Creates a project. `team` is required: an object mapping a member's email address to a role name, with at least one entry. Role names are tenant specific and passed through unchecked. `status` is `active` or `archived`. The API has no delete endpoint for projects, so try it with a throwaway archived project. |
| `list_test_cases` | `GET` | `test-case` | Lists a project's test cases, filterable by test suite, section, key, name, test case type and custom fields. Use it to discover the case keys a project actually contains before recording results. |
| `list_test_suites` | `GET` | `test-suite` | Lists a project's test suites, each with its embedded sections, filterable by name. Use it to find the suite and section IDs that `list_test_cases` filters on. |
| `list_test_runs` | `GET` | `test-run` | Lists a project's test runs, filterable by name, key, status and assignee. `filter_incomplete=True` walks every page and returns only runs below 100% done. |
| `add_test_case` | `POST` | `test-case` | Creates a test case in an existing suite and section. Pass `test_case_type` too: Tuskr answers "Type is required" without one unless the tenant has a default type. Custom-field keys and value shapes are tenant specific. The estimate must be at least 1 minute: Tuskr stores 0 as its default of 15. The API has no delete endpoint for test cases, so try it on a sandbox project. |
| `upsert_test_case` | `POST` | `test-case/upsert` | Creates a test case or updates an existing one. Pass exactly one of `test_case_id` (update that case) or `external_id` (update the case with that external ID, or create it when none exists). Only the fields you pass are sent, so an update cannot clear a field. The suite and section are looked up by name only, and an ID is rejected as a missing suite or section. `create_missing_suite` and `create_missing_section` create the suite or section when no such name exists, so with them set an ID passed by mistake would become the new suite's or section's name. |
| `create_test_run` | `POST` | `test-run` | Creates a test run containing all cases in the project or a named subset. |
| `copy_test_run` | `POST` | `test-run/copy` | Copies an existing run and its case selection within the same project. Results are not carried over; every case starts untested. |
| `add_test_run_results` | `POST` | `test-run-result/bulk` | Records one status against one or many cases in a run, optionally with up to two file attachments (see [Attachments](#attachments)). Pass `project` to resolve a run key or name inside that project instead of tenant-wide. Prefer a single call with a list of cases over repeated single calls. |
| `get_test_run_results` | `GET` | `test-run/<id>/results` | Fetches the cases in a run with their latest result, filterable by status and by `test_cases`. Pass `status='FAILED'` for a confirmation-test worklist. One page of 100 rows by default; `fetch_all_pages=True` walks every page. Each row's nested test case is trimmed to its ID, key, name and this run's result by default; `trim_response=False` keeps the full object, including its whole result history. Takes the run's ID, not its key. |
| `bulk_export_test_runs` | `GET` | `test-run/bulk-export` | Exports a project's test runs with their test cases and the latest result per case, in one call. Filterable by run IDs, status, completion status and assignee. Always narrow it with `filter_ids` or another filter when cases are included: a project's runs with all their cases run to tens of megabytes, so nested cases are trimmed by default (`trim_response=False` keeps them whole) and a response over the MCP size limit is replaced by an error naming the arguments that narrow it. `filter_exclude_test_cases=True` returns runs only. Run IDs are the UUIDs the API returns, not the R-numbered keys. |
| `get_test_run_timeline` | `GET` | `test-run/<id>/results`, `test-run/<id>` | Reports when testing in a run actually started and finished, derived from each case's `resultHistory`: first and last execution, span in minutes, distinct days with activity, an optional per-day breakdown and the cases never executed. Walks every results page, so it costs one call per 100 cases. Takes the run's ID, not its key. |
| `get_test_run_details` | `GET` | `test-run/<id>/details` | Fetches a run's metadata plus `firstResultAt`, `lastResultAt` and `durationInMinutes`. Takes the run's ID, not its key. The timestamps are null until a result is recorded, and Tuskr may leave them null for results recorded before it added the endpoint, so a null means unknown, not that the run has no results. |
| `set_test_run_lock` | `POST` | `test-run/set-lock` | Locks a run read-only, or unlocks it. One run per call. |
| `archive_test_runs` | `POST` | `test-run/archive` | Archives one or more runs by ID. The response is a bare boolean, so confirm the new state with `list_test_runs(filter_status='archived')`. Unarchiving is not exposed by the API and has to be done in the Tuskr UI. |
| `delete_test_runs` | `DELETE` | `test-run/bulk` | Deletes every run in the given projects that matches the filters: a case-sensitive name prefix and a minimum age in days, counted from the run's creation. Archived runs are included. It cannot target a single run by ID or key. At least one of `name_starts_with` or `older_than_days` is required, so a call cannot empty a project by accident; pass `older_than_days=0` to match runs of any age. Runs go to the recycle bin unless `purge=True`, which deletes them permanently. Returns `{"count": N}`. |
| `update_test_run` | `PUT` | `test-run/<id>` | Adds test cases to an existing run and removes test cases from it, without losing the results already recorded, and switches a run between all cases and a chosen selection. `test_cases` is the full list to keep, not a delta, and every ID is a UUID. A case that already has a result in the run cannot be removed. Read the run's current case IDs with `get_test_run_results` and find a new case's ID with `list_test_cases`. |

Result status keys such as `PASSED` and `FAILED` are configured per tenant, so use
the keys defined in your own account.

`add_test_run_results` matches a test run given by key or name across the whole
tenant by default, not within one project. When several projects have a run with
the same key, for example `R-1`, Tuskr rejects the call with `Multiple test runs
found matching: R-1` and records nothing. Pass `project` (the project ID) to look
the run up inside that project first: the key is matched exactly, ignoring case,
then the name is matched exactly, and the call fails without recording anything
when no run or several runs match. Passing the run's UUID also avoids the
ambiguity and skips the lookup; `list_test_runs` returns it for a given project.

Tuskr rate-limits every plan at 10 requests/second. The bulk tools exist for that
reason: batch where the endpoint allows it.

### Attachments

`add_test_run_results` accepts an `attachments` list of local file paths, for
screenshots and logs. Tuskr allows two files of 5 MB each per call and attaches
every file to the result record of every test case in that call, so record cases
that need different evidence in separate calls. Both limits are checked before
the request is sent.

Attachments are off by default. The tool arguments are chosen by the model, so a
plain path would let a prompt injection upload any file the server can read.
Set `TUSKR_ATTACHMENT_DIR` to the one directory the server may read from. Relative
paths resolve against it, and anything that resolves outside it, including through
a symlink, is rejected. Because the server reads the files itself, this only works
when the server runs on the machine that holds them (stdio, or an HTTP instance
with the directory mounted).

The server also exposes one resource, `resource://service_description`, which
describes its purpose to the client.

## Installation

### Environment variables / `.env` file

Set up environment variables or configure the `.env` file using the `.env.example` template.

The following environment variables are supported:

```
TUSKR_TENANT_ID=<your tenant id>
TUSKR_ACCESS_TOKEN=<your access token>
```
(this doc desc https://tuskr.app/kb/latest/api)

and optionally
```
MCP_TRANSPORT=<transport type: http or stdio>
MCP_HOST=<host for HTTP transport>
MCP_PORT=<port for HTTP transport>
TUSKR_ATTACHMENT_DIR=<directory attachments may be read from>
TUSKR_BASE_URL=<Tuskr API base URL, default https://api.tuskr.live/api/tenant/>
```

`TUSKR_BASE_URL` only needs changing to point the server at a different Tuskr
API host, such as a mock in tests. Keep the trailing slash.

In HTTP mode the `Authorization` and `Tenant-ID` headers of each request take
precedence over `TUSKR_ACCESS_TOKEN` and `TUSKR_TENANT_ID`, so one server can
serve several users with their own tokens.

## Command Line Parameters

The MCP server supports the following command line parameters:

- `--transport`: Transport type for the MCP server. Options: `http` (default) or `stdio`
- `--host`: Host address for HTTP transport (default: `0.0.0.0`)
- `--port`: Port number for HTTP transport (default: `8000`)

**Note**: The `--host` and `--port` parameters are only applicable when using the `http` transport.

**Note**: The default host `0.0.0.0` listens on every network interface, which
is what a container needs. When you run the server directly on a workstation,
pass `--host 127.0.0.1` (or set `MCP_HOST`) so it is reachable only locally.

### Default Values

- **Transport**: `http` (can be overridden with `MCP_TRANSPORT` environment variable)
- **Host**: `0.0.0.0` (can be overridden with `MCP_HOST` environment variable)
- **Port**: `8000` (can be overridden with `MCP_PORT` environment variable)

## Connect from client

### HTTP Transport (Default)

Use the following template to connect the server via HTTP:

```
{
  "mcpServers": {
    "tuskr": {
      "transport": "http",
      "url": "http://<your-mcp-dns-or-ip>/mcp/",
      "headers": {
        "Authorization": "Bearer <your access token>",
        "Tenant-ID": "<your-tuskr-tenant-id>"
      }
    }
  }
}
```

The `Authorization` is mandatory.

The `Tenant-ID` is not required and can be set on the server side using the `TUSKR_TENANT_ID` env variable. It's convenient in case you have a single MCP Server for your organization.

### Migrating from TUSKR_ACCOUNT_ID

Earlier versions of this server used the env var `TUSKR_ACCOUNT_ID` and the HTTP header
`Account-ID`. These continue to work, but emit a deprecation warning. Tuskr's own
documentation and UI consistently use the term "Tenant ID" (it is part of the REST URL
path: `/api/tenant/<tenant-id>/`), so the preferred names are now `TUSKR_TENANT_ID` and
the `Tenant-ID` HTTP header. Both names will be supported until a future major version
removes the legacy names.

To migrate an existing config, replace `TUSKR_ACCOUNT_ID` with `TUSKR_TENANT_ID`; no
other changes are required.

### stdio Transport (for local development)

For local development and integration with tools like `uvx`, use the `stdio` transport:

```
{
  "mcpServers": {
    "tuskr": {
      "transport": "stdio",
      "command": "uvx",
      "args": ["tuskr-mcp-server", "--transport", "stdio"]
    }
  }
}
```

or use `uv` with source code:

```
{
  "mcpServers": {
    "tuskr": {
      "transport": "stdio",
      "command": "uv",
      "args": [
        "--directory",
        "/path/to/your/tuskr-mcp-server",
        "run",
        "src/main.py",
        "--transport",
        "stdio"
      ]
    }
  }
}
```

## Integrate with Claude

Worked examples for Claude Code and the Claude desktop app. Both name the server
`tuskr`, so its tools appear as `list_projects`, `list_test_runs` and so on.

### Claude Code

Run the server locally over stdio, with credentials passed as environment
variables:

```
claude mcp add --transport stdio tuskr \
  --env TUSKR_TENANT_ID=<your tenant id> \
  --env TUSKR_ACCESS_TOKEN=<your access token> \
  -- uvx tuskr-mcp-server --transport stdio
```

Everything after `--` is the command Claude Code runs to start the server, kept
separate from Claude Code's own flags.

Or connect to a shared instance over HTTP:

```
claude mcp add --transport http tuskr https://<your-mcp-dns-or-ip>/mcp/ \
  --header "Authorization: Bearer <your access token>" \
  --header "Tenant-ID: <your-tuskr-tenant-id>"
```

Drop the `Tenant-ID` header if the instance already sets `TUSKR_TENANT_ID`
server-side.

`claude mcp add` stores the configuration without validating credentials, so a
wrong token is only reported when the server is first contacted. Run `/mcp`
inside a session and check that `tuskr` shows as connected.

Add `--scope project` to write the entry to the repository's `.mcp.json` and
share it with the team. Keep credentials out of a shared file: reference them as
`--env TUSKR_ACCESS_TOKEN=${TUSKR_ACCESS_TOKEN}` and let each developer supply
the value from their own environment.

### Claude desktop app

Edit `claude_desktop_config.json` (Settings, Developer, Edit Config):

- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`

For a local stdio server:

```
{
  "mcpServers": {
    "tuskr": {
      "command": "uvx",
      "args": ["tuskr-mcp-server", "--transport", "stdio"],
      "env": {
        "TUSKR_TENANT_ID": "<your tenant id>",
        "TUSKR_ACCESS_TOKEN": "<your access token>"
      }
    }
  }
}
```

`command` has to resolve on the app's own `PATH`, which is not the same as your
shell's. If the server fails to start, use the absolute path to `uvx`.

For a shared HTTP instance:

```
{
  "mcpServers": {
    "tuskr": {
      "type": "http",
      "url": "https://<your-mcp-dns-or-ip>/mcp/",
      "headers": {
        "Authorization": "Bearer <your access token>",
        "Tenant-ID": "<your-tuskr-tenant-id>"
      }
    }
  }
}
```

The `type` key is required on any entry that uses `url`; without it the entry is
read as a stdio server and skipped. Restart the app after editing the file.

### Check it works

Ask Claude to list your Tuskr projects. It should call `list_projects` and come
back with project names and IDs. A useful follow-up, since it exercises a filter
and a second tool: ask which test runs in a given project are not yet finished,
which resolves to `list_test_runs` with `filter_incomplete`.

A request such as "mark C-14 and C-15 as passed in run R-3" resolves to a
single `add_test_run_results` call. Bear in mind that several of these tools
write to Tuskr, so point the server at a sandbox project while you are still
exploring.

## Development

### Setup

1. Clone repo
2. Install development dependencies:
`uv sync --dev`
3. Create `.env` from `.env.example`

### Running MCP service

#### HTTP Transport (Default)
```
uv run --env-file .env src/main.py
```

#### stdio Transport (for local development)
```
uv run --env-file .env src/main.py --transport stdio
```

#### Custom Host/Port
```
uv run --env-file .env src/main.py --host 127.0.0.1 --port 9000
```

### Running tests

The project uses pytest for testing. The following command will run all tests

```
uv run pytest -vsx
```

### Running linters

The project uses the `ruff` tool as a linter.

CI runs both of these and fails on any finding, so run them before pushing:

```
uv run ruff format --check .
uv run ruff check .
```

`uv run ruff format .` rewrites the files in place to fix formatting.

### Dockerization

The following command allows to build a docker image
```
docker build -t tuskr-mcp .
```

and then run it with your `.env` file, publishing the server port:
```
docker run -it --rm --env-file .env -p 8000:8000 tuskr-mcp
```

The server is then reachable at `http://localhost:8000/mcp/`. If your `.env`
sets `MCP_PORT`, publish that port instead.

TDQS

A3.6/5.0

Scored across 16 tools

Disambiguation4/5

Most tools target clearly distinct operations across test runs, test cases, results, suites, and projects. The main overlap is add_test_case vs upsert_test_case and the several run-reading tools, but the detailed descriptions explicitly differentiate them, reducing misselection risk.

Naming Consistency5/5

All tool names use snake_case with a predictable verb_noun structure such as create_test_run, get_test_run_results, and list_test_cases. Minor compound verbs like bulk_export are still readable and consistent with the overall convention.

Tool Count4/5

Sixteen tools is slightly above the ideal compact range but justified by the breadth of the test-management domain: runs, cases, results, suites, projects, export, archiving, and locking. Each tool maps to a reasonably distinct operation rather than redundant surface area.

Completeness3/5

The core lifecycle for test runs, test cases, and results is covered, including creation, listing, result recording, locking, archiving, and export. However, there are notable gaps: no dedicated suite/section management beyond listing, no project creation/update, no test run metadata editing, and no restore-from-archive operation.

Maintenance

ActivityActive
ResponsivenessUnresponsive