@smartruns/mcp 1.0.1 → 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/dist/tools/specs.js +54 -32
- package/dist/tools/test-runs.js +20 -2
- package/package.json +3 -3
package/dist/tools/specs.js
CHANGED
|
@@ -13,13 +13,19 @@ import { handleError } from './handle-error.js';
|
|
|
13
13
|
// PUT /specs/:id (update → spec_payload; PARTIAL via strong-params permit)
|
|
14
14
|
// GET /specs/:id/history (member → PLAIN ARRAY of AuditLog entries, newest-first)
|
|
15
15
|
// DELETE /specs/:id (destroy → { deleted: true })
|
|
16
|
-
// Strong params (spec_params): title, description,
|
|
17
|
-
//
|
|
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.
|
|
18
25
|
// IMPORTANT: Spec is NOT optimistically locked — there is no lock_version column, so
|
|
19
26
|
// update_spec must NOT require or send one (unlike test-suites/test-runs). Because the
|
|
20
27
|
// controller uses params.permit + assign_attributes, omitted fields are left untouched,
|
|
21
28
|
// so update_spec is a true partial update.
|
|
22
|
-
const SPEC_STATUSES = ['draft', 'active', 'archived'];
|
|
23
29
|
// Acceptance criteria are nested records. On create supply { description, position };
|
|
24
30
|
// on update include { id } to edit an existing criterion or { id, _destroy: true } to
|
|
25
31
|
// remove one. Shape mirrors the controller's permitted acceptance_criteria keys.
|
|
@@ -30,7 +36,7 @@ const acceptanceCriterionShape = z.object({
|
|
|
30
36
|
_destroy: z.boolean().optional().describe('Set true to delete this existing criterion (requires id)'),
|
|
31
37
|
});
|
|
32
38
|
export function registerSpecTools(server, client) {
|
|
33
|
-
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.', {
|
|
34
40
|
page: z.number().int().optional().describe('Page number for pagination (default 1)'),
|
|
35
41
|
per_page: z.number().int().optional().describe('Results per page (default 10)'),
|
|
36
42
|
project_id: z.string().optional().describe('Override the project for this call (WTProject header). Defaults to SMARTRUNS_PROJECT_ID.'),
|
|
@@ -48,7 +54,7 @@ export function registerSpecTools(server, client) {
|
|
|
48
54
|
return handleError(err);
|
|
49
55
|
}
|
|
50
56
|
});
|
|
51
|
-
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.', {
|
|
52
58
|
id: z.number().int().describe('Spec ID'),
|
|
53
59
|
project_id: z.string().optional().describe('Override the project for this call (WTProject header). Defaults to SMARTRUNS_PROJECT_ID.'),
|
|
54
60
|
}, async ({ id, project_id }) => {
|
|
@@ -60,24 +66,33 @@ export function registerSpecTools(server, client) {
|
|
|
60
66
|
return handleError(err);
|
|
61
67
|
}
|
|
62
68
|
});
|
|
63
|
-
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.', {
|
|
64
70
|
title: z.string().describe('Spec title (required — the model validates its presence)'),
|
|
65
|
-
description: z.string().optional().describe('Spec description / body'),
|
|
66
|
-
|
|
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)'),
|
|
67
86
|
acceptance_criteria: z.array(acceptanceCriterionShape).optional().describe('Acceptance criteria to create with the spec, each { description, position }'),
|
|
68
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 })'),
|
|
69
88
|
project_id: z.string().optional().describe('Override the project for this call (WTProject header). Defaults to SMARTRUNS_PROJECT_ID.'),
|
|
70
|
-
}, async ({
|
|
89
|
+
}, async ({ project_id, ...rest }) => {
|
|
71
90
|
try {
|
|
72
|
-
const body = {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (acceptance_criteria !== undefined)
|
|
78
|
-
body.acceptance_criteria = acceptance_criteria;
|
|
79
|
-
if (tickets !== undefined)
|
|
80
|
-
body.tickets = tickets;
|
|
91
|
+
const body = {};
|
|
92
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
93
|
+
if (value !== undefined)
|
|
94
|
+
body[key] = value;
|
|
95
|
+
}
|
|
81
96
|
const result = await client.post('/specs', body, project_id);
|
|
82
97
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
83
98
|
}
|
|
@@ -85,27 +100,34 @@ export function registerSpecTools(server, client) {
|
|
|
85
100
|
return handleError(err);
|
|
86
101
|
}
|
|
87
102
|
});
|
|
88
|
-
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.', {
|
|
89
104
|
id: z.number().int().describe('Spec ID'),
|
|
90
105
|
title: z.string().optional().describe('New spec title (omit to leave unchanged)'),
|
|
91
|
-
description: z.string().optional().describe('New spec description / body'),
|
|
92
|
-
|
|
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)'),
|
|
93
121
|
acceptance_criteria: z.array(acceptanceCriterionShape).optional().describe('Acceptance criteria changes: new ones, edits (with id), or removals (id + _destroy)'),
|
|
94
122
|
tickets: z.array(z.record(z.string(), z.unknown())).optional().describe('Replacement set of linked Jira tickets'),
|
|
95
123
|
project_id: z.string().optional().describe('Override the project for this call (WTProject header). Defaults to SMARTRUNS_PROJECT_ID.'),
|
|
96
|
-
}, async ({ id,
|
|
124
|
+
}, async ({ id, project_id, ...rest }) => {
|
|
97
125
|
try {
|
|
98
126
|
const body = {};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (status !== undefined)
|
|
104
|
-
body.status = status;
|
|
105
|
-
if (acceptance_criteria !== undefined)
|
|
106
|
-
body.acceptance_criteria = acceptance_criteria;
|
|
107
|
-
if (tickets !== undefined)
|
|
108
|
-
body.tickets = tickets;
|
|
127
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
128
|
+
if (value !== undefined)
|
|
129
|
+
body[key] = value;
|
|
130
|
+
}
|
|
109
131
|
const result = await client.put(`/specs/${id}`, body, project_id);
|
|
110
132
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
111
133
|
}
|
package/dist/tools/test-runs.js
CHANGED
|
@@ -29,7 +29,16 @@ export function registerTestRunTools(server, client) {
|
|
|
29
29
|
return handleError(err);
|
|
30
30
|
}
|
|
31
31
|
});
|
|
32
|
-
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'), {
|
|
33
42
|
status: z.object({ id: z.number().int().describe('Status ID') }).describe('Status object with ID'),
|
|
34
43
|
stage: z.object({ id: z.number().int().describe('Stage ID') }).describe('Stage object with ID'),
|
|
35
44
|
details: z.string().optional().describe('Free-text details or description of the test run'),
|
|
@@ -38,7 +47,16 @@ export function registerTestRunTools(server, client) {
|
|
|
38
47
|
// The API binds this to testing_environment_id via params.dig(:testing_environment, :id),
|
|
39
48
|
// so it must be an object { id } — a bare string is silently ignored (SR-380).
|
|
40
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.'),
|
|
41
|
-
|
|
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.'),
|
|
42
60
|
custom_fields: z.record(z.string(), z.unknown()).optional().describe('Custom field key-value pairs'),
|
|
43
61
|
project_id: z.string().optional().describe('Override the project for this call (WTProject header). Defaults to SMARTRUNS_PROJECT_ID.'),
|
|
44
62
|
}, async ({ status, stage, details, assignee, test_plan, testing_environment, test_results, custom_fields, project_id }) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartruns/mcp",
|
|
3
|
-
"version": "1.0
|
|
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
|
}
|