@taiga-ai/mcp-server 0.25.0-rc.472 → 0.26.0-dev.479

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.
Files changed (55) hide show
  1. package/README.md +111 -0
  2. package/dist/index.js +322 -122
  3. package/package.json +15 -2
  4. package/scripts/build.mjs +0 -37
  5. package/scripts/prepare-publish.cjs +0 -49
  6. package/src/client.test.ts +0 -424
  7. package/src/client.ts +0 -976
  8. package/src/config.test.ts +0 -24
  9. package/src/config.ts +0 -28
  10. package/src/index.ts +0 -45
  11. package/src/modern.integration.test.ts +0 -153
  12. package/src/server.integration.test.ts +0 -9075
  13. package/src/server.ts +0 -18
  14. package/src/tools/annotations.ts +0 -38
  15. package/src/tools/autonomy.ts +0 -86
  16. package/src/tools/backlogs.ts +0 -766
  17. package/src/tools/builds.ts +0 -668
  18. package/src/tools/catalogue.test.ts +0 -170
  19. package/src/tools/catalogue.ts +0 -223
  20. package/src/tools/context.ts +0 -513
  21. package/src/tools/documents.ts +0 -375
  22. package/src/tools/errors.ts +0 -172
  23. package/src/tools/hints.ts +0 -99
  24. package/src/tools/inbox.ts +0 -546
  25. package/src/tools/index.ts +0 -239
  26. package/src/tools/inputs.ts +0 -47
  27. package/src/tools/instructions.ts +0 -41
  28. package/src/tools/jobs.ts +0 -90
  29. package/src/tools/knowledge.ts +0 -249
  30. package/src/tools/maintenance.ts +0 -540
  31. package/src/tools/markdown.test.ts +0 -249
  32. package/src/tools/markdown.ts +0 -151
  33. package/src/tools/observe.ts +0 -311
  34. package/src/tools/product-documents.ts +0 -184
  35. package/src/tools/products.ts +0 -529
  36. package/src/tools/prompts.ts +0 -265
  37. package/src/tools/queue.ts +0 -250
  38. package/src/tools/readiness.ts +0 -177
  39. package/src/tools/remote.test.ts +0 -44
  40. package/src/tools/remote.ts +0 -107
  41. package/src/tools/repositories.ts +0 -360
  42. package/src/tools/request-prompt.test.ts +0 -32
  43. package/src/tools/request-prompt.ts +0 -28
  44. package/src/tools/resources.ts +0 -217
  45. package/src/tools/sections.test.ts +0 -143
  46. package/src/tools/sections.ts +0 -287
  47. package/src/tools/setup.ts +0 -455
  48. package/src/tools/summarize.test.ts +0 -20
  49. package/src/tools/summarize.ts +0 -96
  50. package/src/tools/wait.ts +0 -490
  51. package/src/version.test.ts +0 -10
  52. package/src/version.ts +0 -14
  53. package/taiga-ai-mcp-server-0.25.0-rc.472.tgz +0 -0
  54. package/tsconfig.json +0 -21
  55. package/vitest.config.ts +0 -9
