@testplanit/mcp-server 0.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.
@@ -0,0 +1,490 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+
3
+ interface EnvConfig {
4
+ apiToken: string;
5
+ apiUrl: string;
6
+ }
7
+ /**
8
+ * Parse and validate the MCP server's environment.
9
+ *
10
+ * Throws a zod validation error when:
11
+ * - `TESTPLANIT_API_TOKEN` is missing or does not start with `tpi_`
12
+ * - `TESTPLANIT_API_URL` is set but is not a valid URL (omitting it uses the SaaS default)
13
+ *
14
+ * The returned `apiUrl` is normalized (trailing slash stripped).
15
+ */
16
+ declare function parseEnv(env: NodeJS.ProcessEnv): EnvConfig;
17
+
18
+ /**
19
+ * Response shape from `GET /api/auth/whoami` — locked in lockstep with
20
+ * plan 05-02. Field semantics (frozen contract):
21
+ * - `readOnly === scopes.includes("mode:read")`
22
+ * - `isAgent === scopes.includes("client:mcp")`
23
+ */
24
+ interface WhoamiUser {
25
+ id: string;
26
+ name: string | null;
27
+ email: string;
28
+ scopes: string[];
29
+ readOnly: boolean;
30
+ isAgent: boolean;
31
+ }
32
+ /**
33
+ * Result of `validateToken()`.
34
+ *
35
+ * The failure variant carries `code` (upstream errorCode such as
36
+ * `READ_ONLY_TOKEN`, `EXPIRED_TOKEN`) and `statusCode` (HTTP status) so
37
+ * downstream tools — plan 05-07's `whoami` and Phase 6+ write tools —
38
+ * can construct `TestPlanItHttpError` without re-parsing the message
39
+ * string. This shape is locked at plan 05-06 and consumed directly by
40
+ * plan 05-07; do NOT widen retroactively.
41
+ */
42
+ type ValidateResult = {
43
+ ok: true;
44
+ user: WhoamiUser;
45
+ } | {
46
+ ok: false;
47
+ message: string;
48
+ code?: string;
49
+ statusCode?: number;
50
+ };
51
+ /**
52
+ * Error class carrying upstream HTTP status + errorCode for tool handlers.
53
+ * Plan 05-07 throws this from the `whoami` tool when validateToken returns
54
+ * `ok: false` so the MCP error mapping retains the upstream codes.
55
+ */
56
+ declare class TestPlanItHttpError extends Error {
57
+ statusCode?: number;
58
+ code?: string;
59
+ constructor(message: string, opts?: {
60
+ statusCode?: number;
61
+ code?: string;
62
+ });
63
+ }
64
+ /**
65
+ * Returns at most the `tpi_xxxx`-style 8-character prefix of the token for
66
+ * safe diagnostics. Never returns the full token. T-05-06 mitigation.
67
+ */
68
+ declare function redactToken(token: string): string;
69
+ /**
70
+ * Probe `GET /api/auth/whoami` with the configured Bearer token.
71
+ *
72
+ * Returns `{ ok: true, user }` on a 200 response with a valid JSON body.
73
+ * Returns `{ ok: false, message, code?, statusCode? }` on any non-2xx,
74
+ * unparseable body, or transport failure. The error message NEVER contains
75
+ * the raw token — only the redacted prefix appears (T-05-06).
76
+ */
77
+ declare function validateToken(env: EnvConfig): Promise<ValidateResult>;
78
+
79
+ interface RunDeps {
80
+ parseEnvImpl: (env: NodeJS.ProcessEnv) => EnvConfig;
81
+ validateImpl: (env: EnvConfig) => Promise<ValidateResult>;
82
+ createServerImpl: (deps: {
83
+ env: EnvConfig;
84
+ user: WhoamiUser;
85
+ }) => McpServer;
86
+ connectImpl: (server: McpServer) => Promise<void>;
87
+ errLog: (...args: unknown[]) => void;
88
+ exitImpl: (code: number) => void;
89
+ }
90
+ /**
91
+ * Default implementations wiring the real env, http, and stdio transport.
92
+ * Overridden in tests to assert exit codes + serial order without spawning
93
+ * a real subprocess.
94
+ */
95
+ declare const defaultRunDeps: RunDeps;
96
+ /**
97
+ * Bootstrap the MCP server in strict serial order:
98
+ * parseEnv → validateToken → createServer → connect (stdio)
99
+ *
100
+ * Bad env or bad token exits with code 1 BEFORE any transport connect.
101
+ * All diagnostics go through `errLog` (defaults to `console.error`); raw
102
+ * token values are NEVER logged — only the redacted `tpi_xxxx` prefix
103
+ * appears in error messages (T-05-06).
104
+ */
105
+ declare function runServer(deps?: RunDeps): Promise<void>;
106
+
107
+ /**
108
+ * Dependencies the CLI bootstrap (cli.ts `runServer`) injects into
109
+ * `createServer`.
110
+ *
111
+ * `user` is the `WhoamiUser` resolved by the bootstrap probe; it is
112
+ * intentionally retained on the deps contract even though Phase 5's
113
+ * production tools (`whoami`) re-fetch live data on every call. Future
114
+ * tools may use `user` for static-shape decisions (e.g., gating Phase 6+
115
+ * write tools on `!user.readOnly` at registration time).
116
+ */
117
+ interface ServerDeps {
118
+ env: EnvConfig;
119
+ user: WhoamiUser;
120
+ }
121
+ /**
122
+ * Create an MCP server instance for TestPlanIt.
123
+ *
124
+ * Plan 05-06 registered a placeholder smoke tool so the
125
+ * `InMemoryTransport` handshake test had something to find. Plan 05-07
126
+ * replaces that with the production `whoami` tool, registered through the
127
+ * central `registerAll` registry — Phase 6+ tools plug into the registry
128
+ * without further edits to this file.
129
+ */
130
+ declare function createServer(deps: ServerDeps): McpServer;
131
+
132
+ /**
133
+ * MCP tool-result error envelope.
134
+ *
135
+ * The SDK accepts either a thrown exception (which it auto-converts) OR an
136
+ * explicit return value with `isError: true`. We use the explicit form so
137
+ * tool handlers control the agent-visible message verbatim — no SDK
138
+ * stack-trace leakage and no opaque "An error occurred" wrappers.
139
+ *
140
+ * Reference: 05-RESEARCH.md § Don't Hand-Roll row "Tool error formatting".
141
+ */
142
+ interface ToolErrorResult {
143
+ isError: true;
144
+ content: Array<{
145
+ type: "text";
146
+ text: string;
147
+ }>;
148
+ [x: string]: unknown;
149
+ }
150
+ /**
151
+ * Translate any thrown error from a tool handler into the MCP tool-result
152
+ * error envelope. Three paths:
153
+ *
154
+ * 1. `TestPlanItHttpError` with a known `code`: friendly template, code
155
+ * appended in parentheses for log-grep traceability.
156
+ * 2. `TestPlanItHttpError` with an unknown / missing code: generic
157
+ * "Request failed: <message> (HTTP <status>)" fallback.
158
+ * 3. Any other `Error`: "Network or runtime error: <message>" fallback.
159
+ * 4. Non-Error throwable: "Unknown error".
160
+ *
161
+ * The friendly templates are fixed strings, NOT echoes of `err.message`,
162
+ * so a token-bearing message accidentally constructed upstream cannot leak
163
+ * the raw token through this layer (T-05-06 defense in depth). The fallback
164
+ * paths DO interpolate `err.message`, so the final text is run through
165
+ * `redactTokens` as a belt-and-suspenders scrub (WR-03).
166
+ */
167
+ declare function mapHttpErrorToToolResult(err: unknown): ToolErrorResult;
168
+
169
+ interface WhoamiDeps {
170
+ env: EnvConfig;
171
+ }
172
+ /**
173
+ * Register the `whoami` MCP tool — the production replacement for plan
174
+ * 05-06's smoke tool.
175
+ *
176
+ * The tool re-fetches `GET /api/auth/whoami` on every invocation (NOT
177
+ * cached from the bootstrap probe) so scope changes — particularly the
178
+ * read-only flag — reflect immediately.
179
+ *
180
+ * On failure, the handler consumes the wider `ValidateResult` shape locked
181
+ * in plan 05-06 by reading `probe.code` and `probe.statusCode` directly,
182
+ * NOT by string-parsing `probe.message`. The constructed
183
+ * `TestPlanItHttpError` is then translated by `mapHttpErrorToToolResult`
184
+ * which knows how to format `READ_ONLY_TOKEN` (T-05-01 mitigation) and the
185
+ * other 7 host errorCodes.
186
+ *
187
+ * The description starts with `"Debug:"` so well-behaved agents
188
+ * deprioritize this tool versus the data tools shipping in Phase 6+.
189
+ */
190
+ declare function registerWhoami(server: McpServer, deps: WhoamiDeps): void;
191
+
192
+ interface CasesListDeps {
193
+ env: EnvConfig;
194
+ }
195
+
196
+ interface CasesGetDeps {
197
+ env: EnvConfig;
198
+ }
199
+
200
+ interface CasesCreateDeps {
201
+ env: EnvConfig;
202
+ }
203
+
204
+ interface CasesUpdateDeps {
205
+ env: EnvConfig;
206
+ }
207
+
208
+ interface CasesDeleteDeps {
209
+ env: EnvConfig;
210
+ }
211
+
212
+ type CasesDeps = CasesListDeps & CasesGetDeps & CasesCreateDeps & CasesUpdateDeps & CasesDeleteDeps;
213
+ declare function registerCases(server: McpServer, deps: CasesDeps): void;
214
+
215
+ interface FoldersListDeps {
216
+ env: EnvConfig;
217
+ }
218
+
219
+ interface FoldersGetDeps {
220
+ env: EnvConfig;
221
+ }
222
+
223
+ interface FoldersCreateDeps {
224
+ env: EnvConfig;
225
+ }
226
+
227
+ interface FoldersUpdateDeps {
228
+ env: EnvConfig;
229
+ }
230
+
231
+ interface FoldersDeleteDeps {
232
+ env: EnvConfig;
233
+ }
234
+
235
+ type FoldersDeps = FoldersListDeps & FoldersGetDeps & FoldersCreateDeps & FoldersUpdateDeps & FoldersDeleteDeps;
236
+ declare function registerFolders(server: McpServer, deps: FoldersDeps): void;
237
+
238
+ interface TagsListDeps {
239
+ env: EnvConfig;
240
+ }
241
+
242
+ type TagsDeps = TagsListDeps;
243
+ declare function registerTags(server: McpServer, deps: TagsDeps): void;
244
+
245
+ interface ProjectsListDeps {
246
+ env: EnvConfig;
247
+ }
248
+
249
+ type ProjectsDeps = ProjectsListDeps;
250
+ declare function registerProjects(server: McpServer, deps: ProjectsDeps): void;
251
+
252
+ interface RunsListDeps {
253
+ env: EnvConfig;
254
+ }
255
+
256
+ interface RunsGetDeps {
257
+ env: EnvConfig;
258
+ }
259
+
260
+ interface RunsCasesListDeps {
261
+ env: EnvConfig;
262
+ }
263
+
264
+ interface RunResultsListDeps {
265
+ env: EnvConfig;
266
+ }
267
+
268
+ interface RunResultsGetDeps {
269
+ env: EnvConfig;
270
+ }
271
+
272
+ interface RunResultsCreateDeps {
273
+ env: EnvConfig;
274
+ }
275
+
276
+ type RunResultsDeps = RunResultsListDeps & RunResultsGetDeps & RunResultsCreateDeps;
277
+
278
+ interface RunsCreateDeps {
279
+ env: EnvConfig;
280
+ }
281
+
282
+ interface RunsUpdateDeps {
283
+ env: EnvConfig;
284
+ }
285
+
286
+ interface RunsCasesAddDeps {
287
+ env: EnvConfig;
288
+ }
289
+
290
+ type RunsDeps = RunsListDeps & RunsGetDeps & RunsCasesListDeps & RunResultsDeps & RunsCreateDeps & RunsUpdateDeps & RunsCasesAddDeps;
291
+ declare function registerRuns(server: McpServer, deps: RunsDeps): void;
292
+
293
+ interface SessionsListDeps {
294
+ env: EnvConfig;
295
+ }
296
+
297
+ interface SessionsGetDeps {
298
+ env: EnvConfig;
299
+ }
300
+
301
+ interface SessionResultsListDeps {
302
+ env: EnvConfig;
303
+ }
304
+
305
+ interface SessionResultsGetDeps {
306
+ env: EnvConfig;
307
+ }
308
+
309
+ /**
310
+ * Aggregate dependencies for the SESS-03 / SESS-04 session-result read tools.
311
+ * Both tools share the same EnvConfig; this intersection lets the parent
312
+ * `registerSessions` pass a single deps object (mirrors the runs/results
313
+ * pattern from plan 07-03).
314
+ */
315
+ type SessionResultsDeps = SessionResultsListDeps & SessionResultsGetDeps;
316
+
317
+ interface SessionsFindingsDeps {
318
+ env: EnvConfig;
319
+ }
320
+
321
+ interface SessionsCreateDeps {
322
+ env: EnvConfig;
323
+ }
324
+
325
+ interface SessionsUpdateDeps {
326
+ env: EnvConfig;
327
+ }
328
+
329
+ type SessionsDeps = SessionsListDeps & SessionsGetDeps & SessionResultsDeps & SessionsFindingsDeps & SessionsCreateDeps & SessionsUpdateDeps;
330
+ declare function registerSessions(server: McpServer, deps: SessionsDeps): void;
331
+
332
+ interface CodeRepositoriesListDeps {
333
+ env: EnvConfig;
334
+ }
335
+
336
+ /**
337
+ * Aggregate dependencies for the Phase 8 code-repositories read tools. Only
338
+ * one tool today (`testplanit_code_repositories_list`); aliasing rather than
339
+ * intersecting keeps room to add `_get` etc. via union extension when the
340
+ * single-row-per-project invariant relaxes.
341
+ *
342
+ * Registry wiring into `tools/index.ts` is intentionally deferred to plan
343
+ * 08-05 (wave 3) so plans 08-01..08-04 can run in parallel without touching
344
+ * the same file.
345
+ */
346
+ type CodeRepositoriesDeps = CodeRepositoriesListDeps;
347
+
348
+ interface IssuesFindByKeyDeps {
349
+ env: EnvConfig;
350
+ }
351
+
352
+ interface IssuesListDeps {
353
+ env: EnvConfig;
354
+ }
355
+
356
+ interface IssuesGetDeps {
357
+ env: EnvConfig;
358
+ }
359
+
360
+ interface IssuesListLinksDeps {
361
+ env: EnvConfig;
362
+ }
363
+
364
+ interface IssuesLinkDeps {
365
+ env: EnvConfig;
366
+ }
367
+
368
+ type IssuesDeps = IssuesFindByKeyDeps & IssuesListDeps & IssuesGetDeps & IssuesListLinksDeps & IssuesLinkDeps;
369
+
370
+ interface RepositoryCaseLinksListDeps {
371
+ env: EnvConfig;
372
+ }
373
+
374
+ type RepositoryCaseLinksDeps = RepositoryCaseLinksListDeps;
375
+
376
+ interface MilestonesListDeps {
377
+ env: EnvConfig;
378
+ }
379
+
380
+ interface MilestonesGetDeps {
381
+ env: EnvConfig;
382
+ }
383
+
384
+ interface MilestoneTypesListDeps {
385
+ env: EnvConfig;
386
+ }
387
+
388
+ interface MilestonesCreateDeps {
389
+ env: EnvConfig;
390
+ }
391
+
392
+ interface MilestonesUpdateDeps {
393
+ env: EnvConfig;
394
+ }
395
+
396
+ type MilestonesDeps = MilestonesListDeps & MilestonesGetDeps & MilestoneTypesListDeps & MilestonesCreateDeps & MilestonesUpdateDeps;
397
+
398
+ /**
399
+ * Aggregate dependencies for every tool registered by
400
+ * `@testplanit/mcp-server`. The intersection widens with each new domain;
401
+ * adding a new domain barrel means adding the matching `& <Domain>Deps`
402
+ * here so all registered tools see the same single deps object at runtime.
403
+ */
404
+ type ToolRegistryDeps = WhoamiDeps & CasesDeps & FoldersDeps & TagsDeps & ProjectsDeps & RunsDeps & SessionsDeps & CodeRepositoriesDeps & IssuesDeps & RepositoryCaseLinksDeps & MilestonesDeps;
405
+ /**
406
+ * Register every tool shipped by `@testplanit/mcp-server`.
407
+ *
408
+ * Tools are grouped by domain: whoami (debug/identity), cases, folders,
409
+ * tags, projects (agent context disambiguation), runs, sessions,
410
+ * code-repositories, issues, repository-case-links, and milestones
411
+ * (milestones_list, milestones_get, milestone_types_list).
412
+ */
413
+ declare function registerAll(server: McpServer, deps: ToolRegistryDeps): void;
414
+
415
+ /**
416
+ * Internal ZenStack RPC client.
417
+ *
418
+ * Replicates the dispatch pattern from `packages/api/src/client.ts` so
419
+ * `@testplanit/mcp-server` has NO dependency on `@testplanit/api` (D-01).
420
+ *
421
+ * Read operations use GET with `?q=encodeURIComponent(JSON.stringify(body))`.
422
+ * Write operations use POST/PATCH/DELETE with the JSON body.
423
+ *
424
+ * Errors:
425
+ * - Non-2xx → throws TestPlanItHttpError with statusCode + (when present) code
426
+ * parsed from the response body. Critical: HTTP 422 may be a route.ts
427
+ * remap of a ZenStack 403 (policy denial) or 404 (P2025 missing record).
428
+ * The error mapper in errors.ts disambiguates by message content.
429
+ * - 2xx with `error` envelope → throws TestPlanItHttpError with envelope code.
430
+ * - 2xx with `data` → returns `data` unwrapped.
431
+ *
432
+ * Soft-delete invariant: callers MUST use `update` with `{ data: { isDeleted: true } }`
433
+ * for soft-delete, NEVER the `delete` or `deleteMany` operations (T-06-06).
434
+ */
435
+ declare function zenstack<T>(model: string, operation: string, body: unknown, env: EnvConfig): Promise<T>;
436
+ /**
437
+ * Name → ID lookup via `/api/cli/lookup` (D-02).
438
+ *
439
+ * NOT all entity types are supported — see VERIFIED type union below. Notably,
440
+ * `CaseField` is NOT a lookup type; resolve custom fields via
441
+ * `zenstack("caseFields", "findMany", { where: { displayName: ..., isDeleted: false } })`.
442
+ *
443
+ * The `state` lookup type hardcodes `WorkflowScope.RUNS` on the host
444
+ * (`/api/cli/lookup/route.ts` line 106). For case workflow state, use
445
+ * `resolveCaseWorkflowState` instead.
446
+ */
447
+ type LookupType = "project" | "state" | "config" | "milestone" | "tag" | "folder" | "testRun";
448
+ interface LookupRequest {
449
+ type: LookupType;
450
+ name: string;
451
+ projectId?: number;
452
+ createIfMissing?: boolean;
453
+ }
454
+ interface LookupResponse {
455
+ id: number;
456
+ name: string;
457
+ created?: boolean;
458
+ }
459
+ declare function lookup(options: LookupRequest, env: EnvConfig): Promise<LookupResponse>;
460
+ /**
461
+ * Resolve the single active repository for a project. Cases and folders
462
+ * require `repositoryId` on create; the active repository is selected by
463
+ * the first row matching `isActive=true, isDeleted=false, isArchived=false`.
464
+ *
465
+ * Throws TestPlanItHttpError with statusCode 422 (host-class "operation
466
+ * refused / missing context") when no active repository exists, with a
467
+ * human-readable message instructing the user to initialize the repo via
468
+ * the TestPlanIt UI. The 422 status routes the error through the same
469
+ * `mapHttpErrorToToolResult` branch as host-side missing-record errors.
470
+ */
471
+ declare function resolveActiveRepository(projectId: number, env: EnvConfig): Promise<number>;
472
+ /**
473
+ * Resolve a default template assigned to the project (cases require
474
+ * `templateId` per RepositoryCases.templateId being non-nullable —
475
+ * VERIFIED in schema.zmodel).
476
+ */
477
+ declare function resolveDefaultTemplate(projectId: number, env: EnvConfig): Promise<number>;
478
+ /**
479
+ * Resolve a workflow state for the CASES scope (NOT runs). Pass `name` to
480
+ * select by name; omit to take the first by `order asc`.
481
+ *
482
+ * Cannot use `/api/cli/lookup` — that endpoint hardcodes
483
+ * `WorkflowScope.RUNS` (see /api/cli/lookup/route.ts line 106).
484
+ */
485
+ declare function resolveCaseWorkflowState(projectId: number, env: EnvConfig, name?: string): Promise<{
486
+ id: number;
487
+ name: string;
488
+ }>;
489
+
490
+ export { type CasesDeps, type EnvConfig, type FoldersDeps, type LookupRequest, type LookupResponse, type LookupType, type ProjectsDeps, type RunDeps, type RunsDeps, type ServerDeps, type SessionsDeps, type TagsDeps, TestPlanItHttpError, type ToolErrorResult, type ToolRegistryDeps, type ValidateResult, type WhoamiDeps, type WhoamiUser, createServer, defaultRunDeps, lookup, mapHttpErrorToToolResult, parseEnv, redactToken, registerAll, registerCases, registerFolders, registerProjects, registerRuns, registerSessions, registerTags, registerWhoami, resolveActiveRepository, resolveCaseWorkflowState, resolveDefaultTemplate, runServer, validateToken, zenstack };