@smartruns/mcp 1.0.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 +302 -0
- package/dist/api-client.js +142 -0
- package/dist/config.js +33 -0
- package/dist/index.js +19 -0
- package/dist/server.js +42 -0
- package/dist/tools/ai.js +134 -0
- package/dist/tools/comments.js +72 -0
- package/dist/tools/defects.js +150 -0
- package/dist/tools/labels.js +54 -0
- package/dist/tools/notifications.js +55 -0
- package/dist/tools/projects.js +35 -0
- package/dist/tools/reference-data.js +82 -0
- package/dist/tools/specs.js +146 -0
- package/dist/tools/statuses.js +18 -0
- package/dist/tools/test-plans.js +133 -0
- package/dist/tools/test-runs.js +113 -0
- package/dist/tools/test-suites.js +150 -0
- package/dist/tools/tests.js +167 -0
- package/dist/tools/users.js +27 -0
- package/dist/tools/watchers.js +49 -0
- package/dist/types.js +1 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# SmartRuns MCP Server
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io/) server that wraps the SmartRuns
|
|
4
|
+
REST API, giving AI assistants (Claude Code, Claude Desktop, etc.) direct, user-scoped access
|
|
5
|
+
to your test management data — projects, tests, test plans/runs/suites, defects, specs,
|
|
6
|
+
comments, labels, watchers, notifications and AI generation.
|
|
7
|
+
|
|
8
|
+
## Prerequisites
|
|
9
|
+
|
|
10
|
+
- Node.js 18 or later.
|
|
11
|
+
- A SmartRuns **Personal Access Token** (PAT), your **tenant** (subdomain), and a default project ID.
|
|
12
|
+
|
|
13
|
+
### Creating a Personal Access Token (PAT)
|
|
14
|
+
|
|
15
|
+
1. Sign in to SmartRuns.
|
|
16
|
+
2. Go to **Profile → Access Tokens**.
|
|
17
|
+
3. Create a token. It looks like `srpat_…`.
|
|
18
|
+
4. Copy it immediately — it is shown only once.
|
|
19
|
+
|
|
20
|
+
The PAT authenticates **as your user**, so every tool call runs with **your own RBAC
|
|
21
|
+
permissions** and is scoped to your account. Treat the token like a password; never commit it.
|
|
22
|
+
|
|
23
|
+
> **Integration tokens are deprecated for MCP use.** Account-level integration tokens still
|
|
24
|
+
> authenticate (so existing setups keep working during the migration window), but the server
|
|
25
|
+
> logs a warning on startup if the token does not start with `srpat_`. New setups should use a
|
|
26
|
+
> PAT.
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd mcp
|
|
32
|
+
npm install
|
|
33
|
+
npm run build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Copy `.env.example` to `.env` and fill in your credentials:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cp .env.example .env
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Environment Variables
|
|
43
|
+
|
|
44
|
+
| Variable | Required | Description |
|
|
45
|
+
|----------|----------|-------------|
|
|
46
|
+
| `SMARTRUNS_API_TOKEN` | Yes | Personal Access Token (`srpat_…`, created in SmartRuns → Profile → Access Tokens). Sent raw in the `Authorization` header, no "Bearer" prefix. Integration tokens still work but are deprecated for MCP use. |
|
|
47
|
+
| `SMARTRUNS_TENANT` | Yes | Your SmartRuns tenant (subdomain), e.g. `acme`. Sent as the `X-WT-Tenant` header on every request. Must match the account that owns your PAT — a mismatch is rejected by the API with `403`. |
|
|
48
|
+
| `SMARTRUNS_PROJECT_ID` | Yes | **Default** project ID, sent as the `WTProject` header. Project-scoped tools can override it per call via the optional `project_id` argument. |
|
|
49
|
+
| `SMARTRUNS_API_URL` | No | **Local development override only.** Base API URL; defaults to `https://api.smartruns.io` with zero config. Leave unset in normal use. |
|
|
50
|
+
|
|
51
|
+
### Per-call project override
|
|
52
|
+
|
|
53
|
+
`SMARTRUNS_PROJECT_ID` is only the default. Project-scoped tools accept an optional
|
|
54
|
+
`project_id` argument that overrides the `WTProject` header for that single call, leaving the
|
|
55
|
+
default unchanged for every other call. Pass the numeric project id **as a string**, e.g.
|
|
56
|
+
`list_tests({ search: "login", project_id: "77" })`. Omit it to use the default project.
|
|
57
|
+
|
|
58
|
+
## Usage with Claude Code
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
claude mcp add smartruns \
|
|
62
|
+
-e SMARTRUNS_API_TOKEN=srpat_your_personal_access_token \
|
|
63
|
+
-e SMARTRUNS_TENANT=acme \
|
|
64
|
+
-e SMARTRUNS_PROJECT_ID=your_project_id \
|
|
65
|
+
-- npx -y @smartruns/mcp
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Usage with Claude Desktop
|
|
69
|
+
|
|
70
|
+
Add this to your `claude_desktop_config.json`:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"smartruns": {
|
|
76
|
+
"command": "npx",
|
|
77
|
+
"args": ["-y", "@smartruns/mcp"],
|
|
78
|
+
"env": {
|
|
79
|
+
"SMARTRUNS_API_TOKEN": "srpat_your_personal_access_token",
|
|
80
|
+
"SMARTRUNS_TENANT": "acme",
|
|
81
|
+
"SMARTRUNS_PROJECT_ID": "your_project_id"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
Run directly from source (no build step required):
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
SMARTRUNS_API_TOKEN=... SMARTRUNS_TENANT=... SMARTRUNS_PROJECT_ID=... npm run dev
|
|
94
|
+
# Optionally target a local backend with SMARTRUNS_API_URL=http://localhost:3000
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Run the test suite (unit + full registration smoke; no live API needed):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm test # vitest run
|
|
101
|
+
npm run build # tsc + shebang postbuild — must be clean
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The `build` script compiles with `tsc` and then runs `scripts/add-shebang.mjs`, which
|
|
105
|
+
prepends `#!/usr/bin/env node` to `dist/index.js` and marks it executable so the published
|
|
106
|
+
`bin` works under `npx`. `dist/` is gitignored and built fresh in CI — never commit it.
|
|
107
|
+
|
|
108
|
+
## Publishing
|
|
109
|
+
|
|
110
|
+
This package is published to npm as the public scoped package `@smartruns/mcp` by a
|
|
111
|
+
tag-driven CI workflow (`.github/workflows/mcp-publish.yml`).
|
|
112
|
+
|
|
113
|
+
To cut a release:
|
|
114
|
+
|
|
115
|
+
1. Bump `version` in `mcp/package.json` (e.g. `1.0.0`).
|
|
116
|
+
2. Push a matching tag of the form `mcp-v<version>` (e.g. `mcp-v1.0.0`).
|
|
117
|
+
|
|
118
|
+
The workflow then builds `dist/`, runs the tests, **asserts the tag version equals
|
|
119
|
+
`mcp/package.json` version** (and fails the publish on any mismatch), and runs
|
|
120
|
+
`npm publish --access public`.
|
|
121
|
+
|
|
122
|
+
**Prerequisites (human/ops):**
|
|
123
|
+
|
|
124
|
+
- An `NPM_TOKEN` automation secret must exist in GitHub Actions (npm org `@smartruns`,
|
|
125
|
+
owned by Kalinka). CI uses it as `NODE_AUTH_TOKEN`; publishing fails without it.
|
|
126
|
+
- The tag name and the manifest version must agree — the workflow refuses to publish a
|
|
127
|
+
mismatched tag.
|
|
128
|
+
|
|
129
|
+
## Available Tools (61)
|
|
130
|
+
|
|
131
|
+
> Tip: tools marked **project-scoped** accept the optional per-call `project_id` argument.
|
|
132
|
+
> Write tools on optimistically-locked entities require a `lock_version` taken from the most
|
|
133
|
+
> recent `get_*` response.
|
|
134
|
+
|
|
135
|
+
### Projects (2)
|
|
136
|
+
| Tool | Description |
|
|
137
|
+
|------|-------------|
|
|
138
|
+
| `list_projects` | List all projects in the account; optional `archived` filter |
|
|
139
|
+
| `get_project` | Get a single project by ID |
|
|
140
|
+
|
|
141
|
+
### Test Plans (5)
|
|
142
|
+
| Tool | Description |
|
|
143
|
+
|------|-------------|
|
|
144
|
+
| `list_test_plans` | List test plans in the current project (optional page/status/assignee/search filters) |
|
|
145
|
+
| `get_test_plan` | Get a single test plan by ID |
|
|
146
|
+
| `create_test_plan` | Create a new test plan |
|
|
147
|
+
| `update_test_plan` | Update a test plan — requires `lock_version` |
|
|
148
|
+
| `delete_test_plan` | Permanently delete a test plan and its plan-scoped tests/uploads — RBAC-gated (`test_plans_delete_mine` / `_all`) |
|
|
149
|
+
|
|
150
|
+
### Test Runs (4)
|
|
151
|
+
| Tool | Description |
|
|
152
|
+
|------|-------------|
|
|
153
|
+
| `list_test_runs` | List test runs in the current project (optional filters) |
|
|
154
|
+
| `get_test_run` | Get a single test run by ID |
|
|
155
|
+
| `create_test_run` | Create a new test run (status + stage) |
|
|
156
|
+
| `update_test_run` | Update a test run — requires `lock_version` |
|
|
157
|
+
|
|
158
|
+
### Tests (7)
|
|
159
|
+
| Tool | Description |
|
|
160
|
+
|------|-------------|
|
|
161
|
+
| `list_tests` | Search/list tests via the `/tests/search` endpoint |
|
|
162
|
+
| `get_test` | Get a single test case by ID |
|
|
163
|
+
| `get_test_history` | Read-only audit/activity history for a test (AuditLog entries, newest first) |
|
|
164
|
+
| `create_test` | Create a new test case (`test_kind` required) |
|
|
165
|
+
| `update_test` | Update a test case — requires `lock_version`; always include `test_kind` (cleared if omitted) |
|
|
166
|
+
| `clone_test` | Clone an existing test case into a duplicate |
|
|
167
|
+
| `delete_test` | Permanently delete a test case — tenant-scoped; surfaces 403/404 from the server |
|
|
168
|
+
|
|
169
|
+
### Test Suites (6)
|
|
170
|
+
| Tool | Description |
|
|
171
|
+
|------|-------------|
|
|
172
|
+
| `list_test_suites` | List test suites in the current project (paginated `{ entries, meta }`) |
|
|
173
|
+
| `get_test_suite` | Get a single test suite by ID (includes `lock_version`, watchers, plans) |
|
|
174
|
+
| `create_test_suite` | Create a test suite — `name`, `status` **and** `assignee` all required |
|
|
175
|
+
| `update_test_suite` | Update a test suite — requires `lock_version`; always include `name` + `status` |
|
|
176
|
+
| `clone_test_suite` | Clone a test suite — RBAC-gated (`test_suites_create`) |
|
|
177
|
+
| `delete_test_suite` | Permanently delete a test suite — RBAC-gated (`test_suites_delete_mine` / `_all`) |
|
|
178
|
+
|
|
179
|
+
### Defects (5)
|
|
180
|
+
| Tool | Description |
|
|
181
|
+
|------|-------------|
|
|
182
|
+
| `list_defects` | List defects; at most ONE filter (`ticket_id`, `pull_request_id`, or `test_run_id`) at a time |
|
|
183
|
+
| `get_defect` | Get a single defect by ID |
|
|
184
|
+
| `create_defect` | Create a defect (`summary`/`steps`/`current`/`expected`/`status` required) |
|
|
185
|
+
| `update_defect` | Update an existing defect |
|
|
186
|
+
| `delete_defect` | Permanently delete a defect — tenant-scoped; surfaces 403/404 from the server |
|
|
187
|
+
|
|
188
|
+
### Specs (6)
|
|
189
|
+
| Tool | Description |
|
|
190
|
+
|------|-------------|
|
|
191
|
+
| `list_specs` | List specs (spec-driven development); feature-gated — may return 403/404 if the flag is off |
|
|
192
|
+
| `get_spec` | Get a single spec with acceptance criteria, linked tickets, attachments and coverage roll-up |
|
|
193
|
+
| `create_spec` | Create a spec (`title` required; status defaults to `draft`) |
|
|
194
|
+
| `update_spec` | Partial update of a spec — **no** `lock_version` (specs are not optimistically locked) |
|
|
195
|
+
| `get_spec_history` | Read-only activity/history feed for a spec (AuditLog entries, newest first) |
|
|
196
|
+
| `delete_spec` | Permanently delete a spec — tenant-scoped; surfaces 403/404 from the server |
|
|
197
|
+
|
|
198
|
+
### Comments (4)
|
|
199
|
+
| Tool | Description |
|
|
200
|
+
|------|-------------|
|
|
201
|
+
| `list_comments` | List comments on a parent entity (`test`/`test_plan`/`test_run`/`test_suite`/`spec`) |
|
|
202
|
+
| `create_comment` | Create a comment on a parent entity (body in the `text` field) |
|
|
203
|
+
| `update_comment` | Update a comment — RBAC-gated (`comments_edit_mine` / `_all`) |
|
|
204
|
+
| `delete_comment` | Delete a comment — RBAC-gated (`comments_delete_mine` / `_all`) |
|
|
205
|
+
|
|
206
|
+
### Statuses (1)
|
|
207
|
+
| Tool | Description |
|
|
208
|
+
|------|-------------|
|
|
209
|
+
| `list_statuses` | List all statuses grouped by scope (returns an object keyed by scope, not an array) |
|
|
210
|
+
|
|
211
|
+
### Labels (3)
|
|
212
|
+
| Tool | Description |
|
|
213
|
+
|------|-------------|
|
|
214
|
+
| `list_labels` | List labels in the current project (optional `terms` search) |
|
|
215
|
+
| `create_label` | Create a label (`name` only; duplicate names are allowed — no uniqueness validation) |
|
|
216
|
+
| `update_label` | Rename a label by ID — this is a rename only, NOT a merge |
|
|
217
|
+
|
|
218
|
+
### Watchers (2)
|
|
219
|
+
| Tool | Description |
|
|
220
|
+
|------|-------------|
|
|
221
|
+
| `watch_entity` | Subscribe the current user to an entity; returns `{ watchers: [...] }` |
|
|
222
|
+
| `unwatch_entity` | Unsubscribe the current user from an entity; returns `{ watchers: [...] }` |
|
|
223
|
+
|
|
224
|
+
### Notifications (3)
|
|
225
|
+
| Tool | Description |
|
|
226
|
+
|------|-------------|
|
|
227
|
+
| `list_notifications` | List the authenticated user's notifications (user-scoped, not project-scoped) |
|
|
228
|
+
| `mark_notification_read` | Mark a single notification read (or unread via `read=false`) |
|
|
229
|
+
| `mark_all_notifications_read` | Mark ALL of the user's unread notifications as read |
|
|
230
|
+
|
|
231
|
+
### Users (2)
|
|
232
|
+
| Tool | Description |
|
|
233
|
+
|------|-------------|
|
|
234
|
+
| `list_users` | List all users in the current account |
|
|
235
|
+
| `get_current_user` | Get the currently authenticated user's profile |
|
|
236
|
+
|
|
237
|
+
### Reference data (5, read-only)
|
|
238
|
+
These lookups exist so an LLM can discover valid IDs before constructing a write payload.
|
|
239
|
+
They are **read-only** — there are intentionally no create/update/delete tools for reference
|
|
240
|
+
data (that is admin/project configuration, out of scope).
|
|
241
|
+
|
|
242
|
+
| Tool | Description |
|
|
243
|
+
|------|-------------|
|
|
244
|
+
| `list_product_areas` | List product areas (to get a valid `product_area { id }`) |
|
|
245
|
+
| `list_test_kinds` | List test kinds (to get a valid `test_kind { id }`) |
|
|
246
|
+
| `list_stages` | List stages (to get a valid stage for test runs) |
|
|
247
|
+
| `list_testing_environments` | List testing environments (for test runs) |
|
|
248
|
+
| `list_custom_fields` | List custom field definitions (to learn accepted `custom_fields` keys/types) |
|
|
249
|
+
|
|
250
|
+
### AI generation (6) — ⚠ consumes AI credits / billing
|
|
251
|
+
Every tool below invokes the SmartRuns AI backend and **consumes the account's AI credits**
|
|
252
|
+
(and may incur billing). Each tool's description starts with that warning so an LLM treats it
|
|
253
|
+
cautiously.
|
|
254
|
+
|
|
255
|
+
| Tool | Sync/Async | Description |
|
|
256
|
+
|------|------------|-------------|
|
|
257
|
+
| `generate_tests_with_ai` | Sync | Generate proposed tests from a Jira ticket; results returned inline |
|
|
258
|
+
| `request_ai_suggestions` | Async (fire-and-forget) | Enqueue AI suggestions for an existing test; re-read the test for results |
|
|
259
|
+
| `create_agent_task` | Async (create → poll) | Create a test-generation / requirements-review task; returns `202` with a task id. Returns `403 plan_limit` when AI credits are exhausted |
|
|
260
|
+
| `get_agent_task` | Poll | Poll a task's status, logs and result payload until terminal |
|
|
261
|
+
| `list_agent_tasks` | Read | List/filter agent tasks by `status` / `type` |
|
|
262
|
+
| `confirm_agent_task` | Write | Persist generated tests once a task is `awaiting_confirmation` |
|
|
263
|
+
|
|
264
|
+
The agent-task flow is **asynchronous**: `create_agent_task` returns immediately with a task
|
|
265
|
+
id; poll `get_agent_task` until `status.value` is terminal (`succeeded` / `failed` /
|
|
266
|
+
`awaiting_confirmation`), then `confirm_agent_task` for test-case generation. No tool
|
|
267
|
+
blocks/long-polls inside a single call.
|
|
268
|
+
|
|
269
|
+
## Intentionally excluded / out of scope
|
|
270
|
+
|
|
271
|
+
The MCP deliberately exposes a **safe, read-and-write-where-it-makes-sense** surface for
|
|
272
|
+
day-to-day test management. The following are **not** exposed — by design — so both users and
|
|
273
|
+
LLMs know the boundaries. If you need one of these, use the SmartRuns web app:
|
|
274
|
+
|
|
275
|
+
- **Admin / project configuration writes** — creating or editing product areas, test kinds,
|
|
276
|
+
stages, testing environments, custom field *definitions*, statuses, roles or permissions.
|
|
277
|
+
Reference data is read-only here.
|
|
278
|
+
- **User management** — inviting, editing, deactivating or deleting users (`list_users` and
|
|
279
|
+
`get_current_user` are read-only).
|
|
280
|
+
- **PAT / token management** — creating, listing, rotating or revoking Personal Access Tokens
|
|
281
|
+
or integration tokens.
|
|
282
|
+
- **Password / credential changes** — no auth or account-credential mutation.
|
|
283
|
+
- **Bulk test deletion** — only single-record `delete_test` is exposed; the bulk-delete
|
|
284
|
+
endpoint is intentionally omitted.
|
|
285
|
+
- **Label merge** — `update_label` renames a single label; merging labels is not exposed.
|
|
286
|
+
- **File / attachment uploads** — uploading or deleting attachments on any entity.
|
|
287
|
+
|
|
288
|
+
## Notes
|
|
289
|
+
|
|
290
|
+
- Write operations on optimistically-locked entities (`update_test`, `update_test_run`,
|
|
291
|
+
`update_test_plan`, `update_test_suite`) require a `lock_version` taken from the most recent
|
|
292
|
+
`get_*` response. On a 409 lock conflict, re-fetch the record and retry with the new
|
|
293
|
+
`lock_version`. (Specs are **not** locked — `update_spec` takes no `lock_version`.)
|
|
294
|
+
- Defect list filters (`ticket_id`, `pull_request_id`, `test_run_id`) are mutually exclusive —
|
|
295
|
+
supply at most one per call.
|
|
296
|
+
- `list_tests` uses `/tests/search` under the hood (not the base `/tests` index, which
|
|
297
|
+
requires explicit IDs).
|
|
298
|
+
- Label names are **not** unique — `create_label`/`update_label` accept duplicate names.
|
|
299
|
+
- Specs are feature-gated; if the flag is off for the account, the specs tools surface the
|
|
300
|
+
server's 403/404 verbatim.
|
|
301
|
+
- The full tool catalogue above is asserted by `src/smoke.test.ts` (registration smoke test):
|
|
302
|
+
if a tool is added, removed or renamed, update both that test and this README.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
2
|
+
export class ApiClient {
|
|
3
|
+
baseUrl;
|
|
4
|
+
token;
|
|
5
|
+
projectId;
|
|
6
|
+
tenant;
|
|
7
|
+
// SR-393: `tenant` is the customer's SMARTRUNS_TENANT and is threaded through
|
|
8
|
+
// like `projectId`. It is sent as the X-WT-Tenant header on EVERY request so
|
|
9
|
+
// the backend can enforce the token/tenant guardrail on the PAT auth path.
|
|
10
|
+
constructor(baseUrl, token, projectId, tenant) {
|
|
11
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
12
|
+
this.token = token;
|
|
13
|
+
this.projectId = projectId;
|
|
14
|
+
this.tenant = tenant;
|
|
15
|
+
}
|
|
16
|
+
// An optional projectIdOverride lets a single call target a different project
|
|
17
|
+
// (WTProject header) without changing the client default (SMARTRUNS_PROJECT_ID).
|
|
18
|
+
// X-WT-Tenant is constant for the lifetime of the client (one tenant per token).
|
|
19
|
+
commonHeaders(projectIdOverride) {
|
|
20
|
+
return {
|
|
21
|
+
Authorization: this.token,
|
|
22
|
+
'WTProject': projectIdOverride ?? this.projectId,
|
|
23
|
+
'X-WT-Tenant': this.tenant,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async get(path, params, projectId) {
|
|
27
|
+
let url = `${this.baseUrl}${path}`;
|
|
28
|
+
if (params) {
|
|
29
|
+
const searchParams = new URLSearchParams();
|
|
30
|
+
for (const [key, value] of Object.entries(params)) {
|
|
31
|
+
if (value !== undefined && value !== null) {
|
|
32
|
+
searchParams.append(key, String(value));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const queryString = searchParams.toString();
|
|
36
|
+
if (queryString) {
|
|
37
|
+
url += `?${queryString}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(url, {
|
|
42
|
+
method: 'GET',
|
|
43
|
+
headers: this.commonHeaders(projectId),
|
|
44
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
45
|
+
});
|
|
46
|
+
return this.handleResponse(response, 'GET', path);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
ApiClient.wrapTimeoutError(err, 'GET', path);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async post(path, body, projectId) {
|
|
53
|
+
const url = `${this.baseUrl}${path}`;
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch(url, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
...this.commonHeaders(projectId),
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
63
|
+
});
|
|
64
|
+
return this.handleResponse(response, 'POST', path);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
ApiClient.wrapTimeoutError(err, 'POST', path);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async put(path, body, projectId) {
|
|
71
|
+
const url = `${this.baseUrl}${path}`;
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(url, {
|
|
74
|
+
method: 'PUT',
|
|
75
|
+
headers: {
|
|
76
|
+
...this.commonHeaders(projectId),
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify(body),
|
|
80
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
81
|
+
});
|
|
82
|
+
return this.handleResponse(response, 'PUT', path);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
ApiClient.wrapTimeoutError(err, 'PUT', path);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// DELETE mirrors put()/post() minus the body and Content-Type. Many SmartRuns
|
|
89
|
+
// DELETE endpoints return 204 No Content; handleResponse short-circuits that to null.
|
|
90
|
+
// A few DELETE routes (e.g. DELETE /watch) identify the target via a JSON body
|
|
91
|
+
// rather than the path; pass `body` for those. When omitted, no body or
|
|
92
|
+
// Content-Type header is sent (the common case).
|
|
93
|
+
async delete(path, projectId, body) {
|
|
94
|
+
const url = `${this.baseUrl}${path}`;
|
|
95
|
+
const hasBody = body !== undefined;
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(url, {
|
|
98
|
+
method: 'DELETE',
|
|
99
|
+
headers: hasBody
|
|
100
|
+
? { ...this.commonHeaders(projectId), 'Content-Type': 'application/json' }
|
|
101
|
+
: this.commonHeaders(projectId),
|
|
102
|
+
...(hasBody ? { body: JSON.stringify(body) } : {}),
|
|
103
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
104
|
+
});
|
|
105
|
+
return this.handleResponse(response, 'DELETE', path);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
ApiClient.wrapTimeoutError(err, 'DELETE', path);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Called by callers to wrap fetch so AbortError surfaces as a readable message.
|
|
112
|
+
static wrapTimeoutError(err, method, path) {
|
|
113
|
+
if (err instanceof Error && err.name === 'TimeoutError') {
|
|
114
|
+
throw { status: 408, method, path, body: `Request timed out after ${REQUEST_TIMEOUT_MS}ms` };
|
|
115
|
+
}
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
async handleResponse(response, method, path) {
|
|
119
|
+
// 204 No Content has an empty body; calling response.json() would throw.
|
|
120
|
+
// Short-circuit so delete() (and any empty-bodied success) resolves to null.
|
|
121
|
+
if (response.status === 204) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
let responseBody;
|
|
125
|
+
try {
|
|
126
|
+
responseBody = await response.json();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
responseBody = await response.text().catch(() => null);
|
|
130
|
+
}
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
const error = {
|
|
133
|
+
status: response.status,
|
|
134
|
+
method,
|
|
135
|
+
path,
|
|
136
|
+
body: responseBody,
|
|
137
|
+
};
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
return responseBody;
|
|
141
|
+
}
|
|
142
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SR-393: Public env contract for the npx-distributed MCP.
|
|
2
|
+
//
|
|
3
|
+
// The MCP is published to npm and installed by external customers via
|
|
4
|
+
// claude mcp add smartruns -e SMARTRUNS_API_TOKEN=… -e SMARTRUNS_TENANT=acme \
|
|
5
|
+
// -e SMARTRUNS_PROJECT_ID=… -- npx -y @smartruns/mcp
|
|
6
|
+
// so the API base URL is NOT something a customer should ever supply. It is
|
|
7
|
+
// hardcoded to the production host. SMARTRUNS_API_URL is retained ONLY as an
|
|
8
|
+
// optional override for local development against a non-prod backend — it is
|
|
9
|
+
// never required and must default to the production host with zero config.
|
|
10
|
+
export const DEFAULT_API_URL = 'https://api.smartruns.io';
|
|
11
|
+
// Required, customer-supplied env vars. SMARTRUNS_API_URL is intentionally NOT
|
|
12
|
+
// here — it is optional (see DEFAULT_API_URL above).
|
|
13
|
+
export const REQUIRED_ENV = [
|
|
14
|
+
'SMARTRUNS_API_TOKEN',
|
|
15
|
+
'SMARTRUNS_TENANT',
|
|
16
|
+
'SMARTRUNS_PROJECT_ID',
|
|
17
|
+
];
|
|
18
|
+
// Resolve and validate the MCP configuration from the environment. Throws an
|
|
19
|
+
// Error naming the first missing required variable so a misconfigured install
|
|
20
|
+
// fails fast with an actionable message (no token bytes are ever included).
|
|
21
|
+
export function resolveConfig(env = process.env) {
|
|
22
|
+
for (const key of REQUIRED_ENV) {
|
|
23
|
+
if (!env[key]) {
|
|
24
|
+
throw new Error(`Missing required env var: ${key}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
apiUrl: env['SMARTRUNS_API_URL'] || DEFAULT_API_URL,
|
|
29
|
+
apiToken: env['SMARTRUNS_API_TOKEN'],
|
|
30
|
+
tenant: env['SMARTRUNS_TENANT'],
|
|
31
|
+
projectId: env['SMARTRUNS_PROJECT_ID'],
|
|
32
|
+
};
|
|
33
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { createServer } from './server.js';
|
|
4
|
+
import { resolveConfig } from './config.js';
|
|
5
|
+
// SR-393: validate + resolve the public env contract (throws, naming the first
|
|
6
|
+
// missing required var). The API base URL defaults to https://api.smartruns.io
|
|
7
|
+
// with zero config; the customer supplies SMARTRUNS_TENANT (sent as X-WT-Tenant).
|
|
8
|
+
const config = resolveConfig();
|
|
9
|
+
// Warn-and-continue PAT sanity-check. The MCP now expects a user Personal Access Token
|
|
10
|
+
// (srpat_…); integration tokens still authenticate but are deprecated for MCP use, so we
|
|
11
|
+
// must NOT hard-fail during the migration window. Log to stderr (stdout is the MCP
|
|
12
|
+
// transport) and never echo any token bytes.
|
|
13
|
+
if (!config.apiToken.startsWith('srpat_')) {
|
|
14
|
+
console.error('SmartRuns MCP: SMARTRUNS_API_TOKEN does not start with "srpat_" — it does not look like a Personal Access Token. This MCP now expects a user PAT (create one in SmartRuns → Profile → Tokens). Continuing for backward compatibility; integration tokens still authenticate but are deprecated for MCP use.');
|
|
15
|
+
}
|
|
16
|
+
const server = createServer(config.apiUrl, config.apiToken, config.projectId, config.tenant);
|
|
17
|
+
const transport = new StdioServerTransport();
|
|
18
|
+
console.error(`SmartRuns MCP starting — API: ${config.apiUrl}, tenant: ${config.tenant}, project: ${config.projectId}`);
|
|
19
|
+
await server.connect(transport);
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { ApiClient } from './api-client.js';
|
|
3
|
+
import { registerProjectTools } from './tools/projects.js';
|
|
4
|
+
import { registerTestPlanTools } from './tools/test-plans.js';
|
|
5
|
+
import { registerTestRunTools } from './tools/test-runs.js';
|
|
6
|
+
import { registerTestTools } from './tools/tests.js';
|
|
7
|
+
import { registerTestSuiteTools } from './tools/test-suites.js';
|
|
8
|
+
import { registerDefectTools } from './tools/defects.js';
|
|
9
|
+
import { registerSpecTools } from './tools/specs.js';
|
|
10
|
+
import { registerCommentTools } from './tools/comments.js';
|
|
11
|
+
import { registerStatusTools } from './tools/statuses.js';
|
|
12
|
+
import { registerLabelTools } from './tools/labels.js';
|
|
13
|
+
import { registerWatcherTools } from './tools/watchers.js';
|
|
14
|
+
import { registerNotificationTools } from './tools/notifications.js';
|
|
15
|
+
import { registerUserTools } from './tools/users.js';
|
|
16
|
+
import { registerReferenceDataTools } from './tools/reference-data.js';
|
|
17
|
+
import { registerAiTools } from './tools/ai.js';
|
|
18
|
+
export function createServer(apiUrl, apiToken, projectId, tenant) {
|
|
19
|
+
const server = new McpServer({
|
|
20
|
+
name: 'smartruns-mcp',
|
|
21
|
+
version: '1.0.0',
|
|
22
|
+
});
|
|
23
|
+
const client = new ApiClient(apiUrl, apiToken, projectId, tenant);
|
|
24
|
+
registerProjectTools(server, client);
|
|
25
|
+
registerTestPlanTools(server, client);
|
|
26
|
+
registerTestRunTools(server, client);
|
|
27
|
+
registerTestTools(server, client);
|
|
28
|
+
registerTestSuiteTools(server, client);
|
|
29
|
+
registerDefectTools(server, client);
|
|
30
|
+
registerSpecTools(server, client);
|
|
31
|
+
registerCommentTools(server, client);
|
|
32
|
+
registerStatusTools(server, client);
|
|
33
|
+
registerLabelTools(server, client);
|
|
34
|
+
registerWatcherTools(server, client);
|
|
35
|
+
registerNotificationTools(server, client);
|
|
36
|
+
registerUserTools(server, client);
|
|
37
|
+
registerReferenceDataTools(server, client);
|
|
38
|
+
// SR-387 (slice 9, BILLING-flagged): AI-generation tools — ship last; every tool
|
|
39
|
+
// description warns that invoking it consumes the account's AI credits.
|
|
40
|
+
registerAiTools(server, client);
|
|
41
|
+
return server;
|
|
42
|
+
}
|