@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 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
+ }