@testplanit/mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +93 -0
- package/README.md +1346 -0
- package/dist/cli.js +5525 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.mts +490 -0
- package/dist/index.d.ts +490 -0
- package/dist/index.js +5543 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5501 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,1346 @@
|
|
|
1
|
+
# @testplanit/mcp-server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server for [TestPlanIt](https://github.com/testplanit/testplanit) — exposes test-management data to AI agents (Claude Desktop, Cursor, etc.) over stdio JSON-RPC.
|
|
4
|
+
|
|
5
|
+
## Quick install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx @testplanit/mcp-server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The server runs as a stdio MCP transport — your MCP-aware client (Claude Desktop, Cursor, etc.) starts it on demand. There is no daemon to manage and no port to forward.
|
|
12
|
+
|
|
13
|
+
## Environment variables
|
|
14
|
+
|
|
15
|
+
| Variable | Required | Description |
|
|
16
|
+
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `TESTPLANIT_API_TOKEN` | yes | API token from your TestPlanIt profile. Must start with `tpi_`. Mint one under **Profile → API Tokens**. |
|
|
18
|
+
| `TESTPLANIT_API_URL` | yes | Base URL of your TestPlanIt instance (e.g. `https://testplanit.yourcompany.com`). |
|
|
19
|
+
|
|
20
|
+
The server validates `TESTPLANIT_API_TOKEN` against the TestPlanIt API on startup. Invalid, expired, or revoked tokens cause the server to exit with code 1 before the MCP handshake completes — the agent will report a clean failure rather than hang.
|
|
21
|
+
|
|
22
|
+
## Token scopes
|
|
23
|
+
|
|
24
|
+
API tokens have two optional scope tags that change the server's behavior:
|
|
25
|
+
|
|
26
|
+
- **`mode:read`** — narrows the token to read-only operations across REST and MCP. The host enforces a single chokepoint that returns HTTP 403 with `code: "READ_ONLY_TOKEN"` on any write attempt; the MCP server translates that into a friendly agent-visible error. Recommended for AI agents that should be able to query data but never modify it.
|
|
27
|
+
- **`client:mcp`** — attributes audit-log entries from this token to the MCP source (`metadata.source: "mcp"`). The attribution is derived from the token scope itself — it cannot be forged by request-time headers. Recommended for any token used by an MCP-aware agent so administrators can correctly attribute agent-driven changes.
|
|
28
|
+
|
|
29
|
+
Set scopes when creating the token in **Profile → API Tokens** (checkboxes: "Read-only" and "Mark as agent token"). A token with no scopes behaves as a full-access traditional API token (backwards compatible).
|
|
30
|
+
|
|
31
|
+
## Tool Catalog
|
|
32
|
+
|
|
33
|
+
Phase 6 + Phase 7 + Phase 8 ship 27 production tools across cases / folders / tags / projects / runs / sessions / findings / code-repositories / issues / repository-case-links, plus three milestones-domain read tools (`testplanit_milestones_list`, `testplanit_milestones_get`, `testplanit_milestone_types_list`), two issue-link write tools (`testplanit_issues_link`, `testplanit_issues_unlink`), and the `testplanit_whoami` debug helper. Phase 9 adds 8 write tools: four for the runs domain (`testplanit_runs_create`, `testplanit_runs_update`, `testplanit_runs_cases_add`, `testplanit_test_run_results_create`), two for sessions (`testplanit_sessions_create`, `testplanit_sessions_update`), and two for milestones (`testplanit_milestones_create`, `testplanit_milestones_update`) — **42 total**. All tools authenticate via the bearer token in `TESTPLANIT_API_TOKEN`. Read tools return JSON; write tools return the same shape as their corresponding `_get` tool.
|
|
34
|
+
|
|
35
|
+
### Killer-app chain: "Who tested issue X?"
|
|
36
|
+
|
|
37
|
+
Two MCP calls give an agent the full executor lineage for an issue:
|
|
38
|
+
|
|
39
|
+
1. `testplanit_cases_list({ projectId: P, issueId: I })` → returns RepositoryCases linked to issue I (Phase 7 / D7-03 — additive filter).
|
|
40
|
+
2. `testplanit_test_run_results_list({ caseIds: [<from step 1>] })` → returns the most recent results (default `orderBy executedAt DESC` per D7-02) with `executedBy: { id, name, email }` inline.
|
|
41
|
+
|
|
42
|
+
No aggregate helper tool needed — the two-call composition is reusable for PR-diff impact (Phase 8) and any future "what tested this?" prompts.
|
|
43
|
+
|
|
44
|
+
### Context
|
|
45
|
+
|
|
46
|
+
#### `testplanit_whoami`
|
|
47
|
+
|
|
48
|
+
Debug helper. Returns the authenticated user (token owner), email, and scopes.
|
|
49
|
+
|
|
50
|
+
**Input:** None
|
|
51
|
+
|
|
52
|
+
**Output:**
|
|
53
|
+
```json
|
|
54
|
+
{ "id": "user-1", "name": "Alice", "email": "alice@example.com", "scopes": ["client:mcp"] }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### `testplanit_projects_list`
|
|
58
|
+
|
|
59
|
+
List all projects the token has access to. Use this to discover `projectId` for downstream tool calls.
|
|
60
|
+
|
|
61
|
+
**Input:** None
|
|
62
|
+
|
|
63
|
+
**Output:**
|
|
64
|
+
```json
|
|
65
|
+
{ "projects": [{ "id": 1, "name": "TestProject" }] }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Cases
|
|
69
|
+
|
|
70
|
+
#### `testplanit_cases_list`
|
|
71
|
+
|
|
72
|
+
List test cases scoped to a project. Supports filters and cursor-based pagination.
|
|
73
|
+
|
|
74
|
+
**Input:**
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"projectId": 1,
|
|
78
|
+
"folderId": 2,
|
|
79
|
+
"tagIds": [3, 4],
|
|
80
|
+
"name": "login",
|
|
81
|
+
"stateId": 5,
|
|
82
|
+
"customField": { "name": "Priority" },
|
|
83
|
+
"issueId": 55,
|
|
84
|
+
"cursor": 100,
|
|
85
|
+
"limit": 25
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The `issueId` filter (Phase 7 / D7-03 — additive) returns RepositoryCases linked to the named Issue. Used as the front-half of the killer-app chain `cases_list({issueId}) → test_run_results_list({caseIds})`.
|
|
90
|
+
|
|
91
|
+
**Phase 8 maintenance filters (D8-01 / D8-02 — additive):** seven new filters narrow the list to test-maintenance questions:
|
|
92
|
+
|
|
93
|
+
- `automated: boolean` — narrows to user-flagged automated tests (`RepositoryCases.automated`); independent of `source`.
|
|
94
|
+
- `source: 'MANUAL'|'JUNIT'|'TESTNG'|'XUNIT'|'NUNIT'|'MSTEST'|'MOCHA'|'CUCUMBER'|'API'` — single value or array; the import-format that originally created the row.
|
|
95
|
+
- `repositoryId: number` — scopes to a specific per-project case container (useful for multi-repository projects).
|
|
96
|
+
- `hasNeverExecuted: boolean` — narrows to cases with **zero** JUnit results AND zero TestRunResults (via TestRunCases).
|
|
97
|
+
- `staleSinceUpdate: boolean` — narrows to cases whose latest execution timestamp is earlier than the latest update timestamp (or never executed). Implemented as a handler-side post-filter with a bounded scan cap of 400 rows; the response stamps `truncated: true` when the cap is hit.
|
|
98
|
+
- `updatedAfter: ISODate`, `updatedBefore: ISODate` — calendar filters that route through the `repositoryCaseVersions` relation (`RepositoryCases` has no `updatedAt` column).
|
|
99
|
+
|
|
100
|
+
**Creator + creation-date filters (additive):** three more optional inputs narrow the list to authorship questions like *"How many test cases did I write last month?"*:
|
|
101
|
+
|
|
102
|
+
- `creatorIds: string[]` — array of user IDs; matches any. Deliberately wider than `runs_list` / `sessions_list` `createdById` (single string) — a frequent question is "what did **anyone on my team** write" and the array shape avoids round-tripping through union of single-creator calls.
|
|
103
|
+
- `from: ISODate`, `to: ISODate` — `createdAt` range filter. Naming mirrors `runs_list` / `sessions_list` for consistency.
|
|
104
|
+
|
|
105
|
+
**Phase 8 row fields (additive):** every row carries `lastUpdatedAt` (from `repositoryCaseVersions[currentVersion].createdAt`) and `latestResult: { id, status, executedAt, source: 'TestRun' | 'JUnit' } | null` (the most recent execution across both pipelines, with a `source` discriminator). Returns `null` when the case has never been executed.
|
|
106
|
+
|
|
107
|
+
**Output:**
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"items": [
|
|
111
|
+
{
|
|
112
|
+
"id": 99,
|
|
113
|
+
"name": "Login flow",
|
|
114
|
+
"source": "MANUAL",
|
|
115
|
+
"automated": false,
|
|
116
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
117
|
+
"project": { "id": 1, "name": "TestProject" },
|
|
118
|
+
"folder": { "id": 2, "name": "Auth" },
|
|
119
|
+
"state": { "id": 5, "name": "Active" },
|
|
120
|
+
"creator": { "id": "user-1", "name": "Alice", "email": "alice@example.com" },
|
|
121
|
+
"tags": [{ "id": 3, "name": "regression" }]
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"hasNextPage": false,
|
|
125
|
+
"nextCursor": null
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
When `hasNextPage` is `true`, pass `nextCursor` as `cursor` to fetch the next page.
|
|
130
|
+
|
|
131
|
+
#### `testplanit_cases_get`
|
|
132
|
+
|
|
133
|
+
Fetch full details for a single test case, including steps (plain text), custom fields (flat dict keyed by display name), folder breadcrumb, linked issues, and linked automated tests. **Phase 8 (additive):** the response now includes inline `codeRepository: { id, name, type, url? } | null` derived from the project's configured `ProjectCodeRepositoryConfig.repository` — surfaces the test-framework source location alongside the existing case detail. The repository's `credentials` column is never returned and `settings` is stripped to a per-provider public-key allow-list.
|
|
134
|
+
|
|
135
|
+
**Input:**
|
|
136
|
+
```json
|
|
137
|
+
{ "caseId": 99 }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Output:**
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"id": 99,
|
|
144
|
+
"name": "Login flow",
|
|
145
|
+
"source": "MANUAL",
|
|
146
|
+
"automated": false,
|
|
147
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
148
|
+
"project": { "id": 1, "name": "TestProject" },
|
|
149
|
+
"folder": { "id": 2, "name": "Auth" },
|
|
150
|
+
"folderBreadcrumb": [{ "id": 10, "name": "Regression" }, { "id": 2, "name": "Auth" }],
|
|
151
|
+
"folderFullPath": "Regression / Auth",
|
|
152
|
+
"state": { "id": 5, "name": "Active" },
|
|
153
|
+
"creator": { "id": "user-1", "name": "Alice", "email": "alice@example.com" },
|
|
154
|
+
"tags": [{ "id": 3, "name": "regression" }],
|
|
155
|
+
"steps": [
|
|
156
|
+
{ "id": 1, "order": 0, "step": "Open the login page", "expectedResult": "Login form is visible" }
|
|
157
|
+
],
|
|
158
|
+
"customFields": { "Priority": "High", "Severity": 2 },
|
|
159
|
+
"issues": [{ "id": 55, "externalKey": "JIRA-99", "title": "Login bug", "externalStatus": "Open" }],
|
|
160
|
+
"linkedAutomatedTests": [{ "id": 9, "name": "automated_test_a", "source": "JUNIT" }]
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### `testplanit_cases_create`
|
|
165
|
+
|
|
166
|
+
Create a new test case. Returns the full CASE-02 shape (same as `testplanit_cases_get`).
|
|
167
|
+
|
|
168
|
+
**Input:**
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"projectId": 1,
|
|
172
|
+
"folderId": 2,
|
|
173
|
+
"name": "New login test",
|
|
174
|
+
"stateName": "Active",
|
|
175
|
+
"steps": [
|
|
176
|
+
{ "text": "Open the login page", "expectedResult": "Login form is visible", "order": 0 }
|
|
177
|
+
],
|
|
178
|
+
"tags": [3, "regression"],
|
|
179
|
+
"customFields": { "Priority": "High" }
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- `stateName` — defaults to the first CASES-scope workflow state for the project.
|
|
184
|
+
- `tags` — accepts tag IDs (numbers) or tag names (strings, created if missing).
|
|
185
|
+
- `customFields` — flat dict keyed by display name; unknown names return a structured error.
|
|
186
|
+
|
|
187
|
+
**Output:** Same shape as `testplanit_cases_get`.
|
|
188
|
+
|
|
189
|
+
#### `testplanit_cases_update`
|
|
190
|
+
|
|
191
|
+
Partially update a test case (name, stateName, folderId, steps, tags, customFields). Providing `steps` replaces the entire step set (old steps are soft-deleted). Returns the full CASE-02 shape.
|
|
192
|
+
|
|
193
|
+
**Input:**
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"caseId": 99,
|
|
197
|
+
"name": "Updated name",
|
|
198
|
+
"stateName": "In Progress",
|
|
199
|
+
"folderId": 3,
|
|
200
|
+
"steps": [{ "text": "Step 1", "expectedResult": "Expected 1" }],
|
|
201
|
+
"tags": ["smoke"],
|
|
202
|
+
"customFields": { "Priority": "Low" }
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Output:** Same shape as `testplanit_cases_get`.
|
|
207
|
+
|
|
208
|
+
#### `testplanit_cases_delete`
|
|
209
|
+
|
|
210
|
+
Soft-delete a test case (sets `isDeleted: true`). The case is hidden from subsequent list/get calls but retained in the database for audit purposes.
|
|
211
|
+
|
|
212
|
+
**Input:**
|
|
213
|
+
```json
|
|
214
|
+
{ "caseId": 99 }
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Output:**
|
|
218
|
+
```json
|
|
219
|
+
{ "id": 99, "isDeleted": true }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Folders
|
|
223
|
+
|
|
224
|
+
#### `testplanit_folders_list`
|
|
225
|
+
|
|
226
|
+
List all folders for a project as a tree. Returns root folders with up to 2 levels of children inline. Each node includes a case count (non-deleted cases only). For deeper subtrees, use `testplanit_folders_get`.
|
|
227
|
+
|
|
228
|
+
**Input:**
|
|
229
|
+
```json
|
|
230
|
+
{ "projectId": 1 }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Output:**
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"tree": [
|
|
237
|
+
{
|
|
238
|
+
"id": 10,
|
|
239
|
+
"name": "Regression",
|
|
240
|
+
"parentId": null,
|
|
241
|
+
"caseCount": 3,
|
|
242
|
+
"children": [
|
|
243
|
+
{ "id": 2, "name": "Auth", "parentId": 10, "caseCount": 1, "children": [] }
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### `testplanit_folders_get`
|
|
251
|
+
|
|
252
|
+
Fetch full details for a single folder, including parent breadcrumb, direct children, and a summary of cases (capped at 100 rows).
|
|
253
|
+
|
|
254
|
+
**Input:**
|
|
255
|
+
```json
|
|
256
|
+
{ "folderId": 2 }
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Output:**
|
|
260
|
+
```json
|
|
261
|
+
{
|
|
262
|
+
"id": 2,
|
|
263
|
+
"name": "Auth",
|
|
264
|
+
"parentId": 10,
|
|
265
|
+
"breadcrumb": [{ "id": 10, "name": "Regression" }, { "id": 2, "name": "Auth" }],
|
|
266
|
+
"fullPath": "Regression / Auth",
|
|
267
|
+
"children": [{ "id": 20, "name": "OAuth", "parentId": 2, "caseCount": 0, "children": [] }],
|
|
268
|
+
"cases": [{ "id": 99, "name": "Login flow" }],
|
|
269
|
+
"caseCount": 1
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### `testplanit_folders_create`
|
|
274
|
+
|
|
275
|
+
Create a folder. Omit `parentId` for a root folder. Returns the full `testplanit_folders_get` shape.
|
|
276
|
+
|
|
277
|
+
**Input:**
|
|
278
|
+
```json
|
|
279
|
+
{
|
|
280
|
+
"projectId": 1,
|
|
281
|
+
"name": "New Folder",
|
|
282
|
+
"parentId": 10
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Output:** Same shape as `testplanit_folders_get`.
|
|
287
|
+
|
|
288
|
+
#### `testplanit_folders_update`
|
|
289
|
+
|
|
290
|
+
Rename a folder, reparent it, or both. Pass `parentId: null` to move the folder to root (disconnect from parent). Returns the full `testplanit_folders_get` shape.
|
|
291
|
+
|
|
292
|
+
**Input:**
|
|
293
|
+
```json
|
|
294
|
+
{
|
|
295
|
+
"folderId": 2,
|
|
296
|
+
"name": "Auth Tests",
|
|
297
|
+
"parentId": null
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Output:** Same shape as `testplanit_folders_get`.
|
|
302
|
+
|
|
303
|
+
#### `testplanit_folders_delete`
|
|
304
|
+
|
|
305
|
+
Soft-delete a folder. The tool checks that the folder has no active cases and no active sub-folders before issuing the delete — non-empty folders surface a structured CASE-12 error naming the violation. Returns `{ id, isDeleted: true }` on success.
|
|
306
|
+
|
|
307
|
+
**Input:**
|
|
308
|
+
```json
|
|
309
|
+
{ "folderId": 2 }
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Output:**
|
|
313
|
+
```json
|
|
314
|
+
{ "id": 2, "isDeleted": true }
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Tags
|
|
318
|
+
|
|
319
|
+
#### `testplanit_tags_list`
|
|
320
|
+
|
|
321
|
+
List all tags (global). When `projectId` is provided, usage counts are scoped to that project's cases, test runs, and sessions.
|
|
322
|
+
|
|
323
|
+
**Input:**
|
|
324
|
+
```json
|
|
325
|
+
{ "projectId": 1 }
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Output:**
|
|
329
|
+
```json
|
|
330
|
+
{
|
|
331
|
+
"tags": [
|
|
332
|
+
{
|
|
333
|
+
"id": 3,
|
|
334
|
+
"name": "regression",
|
|
335
|
+
"usageCounts": { "repositoryCases": 12, "testRuns": 5, "sessions": 0 }
|
|
336
|
+
}
|
|
337
|
+
]
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Execution + Session Read (Phase 7)
|
|
342
|
+
|
|
343
|
+
Phase 7 ships 10 read-only tools across the test-execution domain (5 run-side tools + 5 session-side tools). All carry `isDeleted: false` filters, deterministic `[{<order>:'desc'},{id:'desc'}]` orderBy, and cursor pagination capped at `limit: 100` (T-07-06 DoS guard).
|
|
344
|
+
|
|
345
|
+
#### `testplanit_test_runs_list` (EXEC-01)
|
|
346
|
+
|
|
347
|
+
List test runs scoped to a project. Each row carries `statusCounts: [{id,name,count}]` + `untested` + `total` inline (per D7-06 — counts SUM to `total` per R3 invariant). The rollup is fetched via a SINGLE batched groupBy per page (no N+1) so an agent can list 100 runs in one tool call.
|
|
348
|
+
|
|
349
|
+
**Input:**
|
|
350
|
+
```json
|
|
351
|
+
{
|
|
352
|
+
"projectId": 1,
|
|
353
|
+
"stateId": 3,
|
|
354
|
+
"isCompleted": false,
|
|
355
|
+
"createdById": "user-1",
|
|
356
|
+
"from": "2026-01-01T00:00:00Z",
|
|
357
|
+
"to": "2026-02-01T00:00:00Z",
|
|
358
|
+
"cursor": 100,
|
|
359
|
+
"limit": 25
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Output:**
|
|
364
|
+
```json
|
|
365
|
+
{
|
|
366
|
+
"items": [
|
|
367
|
+
{
|
|
368
|
+
"id": 5,
|
|
369
|
+
"name": "Sprint 12 regression",
|
|
370
|
+
"isCompleted": false,
|
|
371
|
+
"completedAt": null,
|
|
372
|
+
"createdAt": "2026-01-05T00:00:00Z",
|
|
373
|
+
"testRunType": "REGULAR",
|
|
374
|
+
"project": { "id": 1, "name": "TestProject" },
|
|
375
|
+
"state": { "id": 3, "name": "In Progress" },
|
|
376
|
+
"createdBy": { "id": "user-1", "name": "Alice", "email": "alice@example.com" },
|
|
377
|
+
"configuration": null,
|
|
378
|
+
"milestone": null,
|
|
379
|
+
"tags": [],
|
|
380
|
+
"issues": [],
|
|
381
|
+
"statusCounts": [
|
|
382
|
+
{ "id": 1, "name": "Passed", "count": 5 },
|
|
383
|
+
{ "id": 2, "name": "Failed", "count": 2 }
|
|
384
|
+
],
|
|
385
|
+
"untested": 1,
|
|
386
|
+
"total": 8
|
|
387
|
+
}
|
|
388
|
+
],
|
|
389
|
+
"hasNextPage": false,
|
|
390
|
+
"nextCursor": null
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Example:** "List the most recent 10 test runs in project 5."
|
|
395
|
+
|
|
396
|
+
#### `testplanit_test_runs_get` (EXEC-02)
|
|
397
|
+
|
|
398
|
+
Fetch a single test run with the first 50 testCases inline + status rollup. Each inline testCase carries `latestResult: { id, status, executedBy, executedAt }` so the agent sees the most-recent execution per case in one call.
|
|
399
|
+
|
|
400
|
+
**Input:**
|
|
401
|
+
```json
|
|
402
|
+
{ "runId": 5 }
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Output:**
|
|
406
|
+
```json
|
|
407
|
+
{
|
|
408
|
+
"id": 5,
|
|
409
|
+
"name": "Sprint 12 regression",
|
|
410
|
+
"project": { "id": 1, "name": "TestProject" },
|
|
411
|
+
"state": { "id": 3, "name": "In Progress" },
|
|
412
|
+
"createdBy": { "id": "user-1", "name": "Alice", "email": "alice@example.com" },
|
|
413
|
+
"statusCounts": [{ "id": 1, "name": "Passed", "count": 5 }],
|
|
414
|
+
"untested": 1,
|
|
415
|
+
"total": 8,
|
|
416
|
+
"testCases": [
|
|
417
|
+
{
|
|
418
|
+
"id": 100,
|
|
419
|
+
"order": 0,
|
|
420
|
+
"isCompleted": false,
|
|
421
|
+
"repositoryCase": { "id": 99, "name": "Login flow", "source": "MANUAL" },
|
|
422
|
+
"assignedTo": null,
|
|
423
|
+
"status": { "id": 1, "name": "Passed" },
|
|
424
|
+
"latestResult": {
|
|
425
|
+
"id": 555,
|
|
426
|
+
"status": { "id": 1, "name": "Passed" },
|
|
427
|
+
"executedBy": { "id": "user-1", "name": "Alice", "email": "alice@example.com" },
|
|
428
|
+
"executedAt": "2026-01-05T01:00:00Z"
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
],
|
|
432
|
+
"testCasesNextCursor": null
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**Example:** "Show me the details of run 5 including the status counts."
|
|
437
|
+
|
|
438
|
+
#### `testplanit_test_runs_cases_list` (EXEC-03)
|
|
439
|
+
|
|
440
|
+
Paginate testCases for a run beyond the 50-cap returned by `testplanit_test_runs_get`. Same row shape as the inline `testCases[i]`.
|
|
441
|
+
|
|
442
|
+
**Input:**
|
|
443
|
+
```json
|
|
444
|
+
{
|
|
445
|
+
"runId": 5,
|
|
446
|
+
"isCompleted": false,
|
|
447
|
+
"statusId": 2,
|
|
448
|
+
"assignedToId": "user-1",
|
|
449
|
+
"cursor": 100,
|
|
450
|
+
"limit": 25
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**Output:** `{ items: [...], hasNextPage, nextCursor }` — items match the `testCases[i]` shape from `testplanit_test_runs_get`.
|
|
455
|
+
|
|
456
|
+
**Example:** "List the failed cases in run 5."
|
|
457
|
+
|
|
458
|
+
#### `testplanit_test_run_results_list` (EXEC-04 + EXEC-06 back-half)
|
|
459
|
+
|
|
460
|
+
List test-run results with denormalized status / executedBy / testRunCase. The `caseIds` filter ships the **back-half** of the killer-app chain — pass the RepositoryCase ids returned by `testplanit_cases_list({issueId})` to get the latest results per case (`orderBy executedAt DESC` matches the schema index).
|
|
461
|
+
|
|
462
|
+
**Input:**
|
|
463
|
+
```json
|
|
464
|
+
{
|
|
465
|
+
"runId": 5,
|
|
466
|
+
"caseIds": [99, 100],
|
|
467
|
+
"executedById": "user-1",
|
|
468
|
+
"statusId": 2,
|
|
469
|
+
"from": "2026-01-01T00:00:00Z",
|
|
470
|
+
"to": "2026-02-01T00:00:00Z",
|
|
471
|
+
"cursor": 200,
|
|
472
|
+
"limit": 25
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Output:**
|
|
477
|
+
```json
|
|
478
|
+
{
|
|
479
|
+
"items": [
|
|
480
|
+
{
|
|
481
|
+
"id": 555,
|
|
482
|
+
"attempt": 1,
|
|
483
|
+
"executedAt": "2026-01-05T01:00:00Z",
|
|
484
|
+
"status": { "id": 1, "name": "Passed" },
|
|
485
|
+
"executedBy": { "id": "user-1", "name": "Alice", "email": "alice@example.com" },
|
|
486
|
+
"testRunCase": {
|
|
487
|
+
"id": 100,
|
|
488
|
+
"repositoryCaseId": 99,
|
|
489
|
+
"repositoryCase": { "id": 99, "name": "Login flow", "source": "MANUAL" },
|
|
490
|
+
"testRun": { "id": 5, "name": "Sprint 12 regression" }
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
],
|
|
494
|
+
"hasNextPage": false,
|
|
495
|
+
"nextCursor": null
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
**Example:** "Who tested the cases linked to issue JIRA-42?" — chain `cases_list({issueId})` then this tool with `caseIds`.
|
|
500
|
+
|
|
501
|
+
#### `testplanit_test_run_results_get` (EXEC-05)
|
|
502
|
+
|
|
503
|
+
Drill-down — fetch a single test-run result with `stepResults: [...]` inlined. Each step result carries `stepText` / `expectedResultText` (ProseMirror-extracted) + `status` (from the `stepStatus` relation per R2) + `notes` + `evidence` (as-is per D7-08) + `attachments` + `issues`. Top-level result carries `customFields` denormalized and the parent `testRunCase` summary.
|
|
504
|
+
|
|
505
|
+
**Input:**
|
|
506
|
+
```json
|
|
507
|
+
{ "resultId": 555 }
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
**Output:**
|
|
511
|
+
```json
|
|
512
|
+
{
|
|
513
|
+
"id": 555,
|
|
514
|
+
"attempt": 1,
|
|
515
|
+
"executedAt": "2026-01-05T01:00:00Z",
|
|
516
|
+
"elapsed": 320,
|
|
517
|
+
"notes": "Step 2 had a 200ms hiccup",
|
|
518
|
+
"evidence": { "url": "https://traces.example.com/run-5/case-100" },
|
|
519
|
+
"status": { "id": 1, "name": "Passed" },
|
|
520
|
+
"executedBy": { "id": "user-1", "name": "Alice", "email": "alice@example.com" },
|
|
521
|
+
"testRunCase": {
|
|
522
|
+
"id": 100,
|
|
523
|
+
"repositoryCaseId": 99,
|
|
524
|
+
"repositoryCase": { "id": 99, "name": "Login flow", "source": "MANUAL" },
|
|
525
|
+
"testRun": { "id": 5, "name": "Sprint 12 regression" }
|
|
526
|
+
},
|
|
527
|
+
"customFields": { "Priority": "High" },
|
|
528
|
+
"stepResults": [
|
|
529
|
+
{
|
|
530
|
+
"id": 7000,
|
|
531
|
+
"status": { "id": 1, "name": "Passed" },
|
|
532
|
+
"stepId": 1,
|
|
533
|
+
"stepOrder": 0,
|
|
534
|
+
"stepText": "Open the login page",
|
|
535
|
+
"expectedResultText": "Login form is visible",
|
|
536
|
+
"notes": "",
|
|
537
|
+
"evidence": null,
|
|
538
|
+
"executedAt": "2026-01-05T01:00:00Z",
|
|
539
|
+
"elapsed": 80,
|
|
540
|
+
"attachments": [],
|
|
541
|
+
"issues": []
|
|
542
|
+
}
|
|
543
|
+
],
|
|
544
|
+
"attachments": [],
|
|
545
|
+
"issues": []
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Example:** "Show me the step-level breakdown for result 555."
|
|
550
|
+
|
|
551
|
+
#### `testplanit_runs_create`
|
|
552
|
+
|
|
553
|
+
Create a new test run. Optionally adds repository test cases in the same call (up to 250). Defaults to the first RUNS-scope workflow state if `stateName` is omitted.
|
|
554
|
+
|
|
555
|
+
**Input:**
|
|
556
|
+
```json
|
|
557
|
+
{
|
|
558
|
+
"projectId": 1,
|
|
559
|
+
"name": "Sprint 13 regression",
|
|
560
|
+
"caseIds": [99, 100, 101],
|
|
561
|
+
"milestoneId": 7,
|
|
562
|
+
"configId": 3,
|
|
563
|
+
"stateName": "In Progress",
|
|
564
|
+
"tags": [3, "smoke"]
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
- `caseIds` — optional, max 250; appended as TestRunCases in order.
|
|
569
|
+
- `tags` — accepts tag IDs (numbers) or tag names (strings, created if missing).
|
|
570
|
+
- `stateName` — defaults to the first RUNS-scope workflow state for the project.
|
|
571
|
+
|
|
572
|
+
**Output:** Same shape as `testplanit_test_runs_get`.
|
|
573
|
+
|
|
574
|
+
#### `testplanit_runs_update`
|
|
575
|
+
|
|
576
|
+
Update an existing test run. Pass `milestoneId: null` or `configId: null` to remove those associations. Providing `tags` replaces the full tag set.
|
|
577
|
+
|
|
578
|
+
**Input:**
|
|
579
|
+
```json
|
|
580
|
+
{
|
|
581
|
+
"runId": 5,
|
|
582
|
+
"name": "Sprint 13 regression (updated)",
|
|
583
|
+
"stateName": "Completed",
|
|
584
|
+
"milestoneId": null,
|
|
585
|
+
"configId": null,
|
|
586
|
+
"tags": ["smoke"],
|
|
587
|
+
"isCompleted": true
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**Output:** Same shape as `testplanit_test_runs_get`.
|
|
592
|
+
|
|
593
|
+
#### `testplanit_runs_cases_add`
|
|
594
|
+
|
|
595
|
+
Add repository test cases to an existing run. Cases are appended in order after any existing cases; duplicates are skipped silently.
|
|
596
|
+
|
|
597
|
+
**Input:**
|
|
598
|
+
```json
|
|
599
|
+
{
|
|
600
|
+
"runId": 5,
|
|
601
|
+
"caseIds": [102, 103, 104]
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
- `caseIds` — required, 1–250.
|
|
606
|
+
|
|
607
|
+
**Output:**
|
|
608
|
+
```json
|
|
609
|
+
{ "runId": 5, "requested": 3, "total": 11 }
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
- `requested` — number of caseIds submitted.
|
|
613
|
+
- `total` — total TestRunCases in the run after the add (including pre-existing cases).
|
|
614
|
+
|
|
615
|
+
#### `testplanit_test_run_results_create`
|
|
616
|
+
|
|
617
|
+
Submit a test result for a case in a run. Atomically creates the result and updates the run case's current status. The attempt number is auto-incremented — callers do not track it.
|
|
618
|
+
|
|
619
|
+
**Input:**
|
|
620
|
+
```json
|
|
621
|
+
{
|
|
622
|
+
"testRunCaseId": 100,
|
|
623
|
+
"statusName": "Passed",
|
|
624
|
+
"notes": "All steps green.",
|
|
625
|
+
"elapsed": 320
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
- `statusName` — matched by name within the project's configured statuses.
|
|
630
|
+
- `elapsed` — optional duration in seconds; pass `null` to omit.
|
|
631
|
+
|
|
632
|
+
**Output:** Same shape as `testplanit_test_run_results_get`.
|
|
633
|
+
|
|
634
|
+
#### `testplanit_sessions_list` (SESS-01)
|
|
635
|
+
|
|
636
|
+
List exploratory sessions scoped to a project, with denormalized state / createdBy / assignedTo / template / configuration / milestone / tags. Mission and note are extracted from ProseMirror to plain text.
|
|
637
|
+
|
|
638
|
+
**Input:**
|
|
639
|
+
```json
|
|
640
|
+
{
|
|
641
|
+
"projectId": 1,
|
|
642
|
+
"stateId": 3,
|
|
643
|
+
"isCompleted": false,
|
|
644
|
+
"createdById": "user-1",
|
|
645
|
+
"from": "2026-01-01T00:00:00Z",
|
|
646
|
+
"to": "2026-02-01T00:00:00Z",
|
|
647
|
+
"cursor": 50,
|
|
648
|
+
"limit": 25
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
**Output:**
|
|
653
|
+
```json
|
|
654
|
+
{
|
|
655
|
+
"items": [
|
|
656
|
+
{
|
|
657
|
+
"id": 12,
|
|
658
|
+
"name": "Login flow exploration",
|
|
659
|
+
"isCompleted": false,
|
|
660
|
+
"completedAt": null,
|
|
661
|
+
"createdAt": "2026-01-10T00:00:00Z",
|
|
662
|
+
"mission": "Explore login edge cases",
|
|
663
|
+
"note": "",
|
|
664
|
+
"project": { "id": 1, "name": "TestProject" },
|
|
665
|
+
"state": { "id": 3, "name": "In Progress" },
|
|
666
|
+
"createdBy": { "id": "user-1", "name": "Alice", "email": "alice@example.com" },
|
|
667
|
+
"assignedTo": null,
|
|
668
|
+
"template": { "id": 4, "name": "ExploratoryTemplate" },
|
|
669
|
+
"configuration": null,
|
|
670
|
+
"milestone": null,
|
|
671
|
+
"tags": []
|
|
672
|
+
}
|
|
673
|
+
],
|
|
674
|
+
"hasNextPage": false,
|
|
675
|
+
"nextCursor": null
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
**Example:** "List my open sessions in project 1."
|
|
680
|
+
|
|
681
|
+
#### `testplanit_sessions_get` (SESS-02)
|
|
682
|
+
|
|
683
|
+
Fetch a single session with up to 100 sessionResults inlined and a `truncated: boolean` marker (D7-12). When `truncated: true`, paginate the rest via `testplanit_session_results_list({sessionId})`.
|
|
684
|
+
|
|
685
|
+
**Input:**
|
|
686
|
+
```json
|
|
687
|
+
{ "sessionId": 12 }
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
**Output:**
|
|
691
|
+
```json
|
|
692
|
+
{
|
|
693
|
+
"id": 12,
|
|
694
|
+
"name": "Login flow exploration",
|
|
695
|
+
"mission": "Explore login edge cases",
|
|
696
|
+
"note": "",
|
|
697
|
+
"project": { "id": 1, "name": "TestProject" },
|
|
698
|
+
"state": { "id": 3, "name": "In Progress" },
|
|
699
|
+
"createdBy": { "id": "user-1", "name": "Alice", "email": "alice@example.com" },
|
|
700
|
+
"issues": [],
|
|
701
|
+
"customFields": {},
|
|
702
|
+
"sessionResults": [
|
|
703
|
+
{
|
|
704
|
+
"id": 800,
|
|
705
|
+
"createdAt": "2026-01-10T00:30:00Z",
|
|
706
|
+
"elapsed": 600,
|
|
707
|
+
"resultDataText": "Tried 5 invalid passwords; UI showed correct error.",
|
|
708
|
+
"status": { "id": 1, "name": "Passed" },
|
|
709
|
+
"createdBy": { "id": "user-1", "name": "Alice", "email": "alice@example.com" },
|
|
710
|
+
"session": { "id": 12, "name": "Login flow exploration", "projectId": 1 }
|
|
711
|
+
}
|
|
712
|
+
],
|
|
713
|
+
"truncated": false
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
**Example:** "Show me session 12 with its results."
|
|
718
|
+
|
|
719
|
+
#### `testplanit_sessions_create`
|
|
720
|
+
|
|
721
|
+
Create a new exploratory test session. Auto-resolves the default template and the first SESSIONS-scope workflow state if not specified.
|
|
722
|
+
|
|
723
|
+
**Input:**
|
|
724
|
+
```json
|
|
725
|
+
{
|
|
726
|
+
"projectId": 1,
|
|
727
|
+
"name": "Login edge case exploration",
|
|
728
|
+
"mission": "Explore login edge cases under slow network conditions",
|
|
729
|
+
"milestoneId": 7,
|
|
730
|
+
"configId": 3,
|
|
731
|
+
"stateName": "In Progress",
|
|
732
|
+
"tags": [3, "exploratory"]
|
|
733
|
+
}
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
- `mission` — optional plain-text mission statement.
|
|
737
|
+
- `stateName` — defaults to the first SESSIONS-scope workflow state for the project.
|
|
738
|
+
- `tags` — accepts tag IDs (numbers) or tag names (strings, created if missing).
|
|
739
|
+
|
|
740
|
+
**Output:** Same shape as `testplanit_sessions_get`.
|
|
741
|
+
|
|
742
|
+
#### `testplanit_sessions_update`
|
|
743
|
+
|
|
744
|
+
Update an existing session. Pass `mission: null`, `milestoneId: null`, or `configId: null` to remove those fields. Providing `tags` replaces the full tag set.
|
|
745
|
+
|
|
746
|
+
**Input:**
|
|
747
|
+
```json
|
|
748
|
+
{
|
|
749
|
+
"sessionId": 12,
|
|
750
|
+
"name": "Login edge case exploration (updated)",
|
|
751
|
+
"mission": null,
|
|
752
|
+
"stateName": "Completed",
|
|
753
|
+
"milestoneId": null,
|
|
754
|
+
"configId": null,
|
|
755
|
+
"tags": ["smoke"],
|
|
756
|
+
"isCompleted": true
|
|
757
|
+
}
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
**Output:** Same shape as `testplanit_sessions_get`.
|
|
761
|
+
|
|
762
|
+
#### `testplanit_session_results_list` (SESS-03)
|
|
763
|
+
|
|
764
|
+
List session results with filters `sessionId` / `createdById` / `statusId`. **NO** `testCaseId` filter — Sessions are exploratory and SessionResults has no `testCaseId` FK (R4 invariant; enforced at the input schema and at the where-clause Prisma type).
|
|
765
|
+
|
|
766
|
+
**Input:**
|
|
767
|
+
```json
|
|
768
|
+
{
|
|
769
|
+
"sessionId": 12,
|
|
770
|
+
"createdById": "user-1",
|
|
771
|
+
"statusId": 2,
|
|
772
|
+
"cursor": 100,
|
|
773
|
+
"limit": 25
|
|
774
|
+
}
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
**Output:** `{ items, hasNextPage, nextCursor }` — items match the `sessionResults[i]` shape from `testplanit_sessions_get`.
|
|
778
|
+
|
|
779
|
+
**Example:** "List failed results in session 12."
|
|
780
|
+
|
|
781
|
+
#### `testplanit_session_results_get` (SESS-04)
|
|
782
|
+
|
|
783
|
+
Fetch a single session result with denormalized status / `createdBy` (the executor — D7-13: NO separate `executedBy` field) / session / customFields / attachments / issues. **NO** step-level results — sessions are exploratory and don't have ordered steps.
|
|
784
|
+
|
|
785
|
+
**Input:**
|
|
786
|
+
```json
|
|
787
|
+
{ "resultId": 800 }
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
**Output:**
|
|
791
|
+
```json
|
|
792
|
+
{
|
|
793
|
+
"id": 800,
|
|
794
|
+
"createdAt": "2026-01-10T00:30:00Z",
|
|
795
|
+
"elapsed": 600,
|
|
796
|
+
"resultDataText": "Tried 5 invalid passwords; UI showed correct error.",
|
|
797
|
+
"status": { "id": 1, "name": "Passed" },
|
|
798
|
+
"createdBy": { "id": "user-1", "name": "Alice", "email": "alice@example.com" },
|
|
799
|
+
"session": { "id": 12, "name": "Login flow exploration", "projectId": 1 },
|
|
800
|
+
"customFields": { "Severity": "Low" },
|
|
801
|
+
"attachments": [],
|
|
802
|
+
"issues": []
|
|
803
|
+
}
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
**Example:** "Show me session result 800."
|
|
807
|
+
|
|
808
|
+
#### `testplanit_sessions_findings_list` (SESS-05)
|
|
809
|
+
|
|
810
|
+
Dual-mode (XOR — provide exactly one of `sessionId` or `issueId`):
|
|
811
|
+
|
|
812
|
+
- **sessionId mode** — returns Issue rows linked to the session OR to any of its sessionResults. Mirrors the agent-facing prompt "what issues surfaced in session X?"
|
|
813
|
+
- **issueId mode** — returns the issue plus its `linkedSessions` and `linkedSessionResults`. Mirrors "where did issue Y appear?"
|
|
814
|
+
|
|
815
|
+
**Input (sessionId mode):**
|
|
816
|
+
```json
|
|
817
|
+
{ "sessionId": 12 }
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
**Output (sessionId mode):**
|
|
821
|
+
```json
|
|
822
|
+
{
|
|
823
|
+
"session": { "id": 12, "name": "Login flow exploration" },
|
|
824
|
+
"findings": [
|
|
825
|
+
{
|
|
826
|
+
"id": 55,
|
|
827
|
+
"externalKey": "JIRA-99",
|
|
828
|
+
"title": "Login fails on Safari",
|
|
829
|
+
"status": "Open",
|
|
830
|
+
"externalStatus": "Open",
|
|
831
|
+
"priority": "High",
|
|
832
|
+
"externalSystem": "JIRA"
|
|
833
|
+
}
|
|
834
|
+
],
|
|
835
|
+
"truncated": false
|
|
836
|
+
}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
**Input (issueId mode):**
|
|
840
|
+
```json
|
|
841
|
+
{ "issueId": 55 }
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
**Output (issueId mode):**
|
|
845
|
+
```json
|
|
846
|
+
{
|
|
847
|
+
"issue": { "id": 55, "externalKey": "JIRA-99", "title": "Login fails on Safari", "externalStatus": "Open", "externalSystem": "JIRA" },
|
|
848
|
+
"linkedSessions": [
|
|
849
|
+
{
|
|
850
|
+
"id": 12,
|
|
851
|
+
"name": "Login flow exploration",
|
|
852
|
+
"createdAt": "2026-01-10T00:00:00Z",
|
|
853
|
+
"isCompleted": false,
|
|
854
|
+
"createdBy": { "id": "user-1", "name": "Alice", "email": "alice@example.com" }
|
|
855
|
+
}
|
|
856
|
+
],
|
|
857
|
+
"linkedSessionResults": [
|
|
858
|
+
{
|
|
859
|
+
"id": 800,
|
|
860
|
+
"sessionId": 12,
|
|
861
|
+
"createdAt": "2026-01-10T00:30:00Z",
|
|
862
|
+
"status": { "id": 1, "name": "Passed" },
|
|
863
|
+
"createdBy": { "id": "user-1", "name": "Alice", "email": "alice@example.com" }
|
|
864
|
+
}
|
|
865
|
+
]
|
|
866
|
+
}
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
**Example:** "What sessions surfaced JIRA-99?"
|
|
870
|
+
|
|
871
|
+
> Note: For resolving an external key like `JIRA-99` to an Issue, see `testplanit_issues_find_by_key` (Phase 8 — ISSUE-01). The lookup tuple is `(externalKey, externalSystem, projectId)` to disambiguate when multiple integrations of the same provider exist (`Issue.externalKey` is not globally unique — schema enforces only `@@unique([externalId, integrationId])`).
|
|
872
|
+
|
|
873
|
+
### Code Repositories
|
|
874
|
+
|
|
875
|
+
#### `testplanit_code_repositories_list` (Phase 8 — REPO-01)
|
|
876
|
+
|
|
877
|
+
List the project's code-repository configuration. Returns `ProjectCodeRepositoryConfig` rows with the underlying `CodeRepository` denormalized inline. The schema enforces `@@unique([projectId])` so the response carries 0 or 1 row today; cursor pagination is shape-compatible with future multi-config relaxation.
|
|
878
|
+
|
|
879
|
+
**Input:**
|
|
880
|
+
```json
|
|
881
|
+
{ "projectId": 1, "cursor": 100, "limit": 25 }
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
**Output:**
|
|
885
|
+
```json
|
|
886
|
+
{
|
|
887
|
+
"items": [
|
|
888
|
+
{
|
|
889
|
+
"id": 1,
|
|
890
|
+
"projectId": 42,
|
|
891
|
+
"branch": "main",
|
|
892
|
+
"pathPatterns": [{ "path": "src/**", "pattern": "*.test.ts" }],
|
|
893
|
+
"cacheEnabled": true,
|
|
894
|
+
"cacheTtlDays": 7,
|
|
895
|
+
"cacheStatus": "READY",
|
|
896
|
+
"cacheLastFetchedAt": "2026-05-01T12:00:00Z",
|
|
897
|
+
"cacheFileCount": 1234,
|
|
898
|
+
"cacheTotalSize": 5678901,
|
|
899
|
+
"cacheError": null,
|
|
900
|
+
"repository": {
|
|
901
|
+
"id": 5,
|
|
902
|
+
"name": "acme/tools",
|
|
903
|
+
"provider": "GITHUB",
|
|
904
|
+
"status": "ACTIVE",
|
|
905
|
+
"lastTestedAt": "2026-04-30T08:00:00Z",
|
|
906
|
+
"settings": { "owner": "acme", "repo": "tools" },
|
|
907
|
+
"url": "https://github.com/acme/tools"
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
],
|
|
911
|
+
"hasNextPage": false,
|
|
912
|
+
"nextCursor": null
|
|
913
|
+
}
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
> **Security (T-08-CRED-LEAK):** the underlying `credentials` column is never selected (defense-in-depth #1 — TS2353 at compile time if reintroduced). The wholesale `settings` JSON is stripped to a per-provider public-key allow-list at the mapper boundary (defense-in-depth #2): `GITHUB ["owner","repo"]`, `GITLAB / BITBUCKET / GITEA ["baseUrl","owner","repo"]`, `AZURE_DEVOPS ["organizationUrl","project","repositoryId"]`. The derived web `url` follows each provider's canonical pattern; trailing slashes sanitized.
|
|
917
|
+
|
|
918
|
+
### Issues
|
|
919
|
+
|
|
920
|
+
#### `testplanit_issues_find_by_key` (Phase 8 — ISSUE-01)
|
|
921
|
+
|
|
922
|
+
Resolve `(externalKey, externalSystem, projectId)` to an Issue. The schema enforces only `@@unique([externalId, integrationId])` — `externalKey` is NOT globally unique when multiple integrations of the same provider exist in the same project. The tool's input tuple matches how integrations are configured in practice and how an agent already disambiguates project context.
|
|
923
|
+
|
|
924
|
+
**Input:**
|
|
925
|
+
```json
|
|
926
|
+
{
|
|
927
|
+
"externalKey": "JIRA-123",
|
|
928
|
+
"externalSystem": "JIRA",
|
|
929
|
+
"projectId": 42,
|
|
930
|
+
"integrationId": 7
|
|
931
|
+
}
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
- `externalSystem` — `IntegrationProvider` enum: `JIRA | GITHUB | AZURE_DEVOPS | SIMPLE_URL`. Internal-only issues (`Issue.integrationId IS NULL`) are NOT addressable here — agents reach those via `testplanit_issues_list`.
|
|
935
|
+
- `integrationId` (optional) — narrows to that integration directly and skips multi-match logic.
|
|
936
|
+
|
|
937
|
+
**Output (single match):**
|
|
938
|
+
```json
|
|
939
|
+
{
|
|
940
|
+
"issue": {
|
|
941
|
+
"id": 99,
|
|
942
|
+
"externalKey": "JIRA-123",
|
|
943
|
+
"externalSystem": "JIRA",
|
|
944
|
+
"externalUrl": "https://acme.atlassian.net/browse/JIRA-123",
|
|
945
|
+
"externalStatus": "Open",
|
|
946
|
+
"summary": "Login fails on Safari",
|
|
947
|
+
"status": "open",
|
|
948
|
+
"projectId": 42,
|
|
949
|
+
"integration": { "id": 7, "name": "Acme Jira", "provider": "JIRA" },
|
|
950
|
+
"createdBy": { "id": "user-1", "name": "Alice", "email": "alice@example.com" },
|
|
951
|
+
"createdAt": "2026-01-01T00:00:00Z",
|
|
952
|
+
"lastSyncedAt": "2026-05-01T12:00:00Z",
|
|
953
|
+
"linkedCaseCount": 6
|
|
954
|
+
},
|
|
955
|
+
"multipleMatches": false
|
|
956
|
+
}
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
**Output (multi-match fallback):** when `findMany` returns more than one row (rare — happens when two integrations of the same provider share an external key in the same project), the response shape switches:
|
|
960
|
+
```json
|
|
961
|
+
{
|
|
962
|
+
"issues": [ /* up to 5 issues; same shape as `issue` above */ ],
|
|
963
|
+
"multipleMatches": true,
|
|
964
|
+
"hint": "Pass integrationId to disambiguate."
|
|
965
|
+
}
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
#### `testplanit_issues_list` (Phase 8 — ISSUE-02)
|
|
969
|
+
|
|
970
|
+
List issues scoped to a project, with cursor-based pagination and deterministic `[{createdAt:'desc'},{id:'desc'}]` ordering. Each row carries `linkedCaseCount` inline (the dominant fan-out — median 6, p95 35, max 1061 in dev DB) so agents can rank issues by reach without a follow-up.
|
|
971
|
+
|
|
972
|
+
**Input:**
|
|
973
|
+
```json
|
|
974
|
+
{
|
|
975
|
+
"projectId": 42,
|
|
976
|
+
"externalSystem": "JIRA",
|
|
977
|
+
"integrationId": 7,
|
|
978
|
+
"status": "open",
|
|
979
|
+
"externalStatus": "In Progress",
|
|
980
|
+
"cursor": 100,
|
|
981
|
+
"limit": 25
|
|
982
|
+
}
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
> **Note:** an `assignee` filter is intentionally absent — `Issue` has no native assignee column; assignee data lives in `Issue.data: Json` (provider-shaped). Filtering on a JSON path would be brittle and per-provider; deferred until a real ask.
|
|
986
|
+
|
|
987
|
+
**Output:** `{ items, hasNextPage, nextCursor }` — items match the `issue` shape from `testplanit_issues_find_by_key`.
|
|
988
|
+
|
|
989
|
+
#### `testplanit_issues_get` (Phase 8 — ISSUE-03)
|
|
990
|
+
|
|
991
|
+
Fetch a single issue header plus three denormalized linked-row arrays inline. Mirrors Phase 7's D7-12 / D7-13 inline-with-truncation pattern.
|
|
992
|
+
|
|
993
|
+
**Input:**
|
|
994
|
+
```json
|
|
995
|
+
{ "issueId": 99 }
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
**Output:**
|
|
999
|
+
```json
|
|
1000
|
+
{
|
|
1001
|
+
"id": 99,
|
|
1002
|
+
"externalKey": "JIRA-123",
|
|
1003
|
+
"summary": "Login fails on Safari",
|
|
1004
|
+
"description": "Steps to repro: ...",
|
|
1005
|
+
"priority": "high",
|
|
1006
|
+
"issueTypeName": "Bug",
|
|
1007
|
+
"issueTypeIconUrl": "https://...",
|
|
1008
|
+
"note": "Reproduced on Safari 17.",
|
|
1009
|
+
"status": "open",
|
|
1010
|
+
"externalStatus": "In Progress",
|
|
1011
|
+
"linkedCases": [
|
|
1012
|
+
{ "id": 7, "name": "Login flow", "source": "MANUAL", "automated": false, "latestResult": null }
|
|
1013
|
+
],
|
|
1014
|
+
"linkedSessions": [
|
|
1015
|
+
{ "id": 12, "name": "Login flow exploration", "mission": "Try edge cases.", "isCompleted": false, "state": { "id": 1, "name": "Draft" } }
|
|
1016
|
+
],
|
|
1017
|
+
"linkedTestRuns": [
|
|
1018
|
+
{ "id": 200, "name": "Smoke run", "isCompleted": true, "completedAt": "2026-05-01T12:00:00Z" }
|
|
1019
|
+
],
|
|
1020
|
+
"truncated": { }
|
|
1021
|
+
}
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
> Each inline array is capped at 100 rows. When overflow occurs the response stamps `truncated.linkedCases: true` (or `linkedSessions` / `linkedTestRuns` as appropriate); the rest are reachable via `testplanit_cases_list({issueId})` and `testplanit_issues_list_links({issueId, target})`. `linkedSessionResults`, `linkedTestRunResults`, and `linkedTestRunStepResults` are NOT inlined (small junctions; agents reach them via the link tool).
|
|
1025
|
+
|
|
1026
|
+
#### `testplanit_issues_list_links` (Phase 8 — ISSUE-04)
|
|
1027
|
+
|
|
1028
|
+
Single dual-mode XOR tool covering all six `Issue` M:N junctions. Mirrors Phase 7's D7-11 dual-mode pattern (sessions findings).
|
|
1029
|
+
|
|
1030
|
+
- **Outbound mode** — given an `issueId` plus a `target`, returns the linked counterparts denormalized.
|
|
1031
|
+
- **Inbound mode** — given exactly one of `caseId | sessionId | sessionResultId | runId | runResultId | runStepResultId`, returns the linked Issue rows.
|
|
1032
|
+
|
|
1033
|
+
**Input (outbound):**
|
|
1034
|
+
```json
|
|
1035
|
+
{ "issueId": 99, "target": "cases", "cursor": 100, "limit": 25 }
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
- `target`: `cases | sessions | sessionResults | testRuns | testRunResults | testRunStepResults` (one of six).
|
|
1039
|
+
|
|
1040
|
+
**Input (inbound — exactly ONE inbound ID; the others MUST be omitted):**
|
|
1041
|
+
```json
|
|
1042
|
+
{ "caseId": 7 }
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
**Output:** `{ items, hasNextPage, nextCursor }` — items are typed per the queried direction.
|
|
1046
|
+
|
|
1047
|
+
> **Why one tool not six:** Phase 7's `sessions_findings_list` already proved the dual-mode shape composes cleanly. Six small tools would push the catalog past 30 and bury the mental model ("I'm walking the issue ↔ X graph") under ceremony.
|
|
1048
|
+
|
|
1049
|
+
#### `testplanit_issues_link`
|
|
1050
|
+
|
|
1051
|
+
Link one or more entities to an issue in a single call. Batch-capable: pass up to 100 entity IDs.
|
|
1052
|
+
|
|
1053
|
+
**Input:**
|
|
1054
|
+
```json
|
|
1055
|
+
{ "issueId": 7978, "entityType": "testCase", "entityIds": [201, 202, 203] }
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
- `entityType`: `testCase | session | testRun | testRunResult | testRunStepResult`
|
|
1059
|
+
- `entityIds`: array of 1–100 entity IDs
|
|
1060
|
+
|
|
1061
|
+
**Output:** `{ linked, issueId, entityType, entityIds }`
|
|
1062
|
+
|
|
1063
|
+
#### `testplanit_issues_unlink`
|
|
1064
|
+
|
|
1065
|
+
Remove links between entities and an issue. Same shape as `issues_link`.
|
|
1066
|
+
|
|
1067
|
+
**Input:**
|
|
1068
|
+
```json
|
|
1069
|
+
{ "issueId": 7978, "entityType": "testCase", "entityIds": [201] }
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
**Output:** `{ unlinked, issueId, entityType, entityIds }`
|
|
1073
|
+
|
|
1074
|
+
### Repository Case Links
|
|
1075
|
+
|
|
1076
|
+
#### `testplanit_repository_case_links_list` (Phase 8 — REPO-05)
|
|
1077
|
+
|
|
1078
|
+
Traverse the manual-↔-imported case linkage graph. Three input modes (3-way XOR; exactly one ID supplied):
|
|
1079
|
+
|
|
1080
|
+
- `caseId` — bidirectional, matches both endpoints via `OR=[{caseAId},{caseBId}]`.
|
|
1081
|
+
- `caseAId` — one-way, originating side.
|
|
1082
|
+
- `caseBId` — one-way, destination side.
|
|
1083
|
+
|
|
1084
|
+
Optional `linkType` narrows to `SAME_TEST_DIFFERENT_SOURCE` or `DEPENDS_ON`. The response shape varies by mode: `caseId` collapses each row to an inline `otherCase` (the side opposite the queried id); `caseAId` / `caseBId` modes preserve both `caseA` and `caseB`.
|
|
1085
|
+
|
|
1086
|
+
**Input:**
|
|
1087
|
+
```json
|
|
1088
|
+
{ "caseId": 7, "linkType": "SAME_TEST_DIFFERENT_SOURCE", "cursor": 100, "limit": 25 }
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
**Output (caseId mode):**
|
|
1092
|
+
```json
|
|
1093
|
+
{
|
|
1094
|
+
"items": [
|
|
1095
|
+
{
|
|
1096
|
+
"id": 31,
|
|
1097
|
+
"type": "SAME_TEST_DIFFERENT_SOURCE",
|
|
1098
|
+
"createdAt": "2026-01-01T00:00:00Z",
|
|
1099
|
+
"createdBy": { "id": "user-1", "name": "Alice", "email": "alice@example.com" },
|
|
1100
|
+
"otherCase": { "id": 9, "name": "automated_login_test", "source": "JUNIT", "automated": true }
|
|
1101
|
+
}
|
|
1102
|
+
],
|
|
1103
|
+
"hasNextPage": false,
|
|
1104
|
+
"nextCursor": null
|
|
1105
|
+
}
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
> Project scope is enforced transitively by the host's access policy on `caseA.project` — `RepositoryCaseLink` itself is not project-scoped, so the tool deliberately exposes no project-id input.
|
|
1109
|
+
|
|
1110
|
+
### Milestones
|
|
1111
|
+
|
|
1112
|
+
#### `testplanit_milestones_list`
|
|
1113
|
+
|
|
1114
|
+
List milestones scoped to a project, with **pooled `statusCounts` rollup** inline on every row (merged across linked test runs AND linked sessions). Single call answers *"How much work is left in milestone X."*
|
|
1115
|
+
|
|
1116
|
+
**Inputs:** `projectId` (required), `isCompleted?`, `isStarted?`, `milestoneTypeId?`, `createdById?` (single string), `from?` / `to?` (ISO 8601 createdAt range), `parentId?` (`null` = root-only, `number` = direct children of, omitted = all), `cursor?`, `limit?` (default 25, max 100).
|
|
1117
|
+
|
|
1118
|
+
**Each row carries:** `id`, `name`, `milestoneType: {id, name}`, `creator: {id, name, email}`, `parentId`, `directChildrenCount`, `commentCount`, `totalDescendants` (recursive CTE — counts the full subtree), `statusCounts: [{id, name, count}]`, `untested`, `total` (counts SUM to total), plus `isStarted`, `isCompleted`, `automaticCompletion`, `startedAt`, `completedAt`, `createdAt`.
|
|
1119
|
+
|
|
1120
|
+
**Cost model:** at most 5 backend round trips per page — one `milestones.findMany`, two batched `groupBy` calls (`testRunCases` + `sessionResults`; either skipped if the page has no runs or no sessions), one `status.findMany` for the status names, and one batched recursive-CTE call to `/api/mcp/milestones-descendants` for the page's `totalDescendants`. NEVER per-row.
|
|
1121
|
+
|
|
1122
|
+
#### `testplanit_milestones_get`
|
|
1123
|
+
|
|
1124
|
+
Fetch a single Milestone by id. Returns the full denormalized header + `note` and `docs` rendered to plain text (ProseMirror walked) + three inlined linked arrays:
|
|
1125
|
+
|
|
1126
|
+
- `linkedTestRuns` (cap **250** rows — wider than the standard 100 because milestones legitimately carry hundreds of runs; this is the dominant fan-out)
|
|
1127
|
+
- `linkedSessions` (cap 100)
|
|
1128
|
+
- `children` (cap 100, **1-level deep only**; each child carries `totalDescendants` so agents can prioritize which subtree to walk first)
|
|
1129
|
+
|
|
1130
|
+
When an array is over capacity the response carries `truncated.<key>: true`. The pooled `statusCounts` rollup at the milestone level is included; per-run rollups are reachable via `testplanit_test_runs_get`.
|
|
1131
|
+
|
|
1132
|
+
#### `testplanit_milestone_types_list`
|
|
1133
|
+
|
|
1134
|
+
List the milestone types assigned to a project (via the `MilestoneTypesAssignment` junction). Returns `{ items: [{id, name, isDefault}] }` ordered by name. No cursor pagination — types-per-project is small. Every `milestones_list` row + `milestones_get` response also denormalizes `milestoneType: {id, name}` inline, so this tool exists for full-catalog and filter-picker use cases.
|
|
1135
|
+
|
|
1136
|
+
#### `testplanit_milestones_create`
|
|
1137
|
+
|
|
1138
|
+
Create a new milestone. Use `testplanit_milestone_types_list` to enumerate valid `milestoneTypeId` values.
|
|
1139
|
+
|
|
1140
|
+
**Input:**
|
|
1141
|
+
```json
|
|
1142
|
+
{
|
|
1143
|
+
"projectId": 1,
|
|
1144
|
+
"name": "v2.0 Release",
|
|
1145
|
+
"milestoneTypeId": 3,
|
|
1146
|
+
"parentId": 5,
|
|
1147
|
+
"note": "Target: end of Q3."
|
|
1148
|
+
}
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
- `milestoneTypeId` — required; use `testplanit_milestone_types_list` to enumerate.
|
|
1152
|
+
- `parentId` — optional; omit for a top-level milestone.
|
|
1153
|
+
- `note` — optional plain text.
|
|
1154
|
+
|
|
1155
|
+
**Output:**
|
|
1156
|
+
```json
|
|
1157
|
+
{
|
|
1158
|
+
"id": 42,
|
|
1159
|
+
"name": "v2.0 Release",
|
|
1160
|
+
"isStarted": false,
|
|
1161
|
+
"isCompleted": false,
|
|
1162
|
+
"createdAt": "2026-05-07T00:00:00Z",
|
|
1163
|
+
"milestoneType": { "id": 3, "name": "Release" },
|
|
1164
|
+
"creator": { "id": "user-1", "name": "Alice", "email": "alice@example.com" },
|
|
1165
|
+
"parent": { "id": 5, "name": "Q3 Goals" },
|
|
1166
|
+
"note": "Target: end of Q3."
|
|
1167
|
+
}
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
#### `testplanit_milestones_update`
|
|
1171
|
+
|
|
1172
|
+
Update an existing milestone. Pass `note: null` or `parentId: null` to clear those fields. Setting `isStarted: true` records `startedAt`; setting `isCompleted: true` records `completedAt`.
|
|
1173
|
+
|
|
1174
|
+
**Input:**
|
|
1175
|
+
```json
|
|
1176
|
+
{
|
|
1177
|
+
"milestoneId": 42,
|
|
1178
|
+
"name": "v2.0 Release (revised)",
|
|
1179
|
+
"note": null,
|
|
1180
|
+
"milestoneTypeId": 4,
|
|
1181
|
+
"parentId": null,
|
|
1182
|
+
"isStarted": true,
|
|
1183
|
+
"isCompleted": false
|
|
1184
|
+
}
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
**Output:** Same shape as `testplanit_milestones_create`.
|
|
1188
|
+
|
|
1189
|
+
## Killer-app compositions (Phase 8)
|
|
1190
|
+
|
|
1191
|
+
### Issue → linked test cases (2 calls)
|
|
1192
|
+
|
|
1193
|
+
```json
|
|
1194
|
+
{ "tool": "testplanit_issues_find_by_key",
|
|
1195
|
+
"input": { "externalKey": "JIRA-123", "externalSystem": "JIRA", "projectId": 42 } }
|
|
1196
|
+
// → { issue: { id: 99, ... }, multipleMatches: false }
|
|
1197
|
+
|
|
1198
|
+
{ "tool": "testplanit_cases_list", "input": { "projectId": 42, "issueId": 99 } }
|
|
1199
|
+
// → { items: [{ id: 7, name: "Login flow", lastUpdatedAt: "...", latestResult: {...} }, ...] }
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
### Stale automated tests (1 call)
|
|
1203
|
+
|
|
1204
|
+
```json
|
|
1205
|
+
{ "tool": "testplanit_cases_list",
|
|
1206
|
+
"input": { "projectId": 42, "automated": true, "staleSinceUpdate": true } }
|
|
1207
|
+
// → each row carries lastUpdatedAt + latestResult; envelope.truncated:true when scan cap (400) hit.
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
### Recently-updated automated scripts (1 call)
|
|
1211
|
+
|
|
1212
|
+
```json
|
|
1213
|
+
{ "tool": "testplanit_cases_list",
|
|
1214
|
+
"input": { "projectId": 42, "automated": true, "updatedAfter": "2026-04-01T00:00:00Z" } }
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
### Manual ↔ imported case graph walk (1 call)
|
|
1218
|
+
|
|
1219
|
+
```json
|
|
1220
|
+
{ "tool": "testplanit_repository_case_links_list",
|
|
1221
|
+
"input": { "caseId": 7, "linkType": "SAME_TEST_DIFFERENT_SOURCE" } }
|
|
1222
|
+
// → each row's otherCase carries the counterpart denormalized.
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
### Never-run scripts (1 call)
|
|
1226
|
+
|
|
1227
|
+
```json
|
|
1228
|
+
{ "tool": "testplanit_cases_list",
|
|
1229
|
+
"input": { "projectId": 42, "automated": true, "hasNeverExecuted": true } }
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
### Milestone progress overview (1 call)
|
|
1233
|
+
|
|
1234
|
+
`testplanit_milestones_list({ projectId, isCompleted: false })` returns every open milestone for a project with pooled `statusCounts + untested + total` inline (merged across linked test runs AND sessions). An agent answers *"How much work is left in each open milestone?"* in a single round trip without any follow-up `_get` calls.
|
|
1235
|
+
|
|
1236
|
+
```json
|
|
1237
|
+
{ "tool": "testplanit_milestones_list",
|
|
1238
|
+
"input": { "projectId": 42, "isCompleted": false } }
|
|
1239
|
+
// → each row: { name, statusCounts:[{name,count}], untested, total, totalDescendants, ... }
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
### Create a run, add cases, and execute (3 calls)
|
|
1243
|
+
|
|
1244
|
+
Chain `testplanit_runs_create` → `testplanit_runs_cases_add` (if cases were not supplied at creation) → `testplanit_test_run_results_create` per case.
|
|
1245
|
+
|
|
1246
|
+
```json
|
|
1247
|
+
{ "tool": "testplanit_runs_create",
|
|
1248
|
+
"input": { "projectId": 42, "name": "JIRA-892 coverage run", "caseIds": [99, 100, 101] } }
|
|
1249
|
+
// → full run detail; total: 3, untested: 3
|
|
1250
|
+
|
|
1251
|
+
{ "tool": "testplanit_runs_cases_add",
|
|
1252
|
+
"input": { "runId": 5, "caseIds": [102, 103] } }
|
|
1253
|
+
// → { runId: 5, requested: 2, total: 5 }
|
|
1254
|
+
|
|
1255
|
+
{ "tool": "testplanit_test_run_results_create",
|
|
1256
|
+
"input": { "testRunCaseId": 100, "statusName": "Passed", "elapsed": 320 } }
|
|
1257
|
+
// → full result detail; attempt: 1, status: { name: "Passed" }
|
|
1258
|
+
```
|
|
1259
|
+
|
|
1260
|
+
## Soft-Delete Invariant
|
|
1261
|
+
|
|
1262
|
+
All TestPlanIt MCP "delete" tools perform soft-delete: they set `isDeleted: true` via PATCH update and never call the underlying ZenStack `delete` operation. Soft-deleted records remain in the database for audit purposes and are hidden from subsequent list/get tool calls.
|
|
1263
|
+
|
|
1264
|
+
## Read-Only Tokens
|
|
1265
|
+
|
|
1266
|
+
Tokens minted with the `mode:read` scope are blocked at the host on POST/PATCH/DELETE — including all Phase 6 write tools. The MCP layer surfaces a structured error message naming the `mode:read` scope. See [Token scopes](#token-scopes) for minting steps.
|
|
1267
|
+
|
|
1268
|
+
## Tool registry growth
|
|
1269
|
+
|
|
1270
|
+
The registry has grown additively over multiple releases:
|
|
1271
|
+
|
|
1272
|
+
- `whoami` — initial debug tool.
|
|
1273
|
+
- Cases / folders / tags / projects domain — 12 tools.
|
|
1274
|
+
- Test-run + session + findings read domain — 10 tools, plus an additive `issueId` filter on `testplanit_cases_list`.
|
|
1275
|
+
- Code-repositories / issues / repository-case-links read domain — 6 tools (`testplanit_code_repositories_list`, `testplanit_issues_find_by_key`, `testplanit_issues_list`, `testplanit_issues_get`, `testplanit_issues_list_links`, `testplanit_repository_case_links_list`), plus 7 maintenance filters + 2 row fields on `testplanit_cases_list` and inline `codeRepository` on `testplanit_cases_get`.
|
|
1276
|
+
- Milestones domain — 3 read tools (`testplanit_milestones_list`, `testplanit_milestones_get`, `testplanit_milestone_types_list`), plus 3 additive filters on `testplanit_cases_list` (`creatorIds`, `from`, `to`).
|
|
1277
|
+
- Issue link write domain — 2 write tools (`testplanit_issues_link`, `testplanit_issues_unlink`): batch-link / unlink any entity type to an issue in one call.
|
|
1278
|
+
- Run / session / milestone write domain — 8 write tools added: `testplanit_runs_create`, `testplanit_runs_update`, `testplanit_runs_cases_add`, `testplanit_test_run_results_create`, `testplanit_sessions_create`, `testplanit_sessions_update`, `testplanit_milestones_create`, `testplanit_milestones_update`.
|
|
1279
|
+
|
|
1280
|
+
Total registered tools: **42** (matches the count at the top of this catalog).
|
|
1281
|
+
|
|
1282
|
+
## Claude Desktop configuration
|
|
1283
|
+
|
|
1284
|
+
Add the server to your `claude_desktop_config.json`:
|
|
1285
|
+
|
|
1286
|
+
```json
|
|
1287
|
+
{
|
|
1288
|
+
"mcpServers": {
|
|
1289
|
+
"testplanit": {
|
|
1290
|
+
"command": "npx",
|
|
1291
|
+
"args": ["-y", "@testplanit/mcp-server"],
|
|
1292
|
+
"env": {
|
|
1293
|
+
"TESTPLANIT_API_TOKEN": "tpi_your_token_here",
|
|
1294
|
+
"TESTPLANIT_API_URL": "https://yourcompany.testplanit.com"
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
Restart Claude Desktop after editing the config. The TestPlanIt server should appear in the MCP servers list. Send a message asking Claude to "use the testplanit whoami tool" to verify the wiring.
|
|
1302
|
+
|
|
1303
|
+
## Cursor configuration
|
|
1304
|
+
|
|
1305
|
+
Add the server to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project-scoped):
|
|
1306
|
+
|
|
1307
|
+
```json
|
|
1308
|
+
{
|
|
1309
|
+
"mcpServers": {
|
|
1310
|
+
"testplanit": {
|
|
1311
|
+
"type": "stdio",
|
|
1312
|
+
"command": "npx",
|
|
1313
|
+
"args": ["-y", "@testplanit/mcp-server"],
|
|
1314
|
+
"env": {
|
|
1315
|
+
"TESTPLANIT_API_TOKEN": "tpi_your_token_here",
|
|
1316
|
+
"TESTPLANIT_API_URL": "https://yourcompany.testplanit.com"
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
Restart Cursor after editing. If you prefer to pull the token from your shell environment rather than hardcoding it, Cursor supports interpolation: `"TESTPLANIT_API_TOKEN": "${env:TESTPLANIT_API_TOKEN}"`.
|
|
1324
|
+
|
|
1325
|
+
## Diagnostics
|
|
1326
|
+
|
|
1327
|
+
- All diagnostic output is written to **stderr**. Stdout is reserved for the JSON-RPC stream — never write to it.
|
|
1328
|
+
- On token-validation failure, a clear error is written to stderr and the process exits with code 1 **before** the agent expects a handshake response. Check the host client's MCP logs for the stderr text.
|
|
1329
|
+
- Token strings are redacted to the first 8 characters (`tpi_xxxx`) in any error message — the full secret is never logged.
|
|
1330
|
+
|
|
1331
|
+
## Security notes
|
|
1332
|
+
|
|
1333
|
+
- Published with [npm provenance attestation](https://docs.npmjs.com/generating-provenance-statements). Verify the published artifact's chain of custody with:
|
|
1334
|
+
```sh
|
|
1335
|
+
npm audit signatures @testplanit/mcp-server
|
|
1336
|
+
```
|
|
1337
|
+
- The package's `publishConfig` locks `provenance: true` and `access: "public"` at the source. The release workflow in this repo declares `id-token: write` so npm can attest the build.
|
|
1338
|
+
- Read-only enforcement (`mode:read`) is verified end-to-end via Playwright in the host repo (`testplanit/e2e/tests/api-tokens/scopes.spec.ts`) — the chokepoint is shared with the REST API, so any client (browser, MCP, custom integration) hits the same gate.
|
|
1339
|
+
|
|
1340
|
+
## Roadmap
|
|
1341
|
+
|
|
1342
|
+
Phase 6+ adds production tools for the test-case, execution, and repository domains. Each new tool plugs into the same registry pattern (`tools/index.ts → registerAll`) without touching `server.ts`. The error-mapping seam (`mapHttpErrorToToolResult`) is already shared, so write tools that hit a `READ_ONLY_TOKEN` 403 surface the same friendly message Phase 5's `whoami` would.
|
|
1343
|
+
|
|
1344
|
+
## License
|
|
1345
|
+
|
|
1346
|
+
MIT
|