@smartruns/mcp 1.0.0 → 1.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/README.md +3 -2
- package/dist/tools/ai.js +1 -7
- package/dist/tools/comments.js +4 -5
- package/dist/tools/defects.js +1 -7
- package/dist/tools/handle-error.js +44 -0
- package/dist/tools/labels.js +16 -5
- package/dist/tools/notifications.js +1 -5
- package/dist/tools/projects.js +1 -7
- package/dist/tools/reference-data.js +1 -5
- package/dist/tools/specs.js +55 -39
- package/dist/tools/statuses.js +1 -7
- package/dist/tools/test-plans.js +1 -7
- package/dist/tools/test-runs.js +21 -9
- package/dist/tools/test-suites.js +1 -18
- package/dist/tools/tests.js +1 -7
- package/dist/tools/users.js +1 -7
- package/dist/tools/watchers.js +4 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ The workflow then builds `dist/`, runs the tests, **asserts the tag version equa
|
|
|
126
126
|
- The tag name and the manifest version must agree — the workflow refuses to publish a
|
|
127
127
|
mismatched tag.
|
|
128
128
|
|
|
129
|
-
## Available Tools (
|
|
129
|
+
## Available Tools (62)
|
|
130
130
|
|
|
131
131
|
> Tip: tools marked **project-scoped** accept the optional per-call `project_id` argument.
|
|
132
132
|
> Write tools on optimistically-locked entities require a `lock_version` taken from the most
|
|
@@ -208,12 +208,13 @@ The workflow then builds `dist/`, runs the tests, **asserts the tag version equa
|
|
|
208
208
|
|------|-------------|
|
|
209
209
|
| `list_statuses` | List all statuses grouped by scope (returns an object keyed by scope, not an array) |
|
|
210
210
|
|
|
211
|
-
### Labels (
|
|
211
|
+
### Labels (4)
|
|
212
212
|
| Tool | Description |
|
|
213
213
|
|------|-------------|
|
|
214
214
|
| `list_labels` | List labels in the current project (optional `terms` search) |
|
|
215
215
|
| `create_label` | Create a label (`name` only; duplicate names are allowed — no uniqueness validation) |
|
|
216
216
|
| `update_label` | Rename a label by ID — this is a rename only, NOT a merge |
|
|
217
|
+
| `delete_label` | Permanently delete a label by ID (IRREVERSIBLE; removes the label and its associations, not the tagged entities) |
|
|
217
218
|
|
|
218
219
|
### Watchers (2)
|
|
219
220
|
| Tool | Description |
|
package/dist/tools/ai.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
const e = err;
|
|
4
|
-
const msg = e.status === 409
|
|
5
|
-
? `Conflict (lock_version stale). Re-fetch the record and retry with the updated lock_version. API response: ${JSON.stringify(e.body)}`
|
|
6
|
-
: `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
7
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
8
|
-
}
|
|
2
|
+
import { handleError } from './handle-error.js';
|
|
9
3
|
// SR-387 (slice 9, BILLING-flagged). Every tool description in this file MUST begin
|
|
10
4
|
// with this warning so an external LLM treats the tool cautiously and a user knows the
|
|
11
5
|
// cost: invoking any of these tools generates content via the SmartRuns AI backend and
|
package/dist/tools/comments.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { handleError as sharedHandleError } from './handle-error.js';
|
|
2
3
|
function handleError(err) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
: `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
7
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
4
|
+
return sharedHandleError(err, {
|
|
5
|
+
403: (e) => `Forbidden (403) on ${e.method} ${e.path}: editing or deleting a comment may be restricted to its author, or your token lacks the required permission. API response: ${JSON.stringify(e.body)}`,
|
|
6
|
+
});
|
|
8
7
|
}
|
|
9
8
|
// Comments in SmartRuns are polymorphic but NOT via Rails-standard commentable_type /
|
|
10
9
|
// commentable_id, and NOT nested under a parent route. The CommentsController
|
package/dist/tools/defects.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
const e = err;
|
|
4
|
-
const msg = e.status === 409
|
|
5
|
-
? `Conflict (lock_version stale). Re-fetch the record and retry with the updated lock_version. API response: ${JSON.stringify(e.body)}`
|
|
6
|
-
: `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
7
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
8
|
-
}
|
|
2
|
+
import { handleError } from './handle-error.js';
|
|
9
3
|
export function registerDefectTools(server, client) {
|
|
10
4
|
server.tool('list_defects', 'List defects in the current project. At most ONE filter (ticket_id, pull_request_id, or test_run_id) may be specified at a time.', {
|
|
11
5
|
page: z.number().int().optional().describe('Page number for pagination'),
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Shared error formatter for all MCP tools.
|
|
2
|
+
//
|
|
3
|
+
// SR-399: previously every tool file carried its own `handleError` that mapped
|
|
4
|
+
// *every* HTTP 409 to a canned "Conflict (lock_version stale). Re-fetch and retry"
|
|
5
|
+
// message. That is wrong for the many non-lock 409s the API returns (e.g.
|
|
6
|
+
// "Couldn't find TestKind without an ID", "Assignee must exist", and other
|
|
7
|
+
// validation conflicts): it told the LLM/user to re-fetch and retry forever on
|
|
8
|
+
// errors that retrying can never resolve. SR-388 softened only test-suites.ts;
|
|
9
|
+
// this centralises the correct behaviour so it is consistent everywhere.
|
|
10
|
+
//
|
|
11
|
+
// Rule: the stale-lock wording appears ONLY when the response body actually
|
|
12
|
+
// indicates a stale optimistic-lock conflict (it carries a `lock_version` field,
|
|
13
|
+
// or its message text references `lock_version`). Any other 409 surfaces the
|
|
14
|
+
// server's real error body plainly, with no retry advice. Tools that need
|
|
15
|
+
// status-specific guidance (e.g. comments' 403, watchers' 422) pass `overrides`.
|
|
16
|
+
// True when the 409 body genuinely reflects a stale optimistic-lock conflict:
|
|
17
|
+
// either a structured `lock_version` field, or a message that references it.
|
|
18
|
+
export function indicatesStaleLock(body) {
|
|
19
|
+
if (typeof body === 'object' && body !== null && 'lock_version' in body) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return JSON.stringify(body).toLowerCase().includes('lock_version');
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function handleError(err, overrides = {}) {
|
|
30
|
+
const e = err;
|
|
31
|
+
let msg;
|
|
32
|
+
if (e.status !== undefined && overrides[e.status]) {
|
|
33
|
+
msg = overrides[e.status](e);
|
|
34
|
+
}
|
|
35
|
+
else if (e.status === 409) {
|
|
36
|
+
msg = indicatesStaleLock(e.body)
|
|
37
|
+
? `Conflict (409, stale lock_version) on ${e.method} ${e.path}. Re-fetch the record and retry with the updated lock_version. API response: ${JSON.stringify(e.body)}`
|
|
38
|
+
: `Conflict (409) on ${e.method} ${e.path}: the request conflicts with the current state (e.g. a validation conflict such as a missing/invalid reference), not a stale lock — do NOT blindly retry. API response: ${JSON.stringify(e.body)}`;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
msg = `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
42
|
+
}
|
|
43
|
+
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
44
|
+
}
|
package/dist/tools/labels.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { handleError } from './handle-error.js';
|
|
2
3
|
// NOTE: the Label model has NO uniqueness validation, so a duplicate name is NOT rejected
|
|
3
4
|
// (create/rename to an existing name succeeds with 200). Do not special-case 409 as a
|
|
4
5
|
// "name already taken" conflict — surface any error uniformly.
|
|
5
|
-
function handleError(err) {
|
|
6
|
-
const e = err;
|
|
7
|
-
const msg = `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
8
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
9
|
-
}
|
|
10
6
|
export function registerLabelTools(server, client) {
|
|
11
7
|
server.tool('list_labels', 'List labels in the current project, optionally filtered by search terms.', {
|
|
12
8
|
terms: z.string().optional().describe('Search terms to filter labels by name'),
|
|
@@ -51,4 +47,19 @@ export function registerLabelTools(server, client) {
|
|
|
51
47
|
return handleError(err);
|
|
52
48
|
}
|
|
53
49
|
});
|
|
50
|
+
// SR-399: DELETE /labels/:id is exposed by the backend (LabelsController#destroy,
|
|
51
|
+
// routes: `resources :labels, except: [:new, :edit, :show]`). Mirrors the other
|
|
52
|
+
// delete_* tools (client.delete forwards project_id to the WTProject header only).
|
|
53
|
+
server.tool('delete_label', 'Permanently delete a label by ID. This action is IRREVERSIBLE. The label is resolved tenant-scoped (account + project), so the server returns 404 if it does not exist in the current project. Deleting a label only removes the label itself and its associations to entities — it does not delete the tagged tests/plans. Returns { deleted: true, id } on success.', {
|
|
54
|
+
id: z.number().int().describe('Label ID to delete'),
|
|
55
|
+
project_id: z.string().optional().describe('Override the project for this call (WTProject header). Defaults to SMARTRUNS_PROJECT_ID.'),
|
|
56
|
+
}, async ({ id, project_id }) => {
|
|
57
|
+
try {
|
|
58
|
+
await client.delete(`/labels/${id}`, project_id);
|
|
59
|
+
return { content: [{ type: 'text', text: JSON.stringify({ deleted: true, id }, null, 2) }] };
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
return handleError(err);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
54
65
|
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
const e = err;
|
|
4
|
-
const msg = `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
5
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
6
|
-
}
|
|
2
|
+
import { handleError } from './handle-error.js';
|
|
7
3
|
// Notifications are USER-scoped, not project-scoped: NotificationsController#base_scope
|
|
8
4
|
// is Notification.for_current_account.where(user: Current.user) with no project filter.
|
|
9
5
|
// Therefore these tools intentionally do NOT expose a per-call project_id arg.
|
package/dist/tools/projects.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
const e = err;
|
|
4
|
-
const msg = e.status === 409
|
|
5
|
-
? `Conflict (lock_version stale). Re-fetch the record and retry with the updated lock_version. API response: ${JSON.stringify(e.body)}`
|
|
6
|
-
: `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
7
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
8
|
-
}
|
|
2
|
+
import { handleError } from './handle-error.js';
|
|
9
3
|
export function registerProjectTools(server, client) {
|
|
10
4
|
server.tool('list_projects', 'List all projects in the account. Optionally filter by archived status.', {
|
|
11
5
|
archived: z.boolean().optional().describe('Filter by archived status. Omit to return all projects.'),
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { handleError } from './handle-error.js';
|
|
2
3
|
// These are all GET-only reference lookups, so there is no optimistic-lock conflict to
|
|
3
4
|
// handle — a 409 lock_version branch would be dead code. Surface every error uniformly.
|
|
4
|
-
function handleError(err) {
|
|
5
|
-
const e = err;
|
|
6
|
-
const msg = `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
7
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
8
|
-
}
|
|
9
5
|
// Shared per-call project override. Every reference list below is project-scoped on the
|
|
10
6
|
// backend (`for_current_account.for_current_project`), so each accepts an optional project_id
|
|
11
7
|
// that overrides the WTProject header for the call (SR-378 per-call override decision).
|
package/dist/tools/specs.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
const e = err;
|
|
4
|
-
const msg = e.status === 409
|
|
5
|
-
? `Conflict (lock_version stale). Re-fetch the record and retry with the updated lock_version. API response: ${JSON.stringify(e.body)}`
|
|
6
|
-
: `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
7
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
8
|
-
}
|
|
2
|
+
import { handleError } from './handle-error.js';
|
|
9
3
|
// SR-382 (slice 3) introduces this file with ONLY delete_spec. SR-384 (slice 6) adds
|
|
10
4
|
// the rest of the specs toolset (list / get / create / update / history) to this same
|
|
11
5
|
// file — keep additions additive and do not duplicate the file.
|
|
@@ -19,13 +13,19 @@ function handleError(err) {
|
|
|
19
13
|
// PUT /specs/:id (update → spec_payload; PARTIAL via strong-params permit)
|
|
20
14
|
// GET /specs/:id/history (member → PLAIN ARRAY of AuditLog entries, newest-first)
|
|
21
15
|
// DELETE /specs/:id (destroy → { deleted: true })
|
|
22
|
-
// Strong params (spec_params): title, description,
|
|
23
|
-
//
|
|
16
|
+
// Strong params (spec_params): title, description, due_date, problem_statement,
|
|
17
|
+
// out_of_scope, constraints, open_questions, decision_log, acceptance_criteria:
|
|
18
|
+
// [id, description, position, _destroy]. The controller also resolves, via tenant-scoped
|
|
19
|
+
// finders (not mass-assignment): status_id, assignee_id, product_area_id, and the
|
|
20
|
+
// set-semantics arrays reviewer_ids[], label_ids[], test_plan_ids[], test_run_ids[],
|
|
21
|
+
// defect_ids[]. It also reads params[:tickets]. `author` is server-set and never accepted.
|
|
22
|
+
// SR-413: status migrated from the draft/active/archived enum to a configurable Status
|
|
23
|
+
// record — send status_id (integer); the returned spec exposes `status` as a nested
|
|
24
|
+
// object { id, name, color, category } plus the scalar status_id.
|
|
24
25
|
// IMPORTANT: Spec is NOT optimistically locked — there is no lock_version column, so
|
|
25
26
|
// update_spec must NOT require or send one (unlike test-suites/test-runs). Because the
|
|
26
27
|
// controller uses params.permit + assign_attributes, omitted fields are left untouched,
|
|
27
28
|
// so update_spec is a true partial update.
|
|
28
|
-
const SPEC_STATUSES = ['draft', 'active', 'archived'];
|
|
29
29
|
// Acceptance criteria are nested records. On create supply { description, position };
|
|
30
30
|
// on update include { id } to edit an existing criterion or { id, _destroy: true } to
|
|
31
31
|
// remove one. Shape mirrors the controller's permitted acceptance_criteria keys.
|
|
@@ -36,7 +36,7 @@ const acceptanceCriterionShape = z.object({
|
|
|
36
36
|
_destroy: z.boolean().optional().describe('Set true to delete this existing criterion (requires id)'),
|
|
37
37
|
});
|
|
38
38
|
export function registerSpecTools(server, client) {
|
|
39
|
-
server.tool('list_specs', 'List specs (spec-driven-development entities) in the current project, newest first. Returns a plain array of specs, each with
|
|
39
|
+
server.tool('list_specs', 'List specs (spec-driven-development entities) in the current project, newest first. Returns a plain array of specs, each with status (nested object), author, assignee (safe user fields), due_date, product_area, labels and acceptance_criteria_count. NOTE: Specs are feature-gated (SR-371); if the flag is off for the account the API may return 403/404, surfaced verbatim.', {
|
|
40
40
|
page: z.number().int().optional().describe('Page number for pagination (default 1)'),
|
|
41
41
|
per_page: z.number().int().optional().describe('Results per page (default 10)'),
|
|
42
42
|
project_id: z.string().optional().describe('Override the project for this call (WTProject header). Defaults to SMARTRUNS_PROJECT_ID.'),
|
|
@@ -54,7 +54,7 @@ export function registerSpecTools(server, client) {
|
|
|
54
54
|
return handleError(err);
|
|
55
55
|
}
|
|
56
56
|
});
|
|
57
|
-
server.tool('get_spec', 'Get a single spec by ID, including its acceptance_criteria, linked Jira tickets, attachments, linked_tests and coverage roll-up.', {
|
|
57
|
+
server.tool('get_spec', 'Get a single spec by ID, including its acceptance_criteria, status (nested object), author/assignee/reviewers (safe user fields), due_date, product_area, labels, narrative fields (problem_statement, out_of_scope, constraints, open_questions, decision_log), linked Jira tickets, linked_test_plans, linked_test_runs, linked_defects, attachments, linked_tests and coverage roll-up.', {
|
|
58
58
|
id: z.number().int().describe('Spec ID'),
|
|
59
59
|
project_id: z.string().optional().describe('Override the project for this call (WTProject header). Defaults to SMARTRUNS_PROJECT_ID.'),
|
|
60
60
|
}, async ({ id, project_id }) => {
|
|
@@ -66,24 +66,33 @@ export function registerSpecTools(server, client) {
|
|
|
66
66
|
return handleError(err);
|
|
67
67
|
}
|
|
68
68
|
});
|
|
69
|
-
server.tool('create_spec', 'Create a new spec in the current project. Only title is required; status defaults to
|
|
69
|
+
server.tool('create_spec', 'Create a new spec in the current project. Only title is required; status defaults to the project default spec status when status_id is omitted. The author is set automatically to the calling user. Optionally seed acceptance_criteria, narrative fields, ownership/scheduling, product area, labels, reviewers and links to test plans / test runs / defects.', {
|
|
70
70
|
title: z.string().describe('Spec title (required — the model validates its presence)'),
|
|
71
|
-
description: z.string().optional().describe('Spec description / body'),
|
|
72
|
-
|
|
71
|
+
description: z.string().optional().describe('Spec description / body (markdown)'),
|
|
72
|
+
status_id: z.number().int().optional().describe('Configurable Status id (scope: spec). Omit to use the project default. Cross-tenant ids are ignored.'),
|
|
73
|
+
assignee_id: z.number().int().optional().describe('User id responsible for the spec (must belong to the current account)'),
|
|
74
|
+
due_date: z.string().optional().describe('Due date, ISO yyyy-mm-dd (date only, no time)'),
|
|
75
|
+
product_area_id: z.number().int().optional().describe('Product area id to classify the spec (current account/project)'),
|
|
76
|
+
problem_statement: z.string().optional().describe('Problem statement (markdown)'),
|
|
77
|
+
out_of_scope: z.string().optional().describe('Out of scope (markdown)'),
|
|
78
|
+
constraints: z.string().optional().describe('Constraints (markdown)'),
|
|
79
|
+
open_questions: z.string().optional().describe('Open questions (markdown)'),
|
|
80
|
+
decision_log: z.string().optional().describe('Decision log (markdown)'),
|
|
81
|
+
reviewer_ids: z.array(z.number().int()).optional().describe('User ids of reviewers (set semantics — replaces the full set; [] clears)'),
|
|
82
|
+
label_ids: z.array(z.number().int()).optional().describe('Label ids to tag the spec (set semantics; [] clears)'),
|
|
83
|
+
test_plan_ids: z.array(z.number().int()).optional().describe('Test plan ids to link (set semantics; [] clears)'),
|
|
84
|
+
test_run_ids: z.array(z.number().int()).optional().describe('Test run ids to link (set semantics; [] clears)'),
|
|
85
|
+
defect_ids: z.array(z.number().int()).optional().describe('Defect ids to link (set semantics; [] clears)'),
|
|
73
86
|
acceptance_criteria: z.array(acceptanceCriterionShape).optional().describe('Acceptance criteria to create with the spec, each { description, position }'),
|
|
74
87
|
tickets: z.array(z.record(z.string(), z.unknown())).optional().describe('Linked Jira tickets to associate (each e.g. { id, key, name, kind, source, status })'),
|
|
75
88
|
project_id: z.string().optional().describe('Override the project for this call (WTProject header). Defaults to SMARTRUNS_PROJECT_ID.'),
|
|
76
|
-
}, async ({
|
|
89
|
+
}, async ({ project_id, ...rest }) => {
|
|
77
90
|
try {
|
|
78
|
-
const body = {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (acceptance_criteria !== undefined)
|
|
84
|
-
body.acceptance_criteria = acceptance_criteria;
|
|
85
|
-
if (tickets !== undefined)
|
|
86
|
-
body.tickets = tickets;
|
|
91
|
+
const body = {};
|
|
92
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
93
|
+
if (value !== undefined)
|
|
94
|
+
body[key] = value;
|
|
95
|
+
}
|
|
87
96
|
const result = await client.post('/specs', body, project_id);
|
|
88
97
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
89
98
|
}
|
|
@@ -91,27 +100,34 @@ export function registerSpecTools(server, client) {
|
|
|
91
100
|
return handleError(err);
|
|
92
101
|
}
|
|
93
102
|
});
|
|
94
|
-
server.tool('update_spec', 'Update an existing spec. This is a PARTIAL update — only the fields you pass are changed; omitted fields are left untouched. Specs are NOT optimistically locked, so no lock_version is needed. To edit acceptance criteria include their { id }; to remove one pass { id, _destroy: true }.', {
|
|
103
|
+
server.tool('update_spec', 'Update an existing spec. This is a PARTIAL update — only the fields you pass are changed; omitted fields are left untouched. Specs are NOT optimistically locked, so no lock_version is needed. The *_ids arrays use set semantics (replace the full set; pass [] to clear). To edit acceptance criteria include their { id }; to remove one pass { id, _destroy: true }. The author cannot be changed.', {
|
|
95
104
|
id: z.number().int().describe('Spec ID'),
|
|
96
105
|
title: z.string().optional().describe('New spec title (omit to leave unchanged)'),
|
|
97
|
-
description: z.string().optional().describe('New spec description / body'),
|
|
98
|
-
|
|
106
|
+
description: z.string().optional().describe('New spec description / body (markdown)'),
|
|
107
|
+
status_id: z.number().int().optional().describe('New configurable Status id (scope: spec). Cross-tenant ids are ignored.'),
|
|
108
|
+
assignee_id: z.number().int().optional().describe('New assignee user id (current account). Pass null to clear via the API directly.'),
|
|
109
|
+
due_date: z.string().optional().describe('Due date, ISO yyyy-mm-dd (date only)'),
|
|
110
|
+
product_area_id: z.number().int().optional().describe('Product area id (current account/project)'),
|
|
111
|
+
problem_statement: z.string().optional().describe('Problem statement (markdown)'),
|
|
112
|
+
out_of_scope: z.string().optional().describe('Out of scope (markdown)'),
|
|
113
|
+
constraints: z.string().optional().describe('Constraints (markdown)'),
|
|
114
|
+
open_questions: z.string().optional().describe('Open questions (markdown)'),
|
|
115
|
+
decision_log: z.string().optional().describe('Decision log (markdown)'),
|
|
116
|
+
reviewer_ids: z.array(z.number().int()).optional().describe('Replacement set of reviewer user ids ([] clears)'),
|
|
117
|
+
label_ids: z.array(z.number().int()).optional().describe('Replacement set of label ids ([] clears)'),
|
|
118
|
+
test_plan_ids: z.array(z.number().int()).optional().describe('Replacement set of linked test plan ids ([] clears)'),
|
|
119
|
+
test_run_ids: z.array(z.number().int()).optional().describe('Replacement set of linked test run ids ([] clears)'),
|
|
120
|
+
defect_ids: z.array(z.number().int()).optional().describe('Replacement set of linked defect ids ([] clears)'),
|
|
99
121
|
acceptance_criteria: z.array(acceptanceCriterionShape).optional().describe('Acceptance criteria changes: new ones, edits (with id), or removals (id + _destroy)'),
|
|
100
122
|
tickets: z.array(z.record(z.string(), z.unknown())).optional().describe('Replacement set of linked Jira tickets'),
|
|
101
123
|
project_id: z.string().optional().describe('Override the project for this call (WTProject header). Defaults to SMARTRUNS_PROJECT_ID.'),
|
|
102
|
-
}, async ({ id,
|
|
124
|
+
}, async ({ id, project_id, ...rest }) => {
|
|
103
125
|
try {
|
|
104
126
|
const body = {};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (status !== undefined)
|
|
110
|
-
body.status = status;
|
|
111
|
-
if (acceptance_criteria !== undefined)
|
|
112
|
-
body.acceptance_criteria = acceptance_criteria;
|
|
113
|
-
if (tickets !== undefined)
|
|
114
|
-
body.tickets = tickets;
|
|
127
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
128
|
+
if (value !== undefined)
|
|
129
|
+
body[key] = value;
|
|
130
|
+
}
|
|
115
131
|
const result = await client.put(`/specs/${id}`, body, project_id);
|
|
116
132
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
117
133
|
}
|
package/dist/tools/statuses.js
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
const e = err;
|
|
3
|
-
const msg = e.status === 409
|
|
4
|
-
? `Conflict (lock_version stale). Re-fetch the record and retry with the updated lock_version. API response: ${JSON.stringify(e.body)}`
|
|
5
|
-
: `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
6
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
7
|
-
}
|
|
1
|
+
import { handleError } from './handle-error.js';
|
|
8
2
|
export function registerStatusTools(server, client) {
|
|
9
3
|
server.tool('list_statuses', 'List all available statuses grouped by scope (test plan, test run, defect, etc.). Returns an object keyed by scope name.', {}, async () => {
|
|
10
4
|
try {
|
package/dist/tools/test-plans.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
const e = err;
|
|
4
|
-
const msg = e.status === 409
|
|
5
|
-
? `Conflict (lock_version stale). Re-fetch the record and retry with the updated lock_version. API response: ${JSON.stringify(e.body)}`
|
|
6
|
-
: `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
7
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
8
|
-
}
|
|
2
|
+
import { handleError } from './handle-error.js';
|
|
9
3
|
export function registerTestPlanTools(server, client) {
|
|
10
4
|
server.tool('list_test_plans', 'List test plans in the current project with optional filters.', {
|
|
11
5
|
page: z.number().int().optional().describe('Page number for pagination'),
|
package/dist/tools/test-runs.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
const e = err;
|
|
4
|
-
const msg = e.status === 409
|
|
5
|
-
? `Conflict (lock_version stale). Re-fetch the record and retry with the updated lock_version. API response: ${JSON.stringify(e.body)}`
|
|
6
|
-
: `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
7
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
8
|
-
}
|
|
2
|
+
import { handleError } from './handle-error.js';
|
|
9
3
|
export function registerTestRunTools(server, client) {
|
|
10
4
|
server.tool('list_test_runs', 'List test runs in the current project with optional filters.', {
|
|
11
5
|
page: z.number().int().optional().describe('Page number for pagination'),
|
|
@@ -35,7 +29,16 @@ export function registerTestRunTools(server, client) {
|
|
|
35
29
|
return handleError(err);
|
|
36
30
|
}
|
|
37
31
|
});
|
|
38
|
-
server.tool('create_test_run',
|
|
32
|
+
server.tool('create_test_run', [
|
|
33
|
+
'Create a new test run in the current project.',
|
|
34
|
+
'',
|
|
35
|
+
'WORKFLOW (test plans): when `test_plan` is provided, the run is AUTOMATICALLY POPULATED',
|
|
36
|
+
'server-side with one UNTESTED test result per plan test — you do NOT need to enumerate the',
|
|
37
|
+
'plan tests yourself. To record pass/fail outcomes, SET the `test_results` statuses (either',
|
|
38
|
+
'here at creation, or later via `update_test_run`). NEVER record test outcomes in a comment —',
|
|
39
|
+
'comments are for genuine commentary only; outcomes must be real `test_results` so reporting',
|
|
40
|
+
'works. Use `list_statuses` (scope `test_result`) to find valid status IDs.',
|
|
41
|
+
].join('\n'), {
|
|
39
42
|
status: z.object({ id: z.number().int().describe('Status ID') }).describe('Status object with ID'),
|
|
40
43
|
stage: z.object({ id: z.number().int().describe('Stage ID') }).describe('Stage object with ID'),
|
|
41
44
|
details: z.string().optional().describe('Free-text details or description of the test run'),
|
|
@@ -44,7 +47,16 @@ export function registerTestRunTools(server, client) {
|
|
|
44
47
|
// The API binds this to testing_environment_id via params.dig(:testing_environment, :id),
|
|
45
48
|
// so it must be an object { id } — a bare string is silently ignored (SR-380).
|
|
46
49
|
testing_environment: z.object({ id: z.number().int().describe('Testing environment ID') }).optional().describe('Testing environment object with ID — use list_testing_environments to find a valid ID.'),
|
|
47
|
-
|
|
50
|
+
// SR-425: typed shape mirroring what the backend consumes. When `test_plan` is set the
|
|
51
|
+
// backend auto-creates UNTESTED results for any plan test not listed here, so you only need
|
|
52
|
+
// to send entries for tests you want to give a specific status/details — the rest are filled in.
|
|
53
|
+
test_results: z.array(z.object({
|
|
54
|
+
test: z.object({ id: z.number().int().describe('Test ID') }).describe('The test this result is for'),
|
|
55
|
+
status: z.object({ id: z.number().int().describe('test_result-scoped Status ID (use list_statuses)') }).describe('The outcome status for this test'),
|
|
56
|
+
details: z.string().optional().describe('Free-text notes about this result'),
|
|
57
|
+
assignee: z.object({ id: z.number().int().describe('Assignee user ID') }).optional().describe('Who this result is assigned to'),
|
|
58
|
+
run_time: z.number().optional().describe('Execution time in seconds'),
|
|
59
|
+
})).optional().describe('Explicit per-test outcomes. Optional: for a plan-backed run, omitted tests are auto-populated as UNTESTED server-side.'),
|
|
48
60
|
custom_fields: z.record(z.string(), z.unknown()).optional().describe('Custom field key-value pairs'),
|
|
49
61
|
project_id: z.string().optional().describe('Override the project for this call (WTProject header). Defaults to SMARTRUNS_PROJECT_ID.'),
|
|
50
62
|
}, async ({ status, stage, details, assignee, test_plan, testing_environment, test_results, custom_fields, project_id }) => {
|
|
@@ -1,22 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
const e = err;
|
|
4
|
-
let msg;
|
|
5
|
-
if (e.status === 409) {
|
|
6
|
-
// Not every 409 here is a stale optimistic-lock conflict. update_test_suite can hit a
|
|
7
|
-
// lock conflict (body carries a lock_version), but create_test_suite can also return 409
|
|
8
|
-
// for a plain validation conflict (e.g. "Assignee must exist"). Only call it a lock
|
|
9
|
-
// conflict when the body actually carries lock_version, so we don't mislabel the rest.
|
|
10
|
-
const isLockConflict = typeof e.body === 'object' && e.body !== null && 'lock_version' in e.body;
|
|
11
|
-
msg = isLockConflict
|
|
12
|
-
? `Conflict (409, stale lock_version) on ${e.method} ${e.path}. Re-fetch the record and retry with the updated lock_version. API response: ${JSON.stringify(e.body)}`
|
|
13
|
-
: `Conflict (409) on ${e.method} ${e.path}: the request conflicts with the current state (e.g. a validation conflict such as a missing/invalid assignee), not necessarily a stale lock. API response: ${JSON.stringify(e.body)}`;
|
|
14
|
-
}
|
|
15
|
-
else {
|
|
16
|
-
msg = `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
17
|
-
}
|
|
18
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
19
|
-
}
|
|
2
|
+
import { handleError } from './handle-error.js';
|
|
20
3
|
// SR-383 (slice 5) — full test-suites tool surface.
|
|
21
4
|
// Routes verified against config/routes.rb + app/controllers/test_suites_controller.rb:
|
|
22
5
|
// GET /test-suites (index → { entries, meta.pagination })
|
package/dist/tools/tests.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
const e = err;
|
|
4
|
-
const msg = e.status === 409
|
|
5
|
-
? `Conflict (lock_version stale). Re-fetch the record and retry with the updated lock_version. API response: ${JSON.stringify(e.body)}`
|
|
6
|
-
: `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
7
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
8
|
-
}
|
|
2
|
+
import { handleError } from './handle-error.js';
|
|
9
3
|
export function registerTestTools(server, client) {
|
|
10
4
|
server.tool('list_tests', 'Search/list tests in the current project. Uses the /tests/search endpoint.', {
|
|
11
5
|
search: z.string().optional().describe('Search term to filter tests by name or content'),
|
package/dist/tools/users.js
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
const e = err;
|
|
3
|
-
const msg = e.status === 409
|
|
4
|
-
? `Conflict (lock_version stale). Re-fetch the record and retry with the updated lock_version. API response: ${JSON.stringify(e.body)}`
|
|
5
|
-
: `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
6
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
7
|
-
}
|
|
1
|
+
import { handleError } from './handle-error.js';
|
|
8
2
|
export function registerUserTools(server, client) {
|
|
9
3
|
server.tool('list_users', 'List all users in the current account.', {}, async () => {
|
|
10
4
|
try {
|
package/dist/tools/watchers.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { handleError as sharedHandleError } from './handle-error.js';
|
|
2
3
|
function handleError(err) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
: `API error ${e.status} on ${e.method} ${e.path}: ${JSON.stringify(e.body)}`;
|
|
7
|
-
return { isError: true, content: [{ type: 'text', text: msg }] };
|
|
4
|
+
return sharedHandleError(err, {
|
|
5
|
+
422: (e) => `Unsupported source_type (422) on ${e.method} ${e.path}: the entity type cannot be watched. API response: ${JSON.stringify(e.body)}`,
|
|
6
|
+
});
|
|
8
7
|
}
|
|
9
8
|
// Watching in SmartRuns is polymorphic and driven by two flat params,
|
|
10
9
|
// `source_type` + `source_id`, on a pair of singular routes:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartruns/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server for the SmartRuns REST API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"zod": "^4.4.3"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@types/node": "^
|
|
31
|
+
"@types/node": "^26.0.0",
|
|
32
32
|
"tsx": "^4.22.4",
|
|
33
33
|
"typescript": "^6.0.3",
|
|
34
|
-
"vitest": "^
|
|
34
|
+
"vitest": "^4.1.9"
|
|
35
35
|
}
|
|
36
36
|
}
|