@@ -1,540 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/server';
2
- import {
3
- MAINTENANCE_FINDING_KINDS,
4
- MAINTENANCE_FINDING_SEVERITIES,
5
- MAINTENANCE_FINDING_STATUSES,
6
- MAINTENANCE_IGNORE_REASONS,
7
- } from '@taiga/types';
8
- import { z } from 'zod';
9
- import { TaigaApiError, type TaigaClient } from '../client.js';
10
- import { OPERATE, READ_ONLY } from './annotations.js';
11
- import { isNetworkFailure, runTool, toolError, toolResult } from './errors.js';
12
- import { idInput, pagination, productId } from './inputs.js';
13
- import { page, pickFields } from './summarize.js';
14
-
15
- /**
16
- * Maintaining (docs.tai.ga, Operate): what the sweeps found in a product's
17
- * repository and where it stands against the organization's own policies,
18
- * and the two things a person does with a finding, Ignore and Fix. Sweeps run
19
- * themselves and nothing is marked fixed by hand: a finding resolves when the
20
- * next sweep no longer detects it, so there is no tool for either.
21
- */
22
-
23
- /** A finding as a list row: enough to decide whether to open, fix or ignore it. */
24
- const FINDING_ROW_FIELDS = [
25
- 'id',
26
- 'kind',
27
- 'severity',
28
- 'tier',
29
- 'status',
30
- 'title',
31
- 'ecosystem',
32
- 'packageName',
33
- 'currentVersion',
34
- 'recommendedVersion',
35
- 'advisoryId',
36
- 'location',
37
- 'sourcePaths',
38
- 'productRepositoryId',
39
- 'ownership',
40
- 'remediableBy',
41
- 'controls',
42
- 'backlogItemId',
43
- 'detectedAt',
44
- 'ignoreReason',
45
- 'ignoreExpiresAt',
46
- ] as const;
47
-
48
- /**
49
- * Whether taiga_fix_finding would take a finding, said on the row (PRO-2800): three reviews found a dependency
50
- * marked remediableBy coding_agent with no fixed version, which the API refuses to fix. The API's own refusals,
51
- * read off the row; remediableBy stays as the sweep recorded it.
52
- */
53
- function withFixability(row: Row): Row {
54
- const dependency = row['kind'] === 'vulnerable_dependency' || row['kind'] === 'outdated_dependency';
55
- if (dependency && !row['recommendedVersion'] && row['remediableBy'] === 'coding_agent') {
56
- return {
57
- ...row,
58
- fixable: false,
59
- fixNote: 'No fixed version is known yet: taiga_fix_finding refuses it until one is published.',
60
- };
61
- }
62
- return row;
63
- }
64
-
65
- /** The parts of an advisory record an agent reads; the rest (every affected range, every reference) is full: true. */
66
- const ADVISORY_KEPT = ['id', 'aliases', 'summary', 'details', 'severity', 'published'];
67
- const ADVISORY_TEXT_LIMIT = 1_500;
68
-
69
- function leanAdvisory(data: unknown): unknown {
70
- if (typeof data !== 'object' || data === null || !('affected' in data || 'references' in data)) return data;
71
- const record = data as Row;
72
- const kept = Object.fromEntries(ADVISORY_KEPT.filter((key) => key in record).map((key) => [key, record[key]]));
73
- const details = kept['details'];
74
- if (typeof details === 'string' && details.length > ADVISORY_TEXT_LIMIT) {
75
- kept['details'] =
76
- `${details.slice(0, ADVISORY_TEXT_LIMIT)}... [cut at ${ADVISORY_TEXT_LIMIT} of ${details.length} characters; full: true reads it whole]`;
77
- }
78
- const references = Array.isArray(record['references']) ? record['references'] : [];
79
- return {
80
- ...kept,
81
- ...(references.length > 0
82
- ? {
83
- references: references.slice(0, 3),
84
- ...(references.length > 3 ? { referencesNote: `3 of ${references.length}; full: true has every one.` } : {}),
85
- }
86
- : {}),
87
- };
88
- }
89
-
90
- /** A sweep as the record of when the product was last checked, and whether that check held. */
91
- const SWEEP_FIELDS = [
92
- 'id',
93
- 'trigger',
94
- 'status',
95
- 'scanners',
96
- 'findingsCount',
97
- 'newFindingsCount',
98
- 'error',
99
- 'createdAt',
100
- 'completedAt',
101
- ] as const;
102
-
103
- /** A policy violation in a list: what it is, how bad, and what is being done about it. */
104
- const VIOLATION_FIELDS = [
105
- 'id',
106
- 'controlId',
107
- 'policyType',
108
- 'severity',
109
- 'status',
110
- 'title',
111
- 'summary',
112
- 'work',
113
- 'unconfirmedSince',
114
- 'verifyRequestedAt',
115
- 'assessedAt',
116
- ] as const;
117
-
118
- type Row = Record<string, unknown>;
119
-
120
- /** Characters of one text field the full standing carries; violationId reads one violation uncut. */
121
- const FULL_TEXT_LIMIT = 1_500;
122
-
123
- /** Resolved violations the full standing carries. */
124
- const FULL_RESOLVED = 20;
125
-
126
- /** A copy with every string longer than the limit cut and marked, at any depth. */
127
- function cutLongText(value: unknown): unknown {
128
- if (typeof value === 'string') {
129
- return value.length > FULL_TEXT_LIMIT
130
- ? `${value.slice(0, FULL_TEXT_LIMIT)}... [cut at ${FULL_TEXT_LIMIT} of ${value.length} characters; violationId reads it whole]`
131
- : value;
132
- }
133
- if (Array.isArray(value)) return value.map(cutLongText);
134
- if (value === null || typeof value !== 'object') return value;
135
- return Object.fromEntries(Object.entries(value).map(([key, inner]) => [key, cutLongText(inner)]));
136
- }
137
-
138
- /**
139
- * How bad a product's policy standing is, as counts: the controls something is
140
- * recorded against, the open violations by severity, the resolved ones. The
141
- * API's standing carries the lists only; this tool and the product context
142
- * both count them here.
143
- */
144
- export function standingSummary(standing: {
145
- controls?: unknown;
146
- divergences?: unknown;
147
- resolvedDivergences?: unknown;
148
- }): {
149
- controls: number;
150
- controlsInPolicies: number;
151
- controlsWithNothingRecorded: number;
152
- violations: number;
153
- bySeverity: Record<string, number>;
154
- resolvedViolations: number;
155
- } {
156
- const controls = Array.isArray(standing.controls) ? (standing.controls as Row[]) : [];
157
- const violations = Array.isArray(standing.divergences) ? (standing.divergences as Row[]) : [];
158
- const resolved = Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences.length : 0;
159
- const listed = controls.filter((c) => Number(c['divergenceCount'] ?? 0) > 0 || Number(c['findingCount'] ?? 0) > 0);
160
- const bySeverity: Record<string, number> = {};
161
- for (const violation of violations) {
162
- const severity = String(violation['severity'] ?? 'unknown');
163
- bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
164
- }
165
- return {
166
- controls: listed.length,
167
- // Every control the product's policies hold, and those nothing is recorded against: the positive side of
168
- // "how do we stand", which is no evidence against them, not proof they are met.
169
- controlsInPolicies: controls.length,
170
- controlsWithNothingRecorded: controls.length - listed.length,
171
- violations: violations.length,
172
- bySeverity,
173
- resolvedViolations: resolved,
174
- };
175
- }
176
-
177
- const findingId = idInput('finding', 'taiga_list_findings gives it');
178
-
179
- /** Open findings by severity and kind, and the latest sweeps, as the first page of findings carries them. */
180
- async function sweepOverview(client: TaigaClient, productId: string) {
181
- const [summary, sweeps] = await Promise.all([
182
- client.getMaintenanceSummary(productId),
183
- client.listMaintenanceSweeps(productId, { limit: 3 }),
184
- ]);
185
- const latest = pickFields(sweeps.data, SWEEP_FIELDS);
186
- // The summary's own run rows (one per repository) would repeat the sweeps with every column.
187
- const {
188
- lastRun: _lastRun,
189
- lastRuns: _lastRuns,
190
- topFindings: _top,
191
- violatedControls: _controls,
192
- ...counts
193
- } = summary.data as Record<string, unknown>;
194
- return {
195
- ...counts,
196
- latestSweeps: latest,
197
- ...(latest.length === 0 ? { note: 'This product has never been swept: no findings is not an all-clear.' } : {}),
198
- };
199
- }
200
-
201
- export function registerMaintenanceTools(server: McpServer, client: TaigaClient) {
202
- if (!client.hasCapability('products:read')) return;
203
-
204
- server.registerTool(
205
- 'taiga_list_findings',
206
- {
207
- title: 'List maintenance findings',
208
- description:
209
- "List what maintenance sweeps found in a product's repository: dependencies, secrets, insecure code " +
210
- '(sast) and infrastructure. The first page opens with sweep (open counts, latest sweeps), so an old or ' +
211
- 'failed sweep is not read as an all-clear. Rows: severity, tier (act_now: known exploited; urgent: ' +
212
- 'likely and serious; elevated: one of the two; routine), location, remediableBy (coding_agent: a run here ' +
213
- 'can fix it; upstream_only: the code is in a submodule, so the fix belongs in its own repository), fixable: false ' +
214
- 'when no fixed version is known yet, breached controls and initiativeId of a fix under way. Omit status ' +
215
- 'for open findings. taiga_get_finding reads one.',
216
- inputSchema: z.object({
217
- productId,
218
- status: z
219
- .enum(MAINTENANCE_FINDING_STATUSES)
220
- .optional()
221
- .describe('Only findings in this status; omit for the ones needing action'),
222
- severity: z.enum(MAINTENANCE_FINDING_SEVERITIES).optional().describe('Only this severity'),
223
- kind: z.enum(MAINTENANCE_FINDING_KINDS).optional().describe('Only this kind of finding'),
224
- ...pagination,
225
- }),
226
- annotations: READ_ONLY,
227
- },
228
- (params) =>
229
- runTool(async () => {
230
- const { productId: id, ...filters } = params;
231
- // The sweep overview on the first page: when the product was last
232
- // checked decides whether an empty list can be trusted.
233
- const [result, sweep] = await Promise.all([
234
- client.listFindings(id, filters),
235
- // Best effort: the findings the agent asked for do not fail on the overview.
236
- params.cursor === undefined
237
- ? sweepOverview(client, id).catch((error: unknown) => {
238
- if (error instanceof TaigaApiError || isNetworkFailure(error)) return 'unread' as const;
239
- throw error;
240
- })
241
- : Promise.resolve(null),
242
- ]);
243
- return toolResult({
244
- // An overview that could not be read says so: without it an empty list would read as an all-clear.
245
- ...(sweep === 'unread'
246
- ? {
247
- sweep: null,
248
- sweepNote: 'The sweep overview could not be read: an empty list here is not an all-clear.',
249
- }
250
- : sweep
251
- ? { sweep }
252
- : {}),
253
- ...page(pickFields(result.data, FINDING_ROW_FIELDS).map(withFixability), result.nextCursor),
254
- ...(typeof result.totalCount === 'number' ? { totalCount: result.totalCount } : {}),
255
- // The default list has findings with a fix under way too; the sweep's count does not (31 against 33).
256
- ...(params.status === undefined &&
257
- params.severity === undefined &&
258
- params.kind === undefined &&
259
- typeof result.totalCount === 'number' &&
260
- sweep &&
261
- sweep !== 'unread' &&
262
- typeof (sweep as Row)['openFindings'] === 'number' &&
263
- (sweep as Row)['openFindings'] !== result.totalCount
264
- ? {
265
- totalNote:
266
- 'totalCount includes findings with a fix under way (remediating) and ignored findings whose ' +
267
- 'ignore has expired; ' +
268
- 'sweep.openFindings counts open ones only.',
269
- }
270
- : {}),
271
- });
272
- })
273
- );
274
-
275
- server.registerTool(
276
- 'taiga_get_finding',
277
- {
278
- title: 'Get a maintenance finding',
279
- description:
280
- 'Read one maintenance finding: exploit likelihood (EPSS, known-exploited list), the advisory summarized ' +
281
- '(full: true for its whole record: every affected range and reference) and ' +
282
- 'fixed version, code location, breached policy controls, why it ranks where it does, history ' +
283
- '(reintroduced, previously resolved) and the remediation under way. fixable: false with fixNote says ' +
284
- "taiga_fix_finding would refuse it. A secret's value is never stored, only the rule and location.",
285
- inputSchema: z.object({
286
- findingId,
287
- full: z
288
- .boolean()
289
- .optional()
290
- .describe("Carry the advisory's whole record: every affected range and reference (default false)"),
291
- }),
292
- annotations: READ_ONLY,
293
- },
294
- (params) =>
295
- runTool(async () => {
296
- // The finding's runId is the sweep that found it, not a coding run:
297
- // in the product's words runId is what taiga_get_run takes (PRO-2800).
298
- const { runId, ...raw } = (await client.getFinding(params.findingId)).data as Row;
299
- const finding = withFixability(params.full ? raw : { ...raw, data: leanAdvisory(raw['data']) });
300
- return toolResult(runId === undefined ? finding : { sweepId: runId, ...finding });
301
- })
302
- );
303
-
304
- server.registerTool(
305
- 'taiga_get_policy_standing',
306
- {
307
- title: "Get a product's policy standing",
308
- description:
309
- "How a product stands against the organization's published policies: summary counts, when the documents " +
310
- 'were assessed, and each violated control with its violations (critical and high only, citing the policy ' +
311
- 'sentence and the contradicting passage) and the work on each. state: breached (open scanner findings ' +
312
- 'map to it, by findingsByTier; routine-only is not urgent) or diverged (the documents contradict it). ' +
313
- "live: false means the violation's initiative ended without fixing it: it needs a person or a new fix. " +
314
- "raisedFor: false means the link is the backlog planner's: it said an initiative a person asked it for " +
315
- 'would close the violation, which may be a dedicated fix or broader work; true means a fix was raised ' +
316
- 'for it (taiga_fix_policy_violation or the sweep). ' +
317
- 'An unlisted control is not "passing" (summary.controlsWithNothingRecorded counts them). full: true adds ' +
318
- 'evidence and resolved ones. Fix with taiga_fix_policy_violation.',
319
- inputSchema: z.object({
320
- productId,
321
- full: z
322
- .boolean()
323
- .optional()
324
- .describe(
325
- "Carry each violation's evidence and policy excerpt, cut to about 1,500 characters each, and up to 20 resolved violations (default false)"
326
- ),
327
- violationId: idInput('violation', 'this tool lists them')
328
- .optional()
329
- .describe('Only this violation, in full and uncut: its evidence, policy excerpt and work'),
330
- }),
331
- annotations: READ_ONLY,
332
- },
333
- (params) =>
334
- runTool(async () => {
335
- const standing = (await client.getPolicyStanding(params.productId)).data;
336
- const controls = Array.isArray(standing.controls) ? standing.controls : [];
337
- const violations = Array.isArray(standing.divergences) ? standing.divergences : [];
338
- const listed = controls.filter((c) => Number(c.divergenceCount ?? 0) > 0 || Number(c.findingCount ?? 0) > 0);
339
- // The counts first, on both answers: the lists run to thousands of
340
- // tokens, and "how bad is it" is the first question.
341
- const summary = standingSummary(standing);
342
- // Never assessed reads like a clean bill otherwise (PRO-2800).
343
- const assessment = standing.assessment as { state?: unknown } | null | undefined;
344
- const note =
345
- assessment?.state === 'never'
346
- ? 'Never assessed: the documents have not been checked against the policies yet, so nothing here is an ' +
347
- 'all-clear. An assessment runs when the reality documents are derived or refreshed ' +
348
- '(taiga_refresh_reality_documents, paid work: ask first).'
349
- : null;
350
- // Each assessment refreshes a violation's evidence but rewords its title and summary only when it
351
- // re-describes the gap, so the words can describe code the evidence says is gone (PRO-2810). Said once.
352
- const resolvedAny = Array.isArray(standing.resolvedDivergences) && standing.resolvedDivergences.length > 0;
353
- const wording =
354
- violations.length > 0 || resolvedAny
355
- ? {
356
- wordingNote:
357
- "A violation's evidence is from its assessedAt; its title and summary may be from when it was " +
358
- 'first recorded. Where they disagree, the evidence is current.',
359
- }
360
- : {};
361
- if (params.violationId) {
362
- const one = [
363
- ...violations,
364
- ...(Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences : []),
365
- ].find((violation) => (violation as Row)['id'] === params.violationId);
366
- if (!one) {
367
- return toolError(
368
- 'NOT_FOUND',
369
- 'No violation with that ID on this product; this tool without violationId lists them.'
370
- );
371
- }
372
- return toolResult({ ...(one as Row), ...wording });
373
- }
374
- // The full form ran to 228k characters on a product with eleven violations (PRO-2800): each text is
375
- // cut and the resolved list bounded; violationId reads one whole.
376
- if (params.full) {
377
- const resolvedAll = Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences : [];
378
- return toolResult({
379
- summary,
380
- ...(note ? { note } : {}),
381
- ...wording,
382
- ...standing,
383
- // Only the controls something is recorded against, as the summary answer lists them.
384
- controls: listed,
385
- divergences: cutLongText(violations),
386
- resolvedDivergences: cutLongText(resolvedAll.slice(0, FULL_RESOLVED)),
387
- ...(resolvedAll.length > FULL_RESOLVED
388
- ? { resolvedNote: `The first ${FULL_RESOLVED} of ${resolvedAll.length} resolved violations.` }
389
- : {}),
390
- });
391
- }
392
- return toolResult({
393
- summary,
394
- ...(note ? { note } : {}),
395
- ...wording,
396
- productId: standing.productId,
397
- hasPolicies: standing.hasPolicies,
398
- assessment: standing.assessment,
399
- documents: standing.documents,
400
- // Only the controls something is recorded against; every other
401
- // control is simply not reported, as the page does.
402
- controls: pickFields(listed, [
403
- 'controlId',
404
- 'policyType',
405
- 'text',
406
- 'state',
407
- 'divergenceCount',
408
- 'findingCount',
409
- 'findingsByTier',
410
- 'plannedCount',
411
- ]),
412
- violations: pickFields(violations, VIOLATION_FIELDS),
413
- });
414
- })
415
- );
416
-
417
- if (!client.hasCapability('products:write')) return;
418
-
419
- server.registerTool(
420
- 'taiga_ignore_finding',
421
- {
422
- title: 'Ignore or reopen a finding',
423
- description:
424
- 'Ignore a maintenance finding with a reason, or reopen one (ignored: false). Reasons: false_positive, ' +
425
- 'not_reachable, accepted_risk, wont_fix; plus an optional note and an expiry after which it resurfaces ' +
426
- 'if still present. There is no "mark fixed": a finding resolves when the next sweep stops detecting it. ' +
427
- 'For a secret only in history, rotate it outside Taiga, then ignore it. Ignoring can record the team ' +
428
- "accepting a risk, which is the person's decision: ask first.",
429
- inputSchema: z.object({
430
- findingId,
431
- ignored: z.boolean().describe('true ignores the finding, false reopens it'),
432
- reason: z.enum(MAINTENANCE_IGNORE_REASONS).optional().describe('Why it is ignored; required when ignoring'),
433
- note: z.string().trim().max(280).optional().describe('A short justification, kept for audit'),
434
- expiresAt: z
435
- .string()
436
- .datetime({ offset: true })
437
- // format: date-time says it; the leap-year pattern beside it ran to hundreds of characters (PRO-2800).
438
- .meta({ pattern: undefined })
439
- .optional()
440
- .describe('When the ignore lapses and the finding resurfaces if it still exists (ISO 8601, in the future)'),
441
- }),
442
- annotations: OPERATE,
443
- },
444
- (params) =>
445
- runTool(async () => {
446
- if (params.ignored && params.reason === undefined) {
447
- return toolError('VALIDATION_ERROR', 'Say why the finding is ignored (reason).');
448
- }
449
- const body = params.ignored
450
- ? {
451
- status: 'ignored' as const,
452
- ignoreReason: params.reason,
453
- ...(params.note !== undefined ? { ignoreNote: params.note } : {}),
454
- ...(params.expiresAt !== undefined ? { ignoreExpiresAt: params.expiresAt } : {}),
455
- }
456
- : { status: 'open' as const };
457
- const finding = (await client.updateFinding(params.findingId, body)).data;
458
- return toolResult(pickFields([finding], [...FINDING_ROW_FIELDS, 'ignoreNote'])[0]);
459
- })
460
- );
461
-
462
- server.registerTool(
463
- 'taiga_fix_finding',
464
- {
465
- title: 'Fix a finding',
466
- description:
467
- "Raise an initiative that fixes a maintenance finding, at the end of the product's line; it ends in a " +
468
- "pull request a person merges, and the initiative's review gate applies. wholeGroup: true fixes the " +
469
- 'group in one initiative: every open advisory on that package version in one bump, or every location the ' +
470
- 'same rule flagged. Refused for a secret only in history, remediableBy upstream_only (code in a ' +
471
- 'submodule), and a dependency with no fixed version yet (fixable: false).',
472
- inputSchema: z.object({
473
- productId,
474
- findingId,
475
- wholeGroup: z
476
- .boolean()
477
- .optional()
478
- .describe("Fix every finding in this finding's group (its package and version, or its rule) together"),
479
- }),
480
- annotations: OPERATE,
481
- },
482
- (params) =>
483
- runTool(async () => {
484
- if (!params.wholeGroup) {
485
- const fixed = (await client.remediateFinding(params.productId, params.findingId)).data;
486
- return toolResult({
487
- backlogItemId: fixed.backlogItemId,
488
- finding: fixed.finding ? pickFields([fixed.finding], FINDING_ROW_FIELDS)[0] : null,
489
- });
490
- }
491
- // The group is the finding's own: the route matches the package (or
492
- // rule), the installed version and the repository exactly, so they are
493
- // taken from the finding rather than asked of the caller.
494
- const finding = (await client.getFinding(params.findingId)).data;
495
- // The finding is read by its own id, so it is held to the product the
496
- // group fix is sent to, as the single-finding route holds it.
497
- if (finding.productId !== params.productId) {
498
- return toolError('NOT_FOUND', 'No such finding in this product.');
499
- }
500
- const result = await client.remediatePackage(params.productId, {
501
- ecosystem: String(finding.ecosystem),
502
- packageName: String(finding.packageName),
503
- currentVersion: typeof finding.currentVersion === 'string' ? finding.currentVersion : null,
504
- productRepositoryId: typeof finding.productRepositoryId === 'string' ? finding.productRepositoryId : null,
505
- });
506
- return toolResult(result.data);
507
- })
508
- );
509
-
510
- server.registerTool(
511
- 'taiga_fix_policy_violation',
512
- {
513
- title: 'Fix policy violations',
514
- description:
515
- "Raise one initiative that fixes a product's policy violations and add it to the end of the line. Name a " +
516
- 'control to cover all its violations with no initiative yet, or one violationId when the control has ' +
517
- 'several. The violation stays listed until the next assessment no longer finds it, even while the fix is ' +
518
- 'under way.',
519
- inputSchema: z.object({
520
- productId,
521
- controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
522
- violationId: idInput('violation', 'taiga_get_policy_standing gives it').optional(),
523
- }),
524
- annotations: OPERATE,
525
- },
526
- (params) =>
527
- runTool(async () => {
528
- if ((params.controlId === undefined) === (params.violationId === undefined)) {
529
- return toolError(
530
- 'VALIDATION_ERROR',
531
- 'Name either a control (controlId) or one violation (violationId), not both or neither.'
532
- );
533
- }
534
- const result = params.controlId
535
- ? await client.remediatePolicyControl(params.productId, params.controlId)
536
- : await client.remediatePolicyViolation(params.productId, params.violationId!);
537
- return toolResult(result.data);
538
- })
539
- );
540
- }