@taiga-ai/mcp-server 0.20.0-dev.454 → 0.21.0-dev.455

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.
@@ -6,9 +6,9 @@ import {
6
6
  MAINTENANCE_IGNORE_REASONS,
7
7
  } from '@taiga/types';
8
8
  import { z } from 'zod';
9
- import type { TaigaClient } from '../client.js';
9
+ import { TaigaApiError, type TaigaClient } from '../client.js';
10
10
  import { OPERATE, READ_ONLY } from './annotations.js';
11
- import { runTool, toolError, toolResult } from './errors.js';
11
+ import { isNetworkFailure, runTool, toolError, toolResult } from './errors.js';
12
12
  import { idInput, pagination, productId } from './inputs.js';
13
13
  import { page, pickFields } from './summarize.js';
14
14
 
@@ -75,6 +75,24 @@ const VIOLATION_FIELDS = [
75
75
 
76
76
  type Row = Record<string, unknown>;
77
77
 
78
+ /** Characters of one text field the full standing carries; violationId reads one violation uncut. */
79
+ const FULL_TEXT_LIMIT = 1_500;
80
+
81
+ /** Resolved violations the full standing carries. */
82
+ const FULL_RESOLVED = 20;
83
+
84
+ /** A copy with every string longer than the limit cut and marked, at any depth. */
85
+ function cutLongText(value: unknown): unknown {
86
+ if (typeof value === 'string') {
87
+ return value.length > FULL_TEXT_LIMIT
88
+ ? `${value.slice(0, FULL_TEXT_LIMIT)}... [cut at ${FULL_TEXT_LIMIT} of ${value.length} characters; violationId reads it whole]`
89
+ : value;
90
+ }
91
+ if (Array.isArray(value)) return value.map(cutLongText);
92
+ if (value === null || typeof value !== 'object') return value;
93
+ return Object.fromEntries(Object.entries(value).map(([key, inner]) => [key, cutLongText(inner)]));
94
+ }
95
+
78
96
  /**
79
97
  * How bad a product's policy standing is, as counts: the controls something is
80
98
  * recorded against, the open violations by severity, the resolved ones. The
@@ -116,55 +134,41 @@ export function standingSummary(standing: {
116
134
 
117
135
  const findingId = idInput('finding', 'taiga_list_findings gives it');
118
136
 
137
+ /** Open findings by severity and kind, and the latest sweeps, as the first page of findings carries them. */
138
+ async function sweepOverview(client: TaigaClient, productId: string) {
139
+ const [summary, sweeps] = await Promise.all([
140
+ client.getMaintenanceSummary(productId),
141
+ client.listMaintenanceSweeps(productId, { limit: 3 }),
142
+ ]);
143
+ const latest = pickFields(sweeps.data, SWEEP_FIELDS);
144
+ // The summary's own run rows (one per repository) would repeat the sweeps with every column.
145
+ const {
146
+ lastRun: _lastRun,
147
+ lastRuns: _lastRuns,
148
+ topFindings: _top,
149
+ violatedControls: _controls,
150
+ ...counts
151
+ } = summary.data as Record<string, unknown>;
152
+ return {
153
+ ...counts,
154
+ latestSweeps: latest,
155
+ ...(latest.length === 0 ? { note: 'This product has never been swept: no findings is not an all-clear.' } : {}),
156
+ };
157
+ }
158
+
119
159
  export function registerMaintenanceTools(server: McpServer, client: TaigaClient) {
120
160
  if (!client.hasCapability('products:read')) return;
121
161
 
122
- server.registerTool(
123
- 'taiga_get_maintenance',
124
- {
125
- title: "Get a product's maintenance standing",
126
- description:
127
- "Where a product's repository stands: open findings by severity and kind, the most pressing ones, and " +
128
- 'the latest sweeps, so you can tell when the product was last checked and whether that check succeeded. A ' +
129
- 'quiet list from a sweep that last ran long ago, or that failed, is not an all-clear; a product never swept ' +
130
- 'says so. Sweeps run on their own after a push and periodically; nothing here starts one. This is the ' +
131
- 'overview of the scanners alone; taiga_list_findings lists each finding to act on, and ' +
132
- 'taiga_get_policy_standing judges the product against the policies (scanner findings and the documents).',
133
- inputSchema: z.object({ productId }),
134
- annotations: READ_ONLY,
135
- },
136
- (params) =>
137
- runTool(async () => {
138
- const [summary, sweeps] = await Promise.all([
139
- client.getMaintenanceSummary(params.productId),
140
- client.listMaintenanceSweeps(params.productId, { limit: 3 }),
141
- ]);
142
- const latest = pickFields(sweeps.data, SWEEP_FIELDS);
143
- // The sweeps are reported as `latestSweeps`, narrowed to SWEEP_FIELDS;
144
- // the summary's own run rows (PRO-2527: one per repository) would
145
- // repeat them with every column.
146
- const { lastRun: _lastRun, lastRuns: _lastRuns, ...standing } = summary.data;
147
- return toolResult({
148
- ...standing,
149
- latestSweeps: latest,
150
- ...(latest.length === 0 ? { note: 'This product has never been swept: no result is not an all-clear.' } : {}),
151
- });
152
- })
153
- );
154
-
155
162
  server.registerTool(
156
163
  'taiga_list_findings',
157
164
  {
158
165
  title: 'List maintenance findings',
159
166
  description:
160
- "List what the sweeps found in a product's repository: vulnerable or outdated dependencies, committed " +
161
- 'secrets, insecure code (sast) and infrastructure misconfigurations (iac_misconfig). Each row carries its ' +
162
- 'severity and its priority tier (act_now: known exploited; urgent: likely and serious; elevated: one of ' +
163
- 'the two; routine), where it is, who can fix it (remediableBy: coding_agent, or upstream_only for a ' +
164
- 'submodule or a dependency with no fixed version), the policy controls it breaches, and the initiative ' +
165
- 'already fixing it (initiativeId). Omit status for the findings that need action; work from the top tier ' +
166
- 'down. Read one in full with taiga_get_finding. taiga_get_maintenance has the counts and when the last ' +
167
- 'sweep ran, which says whether an empty list can be trusted.',
167
+ "List what maintenance sweeps found in a product's repository: dependencies, secrets, insecure code " +
168
+ '(sast) and infrastructure. The first page opens with sweep (open counts, latest sweeps), so an old or ' +
169
+ 'failed sweep is not read as an all-clear. Rows: severity, tier (act_now: known exploited; urgent: ' +
170
+ 'likely and serious; elevated: one of the two; routine), location, remediableBy (upstream_only: no fixed ' +
171
+ 'version), breached controls and initiativeId of a fix under way. Omit status for open findings. taiga_get_finding reads one.',
168
172
  inputSchema: z.object({
169
173
  productId,
170
174
  status: z
@@ -180,8 +184,28 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
180
184
  (params) =>
181
185
  runTool(async () => {
182
186
  const { productId: id, ...filters } = params;
183
- const result = await client.listFindings(id, filters);
187
+ // The sweep overview on the first page: when the product was last
188
+ // checked decides whether an empty list can be trusted.
189
+ const [result, sweep] = await Promise.all([
190
+ client.listFindings(id, filters),
191
+ // Best effort: the findings the agent asked for do not fail on the overview.
192
+ params.cursor === undefined
193
+ ? sweepOverview(client, id).catch((error: unknown) => {
194
+ if (error instanceof TaigaApiError || isNetworkFailure(error)) return 'unread' as const;
195
+ throw error;
196
+ })
197
+ : Promise.resolve(null),
198
+ ]);
184
199
  return toolResult({
200
+ // An overview that could not be read says so: without it an empty list would read as an all-clear.
201
+ ...(sweep === 'unread'
202
+ ? {
203
+ sweep: null,
204
+ sweepNote: 'The sweep overview could not be read: an empty list here is not an all-clear.',
205
+ }
206
+ : sweep
207
+ ? { sweep }
208
+ : {}),
185
209
  ...page(pickFields(result.data, FINDING_ROW_FIELDS), result.nextCursor),
186
210
  ...(typeof result.totalCount === 'number' ? { totalCount: result.totalCount } : {}),
187
211
  });
@@ -193,10 +217,10 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
193
217
  {
194
218
  title: 'Get a maintenance finding',
195
219
  description:
196
- 'Read one maintenance finding in full: what it is, how likely it is to be exploited (EPSS, the known-exploited list), ' +
197
- 'the advisory and the fixed version, the place in the code, the policy controls it breaches, why it is ' +
198
- 'ranked where it is, its history (reintroduced, previously resolved) and the remediation under way. A ' +
199
- "secret's value is never stored; the finding names the rule and the location only.",
220
+ 'Read one maintenance finding in full: exploit likelihood (EPSS, known-exploited list), advisory and ' +
221
+ 'fixed version, code location, breached policy controls, why it ranks where it does, history ' +
222
+ "(reintroduced, previously resolved) and the remediation under way. A secret's value is never stored, " +
223
+ 'only the rule and location.',
200
224
  inputSchema: z.object({ findingId }),
201
225
  annotations: READ_ONLY,
202
226
  },
@@ -214,24 +238,24 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
214
238
  {
215
239
  title: "Get a product's policy standing",
216
240
  description:
217
- "How a product stands against the organization's own published policies: the controls it violates, " +
218
- "each with its violations (critical and high only, each citing the policy's sentence and the document " +
219
- 'passage that contradicts it), what is being done about each (an initiative raised, a fix in progress, ' +
220
- 'awaiting verification), and when the documents were derived and assessed, led by a summary of the ' +
221
- 'counts. A control is breached when open scanner findings map to it, diverged when the assessment of the ' +
222
- 'product documents found them contradicting it (a control can be both; state names the stronger, ' +
223
- "breached). A violation's work is the initiative raised for it; live: false means that initiative is " +
224
- 'done or canceled without the violation going away, so it needs a person or a new fix. A control ' +
225
- 'breached by scanner findings counts them by tier (findingsByTier): a breach of ' +
226
- 'routine findings only, such as secrets in test fixtures, is not an urgent one. A control with no violation ' +
227
- 'is not listed, and it is not "passing" either: there is no such state; summary.controlsWithNothingRecorded counts them. Pass full: true for every ' +
228
- "violation's evidence and the resolved ones. Fix a control with taiga_fix_policy_violation.",
241
+ "How a product stands against the organization's published policies: summary counts, when the documents " +
242
+ 'were assessed, and each violated control with its violations (critical and high only, citing the policy ' +
243
+ 'sentence and the contradicting passage) and the work on each. state: breached (open scanner findings ' +
244
+ 'map to it, by findingsByTier; routine-only is not urgent) or diverged (the documents contradict it). ' +
245
+ "live: false means the violation's initiative ended without fixing it: it needs a person or a new fix. " +
246
+ 'An unlisted control is not "passing" (summary.controlsWithNothingRecorded counts them). full: true adds ' +
247
+ 'evidence and resolved ones. Fix with taiga_fix_policy_violation.',
229
248
  inputSchema: z.object({
230
249
  productId,
231
250
  full: z
232
251
  .boolean()
233
252
  .optional()
234
- .describe("Carry each violation's evidence, the policy excerpt and the resolved violations (default false)"),
253
+ .describe(
254
+ "Carry each violation's evidence and policy excerpt, cut to about 1,500 characters each, and up to 20 resolved violations (default false)"
255
+ ),
256
+ violationId: idInput('violation', 'this tool lists them')
257
+ .optional()
258
+ .describe('Only this violation, in full and uncut: its evidence, policy excerpt and work'),
235
259
  }),
236
260
  annotations: READ_ONLY,
237
261
  },
@@ -252,7 +276,36 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
252
276
  'all-clear. An assessment runs when the reality documents are derived or refreshed ' +
253
277
  '(taiga_refresh_reality_documents, paid work: ask first).'
254
278
  : null;
255
- if (params.full) return toolResult({ summary, ...(note ? { note } : {}), ...standing });
279
+ if (params.violationId) {
280
+ const one = [
281
+ ...violations,
282
+ ...(Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences : []),
283
+ ].find((violation) => (violation as Row)['id'] === params.violationId);
284
+ if (!one) {
285
+ return toolError(
286
+ 'NOT_FOUND',
287
+ 'No violation with that ID on this product; this tool without violationId lists them.'
288
+ );
289
+ }
290
+ return toolResult(one);
291
+ }
292
+ // The full form ran to 228k characters on a product with eleven violations (PRO-2800): each text is
293
+ // cut and the resolved list bounded; violationId reads one whole.
294
+ if (params.full) {
295
+ const resolvedAll = Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences : [];
296
+ return toolResult({
297
+ summary,
298
+ ...(note ? { note } : {}),
299
+ ...standing,
300
+ // Only the controls something is recorded against, as the summary answer lists them.
301
+ controls: listed,
302
+ divergences: cutLongText(violations),
303
+ resolvedDivergences: cutLongText(resolvedAll.slice(0, FULL_RESOLVED)),
304
+ ...(resolvedAll.length > FULL_RESOLVED
305
+ ? { resolvedNote: `The first ${FULL_RESOLVED} of ${resolvedAll.length} resolved violations.` }
306
+ : {}),
307
+ });
308
+ }
256
309
  return toolResult({
257
310
  summary,
258
311
  ...(note ? { note } : {}),
@@ -284,13 +337,11 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
284
337
  {
285
338
  title: 'Ignore or reopen a finding',
286
339
  description:
287
- 'Ignore a maintenance finding, with a reason, or reopen it. Ignore one that does not apply or whose risk is accepted, with the reason (false_positive, ' +
288
- 'not_reachable, accepted_risk, wont_fix), an optional note and an optional expiry after which it resurfaces ' +
289
- 'if it still exists; or reopen an ignored one (ignored: false). Ignored findings are kept and reviewable. ' +
290
- 'There is no "mark fixed": a finding resolves when the next sweep no longer detects it. For a secret that ' +
291
- 'is only in history, rotate the credential outside Taiga, then ignore it with a reason. Ignoring hides a ' +
292
- "shared signal and can record the team's acceptance of a risk, which is the person's decision: ask the " +
293
- 'person first.',
340
+ 'Ignore a maintenance finding with a reason, or reopen one (ignored: false). Reasons: false_positive, ' +
341
+ 'not_reachable, accepted_risk, wont_fix; plus an optional note and an expiry after which it resurfaces ' +
342
+ 'if still present. There is no "mark fixed": a finding resolves when the next sweep stops detecting it. ' +
343
+ 'For a secret only in history, rotate it outside Taiga, then ignore it. Ignoring can record the team ' +
344
+ "accepting a risk, which is the person's decision: ask first.",
294
345
  inputSchema: z.object({
295
346
  findingId,
296
347
  ignored: z.boolean().describe('true ignores the finding, false reopens it'),
@@ -327,14 +378,11 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
327
378
  {
328
379
  title: 'Fix a finding',
329
380
  description:
330
- "Ask Taiga to fix a maintenance finding: it raises an initiative and adds it to the end of the product's queue, where " +
331
- "the line plans it in turn; whether the build then waits for a person is the initiative's review gate, " +
332
- 'as for any initiative. The fix is a normal run that ends in a pull request you review and merge. With ' +
333
- "wholeGroup: true it fixes the finding's whole group in one initiative, as the page does: for a " +
334
- 'dependency, every open advisory on that package at that installed version in one version bump; for ' +
335
- 'code or infrastructure, every location the same rule flagged. Absent for a secret only in history, ' +
336
- 'anything in a submodule, and a dependency with no fixed version (remediableBy says upstream_only); the ' +
337
- 'API refuses those. Starts paid work.',
381
+ "Raise an initiative that fixes a maintenance finding, at the end of the product's line; it ends in a " +
382
+ "pull request a person merges, and the initiative's review gate applies. wholeGroup: true fixes the " +
383
+ 'group in one initiative: every open advisory on that package version in one bump, or every location the ' +
384
+ 'same rule flagged. Refused for a secret only in history, anything in a submodule, and remediableBy ' +
385
+ 'upstream_only.',
338
386
  inputSchema: z.object({
339
387
  productId,
340
388
  findingId,
@@ -378,11 +426,10 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
378
426
  {
379
427
  title: 'Fix policy violations',
380
428
  description:
381
- "Ask Taiga to fix a product's policy violations (it raises initiatives): name a control to cover every violation recorded " +
382
- 'against it that has no initiative yet, or a single violation (violationId) when the control has more ' +
383
- "than one. Taiga raises one initiative and adds it to the end of the product's queue, exactly as a " +
384
- 'finding fix does. The violation stays listed while the work is under way: only the next assessment ' +
385
- 'can record that the documents no longer show it. Starts paid work.',
429
+ "Raise one initiative that fixes a product's policy violations and add it to the end of the line. Name a " +
430
+ 'control to cover all its violations with no initiative yet, or one violationId when the control has ' +
431
+ 'several. The violation stays listed until the next assessment no longer finds it, even while the fix is ' +
432
+ 'under way.',
386
433
  inputSchema: z.object({
387
434
  productId,
388
435
  controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
@@ -100,13 +100,11 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
100
100
  {
101
101
  title: 'Get what runs where',
102
102
  description:
103
- "What runs in each of a product's environments, and recent deployments: what is running now, anything deploying, and the recent " +
104
- 'history, newest first: each deployment with its commit, pull request, status, who deployed it and, ' +
105
- 'for a failure, the error the provider reported. What runs is the most recent deployment that actually ' +
106
- 'succeeded, so a newer failed attempt shows beside the version still running, not in its place. The ' +
107
- 'history is mirrored from the provider, including deployments made outside Taiga; syncStatus says when ' +
108
- "it last synced. Pass environmentId for one environment's history. Taiga observes deployments and " +
109
- 'never performs them.',
103
+ "Read what runs in each of a product's environments and its recent deployments, newest first: commit, " +
104
+ "pull request, status, deployer and, on failure, the provider's error. What runs is the latest " +
105
+ 'successful deployment, so a newer failure shows beside it. History is mirrored from the provider, ' +
106
+ 'including deployments made outside Taiga; syncStatus says when it last synced. environmentId narrows to ' +
107
+ 'one environment. Taiga never deploys.',
110
108
  inputSchema: z.object({ productId, environmentId: environmentId.optional(), ...pagination }),
111
109
  annotations: READ_ONLY,
112
110
  },
@@ -127,7 +125,7 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
127
125
  // environment, or none deployed to yet, is not "nothing is wrong".
128
126
  const note =
129
127
  environments.rows.length === 0
130
- ? 'This product has no environments yet: an environment is defined with taiga_define_environment, ' +
128
+ ? 'This product has no environments yet: an environment is defined with taiga_create_environment, ' +
131
129
  'and its deployments are mirrored from the provider once it is.'
132
130
  : history.data.length === 0 && !params.cursor
133
131
  ? 'No deployments recorded yet: none was mirrored from the provider (syncStatus says when it last ' +
@@ -152,9 +150,9 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
152
150
  {
153
151
  title: "List a product's environments",
154
152
  description:
155
- "The places a product's software runs (development, staging, production or its own ladder), each with " +
156
- 'its ID, type, order and App URL. The environment IDs taiga_get_delivery_metrics, taiga_get_monitoring ' +
157
- 'and taiga_update_environment take. taiga_get_deployments lists them too, beside what runs where.',
153
+ "List a product's environments with ID, type, order and App URL. These are the environment IDs " +
154
+ 'taiga_get_delivery_metrics, taiga_get_monitoring and taiga_update_environment take. ' +
155
+ 'taiga_get_deployments lists them too, with what runs where.',
158
156
  inputSchema: z.object({ productId }),
159
157
  annotations: READ_ONLY,
160
158
  },
@@ -164,7 +162,7 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
164
162
  return toolResult({
165
163
  data: pickFields(rows, ENVIRONMENT_FIELDS),
166
164
  ...(more ? { more: 'More environments than this answer reads; the rest were left out.' } : {}),
167
- ...(rows.length === 0 ? { note: 'No environment is defined yet; taiga_define_environment adds one.' } : {}),
165
+ ...(rows.length === 0 ? { note: 'No environment is defined yet; taiga_create_environment adds one.' } : {}),
168
166
  });
169
167
  })
170
168
  );
@@ -174,13 +172,11 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
174
172
  {
175
173
  title: 'Get delivery metrics',
176
174
  description:
177
- "DORA's five delivery metrics for one environment (the one marked production, as the page " +
178
- 'reports them), computed from its deployment history over the last 30, 60 or 90 days: deployment ' +
179
- 'frequency, lead time and failed deployment recovery time (throughput), change-failure rate and rework ' +
180
- 'rate (instability), each with its trend against the window before, and a performance band for all but ' +
181
- 'rework rate, which DORA has published none for. Failures and recoveries are read from deployments, not ' +
182
- 'incidents. A trend built on fewer than ten deployments is approximate; a metric no deployment fed says ' +
183
- 'so instead of showing a number. Previews and checks do not count.',
175
+ "Read DORA's five delivery metrics for one environment (the page shows production) over 30, 60 or 90 " +
176
+ 'days: deployment frequency, lead time, failed deployment recovery time, change-failure rate and rework ' +
177
+ 'rate, each with its trend against the window before and a performance band (none for rework rate). ' +
178
+ 'Computed from deployments, not incidents; previews and checks do not count. Under ten deployments a ' +
179
+ 'trend is approximate; a metric with no data says so instead of a number.',
184
180
  inputSchema: z.object({ productId, environmentId, windowDays }),
185
181
  annotations: READ_ONLY,
186
182
  },
@@ -196,13 +192,11 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
196
192
  {
197
193
  title: "Get a product's monitoring",
198
194
  description:
199
- "How a product's environments are doing (health, uptime): each environment's current health from Taiga's availability " +
200
- 'checks of its App URL, with its uptime and when it was last checked; a status Taiga cannot verify now ' +
201
- 'reads as not checked rather than the last thing it knew, and a new environment as baseline learning. ' +
202
- "With environmentId, that environment's uptime over a 30, 60 or 90-day window, and the experience " +
203
- 'signals the beacon reports (Core Web Vitals, JavaScript errors, client-observed API health) where it is ' +
204
- 'installed; until then they read as not set up, which means no data, not good data. Setting up the ' +
205
- "beacon raises an initiative from the environment's settings in the web.",
195
+ "How a product's environments are doing: each one's health from Taiga's checks of its App URL, uptime " +
196
+ 'and last check (not checked when unverifiable now, baseline learning when new). With environmentId: ' +
197
+ 'uptime over 30, 60 or 90 days and the beacon signals (Core Web Vitals, JavaScript errors, API health); ' +
198
+ "not set up means no data. Setting up the beacon raises an initiative from the environment's settings in " +
199
+ 'the web.',
206
200
  inputSchema: z.object({
207
201
  productId,
208
202
  environmentId: environmentId.optional(),
@@ -220,7 +214,7 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
220
214
  ? {
221
215
  note:
222
216
  'No environment is monitored: monitoring starts when an environment has its App URL ' +
223
- '(taiga_define_environment, taiga_update_environment). No health is not good health.',
217
+ '(taiga_create_environment, taiga_update_environment). No health is not good health.',
224
218
  }
225
219
  : {}),
226
220
  });
@@ -265,16 +259,14 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
265
259
  if (!client.hasCapability('products:write')) return;
266
260
 
267
261
  server.registerTool(
268
- 'taiga_define_environment',
262
+ 'taiga_create_environment',
269
263
  {
270
- title: 'Define an environment',
264
+ title: 'Create an environment',
271
265
  description:
272
- "Add a place a product's software runs: development, staging, production or whatever ladder it uses. " +
273
- 'Deployments are recorded against an environment, monitoring is reported per environment, and plans ' +
274
- 'read where the work will run. Describing one gives Taiga no access to it: the pipeline deploys, Taiga ' +
275
- 'learns what happened from source control. Member-level work. Give it its App URL afterwards with ' +
276
- 'taiga_update_environment, which is all monitoring needs to start checking. Changes the ' +
277
- "product's setup for everybody.",
266
+ "Add an environment a product's software runs in (development, staging, production or its own). " +
267
+ 'Deployments, monitoring and plans are tracked per environment. Taiga gets no access to it: the pipeline ' +
268
+ 'deploys and Taiga learns what happened from source control. Members can do this. Then set its App URL ' +
269
+ "with taiga_update_environment so monitoring starts. Changes the product's setup for everybody.",
278
270
  inputSchema: z.object({
279
271
  productId,
280
272
  name: z.string().trim().min(1).max(100).describe('What the team calls it, e.g. staging'),
@@ -295,10 +287,9 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
295
287
  {
296
288
  title: 'Update an environment',
297
289
  description:
298
- 'Rename an environment, change its kind, or set its public App URL (null clears it). The App URL is all ' +
299
- "monitoring needs: once it is saved, Taiga checks the app's availability every few minutes on its own. " +
300
- 'Marking an environment production makes it the one delivery metrics report on. Changes the ' +
301
- "product's setup for everybody.",
290
+ 'Rename an environment, change its kind, or set its public App URL (null clears it). Once an App URL is ' +
291
+ 'saved, Taiga checks availability every few minutes. Marking one production makes it the one delivery ' +
292
+ "metrics report on. Changes the product's setup for everybody.",
302
293
  inputSchema: z.object({
303
294
  environmentId,
304
295
  name: z.string().trim().min(1).max(100).optional().describe('A new name'),
@@ -65,11 +65,10 @@ export function registerProductDocumentWrites(server: McpServer, client: TaigaCl
65
65
  {
66
66
  title: 'Update a section of a product document draft',
67
67
  description:
68
- 'Replace the text of one section of a product document on its plan timeline, in the draft. Every other ' +
69
- 'section stays as it is. A draft changes nothing downstream: agents keep reading the published version ' +
70
- 'until the draft is published (taiga_publish_product_document). Refused once Discovery is finished and ' +
71
- 'the set is locked: reopening Discovery is done in the web. Reality documents are derived from the code ' +
72
- 'and not edited.',
68
+ "Replace one section's text in a plan-timeline product document's draft; other sections stay. Agents " +
69
+ 'read the published version until taiga_publish_product_document. Refused once Discovery is finished and ' +
70
+ 'the set is locked (reopen it with taiga_reopen_discovery or in the web). Reality documents come from ' +
71
+ 'the code and are not edited.',
73
72
  inputSchema: z.object({
74
73
  productId,
75
74
  type: documentType,
@@ -91,9 +90,8 @@ export function registerProductDocumentWrites(server: McpServer, client: TaigaCl
91
90
  title: 'Publish a product document draft',
92
91
  description:
93
92
  "Publish a product document's draft as its new version, the one agents work from. Documents generated " +
94
- 'from it are not rewritten: each is marked outdated, and is regenerated when someone chooses to ' +
95
- '(taiga_regenerate_product_document, or taiga_generate_product_documents for every outdated and missing ' +
96
- 'one).',
93
+ 'from it are marked outdated, not rewritten; regenerate them with taiga_regenerate_product_document, or ' +
94
+ 'taiga_generate_product_documents for every outdated and missing one.',
97
95
  inputSchema: z.object({ productId, type: documentType }),
98
96
  annotations: OPERATE,
99
97
  },
@@ -105,10 +103,9 @@ export function registerProductDocumentWrites(server: McpServer, client: TaigaCl
105
103
  {
106
104
  title: 'Mark a product document reviewed',
107
105
  description:
108
- 'Record that a person reviewed a product document: its latest published version, as the Documents page ' +
109
- "shows it beside each version; once every published document is reviewed, the product's review item in " +
110
- 'Action Required resolves. The mark says a human read it, so make it only when the person tells you they ' +
111
- 'have: reading it yourself is not a review.',
106
+ "Record that a person reviewed a product document's latest published version. Once every published " +
107
+ "document is reviewed, the product's review item in Action Required resolves. Only when the person says " +
108
+ 'they read it; your own reading is not a review.',
112
109
  inputSchema: z.object({ productId, type: reviewableType }),
113
110
  annotations: OPERATE,
114
111
  },
@@ -122,13 +119,11 @@ export function registerProductDocumentWrites(server: McpServer, client: TaigaCl
122
119
  {
123
120
  title: 'Regenerate a product document',
124
121
  description:
125
- 'Ask an agent to generate one product document again from the documents it is derived from, for one ' +
126
- 'marked outdated or one that needs another pass. The Specification comes from the Discovery ' +
127
- 'conversation and the Look & Feel from its concepts, both in the web. The documents Discovery requires ' +
128
- 'are regenerated while it is open only: a finished Discovery locks them, and reopening it is done in ' +
129
- 'the web. The optional Service Blueprint can be generated whenever someone wants one. The result is published as the new version, and ' +
130
- 'the documents generated from it are marked outdated in turn. Runs an agent in the background: this ' +
131
- 'returns the job; taiga_wait_for_job says when it is done.',
122
+ 'Regenerate one product document from the documents it derives from, when it is outdated or needs ' +
123
+ 'another pass, and return the job. The result is published and documents derived from it are marked ' +
124
+ 'outdated. The Specification (from the Discovery conversation) and the Look & Feel (from its concepts) ' +
125
+ 'are made in the web. Documents Discovery requires regenerate only while Discovery is open; the optional ' +
126
+ 'Service Blueprint any time. taiga_wait_for_job says when it is done.',
132
127
  inputSchema: z.object({ productId, type: generatedType }),
133
128
  annotations: OPERATE,
134
129
  },
@@ -164,11 +159,10 @@ export function registerProductDocumentWrites(server: McpServer, client: TaigaCl
164
159
  {
165
160
  title: 'Generate the remaining product documents',
166
161
  description:
167
- 'Generate remaining: every document the product still needs, and every one marked outdated, in the ' +
168
- 'order they build on each other, one after another (each next one starts when the one before it is ' +
169
- 'published). Refused (CONFLICT) while a document is already being generated. On an imported product, ' +
170
- 'it queues every unpublished document. Runs agents in the background: this returns the jobs it started ' +
171
- '(jobIds); taiga_stop_generating_documents stops the run.',
162
+ 'Generate every missing or outdated product document (Generate remaining in the web), one after another ' +
163
+ 'in the order they build on each other; on an imported product, every unpublished one. Returns the jobs ' +
164
+ 'it started (jobIds). Refused (CONFLICT) while a document is already being generated. ' +
165
+ 'taiga_stop_generating_documents stops it.',
172
166
  inputSchema: z.object({ productId }),
173
167
  annotations: OPERATE,
174
168
  },
@@ -180,8 +174,8 @@ export function registerProductDocumentWrites(server: McpServer, client: TaigaCl
180
174
  {
181
175
  title: 'Stop generating product documents',
182
176
  description:
183
- 'Stop a Generate remaining run: every document of it still being generated is cancelled and nothing ' +
184
- 'after them starts; published documents stay. An empty list means no run was going. Interrupts agent work.',
177
+ 'Stop a Generate remaining run (taiga_generate_product_documents): documents still being generated are ' +
178
+ 'cancelled and nothing after them starts; published ones stay. An empty list means none was running.',
185
179
  inputSchema: z.object({ productId }),
186
180
  annotations: OPERATE,
187
181
  },