From 8284e32a1e573c874388cbfc69d83d8a80501248 Mon Sep 17 00:00:00 2001 From: Denys Kuchma Date: Wed, 30 Sep 2026 19:27:52 +0200 Subject: [PATCH] Squash MCP tools into one CLI-style tool per entity --- README.md | 48 +- docs/tools.md | 1831 ++-------- src/index.js | 7 +- src/mcp/definitions/attachments.js | 70 - src/mcp/definitions/branches.js | 138 +- src/mcp/definitions/entity-tool.js | 47 + src/mcp/definitions/issues.js | 122 +- src/mcp/definitions/labels.js | 181 +- src/mcp/definitions/milestones.js | 64 +- src/mcp/definitions/params.js | 76 + src/mcp/definitions/plans.js | 376 +- src/mcp/definitions/requirements.js | 169 +- src/mcp/definitions/rungroups.js | 156 +- src/mcp/definitions/runs.js | 403 +-- src/mcp/definitions/shares.js | 110 - src/mcp/definitions/snippets.js | 183 +- src/mcp/definitions/steps.js | 183 +- src/mcp/definitions/suites.js | 405 +-- src/mcp/definitions/tags.js | 60 +- src/mcp/definitions/testruns.js | 394 +-- src/mcp/definitions/tests.js | 408 +-- src/mcp/entity-commands.js | 28 + src/mcp/list-projection.js | 6 +- src/mcp/registry/attachments.js | 2 +- src/mcp/registry/handlers.js | 1 + src/mcp/tool-definitions.js | 83 +- src/mcp/tool-profiles.js | 56 +- src/mcp/tool-registry.js | 39 +- test/__snapshots__/tools-list.test.js.snap | 3688 ++++++-------------- test/command-dispatch.test.js | 150 + test/shares.test.js | 72 +- test/tools-list.test.js | 13 + worker/test/mcp-endpoint.test.js | 11 +- worker/test/oauth-flow.test.js | 2 +- worker/test/token-revocation.test.js | 2 +- 35 files changed, 2619 insertions(+), 6965 deletions(-) delete mode 100644 src/mcp/definitions/attachments.js create mode 100644 src/mcp/definitions/entity-tool.js create mode 100644 src/mcp/definitions/params.js delete mode 100644 src/mcp/definitions/shares.js create mode 100644 src/mcp/entity-commands.js create mode 100644 test/command-dispatch.test.js diff --git a/README.md b/README.md index f2620ec..16173f0 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,15 @@ Model Context Protocol (MCP) server that enables AI assistants (Claude, Cursor, ## Features -- **Full CRUD** for core entities: - - Tests, Suites, Plans, Runs, TestRuns, RunGroups, Steps, Snippets, Labels - - Tags and Milestones (read-only access) - - Issues (global + scoped helpers for tests/suites/runs/testruns/plans) - - Attachments (scoped helpers for tests/suites/testruns) - - Requirements (including file uploads from local file paths) +- **One CLI-style tool per entity** - `tests`, `suites`, `runs`, `testruns`, `plans`, `rungroups`, `steps`, `snippets`, `labels`, `requirements`, `branches`, `tags`, `milestones`, `issues`; each takes a `command` argument (`list`, `get`, `create`, `update`, `delete`, plus scoped `issues_*`/`attachments_*`/`share` commands where applicable) and flat command params - **Project Information** - fetch project configuration, metadata, features, and CI profiles -- **Issue Linking** - link/unlink issues to any resource +- **Issue Linking** - link/unlink issues to any resource (`issues_*` commands on each entity, plus the global `issues` tool) +- **Attachments** - `attachments_*` commands on tests, suites, and testruns; upload sends a local file path as multipart field `files` +- **Requirements** - including file uploads from local file paths - **API Compatibility** - automatic handling of payload format differences (flat vs wrapped) - **Automatic API Sessions** - groups MCP changes in Testomat.io history using API sessions - **Run Management** - status transitions via `status_event` parameter -- **TQL-Only Search** - `tests_list` and `runs_list` use `tql` as the single search/filter input +- **TQL-Only Search** - `tests`/`runs` with `command: "list"` use `tql` as the single search/filter input - **Built-In TQL Reference** - TQL parameters include the exact field whitelist and examples; `tql_help` provides syntax details on demand - **Tool Surface Profiles** - expose only the tools a session needs via `--tools full|core|read` (default `full`); cuts the per-call schema cost for long agentic sessions @@ -61,8 +58,8 @@ npx testomatio-mcp --token --project --tools core | Profile | What's exposed | |---------|----------------| | `full` (default) | Everything | -| `core` | Core entities + CRUD (excludes steps, snippets, labels, rungroups, attachments) | -| `read` | Core entities, read-only (list/get) | +| `core` | Core entities with all commands (excludes the steps, snippets, labels, rungroups tools) | +| `read` | Core entities restricted to read-only commands (`list`, `get`, `search`, `issues_list`, `attachments_list`) | Values are case-insensitive; an unknown value prevents the server from starting. Set the profile at launch with the flag or the `TESTOMATIO_TOOLS` environment variable — it can't be changed mid-session. The CLI flag takes precedence when both are set. @@ -193,19 +190,22 @@ Self-hosted installations keep using stdio. ## Quick Examples +Every entity tool works like a CLI: pass `command` plus the params that command needs. Each param description in the tool schema lists the commands it applies to. + **List tests:** ```json { - "name": "tests_list", - "arguments": { "page": 1, "per_page": 50, "tql": "priority == 'high'" } + "name": "tests", + "arguments": { "command": "list", "page": 1, "per_page": 50, "tql": "priority == 'high'" } } ``` **Create test:** ```json { - "name": "tests_create", + "name": "tests", "arguments": { + "command": "create", "title": "User login test", "suite_id": "123", "priority": "high" @@ -216,8 +216,9 @@ Self-hosted installations keep using stdio. **Create run:** ```json { - "name": "runs_create", + "name": "runs", "arguments": { + "command": "create", "title": "Smoke tests", "kind": "automated", "env": "production" @@ -228,8 +229,9 @@ Self-hosted installations keep using stdio. **Finish run:** ```json { - "name": "runs_update", + "name": "runs", "arguments": { + "command": "update", "run_id": "456", "status_event": "finish" } @@ -239,8 +241,9 @@ Self-hosted installations keep using stdio. **Upload attachment to a test:** ```json { - "name": "tests_attachments_upload", + "name": "tests", "arguments": { + "command": "attachments_upload", "test_id": "123", "file_path": "/path/to/screenshot.png" } @@ -312,14 +315,15 @@ NODE_EXTRA_CA_CERTS=/path/to/company-root-ca.pem testomatio-mcp --token ## Important Notes -- **Run Status** - Use `runs_update` with `status_event` for transitions (finish, launch, rerun, etc.) -- **Search/Filter** - No dedicated `/search` endpoints; filtering is done via the `*_list` tools (`tql` for tests and runs, OpenAPI-aligned filters for other entities) -- **Slim List Responses** - List tools request compact API responses by default and omit heavy entity fields and null values. Pass `verbose: true` to return full objects, or `fields: ["id", "title", "description"]` to return only selected fields. Both options disable the backend `slim=true` request so heavy fields remain available when requested. -- **TQL** - Use `tql` as the single search/filter input for `tests_list` and `runs_list` +- **CLI-style Tools** - One tool per entity; all operations are the `command` argument (`tests` + `command: "list"` instead of the old `tests_list`). Unknown commands return an error listing the valid ones +- **Run Status** - Use `runs` with `command: "update"` and `status_event` for transitions (finish, launch, rerun, etc.) +- **Search/Filter** - No dedicated `/search` endpoints; filtering is done via the `list` command (`tql` for tests and runs, OpenAPI-aligned filters for other entities) +- **Slim List Responses** - List commands request compact API responses by default and omit heavy entity fields and null values. Pass `verbose: true` to return full objects, or `fields: ["id", "title", "description"]` to return only selected fields. Both options disable the backend `slim=true` request so heavy fields remain available when requested. +- **TQL** - Use `tql` as the single search/filter input for `tests`/`runs` with `command: "list"` - **TQL Syntax** - For user-facing syntax details and more examples, see the official TQL docs: https://docs.testomat.io/advanced/tql/ - **TQL Scope** - TQL parameter descriptions keep the documented field whitelist in-band; call `tql_help` for syntax details and additional examples -- **Issue Linking** - Scoped helpers available: `{entity}_issues_link/unlink` -- **Attachments** - Scoped helpers available for tests, suites, and testruns: `{entity}_attachments_list/upload/delete`. Upload sends one local file path as multipart field `file`. +- **Issue Linking** - Scoped commands available on each entity: `command: "issues_link"` / `"issues_unlink"` +- **Attachments** - Scoped commands available on tests, suites, and testruns: `command: "attachments_list" | "attachments_upload" | "attachments_delete"`. Upload sends one local file path as multipart field `files`. - **Enterprise Package** - Analytics tools are intentionally exposed only by `@testomatio/mcp-enterprise`, not by the standard `@testomatio/mcp` package - **API Sessions** - The server automatically starts a Testomat.io session before the first `POST`, `PUT`, or `DELETE` request, sends the returned session hash as `X-Session-Hash` on later mutating requests, and stops the session when the MCP server shuts down. `GET` requests do not start or use sessions. diff --git a/docs/tools.md b/docs/tools.md index e51c8f7..2289328 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -2,40 +2,58 @@ Complete reference for the MCP tools available in the Testomat.io MCP Server. +## Calling Convention + +Every entity is exposed as **one CLI-style tool** named after the entity (`tests`, `suites`, `runs`, ...). All operations are passed as the required `command` argument, and command params are passed as flat properties next to it: + +```json +{ + "name": "tests", + "arguments": { + "command": "list", + "tql": "priority == 'high'" + } +} +``` + +- Each param in the tool schema is prefixed with the commands it applies to, e.g. `(get|update|delete) Test ID`. +- An unknown or missing `command` returns an error listing the valid commands for the tool. +- Command-specific required params are validated at runtime with descriptive errors (a flat JSON schema cannot express per-command `required`). +- Command-less singleton tools (`system_ping`, `tql_help`, `project_info`) take no arguments. + ## Table of Contents - [Tool Surface Profiles](#tool-surface-profiles) - [System Tools](#system-tools) - [Project Tools](#project-tools) -- [Test Management](#test-management) -- [Suite Management](#suite-management) -- [Share Management](#share-management) -- [Run Management](#run-management) -- [TestRun Management](#testrun-management) -- [Plan Management](#plan-management) -- [RunGroup Management](#rungroup-management) -- [Step Management](#step-management) -- [Snippet Management](#snippet-management) -- [Label Management](#label-management) -- [Tag Management](#tag-management) -- [Milestone Management](#milestone-management) -- [Issue Management](#issue-management-global) -- [Attachment Management](#attachment-management) -- [Requirement Management](#requirement-management) -- [Branch Management](#branch-management) +- [tests](#tests) +- [suites](#suites) +- [runs](#runs) +- [testruns](#testruns) +- [plans](#plans) +- [rungroups](#rungroups) +- [steps](#steps) +- [snippets](#snippets) +- [labels](#labels) +- [tags](#tags) +- [milestones](#milestones) +- [issues](#issues) +- [requirements](#requirements) +- [branches](#branches) +- [Common Patterns](#common-patterns) - [Enterprise Analytics](#enterprise-analytics) --- ## Tool Surface Profiles -Every exposed tool's schema is sent to the model on each call, so the full tool set has a significant token cost. Use the `--tools` flag to expose only a subset — useful for long, token-sensitive sessions. +Every exposed tool's schema is sent to the model on each call, so the tool set has a significant token cost. Use the `--tools` flag to expose only a subset — useful for long, token-sensitive sessions. | Profile | Description | |---------|-------------| -| `full` (default) | All tools | -| `core` | Core entities + CRUD. Excludes steps, snippets, labels, rungroups, attachments | -| `read` | Core entities, read-only (list/get) | +| `full` (default) | All tools with all commands | +| `core` | Core entities with all commands. Excludes the `steps`, `snippets`, `labels`, `rungroups` tools | +| `read` | Core entities restricted to read-only commands (`list`, `get`, `search`, `issues_list`, `attachments_list`) | ```bash testomatio-mcp --token --project --tools core @@ -95,1584 +113,444 @@ enabled features, and CI profiles. --- -## Test Management - -### tests_list - -List all tests in the project with filtering. - -Use `tql` for search/filtering. -TQL means `Testomat.io Query Language`. -Use standard TQL syntax such as `==`, `!=`, `in [...]`, `%`, `and`, `or`, `not`, and parentheses. -For the full syntax and field reference, see the official docs: https://docs.testomat.io/advanced/tql/ - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| page | integer | No | Page number (min: 1) | -| per_page | integer | No | Items per page (min: 1, max: 100) | -| tql | string | No | TQL filter for tests. Examples: `priority == 'high'`, `state == 'automated'`, `suite % 'Checkout'` | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | - -**Example:** -```json -{ - "name": "tests_list", - "arguments": { - "page": 1, - "per_page": 50, - "tql": "priority == 'high'" - } -} -``` - -**API Endpoint:** `GET /api/v2/{project_id}/tests` - ---- - -### tests_get - -Get a specific test by ID. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| test_id | string | Yes | Test ID | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | - -**Example:** -```json -{ - "name": "tests_get", - "arguments": { - "test_id": "12345" - } -} -``` - -**API Endpoint:** `GET /api/v2/{project_id}/tests/{id}` - ---- - -### tests_create - -Create a new test. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| title | string | Yes | Test title | -| suite_id | string | Yes | Parent suite ID | -| description | string | No | Test description | -| emoji | string | No | Test emoji icon | -| priority | string | No | One of: `low`, `normal`, `important`, `high`, `critical` | -| assigned_to | string | No | Assignee ID | -| code | string | No | Test code/automation reference | -| state | string | No | One of: `manual`, `detached`, `automated` | -| link | array | No | Links to labels, tags, milestones, issues, or jira | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | - -**Link Array Format:** +## tests + +Manage tests. `/api/v2/{project_id}/tests` + +**Commands:** + +| Command | Description | Runtime-required params | +|---------|-------------|------------------------| +| `list` | List tests (tql filter, pagination) | — | +| `get` | Get test by ID | `test_id` | +| `create` | Create test | `title`, `suite_id` | +| `update` | Update test | `test_id` | +| `delete` | Delete test | `test_id` | +| `share` | Share tests into a suite of another project | `target_project_id`, `target_suite_id`, plus a selection (`test_ids` and/or `labels`) | +| `unshare` | Remove a test's share (converts the shared copy back into a regular test) | `test_id` | +| `issues_list` | List linked issues for a test | `test_id` | +| `issues_link` | Link issue to a test | `test_id`, plus exactly one of `url`/`jira_id` | +| `issues_unlink` | Unlink issue from a test | `issue_id`, `type` | +| `attachments_list` | List attachments for a test | `test_id` | +| `attachments_upload` | Upload one attachment to a test | `test_id`, `file_path` | +| `attachments_delete` | Delete attachment from a test | `test_id`, `attachment_id` | + +**Parameters:** + +| Name | Type | Commands | Description | +|------|------|----------|-------------| +| test_id | string | get, update, delete, unshare, issues_*, attachments_* | Test ID (for `unshare`: ID of the shared test copy to unlink) | +| title | string | create, update | Test title | +| suite_id | string | create, update | Suite to place the test in | +| description | string | create, update | Test description | +| emoji | string | create, update | Emoji icon | +| priority | string | create, update | `low`, `normal`, `important`, `high`, `critical` | +| assigned_to | string | create, update | Assignee | +| code | string | create, update | Test code | +| state | string | create, update | `manual`, `detached`, `automated` | +| sync | boolean | update | Sync flags | +| link | array | create, update | Link actions, see [Link Parameter Structure](#link-parameter-structure) | +| tql | string | list | TQL filter for tests. Examples: `priority == 'high'`, `state == 'automated'`, `suite % 'Checkout'` | +| branch | string | list, get, create, update, delete | Branch slug, see [Branch Scoping](#branch-scoping) | +| page / per_page | integer | list, issues_list | Pagination | +| source | string | issues_list | Filter issues by source (e.g. `jira`) | +| url | string | issues_link | Issue URL to link | +| jira_id | string | issues_link | Jira issue key to link (alternative to `url`) | +| issue_id | integer | issues_unlink | ID of the linked issue to remove | +| type | string | issues_unlink | `issue` or `jira_issue` | +| file_path | string | attachments_upload | Local path to the file sent as multipart field `files` | +| attachment_id | string | attachments_delete | ID of the attachment to delete | +| test_ids | string[] | share | Test IDs to share. Max 1000 per request | +| labels | string[] | share | Label slugs/titles; every test carrying any of these labels is shared, in addition to test_ids | +| target_project_id | string | share | Project ID (slug) of the destination project | +| target_suite_id | string | share | Suite ID in the target project (must be a file-type suite) | + +**Sharing semantics:** the source project stays the single source of truth; shared copies in the target project are read-only until unlinked. Re-sharing into the same target project does not duplicate. Requests are processed asynchronously — status `queued` means accepted, not completed. Matched tests that are themselves shared copies are skipped and listed in `skipped_test_ids`. Source and target projects must be of the same type (Classic/BDD). + +**Examples:** ```json { - "link": [ - { - "action": "add|remove", - "type": "label|custom_field|tag|milestone|issue|jira|requirement", - "value": "identifier" - } - ] + "name": "tests", + "arguments": { "command": "list", "page": 1, "per_page": 50, "tql": "priority == 'high'" } } ``` -**Example:** ```json { - "name": "tests_create", + "name": "tests", "arguments": { + "command": "create", "title": "User login test", "suite_id": "123", - "priority": "high", - "link": [ - { "action": "add", "type": "label", "value": "smoke" }, - { "action": "add", "type": "tag", "value": "auth" } - ] + "priority": "high" } } ``` -**API Endpoint:** `POST /api/v2/{project_id}/tests` - ---- - -### tests_update - -Update an existing test. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| test_id | string | Yes | Test ID | -| title | string | No | New test title | -| suite_id | string | No | New parent suite ID | -| description | string | No | Updated description | -| emoji | string | No | Test emoji | -| priority | string | No | One of: `low`, `normal`, `important`, `high`, `critical` | -| assigned_to | string | No | Assignee ID | -| code | string | No | Test code | -| state | string | No | One of: `manual`, `detached`, `automated` | -| sync | boolean | No | Sync with automation | -| link | array | No | Link updates | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | - -**Example:** ```json { - "name": "tests_update", + "name": "tests", "arguments": { - "test_id": "12345", - "title": "Updated test title", - "priority": "critical" + "command": "share", + "test_ids": ["be779025", "sgqat108"], + "target_project_id": "sugar-king", + "target_suite_id": "e73d559c" } } ``` -**API Endpoint:** `PUT /api/v2/{project_id}/tests/{id}` - --- -### tests_delete - -Delete a test. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| test_id | string | Yes | Test ID | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | - -**Example:** -```json -{ - "name": "tests_delete", - "arguments": { - "test_id": "12345" - } -} -``` - -**API Endpoint:** `DELETE /api/v2/{project_id}/tests/{id}` +## suites ---- +Manage suites as a tree. `/api/v2/{project_id}/suites` -### tests_issues_list +**Commands:** -List linked issues for a test. +| Command | Description | Runtime-required params | +|---------|-------------|------------------------| +| `list` | List suites as tree (file_type, tag, labels, search_text filters) | — | +| `get` | Get suite by ID | `suite_id` | +| `create` | Create suite | `title` | +| `update` | Update suite | `suite_id` | +| `delete` | Delete suite | `suite_id` | +| `share` | Share suites (with their tests) into one or more other projects | `target_project_ids`, plus a selection (`suite_ids` and/or `labels`) | +| `unshare` | Remove a suite's share (converts the shared copy back into a regular suite) | `suite_id` | +| `issues_list` / `issues_link` / `issues_unlink` | Scoped issue operations | as on `tests` | +| `attachments_list` / `attachments_upload` / `attachments_delete` | Scoped attachment operations | as on `tests` | **Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| test_id | string | Yes | Test ID | -| page | integer | No | Page number | -| per_page | integer | No | Items per page | -| source | string | No | Filter by issue source | - -**Example:** -```json -{ - "name": "tests_issues_list", - "arguments": { - "test_id": "12345" - } -} -``` - -**API Endpoint:** `GET /api/v2/{project_id}/issues?test_id=...` - ---- - -### tests_issues_link -Link an issue to a test. - -**Parameters:** -| Name | Type | Required | Description | +| Name | Type | Commands | Description | |------|------|----------|-------------| -| test_id | string | Yes | Test ID | -| url | string | No* | Issue URL | -| jira_id | string | No* | Jira issue ID | - -*Either url or jira_id required +| suite_id | string | get, update, delete, unshare, issues_*, attachments_* | Suite ID (for `unshare`: ID of the shared suite copy to unlink) | +| title | string | create, update | Suite title | +| description | string | create, update | Suite description | +| emoji | string | create, update | Emoji icon | +| parent_id | string | create, update | Parent suite ID | +| file_type | string | list, create, update | `file` or `folder` | +| assigned_to | string | create, update | Assignee | +| file | string | create, update | File reference | +| children | array | create, update | Child items | +| link | array | create, update | Link actions (supports `requirement` type), see [Link Parameter Structure](#link-parameter-structure) | +| tag | string | list | Filter by tag title | +| labels | string \| string[] | list, share | list: filter by labels; share: label selection for sharing | +| search_text | string | list | Text search | +| branch | string | list, get, create, update, delete | Branch slug, see [Branch Scoping](#branch-scoping) | +| page / per_page | integer | list, issues_list | Pagination | +| suite_ids | string[] | share | Suite IDs to share. Max 200 per request | +| target_project_ids | string[] | share | Project IDs (slugs) of the destination projects | +| destination_folder_id | string | share | Folder suite ID in the target project (only allowed when sharing to a single target project) | -**Example (Generic Issue):** -```json -{ - "name": "tests_issues_link", - "arguments": { - "test_id": "12345", - "url": "https://jira.example.com/TEST-123" - } -} -``` +**Sharing semantics:** file-type suites are linked (read-only copies that stay in sync); folder suites are deep-copied as regular editable copies. Re-sharing a linked suite into a project that already has it does not duplicate. Suites that are themselves shared copies link to their original source. Omit `destination_folder_id` to share into the root of the target project(s). Requests are processed asynchronously; projects must be of the same type (Classic/BDD). -**Example (Jira):** -```json -{ - "name": "tests_issues_link", - "arguments": { - "test_id": "12345", - "jira_id": "TEST-123" - } -} -``` +--- -**API Endpoint:** `POST /api/v2/{project_id}/issues` +## runs ---- +Manage runs. `/api/v2/{project_id}/runs` -### tests_issues_unlink +**Commands:** -Unlink an issue from a test. +| Command | Description | Runtime-required params | +|---------|-------------|------------------------| +| `list` | List runs (tql filter, pagination) | — | +| `get` | Get run by ID | `run_id` | +| `create` | Create run | `title` | +| `update` | Update run (status transitions via `status_event`) | `run_id` | +| `delete` | Delete run | `run_id` | +| `issues_list` / `issues_link` / `issues_unlink` | Scoped issue operations | as on `tests` (with `run_id`) | **Parameters:** -| Name | Type | Required | Description | + +| Name | Type | Commands | Description | |------|------|----------|-------------| -| issue_id | integer | Yes | Issue ID | -| type | string | Yes | "issue" or "jira_issue" | +| run_id | string | get, update, delete, issues_list, issues_link | Run ID | +| title | string | create, update | Run title | +| description | string | create, update | Run description | +| plan_ids | string[] | create | Plans to include | +| kind | string | create, update | `manual`, `automated`, `mixed` | +| rungroup_id | string | create, update | Run group | +| env | string | create, update | Environment | +| status_event | string | update | `finish`, `finish_manual`, `launch`, `rerun`, `scheduled`, `terminate` | +| assigned_to | string | create, update | Assignee | +| assign_strategy | string | create, update | `test`, `random`, `none` | +| test_ids | string[] | create, update | Tests to include | +| suite_ids | string[] | create, update | Suites to include | +| envs | string[] | create | Environments | +| link | array | create, update | Link actions, see [Link Parameter Structure](#link-parameter-structure) | +| tql | string | list | TQL filter for runs | +| branch | string | list, get, create, update, delete | Branch slug, see [Branch Scoping](#branch-scoping) | +| page / per_page | integer | list, issues_list | Pagination | -**Example:** +**Example — finish a run:** ```json { - "name": "tests_issues_unlink", - "arguments": { - "issue_id": 123, - "type": "issue" - } + "name": "runs", + "arguments": { "command": "update", "run_id": "456", "status_event": "finish" } } ``` -**API Endpoint:** `DELETE /api/v2/{project_id}/issues/{id}` - --- -## Suite Management - -### suites_list - -List suites as a tree structure. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| page | integer | No | Page number | -| per_page | integer | No | Items per page | -| file_type | string | No | "file" or "folder" | -| tag | string | No | Filter by tag | -| labels | string | No | Filter by labels | -| search_text | string | No | Search text | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | - -**API Endpoint:** `GET /api/v2/{project_id}/suites` +## testruns ---- +Manage individual test runs (result records inside a run). `/api/v2/{project_id}/testruns` -### suites_get +**Commands:** -Get a specific suite by ID. +| Command | Description | Runtime-required params | +|---------|-------------|------------------------| +| `list` | List testruns (rich filters) | — | +| `get` | Get testrun by ID | `testrun_id` | +| `create` | Create testrun in a run | `run_id` | +| `update` | Update testrun | `testrun_id` | +| `delete` | Delete testrun | `testrun_id` | +| `issues_list` / `issues_link` / `issues_unlink` | Scoped issue operations | as on `tests` (with `testrun_id`) | +| `attachments_list` / `attachments_upload` / `attachments_delete` | Scoped attachment operations | as on `tests` (with `testrun_id`) | **Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| suite_id | string | Yes | Suite ID | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | -**API Endpoint:** `GET /api/v2/{project_id}/suites/{id}` +| Name | Type | Commands | Description | +|------|------|----------|-------------| +| testrun_id | integer | get, update, delete, issues_*, attachments_* | TestRun ID | +| run_id | string | list, create, update | list: filter by run; create/update: the owning run | +| test_id | string | create, update | Test reference | +| test_ids | string \| string[] | list | Filter by tests | +| status | string | create, update | `passed`, `failed`, `skipped`, `pending` | +| message | string | create, update | Result message | +| run_time | number | create, update | Execution time | +| assigned_to | string | create, update | Assignee | +| test_title | string | create, update | Title override | +| automated | boolean | create, update | Automated flag | +| filter_status | string | list | `passed`, `failed`, `skipped`, `pending` | +| filter_kind | string | list | `manual`, `automated` | +| filter_user | integer \| string | list | Filter by user | +| filter_priority | string | list | `low` … `critical` | +| filter_substatus | string | list | Filter by substatus | +| filter_search | string | list | Text search | +| filter_message | boolean | list | Has message | +| filter_link | boolean | list | Has link | +| filter_finished_at_date_range | string | list | Date range filter | +| tags / labels / envs / rungroups | string \| string[] | list | Filter lists (comma-joined) | +| defects | string | list | `has_defects` / `without_defects` | +| page / per_page | integer | list, issues_list | Pagination | --- -### suites_create - -Create a new suite. +## plans -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| title | string | Yes | Suite title | -| description | string | No | Suite description | -| emoji | string | No | Suite emoji | -| parent_id | string | No | Parent suite ID | -| file_type | string | No | One of: `file`, `folder` | -| assigned_to | string | No | Assignee ID | -| file | string | No | File reference | -| children | array | No | Child suites | -| link | array | No | Links to labels, tags, milestones, issues, jira, or requirements | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | - -**API Endpoint:** `POST /api/v2/{project_id}/suites` - ---- +Manage test plans. `/api/v2/{project_id}/plans` -### suites_update +**Commands:** -Update an existing suite. +| Command | Description | Runtime-required params | +|---------|-------------|------------------------| +| `list` | List plans (kind, hidden, labels, search_text filters) | — | +| `get` | Get plan by ID | `plan_id` | +| `create` | Create plan | `title` | +| `update` | Update plan | `plan_id` | +| `delete` | Delete plan | `plan_id` | +| `issues_list` / `issues_link` / `issues_unlink` | Scoped issue operations | as on `tests` (with `plan_id`) | **Parameters:** -| Name | Type | Required | Description | + +| Name | Type | Commands | Description | |------|------|----------|-------------| -| suite_id | string | Yes | Suite ID | -| title | string | No | New title | -| description | string | No | Description | -| emoji | string | No | Emoji | -| parent_id | string | No | Parent suite ID | -| file_type | string | No | One of: `file`, `folder` | -| assigned_to | string | No | Assignee ID | -| file | string | No | File reference | -| children | array | No | Child suites | -| link | array | No | Link updates for labels, tags, milestones, issues, jira, or requirements | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | - -**API Endpoint:** `PUT /api/v2/{project_id}/suites/{id}` +| plan_id | string | get, update, delete, issues_list, issues_link | Plan ID | +| title | string | create, update | Plan title | +| description | string | create, update | Plan description | +| kind | string | list, create, update | `manual`, `automated`, `mixed` | +| hidden | boolean | list, create, update | list: include hidden; create/update: set flag | +| as_manual | boolean | create, update | Create manual testruns | +| labels | string[] | list | Filter by labels | +| search_text | string | list | Text search | +| test_ids | string[] | create, update | Test IDs to include (if omitted, all tests matching the plan kind) | +| suite_ids | string[] | create, update | Suite IDs to include (if omitted, all suites considered) | +| tql | string | create, update | TQL filter selecting plan contents | +| link | array | create, update | Link actions | +| page / per_page | integer | list, issues_list | Pagination | --- -### suites_delete - -Delete a suite. +## rungroups -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| suite_id | string | Yes | Suite ID | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | - -**API Endpoint:** `DELETE /api/v2/{project_id}/suites/{id}` +Manage run groups as a tree. `/api/v2/{project_id}/rungroups` ---- - -### Suite Issue Operations +**Commands:** `list`, `get`, `create` (`title`), `update` (`rungroup_id`), `delete` (`rungroup_id`) -**suites_issues_list**, **suites_issues_link**, **suites_issues_unlink** +**Parameters:** -Same pattern as test issue operations, but for suites. +| Name | Type | Commands | Description | +|------|------|----------|-------------| +| rungroup_id | string | get, update, delete | Run group ID | +| title | string | create, update | Title | +| description | string | create, update | Description | +| emoji | string | create, update | Emoji icon | +| kind | string | create, update | Group kind | +| pin | boolean | create, update | Pinned flag | +| status | string | create, update | Status | +| parent_id | string | create, update | Parent group | +| children | array | create, update | Child items | +| page / per_page | integer | list | Pagination | --- -## Share Management +## steps -Share tests and suites into other projects. The source project stays the single source of truth: shared copies in target projects are read-only and stay in sync until unlinked. Source and target projects must be of the same type (Classic/BDD). Share requests are queued and processed asynchronously — `status: "queued"` only confirms the request was accepted, not that sharing has completed. +Manage test steps. `/api/v2/{project_id}/steps` -### tests_share - -Share one or more tests into a suite of another project. Tests are selected by `test_ids`, by `labels`, or both (the two sets are combined); at least one is required, max 1000 tests per request. Re-sharing a test into a target project that already has it does not create a duplicate. A matched test that is itself a shared copy is skipped and listed in `skipped_test_ids` — share from the original test's project instead. +**Commands:** `list`, `get` (`step_id`), `create` (`title`), `update` (`step_id`), `delete` (`step_id`) **Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| test_ids | string[] | No* | Test IDs to share | -| labels | string[] | No* | Label slugs or titles; every test carrying any of these labels is shared, in addition to test_ids | -| target_project_id | string | Yes | Project ID (slug) of the destination project | -| target_suite_id | string | Yes | Suite ID in the target project; must be a file-type suite, not a folder | - -*At least one of test_ids/labels is required - -**Example (bulk by IDs):** -```json -{ - "name": "tests_share", - "arguments": { - "test_ids": ["be779025", "sgqat108", "sgqat104"], - "target_project_id": "sugar-king", - "target_suite_id": "e73d559c" - } -} -``` - -**Example (by label):** -```json -{ - "name": "tests_share", - "arguments": { - "labels": ["pre-cert"], - "target_project_id": "sugar-king", - "target_suite_id": "e73d559c" - } -} -``` - -**Returns:** -```json -{ - "data": { - "status": "queued", - "target_project_id": "sugar-king", - "target_suite_id": "e73d559c", - "test_ids": ["be779025", "sgqat108", "sgqat104"], - "skipped_test_ids": [] - } -} -``` -**API Endpoint:** `POST /api/v2/{project_id}/shares/tests` +| Name | Type | Commands | Description | +|------|------|----------|-------------| +| step_id | integer | get, update, delete | Step ID | +| title | string | create, update | Step title | +| description | string | create, update | Step description | +| link | array | create, update | Link actions | +| page / per_page | integer | list | Pagination | --- -### suites_share - -Share one or more suites (with their tests) into one or more other projects. Suites are selected by `suite_ids`, by `labels`, or both (the two sets are combined); at least one is required, max 200 suites per request. File-type suites are linked (read-only copies that stay in sync); folder suites are deep-copied as regular editable copies. Re-sharing a linked suite into a project that already has it does not duplicate — checked per target project. Omit `destination_folder_id` to share into the root of the target project(s). +## snippets -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| suite_ids | string[] | No* | Suite IDs to share | -| labels | string[] | No* | Label slugs or titles; every suite carrying any of these labels is shared, in addition to suite_ids | -| target_project_ids | string[] | Yes | Project IDs (slugs) of the destination projects | -| destination_folder_id | string | No | Folder suite ID in the target project to place the shared suites into; only allowed when sharing to a single target project | - -*At least one of suite_ids/labels is required +Manage code snippets. `/api/v2/{project_id}/snippets` -**Example:** -```json -{ - "name": "suites_share", - "arguments": { - "suite_ids": ["e73d559c"], - "target_project_ids": ["sugar-king", "game-qa"] - } -} -``` +**Commands:** `list`, `get` (`snippet_id`), `create` (`title`), `update` (`snippet_id`), `delete` (`snippet_id`) -**Returns:** -```json -{ - "data": { - "status": "queued", - "target_project_ids": ["sugar-king", "game-qa"], - "destination_folder_id": null, - "suite_ids": ["e73d559c"] - } -} -``` +**Parameters:** -**API Endpoint:** `POST /api/v2/{project_id}/shares/suites` +| Name | Type | Commands | Description | +|------|------|----------|-------------| +| snippet_id | integer | get, update, delete | Snippet ID | +| title | string | create, update | Snippet title | +| description | string | create, update | Snippet code/description | +| link | array | create, update | Link actions | +| page / per_page | integer | list | Pagination | --- -### tests_unshare +## labels + +Manage labels. `/api/v2/{project_id}/labels` -Remove a test's share, converting the shared copy back into a regular, editable test. Only the shared copy can be targeted — the original source test is untouched. Must be called against the project that holds the shared copy. +**Commands:** `list`, `get` (`label_id`), `create` (`title`), `update` (`label_id`), `delete` (`label_id`) **Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| test_id | string | Yes | ID of the shared test copy to unlink | -**API Endpoint:** `DELETE /api/v2/{project_id}/shares/tests/{id}` +| Name | Type | Commands | Description | +|------|------|----------|-------------| +| label_id | string | get, update, delete | Label slug | +| title | string | create, update | Label title | +| color | string | create, update | Color | +| visibility | string[] | create, update | `filter`, `list` | +| scope | string[] | create, update | `tests`, `suites`, `runs`, `plans`, `steps`, `templates` | +| field | object | create, update | Custom field definition | +| page / per_page | integer | list | Pagination | --- -### suites_unshare +## tags + +Read-only tag access with counts. `/api/v2/{project_id}/tags` -Remove a suite's share, converting the shared copy back into a regular, editable suite. Only the shared (linked) copy can be targeted — the original source suite is untouched. Must be called against the project that holds the shared copy. +**Commands:** `list`, `get` (`tag_id`), `search` (`tag_id` or `query`; delegates to `get`) **Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| suite_id | string | Yes | ID of the shared suite copy to unlink | -**API Endpoint:** `DELETE /api/v2/{project_id}/shares/suites/{id}` +| Name | Type | Commands | Description | +|------|------|----------|-------------| +| tag_id | string | get, search | Tag title to look up | +| query | string | search | Search text | --- -## Run Management - -### runs_list +## milestones -List all test runs. +Read-only milestone access. `/api/v2/{project_id}/milestones` -Use `tql` for search/filtering. -TQL means `Testomat.io Query Language`. -Use standard TQL syntax such as `==`, `!=`, `>`, `<`, `>=`, `<=`, `in [...]`, `%`, `and`, `or`, `not`, and parentheses. -For the full syntax and field reference, see the official docs: https://docs.testomat.io/advanced/tql/ +**Commands:** `list`, `get` (`milestone_id`) **Parameters:** -| Name | Type | Required | Description | + +| Name | Type | Commands | Description | |------|------|----------|-------------| -| page | integer | No | Page number | -| per_page | integer | No | Items per page | -| tql | string | No | TQL filter for runs. Examples: `title % 'Manual tests'`, `plan == '{PLAN_ID}'`, `finished and with_defect` | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | +| milestone_id | string | get | Milestone slug | +| type | string | list | Filter by milestone type title, e.g. `Sprint` or `Release` | +| status | string | list | `created`, `active`, or `closed` | +| page / per_page | integer | list | Pagination | -**Example:** -```json -{ - "name": "runs_list", - "arguments": { - "page": 1, - "per_page": 10, - "tql": "failed and has_test_tag == 'regression'" - } -} -``` +--- -**API Endpoint:** `GET /api/v2/{project_id}/runs` +## issues ---- +Global issue operations across resources. `/api/v2/{project_id}/issues` -### runs_get +**Commands:** -Get a specific run by ID. +| Command | Description | Runtime-required params | +|---------|-------------|------------------------| +| `list` | List linked issues (scope by one resource id, filter by source) | — | +| `create` | Link issue to a resource | exactly one resource id, plus exactly one of `url`/`jira_id` | +| `delete` | Unlink issue | `issue_id`, `type` | **Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| run_id | string | Yes | Run ID | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | -**API Endpoint:** `GET /api/v2/{project_id}/runs/{id}` +| Name | Type | Commands | Description | +|------|------|----------|-------------| +| test_id / suite_id / run_id / plan_id | string | list, create | Resource scope (one at a time) | +| testrun_id | integer | list, create | Resource scope (one at a time) | +| source | string | list | Filter by source | +| url | string | create | Issue URL | +| jira_id | string | create | Jira issue ID | +| issue_id | integer | delete | Issue ID | +| type | string | delete | `issue` or `jira_issue` | +| page / per_page | integer | list | Pagination | --- -### runs_create +## requirements + +Manage requirements, including file uploads. `/api/v2/{project_id}/requirements` -Create a new test run. +**Commands:** `list`, `get` (`requirement_id`), `create` (`title`, `source_type`), `update` (`requirement_id`), `delete` (`requirement_id`) **Parameters:** -| Name | Type | Required | Description | + +| Name | Type | Commands | Description | |------|------|----------|-------------| -| title | string | Yes | Run title | -| description | string | No | Run description | -| plan_ids | array | No | List of plan public UIDs to include in the run | -| kind | string | No | "manual", "automated", or "mixed" | -| rungroup_id | string | No | Run group ID | -| env | string | No | Environment name | -| assigned_to | string | No | Assignee ID | -| assign_strategy | string | No | "test", "random", or "none" | -| test_ids | array | No | Array of test public UIDs to include (use ["*"] for all tests) | -| suite_ids | array | No | Array of suite public UIDs whose tests to include | -| envs | array | No | Array of environment names | -| link | array | No | Links to labels, tags, milestones, issues, or jira | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | +| requirement_id | string | get, update, delete | Requirement ID | +| title | string | create, update | Requirement title | +| source_type | string | create | `jira`, `confluence`, `file`, `text` | +| source | string | list | Filter by source: `jira`, `confluence`, `file`, `text` | +| scope | string | list | `global`, `attached`, `detached`, `without_suites` | +| description | string | create, update | Required for text requirements (min 500 chars on create); only applied for text requirements on update | +| details | string | create, update | Details | +| active | boolean | create, update | Active flag | +| global | boolean | create, update | Global flag | +| confluence_url | string | create | Required for confluence requirements | +| files | string[] | create, update | Local file paths to upload for file requirements | +| page / per_page | integer | list | Pagination | -**Example:** -```json -{ - "name": "runs_create", - "arguments": { - "title": "Smoke tests - Prod", - "kind": "automated", - "env": "production", - "test_ids": ["123", "456", "789"] - } -} -``` +--- -**Example with suites:** -```json -{ - "name": "runs_create", - "arguments": { - "title": "Auth Suite Tests", - "kind": "automated", - "suite_ids": ["suite1", "suite2"] - } -} -``` +## branches -**API Endpoint:** `POST /api/v2/{project_id}/runs` +Manage project branches. Requires the branches feature (enterprise plan). `/api/v2/{project_id}/branches` ---- +**Commands:** `list`, `get` (`branch_id`), `create` (`title`), `update` (`branch_id`), `delete` (`branch_id`) -### runs_update +**Parameters:** -Update an existing run. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| run_id | string | Yes | Run ID | -| title | string | No | New title | -| description | string | No | Description | -| kind | string | No | Run type | -| rungroup_id | string | No | Run group ID | -| env | string | No | Environment | -| **status_event** | string | No | **"finish"\|"finish_manual"\|"launch"\|"rerun"\|"scheduled"\|"terminate"** | -| assigned_to | string | No | Assignee ID | -| assign_strategy | string | No | Assignment strategy | -| test_ids | array | No | Test public UIDs | -| suite_ids | array | No | Suite public UIDs whose tests to include | -| link | array | No | Link updates for labels, tags, milestones, issues, or jira | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | - -**Status Event Example:** -```json -{ - "name": "runs_update", - "arguments": { - "run_id": "12345", - "status_event": "finish" - } -} -``` - -**API Endpoint:** `PUT /api/v2/{project_id}/runs/{id}` - ---- - -### runs_delete - -Delete a run. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| run_id | string | Yes | Run ID | -| branch | string | No | Branch slug to scope the operation to (omit or `main` for the main branch). See [Branch Scoping](#branch-scoping) | - -**API Endpoint:** `DELETE /api/v2/{project_id}/runs/{id}` - ---- - -### Run Issue Operations - -**runs_issues_list**, **runs_issues_link**, **runs_issues_unlink** - -Same pattern as test issue operations, but for runs. - ---- - -## TestRun Management - -### testruns_list - -List test runs (individual test results within a run). - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| page | integer | No | Page number | -| per_page | integer | No | Items per page | -| run_id | string | No | Filter by parent run ID | -| test_ids | array\|string | No | Test IDs; arrays are sent as comma-separated values | -| filter_status | string | No | `passed`, `failed`, `skipped`, `pending` | -| filter_kind | string | No | `manual` or `automated` | -| filter_user | integer\|string | No | Assigned user ID | -| filter_priority | string | No | One of: `low`, `normal`, `important`, `high`, `critical` | -| filter_substatus | string | No | Custom substatus filter | -| filter_search | string | No | Text search across test title | -| filter_message | boolean | No | Only testruns with a message | -| filter_link | boolean | No | Only testruns with linked issues | -| filter_finished_at_date_range | string | No | ISO date range, comma-separated | -| tags | array\|string | No | Test tags, comma-separated when sent to API | -| labels | array\|string | No | Run labels, comma-separated when sent to API | -| envs | array\|string | No | Run environments, comma-separated when sent to API | -| rungroups | array\|string | No | Rungroup IDs, comma-separated when sent to API | -| defects | string | No | `has_defects` or `without_defects` | - -**API Endpoint:** `GET /api/v2/{project_id}/testruns` - ---- - -### testruns_get - -Get a specific test run by ID. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| testrun_id | integer | Yes | Test run ID | - -**API Endpoint:** `GET /api/v2/{project_id}/testruns/{id}` - ---- - -### testruns_create - -Create a new test run result. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| run_id | string | Yes | Parent run ID | -| test_id | string | No | Test ID | -| status | string | No | One of: `passed`, `failed`, `skipped`, `pending` | -| message | string | No | Status message | -| run_time | number | No | Execution time in seconds | -| assigned_to | string | No | Assignee ID | -| test_title | string | No | Test title | -| automated | boolean | No | Is automated test | - -**Example:** -```json -{ - "name": "testruns_create", - "arguments": { - "run_id": "12345", - "test_id": "67890", - "status": "passed", - "run_time": 2.5 - } -} -``` - -**API Endpoint:** `POST /api/v2/{project_id}/testruns` - ---- - -### testruns_update - -Update a test run. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| testrun_id | integer | Yes | Test run ID | -| run_id | string | No | Parent run ID | -| test_id | string | No | Test ID | -| status | string | No | One of: `passed`, `failed`, `skipped`, `pending` | -| message | string | No | Status message | -| run_time | number | No | Execution time | -| assigned_to | string | No | Assignee ID | -| test_title | string | No | Test title | -| automated | boolean | No | Is automated | - -**API Endpoint:** `PUT /api/v2/{project_id}/testruns/{id}` - ---- - -### testruns_delete - -Delete a test run. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| testrun_id | integer | Yes | Test run ID | - -**API Endpoint:** `DELETE /api/v2/{project_id}/testruns/{id}` - ---- - -### TestRun Issue Operations - -**testruns_issues_list**, **testruns_issues_link**, **testruns_issues_unlink** - -Same pattern as test issue operations, but for testruns. - ---- - -## Plan Management - -### plans_list - -List test plans. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| page | integer | No | Page number | -| per_page | integer | No | Items per page | -| kind | string | No | `manual`, `automated`, `mixed` | -| hidden | boolean | No | Filter hidden vs visible plans | -| labels | array | No | Filter by labels (OR logic) | -| search_text | string | No | Plain text search across plan titles | - -**API Endpoint:** `GET /api/v2/{project_id}/plans` - ---- - -### plans_get - -Get a specific plan by ID. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| plan_id | string | Yes | Plan ID | - -**API Endpoint:** `GET /api/v2/{project_id}/plans/{id}` - ---- - -### plans_create - -Create a new test plan. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| title | string | Yes | Plan title | -| description | string | No | Plan description | -| kind | string | No | "manual", "automated", or "mixed" | -| hidden | boolean | No | Hide plan | -| as_manual | boolean | No | Treat as manual | -| test_ids | array | No | List of test IDs (8-char) to include | -| suite_ids | array | No | List of suite IDs (8-char) to include | -| tql | string | No | TQL query expression to filter tests for the plan | -| link | array | No | Links to labels, tags, milestones, issues, or jira | - -**API Endpoint:** `POST /api/v2/{project_id}/plans` - ---- - -### plans_update - -Update an existing plan. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| plan_id | string | Yes | Plan ID | -| title | string | No | New title | -| description | string | No | Description | -| kind | string | No | Plan type | -| hidden | boolean | No | Hidden flag | -| as_manual | boolean | No | Manual flag | -| test_ids | array | No | List of test IDs (8-char) to include | -| suite_ids | array | No | List of suite IDs (8-char) to include | -| tql | string | No | TQL query expression to filter tests for the plan | -| link | array | No | Link updates for labels, tags, milestones, issues, or jira | - -**API Endpoint:** `PUT /api/v2/{project_id}/plans/{id}` - ---- - -### plans_delete - -Delete a plan. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| plan_id | string | Yes | Plan ID | - -**API Endpoint:** `DELETE /api/v2/{project_id}/plans/{id}` - ---- - -### Plan Issue Operations - -**plans_issues_list**, **plans_issues_link**, **plans_issues_unlink** - -Same pattern as test issue operations, but for plans. - ---- - -## RunGroup Management - -### rungroups_list - -List run groups as a tree. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| page | integer | No | Page number | -| per_page | integer | No | Items per page | - -**API Endpoint:** `GET /api/v2/{project_id}/rungroups` - ---- - -### rungroups_get - -Get a specific run group by ID. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| rungroup_id | string | Yes | Run group ID | - -**API Endpoint:** `GET /api/v2/{project_id}/rungroups/{id}` - ---- - -### rungroups_create - -Create a new run group. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| title | string | Yes | Group title | -| description | string | No | Description | -| emoji | string | No | Emoji icon | -| kind | string | No | Group kind | -| pin | boolean | No | Pin group | -| status | string | No | Group status | -| parent_id | string | No | Parent group ID | -| children | array | No | Child groups | - -**API Endpoint:** `POST /api/v2/{project_id}/rungroups` - ---- - -### rungroups_update - -Update an existing run group. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| rungroup_id | string | Yes | Run group ID | -| title | string | No | New title | -| description | string | No | Description | -| emoji | string | No | Emoji | -| kind | string | No | Kind | -| pin | boolean | No | Pin flag | -| status | string | No | Status | -| parent_id | string | No | Parent ID | -| children | array | No | Children | - -**API Endpoint:** `PUT /api/v2/{project_id}/rungroups/{id}` - ---- - -### rungroups_delete - -Delete a run group. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| rungroup_id | string | Yes | Run group ID | - -**API Endpoint:** `DELETE /api/v2/{project_id}/rungroups/{id}` - ---- - -## Step Management - -### steps_list - -List test steps. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| page | integer | No | Page number | -| per_page | integer | No | Items per page | - -**API Endpoint:** `GET /api/v2/{project_id}/steps` - ---- - -### steps_get - -Get a specific step by ID. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| step_id | integer | Yes | Step ID | - -**API Endpoint:** `GET /api/v2/{project_id}/steps/{id}` - ---- - -### steps_create - -Create a new step. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| title | string | Yes | Step title | -| description | string | No | Step description | -| link | array | No | Links to labels, tags, milestones, issues, or jira | - -**API Endpoint:** `POST /api/v2/{project_id}/steps` - ---- - -### steps_update - -Update an existing step. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| step_id | integer | Yes | Step ID | -| title | string | No | New title | -| description | string | No | Description | -| link | array | No | Link updates for labels, tags, milestones, issues, or jira | - -**API Endpoint:** `PUT /api/v2/{project_id}/steps/{id}` - ---- - -### steps_delete - -Delete a step. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| step_id | integer | Yes | Step ID | - -**API Endpoint:** `DELETE /api/v2/{project_id}/steps/{id}` - ---- - -## Snippet Management - -### snippets_list - -List code snippets. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| page | integer | No | Page number | -| per_page | integer | No | Items per page | - -**API Endpoint:** `GET /api/v2/{project_id}/snippets` - ---- - -### snippets_get - -Get a specific snippet by ID. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| snippet_id | integer | Yes | Snippet ID | - -**API Endpoint:** `GET /api/v2/{project_id}/snippets/{id}` - ---- - -### snippets_create - -Create a new snippet. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| title | string | Yes | Snippet title | -| description | string | No | Description | -| link | array | No | Links to labels, tags, milestones, issues, or jira | - -**API Endpoint:** `POST /api/v2/{project_id}/snippets` - ---- - -### snippets_update - -Update an existing snippet. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| snippet_id | integer | Yes | Snippet ID | -| title | string | No | New title | -| description | string | No | Description | -| link | array | No | Link updates for labels, tags, milestones, issues, or jira | - -**API Endpoint:** `PUT /api/v2/{project_id}/snippets/{id}` - ---- - -### snippets_delete - -Delete a snippet. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| snippet_id | integer | Yes | Snippet ID | - -**API Endpoint:** `DELETE /api/v2/{project_id}/snippets/{id}` - ---- - -## Label Management - -### labels_list - -List labels. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| page | integer | No | Page number | -| per_page | integer | No | Items per page | - -**API Endpoint:** `GET /api/v2/{project_id}/labels` - ---- - -### labels_get - -Get a specific label by slug. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| label_id | string | Yes | Label ID/slug | - -**API Endpoint:** `GET /api/v2/{project_id}/labels/{id}` - ---- - -### labels_create - -Create a new label. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| title | string | Yes | Label title | -| color | string | No | Label color (hex) | -| visibility | array | No | ["filter", "list"] | -| scope | array | No | ["tests", "suites", "runs", "plans", "steps", "templates"] | -| field | object | No | Field configuration | - -**API Endpoint:** `POST /api/v2/{project_id}/labels` - ---- - -### labels_update - -Update an existing label. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| label_id | string | Yes | Label ID | -| title | string | No | New title | -| color | string | No | Color | -| visibility | array | No | Visibility options | -| scope | array | No | Label scope | -| field | object | No | Field config | - -**API Endpoint:** `PUT /api/v2/{project_id}/labels/{id}` - ---- - -### labels_delete - -Delete a label. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| label_id | string | Yes | Label ID | - -**API Endpoint:** `DELETE /api/v2/{project_id}/labels/{id}` - ---- - -## Tag Management (Read-Only) - -### tags_list - -List all tags with counts. - -**Parameters:** None - -**API Endpoint:** `GET /api/v2/{project_id}/tags` - ---- - -### tags_get - -Get tests by tag title. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| tag_id | string | Yes | Tag title/ID | - -**API Endpoint:** `GET /api/v2/{project_id}/tags/{id}` - ---- - -### tags_search - -Search by tag title (delegates to tags_get). - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| tag_id | string | No | Tag ID | -| query | string | No | Search query | - ---- - -## Milestone Management - -### milestones_list - -List milestones. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| page | integer | No | Page number | -| per_page | integer | No | Items per page | -| type | string | No | Filter by milestone type title, e.g. `Sprint` or `Release` | -| status | string | No | `created`, `active`, or `closed` | - -**API Endpoint:** `GET /api/v2/{project_id}/milestones` - ---- - -### milestones_get - -Get a milestone by ID. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| milestone_id | string | Yes | Milestone slug | - -**API Endpoint:** `GET /api/v2/{project_id}/milestones/{id}` - ---- - -## Issue Management (Global) - -### issues_list - -List linked issues (global or filtered by resource). - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| page | integer | No | Page number | -| per_page | integer | No | Items per page | -| test_id | string | No | Filter by test | -| suite_id | string | No | Filter by suite | -| run_id | string | No | Filter by run | -| testrun_id | integer | No | Filter by testrun | -| plan_id | string | No | Filter by plan | -| source | string | No | Filter by source | - -**API Endpoint:** `GET /api/v2/{project_id}/issues` - ---- - -### issues_create - -Link an issue to a resource. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| test_id | string | No* | Link to test | -| suite_id | string | No* | Link to suite | -| run_id | string | No* | Link to run | -| testrun_id | integer | No* | Link to testrun | -| plan_id | string | No* | Link to plan | -| url | string | No** | Issue URL | -| jira_id | string | No** | Jira issue ID | - -*At least one resource ID required -**Either url or jira_id required - -**API Endpoint:** `POST /api/v2/{project_id}/issues` - ---- - -### issues_delete - -Unlink an issue. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| issue_id | integer | Yes | Issue ID | -| type | string | Yes | "issue" or "jira_issue" | - -**API Endpoint:** `DELETE /api/v2/{project_id}/issues/{id}` - ---- - -## Attachment Management - -Attachments are scoped to tests, suites, and testruns. Each operation requires exactly one entity ID through the matching scoped tool. - -### tests_attachments_list - -List attachments for a test. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| test_id | string | Yes | Test ID | - -**API Endpoint:** `GET /api/v2/{project_id}/attachments?test_id=...` - ---- - -### tests_attachments_upload - -Upload one attachment to a test. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| test_id | string | Yes | Test ID | -| file_path | string | Yes | Local path to the file readable by the MCP server | - -**API Endpoint:** `POST /api/v2/{project_id}/attachments?test_id=...` - ---- - -### tests_attachments_delete - -Delete an attachment from a test. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| test_id | string | Yes | Test ID | -| attachment_id | string | Yes | Attachment ID | - -**API Endpoint:** `DELETE /api/v2/{project_id}/attachments/{attachment_id}?test_id=...` - ---- - -### suites_attachments_list - -List attachments for a suite. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| suite_id | string | Yes | Suite ID | - -**API Endpoint:** `GET /api/v2/{project_id}/attachments?suite_id=...` - ---- - -### suites_attachments_upload - -Upload one attachment to a suite. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| suite_id | string | Yes | Suite ID | -| file_path | string | Yes | Local path to the file readable by the MCP server | - -**API Endpoint:** `POST /api/v2/{project_id}/attachments?suite_id=...` - ---- - -### suites_attachments_delete - -Delete an attachment from a suite. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| suite_id | string | Yes | Suite ID | -| attachment_id | string | Yes | Attachment ID | - -**API Endpoint:** `DELETE /api/v2/{project_id}/attachments/{attachment_id}?suite_id=...` - ---- - -### testruns_attachments_list - -List attachments for a testrun. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| testrun_id | integer | Yes | TestRun ID | - -**API Endpoint:** `GET /api/v2/{project_id}/attachments?testrun_id=...` - ---- - -### testruns_attachments_upload - -Upload one attachment to a testrun. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| testrun_id | integer | Yes | TestRun ID | -| file_path | string | Yes | Local path to the file readable by the MCP server | - -**API Endpoint:** `POST /api/v2/{project_id}/attachments?testrun_id=...` - ---- - -### testruns_attachments_delete - -Delete an attachment from a testrun. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| testrun_id | integer | Yes | TestRun ID | -| attachment_id | string | Yes | Attachment ID | - -**API Endpoint:** `DELETE /api/v2/{project_id}/attachments/{attachment_id}?testrun_id=...` - ---- - -## Requirement Management - -### requirements_list - -List requirements with optional filters. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| page | integer | No | Page number | -| per_page | integer | No | Items per page | -| source | string | No | Filter by source type: `jira`, `confluence`, `file`, `text` | -| scope | string | No | Filter by scope: `global`, `attached`, `detached`, `without_suites` | - -**API Endpoint:** `GET /api/v2/{project_id}/requirements` - ---- - -### requirements_get - -Get a requirement by ID. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| requirement_id | string | Yes | Requirement ID (8-char) | - -**API Endpoint:** `GET /api/v2/{project_id}/requirements/{id}` - ---- - -### requirements_create - -Create a requirement. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| title | string | Yes | Requirement title | -| source_type | string | Yes | `jira`, `confluence`, `file`, or `text` | -| description | string | No | Required for text requirements; must be at least 500 characters | -| details | string | No | Extended details or raw content | -| active | boolean | No | Active flag | -| global | boolean | No | Project-level requirement flag | -| confluence_url | string | No | Required for confluence requirements | -| files | array | No | Local file paths to upload for file requirements | - -**API Endpoint:** `POST /api/v2/{project_id}/requirements` - ---- - -### requirements_update - -Update a requirement. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| requirement_id | string | Yes | Requirement ID (8-char) | -| title | string | No | New title | -| description | string | No | Text requirement description | -| details | string | No | Extended details or raw content | -| active | boolean | No | Active flag | -| global | boolean | No | Project-level requirement flag | -| files | array | No | Local file paths to upload for file requirements | - -**API Endpoint:** `PATCH /api/v2/{project_id}/requirements/{id}` - ---- - -### requirements_delete - -Delete a requirement. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| requirement_id | string | Yes | Requirement ID (8-char) | - -**API Endpoint:** `DELETE /api/v2/{project_id}/requirements/{id}` - ---- - -## Branch Management - -Requires the `branches` subscription feature (enterprise plan). Branches are identified by slug. - -### branches_list - -List project branches. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| page | integer | No | Page number (min: 1) | -| per_page | integer | No | Items per page (min: 1, max: 100) | -| filter_state | string | No | Filter by state: `active`, `merged` | -| filter_title | string | No | Filter by title (partial substring match) | -| count | boolean | No | Return only total counts | -| group_by | string | No | Aggregate counts by field (use with count=true) | - -**API Endpoint:** `GET /api/v2/{project_id}/branches` - ---- - -### branches_get - -Get a branch by slug. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| branch_id | string | Yes | Branch slug | - -**Returns:** Branch slug, title, state (`active`/`merged`), tests_count, suites_count. - -**API Endpoint:** `GET /api/v2/{project_id}/branches/{slug}` - ---- - -### branches_create - -Create a branch. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| title | string | Yes | Branch title; a slug is generated from it | - -**API Endpoint:** `POST /api/v2/{project_id}/branches` - ---- - -### branches_update - -Update a branch title. - -**Parameters:** -| Name | Type | Required | Description | -|------|------|----------|-------------| -| branch_id | string | Yes | Branch slug | -| title | string | No | New branch title | - -**API Endpoint:** `PUT /api/v2/{project_id}/branches/{slug}` - ---- - -### branches_delete - -Delete a branch. - -**Parameters:** -| Name | Type | Required | Description | +| Name | Type | Commands | Description | |------|------|----------|-------------| -| branch_id | string | Yes | Branch slug | - -**API Endpoint:** `DELETE /api/v2/{project_id}/branches/{slug}` +| branch_id | string | get, update, delete | Branch slug | +| title | string | create, update | Branch title; a slug is generated from it | +| filter_state | string | list | `active` or `merged` | +| filter_title | string | list | Partial substring match on title | +| page / per_page | integer | list | Pagination | --- @@ -1680,7 +558,7 @@ Delete a branch. ### Branch Scoping -Tests, suites, and runs CRUD tools accept an optional `branch` parameter (branch slug). +The `tests`, `suites`, and `runs` tools accept an optional `branch` parameter (branch slug) on their CRUD commands. Omit it (or pass `main`) to operate on the main branch. - For **tests** and **suites**: a matching branch-local record is used when it exists, falling back to main; updating or deleting a main-only record creates an isolated branch-local copy rather than mutating main. @@ -1688,8 +566,9 @@ Omit it (or pass `main`) to operate on the main branch. ```json { - "name": "tests_list", + "name": "tests", "arguments": { + "command": "list", "branch": "feature-login", "tql": "priority == 'high'" } @@ -1720,13 +599,13 @@ Most entities support linking via the `link` parameter: ### Pagination -All list operations support: +All list commands support: - `page` (integer, min: 1) - `per_page` (integer, min: 1, max: 100) ### List Response Projection -List operations request slim responses from the API by default. Heavy entity fields such as `description` and `code`, duplicate title fields, and null values are omitted from the result. +List commands request slim responses from the API by default. Heavy entity fields such as `description` and `code`, duplicate title fields, and null values are omitted from the result. - `verbose: true` disables the backend slim request and returns full objects. - `fields: ["id", "title", "description"]` disables the backend slim request and returns only the selected non-null fields. @@ -1736,7 +615,7 @@ The backend `slim` parameter is managed internally by MCP; callers should use `v ### Counts & Aggregation -List operations accept `count` (and `group_by`) to fetch totals and aggregated breakdowns without transferring the entity list — useful for "how many" questions instead of pulling full pages. Available on every list tool except the scoped `*_issues_list` / `*_attachments_list`. +The primary `list` command of each entity accepts `count` (and `group_by`) to fetch totals and aggregated breakdowns without transferring the entity list — useful for "how many" questions instead of pulling full pages. Scoped list commands (`issues_list`, `attachments_list`) do not support aggregation. - `count: true` returns only `meta` with `total` (no `data`). Example response: ```json @@ -1759,7 +638,7 @@ Two ways to link issues: ### Attachments -Attachment uploads use local file paths readable by the MCP server process and send one multipart/form-data field named `file`. Multiple files per request are not supported by the Public API v2 endpoint. +Attachment uploads use local file paths readable by the MCP server process and send one multipart/form-data field named `files`. Multiple files per request are not supported by the Public API v2 endpoint. ### Search diff --git a/src/index.js b/src/index.js index ff4b38f..b80a62a 100644 --- a/src/index.js +++ b/src/index.js @@ -3,9 +3,11 @@ import { loadConfig } from './config/load-config.js'; import { ConfigurationError } from './core/errors.js'; import { createLogger } from './core/logger.js'; import { TestomatioMCPServer } from './mcp/server.js'; -import { TOOL_DEFINITIONS } from './mcp/tool-definitions.js'; +import { ENTITY_TOOL_SPECS, TOOL_DEFINITIONS } from './mcp/tool-definitions.js'; import { backendSlimQuery, slimList, withListOptions } from './mcp/list-projection.js'; import { selectTools } from './mcp/tool-profiles.js'; +import { ENTITY_COMMANDS } from './mcp/entity-commands.js'; +import { buildEntityTool } from './mcp/definitions/entity-tool.js'; import { ANALYTICS_STATS_TQL_INPUT_DESCRIPTION, ANALYTICS_STATS_TQL_REFERENCE, @@ -21,6 +23,9 @@ export { ANALYTICS_TESTS_TQL_REFERENCE, ConfigurationError, TOOL_DEFINITIONS, + ENTITY_TOOL_SPECS, + ENTITY_COMMANDS, + buildEntityTool, slimList, backendSlimQuery, withListOptions, diff --git a/src/mcp/definitions/attachments.js b/src/mcp/definitions/attachments.js deleted file mode 100644 index ecea9aa..0000000 --- a/src/mcp/definitions/attachments.js +++ /dev/null @@ -1,70 +0,0 @@ -function buildAttachmentTools({ toolPrefix, entityName, idKey, idType = 'string' }) { - const entityId = { - type: idType, - }; - - return [ - { - name: `${toolPrefix}_attachments_list`, - description: `List attachments for a ${entityName} (/api/v2/{project_id}/attachments?${idKey}=...)`, - inputSchema: { - type: 'object', - properties: { - [idKey]: entityId, - }, - required: [idKey], - additionalProperties: false, - }, - }, - { - name: `${toolPrefix}_attachments_upload`, - description: `Upload one attachment to a ${entityName} (/api/v2/{project_id}/attachments?${idKey}=...)`, - inputSchema: { - type: 'object', - properties: { - [idKey]: entityId, - file_path: { - type: 'string', - description: 'Local path to the file that will be sent as multipart/form-data field "file".', - }, - }, - required: [idKey, 'file_path'], - additionalProperties: false, - }, - }, - { - name: `${toolPrefix}_attachments_delete`, - description: `Delete attachment from a ${entityName} (/api/v2/{project_id}/attachments/{id}?${idKey}=...)`, - inputSchema: { - type: 'object', - properties: { - [idKey]: entityId, - attachment_id: { - type: 'string', - }, - }, - required: [idKey, 'attachment_id'], - additionalProperties: false, - }, - }, - ]; -} - -export const ATTACHMENT_TOOLS = [ - ...buildAttachmentTools({ - toolPrefix: 'tests', - entityName: 'test', - idKey: 'test_id', - }), - ...buildAttachmentTools({ - toolPrefix: 'suites', - entityName: 'suite', - idKey: 'suite_id', - }), - ...buildAttachmentTools({ - toolPrefix: 'testruns', - entityName: 'testrun', - idKey: 'testrun_id', - idType: 'integer', - }), -]; diff --git a/src/mcp/definitions/branches.js b/src/mcp/definitions/branches.js index b003061..2da8360 100644 --- a/src/mcp/definitions/branches.js +++ b/src/mcp/definitions/branches.js @@ -1,110 +1,42 @@ +import { buildEntityTool } from './entity-tool.js'; +import { paginationParams } from './params.js'; + export const BRANCH_PARAM = { "type": "string", "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature." }; -export const BRANCHES_TOOLS = [ - { - "name": "branches_list", - "description": - 'List project branches (/api/v2/{project_id}/branches). Requires the branches feature (enterprise plan). Use filter[state] / filter[title] to narrow down.', - "inputSchema": { - "type": "object", - "properties": { - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "filter_state": { - "type": "string", - "enum": [ - "active", - "merged" - ], - "description": "Filter by branch state" - }, - "filter_title": { - "type": "string", - "description": "Filter by title (partial substring match)" - } - }, - "additionalProperties": false - } - }, - { - "name": "branches_get", - "description": "Get branch by slug", - "inputSchema": { - "type": "object", - "properties": { - "branch_id": { - "type": "string", - "description": "Branch slug" - } - }, - "required": [ - "branch_id" - ], - "additionalProperties": false - } - }, - { - "name": "branches_create", - "description": "Create branch (/api/v2/{project_id}/branches)", - "inputSchema": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Branch title; a slug is generated from it" - } - }, - "required": [ - "title" - ], - "additionalProperties": false - } +export const BRANCHES_TOOL_SPEC = { + name: 'branches', + summary: + 'Manage project branches (/api/v2/{project_id}/branches). Requires the branches feature (enterprise plan).', + commands: { + list: 'List project branches (filter_state / filter_title to narrow down)', + get: 'Get branch by slug', + create: 'Create branch (title required; slug is generated from it)', + update: 'Update branch title', + delete: 'Delete branch by slug', }, - { - "name": "branches_update", - "description": "Update branch title (/api/v2/{project_id}/branches/{slug})", - "inputSchema": { - "type": "object", - "properties": { - "branch_id": { - "type": "string", - "description": "Branch slug" - }, - "title": { - "type": "string" - } - }, - "required": [ - "branch_id" - ], - "additionalProperties": false - } + params: { + branch_id: { commands: ['get', 'update', 'delete'], type: 'string', description: 'Branch slug' }, + title: { + commands: ['create', 'update'], + type: 'string', + description: 'Branch title; a slug is generated from it', + }, + filter_state: { + commands: ['list'], + type: 'string', + enum: ['active', 'merged'], + description: 'Filter by branch state', + }, + filter_title: { + commands: ['list'], + type: 'string', + description: 'Filter by title (partial substring match)', + }, + ...paginationParams(['list']), }, - { - "name": "branches_delete", - "description": "Delete branch by slug", - "inputSchema": { - "type": "object", - "properties": { - "branch_id": { - "type": "string", - "description": "Branch slug" - } - }, - "required": [ - "branch_id" - ], - "additionalProperties": false - } - } -]; +}; + +export const BRANCHES_TOOL = buildEntityTool(BRANCHES_TOOL_SPEC); diff --git a/src/mcp/definitions/entity-tool.js b/src/mcp/definitions/entity-tool.js new file mode 100644 index 0000000..c43e645 --- /dev/null +++ b/src/mcp/definitions/entity-tool.js @@ -0,0 +1,47 @@ +import { COUNT_PROPERTY, GROUP_BY_PROPERTY, LIST_OPTION_PROPERTIES } from '../list-projection.js'; + +function withCommandPrefix(schema, commands) { + return { + ...schema, + description: `(${commands.join('|')}) ${schema.description ?? ''}`.trim(), + }; +} + +export function buildEntityTool(spec) { + const properties = { + command: { + type: 'string', + enum: Object.keys(spec.commands), + description: `CLI-style operation to perform. ${Object.entries(spec.commands) + .map(([command, summary]) => `${command}: ${summary}`) + .join(' | ')}`, + }, + }; + + for (const [key, { commands: appliesTo = [], ...schema }] of Object.entries(spec.params)) { + if (!appliesTo.length) continue; // no commands left after profile pruning + properties[key] = withCommandPrefix(schema, appliesTo); + } + + for (const command of Object.keys(spec.commands)) { + const isListStyle = command === 'list' || command.endsWith('_list'); + if (!isListStyle) continue; + const projection = command === 'list' + ? { ...LIST_OPTION_PROPERTIES, count: COUNT_PROPERTY, group_by: GROUP_BY_PROPERTY } + : LIST_OPTION_PROPERTIES; + for (const [key, schema] of Object.entries(projection)) { + properties[key] = withCommandPrefix(schema, [command]); + } + } + + return { + name: spec.name, + description: `${spec.summary} CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.`, + inputSchema: { + type: 'object', + properties, + required: ['command'], + additionalProperties: false, + }, + }; +} diff --git a/src/mcp/definitions/issues.js b/src/mcp/definitions/issues.js index 1e03d0e..0aa38a3 100644 --- a/src/mcp/definitions/issues.js +++ b/src/mcp/definitions/issues.js @@ -1,94 +1,32 @@ -export const ISSUES_TOOLS = [ - { - "name": "issues_list", - "description": "List linked issues (/api/v2/{project_id}/issues)", - "inputSchema": { - "type": "object", - "properties": { - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "test_id": { - "type": "string" - }, - "suite_id": { - "type": "string" - }, - "run_id": { - "type": "string" - }, - "testrun_id": { - "type": "integer" - }, - "plan_id": { - "type": "string" - }, - "source": { - "type": "string" - } - }, - "additionalProperties": false - } +import { buildEntityTool } from './entity-tool.js'; +import { paginationParams } from './params.js'; + +export const ISSUES_TOOL_SPEC = { + name: 'issues', + summary: 'Linked issues across resources (/api/v2/{project_id}/issues)', + commands: { + list: 'List linked issues (scope by test_id/suite_id/run_id/testrun_id/plan_id, filter by source)', + create: 'Link issue to a resource (url or jira_id + one scope id)', + delete: 'Unlink issue', }, - { - "name": "issues_create", - "description": "Link issue to resource (/api/v2/{project_id}/issues)", - "inputSchema": { - "type": "object", - "properties": { - "test_id": { - "type": "string" - }, - "suite_id": { - "type": "string" - }, - "run_id": { - "type": "string" - }, - "testrun_id": { - "type": "integer" - }, - "plan_id": { - "type": "string" - }, - "url": { - "type": "string" - }, - "jira_id": { - "type": "string" - } - }, - "additionalProperties": false - } + params: { + test_id: { commands: ['list', 'create'], type: 'string' }, + suite_id: { commands: ['list', 'create'], type: 'string' }, + run_id: { commands: ['list', 'create'], type: 'string' }, + testrun_id: { commands: ['list', 'create'], type: 'integer' }, + plan_id: { commands: ['list', 'create'], type: 'string' }, + source: { commands: ['list'], type: 'string', description: 'Filter issues by source (e.g. jira)' }, + url: { commands: ['create'], type: 'string', description: 'Issue URL to link' }, + jira_id: { commands: ['create'], type: 'string', description: 'Jira issue key to link (alternative to url)' }, + issue_id: { commands: ['delete'], type: 'integer', description: 'ID of the linked issue to remove' }, + type: { + commands: ['delete'], + type: 'string', + enum: ['issue', 'jira_issue'], + description: 'Kind of the linked issue', + }, + ...paginationParams(['list']), }, - { - "name": "issues_delete", - "description": "Unlink issue (/api/v2/{project_id}/issues/{id})", - "inputSchema": { - "type": "object", - "properties": { - "issue_id": { - "type": "integer" - }, - "type": { - "type": "string", - "enum": [ - "issue", - "jira_issue" - ] - } - }, - "required": [ - "issue_id", - "type" - ], - "additionalProperties": false - } - }, -]; +}; + +export const ISSUES_TOOL = buildEntityTool(ISSUES_TOOL_SPEC); diff --git a/src/mcp/definitions/labels.js b/src/mcp/definitions/labels.js index d7b95ce..a024483 100644 --- a/src/mcp/definitions/labels.js +++ b/src/mcp/definitions/labels.js @@ -1,148 +1,37 @@ -export const LABELS_TOOLS = [ - { - "name": "labels_list", - "description": "List labels (/api/v2/{project_id}/labels)", - "inputSchema": { - "type": "object", - "properties": { - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - } - }, - "additionalProperties": false - } +import { buildEntityTool } from './entity-tool.js'; +import { paginationParams } from './params.js'; + +const VISIBILITY_PROPERTY = { + commands: ['create', 'update'], + type: 'array', + items: { type: 'string', enum: ['filter', 'list'] }, +}; + +const SCOPE_PROPERTY = { + commands: ['create', 'update'], + type: 'array', + items: { type: 'string', enum: ['tests', 'suites', 'runs', 'plans', 'steps', 'templates'] }, +}; + +export const LABELS_TOOL_SPEC = { + name: 'labels', + summary: 'Manage labels (/api/v2/{project_id}/labels)', + commands: { + list: 'List labels', + get: 'Get label by slug', + create: 'Create label (title required)', + update: 'Update label by slug', + delete: 'Delete label by slug', }, - { - "name": "labels_get", - "description": "Get label by slug", - "inputSchema": { - "type": "object", - "properties": { - "label_id": { - "type": "string" - } - }, - "required": [ - "label_id" - ], - "additionalProperties": false - } + params: { + label_id: { commands: ['get', 'update', 'delete'], type: 'string', description: 'Label slug' }, + title: { commands: ['create', 'update'], type: 'string' }, + color: { commands: ['create', 'update'], type: 'string' }, + visibility: VISIBILITY_PROPERTY, + scope: SCOPE_PROPERTY, + field: { commands: ['create', 'update'], type: 'object' }, + ...paginationParams(['list']), }, - { - "name": "labels_create", - "description": "Create label (/api/v2/{project_id}/labels)", - "inputSchema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "color": { - "type": "string" - }, - "visibility": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "filter", - "list" - ] - } - }, - "scope": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "tests", - "suites", - "runs", - "plans", - "steps", - "templates" - ] - } - }, - "field": { - "type": "object" - } - }, - "required": [ - "title" - ], - "additionalProperties": false - } - }, - { - "name": "labels_update", - "description": "Update label (/api/v2/{project_id}/labels/{id})", - "inputSchema": { - "type": "object", - "properties": { - "label_id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "color": { - "type": "string" - }, - "visibility": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "filter", - "list" - ] - } - }, - "scope": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "tests", - "suites", - "runs", - "plans", - "steps", - "templates" - ] - } - }, - "field": { - "type": "object" - } - }, - "required": [ - "label_id" - ], - "additionalProperties": false - } - }, - { - "name": "labels_delete", - "description": "Delete label (/api/v2/{project_id}/labels/{id})", - "inputSchema": { - "type": "object", - "properties": { - "label_id": { - "type": "string" - } - }, - "required": [ - "label_id" - ], - "additionalProperties": false - } - }, -]; +}; + +export const LABELS_TOOL = buildEntityTool(LABELS_TOOL_SPEC); diff --git a/src/mcp/definitions/milestones.js b/src/mcp/definitions/milestones.js index b0a614a..631100f 100644 --- a/src/mcp/definitions/milestones.js +++ b/src/mcp/definitions/milestones.js @@ -1,43 +1,27 @@ -export const MILESTONES_TOOLS = [ - { - name: 'milestones_list', - description: 'List milestones (/api/v2/{project_id}/milestones)', - inputSchema: { - type: 'object', - properties: { - page: { - type: 'integer', - minimum: 1, - }, - per_page: { - type: 'integer', - minimum: 1, - maximum: 100, - }, - type: { - type: 'string', - description: 'Filter by milestone type (title), e.g. Sprint or Release.', - }, - status: { - type: 'string', - enum: ['created', 'active', 'closed'], - }, - }, - additionalProperties: false, - }, +import { buildEntityTool } from './entity-tool.js'; +import { paginationParams } from './params.js'; + +export const MILESTONES_TOOL_SPEC = { + name: 'milestones', + summary: 'Milestones: list and get (/api/v2/{project_id}/milestones)', + commands: { + list: 'List milestones (type, status filters)', + get: 'Get milestone by ID', }, - { - name: 'milestones_get', - description: 'Get milestone by ID', - inputSchema: { - type: 'object', - properties: { - milestone_id: { - type: 'string', - }, - }, - required: ['milestone_id'], - additionalProperties: false, + params: { + milestone_id: { commands: ['get'], type: 'string' }, + type: { + commands: ['list'], + type: 'string', + description: 'Filter by milestone type (title), e.g. Sprint or Release.', + }, + status: { + commands: ['list'], + type: 'string', + enum: ['created', 'active', 'closed'], }, + ...paginationParams(['list']), }, -]; +}; + +export const MILESTONES_TOOL = buildEntityTool(MILESTONES_TOOL_SPEC); diff --git a/src/mcp/definitions/params.js b/src/mcp/definitions/params.js new file mode 100644 index 0000000..08d12c6 --- /dev/null +++ b/src/mcp/definitions/params.js @@ -0,0 +1,76 @@ +import { BRANCH_PARAM } from './branches.js'; + +export function paginationParams(commands) { + return { + page: { commands, type: 'integer', minimum: 1 }, + per_page: { commands, type: 'integer', minimum: 1, maximum: 100 }, + }; +} + +export function branchParam(commands) { + return { branch: { commands, ...BRANCH_PARAM } }; +} + +export function stringArrayParam(commands, description) { + return { + type: 'array', + items: { type: 'string' }, + ...(description ? { description } : {}), + commands, + }; +} + +export function idArrayParam(commands, description) { + return stringArrayParam(commands, description); +} + +export function linkActionParam(commands, extraTypes = []) { + const types = ['label', 'custom_field', 'tag', 'milestone', 'issue', 'jira', ...extraTypes]; + return { + commands, + type: 'array', + items: { + type: 'object', + properties: { + action: { type: 'string', enum: ['add', 'remove'] }, + type: { type: 'string', enum: types }, + value: { type: 'string' }, + }, + required: ['action', 'type', 'value'], + additionalProperties: false, + }, + }; +} + +export function issuesSourceParam(commands) { + return { source: { commands, type: 'string', description: 'Filter issues by source (e.g. jira)' } }; +} + +export function issuesLinkParams(commands) { + return { + url: { commands, type: 'string', description: 'Issue URL to link' }, + jira_id: { commands, type: 'string', description: 'Jira issue key to link (alternative to url)' }, + }; +} + +export function issuesUnlinkParams(commands) { + return { + issue_id: { commands, type: 'integer', description: 'ID of the linked issue to remove' }, + type: { commands, type: 'string', enum: ['issue', 'jira_issue'], description: 'Kind of the linked issue' }, + }; +} + +export function attachmentParams() { + return { + file_path: { + commands: ['attachments_upload'], + type: 'string', + description: 'Local path to the file that will be sent as multipart/form-data field "files".', + }, + attachment_id: { + commands: ['attachments_delete'], + type: 'string', + description: 'ID of the attachment to delete', + }, + }; +} diff --git a/src/mcp/definitions/plans.js b/src/mcp/definitions/plans.js index 48c4fe8..a38baa6 100644 --- a/src/mcp/definitions/plans.js +++ b/src/mcp/definitions/plans.js @@ -1,324 +1,60 @@ import { PLANS_TQL_INPUT_DESCRIPTION, PLANS_TQL_REFERENCE } from './tql-reference.js'; +import { buildEntityTool } from './entity-tool.js'; +import { + idArrayParam, + issuesLinkParams, + issuesSourceParam, + issuesUnlinkParams, + linkActionParam, + paginationParams, +} from './params.js'; -export const PLANS_TOOLS = [ - { - "name": "plans_list", - "description": "List plans (/api/v2/{project_id}/plans)", - "inputSchema": { - "type": "object", - "properties": { - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "kind": { - "type": "string", - "enum": [ - "manual", - "automated", - "mixed" - ] - }, - "hidden": { - "type": "boolean" - }, - "labels": { - "type": "array", - "items": { - "type": "string" - } - }, - "search_text": { - "type": "string" - } - }, - "additionalProperties": false - } +export const PLANS_TOOL_SPEC = { + name: 'plans', + summary: `Manage test plans (/api/v2/{project_id}/plans). ${PLANS_TQL_REFERENCE}`, + commands: { + list: 'List plans (kind, hidden, labels, search_text filters)', + get: 'Get plan by ID', + create: 'Create plan (title required; select tests via test_ids/suite_ids/tql)', + update: 'Update plan by ID', + delete: 'Delete plan by ID', + issues_list: 'List linked issues for a plan', + issues_link: 'Link issue to a plan (url or jira_id)', + issues_unlink: 'Unlink issue from a plan', }, - { - "name": "plans_get", - "description": "Get plan by ID", - "inputSchema": { - "type": "object", - "properties": { - "plan_id": { - "type": "string" - } - }, - "required": [ - "plan_id" - ], - "additionalProperties": false - } + params: { + plan_id: { commands: ['get', 'update', 'delete', 'issues_list', 'issues_link'], type: 'string' }, + title: { commands: ['create', 'update'], type: 'string' }, + description: { commands: ['create', 'update'], type: 'string' }, + kind: { + commands: ['list', 'create', 'update'], + type: 'string', + enum: ['manual', 'automated', 'mixed'], + description: 'list: filter by kind; create/update: the plan kind', + }, + hidden: { commands: ['list', 'create', 'update'], type: 'boolean', description: 'list: include hidden plans; create/update: set hidden flag' }, + as_manual: { commands: ['create', 'update'], type: 'boolean' }, + labels: { commands: ['list'], type: 'array', items: { type: 'string' } }, + search_text: { commands: ['list'], type: 'string' }, + tql: { + commands: ['create', 'update'], + type: 'string', + description: PLANS_TQL_INPUT_DESCRIPTION, + }, + test_ids: idArrayParam( + ['create', 'update'], + 'List of test IDs (8-char) to include in the plan. If omitted, all tests matching the plan kind are included.' + ), + suite_ids: idArrayParam( + ['create', 'update'], + 'List of suite IDs (8-char) to include in the plan. If omitted, all suites are considered.' + ), + link: linkActionParam(['create', 'update']), + ...paginationParams(['list', 'issues_list']), + ...issuesSourceParam(['issues_list']), + ...issuesLinkParams(['issues_link']), + ...issuesUnlinkParams(['issues_unlink']), }, - { - "name": "plans_create", - "description": `Create plan (/api/v2/{project_id}/plans). ${PLANS_TQL_REFERENCE}`, - "inputSchema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "kind": { - "type": "string", - "enum": [ - "manual", - "automated", - "mixed" - ] - }, - "hidden": { - "type": "boolean" - }, - "as_manual": { - "type": "boolean" - }, - "test_ids": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of test IDs (8-char) to include in the plan. If omitted, all tests matching the plan kind are included." - }, - "suite_ids": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of suite IDs (8-char) to include in the plan. If omitted, all suites are considered." - }, - "tql": { - "type": "string", - "description": PLANS_TQL_INPUT_DESCRIPTION - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": [ - "add", - "remove" - ] - }, - "type": { - "type": "string", - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira" - ] - }, - "value": { - "type": "string" - } - }, - "required": [ - "action", - "type", - "value" - ], - "additionalProperties": false - } - } - }, - "required": [ - "title" - ], - "additionalProperties": false - } - }, - { - "name": "plans_update", - "description": `Update plan (/api/v2/{project_id}/plans/{id}). ${PLANS_TQL_REFERENCE}`, - "inputSchema": { - "type": "object", - "properties": { - "plan_id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "kind": { - "type": "string", - "enum": [ - "manual", - "automated", - "mixed" - ] - }, - "hidden": { - "type": "boolean" - }, - "as_manual": { - "type": "boolean" - }, - "test_ids": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of test IDs (8-char) to include in the plan. If omitted, all tests matching the plan kind are included." - }, - "suite_ids": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of suite IDs (8-char) to include in the plan. If omitted, all suites are considered." - }, - "tql": { - "type": "string", - "description": PLANS_TQL_INPUT_DESCRIPTION - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": [ - "add", - "remove" - ] - }, - "type": { - "type": "string", - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira" - ] - }, - "value": { - "type": "string" - } - }, - "required": [ - "action", - "type", - "value" - ], - "additionalProperties": false - } - } - }, - "required": [ - "plan_id" - ], - "additionalProperties": false - } - }, - { - "name": "plans_delete", - "description": "Delete plan", - "inputSchema": { - "type": "object", - "properties": { - "plan_id": { - "type": "string" - } - }, - "required": [ - "plan_id" - ], - "additionalProperties": false - } - }, - { - "name": "plans_issues_list", - "description": "List linked issues for a plan (/api/v2/{project_id}/issues?plan_id=...)", - "inputSchema": { - "type": "object", - "properties": { - "plan_id": { - "type": "string" - }, - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "source": { - "type": "string" - } - }, - "required": [ - "plan_id" - ], - "additionalProperties": false - } - }, - { - "name": "plans_issues_link", - "description": "Link issue to a plan (/api/v2/{project_id}/issues)", - "inputSchema": { - "type": "object", - "properties": { - "plan_id": { - "type": "string" - }, - "url": { - "type": "string" - }, - "jira_id": { - "type": "string" - } - }, - "required": [ - "plan_id" - ], - "additionalProperties": false - } - }, - { - "name": "plans_issues_unlink", - "description": "Unlink issue from a plan (/api/v2/{project_id}/issues/{id})", - "inputSchema": { - "type": "object", - "properties": { - "issue_id": { - "type": "integer" - }, - "type": { - "type": "string", - "enum": [ - "issue", - "jira_issue" - ] - } - }, - "required": [ - "issue_id", - "type" - ], - "additionalProperties": false - } - } -]; +}; + +export const PLANS_TOOL = buildEntityTool(PLANS_TOOL_SPEC); diff --git a/src/mcp/definitions/requirements.js b/src/mcp/definitions/requirements.js index 81260cf..cd05915 100644 --- a/src/mcp/definitions/requirements.js +++ b/src/mcp/definitions/requirements.js @@ -1,136 +1,43 @@ -export const REQUIREMENTS_TOOLS = [ - { - name: 'requirements_list', - description: 'List requirements (/api/v2/{project_id}/requirements)', - inputSchema: { - type: 'object', - properties: { - page: { - type: 'integer', - minimum: 1, - }, - per_page: { - type: 'integer', - minimum: 1, - maximum: 100, - }, - source: { - type: 'string', - enum: ['jira', 'confluence', 'file', 'text'], - }, - scope: { - type: 'string', - enum: ['global', 'attached', 'detached', 'without_suites'], - }, - }, - additionalProperties: false, - }, +import { buildEntityTool } from './entity-tool.js'; +import { paginationParams } from './params.js'; + +export const REQUIREMENTS_TOOL_SPEC = { + name: 'requirements', + summary: 'Manage requirements (/api/v2/{project_id}/requirements)', + commands: { + list: 'List requirements (source, scope filters)', + get: 'Get requirement by ID', + create: 'Create requirement (title and source_type required)', + update: 'Update requirement by ID', + delete: 'Delete requirement by ID', }, - { - name: 'requirements_get', - description: 'Get requirement by ID', - inputSchema: { - type: 'object', - properties: { - requirement_id: { - type: 'string', - }, - }, - required: ['requirement_id'], - additionalProperties: false, + params: { + requirement_id: { commands: ['get', 'update', 'delete'], type: 'string' }, + title: { commands: ['create', 'update'], type: 'string' }, + source_type: { commands: ['create'], type: 'string', enum: ['jira', 'confluence', 'file', 'text'] }, + source: { commands: ['list'], type: 'string', enum: ['jira', 'confluence', 'file', 'text'] }, + scope: { commands: ['list'], type: 'string', enum: ['global', 'attached', 'detached', 'without_suites'] }, + description: { + commands: ['create', 'update'], + type: 'string', + description: 'Required for text requirements (min 500 chars on create); only applied for text requirements on update.', }, - }, - { - name: 'requirements_create', - description: 'Create requirement (/api/v2/{project_id}/requirements)', - inputSchema: { - type: 'object', - properties: { - title: { - type: 'string', - }, - source_type: { - type: 'string', - enum: ['jira', 'confluence', 'file', 'text'], - }, - description: { - type: 'string', - description: 'Required for text requirements. Must be at least 500 characters.', - }, - details: { - type: 'string', - }, - active: { - type: 'boolean', - }, - global: { - type: 'boolean', - }, - confluence_url: { - type: 'string', - description: 'Required for confluence requirements.', - }, - files: { - type: 'array', - items: { - type: 'string', - }, - description: 'Local file paths to upload for file requirements.', - }, - }, - required: ['title', 'source_type'], - additionalProperties: false, + details: { commands: ['create', 'update'], type: 'string' }, + active: { commands: ['create', 'update'], type: 'boolean' }, + global: { commands: ['create', 'update'], type: 'boolean' }, + confluence_url: { + commands: ['create'], + type: 'string', + description: 'Required for confluence requirements.', }, - }, - { - name: 'requirements_update', - description: 'Update requirement (/api/v2/{project_id}/requirements/{id})', - inputSchema: { - type: 'object', - properties: { - requirement_id: { - type: 'string', - }, - title: { - type: 'string', - }, - description: { - type: 'string', - description: 'Only applied for text requirements.', - }, - details: { - type: 'string', - }, - active: { - type: 'boolean', - }, - global: { - type: 'boolean', - }, - files: { - type: 'array', - items: { - type: 'string', - }, - description: 'Local file paths to upload for file requirements.', - }, - }, - required: ['requirement_id'], - additionalProperties: false, - }, - }, - { - name: 'requirements_delete', - description: 'Delete requirement (/api/v2/{project_id}/requirements/{id})', - inputSchema: { - type: 'object', - properties: { - requirement_id: { - type: 'string', - }, - }, - required: ['requirement_id'], - additionalProperties: false, + files: { + commands: ['create', 'update'], + type: 'array', + items: { type: 'string' }, + description: 'Local file paths to upload for file requirements.', }, + ...paginationParams(['list']), }, -]; +}; + +export const REQUIREMENTS_TOOL = buildEntityTool(REQUIREMENTS_TOOL_SPEC); diff --git a/src/mcp/definitions/rungroups.js b/src/mcp/definitions/rungroups.js index 533c003..8b06374 100644 --- a/src/mcp/definitions/rungroups.js +++ b/src/mcp/definitions/rungroups.js @@ -1,132 +1,28 @@ -export const RUNGROUPS_TOOLS = [ - { - "name": "rungroups_list", - "description": "List run groups as tree (/api/v2/{project_id}/rungroups)", - "inputSchema": { - "type": "object", - "properties": { - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - } - }, - "additionalProperties": false - } +import { buildEntityTool } from './entity-tool.js'; +import { paginationParams } from './params.js'; + +export const RUNGROUPS_TOOL_SPEC = { + name: 'rungroups', + summary: 'Manage run groups as tree (/api/v2/{project_id}/rungroups)', + commands: { + list: 'List run groups as tree', + get: 'Get run group by ID', + create: 'Create run group (title required)', + update: 'Update run group by ID', + delete: 'Delete run group by ID', }, - { - "name": "rungroups_get", - "description": "Get run group by ID", - "inputSchema": { - "type": "object", - "properties": { - "rungroup_id": { - "type": "string" - } - }, - "required": [ - "rungroup_id" - ], - "additionalProperties": false - } + params: { + rungroup_id: { commands: ['get', 'update', 'delete'], type: 'string' }, + title: { commands: ['create', 'update'], type: 'string' }, + description: { commands: ['create', 'update'], type: 'string' }, + emoji: { commands: ['create', 'update'], type: 'string' }, + kind: { commands: ['create', 'update'], type: 'string' }, + pin: { commands: ['create', 'update'], type: 'boolean' }, + status: { commands: ['create', 'update'], type: 'string' }, + parent_id: { commands: ['create', 'update'], type: 'string' }, + children: { commands: ['create', 'update'], type: 'array', items: {} }, + ...paginationParams(['list']), }, - { - "name": "rungroups_create", - "description": "Create run group (/api/v2/{project_id}/rungroups)", - "inputSchema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "emoji": { - "type": "string" - }, - "kind": { - "type": "string" - }, - "pin": { - "type": "boolean" - }, - "status": { - "type": "string" - }, - "parent_id": { - "type": "string" - }, - "children": { - "type": "array", - "items": {} - } - }, - "required": [ - "title" - ], - "additionalProperties": false - } - }, - { - "name": "rungroups_update", - "description": "Update run group (/api/v2/{project_id}/rungroups/{id})", - "inputSchema": { - "type": "object", - "properties": { - "rungroup_id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "emoji": { - "type": "string" - }, - "kind": { - "type": "string" - }, - "pin": { - "type": "boolean" - }, - "status": { - "type": "string" - }, - "parent_id": { - "type": "string" - }, - "children": { - "type": "array", - "items": {} - } - }, - "required": [ - "rungroup_id" - ], - "additionalProperties": false - } - }, - { - "name": "rungroups_delete", - "description": "Delete run group (/api/v2/{project_id}/rungroups/{id})", - "inputSchema": { - "type": "object", - "properties": { - "rungroup_id": { - "type": "string" - } - }, - "required": [ - "rungroup_id" - ], - "additionalProperties": false - } - }, -]; +}; + +export const RUNGROUPS_TOOL = buildEntityTool(RUNGROUPS_TOOL_SPEC); diff --git a/src/mcp/definitions/runs.js b/src/mcp/definitions/runs.js index bc0502f..4779def 100644 --- a/src/mcp/definitions/runs.js +++ b/src/mcp/definitions/runs.js @@ -1,347 +1,64 @@ import { RUNS_TQL_INPUT_DESCRIPTION, RUNS_TQL_REFERENCE } from './tql-reference.js'; -import { BRANCH_PARAM } from './branches.js'; +import { buildEntityTool } from './entity-tool.js'; +import { + branchParam, + issuesLinkParams, + issuesSourceParam, + issuesUnlinkParams, + linkActionParam, + paginationParams, +} from './params.js'; -export const RUNS_TOOLS = [ - { - "name": "runs_list", - "description": `List runs (/api/v2/{project_id}/runs). ${RUNS_TQL_REFERENCE}`, - "inputSchema": { - "type": "object", - "properties": { - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "tql": { - "type": "string", - "description": RUNS_TQL_INPUT_DESCRIPTION - }, - "branch": BRANCH_PARAM - }, - "additionalProperties": false - } +export const RUNS_TOOL_SPEC = { + name: 'runs', + summary: `Manage runs (/api/v2/{project_id}/runs). ${RUNS_TQL_REFERENCE}`, + commands: { + list: 'List runs (tql filter, pagination)', + get: 'Get run by ID', + create: 'Create run (title required)', + update: 'Update run by ID', + delete: 'Delete run by ID', + issues_list: 'List linked issues for a run', + issues_link: 'Link issue to a run (url or jira_id)', + issues_unlink: 'Unlink issue from a run', }, - { - "name": "runs_get", - "description": "Get run by ID", - "inputSchema": { - "type": "object", - "properties": { - "run_id": { - "type": "string" - }, - "branch": BRANCH_PARAM - }, - "required": [ - "run_id" - ], - "additionalProperties": false - } + params: { + run_id: { + commands: ['get', 'update', 'delete', 'issues_list', 'issues_link'], + type: 'string', + }, + title: { commands: ['create', 'update'], type: 'string' }, + description: { commands: ['create', 'update'], type: 'string' }, + plan_ids: { commands: ['create'], type: 'array', items: { type: 'string' } }, + kind: { + commands: ['create', 'update'], + type: 'string', + enum: ['manual', 'automated', 'mixed'], + }, + rungroup_id: { commands: ['create', 'update'], type: 'string' }, + env: { commands: ['create', 'update'], type: 'string' }, + status_event: { + commands: ['update'], + type: 'string', + enum: ['finish', 'finish_manual', 'launch', 'rerun', 'scheduled', 'terminate'], + }, + assigned_to: { commands: ['create', 'update'], type: 'string' }, + assign_strategy: { + commands: ['create', 'update'], + type: 'string', + enum: ['test', 'random', 'none'], + }, + test_ids: { commands: ['create', 'update'], type: 'array', items: { type: 'string' } }, + suite_ids: { commands: ['create', 'update'], type: 'array', items: { type: 'string' } }, + envs: { commands: ['create'], type: 'array', items: { type: 'string' } }, + link: linkActionParam(['create', 'update']), + tql: { commands: ['list'], type: 'string', description: RUNS_TQL_INPUT_DESCRIPTION }, + ...branchParam(['list', 'get', 'create', 'update', 'delete']), + ...paginationParams(['list', 'issues_list']), + ...issuesSourceParam(['issues_list']), + ...issuesLinkParams(['issues_link']), + ...issuesUnlinkParams(['issues_unlink']), }, - { - "name": "runs_create", - "description": "Create run (/api/v2/{project_id}/runs)", - "inputSchema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "plan_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "kind": { - "type": "string", - "enum": [ - "manual", - "automated", - "mixed" - ] - }, - "rungroup_id": { - "type": "string" - }, - "env": { - "type": "string" - }, - "assigned_to": { - "type": "string" - }, - "assign_strategy": { - "type": "string", - "enum": [ - "test", - "random", - "none" - ] - }, - "test_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "suite_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "envs": { - "type": "array", - "items": { - "type": "string" - } - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": [ - "add", - "remove" - ] - }, - "type": { - "type": "string", - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira" - ] - }, - "value": { - "type": "string" - } - }, - "required": [ - "action", - "type", - "value" - ], - "additionalProperties": false - } - }, - "branch": BRANCH_PARAM - }, - "required": [ - "title" - ], - "additionalProperties": false - } - }, - { - "name": "runs_update", - "description": "Update run (/api/v2/{project_id}/runs/{id})", - "inputSchema": { - "type": "object", - "properties": { - "run_id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "kind": { - "type": "string", - "enum": [ - "manual", - "automated", - "mixed" - ] - }, - "rungroup_id": { - "type": "string" - }, - "env": { - "type": "string" - }, - "status_event": { - "type": "string", - "enum": [ - "finish", - "finish_manual", - "launch", - "rerun", - "scheduled", - "terminate" - ] - }, - "assigned_to": { - "type": "string" - }, - "assign_strategy": { - "type": "string", - "enum": [ - "test", - "random", - "none" - ] - }, - "test_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "suite_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": [ - "add", - "remove" - ] - }, - "type": { - "type": "string", - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira" - ] - }, - "value": { - "type": "string" - } - }, - "required": [ - "action", - "type", - "value" - ], - "additionalProperties": false - } - }, - "branch": BRANCH_PARAM - }, - "required": [ - "run_id" - ], - "additionalProperties": false - } - }, - { - "name": "runs_delete", - "description": "Delete run (/api/v2/{project_id}/runs/{id})", - "inputSchema": { - "type": "object", - "properties": { - "run_id": { - "type": "string" - }, - "branch": BRANCH_PARAM - }, - "required": [ - "run_id" - ], - "additionalProperties": false - } - }, - { - "name": "runs_issues_list", - "description": "List linked issues for a run (/api/v2/{project_id}/issues?run_id=...)", - "inputSchema": { - "type": "object", - "properties": { - "run_id": { - "type": "string" - }, - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "source": { - "type": "string" - } - }, - "required": [ - "run_id" - ], - "additionalProperties": false - } - }, - { - "name": "runs_issues_link", - "description": "Link issue to a run (/api/v2/{project_id}/issues)", - "inputSchema": { - "type": "object", - "properties": { - "run_id": { - "type": "string" - }, - "url": { - "type": "string" - }, - "jira_id": { - "type": "string" - } - }, - "required": [ - "run_id" - ], - "additionalProperties": false - } - }, - { - "name": "runs_issues_unlink", - "description": "Unlink issue from a run (/api/v2/{project_id}/issues/{id})", - "inputSchema": { - "type": "object", - "properties": { - "issue_id": { - "type": "integer" - }, - "type": { - "type": "string", - "enum": [ - "issue", - "jira_issue" - ] - } - }, - "required": [ - "issue_id", - "type" - ], - "additionalProperties": false - } - } -]; +}; + +export const RUNS_TOOL = buildEntityTool(RUNS_TOOL_SPEC); diff --git a/src/mcp/definitions/shares.js b/src/mcp/definitions/shares.js deleted file mode 100644 index ea1dfa1..0000000 --- a/src/mcp/definitions/shares.js +++ /dev/null @@ -1,110 +0,0 @@ -export const SHARES_TOOLS = [ - { - "name": "tests_share", - "description": "Share tests into a suite of another project (/api/v2/{project_id}/shares/tests). Select tests by test_ids, labels, or both (combined). The source project stays the single source of truth; shared copies in the target project are read-only until unlinked. Re-sharing into the same target project does not duplicate. Requests are processed asynchronously: status 'queued' means accepted, not completed. Matched tests that are themselves shared copies are skipped and listed in skipped_test_ids. Source and target projects must be of the same type (Classic/BDD).", - "inputSchema": { - "type": "object", - "properties": { - "test_ids": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Test IDs to share. At least one of test_ids/labels is required. Max 1000 tests per request." - }, - "labels": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Label slugs or titles; every test carrying any of these labels is shared, in addition to test_ids." - }, - "target_project_id": { - "type": "string", - "description": "Project ID (slug) of the destination project. Must be accessible to the current user." - }, - "target_suite_id": { - "type": "string", - "description": "Suite ID in the target project to place the shared tests into. Must be a file-type suite, not a folder." - } - }, - "required": [ - "target_project_id", - "target_suite_id" - ], - "additionalProperties": false - } - }, - { - "name": "suites_share", - "description": "Share suites (with their tests) into one or more other projects (/api/v2/{project_id}/shares/suites). Select suites by suite_ids, labels, or both (combined). File-type suites are linked (read-only copies that stay in sync); folder suites are deep-copied as regular editable copies. Re-sharing a linked suite into a project that already has it does not duplicate. Suites that are themselves shared copies link to their original source. Omit destination_folder_id to share into the root of the target project(s). Requests are processed asynchronously: status 'queued' means accepted, not completed. Source and target projects must be of the same type (Classic/BDD).", - "inputSchema": { - "type": "object", - "properties": { - "suite_ids": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Suite IDs to share. At least one of suite_ids/labels is required. Max 200 suites per request." - }, - "labels": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Label slugs or titles; every suite carrying any of these labels is shared, in addition to suite_ids." - }, - "target_project_ids": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Project IDs (slugs) of the destination projects. Must be accessible to the current user." - }, - "destination_folder_id": { - "type": "string", - "description": "Folder suite ID in the target project to place the shared suites into. Only allowed when sharing to a single target project." - } - }, - "required": [ - "target_project_ids" - ], - "additionalProperties": false - } - }, - { - "name": "tests_unshare", - "description": "Remove a test's share, converting the shared copy back into a regular, editable test (/api/v2/{project_id}/shares/tests/{id}). Only the shared copy can be targeted — the original source test is untouched. Must be called against the project that holds the shared copy.", - "inputSchema": { - "type": "object", - "properties": { - "test_id": { - "type": "string", - "description": "ID of the shared test copy to unlink." - } - }, - "required": [ - "test_id" - ], - "additionalProperties": false - } - }, - { - "name": "suites_unshare", - "description": "Remove a suite's share, converting the shared copy back into a regular, editable suite (/api/v2/{project_id}/shares/suites/{id}). Only the shared (linked) copy can be targeted — the original source suite is untouched. Must be called against the project that holds the shared copy.", - "inputSchema": { - "type": "object", - "properties": { - "suite_id": { - "type": "string", - "description": "ID of the shared suite copy to unlink." - } - }, - "required": [ - "suite_id" - ], - "additionalProperties": false - } - } -]; diff --git a/src/mcp/definitions/snippets.js b/src/mcp/definitions/snippets.js index adf6202..5137a23 100644 --- a/src/mcp/definitions/snippets.js +++ b/src/mcp/definitions/snippets.js @@ -1,164 +1,23 @@ -export const SNIPPETS_TOOLS = [ - { - "name": "snippets_list", - "description": "List snippets (/api/v2/{project_id}/snippets)", - "inputSchema": { - "type": "object", - "properties": { - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - } - }, - "additionalProperties": false - } +import { buildEntityTool } from './entity-tool.js'; +import { linkActionParam, paginationParams } from './params.js'; + +export const SNIPPETS_TOOL_SPEC = { + name: 'snippets', + summary: 'Manage code snippets (/api/v2/{project_id}/snippets)', + commands: { + list: 'List snippets', + get: 'Get snippet by ID', + create: 'Create snippet (title required)', + update: 'Update snippet by ID', + delete: 'Delete snippet by ID', }, - { - "name": "snippets_get", - "description": "Get snippet by ID", - "inputSchema": { - "type": "object", - "properties": { - "snippet_id": { - "type": "integer" - } - }, - "required": [ - "snippet_id" - ], - "additionalProperties": false - } + params: { + snippet_id: { commands: ['get', 'update', 'delete'], type: 'integer' }, + title: { commands: ['create', 'update'], type: 'string' }, + description: { commands: ['create', 'update'], type: 'string' }, + link: linkActionParam(['create', 'update']), + ...paginationParams(['list']), }, - { - "name": "snippets_create", - "description": "Create snippet (/api/v2/{project_id}/snippets)", - "inputSchema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": [ - "add", - "remove" - ] - }, - "type": { - "type": "string", - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira" - ] - }, - "value": { - "type": "string" - } - }, - "required": [ - "action", - "type", - "value" - ], - "additionalProperties": false - } - } - }, - "required": [ - "title" - ], - "additionalProperties": false - } - }, - { - "name": "snippets_update", - "description": "Update snippet (/api/v2/{project_id}/snippets/{id})", - "inputSchema": { - "type": "object", - "properties": { - "snippet_id": { - "type": "integer" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": [ - "add", - "remove" - ] - }, - "type": { - "type": "string", - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira" - ] - }, - "value": { - "type": "string" - } - }, - "required": [ - "action", - "type", - "value" - ], - "additionalProperties": false - } - } - }, - "required": [ - "snippet_id" - ], - "additionalProperties": false - } - }, - { - "name": "snippets_delete", - "description": "Delete snippet (/api/v2/{project_id}/snippets/{id})", - "inputSchema": { - "type": "object", - "properties": { - "snippet_id": { - "type": "integer" - } - }, - "required": [ - "snippet_id" - ], - "additionalProperties": false - } - }, -]; +}; + +export const SNIPPETS_TOOL = buildEntityTool(SNIPPETS_TOOL_SPEC); diff --git a/src/mcp/definitions/steps.js b/src/mcp/definitions/steps.js index ad11cdb..f08f08d 100644 --- a/src/mcp/definitions/steps.js +++ b/src/mcp/definitions/steps.js @@ -1,164 +1,23 @@ -export const STEPS_TOOLS = [ - { - "name": "steps_list", - "description": "List steps (/api/v2/{project_id}/steps)", - "inputSchema": { - "type": "object", - "properties": { - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - } - }, - "additionalProperties": false - } +import { buildEntityTool } from './entity-tool.js'; +import { linkActionParam, paginationParams } from './params.js'; + +export const STEPS_TOOL_SPEC = { + name: 'steps', + summary: 'Manage test steps (/api/v2/{project_id}/steps)', + commands: { + list: 'List steps', + get: 'Get step by ID', + create: 'Create step (title required)', + update: 'Update step by ID', + delete: 'Delete step by ID', }, - { - "name": "steps_get", - "description": "Get step by ID", - "inputSchema": { - "type": "object", - "properties": { - "step_id": { - "type": "integer" - } - }, - "required": [ - "step_id" - ], - "additionalProperties": false - } + params: { + step_id: { commands: ['get', 'update', 'delete'], type: 'integer' }, + title: { commands: ['create', 'update'], type: 'string' }, + description: { commands: ['create', 'update'], type: 'string' }, + link: linkActionParam(['create', 'update']), + ...paginationParams(['list']), }, - { - "name": "steps_create", - "description": "Create step (/api/v2/{project_id}/steps)", - "inputSchema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": [ - "add", - "remove" - ] - }, - "type": { - "type": "string", - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira" - ] - }, - "value": { - "type": "string" - } - }, - "required": [ - "action", - "type", - "value" - ], - "additionalProperties": false - } - } - }, - "required": [ - "title" - ], - "additionalProperties": false - } - }, - { - "name": "steps_update", - "description": "Update step (/api/v2/{project_id}/steps/{id})", - "inputSchema": { - "type": "object", - "properties": { - "step_id": { - "type": "integer" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": [ - "add", - "remove" - ] - }, - "type": { - "type": "string", - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira" - ] - }, - "value": { - "type": "string" - } - }, - "required": [ - "action", - "type", - "value" - ], - "additionalProperties": false - } - } - }, - "required": [ - "step_id" - ], - "additionalProperties": false - } - }, - { - "name": "steps_delete", - "description": "Delete step (/api/v2/{project_id}/steps/{id})", - "inputSchema": { - "type": "object", - "properties": { - "step_id": { - "type": "integer" - } - }, - "required": [ - "step_id" - ], - "additionalProperties": false - } - }, -]; +}; + +export const STEPS_TOOL = buildEntityTool(STEPS_TOOL_SPEC); diff --git a/src/mcp/definitions/suites.js b/src/mcp/definitions/suites.js index cbf6eeb..0b653f4 100644 --- a/src/mcp/definitions/suites.js +++ b/src/mcp/definitions/suites.js @@ -1,308 +1,101 @@ -import { BRANCH_PARAM } from './branches.js'; -export const SUITES_TOOLS = [ - { - "name": "suites_list", - "description": "List suites as tree (/api/v2/{project_id}/suites)", - "inputSchema": { - "type": "object", - "properties": { - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "file_type": { - "type": "string", - "enum": [ - "file", - "folder" - ] - }, - "tag": { - "type": "string" - }, - "labels": { - "type": "string" - }, - "search_text": { - "type": "string" - }, - "branch": BRANCH_PARAM - }, - "additionalProperties": false - } - }, - { - "name": "suites_get", - "description": "Get suite by ID", - "inputSchema": { - "type": "object", - "properties": { - "suite_id": { - "type": "string" - }, - "branch": BRANCH_PARAM - }, - "required": [ - "suite_id" - ], - "additionalProperties": false - } - }, - { - "name": "suites_create", - "description": "Create suite (/api/v2/{project_id}/suites)", - "inputSchema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "emoji": { - "type": "string" - }, - "parent_id": { - "type": "string" - }, - "file_type": { - "type": "string", - "enum": [ - "file", - "folder" - ] - }, - "assigned_to": { - "type": "string" - }, - "file": { - "type": "string" - }, - "children": { - "type": "array", - "items": {} - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": [ - "add", - "remove" - ] - }, - "type": { - "type": "string", - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira", - "requirement" - ] - }, - "value": { - "type": "string" - } - }, - "required": [ - "action", - "type", - "value" - ], - "additionalProperties": false - } - }, - "branch": BRANCH_PARAM - }, - "required": [ - "title" - ], - "additionalProperties": false - } - }, - { - "name": "suites_update", - "description": "Update suite (/api/v2/{project_id}/suites/{id})", - "inputSchema": { - "type": "object", - "properties": { - "suite_id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "emoji": { - "type": "string" - }, - "parent_id": { - "type": "string" - }, - "file_type": { - "type": "string", - "enum": [ - "file", - "folder" - ] - }, - "assigned_to": { - "type": "string" - }, - "file": { - "type": "string" - }, - "children": { - "type": "array", - "items": {} - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": [ - "add", - "remove" - ] - }, - "type": { - "type": "string", - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira", - "requirement" - ] - }, - "value": { - "type": "string" - } - }, - "required": [ - "action", - "type", - "value" - ], - "additionalProperties": false - } - }, - "branch": BRANCH_PARAM - }, - "required": [ - "suite_id" - ], - "additionalProperties": false - } - }, - { - "name": "suites_delete", - "description": "Delete suite", - "inputSchema": { - "type": "object", - "properties": { - "suite_id": { - "type": "string" - }, - "branch": BRANCH_PARAM - }, - "required": [ - "suite_id" - ], - "additionalProperties": false - } - }, - { - "name": "suites_issues_list", - "description": "List linked issues for a suite (/api/v2/{project_id}/issues?suite_id=...)", - "inputSchema": { - "type": "object", - "properties": { - "suite_id": { - "type": "string" - }, - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "source": { - "type": "string" - } - }, - "required": [ - "suite_id" - ], - "additionalProperties": false - } +import { buildEntityTool } from './entity-tool.js'; +import { + attachmentParams, + branchParam, + issuesLinkParams, + issuesSourceParam, + issuesUnlinkParams, + linkActionParam, + paginationParams, +} from './params.js'; + +const SUITE_ID_COMMANDS = [ + 'get', + 'update', + 'delete', + 'unshare', + 'issues_list', + 'issues_link', + 'issues_unlink', + 'attachments_list', + 'attachments_upload', + 'attachments_delete', +]; + +export const SUITES_TOOL_SPEC = { + name: 'suites', + summary: 'Manage suites as tree (/api/v2/{project_id}/suites)', + commands: { + list: 'List suites as tree (filters: file_type, tag, labels, search_text)', + get: 'Get suite by ID', + create: 'Create suite (title required)', + update: 'Update suite by ID', + delete: 'Delete suite by ID', + share: + 'Share suites (with their tests) into one or more other projects. Select by suite_ids, labels, or both. File-type suites are linked (read-only copies that stay in sync); folder suites are deep-copied as editable copies; re-sharing a linked suite into a project that already has it does not duplicate; suites that are themselves shared copies link to their original source; omit destination_folder_id to share into the root; processed asynchronously; projects must be of the same type (Classic/BDD).', + unshare: + "Remove a suite's share, converting the shared copy back into a regular, editable suite. Only the shared (linked) copy can be targeted — the original source suite is untouched. Must be called against the project that holds the shared copy.", + issues_list: 'List linked issues for a suite', + issues_link: 'Link issue to a suite (url or jira_id)', + issues_unlink: 'Unlink issue from a suite', + attachments_list: 'List attachments for a suite', + attachments_upload: 'Upload one attachment to a suite', + attachments_delete: 'Delete attachment from a suite', }, - { - "name": "suites_issues_link", - "description": "Link issue to a suite (/api/v2/{project_id}/issues)", - "inputSchema": { - "type": "object", - "properties": { - "suite_id": { - "type": "string" - }, - "url": { - "type": "string" - }, - "jira_id": { - "type": "string" - } - }, - "required": [ - "suite_id" - ], - "additionalProperties": false - } + params: { + suite_id: { + commands: SUITE_ID_COMMANDS, + type: 'string', + description: 'Suite ID (for unshare: ID of the shared suite copy to unlink)', + }, + title: { commands: ['create', 'update'], type: 'string' }, + description: { commands: ['create', 'update'], type: 'string' }, + emoji: { commands: ['create', 'update'], type: 'string' }, + parent_id: { commands: ['create', 'update'], type: 'string', description: 'Parent suite ID' }, + file_type: { + commands: ['list', 'create', 'update'], + type: 'string', + enum: ['file', 'folder'], + description: 'list: filter by type; create/update: the type of suite', + }, + assigned_to: { commands: ['create', 'update'], type: 'string' }, + file: { commands: ['create', 'update'], type: 'string' }, + children: { commands: ['create', 'update'], type: 'array', items: {} }, + link: linkActionParam(['create', 'update'], ['requirement']), + tag: { commands: ['list'], type: 'string', description: 'Filter by tag title' }, + labels: { + commands: ['list', 'share'], + type: ['string', 'array'], + items: { type: 'string' }, + description: + 'list: filter by label slugs/titles; share: every suite carrying any of these labels is shared, in addition to suite_ids.', + }, + search_text: { commands: ['list'], type: 'string' }, + ...branchParam(['list', 'get', 'create', 'update', 'delete']), + ...paginationParams(['list', 'issues_list']), + ...issuesSourceParam(['issues_list']), + ...issuesLinkParams(['issues_link']), + ...issuesUnlinkParams(['issues_unlink']), + ...attachmentParams(), + suite_ids: { + commands: ['share'], + type: 'array', + items: { type: 'string' }, + description: 'Suite IDs to share. At least one of suite_ids/labels is required. Max 200 suites per request.', + }, + target_project_ids: { + commands: ['share'], + type: 'array', + items: { type: 'string' }, + description: 'Project IDs (slugs) of the destination projects. Must be accessible to the current user.', + }, + destination_folder_id: { + commands: ['share'], + type: 'string', + description: + 'Folder suite ID in the target project to place the shared suites into. Only allowed when sharing to a single target project.', + }, }, - { - "name": "suites_issues_unlink", - "description": "Unlink issue from a suite (/api/v2/{project_id}/issues/{id})", - "inputSchema": { - "type": "object", - "properties": { - "issue_id": { - "type": "integer" - }, - "type": { - "type": "string", - "enum": [ - "issue", - "jira_issue" - ] - } - }, - "required": [ - "issue_id", - "type" - ], - "additionalProperties": false - } - } -]; +}; + +export const SUITES_TOOL = buildEntityTool(SUITES_TOOL_SPEC); diff --git a/src/mcp/definitions/tags.js b/src/mcp/definitions/tags.js index 7661c17..427c746 100644 --- a/src/mcp/definitions/tags.js +++ b/src/mcp/definitions/tags.js @@ -1,43 +1,21 @@ -export const TAGS_TOOLS = [ - { - "name": "tags_list", - "description": "List tags with counts (/api/v2/{project_id}/tags)", - "inputSchema": { - "type": "object", - "properties": {}, - "additionalProperties": false - } +import { buildEntityTool } from './entity-tool.js'; + +export const TAGS_TOOL_SPEC = { + name: 'tags', + summary: 'Tags: list with counts and get tests by tag (/api/v2/{project_id}/tags)', + commands: { + list: 'List tags with counts', + get: 'Get tests by tag title (tag_id)', + search: 'Search by tag title (delegates to get)', }, - { - "name": "tags_get", - "description": "Get tests by tag title (/api/v2/{project_id}/tags/{id})", - "inputSchema": { - "type": "object", - "properties": { - "tag_id": { - "type": "string" - } - }, - "required": [ - "tag_id" - ], - "additionalProperties": false - } + params: { + tag_id: { + commands: ['get', 'search'], + type: 'string', + description: 'Tag title to look up', + }, + query: { commands: ['search'], type: 'string' }, }, - { - "name": "tags_search", - "description": "Search by tag title (delegates to tags_get)", - "inputSchema": { - "type": "object", - "properties": { - "tag_id": { - "type": "string" - }, - "query": { - "type": "string" - } - }, - "additionalProperties": false - } - } -]; +}; + +export const TAGS_TOOL = buildEntityTool(TAGS_TOOL_SPEC); diff --git a/src/mcp/definitions/testruns.js b/src/mcp/definitions/testruns.js index 04d805e..67cf55c 100644 --- a/src/mcp/definitions/testruns.js +++ b/src/mcp/definitions/testruns.js @@ -1,318 +1,80 @@ -export const TESTRUNS_TOOLS = [ - { - "name": "testruns_list", - "description": "List testruns (/api/v2/{project_id}/testruns)", - "inputSchema": { - "type": "object", - "properties": { - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "run_id": { - "type": "string" - }, - "test_ids": { - "type": [ - "array", - "string" - ], - "items": { - "type": "string" - } - }, - "filter_status": { - "type": "string", - "enum": [ - "passed", - "failed", - "skipped", - "pending" - ] - }, - "filter_kind": { - "type": "string", - "enum": [ - "manual", - "automated" - ] - }, - "filter_user": { - "type": [ - "integer", - "string" - ] - }, - "filter_priority": { - "type": "string", - "enum": [ - "low", - "normal", - "important", - "high", - "critical" - ] - }, - "filter_substatus": { - "type": "string" - }, - "filter_search": { - "type": "string" - }, - "filter_message": { - "type": "boolean" - }, - "filter_link": { - "type": "boolean" - }, - "filter_finished_at_date_range": { - "type": "string" - }, - "tags": { - "type": [ - "array", - "string" - ], - "items": { - "type": "string" - } - }, - "labels": { - "type": [ - "array", - "string" - ], - "items": { - "type": "string" - } - }, - "envs": { - "type": [ - "array", - "string" - ], - "items": { - "type": "string" - } - }, - "rungroups": { - "type": [ - "array", - "string" - ], - "items": { - "type": "string" - } - }, - "defects": { - "type": "string", - "enum": [ - "has_defects", - "without_defects" - ] - } - }, - "additionalProperties": false - } +import { buildEntityTool } from './entity-tool.js'; +import { + attachmentParams, + issuesLinkParams, + issuesSourceParam, + issuesUnlinkParams, + paginationParams, +} from './params.js'; + +const arrayOrString = { type: ['array', 'string'], items: { type: 'string' } }; + +export const TESTRUNS_TOOL_SPEC = { + name: 'testruns', + summary: 'Manage individual test runs (/api/v2/{project_id}/testruns)', + commands: { + list: 'List testruns (rich filters: filter_status, filter_kind, tags, labels, envs, rungroups, defects, ...)', + get: 'Get testrun by ID', + create: 'Create testrun in a run (run_id required)', + update: 'Update testrun by ID', + delete: 'Delete testrun by ID', + issues_list: 'List linked issues for a testrun', + issues_link: 'Link issue to a testrun (url or jira_id)', + issues_unlink: 'Unlink issue from a testrun', + attachments_list: 'List attachments for a testrun', + attachments_upload: 'Upload one attachment to a testrun', + attachments_delete: 'Delete attachment from a testrun', }, - { - "name": "testruns_get", - "description": "Get testrun by ID", - "inputSchema": { - "type": "object", - "properties": { - "testrun_id": { - "type": "integer" - } - }, - "required": [ - "testrun_id" - ], - "additionalProperties": false - } + params: { + testrun_id: { + commands: ['get', 'update', 'delete', 'issues_list', 'issues_link', 'attachments_list', 'attachments_upload', 'attachments_delete'], + type: 'integer', + }, + run_id: { + commands: ['list', 'create', 'update'], + type: 'string', + description: 'list: filter by run; create/update: the run the testrun belongs to', + }, + test_id: { commands: ['create', 'update'], type: 'string' }, + test_ids: { commands: ['list'], ...arrayOrString }, + status: { + commands: ['create', 'update'], + type: 'string', + enum: ['passed', 'failed', 'skipped', 'pending'], + }, + message: { commands: ['create', 'update'], type: 'string' }, + run_time: { commands: ['create', 'update'], type: 'number' }, + assigned_to: { commands: ['create', 'update'], type: 'string' }, + test_title: { commands: ['create', 'update'], type: 'string' }, + automated: { commands: ['create', 'update'], type: 'boolean' }, + filter_status: { + commands: ['list'], + type: 'string', + enum: ['passed', 'failed', 'skipped', 'pending'], + }, + filter_kind: { commands: ['list'], type: 'string', enum: ['manual', 'automated'] }, + filter_user: { commands: ['list'], type: ['integer', 'string'] }, + filter_priority: { + commands: ['list'], + type: 'string', + enum: ['low', 'normal', 'important', 'high', 'critical'], + }, + filter_substatus: { commands: ['list'], type: 'string' }, + filter_search: { commands: ['list'], type: 'string' }, + filter_message: { commands: ['list'], type: 'boolean' }, + filter_link: { commands: ['list'], type: 'boolean' }, + filter_finished_at_date_range: { commands: ['list'], type: 'string' }, + tags: { commands: ['list'], ...arrayOrString }, + labels: { commands: ['list'], ...arrayOrString }, + envs: { commands: ['list'], ...arrayOrString }, + rungroups: { commands: ['list'], ...arrayOrString }, + defects: { commands: ['list'], type: 'string', enum: ['has_defects', 'without_defects'] }, + ...paginationParams(['list', 'issues_list']), + ...issuesSourceParam(['issues_list']), + ...issuesLinkParams(['issues_link']), + ...issuesUnlinkParams(['issues_unlink']), + ...attachmentParams(), }, - { - "name": "testruns_create", - "description": "Create testrun (/api/v2/{project_id}/testruns)", - "inputSchema": { - "type": "object", - "properties": { - "run_id": { - "type": "string" - }, - "test_id": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "passed", - "failed", - "skipped", - "pending" - ] - }, - "message": { - "type": "string" - }, - "run_time": { - "type": "number" - }, - "assigned_to": { - "type": "string" - }, - "test_title": { - "type": "string" - }, - "automated": { - "type": "boolean" - } - }, - "required": [ - "run_id" - ], - "additionalProperties": false - } - }, - { - "name": "testruns_update", - "description": "Update testrun (/api/v2/{project_id}/testruns/{id})", - "inputSchema": { - "type": "object", - "properties": { - "testrun_id": { - "type": "integer" - }, - "run_id": { - "type": "string" - }, - "test_id": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "passed", - "failed", - "skipped", - "pending" - ] - }, - "message": { - "type": "string" - }, - "run_time": { - "type": "number" - }, - "assigned_to": { - "type": "string" - }, - "test_title": { - "type": "string" - }, - "automated": { - "type": "boolean" - } - }, - "required": [ - "testrun_id" - ], - "additionalProperties": false - } - }, - { - "name": "testruns_delete", - "description": "Delete testrun (/api/v2/{project_id}/testruns/{id})", - "inputSchema": { - "type": "object", - "properties": { - "testrun_id": { - "type": "integer" - } - }, - "required": [ - "testrun_id" - ], - "additionalProperties": false - } - }, - { - "name": "testruns_issues_list", - "description": "List linked issues for a testrun (/api/v2/{project_id}/issues?testrun_id=...)", - "inputSchema": { - "type": "object", - "properties": { - "testrun_id": { - "type": "integer" - }, - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "source": { - "type": "string" - } - }, - "required": [ - "testrun_id" - ], - "additionalProperties": false - } - }, - { - "name": "testruns_issues_link", - "description": "Link issue to a testrun (/api/v2/{project_id}/issues)", - "inputSchema": { - "type": "object", - "properties": { - "testrun_id": { - "type": "integer" - }, - "url": { - "type": "string" - }, - "jira_id": { - "type": "string" - } - }, - "required": [ - "testrun_id" - ], - "additionalProperties": false - } - }, - { - "name": "testruns_issues_unlink", - "description": "Unlink issue from a testrun (/api/v2/{project_id}/issues/{id})", - "inputSchema": { - "type": "object", - "properties": { - "issue_id": { - "type": "integer" - }, - "type": { - "type": "string", - "enum": [ - "issue", - "jira_issue" - ] - } - }, - "required": [ - "issue_id", - "type" - ], - "additionalProperties": false - } - } -]; +}; + +export const TESTRUNS_TOOL = buildEntityTool(TESTRUNS_TOOL_SPEC); diff --git a/src/mcp/definitions/tests.js b/src/mcp/definitions/tests.js index 3a6fa9a..c6cd411 100644 --- a/src/mcp/definitions/tests.js +++ b/src/mcp/definitions/tests.js @@ -1,314 +1,102 @@ import { TESTS_TQL_INPUT_DESCRIPTION, TESTS_TQL_REFERENCE } from './tql-reference.js'; -import { BRANCH_PARAM } from './branches.js'; +import { buildEntityTool } from './entity-tool.js'; +import { + attachmentParams, + branchParam, + idArrayParam, + issuesLinkParams, + issuesSourceParam, + issuesUnlinkParams, + linkActionParam, + paginationParams, +} from './params.js'; -export const TESTS_TOOLS = [ - { - "name": "tests_list", - "description": `List tests (/api/v2/{project_id}/tests). ${TESTS_TQL_REFERENCE}`, - "inputSchema": { - "type": "object", - "properties": { - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "tql": { - "type": "string", - "description": TESTS_TQL_INPUT_DESCRIPTION - }, - "branch": BRANCH_PARAM - }, - "additionalProperties": false - } - }, - { - "name": "tests_get", - "description": "Get test by ID", - "inputSchema": { - "type": "object", - "properties": { - "test_id": { - "type": "string" - }, - "branch": BRANCH_PARAM - }, - "required": [ - "test_id" - ], - "additionalProperties": false - } - }, - { - "name": "tests_create", - "description": "Create test (/api/v2/{project_id}/tests)", - "inputSchema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "suite_id": { - "type": "string" - }, - "description": { - "type": "string" - }, - "emoji": { - "type": "string" - }, - "priority": { - "type": "string", - "enum": [ - "low", - "normal", - "important", - "high", - "critical" - ] - }, - "assigned_to": { - "type": "string" - }, - "code": { - "type": "string" - }, - "state": { - "type": "string", - "enum": [ - "manual", - "detached", - "automated" - ] - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": [ - "add", - "remove" - ] - }, - "type": { - "type": "string", - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira" - ] - }, - "value": { - "type": "string" - } - }, - "required": [ - "action", - "type", - "value" - ], - "additionalProperties": false - } - }, - "branch": BRANCH_PARAM - }, - "required": [ - "title", - "suite_id" - ], - "additionalProperties": false - } - }, - { - "name": "tests_update", - "description": "Update test (/api/v2/{project_id}/tests/{id})", - "inputSchema": { - "type": "object", - "properties": { - "test_id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "suite_id": { - "type": "string" - }, - "description": { - "type": "string" - }, - "emoji": { - "type": "string" - }, - "priority": { - "type": "string", - "enum": [ - "low", - "normal", - "important", - "high", - "critical" - ] - }, - "assigned_to": { - "type": "string" - }, - "code": { - "type": "string" - }, - "state": { - "type": "string", - "enum": [ - "manual", - "detached", - "automated" - ] - }, - "sync": { - "type": "boolean" - }, - "link": { - "type": "array", - "items": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": [ - "add", - "remove" - ] - }, - "type": { - "type": "string", - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira" - ] - }, - "value": { - "type": "string" - } - }, - "required": [ - "action", - "type", - "value" - ], - "additionalProperties": false - } - }, - "branch": BRANCH_PARAM - }, - "required": [ - "test_id" - ], - "additionalProperties": false - } - }, - { - "name": "tests_delete", - "description": "Delete test", - "inputSchema": { - "type": "object", - "properties": { - "test_id": { - "type": "string" - }, - "branch": BRANCH_PARAM - }, - "required": [ - "test_id" - ], - "additionalProperties": false - } - }, - { - "name": "tests_issues_list", - "description": "List linked issues for a test (/api/v2/{project_id}/issues?test_id=...)", - "inputSchema": { - "type": "object", - "properties": { - "test_id": { - "type": "string" - }, - "page": { - "type": "integer", - "minimum": 1 - }, - "per_page": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "source": { - "type": "string" - } - }, - "required": [ - "test_id" - ], - "additionalProperties": false - } +const TEST_ID_COMMANDS = [ + 'get', + 'update', + 'delete', + 'unshare', + 'issues_list', + 'issues_link', + 'issues_unlink', + 'attachments_list', + 'attachments_upload', + 'attachments_delete', +]; + +export const TESTS_TOOL_SPEC = { + name: 'tests', + summary: `Manage tests (/api/v2/{project_id}/tests). ${TESTS_TQL_REFERENCE}`, + commands: { + list: 'List tests (tql filter, pagination)', + get: 'Get test by ID', + create: 'Create test (title and suite_id required)', + update: 'Update test by ID', + delete: 'Delete test by ID', + share: + 'Share tests into a suite of another project. Select by test_ids, labels, or both. Source project stays the single source of truth; shared copies are read-only until unlinked; re-sharing does not duplicate; processed asynchronously ("queued" = accepted); skipped shared copies are listed in skipped_test_ids; projects must be of the same type (Classic/BDD).', + unshare: + "Remove a test's share, converting the shared copy back into a regular, editable test. Only the shared copy can be targeted — the original source test is untouched. Must be called against the project that holds the shared copy.", + issues_list: 'List linked issues for a test', + issues_link: 'Link issue to a test (url or jira_id)', + issues_unlink: 'Unlink issue from a test', + attachments_list: 'List attachments for a test', + attachments_upload: 'Upload one attachment to a test', + attachments_delete: 'Delete attachment from a test', }, - { - "name": "tests_issues_link", - "description": "Link issue to a test (/api/v2/{project_id}/issues)", - "inputSchema": { - "type": "object", - "properties": { - "test_id": { - "type": "string" - }, - "url": { - "type": "string" - }, - "jira_id": { - "type": "string" - } - }, - "required": [ - "test_id" - ], - "additionalProperties": false - } + params: { + test_id: { + commands: TEST_ID_COMMANDS, + type: 'string', + description: 'Test ID (for unshare: ID of the shared test copy to unlink)', + }, + title: { commands: ['create', 'update'], type: 'string' }, + suite_id: { commands: ['create', 'update'], type: 'string', description: 'Suite to place the test in' }, + description: { commands: ['create', 'update'], type: 'string' }, + emoji: { commands: ['create', 'update'], type: 'string' }, + priority: { + commands: ['create', 'update'], + type: 'string', + enum: ['low', 'normal', 'important', 'high', 'critical'], + }, + assigned_to: { commands: ['create', 'update'], type: 'string' }, + code: { commands: ['create', 'update'], type: 'string' }, + state: { + commands: ['create', 'update'], + type: 'string', + enum: ['manual', 'detached', 'automated'], + }, + sync: { commands: ['update'], type: 'boolean' }, + link: linkActionParam(['create', 'update']), + tql: { commands: ['list'], type: 'string', description: TESTS_TQL_INPUT_DESCRIPTION }, + ...branchParam(['list', 'get', 'create', 'update', 'delete']), + ...paginationParams(['list', 'issues_list']), + ...issuesSourceParam(['issues_list']), + ...issuesLinkParams(['issues_link']), + ...issuesUnlinkParams(['issues_unlink']), + ...attachmentParams(), + test_ids: idArrayParam( + ['share'], + 'Test IDs to share. At least one of test_ids/labels is required. Max 1000 tests per request.' + ), + labels: { + commands: ['share'], + type: 'array', + items: { type: 'string' }, + description: + 'Label slugs or titles; every test carrying any of these labels is shared, in addition to test_ids.', + }, + target_project_id: { + commands: ['share'], + type: 'string', + description: 'Project ID (slug) of the destination project. Must be accessible to the current user.', + }, + target_suite_id: { + commands: ['share'], + type: 'string', + description: 'Suite ID in the target project to place the shared tests into. Must be a file-type suite, not a folder.', + }, }, - { - "name": "tests_issues_unlink", - "description": "Unlink issue from a test (/api/v2/{project_id}/issues/{id})", - "inputSchema": { - "type": "object", - "properties": { - "issue_id": { - "type": "integer" - }, - "type": { - "type": "string", - "enum": [ - "issue", - "jira_issue" - ] - } - }, - "required": [ - "issue_id", - "type" - ], - "additionalProperties": false - } - } -]; +}; + +export const TESTS_TOOL = buildEntityTool(TESTS_TOOL_SPEC); diff --git a/src/mcp/entity-commands.js b/src/mcp/entity-commands.js new file mode 100644 index 0000000..b5cc12d --- /dev/null +++ b/src/mcp/entity-commands.js @@ -0,0 +1,28 @@ +const CRUD_COMMANDS = ['list', 'get', 'create', 'update', 'delete']; +const ISSUE_COMMANDS = ['issues_list', 'issues_link', 'issues_unlink']; +const ATTACHMENT_COMMANDS = ['attachments_list', 'attachments_upload', 'attachments_delete']; + +export const ENTITY_COMMANDS = { + tests: [...CRUD_COMMANDS, 'share', 'unshare', ...ISSUE_COMMANDS, ...ATTACHMENT_COMMANDS], + suites: [...CRUD_COMMANDS, 'share', 'unshare', ...ISSUE_COMMANDS, ...ATTACHMENT_COMMANDS], + runs: [...CRUD_COMMANDS, ...ISSUE_COMMANDS], + testruns: [...CRUD_COMMANDS, ...ISSUE_COMMANDS, ...ATTACHMENT_COMMANDS], + plans: [...CRUD_COMMANDS, ...ISSUE_COMMANDS], + rungroups: [...CRUD_COMMANDS], + steps: [...CRUD_COMMANDS], + snippets: [...CRUD_COMMANDS], + labels: [...CRUD_COMMANDS], + requirements: [...CRUD_COMMANDS], + branches: [...CRUD_COMMANDS], + tags: ['list', 'get', 'search'], + milestones: ['list', 'get'], + issues: ['list', 'create', 'delete'], +}; + +export const READ_ONLY_COMMANDS = new Set([ + 'list', + 'get', + 'search', + 'issues_list', + 'attachments_list', +]); diff --git a/src/mcp/list-projection.js b/src/mcp/list-projection.js index 90de03d..e8852a2 100644 --- a/src/mcp/list-projection.js +++ b/src/mcp/list-projection.js @@ -110,7 +110,7 @@ export function backendSlimQuery({ verbose = false, fields, count = false } = {} return !verbose && !(Array.isArray(fields) && fields.length) ? { slim: true } : {}; } -const LIST_OPTION_PROPERTIES = { +export const LIST_OPTION_PROPERTIES = { verbose: { type: 'boolean', default: false, @@ -165,13 +165,13 @@ function isPrimaryListToolName(name) { ); } -const COUNT_PROPERTY = { +export const COUNT_PROPERTY = { type: 'boolean', description: 'Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.', }; -const GROUP_BY_PROPERTY = { +export const GROUP_BY_PROPERTY = { type: 'string', description: 'Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.', diff --git a/src/mcp/registry/attachments.js b/src/mcp/registry/attachments.js index f2c1537..52fe7ab 100644 --- a/src/mcp/registry/attachments.js +++ b/src/mcp/registry/attachments.js @@ -28,7 +28,7 @@ export const attachmentMethods = { const data = await fs.readFile(resolvedPath); const formData = new FormData(); - formData.append('file', new Blob([data]), path.basename(resolvedPath)); + formData.append('files', new Blob([data]), path.basename(resolvedPath)); return formData; }, diff --git a/src/mcp/registry/handlers.js b/src/mcp/registry/handlers.js index 45d8a9c..b92388a 100644 --- a/src/mcp/registry/handlers.js +++ b/src/mcp/registry/handlers.js @@ -118,6 +118,7 @@ export const handlerMethods = { return this.asText(slimList(await this.listTags(listArgs), { ...args, entity: 'tags' })); }; handlers.tags_get = async ({ tag_id: tagId }) => this.asText(await this.getTagByTitle(tagId)); + handlers.tags_search = async (args = {}) => this.asText(await this.searchTags(args)); handlers.milestones_list = async (args = {}) => { const { verbose, fields, ...listArgs } = args; diff --git a/src/mcp/tool-definitions.js b/src/mcp/tool-definitions.js index 827b6ca..b56499b 100644 --- a/src/mcp/tool-definitions.js +++ b/src/mcp/tool-definitions.js @@ -1,40 +1,53 @@ import { SYSTEM_TOOLS } from './definitions/system.js'; import { PROJECT_TOOLS } from './definitions/projects.js'; -import { TESTS_TOOLS } from './definitions/tests.js'; -import { SUITES_TOOLS } from './definitions/suites.js'; -import { SHARES_TOOLS } from './definitions/shares.js'; -import { RUNS_TOOLS } from './definitions/runs.js'; -import { TESTRUNS_TOOLS } from './definitions/testruns.js'; -import { RUNGROUPS_TOOLS } from './definitions/rungroups.js'; -import { STEPS_TOOLS } from './definitions/steps.js'; -import { SNIPPETS_TOOLS } from './definitions/snippets.js'; -import { LABELS_TOOLS } from './definitions/labels.js'; -import { TAGS_TOOLS } from './definitions/tags.js'; -import { MILESTONES_TOOLS } from './definitions/milestones.js'; -import { ISSUES_TOOLS } from './definitions/issues.js'; -import { PLANS_TOOLS } from './definitions/plans.js'; -import { REQUIREMENTS_TOOLS } from './definitions/requirements.js'; -import { ATTACHMENT_TOOLS } from './definitions/attachments.js'; -import { BRANCHES_TOOLS } from './definitions/branches.js'; -import { withListOptions, withCountGroupOptions } from './list-projection.js'; +import { TESTS_TOOL, TESTS_TOOL_SPEC } from './definitions/tests.js'; +import { SUITES_TOOL, SUITES_TOOL_SPEC } from './definitions/suites.js'; +import { RUNS_TOOL, RUNS_TOOL_SPEC } from './definitions/runs.js'; +import { TESTRUNS_TOOL, TESTRUNS_TOOL_SPEC } from './definitions/testruns.js'; +import { RUNGROUPS_TOOL, RUNGROUPS_TOOL_SPEC } from './definitions/rungroups.js'; +import { STEPS_TOOL, STEPS_TOOL_SPEC } from './definitions/steps.js'; +import { SNIPPETS_TOOL, SNIPPETS_TOOL_SPEC } from './definitions/snippets.js'; +import { LABELS_TOOL, LABELS_TOOL_SPEC } from './definitions/labels.js'; +import { TAGS_TOOL, TAGS_TOOL_SPEC } from './definitions/tags.js'; +import { MILESTONES_TOOL, MILESTONES_TOOL_SPEC } from './definitions/milestones.js'; +import { ISSUES_TOOL, ISSUES_TOOL_SPEC } from './definitions/issues.js'; +import { PLANS_TOOL, PLANS_TOOL_SPEC } from './definitions/plans.js'; +import { REQUIREMENTS_TOOL, REQUIREMENTS_TOOL_SPEC } from './definitions/requirements.js'; +import { BRANCHES_TOOL, BRANCHES_TOOL_SPEC } from './definitions/branches.js'; -export const TOOL_DEFINITIONS = withCountGroupOptions(withListOptions([ +export const TOOL_DEFINITIONS = [ ...SYSTEM_TOOLS, ...PROJECT_TOOLS, - ...TESTS_TOOLS, - ...SUITES_TOOLS, - ...SHARES_TOOLS, - ...RUNS_TOOLS, - ...TESTRUNS_TOOLS, - ...RUNGROUPS_TOOLS, - ...STEPS_TOOLS, - ...SNIPPETS_TOOLS, - ...LABELS_TOOLS, - ...TAGS_TOOLS, - ...MILESTONES_TOOLS, - ...ISSUES_TOOLS, - ...ATTACHMENT_TOOLS, - ...PLANS_TOOLS, - ...REQUIREMENTS_TOOLS, - ...BRANCHES_TOOLS, -])); + TESTS_TOOL, + SUITES_TOOL, + RUNS_TOOL, + TESTRUNS_TOOL, + RUNGROUPS_TOOL, + STEPS_TOOL, + SNIPPETS_TOOL, + LABELS_TOOL, + TAGS_TOOL, + MILESTONES_TOOL, + ISSUES_TOOL, + PLANS_TOOL, + REQUIREMENTS_TOOL, + BRANCHES_TOOL, +]; + +// Specs behind the entity tools; tool-profiles rebuilds profiled tools from them. +export const ENTITY_TOOL_SPECS = [ + TESTS_TOOL_SPEC, + SUITES_TOOL_SPEC, + RUNS_TOOL_SPEC, + TESTRUNS_TOOL_SPEC, + RUNGROUPS_TOOL_SPEC, + STEPS_TOOL_SPEC, + SNIPPETS_TOOL_SPEC, + LABELS_TOOL_SPEC, + TAGS_TOOL_SPEC, + MILESTONES_TOOL_SPEC, + ISSUES_TOOL_SPEC, + PLANS_TOOL_SPEC, + REQUIREMENTS_TOOL_SPEC, + BRANCHES_TOOL_SPEC, +]; diff --git a/src/mcp/tool-profiles.js b/src/mcp/tool-profiles.js index 97a1d1f..c8f1755 100644 --- a/src/mcp/tool-profiles.js +++ b/src/mcp/tool-profiles.js @@ -1,41 +1,43 @@ +import { ENTITY_TOOL_SPECS } from './tool-definitions.js'; +import { buildEntityTool } from './definitions/entity-tool.js'; +import { READ_ONLY_COMMANDS } from './entity-commands.js'; + const RARE_ENTITIES = new Set(['steps', 'snippets', 'labels', 'rungroups']); -function entityOf(name) { - if (name === 'system_ping') return 'system'; - return name - .split('_attachments_')[0] - .split('_issues_')[0] - .replace(/_(list|get|create|update|delete|search)$/, ''); -} +const SPEC_BY_NAME = new Map(ENTITY_TOOL_SPECS.map((spec) => [spec.name, spec])); -function isAttachment(name) { - return name.includes('_attachments_'); +function restrictSpecToReadOnly(spec) { + const commands = Object.fromEntries( + Object.entries(spec.commands).filter(([command]) => READ_ONLY_COMMANDS.has(command)) + ); + const params = Object.fromEntries( + Object.entries(spec.params) + .map(([key, param]) => [ + key, + { ...param, commands: param.commands.filter((command) => READ_ONLY_COMMANDS.has(command)) }, + ]) + .filter(([, param]) => param.commands.length > 0) + ); + return { ...spec, commands, params }; } -function isReadOp(name) { - return ( - name === 'system_ping' || - /_(list|get|results)$/.test(name) || - name.endsWith('_issues_list') - ); +function shapeToolForProfile(tool, profile) { + if (!tool || !tool.name) return tool; + if (RARE_ENTITIES.has(tool.name)) return undefined; + if (profile === 'core') return tool; + + const spec = SPEC_BY_NAME.get(tool.name); + if (!spec) return tool; + return buildEntityTool(restrictSpecToReadOnly(spec)); } /** - * Whether a tool is visible under the given profile. Unknown profiles fall back to full + * Select tools for a profile. Unknown profiles fall back to full. * - * @param {string} name tool name + * @param {Array} allTools * @param {string} profile 'full' | 'core' | 'read' */ -export function isToolInProfile(name, profile) { - if (!profile || profile === 'full') return true; - if (isAttachment(name)) return false; - const coreEntity = !RARE_ENTITIES.has(entityOf(name)); - if (profile === 'core') return coreEntity; - if (profile === 'read') return coreEntity && isReadOp(name); - return true; -} - export function selectTools(allTools, profile) { if (!profile || profile === 'full') return allTools; - return allTools.filter((tool) => tool && isToolInProfile(tool.name, profile)); + return allTools.map((tool) => shapeToolForProfile(tool, profile)).filter(Boolean); } diff --git a/src/mcp/tool-registry.js b/src/mcp/tool-registry.js index fbe8e95..ec96b00 100644 --- a/src/mcp/tool-registry.js +++ b/src/mcp/tool-registry.js @@ -1,6 +1,7 @@ import { DEFAULT_TOOL_RESPONSE } from '../config/constants.js'; import { ApiError, NotImplementedToolError } from '../core/errors.js'; import { textResponse } from '../helpers/mcp-response.js'; +import { ENTITY_COMMANDS } from './entity-commands.js'; import { TOOL_DEFINITIONS } from './tool-definitions.js'; import { TQL_FULL_REFERENCE } from './definitions/tql-reference.js'; import { handlerMethods } from './registry/handlers.js'; @@ -58,7 +59,8 @@ export class ToolRegistry { } buildHandlers() { - const handlers = { + // Ops are keyed by "_" (tests_list, tests_issues_link, ...). + const ops = { system_ping: async () => this.asText({ status: 'ok', @@ -69,18 +71,24 @@ export class ToolRegistry { tql_help: async () => textResponse(TQL_FULL_REFERENCE), }; - this.registerEntityCrudHandlers(handlers); - this.registerScopedIssueHandlers(handlers); - this.registerScopedAttachmentHandlers(handlers); - this.registerGlobalHandlers(handlers); - this.registerShareHandlers(handlers); + this.registerEntityCrudHandlers(ops); + this.registerScopedIssueHandlers(ops); + this.registerScopedAttachmentHandlers(ops); + this.registerGlobalHandlers(ops); + this.registerShareHandlers(ops); for (const registerHandlers of this.handlerRegistrars) { - registerHandlers.call(this, handlers); + registerHandlers.call(this, ops); } + const handlers = {}; for (const tool of this.tools) { - if (tool.name === 'system_ping') continue; - if (!handlers[tool.name]) { + const commands = ENTITY_COMMANDS[tool.name]; + if (commands) { + handlers[tool.name] = this.buildCommandHandler(tool.name, commands, ops); + } else if (ops[tool.name]) { + // command-less singletons + custom registrar tools (e.g. enterprise analytics) + handlers[tool.name] = ops[tool.name]; + } else { handlers[tool.name] = async () => textResponse(`${DEFAULT_TOOL_RESPONSE} (${tool.name})`); } } @@ -88,6 +96,19 @@ export class ToolRegistry { return handlers; } + buildCommandHandler(toolName, commands, ops) { + return async (args = {}) => { + const { command, ...commandArgs } = args; + const op = command ? ops[`${toolName}_${command}`] : undefined; + if (!op) { + throw new Error( + `Unknown command ${command === undefined ? '(missing)' : `"${command}"`} for tool "${toolName}". Valid commands: ${commands.join(', ')}.` + ); + } + return op(commandArgs); + }; + } + async execute(name, args = {}) { const handler = this.handlers[name]; if (!handler) { diff --git a/test/__snapshots__/tools-list.test.js.snap b/test/__snapshots__/tools-list.test.js.snap index 9e54026..e0237dd 100644 --- a/test/__snapshots__/tools-list.test.js.snap +++ b/test/__snapshots__/tools-list.test.js.snap @@ -29,94 +29,89 @@ exports[`tools/list > returns the full tool catalog 1`] = ` "name": "project_info", }, { - "description": "List tests (/api/v2/{project_id}/tests). Filter tests with \`tql\` (TQL); call \`tql_help\` for the syntax and full field list. Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", + "description": "Manage tests (/api/v2/{project_id}/tests). Filter tests with \`tql\` (TQL); call \`tql_help\` for the syntax and full field list. CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.", "inputSchema": { "additionalProperties": false, "properties": { - "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", + "assigned_to": { + "description": "(create|update)", "type": "string", }, - "count": { - "description": "Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", - "type": "boolean", - }, - "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", - "items": { - "type": "string", - }, - "type": "array", - }, - "group_by": { - "description": "Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", + "attachment_id": { + "description": "(attachments_delete) ID of the attachment to delete", "type": "string", }, - "page": { - "minimum": 1, - "type": "integer", + "branch": { + "description": "(list|get|create|update|delete) Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", + "type": "string", }, - "per_page": { - "maximum": 100, - "minimum": 1, - "type": "integer", + "code": { + "description": "(create|update)", + "type": "string", }, - "tql": { - "description": "TQL filter for tests. Fields: tag, label, priority, issue, jira, state, status, custom_status, created_at, updated_at, last_run_at, executed_at, created_by, assigned_to, suite, test, shared, milestone. Call \`tql_help\` for syntax. Examples: \`priority == 'high'\`, \`state == 'automated'\`, \`suite % 'Checkout'\`.", + "command": { + "description": "CLI-style operation to perform. list: List tests (tql filter, pagination) | get: Get test by ID | create: Create test (title and suite_id required) | update: Update test by ID | delete: Delete test by ID | share: Share tests into a suite of another project. Select by test_ids, labels, or both. Source project stays the single source of truth; shared copies are read-only until unlinked; re-sharing does not duplicate; processed asynchronously ("queued" = accepted); skipped shared copies are listed in skipped_test_ids; projects must be of the same type (Classic/BDD). | unshare: Remove a test's share, converting the shared copy back into a regular, editable test. Only the shared copy can be targeted — the original source test is untouched. Must be called against the project that holds the shared copy. | issues_list: List linked issues for a test | issues_link: Link issue to a test (url or jira_id) | issues_unlink: Unlink issue from a test | attachments_list: List attachments for a test | attachments_upload: Upload one attachment to a test | attachments_delete: Delete attachment from a test", + "enum": [ + "list", + "get", + "create", + "update", + "delete", + "share", + "unshare", + "issues_list", + "issues_link", + "issues_unlink", + "attachments_list", + "attachments_upload", + "attachments_delete", + ], "type": "string", }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "count": { + "description": "(list) Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", "type": "boolean", }, - }, - "type": "object", - }, - "name": "tests_list", - }, - { - "description": "Get test by ID", - "inputSchema": { - "additionalProperties": false, - "properties": { - "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", + "description": { + "description": "(create|update)", "type": "string", }, - "test_id": { + "emoji": { + "description": "(create|update)", "type": "string", }, - }, - "required": [ - "test_id", - ], - "type": "object", - }, - "name": "tests_get", - }, - { - "description": "Create test (/api/v2/{project_id}/tests)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "assigned_to": { - "type": "string", + "fields": { + "description": "(attachments_list) Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "items": { + "type": "string", + }, + "type": "array", }, - "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", + "file_path": { + "description": "(attachments_upload) Local path to the file that will be sent as multipart/form-data field "files".", "type": "string", }, - "code": { + "group_by": { + "description": "(list) Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", "type": "string", }, - "description": { - "type": "string", + "issue_id": { + "description": "(issues_unlink) ID of the linked issue to remove", + "type": "integer", }, - "emoji": { + "jira_id": { + "description": "(issues_link) Jira issue key to link (alternative to url)", "type": "string", }, + "labels": { + "description": "(share) Label slugs or titles; every test carrying any of these labels is shared, in addition to test_ids.", + "items": { + "type": "string", + }, + "type": "array", + }, "link": { + "description": "(create|update)", "items": { "additionalProperties": false, "properties": { @@ -151,7 +146,19 @@ exports[`tools/list > returns the full tool catalog 1`] = ` }, "type": "array", }, + "page": { + "description": "(list|issues_list)", + "minimum": 1, + "type": "integer", + }, + "per_page": { + "description": "(list|issues_list)", + "maximum": 100, + "minimum": 1, + "type": "integer", + }, "priority": { + "description": "(create|update)", "enum": [ "low", "normal", @@ -161,7 +168,12 @@ exports[`tools/list > returns the full tool catalog 1`] = ` ], "type": "string", }, + "source": { + "description": "(issues_list) Filter issues by source (e.g. jira)", + "type": "string", + }, "state": { + "description": "(create|update)", "enum": [ "manual", "detached", @@ -170,42 +182,169 @@ exports[`tools/list > returns the full tool catalog 1`] = ` "type": "string", }, "suite_id": { + "description": "(create|update) Suite to place the test in", + "type": "string", + }, + "sync": { + "description": "(update)", + "type": "boolean", + }, + "target_project_id": { + "description": "(share) Project ID (slug) of the destination project. Must be accessible to the current user.", + "type": "string", + }, + "target_suite_id": { + "description": "(share) Suite ID in the target project to place the shared tests into. Must be a file-type suite, not a folder.", "type": "string", }, + "test_id": { + "description": "(get|update|delete|unshare|issues_list|issues_link|issues_unlink|attachments_list|attachments_upload|attachments_delete) Test ID (for unshare: ID of the shared test copy to unlink)", + "type": "string", + }, + "test_ids": { + "description": "(share) Test IDs to share. At least one of test_ids/labels is required. Max 1000 tests per request.", + "items": { + "type": "string", + }, + "type": "array", + }, "title": { + "description": "(create|update)", + "type": "string", + }, + "tql": { + "description": "(list) TQL filter for tests. Fields: tag, label, priority, issue, jira, state, status, custom_status, created_at, updated_at, last_run_at, executed_at, created_by, assigned_to, suite, test, shared, milestone. Call \`tql_help\` for syntax. Examples: \`priority == 'high'\`, \`state == 'automated'\`, \`suite % 'Checkout'\`.", + "type": "string", + }, + "type": { + "description": "(issues_unlink) Kind of the linked issue", + "enum": [ + "issue", + "jira_issue", + ], + "type": "string", + }, + "url": { + "description": "(issues_link) Issue URL to link", "type": "string", }, + "verbose": { + "default": false, + "description": "(attachments_list) Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "type": "boolean", + }, }, "required": [ - "title", - "suite_id", + "command", ], "type": "object", }, - "name": "tests_create", + "name": "tests", }, { - "description": "Update test (/api/v2/{project_id}/tests/{id})", + "description": "Manage suites as tree (/api/v2/{project_id}/suites) CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.", "inputSchema": { "additionalProperties": false, "properties": { "assigned_to": { + "description": "(create|update)", + "type": "string", + }, + "attachment_id": { + "description": "(attachments_delete) ID of the attachment to delete", "type": "string", }, "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", + "description": "(list|get|create|update|delete) Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", "type": "string", }, - "code": { + "children": { + "description": "(create|update)", + "items": {}, + "type": "array", + }, + "command": { + "description": "CLI-style operation to perform. list: List suites as tree (filters: file_type, tag, labels, search_text) | get: Get suite by ID | create: Create suite (title required) | update: Update suite by ID | delete: Delete suite by ID | share: Share suites (with their tests) into one or more other projects. Select by suite_ids, labels, or both. File-type suites are linked (read-only copies that stay in sync); folder suites are deep-copied as editable copies; re-sharing a linked suite into a project that already has it does not duplicate; suites that are themselves shared copies link to their original source; omit destination_folder_id to share into the root; processed asynchronously; projects must be of the same type (Classic/BDD). | unshare: Remove a suite's share, converting the shared copy back into a regular, editable suite. Only the shared (linked) copy can be targeted — the original source suite is untouched. Must be called against the project that holds the shared copy. | issues_list: List linked issues for a suite | issues_link: Link issue to a suite (url or jira_id) | issues_unlink: Unlink issue from a suite | attachments_list: List attachments for a suite | attachments_upload: Upload one attachment to a suite | attachments_delete: Delete attachment from a suite", + "enum": [ + "list", + "get", + "create", + "update", + "delete", + "share", + "unshare", + "issues_list", + "issues_link", + "issues_unlink", + "attachments_list", + "attachments_upload", + "attachments_delete", + ], "type": "string", }, + "count": { + "description": "(list) Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", + "type": "boolean", + }, "description": { + "description": "(create|update)", + "type": "string", + }, + "destination_folder_id": { + "description": "(share) Folder suite ID in the target project to place the shared suites into. Only allowed when sharing to a single target project.", "type": "string", }, "emoji": { + "description": "(create|update)", + "type": "string", + }, + "fields": { + "description": "(attachments_list) Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "items": { + "type": "string", + }, + "type": "array", + }, + "file": { + "description": "(create|update)", + "type": "string", + }, + "file_path": { + "description": "(attachments_upload) Local path to the file that will be sent as multipart/form-data field "files".", + "type": "string", + }, + "file_type": { + "description": "(list|create|update) list: filter by type; create/update: the type of suite", + "enum": [ + "file", + "folder", + ], + "type": "string", + }, + "group_by": { + "description": "(list) Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", "type": "string", }, + "issue_id": { + "description": "(issues_unlink) ID of the linked issue to remove", + "type": "integer", + }, + "jira_id": { + "description": "(issues_link) Jira issue key to link (alternative to url)", + "type": "string", + }, + "labels": { + "description": "(list|share) list: filter by label slugs/titles; share: every suite carrying any of these labels is shared, in addition to suite_ids.", + "items": { + "type": "string", + }, + "type": [ + "string", + "array", + ], + }, "link": { + "description": "(create|update)", "items": { "additionalProperties": false, "properties": { @@ -224,6 +363,7 @@ exports[`tools/list > returns the full tool catalog 1`] = ` "milestone", "issue", "jira", + "requirement", ], "type": "string", }, @@ -240,262 +380,165 @@ exports[`tools/list > returns the full tool catalog 1`] = ` }, "type": "array", }, - "priority": { - "enum": [ - "low", - "normal", - "important", - "high", - "critical", - ], + "page": { + "description": "(list|issues_list)", + "minimum": 1, + "type": "integer", + }, + "parent_id": { + "description": "(create|update) Parent suite ID", "type": "string", }, - "state": { - "enum": [ - "manual", - "detached", - "automated", - ], + "per_page": { + "description": "(list|issues_list)", + "maximum": 100, + "minimum": 1, + "type": "integer", + }, + "search_text": { + "description": "(list)", + "type": "string", + }, + "source": { + "description": "(issues_list) Filter issues by source (e.g. jira)", "type": "string", }, "suite_id": { + "description": "(get|update|delete|unshare|issues_list|issues_link|issues_unlink|attachments_list|attachments_upload|attachments_delete) Suite ID (for unshare: ID of the shared suite copy to unlink)", "type": "string", }, - "sync": { - "type": "boolean", + "suite_ids": { + "description": "(share) Suite IDs to share. At least one of suite_ids/labels is required. Max 200 suites per request.", + "items": { + "type": "string", + }, + "type": "array", }, - "test_id": { + "tag": { + "description": "(list) Filter by tag title", "type": "string", }, + "target_project_ids": { + "description": "(share) Project IDs (slugs) of the destination projects. Must be accessible to the current user.", + "items": { + "type": "string", + }, + "type": "array", + }, "title": { + "description": "(create|update)", "type": "string", }, - }, - "required": [ - "test_id", - ], - "type": "object", - }, - "name": "tests_update", - }, - { - "description": "Delete test", - "inputSchema": { - "additionalProperties": false, - "properties": { - "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", + "type": { + "description": "(issues_unlink) Kind of the linked issue", + "enum": [ + "issue", + "jira_issue", + ], "type": "string", }, - "test_id": { + "url": { + "description": "(issues_link) Issue URL to link", "type": "string", }, + "verbose": { + "default": false, + "description": "(attachments_list) Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "type": "boolean", + }, }, "required": [ - "test_id", + "command", ], "type": "object", }, - "name": "tests_delete", + "name": "suites", }, { - "description": "List linked issues for a test (/api/v2/{project_id}/issues?test_id=...) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", + "description": "Manage runs (/api/v2/{project_id}/runs). Filter runs with \`tql\` (TQL); runs also accept boolean flags. Call \`tql_help\` for the syntax and full field list. CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.", "inputSchema": { "additionalProperties": false, "properties": { - "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", - "items": { - "type": "string", - }, - "type": "array", + "assign_strategy": { + "description": "(create|update)", + "enum": [ + "test", + "random", + "none", + ], + "type": "string", }, - "page": { - "minimum": 1, - "type": "integer", - }, - "per_page": { - "maximum": 100, - "minimum": 1, - "type": "integer", - }, - "source": { - "type": "string", - }, - "test_id": { - "type": "string", - }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "required": [ - "test_id", - ], - "type": "object", - }, - "name": "tests_issues_list", - }, - { - "description": "Link issue to a test (/api/v2/{project_id}/issues)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "jira_id": { + "assigned_to": { + "description": "(create|update)", "type": "string", }, - "test_id": { + "branch": { + "description": "(list|get|create|update|delete) Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", "type": "string", }, - "url": { + "command": { + "description": "CLI-style operation to perform. list: List runs (tql filter, pagination) | get: Get run by ID | create: Create run (title required) | update: Update run by ID | delete: Delete run by ID | issues_list: List linked issues for a run | issues_link: Link issue to a run (url or jira_id) | issues_unlink: Unlink issue from a run", + "enum": [ + "list", + "get", + "create", + "update", + "delete", + "issues_list", + "issues_link", + "issues_unlink", + ], "type": "string", }, - }, - "required": [ - "test_id", - ], - "type": "object", - }, - "name": "tests_issues_link", - }, - { - "description": "Unlink issue from a test (/api/v2/{project_id}/issues/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "issue_id": { - "type": "integer", + "count": { + "description": "(list) Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", + "type": "boolean", }, - "type": { - "enum": [ - "issue", - "jira_issue", - ], + "description": { + "description": "(create|update)", "type": "string", }, - }, - "required": [ - "issue_id", - "type", - ], - "type": "object", - }, - "name": "tests_issues_unlink", - }, - { - "description": "List suites as tree (/api/v2/{project_id}/suites) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", + "env": { + "description": "(create|update)", "type": "string", }, - "count": { - "description": "Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", - "type": "boolean", + "envs": { + "description": "(create)", + "items": { + "type": "string", + }, + "type": "array", }, "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "description": "(issues_list) Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", "items": { "type": "string", }, "type": "array", }, - "file_type": { - "enum": [ - "file", - "folder", - ], - "type": "string", - }, "group_by": { - "description": "Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", - "type": "string", - }, - "labels": { + "description": "(list) Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", "type": "string", }, - "page": { - "minimum": 1, - "type": "integer", - }, - "per_page": { - "maximum": 100, - "minimum": 1, + "issue_id": { + "description": "(issues_unlink) ID of the linked issue to remove", "type": "integer", }, - "search_text": { - "type": "string", - }, - "tag": { - "type": "string", - }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "type": "object", - }, - "name": "suites_list", - }, - { - "description": "Get suite by ID", - "inputSchema": { - "additionalProperties": false, - "properties": { - "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", - "type": "string", - }, - "suite_id": { - "type": "string", - }, - }, - "required": [ - "suite_id", - ], - "type": "object", - }, - "name": "suites_get", - }, - { - "description": "Create suite (/api/v2/{project_id}/suites)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "assigned_to": { - "type": "string", - }, - "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", - "type": "string", - }, - "children": { - "items": {}, - "type": "array", - }, - "description": { - "type": "string", - }, - "emoji": { - "type": "string", - }, - "file": { + "jira_id": { + "description": "(issues_link) Jira issue key to link (alternative to url)", "type": "string", }, - "file_type": { + "kind": { + "description": "(create|update)", "enum": [ - "file", - "folder", + "manual", + "automated", + "mixed", ], "type": "string", }, "link": { + "description": "(create|update)", "items": { "additionalProperties": false, "properties": { @@ -514,7 +557,6 @@ exports[`tools/list > returns the full tool catalog 1`] = ` "milestone", "issue", "jira", - "requirement", ], "type": "string", }, @@ -531,2360 +573,1011 @@ exports[`tools/list > returns the full tool catalog 1`] = ` }, "type": "array", }, - "parent_id": { - "type": "string", - }, - "title": { - "type": "string", - }, - }, - "required": [ - "title", - ], - "type": "object", - }, - "name": "suites_create", - }, - { - "description": "Update suite (/api/v2/{project_id}/suites/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "assigned_to": { - "type": "string", + "page": { + "description": "(list|issues_list)", + "minimum": 1, + "type": "integer", }, - "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", - "type": "string", + "per_page": { + "description": "(list|issues_list)", + "maximum": 100, + "minimum": 1, + "type": "integer", }, - "children": { - "items": {}, + "plan_ids": { + "description": "(create)", + "items": { + "type": "string", + }, "type": "array", }, - "description": { + "run_id": { + "description": "(get|update|delete|issues_list|issues_link)", "type": "string", }, - "emoji": { + "rungroup_id": { + "description": "(create|update)", "type": "string", }, - "file": { + "source": { + "description": "(issues_list) Filter issues by source (e.g. jira)", "type": "string", }, - "file_type": { + "status_event": { + "description": "(update)", "enum": [ - "file", - "folder", + "finish", + "finish_manual", + "launch", + "rerun", + "scheduled", + "terminate", ], "type": "string", }, - "link": { + "suite_ids": { + "description": "(create|update)", "items": { - "additionalProperties": false, - "properties": { - "action": { - "enum": [ - "add", - "remove", - ], - "type": "string", - }, - "type": { - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira", - "requirement", - ], - "type": "string", - }, - "value": { - "type": "string", - }, - }, - "required": [ - "action", - "type", - "value", - ], - "type": "object", + "type": "string", }, "type": "array", }, - "parent_id": { + "test_ids": { + "description": "(create|update)", + "items": { + "type": "string", + }, + "type": "array", + }, + "title": { + "description": "(create|update)", "type": "string", }, - "suite_id": { + "tql": { + "description": "(list) TQL filter for runs. Fields: title, plan, rungroup, env, tag, label, jira, duration, passed_count, failed_count, skipped_count, automated, manual, mixed, finished, unfinished, passed, failed, terminated, published, private, archived, unarchived, with_defect, has_defect, has_test, has_test_tag, has_test_label, has_suite, has_message, has_custom_status, has_assigned_to, has_retries, has_test_duration, has_priority, created_at, updated_at, launched_at, finished_at, milestone. Call \`tql_help\` for syntax. Examples: \`finished and with_defect\`, \`env in ['Windows', 'Linux']\`, \`has_retries > 2\`.", "type": "string", }, - "title": { + "type": { + "description": "(issues_unlink) Kind of the linked issue", + "enum": [ + "issue", + "jira_issue", + ], + "type": "string", + }, + "url": { + "description": "(issues_link) Issue URL to link", "type": "string", }, + "verbose": { + "default": false, + "description": "(issues_list) Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "type": "boolean", + }, }, "required": [ - "suite_id", + "command", ], "type": "object", }, - "name": "suites_update", + "name": "runs", }, { - "description": "Delete suite", + "description": "Manage individual test runs (/api/v2/{project_id}/testruns) CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.", "inputSchema": { "additionalProperties": false, "properties": { - "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", + "assigned_to": { + "description": "(create|update)", "type": "string", }, - "suite_id": { + "attachment_id": { + "description": "(attachments_delete) ID of the attachment to delete", "type": "string", }, - }, - "required": [ - "suite_id", - ], - "type": "object", - }, - "name": "suites_delete", - }, - { - "description": "List linked issues for a suite (/api/v2/{project_id}/issues?suite_id=...) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { + "automated": { + "description": "(create|update)", + "type": "boolean", + }, + "command": { + "description": "CLI-style operation to perform. list: List testruns (rich filters: filter_status, filter_kind, tags, labels, envs, rungroups, defects, ...) | get: Get testrun by ID | create: Create testrun in a run (run_id required) | update: Update testrun by ID | delete: Delete testrun by ID | issues_list: List linked issues for a testrun | issues_link: Link issue to a testrun (url or jira_id) | issues_unlink: Unlink issue from a testrun | attachments_list: List attachments for a testrun | attachments_upload: Upload one attachment to a testrun | attachments_delete: Delete attachment from a testrun", + "enum": [ + "list", + "get", + "create", + "update", + "delete", + "issues_list", + "issues_link", + "issues_unlink", + "attachments_list", + "attachments_upload", + "attachments_delete", + ], + "type": "string", + }, + "count": { + "description": "(list) Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", + "type": "boolean", + }, + "defects": { + "description": "(list)", + "enum": [ + "has_defects", + "without_defects", + ], + "type": "string", + }, + "envs": { + "description": "(list)", + "items": { + "type": "string", + }, + "type": [ + "array", + "string", + ], + }, "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "description": "(attachments_list) Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", "items": { "type": "string", }, "type": "array", }, - "page": { - "minimum": 1, - "type": "integer", - }, - "per_page": { - "maximum": 100, - "minimum": 1, - "type": "integer", + "file_path": { + "description": "(attachments_upload) Local path to the file that will be sent as multipart/form-data field "files".", + "type": "string", }, - "source": { + "filter_finished_at_date_range": { + "description": "(list)", "type": "string", }, - "suite_id": { + "filter_kind": { + "description": "(list)", + "enum": [ + "manual", + "automated", + ], "type": "string", }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "filter_link": { + "description": "(list)", "type": "boolean", }, - }, - "required": [ - "suite_id", - ], - "type": "object", - }, - "name": "suites_issues_list", - }, - { - "description": "Link issue to a suite (/api/v2/{project_id}/issues)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "jira_id": { + "filter_message": { + "description": "(list)", + "type": "boolean", + }, + "filter_priority": { + "description": "(list)", + "enum": [ + "low", + "normal", + "important", + "high", + "critical", + ], "type": "string", }, - "suite_id": { + "filter_search": { + "description": "(list)", "type": "string", }, - "url": { + "filter_status": { + "description": "(list)", + "enum": [ + "passed", + "failed", + "skipped", + "pending", + ], + "type": "string", + }, + "filter_substatus": { + "description": "(list)", + "type": "string", + }, + "filter_user": { + "description": "(list)", + "type": [ + "integer", + "string", + ], + }, + "group_by": { + "description": "(list) Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", "type": "string", }, - }, - "required": [ - "suite_id", - ], - "type": "object", - }, - "name": "suites_issues_link", - }, - { - "description": "Unlink issue from a suite (/api/v2/{project_id}/issues/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { "issue_id": { + "description": "(issues_unlink) ID of the linked issue to remove", "type": "integer", }, - "type": { - "enum": [ - "issue", - "jira_issue", - ], + "jira_id": { + "description": "(issues_link) Jira issue key to link (alternative to url)", "type": "string", }, - }, - "required": [ - "issue_id", - "type", - ], - "type": "object", - }, - "name": "suites_issues_unlink", - }, - { - "description": "Share tests into a suite of another project (/api/v2/{project_id}/shares/tests). Select tests by test_ids, labels, or both (combined). The source project stays the single source of truth; shared copies in the target project are read-only until unlinked. Re-sharing into the same target project does not duplicate. Requests are processed asynchronously: status 'queued' means accepted, not completed. Matched tests that are themselves shared copies are skipped and listed in skipped_test_ids. Source and target projects must be of the same type (Classic/BDD).", - "inputSchema": { - "additionalProperties": false, - "properties": { "labels": { - "description": "Label slugs or titles; every test carrying any of these labels is shared, in addition to test_ids.", + "description": "(list)", "items": { "type": "string", }, - "type": "array", + "type": [ + "array", + "string", + ], }, - "target_project_id": { - "description": "Project ID (slug) of the destination project. Must be accessible to the current user.", + "message": { + "description": "(create|update)", "type": "string", }, - "target_suite_id": { - "description": "Suite ID in the target project to place the shared tests into. Must be a file-type suite, not a folder.", - "type": "string", + "page": { + "description": "(list|issues_list)", + "minimum": 1, + "type": "integer", }, - "test_ids": { - "description": "Test IDs to share. At least one of test_ids/labels is required. Max 1000 tests per request.", - "items": { - "type": "string", - }, - "type": "array", + "per_page": { + "description": "(list|issues_list)", + "maximum": 100, + "minimum": 1, + "type": "integer", }, - }, - "required": [ - "target_project_id", - "target_suite_id", - ], - "type": "object", - }, - "name": "tests_share", - }, - { - "description": "Share suites (with their tests) into one or more other projects (/api/v2/{project_id}/shares/suites). Select suites by suite_ids, labels, or both (combined). File-type suites are linked (read-only copies that stay in sync); folder suites are deep-copied as regular editable copies. Re-sharing a linked suite into a project that already has it does not duplicate. Suites that are themselves shared copies link to their original source. Omit destination_folder_id to share into the root of the target project(s). Requests are processed asynchronously: status 'queued' means accepted, not completed. Source and target projects must be of the same type (Classic/BDD).", - "inputSchema": { - "additionalProperties": false, - "properties": { - "destination_folder_id": { - "description": "Folder suite ID in the target project to place the shared suites into. Only allowed when sharing to a single target project.", + "run_id": { + "description": "(list|create|update) list: filter by run; create/update: the run the testrun belongs to", "type": "string", }, - "labels": { - "description": "Label slugs or titles; every suite carrying any of these labels is shared, in addition to suite_ids.", - "items": { - "type": "string", - }, - "type": "array", + "run_time": { + "description": "(create|update)", + "type": "number", }, - "suite_ids": { - "description": "Suite IDs to share. At least one of suite_ids/labels is required. Max 200 suites per request.", + "rungroups": { + "description": "(list)", "items": { "type": "string", }, - "type": "array", + "type": [ + "array", + "string", + ], }, - "target_project_ids": { - "description": "Project IDs (slugs) of the destination projects. Must be accessible to the current user.", + "source": { + "description": "(issues_list) Filter issues by source (e.g. jira)", + "type": "string", + }, + "status": { + "description": "(create|update)", + "enum": [ + "passed", + "failed", + "skipped", + "pending", + ], + "type": "string", + }, + "tags": { + "description": "(list)", "items": { "type": "string", }, - "type": "array", + "type": [ + "array", + "string", + ], }, - }, - "required": [ - "target_project_ids", - ], - "type": "object", - }, - "name": "suites_share", - }, - { - "description": "Remove a test's share, converting the shared copy back into a regular, editable test (/api/v2/{project_id}/shares/tests/{id}). Only the shared copy can be targeted — the original source test is untouched. Must be called against the project that holds the shared copy.", - "inputSchema": { - "additionalProperties": false, - "properties": { "test_id": { - "description": "ID of the shared test copy to unlink.", - "type": "string", - }, - }, - "required": [ - "test_id", - ], - "type": "object", - }, - "name": "tests_unshare", - }, - { - "description": "Remove a suite's share, converting the shared copy back into a regular, editable suite (/api/v2/{project_id}/shares/suites/{id}). Only the shared (linked) copy can be targeted — the original source suite is untouched. Must be called against the project that holds the shared copy.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "suite_id": { - "description": "ID of the shared suite copy to unlink.", - "type": "string", - }, - }, - "required": [ - "suite_id", - ], - "type": "object", - }, - "name": "suites_unshare", - }, - { - "description": "List runs (/api/v2/{project_id}/runs). Filter runs with \`tql\` (TQL); runs also accept boolean flags. Call \`tql_help\` for the syntax and full field list. Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", + "description": "(create|update)", "type": "string", }, - "count": { - "description": "Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", - "type": "boolean", - }, - "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "test_ids": { + "description": "(list)", "items": { "type": "string", }, - "type": "array", + "type": [ + "array", + "string", + ], }, - "group_by": { - "description": "Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", + "test_title": { + "description": "(create|update)", "type": "string", }, - "page": { - "minimum": 1, - "type": "integer", - }, - "per_page": { - "maximum": 100, - "minimum": 1, + "testrun_id": { + "description": "(get|update|delete|issues_list|issues_link|attachments_list|attachments_upload|attachments_delete)", "type": "integer", }, - "tql": { - "description": "TQL filter for runs. Fields: title, plan, rungroup, env, tag, label, jira, duration, passed_count, failed_count, skipped_count, automated, manual, mixed, finished, unfinished, passed, failed, terminated, published, private, archived, unarchived, with_defect, has_defect, has_test, has_test_tag, has_test_label, has_suite, has_message, has_custom_status, has_assigned_to, has_retries, has_test_duration, has_priority, created_at, updated_at, launched_at, finished_at, milestone. Call \`tql_help\` for syntax. Examples: \`finished and with_defect\`, \`env in ['Windows', 'Linux']\`, \`has_retries > 2\`.", - "type": "string", - }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "type": "object", - }, - "name": "runs_list", - }, - { - "description": "Get run by ID", - "inputSchema": { - "additionalProperties": false, - "properties": { - "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", - "type": "string", - }, - "run_id": { - "type": "string", - }, - }, - "required": [ - "run_id", - ], - "type": "object", - }, - "name": "runs_get", - }, - { - "description": "Create run (/api/v2/{project_id}/runs)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "assign_strategy": { - "enum": [ - "test", - "random", - "none", - ], - "type": "string", - }, - "assigned_to": { - "type": "string", - }, - "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", - "type": "string", - }, - "description": { - "type": "string", - }, - "env": { - "type": "string", - }, - "envs": { - "items": { - "type": "string", - }, - "type": "array", - }, - "kind": { - "enum": [ - "manual", - "automated", - "mixed", - ], - "type": "string", - }, - "link": { - "items": { - "additionalProperties": false, - "properties": { - "action": { - "enum": [ - "add", - "remove", - ], - "type": "string", - }, - "type": { - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira", - ], - "type": "string", - }, - "value": { - "type": "string", - }, - }, - "required": [ - "action", - "type", - "value", - ], - "type": "object", - }, - "type": "array", - }, - "plan_ids": { - "items": { - "type": "string", - }, - "type": "array", - }, - "rungroup_id": { - "type": "string", - }, - "suite_ids": { - "items": { - "type": "string", - }, - "type": "array", - }, - "test_ids": { - "items": { - "type": "string", - }, - "type": "array", - }, - "title": { - "type": "string", - }, - }, - "required": [ - "title", - ], - "type": "object", - }, - "name": "runs_create", - }, - { - "description": "Update run (/api/v2/{project_id}/runs/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "assign_strategy": { - "enum": [ - "test", - "random", - "none", - ], - "type": "string", - }, - "assigned_to": { - "type": "string", - }, - "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", - "type": "string", - }, - "description": { - "type": "string", - }, - "env": { - "type": "string", - }, - "kind": { - "enum": [ - "manual", - "automated", - "mixed", - ], - "type": "string", - }, - "link": { - "items": { - "additionalProperties": false, - "properties": { - "action": { - "enum": [ - "add", - "remove", - ], - "type": "string", - }, - "type": { - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira", - ], - "type": "string", - }, - "value": { - "type": "string", - }, - }, - "required": [ - "action", - "type", - "value", - ], - "type": "object", - }, - "type": "array", - }, - "run_id": { - "type": "string", - }, - "rungroup_id": { - "type": "string", - }, - "status_event": { - "enum": [ - "finish", - "finish_manual", - "launch", - "rerun", - "scheduled", - "terminate", - ], - "type": "string", - }, - "suite_ids": { - "items": { - "type": "string", - }, - "type": "array", - }, - "test_ids": { - "items": { - "type": "string", - }, - "type": "array", - }, - "title": { - "type": "string", - }, - }, - "required": [ - "run_id", - ], - "type": "object", - }, - "name": "runs_update", - }, - { - "description": "Delete run (/api/v2/{project_id}/runs/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "branch": { - "description": "Branch slug to scope the request to (omit or main for the main branch). For tests and suites, a branch-local record is used when it exists, falling back to main; updating/deleting a main-only record forks an isolated copy into the branch. Runs are tagged with the branch (only reachable with the same branch afterwards). Requires the branches feature.", - "type": "string", - }, - "run_id": { - "type": "string", - }, - }, - "required": [ - "run_id", - ], - "type": "object", - }, - "name": "runs_delete", - }, - { - "description": "List linked issues for a run (/api/v2/{project_id}/issues?run_id=...) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", - "items": { - "type": "string", - }, - "type": "array", - }, - "page": { - "minimum": 1, - "type": "integer", - }, - "per_page": { - "maximum": 100, - "minimum": 1, - "type": "integer", - }, - "run_id": { - "type": "string", - }, - "source": { - "type": "string", - }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "required": [ - "run_id", - ], - "type": "object", - }, - "name": "runs_issues_list", - }, - { - "description": "Link issue to a run (/api/v2/{project_id}/issues)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "jira_id": { - "type": "string", - }, - "run_id": { - "type": "string", - }, - "url": { - "type": "string", - }, - }, - "required": [ - "run_id", - ], - "type": "object", - }, - "name": "runs_issues_link", - }, - { - "description": "Unlink issue from a run (/api/v2/{project_id}/issues/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "issue_id": { - "type": "integer", - }, - "type": { - "enum": [ - "issue", - "jira_issue", - ], - "type": "string", - }, - }, - "required": [ - "issue_id", - "type", - ], - "type": "object", - }, - "name": "runs_issues_unlink", - }, - { - "description": "List testruns (/api/v2/{project_id}/testruns) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "count": { - "description": "Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", - "type": "boolean", - }, - "defects": { - "enum": [ - "has_defects", - "without_defects", - ], - "type": "string", - }, - "envs": { - "items": { - "type": "string", - }, - "type": [ - "array", - "string", - ], - }, - "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", - "items": { - "type": "string", - }, - "type": "array", - }, - "filter_finished_at_date_range": { - "type": "string", - }, - "filter_kind": { - "enum": [ - "manual", - "automated", - ], - "type": "string", - }, - "filter_link": { - "type": "boolean", - }, - "filter_message": { - "type": "boolean", - }, - "filter_priority": { - "enum": [ - "low", - "normal", - "important", - "high", - "critical", - ], - "type": "string", - }, - "filter_search": { - "type": "string", - }, - "filter_status": { - "enum": [ - "passed", - "failed", - "skipped", - "pending", - ], - "type": "string", - }, - "filter_substatus": { - "type": "string", - }, - "filter_user": { - "type": [ - "integer", - "string", - ], - }, - "group_by": { - "description": "Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", - "type": "string", - }, - "labels": { - "items": { - "type": "string", - }, - "type": [ - "array", - "string", - ], - }, - "page": { - "minimum": 1, - "type": "integer", - }, - "per_page": { - "maximum": 100, - "minimum": 1, - "type": "integer", - }, - "run_id": { - "type": "string", - }, - "rungroups": { - "items": { - "type": "string", - }, - "type": [ - "array", - "string", - ], - }, - "tags": { - "items": { - "type": "string", - }, - "type": [ - "array", - "string", - ], - }, - "test_ids": { - "items": { - "type": "string", - }, - "type": [ - "array", - "string", - ], - }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "type": "object", - }, - "name": "testruns_list", - }, - { - "description": "Get testrun by ID", - "inputSchema": { - "additionalProperties": false, - "properties": { - "testrun_id": { - "type": "integer", - }, - }, - "required": [ - "testrun_id", - ], - "type": "object", - }, - "name": "testruns_get", - }, - { - "description": "Create testrun (/api/v2/{project_id}/testruns)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "assigned_to": { - "type": "string", - }, - "automated": { - "type": "boolean", - }, - "message": { - "type": "string", - }, - "run_id": { - "type": "string", - }, - "run_time": { - "type": "number", - }, - "status": { - "enum": [ - "passed", - "failed", - "skipped", - "pending", - ], - "type": "string", - }, - "test_id": { - "type": "string", - }, - "test_title": { - "type": "string", - }, - }, - "required": [ - "run_id", - ], - "type": "object", - }, - "name": "testruns_create", - }, - { - "description": "Update testrun (/api/v2/{project_id}/testruns/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "assigned_to": { - "type": "string", - }, - "automated": { - "type": "boolean", - }, - "message": { - "type": "string", - }, - "run_id": { - "type": "string", - }, - "run_time": { - "type": "number", - }, - "status": { - "enum": [ - "passed", - "failed", - "skipped", - "pending", - ], - "type": "string", - }, - "test_id": { - "type": "string", - }, - "test_title": { - "type": "string", - }, - "testrun_id": { - "type": "integer", - }, - }, - "required": [ - "testrun_id", - ], - "type": "object", - }, - "name": "testruns_update", - }, - { - "description": "Delete testrun (/api/v2/{project_id}/testruns/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "testrun_id": { - "type": "integer", - }, - }, - "required": [ - "testrun_id", - ], - "type": "object", - }, - "name": "testruns_delete", - }, - { - "description": "List linked issues for a testrun (/api/v2/{project_id}/issues?testrun_id=...) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", - "items": { - "type": "string", - }, - "type": "array", - }, - "page": { - "minimum": 1, - "type": "integer", - }, - "per_page": { - "maximum": 100, - "minimum": 1, - "type": "integer", - }, - "source": { - "type": "string", - }, - "testrun_id": { - "type": "integer", - }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "required": [ - "testrun_id", - ], - "type": "object", - }, - "name": "testruns_issues_list", - }, - { - "description": "Link issue to a testrun (/api/v2/{project_id}/issues)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "jira_id": { - "type": "string", - }, - "testrun_id": { - "type": "integer", - }, - "url": { - "type": "string", - }, - }, - "required": [ - "testrun_id", - ], - "type": "object", - }, - "name": "testruns_issues_link", - }, - { - "description": "Unlink issue from a testrun (/api/v2/{project_id}/issues/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "issue_id": { - "type": "integer", - }, - "type": { - "enum": [ - "issue", - "jira_issue", - ], - "type": "string", - }, - }, - "required": [ - "issue_id", - "type", - ], - "type": "object", - }, - "name": "testruns_issues_unlink", - }, - { - "description": "List run groups as tree (/api/v2/{project_id}/rungroups) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "count": { - "description": "Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", - "type": "boolean", - }, - "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", - "items": { - "type": "string", - }, - "type": "array", - }, - "group_by": { - "description": "Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", - "type": "string", - }, - "page": { - "minimum": 1, - "type": "integer", - }, - "per_page": { - "maximum": 100, - "minimum": 1, - "type": "integer", - }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "type": "object", - }, - "name": "rungroups_list", - }, - { - "description": "Get run group by ID", - "inputSchema": { - "additionalProperties": false, - "properties": { - "rungroup_id": { - "type": "string", - }, - }, - "required": [ - "rungroup_id", - ], - "type": "object", - }, - "name": "rungroups_get", - }, - { - "description": "Create run group (/api/v2/{project_id}/rungroups)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "children": { - "items": {}, - "type": "array", - }, - "description": { - "type": "string", - }, - "emoji": { - "type": "string", - }, - "kind": { - "type": "string", - }, - "parent_id": { - "type": "string", - }, - "pin": { - "type": "boolean", - }, - "status": { - "type": "string", - }, - "title": { - "type": "string", - }, - }, - "required": [ - "title", - ], - "type": "object", - }, - "name": "rungroups_create", - }, - { - "description": "Update run group (/api/v2/{project_id}/rungroups/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "children": { - "items": {}, - "type": "array", - }, - "description": { - "type": "string", - }, - "emoji": { - "type": "string", - }, - "kind": { - "type": "string", - }, - "parent_id": { - "type": "string", - }, - "pin": { - "type": "boolean", - }, - "rungroup_id": { - "type": "string", - }, - "status": { - "type": "string", - }, - "title": { - "type": "string", - }, - }, - "required": [ - "rungroup_id", - ], - "type": "object", - }, - "name": "rungroups_update", - }, - { - "description": "Delete run group (/api/v2/{project_id}/rungroups/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "rungroup_id": { - "type": "string", - }, - }, - "required": [ - "rungroup_id", - ], - "type": "object", - }, - "name": "rungroups_delete", - }, - { - "description": "List steps (/api/v2/{project_id}/steps) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "count": { - "description": "Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", - "type": "boolean", - }, - "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", - "items": { - "type": "string", - }, - "type": "array", - }, - "group_by": { - "description": "Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", - "type": "string", - }, - "page": { - "minimum": 1, - "type": "integer", - }, - "per_page": { - "maximum": 100, - "minimum": 1, - "type": "integer", - }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "type": "object", - }, - "name": "steps_list", - }, - { - "description": "Get step by ID", - "inputSchema": { - "additionalProperties": false, - "properties": { - "step_id": { - "type": "integer", - }, - }, - "required": [ - "step_id", - ], - "type": "object", - }, - "name": "steps_get", - }, - { - "description": "Create step (/api/v2/{project_id}/steps)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "description": { - "type": "string", - }, - "link": { - "items": { - "additionalProperties": false, - "properties": { - "action": { - "enum": [ - "add", - "remove", - ], - "type": "string", - }, - "type": { - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira", - ], - "type": "string", - }, - "value": { - "type": "string", - }, - }, - "required": [ - "action", - "type", - "value", - ], - "type": "object", - }, - "type": "array", - }, - "title": { - "type": "string", - }, - }, - "required": [ - "title", - ], - "type": "object", - }, - "name": "steps_create", - }, - { - "description": "Update step (/api/v2/{project_id}/steps/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "description": { - "type": "string", - }, - "link": { - "items": { - "additionalProperties": false, - "properties": { - "action": { - "enum": [ - "add", - "remove", - ], - "type": "string", - }, - "type": { - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira", - ], - "type": "string", - }, - "value": { - "type": "string", - }, - }, - "required": [ - "action", - "type", - "value", - ], - "type": "object", - }, - "type": "array", - }, - "step_id": { - "type": "integer", - }, - "title": { - "type": "string", - }, - }, - "required": [ - "step_id", - ], - "type": "object", - }, - "name": "steps_update", - }, - { - "description": "Delete step (/api/v2/{project_id}/steps/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "step_id": { - "type": "integer", - }, - }, - "required": [ - "step_id", - ], - "type": "object", - }, - "name": "steps_delete", - }, - { - "description": "List snippets (/api/v2/{project_id}/snippets) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "count": { - "description": "Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", - "type": "boolean", - }, - "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", - "items": { - "type": "string", - }, - "type": "array", - }, - "group_by": { - "description": "Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", - "type": "string", - }, - "page": { - "minimum": 1, - "type": "integer", - }, - "per_page": { - "maximum": 100, - "minimum": 1, - "type": "integer", - }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "type": "object", - }, - "name": "snippets_list", - }, - { - "description": "Get snippet by ID", - "inputSchema": { - "additionalProperties": false, - "properties": { - "snippet_id": { - "type": "integer", - }, - }, - "required": [ - "snippet_id", - ], - "type": "object", - }, - "name": "snippets_get", - }, - { - "description": "Create snippet (/api/v2/{project_id}/snippets)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "description": { - "type": "string", - }, - "link": { - "items": { - "additionalProperties": false, - "properties": { - "action": { - "enum": [ - "add", - "remove", - ], - "type": "string", - }, - "type": { - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira", - ], - "type": "string", - }, - "value": { - "type": "string", - }, - }, - "required": [ - "action", - "type", - "value", - ], - "type": "object", - }, - "type": "array", - }, - "title": { - "type": "string", - }, - }, - "required": [ - "title", - ], - "type": "object", - }, - "name": "snippets_create", - }, - { - "description": "Update snippet (/api/v2/{project_id}/snippets/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "description": { - "type": "string", - }, - "link": { - "items": { - "additionalProperties": false, - "properties": { - "action": { - "enum": [ - "add", - "remove", - ], - "type": "string", - }, - "type": { - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira", - ], - "type": "string", - }, - "value": { - "type": "string", - }, - }, - "required": [ - "action", - "type", - "value", - ], - "type": "object", - }, - "type": "array", - }, - "snippet_id": { - "type": "integer", - }, - "title": { - "type": "string", - }, - }, - "required": [ - "snippet_id", - ], - "type": "object", - }, - "name": "snippets_update", - }, - { - "description": "Delete snippet (/api/v2/{project_id}/snippets/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "snippet_id": { - "type": "integer", - }, - }, - "required": [ - "snippet_id", - ], - "type": "object", - }, - "name": "snippets_delete", - }, - { - "description": "List labels (/api/v2/{project_id}/labels) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "count": { - "description": "Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", - "type": "boolean", - }, - "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", - "items": { - "type": "string", - }, - "type": "array", - }, - "group_by": { - "description": "Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", - "type": "string", - }, - "page": { - "minimum": 1, - "type": "integer", - }, - "per_page": { - "maximum": 100, - "minimum": 1, - "type": "integer", - }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "type": "object", - }, - "name": "labels_list", - }, - { - "description": "Get label by slug", - "inputSchema": { - "additionalProperties": false, - "properties": { - "label_id": { - "type": "string", - }, - }, - "required": [ - "label_id", - ], - "type": "object", - }, - "name": "labels_get", - }, - { - "description": "Create label (/api/v2/{project_id}/labels)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "color": { - "type": "string", - }, - "field": { - "type": "object", - }, - "scope": { - "items": { - "enum": [ - "tests", - "suites", - "runs", - "plans", - "steps", - "templates", - ], - "type": "string", - }, - "type": "array", - }, - "title": { - "type": "string", - }, - "visibility": { - "items": { - "enum": [ - "filter", - "list", - ], - "type": "string", - }, - "type": "array", - }, - }, - "required": [ - "title", - ], - "type": "object", - }, - "name": "labels_create", - }, - { - "description": "Update label (/api/v2/{project_id}/labels/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "color": { - "type": "string", - }, - "field": { - "type": "object", - }, - "label_id": { - "type": "string", - }, - "scope": { - "items": { - "enum": [ - "tests", - "suites", - "runs", - "plans", - "steps", - "templates", - ], - "type": "string", - }, - "type": "array", - }, - "title": { - "type": "string", - }, - "visibility": { - "items": { - "enum": [ - "filter", - "list", - ], - "type": "string", - }, - "type": "array", - }, - }, - "required": [ - "label_id", - ], - "type": "object", - }, - "name": "labels_update", - }, - { - "description": "Delete label (/api/v2/{project_id}/labels/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "label_id": { - "type": "string", - }, - }, - "required": [ - "label_id", - ], - "type": "object", - }, - "name": "labels_delete", - }, - { - "description": "List tags with counts (/api/v2/{project_id}/tags) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "count": { - "description": "Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", - "type": "boolean", - }, - "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", - "items": { - "type": "string", - }, - "type": "array", - }, - "group_by": { - "description": "Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", - "type": "string", - }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "type": "object", - }, - "name": "tags_list", - }, - { - "description": "Get tests by tag title (/api/v2/{project_id}/tags/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "tag_id": { - "type": "string", - }, - }, - "required": [ - "tag_id", - ], - "type": "object", - }, - "name": "tags_get", - }, - { - "description": "Search by tag title (delegates to tags_get)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "query": { - "type": "string", - }, - "tag_id": { - "type": "string", - }, - }, - "type": "object", - }, - "name": "tags_search", - }, - { - "description": "List milestones (/api/v2/{project_id}/milestones) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "count": { - "description": "Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", - "type": "boolean", - }, - "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", - "items": { - "type": "string", - }, - "type": "array", - }, - "group_by": { - "description": "Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", - "type": "string", - }, - "page": { - "minimum": 1, - "type": "integer", - }, - "per_page": { - "maximum": 100, - "minimum": 1, - "type": "integer", - }, - "status": { + "type": { + "description": "(issues_unlink) Kind of the linked issue", "enum": [ - "created", - "active", - "closed", + "issue", + "jira_issue", ], "type": "string", }, - "type": { - "description": "Filter by milestone type (title), e.g. Sprint or Release.", + "url": { + "description": "(issues_link) Issue URL to link", "type": "string", }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "type": "object", - }, - "name": "milestones_list", - }, - { - "description": "Get milestone by ID", - "inputSchema": { - "additionalProperties": false, - "properties": { - "milestone_id": { - "type": "string", + "verbose": { + "default": false, + "description": "(attachments_list) Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "type": "boolean", }, }, "required": [ - "milestone_id", + "command", ], "type": "object", }, - "name": "milestones_get", + "name": "testruns", }, { - "description": "List linked issues (/api/v2/{project_id}/issues) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", + "description": "Manage run groups as tree (/api/v2/{project_id}/rungroups) CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.", "inputSchema": { "additionalProperties": false, "properties": { + "children": { + "description": "(create|update)", + "items": {}, + "type": "array", + }, + "command": { + "description": "CLI-style operation to perform. list: List run groups as tree | get: Get run group by ID | create: Create run group (title required) | update: Update run group by ID | delete: Delete run group by ID", + "enum": [ + "list", + "get", + "create", + "update", + "delete", + ], + "type": "string", + }, "count": { - "description": "Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", + "description": "(list) Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", "type": "boolean", }, + "description": { + "description": "(create|update)", + "type": "string", + }, + "emoji": { + "description": "(create|update)", + "type": "string", + }, "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "description": "(list) Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", "items": { "type": "string", }, "type": "array", }, "group_by": { - "description": "Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", + "description": "(list) Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", + "type": "string", + }, + "kind": { + "description": "(create|update)", "type": "string", }, "page": { + "description": "(list)", "minimum": 1, "type": "integer", }, + "parent_id": { + "description": "(create|update)", + "type": "string", + }, "per_page": { + "description": "(list)", "maximum": 100, "minimum": 1, "type": "integer", }, - "plan_id": { - "type": "string", - }, - "run_id": { - "type": "string", + "pin": { + "description": "(create|update)", + "type": "boolean", }, - "source": { + "rungroup_id": { + "description": "(get|update|delete)", "type": "string", }, - "suite_id": { + "status": { + "description": "(create|update)", "type": "string", }, - "test_id": { + "title": { + "description": "(create|update)", "type": "string", }, - "testrun_id": { - "type": "integer", - }, "verbose": { "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "description": "(list) Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", "type": "boolean", }, }, + "required": [ + "command", + ], "type": "object", }, - "name": "issues_list", + "name": "rungroups", }, { - "description": "Link issue to resource (/api/v2/{project_id}/issues)", + "description": "Manage test steps (/api/v2/{project_id}/steps) CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.", "inputSchema": { "additionalProperties": false, "properties": { - "jira_id": { + "command": { + "description": "CLI-style operation to perform. list: List steps | get: Get step by ID | create: Create step (title required) | update: Update step by ID | delete: Delete step by ID", + "enum": [ + "list", + "get", + "create", + "update", + "delete", + ], "type": "string", }, - "plan_id": { - "type": "string", + "count": { + "description": "(list) Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", + "type": "boolean", }, - "run_id": { + "description": { + "description": "(create|update)", "type": "string", }, - "suite_id": { - "type": "string", + "fields": { + "description": "(list) Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "items": { + "type": "string", + }, + "type": "array", }, - "test_id": { + "group_by": { + "description": "(list) Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", "type": "string", }, - "testrun_id": { + "link": { + "description": "(create|update)", + "items": { + "additionalProperties": false, + "properties": { + "action": { + "enum": [ + "add", + "remove", + ], + "type": "string", + }, + "type": { + "enum": [ + "label", + "custom_field", + "tag", + "milestone", + "issue", + "jira", + ], + "type": "string", + }, + "value": { + "type": "string", + }, + }, + "required": [ + "action", + "type", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "page": { + "description": "(list)", + "minimum": 1, "type": "integer", }, - "url": { - "type": "string", + "per_page": { + "description": "(list)", + "maximum": 100, + "minimum": 1, + "type": "integer", }, - }, - "type": "object", - }, - "name": "issues_create", - }, - { - "description": "Unlink issue (/api/v2/{project_id}/issues/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "issue_id": { + "step_id": { + "description": "(get|update|delete)", "type": "integer", }, - "type": { - "enum": [ - "issue", - "jira_issue", - ], + "title": { + "description": "(create|update)", "type": "string", }, + "verbose": { + "default": false, + "description": "(list) Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "type": "boolean", + }, }, "required": [ - "issue_id", - "type", + "command", ], "type": "object", }, - "name": "issues_delete", + "name": "steps", }, { - "description": "List attachments for a test (/api/v2/{project_id}/attachments?test_id=...) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", + "description": "Manage code snippets (/api/v2/{project_id}/snippets) CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.", "inputSchema": { "additionalProperties": false, "properties": { + "command": { + "description": "CLI-style operation to perform. list: List snippets | get: Get snippet by ID | create: Create snippet (title required) | update: Update snippet by ID | delete: Delete snippet by ID", + "enum": [ + "list", + "get", + "create", + "update", + "delete", + ], + "type": "string", + }, + "count": { + "description": "(list) Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", + "type": "boolean", + }, + "description": { + "description": "(create|update)", + "type": "string", + }, "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "description": "(list) Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", "items": { "type": "string", }, "type": "array", }, - "test_id": { + "group_by": { + "description": "(list) Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", + "type": "string", + }, + "link": { + "description": "(create|update)", + "items": { + "additionalProperties": false, + "properties": { + "action": { + "enum": [ + "add", + "remove", + ], + "type": "string", + }, + "type": { + "enum": [ + "label", + "custom_field", + "tag", + "milestone", + "issue", + "jira", + ], + "type": "string", + }, + "value": { + "type": "string", + }, + }, + "required": [ + "action", + "type", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "page": { + "description": "(list)", + "minimum": 1, + "type": "integer", + }, + "per_page": { + "description": "(list)", + "maximum": 100, + "minimum": 1, + "type": "integer", + }, + "snippet_id": { + "description": "(get|update|delete)", + "type": "integer", + }, + "title": { + "description": "(create|update)", "type": "string", }, "verbose": { "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "description": "(list) Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", "type": "boolean", }, }, "required": [ - "test_id", + "command", ], "type": "object", }, - "name": "tests_attachments_list", + "name": "snippets", }, { - "description": "Upload one attachment to a test (/api/v2/{project_id}/attachments?test_id=...)", + "description": "Manage labels (/api/v2/{project_id}/labels) CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.", "inputSchema": { "additionalProperties": false, "properties": { - "file_path": { - "description": "Local path to the file that will be sent as multipart/form-data field "file".", + "color": { + "description": "(create|update)", "type": "string", }, - "test_id": { + "command": { + "description": "CLI-style operation to perform. list: List labels | get: Get label by slug | create: Create label (title required) | update: Update label by slug | delete: Delete label by slug", + "enum": [ + "list", + "get", + "create", + "update", + "delete", + ], "type": "string", }, - }, - "required": [ - "test_id", - "file_path", - ], - "type": "object", - }, - "name": "tests_attachments_upload", - }, - { - "description": "Delete attachment from a test (/api/v2/{project_id}/attachments/{id}?test_id=...)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "attachment_id": { - "type": "string", + "count": { + "description": "(list) Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", + "type": "boolean", }, - "test_id": { - "type": "string", + "field": { + "description": "(create|update)", + "type": "object", }, - }, - "required": [ - "test_id", - "attachment_id", - ], - "type": "object", - }, - "name": "tests_attachments_delete", - }, - { - "description": "List attachments for a suite (/api/v2/{project_id}/attachments?suite_id=...) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "description": "(list) Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", "items": { "type": "string", }, "type": "array", }, - "suite_id": { + "group_by": { + "description": "(list) Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", "type": "string", }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "required": [ - "suite_id", - ], - "type": "object", - }, - "name": "suites_attachments_list", - }, - { - "description": "Upload one attachment to a suite (/api/v2/{project_id}/attachments?suite_id=...)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "file_path": { - "description": "Local path to the file that will be sent as multipart/form-data field "file".", + "label_id": { + "description": "(get|update|delete) Label slug", "type": "string", }, - "suite_id": { + "page": { + "description": "(list)", + "minimum": 1, + "type": "integer", + }, + "per_page": { + "description": "(list)", + "maximum": 100, + "minimum": 1, + "type": "integer", + }, + "scope": { + "description": "(create|update)", + "items": { + "enum": [ + "tests", + "suites", + "runs", + "plans", + "steps", + "templates", + ], + "type": "string", + }, + "type": "array", + }, + "title": { + "description": "(create|update)", "type": "string", }, + "verbose": { + "default": false, + "description": "(list) Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "type": "boolean", + }, + "visibility": { + "description": "(create|update)", + "items": { + "enum": [ + "filter", + "list", + ], + "type": "string", + }, + "type": "array", + }, }, "required": [ - "suite_id", - "file_path", + "command", ], "type": "object", }, - "name": "suites_attachments_upload", + "name": "labels", }, { - "description": "Delete attachment from a suite (/api/v2/{project_id}/attachments/{id}?suite_id=...)", + "description": "Tags: list with counts and get tests by tag (/api/v2/{project_id}/tags) CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.", "inputSchema": { "additionalProperties": false, "properties": { - "attachment_id": { + "command": { + "description": "CLI-style operation to perform. list: List tags with counts | get: Get tests by tag title (tag_id) | search: Search by tag title (delegates to get)", + "enum": [ + "list", + "get", + "search", + ], "type": "string", }, - "suite_id": { - "type": "string", + "count": { + "description": "(list) Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", + "type": "boolean", }, - }, - "required": [ - "suite_id", - "attachment_id", - ], - "type": "object", - }, - "name": "suites_attachments_delete", - }, - { - "description": "List attachments for a testrun (/api/v2/{project_id}/attachments?testrun_id=...) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "description": "(list) Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", "items": { "type": "string", }, "type": "array", }, - "testrun_id": { - "type": "integer", + "group_by": { + "description": "(list) Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", + "type": "string", + }, + "query": { + "description": "(search)", + "type": "string", + }, + "tag_id": { + "description": "(get|search) Tag title to look up", + "type": "string", }, "verbose": { "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "description": "(list) Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", "type": "boolean", }, }, "required": [ - "testrun_id", + "command", ], "type": "object", }, - "name": "testruns_attachments_list", + "name": "tags", }, { - "description": "Upload one attachment to a testrun (/api/v2/{project_id}/attachments?testrun_id=...)", + "description": "Milestones: list and get (/api/v2/{project_id}/milestones) CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.", "inputSchema": { "additionalProperties": false, "properties": { - "file_path": { - "description": "Local path to the file that will be sent as multipart/form-data field "file".", + "command": { + "description": "CLI-style operation to perform. list: List milestones (type, status filters) | get: Get milestone by ID", + "enum": [ + "list", + "get", + ], "type": "string", }, - "testrun_id": { - "type": "integer", + "count": { + "description": "(list) Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", + "type": "boolean", }, - }, - "required": [ - "testrun_id", - "file_path", - ], - "type": "object", - }, - "name": "testruns_attachments_upload", - }, - { - "description": "Delete attachment from a testrun (/api/v2/{project_id}/attachments/{id}?testrun_id=...)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "attachment_id": { + "fields": { + "description": "(list) Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "items": { + "type": "string", + }, + "type": "array", + }, + "group_by": { + "description": "(list) Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", "type": "string", }, - "testrun_id": { + "milestone_id": { + "description": "(get)", + "type": "string", + }, + "page": { + "description": "(list)", + "minimum": 1, + "type": "integer", + }, + "per_page": { + "description": "(list)", + "maximum": 100, + "minimum": 1, "type": "integer", }, + "status": { + "description": "(list)", + "enum": [ + "created", + "active", + "closed", + ], + "type": "string", + }, + "type": { + "description": "(list) Filter by milestone type (title), e.g. Sprint or Release.", + "type": "string", + }, + "verbose": { + "default": false, + "description": "(list) Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "type": "boolean", + }, }, "required": [ - "testrun_id", - "attachment_id", + "command", ], "type": "object", }, - "name": "testruns_attachments_delete", + "name": "milestones", }, { - "description": "List plans (/api/v2/{project_id}/plans) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", + "description": "Linked issues across resources (/api/v2/{project_id}/issues) CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.", "inputSchema": { "additionalProperties": false, "properties": { + "command": { + "description": "CLI-style operation to perform. list: List linked issues (scope by test_id/suite_id/run_id/testrun_id/plan_id, filter by source) | create: Link issue to a resource (url or jira_id + one scope id) | delete: Unlink issue", + "enum": [ + "list", + "create", + "delete", + ], + "type": "string", + }, "count": { - "description": "Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", + "description": "(list) Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", "type": "boolean", }, "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "description": "(list) Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", "items": { "type": "string", }, "type": "array", }, "group_by": { - "description": "Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", + "description": "(list) Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", "type": "string", }, - "hidden": { - "type": "boolean", + "issue_id": { + "description": "(delete) ID of the linked issue to remove", + "type": "integer", }, - "kind": { - "enum": [ - "manual", - "automated", - "mixed", - ], + "jira_id": { + "description": "(create) Jira issue key to link (alternative to url)", "type": "string", }, - "labels": { - "items": { - "type": "string", - }, - "type": "array", - }, "page": { + "description": "(list)", "minimum": 1, "type": "integer", }, "per_page": { + "description": "(list)", "maximum": 100, "minimum": 1, "type": "integer", }, - "search_text": { + "plan_id": { + "description": "(list|create)", + "type": "string", + }, + "run_id": { + "description": "(list|create)", + "type": "string", + }, + "source": { + "description": "(list) Filter issues by source (e.g. jira)", + "type": "string", + }, + "suite_id": { + "description": "(list|create)", + "type": "string", + }, + "test_id": { + "description": "(list|create)", + "type": "string", + }, + "testrun_id": { + "description": "(list|create)", + "type": "integer", + }, + "type": { + "description": "(delete) Kind of the linked issue", + "enum": [ + "issue", + "jira_issue", + ], + "type": "string", + }, + "url": { + "description": "(create) Issue URL to link", "type": "string", }, "verbose": { "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "description": "(list) Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", "type": "boolean", }, }, - "type": "object", - }, - "name": "plans_list", - }, - { - "description": "Get plan by ID", - "inputSchema": { - "additionalProperties": false, - "properties": { - "plan_id": { - "type": "string", - }, - }, "required": [ - "plan_id", + "command", ], "type": "object", }, - "name": "plans_get", + "name": "issues", }, { - "description": "Create plan (/api/v2/{project_id}/plans). Select tests for the plan with \`tql\` (TQL); the API resolves matching tests. Call \`tql_help\` for the syntax and full field list.", + "description": "Manage test plans (/api/v2/{project_id}/plans). Select tests for the plan with \`tql\` (TQL); the API resolves matching tests. Call \`tql_help\` for the syntax and full field list. CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.", "inputSchema": { "additionalProperties": false, "properties": { "as_manual": { + "description": "(create|update)", "type": "boolean", }, - "description": { - "type": "string", - }, - "hidden": { - "type": "boolean", - }, - "kind": { + "command": { + "description": "CLI-style operation to perform. list: List plans (kind, hidden, labels, search_text filters) | get: Get plan by ID | create: Create plan (title required; select tests via test_ids/suite_ids/tql) | update: Update plan by ID | delete: Delete plan by ID | issues_list: List linked issues for a plan | issues_link: Link issue to a plan (url or jira_id) | issues_unlink: Unlink issue from a plan", "enum": [ - "manual", - "automated", - "mixed", + "list", + "get", + "create", + "update", + "delete", + "issues_list", + "issues_link", + "issues_unlink", ], "type": "string", }, - "link": { - "items": { - "additionalProperties": false, - "properties": { - "action": { - "enum": [ - "add", - "remove", - ], - "type": "string", - }, - "type": { - "enum": [ - "label", - "custom_field", - "tag", - "milestone", - "issue", - "jira", - ], - "type": "string", - }, - "value": { - "type": "string", - }, - }, - "required": [ - "action", - "type", - "value", - ], - "type": "object", - }, - "type": "array", + "count": { + "description": "(list) Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", + "type": "boolean", }, - "suite_ids": { - "description": "List of suite IDs (8-char) to include in the plan. If omitted, all suites are considered.", - "items": { - "type": "string", - }, - "type": "array", + "description": { + "description": "(create|update)", + "type": "string", }, - "test_ids": { - "description": "List of test IDs (8-char) to include in the plan. If omitted, all tests matching the plan kind are included.", + "fields": { + "description": "(issues_list) Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", "items": { "type": "string", }, "type": "array", }, - "title": { - "type": "string", - }, - "tql": { - "description": "TQL to select tests for the plan. Fields: tag, label, priority, issue, jira, state, status, custom_status, created_at, updated_at, last_run_at, executed_at, created_by, assigned_to, suite, test, shared, milestone. Call \`tql_help\` for syntax. Examples: \`priority == 'high'\`, \`tag in ['smoke', 'stage1']\`.", + "group_by": { + "description": "(list) Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", "type": "string", }, - }, - "required": [ - "title", - ], - "type": "object", - }, - "name": "plans_create", - }, - { - "description": "Update plan (/api/v2/{project_id}/plans/{id}). Select tests for the plan with \`tql\` (TQL); the API resolves matching tests. Call \`tql_help\` for the syntax and full field list.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "as_manual": { + "hidden": { + "description": "(list|create|update) list: include hidden plans; create/update: set hidden flag", "type": "boolean", }, - "description": { - "type": "string", + "issue_id": { + "description": "(issues_unlink) ID of the linked issue to remove", + "type": "integer", }, - "hidden": { - "type": "boolean", + "jira_id": { + "description": "(issues_link) Jira issue key to link (alternative to url)", + "type": "string", }, "kind": { + "description": "(list|create|update) list: filter by kind; create/update: the plan kind", "enum": [ "manual", "automated", @@ -2892,7 +1585,15 @@ exports[`tools/list > returns the full tool catalog 1`] = ` ], "type": "string", }, + "labels": { + "description": "(list)", + "items": { + "type": "string", + }, + "type": "array", + }, "link": { + "description": "(create|update)", "items": { "additionalProperties": false, "properties": { @@ -2927,170 +1628,151 @@ exports[`tools/list > returns the full tool catalog 1`] = ` }, "type": "array", }, - "plan_id": { + "page": { + "description": "(list|issues_list)", + "minimum": 1, + "type": "integer", + }, + "per_page": { + "description": "(list|issues_list)", + "maximum": 100, + "minimum": 1, + "type": "integer", + }, + "plan_id": { + "description": "(get|update|delete|issues_list|issues_link)", + "type": "string", + }, + "search_text": { + "description": "(list)", + "type": "string", + }, + "source": { + "description": "(issues_list) Filter issues by source (e.g. jira)", "type": "string", }, "suite_ids": { - "description": "List of suite IDs (8-char) to include in the plan. If omitted, all suites are considered.", + "description": "(create|update) List of suite IDs (8-char) to include in the plan. If omitted, all suites are considered.", "items": { "type": "string", }, "type": "array", }, "test_ids": { - "description": "List of test IDs (8-char) to include in the plan. If omitted, all tests matching the plan kind are included.", + "description": "(create|update) List of test IDs (8-char) to include in the plan. If omitted, all tests matching the plan kind are included.", "items": { "type": "string", }, "type": "array", }, "title": { + "description": "(create|update)", "type": "string", }, "tql": { - "description": "TQL to select tests for the plan. Fields: tag, label, priority, issue, jira, state, status, custom_status, created_at, updated_at, last_run_at, executed_at, created_by, assigned_to, suite, test, shared, milestone. Call \`tql_help\` for syntax. Examples: \`priority == 'high'\`, \`tag in ['smoke', 'stage1']\`.", - "type": "string", - }, - }, - "required": [ - "plan_id", - ], - "type": "object", - }, - "name": "plans_update", - }, - { - "description": "Delete plan", - "inputSchema": { - "additionalProperties": false, - "properties": { - "plan_id": { + "description": "(create|update) TQL to select tests for the plan. Fields: tag, label, priority, issue, jira, state, status, custom_status, created_at, updated_at, last_run_at, executed_at, created_by, assigned_to, suite, test, shared, milestone. Call \`tql_help\` for syntax. Examples: \`priority == 'high'\`, \`tag in ['smoke', 'stage1']\`.", "type": "string", }, - }, - "required": [ - "plan_id", - ], - "type": "object", - }, - "name": "plans_delete", - }, - { - "description": "List linked issues for a plan (/api/v2/{project_id}/issues?plan_id=...) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", - "items": { - "type": "string", - }, - "type": "array", - }, - "page": { - "minimum": 1, - "type": "integer", - }, - "per_page": { - "maximum": 100, - "minimum": 1, - "type": "integer", - }, - "plan_id": { + "type": { + "description": "(issues_unlink) Kind of the linked issue", + "enum": [ + "issue", + "jira_issue", + ], "type": "string", }, - "source": { + "url": { + "description": "(issues_link) Issue URL to link", "type": "string", }, "verbose": { "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "description": "(issues_list) Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", "type": "boolean", }, }, "required": [ - "plan_id", + "command", ], "type": "object", }, - "name": "plans_issues_list", + "name": "plans", }, { - "description": "Link issue to a plan (/api/v2/{project_id}/issues)", + "description": "Manage requirements (/api/v2/{project_id}/requirements) CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.", "inputSchema": { "additionalProperties": false, "properties": { - "jira_id": { - "type": "string", + "active": { + "description": "(create|update)", + "type": "boolean", }, - "plan_id": { + "command": { + "description": "CLI-style operation to perform. list: List requirements (source, scope filters) | get: Get requirement by ID | create: Create requirement (title and source_type required) | update: Update requirement by ID | delete: Delete requirement by ID", + "enum": [ + "list", + "get", + "create", + "update", + "delete", + ], "type": "string", }, - "url": { + "confluence_url": { + "description": "(create) Required for confluence requirements.", "type": "string", }, - }, - "required": [ - "plan_id", - ], - "type": "object", - }, - "name": "plans_issues_link", - }, - { - "description": "Unlink issue from a plan (/api/v2/{project_id}/issues/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "issue_id": { - "type": "integer", + "count": { + "description": "(list) Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", + "type": "boolean", }, - "type": { - "enum": [ - "issue", - "jira_issue", - ], + "description": { + "description": "(create|update) Required for text requirements (min 500 chars on create); only applied for text requirements on update.", "type": "string", }, - }, - "required": [ - "issue_id", - "type", - ], - "type": "object", - }, - "name": "plans_issues_unlink", - }, - { - "description": "List requirements (/api/v2/{project_id}/requirements) Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "count": { - "description": "Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", - "type": "boolean", + "details": { + "description": "(create|update)", + "type": "string", }, "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "description": "(list) Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "items": { + "type": "string", + }, + "type": "array", + }, + "files": { + "description": "(create|update) Local file paths to upload for file requirements.", "items": { "type": "string", }, "type": "array", }, + "global": { + "description": "(create|update)", + "type": "boolean", + }, "group_by": { - "description": "Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", + "description": "(list) Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", "type": "string", }, "page": { + "description": "(list)", "minimum": 1, "type": "integer", }, "per_page": { + "description": "(list)", "maximum": 100, "minimum": 1, "type": "integer", }, + "requirement_id": { + "description": "(get|update|delete)", + "type": "string", + }, "scope": { + "description": "(list)", "enum": [ "global", "attached", @@ -3100,6 +1782,7 @@ exports[`tools/list > returns the full tool catalog 1`] = ` "type": "string", }, "source": { + "description": "(list)", "enum": [ "jira", "confluence", @@ -3108,62 +1791,8 @@ exports[`tools/list > returns the full tool catalog 1`] = ` ], "type": "string", }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "type": "object", - }, - "name": "requirements_list", - }, - { - "description": "Get requirement by ID", - "inputSchema": { - "additionalProperties": false, - "properties": { - "requirement_id": { - "type": "string", - }, - }, - "required": [ - "requirement_id", - ], - "type": "object", - }, - "name": "requirements_get", - }, - { - "description": "Create requirement (/api/v2/{project_id}/requirements)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "active": { - "type": "boolean", - }, - "confluence_url": { - "description": "Required for confluence requirements.", - "type": "string", - }, - "description": { - "description": "Required for text requirements. Must be at least 500 characters.", - "type": "string", - }, - "details": { - "type": "string", - }, - "files": { - "description": "Local file paths to upload for file requirements.", - "items": { - "type": "string", - }, - "type": "array", - }, - "global": { - "type": "boolean", - }, "source_type": { + "description": "(create)", "enum": [ "jira", "confluence", @@ -3173,90 +1802,55 @@ exports[`tools/list > returns the full tool catalog 1`] = ` "type": "string", }, "title": { + "description": "(create|update)", "type": "string", }, - }, - "required": [ - "title", - "source_type", - ], - "type": "object", - }, - "name": "requirements_create", - }, - { - "description": "Update requirement (/api/v2/{project_id}/requirements/{id})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "active": { - "type": "boolean", - }, - "description": { - "description": "Only applied for text requirements.", - "type": "string", - }, - "details": { - "type": "string", - }, - "files": { - "description": "Local file paths to upload for file requirements.", - "items": { - "type": "string", - }, - "type": "array", - }, - "global": { + "verbose": { + "default": false, + "description": "(list) Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", "type": "boolean", }, - "requirement_id": { - "type": "string", - }, - "title": { - "type": "string", - }, }, "required": [ - "requirement_id", + "command", ], "type": "object", }, - "name": "requirements_update", + "name": "requirements", }, { - "description": "Delete requirement (/api/v2/{project_id}/requirements/{id})", + "description": "Manage project branches (/api/v2/{project_id}/branches). Requires the branches feature (enterprise plan). CLI-style: pass "command" plus the params it needs; each param description lists the commands it applies to.", "inputSchema": { "additionalProperties": false, "properties": { - "requirement_id": { + "branch_id": { + "description": "(get|update|delete) Branch slug", + "type": "string", + }, + "command": { + "description": "CLI-style operation to perform. list: List project branches (filter_state / filter_title to narrow down) | get: Get branch by slug | create: Create branch (title required; slug is generated from it) | update: Update branch title | delete: Delete branch by slug", + "enum": [ + "list", + "get", + "create", + "update", + "delete", + ], "type": "string", }, - }, - "required": [ - "requirement_id", - ], - "type": "object", - }, - "name": "requirements_delete", - }, - { - "description": "List project branches (/api/v2/{project_id}/branches). Requires the branches feature (enterprise plan). Use filter[state] / filter[title] to narrow down. Entity-specific heavy fields are stripped by default; verbose:true for full bodies.", - "inputSchema": { - "additionalProperties": false, - "properties": { "count": { - "description": "Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", + "description": "(list) Return only metadata with total counts instead of the entity list. Pair with group_by for an aggregated breakdown.", "type": "boolean", }, "fields": { - "description": "Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", + "description": "(list) Fields to keep per item (e.g. ["id","title","status","message"]). Ignored when verbose is true.", "items": { "type": "string", }, "type": "array", }, "filter_state": { - "description": "Filter by branch state", + "description": "(list) Filter by branch state", "enum": [ "active", "merged", @@ -3264,102 +1858,40 @@ exports[`tools/list > returns the full tool catalog 1`] = ` "type": "string", }, "filter_title": { - "description": "Filter by title (partial substring match)", + "description": "(list) Filter by title (partial substring match)", "type": "string", }, "group_by": { - "description": "Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", + "description": "(list) Aggregate counts by this field, e.g. status, state, priority, created_by (use with count=true). The backend validates supported fields per resource. For created_by, counts are keyed by user email, not ID.", "type": "string", }, "page": { + "description": "(list)", "minimum": 1, "type": "integer", }, "per_page": { + "description": "(list)", "maximum": 100, "minimum": 1, "type": "integer", }, - "verbose": { - "default": false, - "description": "Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", - "type": "boolean", - }, - }, - "type": "object", - }, - "name": "branches_list", - }, - { - "description": "Get branch by slug", - "inputSchema": { - "additionalProperties": false, - "properties": { - "branch_id": { - "description": "Branch slug", - "type": "string", - }, - }, - "required": [ - "branch_id", - ], - "type": "object", - }, - "name": "branches_get", - }, - { - "description": "Create branch (/api/v2/{project_id}/branches)", - "inputSchema": { - "additionalProperties": false, - "properties": { - "title": { - "description": "Branch title; a slug is generated from it", - "type": "string", - }, - }, - "required": [ - "title", - ], - "type": "object", - }, - "name": "branches_create", - }, - { - "description": "Update branch title (/api/v2/{project_id}/branches/{slug})", - "inputSchema": { - "additionalProperties": false, - "properties": { - "branch_id": { - "description": "Branch slug", - "type": "string", - }, "title": { + "description": "(create|update) Branch title; a slug is generated from it", "type": "string", }, - }, - "required": [ - "branch_id", - ], - "type": "object", - }, - "name": "branches_update", - }, - { - "description": "Delete branch by slug", - "inputSchema": { - "additionalProperties": false, - "properties": { - "branch_id": { - "description": "Branch slug", - "type": "string", + "verbose": { + "default": false, + "description": "(list) Return full response bodies. Default strips entity-specific heavy fields and null values; set true when you need those.", + "type": "boolean", }, }, "required": [ - "branch_id", + "command", ], "type": "object", }, - "name": "branches_delete", + "name": "branches", }, ], } diff --git a/test/command-dispatch.test.js b/test/command-dispatch.test.js new file mode 100644 index 0000000..969d1ed --- /dev/null +++ b/test/command-dispatch.test.js @@ -0,0 +1,150 @@ +import { describe, expect, it, vi } from 'vitest'; +import { ToolRegistry } from '../src/mcp/tool-registry.js'; +import { TOOL_DEFINITIONS } from '../src/mcp/tool-definitions.js'; +import { ENTITY_COMMANDS } from '../src/mcp/entity-commands.js'; +import { DEFAULT_TOOL_RESPONSE } from '../src/config/constants.js'; + +const silentLogger = { + error() {}, + warn() {}, + info() {}, + debug() {}, +}; + +function createRegistry(apiClientOverrides = {}) { + const apiClient = { + list: vi.fn().mockResolvedValue({ data: [], meta: { total: 0, page: 1, per_page: 10 } }), + get: vi.fn().mockResolvedValue({ data: { id: 'x' } }), + create: vi.fn().mockResolvedValue({ data: { id: 'new' } }), + update: vi.fn().mockResolvedValue({ data: { id: 'x' } }), + delete: vi.fn().mockResolvedValue({ data: { id: 'x' } }), + ...apiClientOverrides, + }; + + const registry = new ToolRegistry({ + config: { projectId: 'dispatch-project', baseUrl: 'https://app.testomat.io' }, + apiClient, + logger: silentLogger, + }); + + return { registry, apiClient }; +} + +async function resultOf(registry, name, args) { + const response = await registry.execute(name, args); + return JSON.parse(response.content[0].text); +} + +describe('command dispatch', () => { + it('routes a list command to the entity list op', async () => { + const { registry, apiClient } = createRegistry(); + + await resultOf(registry, 'runs', { command: 'list', tql: 'priority:high' }); + + expect(apiClient.list).toHaveBeenCalled(); + const [resource, query] = apiClient.list.mock.calls[0]; + expect(resource).toBe('runs'); + expect(query.tql).toBe('priority:high'); + expect(query.command).toBeUndefined(); + }); + + it('routes a get command with the entity id', async () => { + const { registry, apiClient } = createRegistry(); + + await resultOf(registry, 'tests', { command: 'get', test_id: 'be779025' }); + + expect(apiClient.get).toHaveBeenCalledWith('tests', 'be779025', {}); + }); + + it('routes a scoped issues command', async () => { + const apiClientOverrides = { + createWithQuery: vi.fn().mockResolvedValue({ data: { id: 'issue1' } }), + }; + const { registry, apiClient } = createRegistry(apiClientOverrides); + + await resultOf(registry, 'tests', { command: 'issues_link', test_id: 'be779025', jira_id: 'PROJ-1' }); + + expect(apiClient.createWithQuery).toHaveBeenCalledWith('issues', { + query: { test_id: 'be779025' }, + body: { url: undefined, jira_id: 'PROJ-1' }, + }); + }); + + it('does not leak the command argument into payloads', async () => { + const { registry, apiClient } = createRegistry(); + + await resultOf(registry, 'labels', { command: 'create', title: 'smoke' }); + + const [resource, body] = apiClient.create.mock.calls[0]; + expect(resource).toBe('labels'); + expect(body).toEqual({ title: 'smoke' }); + }); + + it('rejects an unknown command with the valid command list', async () => { + const { registry, apiClient } = createRegistry(); + + const result = await resultOf(registry, 'tests', { command: 'explode' }); + + expect(result.error).toContain('Unknown command "explode"'); + expect(result.error).toContain(ENTITY_COMMANDS.tests.join(', ')); + expect(apiClient.list).not.toHaveBeenCalled(); + expect(apiClient.create).not.toHaveBeenCalled(); + }); + + it('rejects a missing command with the valid command list', async () => { + const { registry } = createRegistry(); + + const result = await resultOf(registry, 'tests', { test_id: 'be779025' }); + + expect(result.error).toContain('Unknown command (missing)'); + expect(result.error).toContain('Valid commands:'); + }); + + it('routes the tags search command (regression: unregistered op)', async () => { + const { registry, apiClient } = createRegistry(); + + await resultOf(registry, 'tags', { command: 'search', query: 'smoke' }); + + expect(apiClient.get).toHaveBeenCalledWith('tags', 'smoke'); + }); + + it('reports an unregistered op as an unknown command, not a TypeError', async () => { + const { registry } = createRegistry(); + + // A command declared in the enum whose op never got registered + const handler = registry.buildCommandHandler('runs', ['list', 'get'], {}); + + await expect(handler({ command: 'list' })).rejects.toThrow('Unknown command "list"'); + }); +}); + +describe('handler wiring', () => { + it('exposes one handler per advertised tool', () => { + const { registry } = createRegistry(); + for (const tool of TOOL_DEFINITIONS) { + expect(registry.handlers[tool.name], `missing handler for ${tool.name}`).toBeDefined(); + } + }); + + it('dispatches singletons directly (no command)', async () => { + const { registry } = createRegistry(); + + const result = await resultOf(registry, 'system_ping', {}); + + expect(result.status).toBe('ok'); + }); + + it('keeps the stub fallback for tools without handlers', async () => { + const customTool = { name: 'custom_future_tool', description: 'not implemented yet', inputSchema: { type: 'object', properties: {} } }; + const { registry } = createRegistry(); + const customRegistry = new ToolRegistry({ + config: { projectId: 'p', baseUrl: 'https://app.testomat.io' }, + apiClient: registry.apiClient, + logger: silentLogger, + tools: [customTool], + }); + + const response = await customRegistry.execute('custom_future_tool', {}); + expect(response.content[0].text).toBe(`${DEFAULT_TOOL_RESPONSE} (custom_future_tool)`); + }); +}); diff --git a/test/shares.test.js b/test/shares.test.js index 13c4f95..70acb60 100644 --- a/test/shares.test.js +++ b/test/shares.test.js @@ -1,7 +1,9 @@ import { describe, expect, it, vi } from 'vitest'; import { ToolRegistry } from '../src/mcp/tool-registry.js'; import { selectTools } from '../src/mcp/tool-profiles.js'; -import { SHARES_TOOLS } from '../src/mcp/definitions/shares.js'; +import { TESTS_TOOL } from '../src/mcp/definitions/tests.js'; +import { SUITES_TOOL } from '../src/mcp/definitions/suites.js'; +import { SYSTEM_TOOLS } from '../src/mcp/definitions/system.js'; const silentLogger = { error() {}, @@ -30,11 +32,12 @@ async function resultOf(registry, name, args) { return JSON.parse(response.content[0].text); } -describe('tests_share', () => { +describe('tests share command', () => { it('shares tests by ids', async () => { const { registry, apiClient } = createRegistry(); - const result = await resultOf(registry, 'tests_share', { + const result = await resultOf(registry, 'tests', { + command: 'share', test_ids: ['be779025', 'sgqat108'], target_project_id: 'sugar-king', target_suite_id: 'e73d559c', @@ -51,7 +54,8 @@ describe('tests_share', () => { it('shares tests by labels and combines with ids', async () => { const { registry, apiClient } = createRegistry(); - await resultOf(registry, 'tests_share', { + await resultOf(registry, 'tests', { + command: 'share', test_ids: ['sgqat104'], labels: ['pre-cert'], target_project_id: 'sugar-king', @@ -69,7 +73,8 @@ describe('tests_share', () => { it('shares tests by labels only', async () => { const { registry, apiClient } = createRegistry(); - await resultOf(registry, 'tests_share', { + await resultOf(registry, 'tests', { + command: 'share', labels: ['pre-cert'], target_project_id: 'sugar-king', target_suite_id: 'e73d559c', @@ -85,7 +90,8 @@ describe('tests_share', () => { it('requires a selection', async () => { const { registry, apiClient } = createRegistry(); - const result = await resultOf(registry, 'tests_share', { + const result = await resultOf(registry, 'tests', { + command: 'share', target_project_id: 'sugar-king', target_suite_id: 'e73d559c', }); @@ -97,7 +103,8 @@ describe('tests_share', () => { it('requires target_suite_id and target_project_id', async () => { const { registry } = createRegistry(); - const result = await resultOf(registry, 'tests_share', { + const result = await resultOf(registry, 'tests', { + command: 'share', test_ids: ['be779025'], target_project_id: 'sugar-king', }); @@ -106,11 +113,12 @@ describe('tests_share', () => { }); }); -describe('suites_share', () => { +describe('suites share command', () => { it('shares suites into multiple target projects', async () => { const { registry, apiClient } = createRegistry(); - const result = await resultOf(registry, 'suites_share', { + const result = await resultOf(registry, 'suites', { + command: 'share', suite_ids: ['e73d559c'], target_project_ids: ['sugar-king', 'game-qa'], }); @@ -125,7 +133,8 @@ describe('suites_share', () => { it('passes destination_folder_id through', async () => { const { registry, apiClient } = createRegistry(); - await resultOf(registry, 'suites_share', { + await resultOf(registry, 'suites', { + command: 'share', labels: ['localisation'], target_project_ids: ['sugar-king'], destination_folder_id: 'folder123', @@ -141,7 +150,8 @@ describe('suites_share', () => { it('requires a selection', async () => { const { registry, apiClient } = createRegistry(); - const result = await resultOf(registry, 'suites_share', { + const result = await resultOf(registry, 'suites', { + command: 'share', target_project_ids: ['sugar-king'], }); @@ -150,11 +160,11 @@ describe('suites_share', () => { }); }); -describe('unshare', () => { +describe('unshare command', () => { it('unshares a test copy', async () => { const { registry, apiClient } = createRegistry(); - await resultOf(registry, 'tests_unshare', { test_id: 'shared1' }); + await resultOf(registry, 'tests', { command: 'unshare', test_id: 'shared1' }); expect(apiClient.delete).toHaveBeenCalledWith('shares/tests', 'shared1'); }); @@ -162,7 +172,7 @@ describe('unshare', () => { it('unshares a suite copy', async () => { const { registry, apiClient } = createRegistry(); - await resultOf(registry, 'suites_unshare', { suite_id: 'shared2' }); + await resultOf(registry, 'suites', { command: 'unshare', suite_id: 'shared2' }); expect(apiClient.delete).toHaveBeenCalledWith('shares/suites', 'shared2'); }); @@ -170,7 +180,7 @@ describe('unshare', () => { it('requires the id', async () => { const { registry, apiClient } = createRegistry(); - const result = await resultOf(registry, 'tests_unshare', {}); + const result = await resultOf(registry, 'tests', { command: 'unshare' }); expect(result.error).toContain('test_id'); expect(apiClient.delete).not.toHaveBeenCalled(); @@ -178,15 +188,31 @@ describe('unshare', () => { }); describe('tool profiles', () => { - it('includes share tools in the core profile but not read', () => { - const names = selectTools(SHARES_TOOLS, 'core').map((tool) => tool.name); - expect(names).toEqual([ - 'tests_share', - 'suites_share', - 'tests_unshare', - 'suites_unshare', + it('keeps the full command surface in the core profile', () => { + const [tool] = selectTools([TESTS_TOOL], 'core'); + expect(tool.name).toBe('tests'); + expect(tool.inputSchema.properties.command.enum).toEqual(TESTS_TOOL.inputSchema.properties.command.enum); + }); + + it('restricts the tests tool to read-only commands in the read profile', () => { + const [tool] = selectTools([TESTS_TOOL], 'read'); + expect(tool.name).toBe('tests'); + expect(tool.inputSchema.properties.command.enum).toEqual([ + 'list', + 'get', + 'issues_list', + 'attachments_list', ]); + expect(tool.inputSchema.properties.title).toBeUndefined(); + expect(tool.inputSchema.properties.test_ids).toBeUndefined(); + expect(tool.inputSchema.properties.test_id).toBeDefined(); + }); + + it('returns the tool unchanged in the full profile', () => { + expect(selectTools([TESTS_TOOL, SUITES_TOOL], 'full')).toEqual([TESTS_TOOL, SUITES_TOOL]); + }); - expect(selectTools(SHARES_TOOLS, 'read')).toEqual([]); + it('passes command-less singletons through the read profile', () => { + expect(selectTools(SYSTEM_TOOLS, 'read')).toEqual(SYSTEM_TOOLS); }); }); diff --git a/test/tools-list.test.js b/test/tools-list.test.js index 62f77dc..fa845aa 100644 --- a/test/tools-list.test.js +++ b/test/tools-list.test.js @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { TestomatioMCPServer } from '../src/mcp/server.js'; import { TOOL_DEFINITIONS } from '../src/mcp/tool-definitions.js'; +import { ENTITY_COMMANDS } from '../src/mcp/entity-commands.js'; const silentLogger = { error() {}, @@ -75,6 +76,18 @@ describe('tools/list', () => { expect(response.error).toBeUndefined(); expect(response.result.tools).toHaveLength(TOOL_DEFINITIONS.length); expect(response.result).toMatchSnapshot(); + + for (const tool of response.result.tools) { + const commands = ENTITY_COMMANDS[tool.name]; + if (!commands) continue; + expect(tool.inputSchema.required, tool.name).toEqual(['command']); + expect(tool.inputSchema.additionalProperties, tool.name).toBe(false); + expect(tool.inputSchema.properties.command.enum, tool.name).toEqual(commands); + for (const [param, schema] of Object.entries(tool.inputSchema.properties)) { + if (param === 'command') continue; + expect(schema.description, `${tool.name}.${param}`).toMatch(/^\([a-z_|]+\)/); + } + } }); it('advertises tool capability on initialize', async () => { diff --git a/worker/test/mcp-endpoint.test.js b/worker/test/mcp-endpoint.test.js index 3a05da1..69622ba 100644 --- a/worker/test/mcp-endpoint.test.js +++ b/worker/test/mcp-endpoint.test.js @@ -63,10 +63,9 @@ describe('POST /mcp/', () => { expect(client.getServerVersion()?.name).toBe('testomatio-mcp-server'); const { tools } = await client.listTools(); - expect(tools.length).toBeGreaterThan(50); - expect(tools.map((tool) => tool.name)).toContain('tests_list'); + expect(tools.map((tool) => tool.name)).toContain('tests'); - const result = await client.callTool({ name: 'tests_list', arguments: { page: 1, per_page: 2 } }); + const result = await client.callTool({ name: 'tests', arguments: { command: 'list', page: 1, per_page: 2 } }); const payload = JSON.parse(result.content[0].text); expect(payload.data.map((test) => test.id)).toEqual(['1', '2']); expect(payload.data.map((test) => test.attributes.title)).toEqual( @@ -94,7 +93,7 @@ describe('POST /mcp/', () => { const client = new Client({ name: 'worker-e2e', version: '1.0.0' }); await client.connect(transport); - await client.callTool({ name: 'suites_list', arguments: {} }); + await client.callTool({ name: 'suites', arguments: { command: 'list' } }); expect(calls[0]).toBe('/api/v2/other-project/suites'); }); @@ -122,8 +121,8 @@ describe('POST /mcp/', () => { const { client, transport } = connect(); await client.connect(transport); await client.callTool({ - name: 'tests_create', - arguments: { title: 'Session cleanup', suite_id: 'suite-1' }, + name: 'tests', + arguments: { command: 'create', title: 'Session cleanup', suite_id: 'suite-1' }, }); expect(calls).toEqual([ diff --git a/worker/test/oauth-flow.test.js b/worker/test/oauth-flow.test.js index 5117e1f..2b33ef0 100644 --- a/worker/test/oauth-flow.test.js +++ b/worker/test/oauth-flow.test.js @@ -165,7 +165,7 @@ describe('static bearer bypass', () => { expect(response.status).toBe(200); const payload = await response.json(); - expect(payload.result.tools.length).toBeGreaterThan(50); + expect(payload.result.tools.length).toBeGreaterThanOrEqual(17); } ); diff --git a/worker/test/token-revocation.test.js b/worker/test/token-revocation.test.js index a5a1656..2a6f74e 100644 --- a/worker/test/token-revocation.test.js +++ b/worker/test/token-revocation.test.js @@ -33,7 +33,7 @@ async function callTool() { jsonrpc: '2.0', id: 1, method: 'tools/call', - params: { name: 'tests_list', arguments: {} }, + params: { name: 'tests', arguments: { command: 'list' } }, }), }); }