@smartruns/mcp 1.0.0 → 1.0.1
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 +1 -7
- package/dist/tools/statuses.js +1 -7
- package/dist/tools/test-plans.js +1 -7
- package/dist/tools/test-runs.js +1 -7
- 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 +1 -1
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.
|
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'),
|
|
@@ -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:
|