@taiga-ai/mcp-server 0.25.0-dev.468 → 0.25.0-dev.470

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/dist/index.js CHANGED
@@ -3741,6 +3741,22 @@ function runOutcome(run) {
3741
3741
  };
3742
3742
  }
3743
3743
  /**
3744
+ * What hardening left blocking, in a sentence: the findings the run names (PRO-2817), or, for a run reported before
3745
+ * it recorded them, how many.
3746
+ */
3747
+ function hardeningSaid(blocking, named) {
3748
+ const count = `Hardening ended with ${blocking} blocking finding${blocking === 1 ? "" : "s"} it could not fix`;
3749
+ if (named.length === 0) return `${count}; the run records how many, not which.`;
3750
+ const shown = named.slice(0, 3).map((finding) => {
3751
+ const message = String(finding["message"]);
3752
+ const where = finding["line"] ? `${String(finding["path"])}:${String(finding["line"])}` : String(finding["path"]);
3753
+ return `${message} (${String(finding["tool"])}, ${where})`;
3754
+ });
3755
+ const rest = blocking - shown.length;
3756
+ const recorded = named.length > shown.length ? `, ${named.length} recorded in taiga_get_run` : "";
3757
+ return `${count}: ${shown.join("; ")}${rest > 0 ? `; and ${rest} more${recorded}` : ""}.`;
3758
+ }
3759
+ /**
3744
3760
  * What each failedReason means for the next move (PRO-2800): reviewers met dispatch_rejected and agent_reported as
3745
3761
  * bare enums. As @taiga/types documents them.
3746
3762
  */
@@ -3765,10 +3781,11 @@ function readableRun(run, opts = {}) {
3765
3781
  const means = FAILED_REASON_MEANS[String(run["failedReason"])];
3766
3782
  const hardening = run["hardeningResult"];
3767
3783
  const blocking = typeof hardening?.blockingFindings === "number" ? hardening.blockingFindings : 0;
3784
+ const named = Array.isArray(hardening?.blocking) ? hardening.blocking : [];
3768
3785
  return {
3769
3786
  ...rest,
3770
3787
  ...means ? { failedReasonMeans: means } : {},
3771
- ...blocking > 0 ? { hardeningNote: `Hardening ended with ${blocking} blocking finding${blocking === 1 ? "" : "s"} it could not fix; the run records how many, not which.` } : {},
3788
+ ...blocking > 0 ? { hardeningNote: hardeningSaid(blocking, named) } : {},
3772
3789
  ...opts.row || error === void 0 ? {} : { error },
3773
3790
  ...opts.row || reviewEscalation === void 0 ? {} : { reviewEscalation },
3774
3791
  ...typeof rest["prNumber"] === "number" || rest["prUrl"] ? { prDraft } : {},
@@ -5227,7 +5244,8 @@ const SETUP_TASK_FIELDS = [
5227
5244
  "providerActionUrl",
5228
5245
  "inboxItemId",
5229
5246
  "inboxItemStatus",
5230
- "createdAt"
5247
+ "createdAt",
5248
+ "stale"
5231
5249
  ];
5232
5250
  /** The API's count across every page, on the first page; nothing from an API that does not send one. */
5233
5251
  function totalOf(result) {
@@ -5267,7 +5285,10 @@ function sameKeyOnce(rows) {
5267
5285
  const { alsoFiledAs: _also, ...was } = kept;
5268
5286
  for (const field of Object.keys(kept)) delete kept[field];
5269
5287
  Object.assign(kept, row, { alsoFiledAs: [...also, record(was)] });
5270
- } else kept["alsoFiledAs"] = [...also, record(row)];
5288
+ } else {
5289
+ kept["alsoFiledAs"] = [...also, record(row)];
5290
+ if (isOpen(row) && row["stale"] === void 0) delete kept["stale"];
5291
+ }
5271
5292
  }
5272
5293
  return out;
5273
5294
  }
@@ -5342,7 +5363,7 @@ function registerInboxTools(server, client) {
5342
5363
  }));
5343
5364
  server.registerTool("taiga_list_setup_tasks", {
5344
5365
  title: "List setup tasks",
5345
- description: "List what a person must set up for a product (secrets, variables, accounts), with exactly what and where: key (the name to create; Taiga never stores values), the provider page, and phase: prereq (the deploy fails without it), wiring (generated values back to providers) or followup (can wait until after launch). Runs do not wait for them. One product or all, oldest first; includeDone: true adds done and dismissed. A variable filed twice lists the others under alsoFiledAs; totalCount counts records, and a page that folded some says recordsOnPage. Unlike taiga_list_action_required, which lists the same work as items to resolve.",
5366
+ description: "List what a person must set up for a product (secrets, variables, accounts), with exactly what and where: key (the name to create; Taiga never stores values), the provider page, and phase: prereq (the deploy fails without it), wiring (generated values back to providers) or followup (can wait until after launch). Runs do not wait for them. One product or all, oldest first; includeDone: true adds done and dismissed. A variable filed twice lists the others under alsoFiledAs; totalCount counts records, and a page that folded some says recordsOnPage. stale on an open task says no document derived from the code since it was raised mentions its key: it may not apply any more, so check before asking anyone to set it. Unlike taiga_list_action_required, which lists the same work as items to resolve.",
5346
5367
  inputSchema: z.object({
5347
5368
  productId: productId.optional(),
5348
5369
  includeDone: z.boolean().optional().describe("Also the tasks already done or dismissed (default false)"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.25.0-dev.468",
3
+ "version": "0.25.0-dev.470",
4
4
  "description": "MCP server for Taiga -- gives AI assistants access to documents and products",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -6856,6 +6856,52 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
6856
6856
  expect(sentUrl(fetchSpy, '/v1/inbox').searchParams.has('withTotal')).toBe(false);
6857
6857
  });
6858
6858
 
6859
+ it("passes on a setup task's stale note (PRO-2819)", async () => {
6860
+ const stale = "None of the current code's documents, derived since this task was raised, mention JWT_SECRET.";
6861
+ const { client } = await bootMcpPair({
6862
+ capabilities: ALL_CAPABILITIES,
6863
+ routes: {
6864
+ 'GET /v1/setup-tasks': {
6865
+ data: [
6866
+ {
6867
+ id: ENV_ID,
6868
+ productId: ENV_ID,
6869
+ key: 'JWT_SECRET',
6870
+ inboxItemStatus: 'open',
6871
+ title: 'Set JWT_SECRET',
6872
+ stale,
6873
+ },
6874
+ ],
6875
+ nextCursor: null,
6876
+ },
6877
+ },
6878
+ });
6879
+
6880
+ const tasks = parseToolResult(await client.callTool({ name: 'taiga_list_setup_tasks', arguments: {} }));
6881
+ expect(tasks.data[0].stale).toBe(stale);
6882
+ });
6883
+
6884
+ it('drops the stale note from a folded variable when one open record of it is live (PRO-2819)', async () => {
6885
+ const stale = "None of the current code's documents, derived since this task was raised, mention JWT_SECRET.";
6886
+ const record = { productId: ENV_ID, key: 'JWT_SECRET', inboxItemStatus: 'open' };
6887
+ const { client } = await bootMcpPair({
6888
+ capabilities: ALL_CAPABILITIES,
6889
+ routes: {
6890
+ 'GET /v1/setup-tasks': {
6891
+ data: [
6892
+ { ...record, id: 'old', category: 'secret', title: 'Set JWT_SECRET', stale },
6893
+ { ...record, id: 'new', category: 'env_var', title: 'Set JWT_SECRET' },
6894
+ ],
6895
+ nextCursor: null,
6896
+ },
6897
+ },
6898
+ });
6899
+
6900
+ const tasks = parseToolResult(await client.callTool({ name: 'taiga_list_setup_tasks', arguments: {} }));
6901
+ expect(tasks.data).toHaveLength(1);
6902
+ expect(tasks.data[0].stale).toBeUndefined();
6903
+ });
6904
+
6859
6905
  it('carries who cancelled a failed plan and when, and leaves the item as it was when the history is refused', async () => {
6860
6906
  const failed = { id: ITEM_ID, title: 'Login', planningStatus: 'failed', planFailureCause: 'cancelled' };
6861
6907
  const history = {
@@ -8904,6 +8950,78 @@ describe('what the seventh agent review found (PRO-2800)', () => {
8904
8950
  expect(bundle.runs[0]).not.toHaveProperty('hardeningResult');
8905
8951
  });
8906
8952
 
8953
+ it('names the blocking hardening findings a run recorded', async () => {
8954
+ const { client } = await bootMcpPair({
8955
+ capabilities: ['products:read'],
8956
+ routes: {
8957
+ [`/v1/builds/${BUILD_ID}/steps`]: { data: [] },
8958
+ [`/v1/builds/${BUILD_ID}`]: {
8959
+ data: {
8960
+ id: BUILD_ID,
8961
+ productId: PRODUCT_ID,
8962
+ status: 'needs_review',
8963
+ prNumber: 4,
8964
+ hardeningResult: {
8965
+ passed: false,
8966
+ scanners: {},
8967
+ blockingFindings: 1,
8968
+ blocking: [
8969
+ {
8970
+ tool: 'betterleaks',
8971
+ severity: 'critical',
8972
+ path: 'src/config.rs',
8973
+ line: 12,
8974
+ message: 'generic-api-key: Hardcoded API key',
8975
+ },
8976
+ ],
8977
+ },
8978
+ },
8979
+ },
8980
+ },
8981
+ });
8982
+
8983
+ const run = parseToolResult(await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID } }));
8984
+
8985
+ expect(run.hardeningNote).toBe(
8986
+ 'Hardening ended with 1 blocking finding it could not fix: generic-api-key: Hardcoded API key (betterleaks, src/config.rs:12).'
8987
+ );
8988
+ });
8989
+
8990
+ it('always says where a blocking finding is, even when its title names the path', async () => {
8991
+ const { client } = await bootMcpPair({
8992
+ capabilities: ['products:read'],
8993
+ routes: {
8994
+ [`/v1/builds/${BUILD_ID}/steps`]: { data: [] },
8995
+ [`/v1/builds/${BUILD_ID}`]: {
8996
+ data: {
8997
+ id: BUILD_ID,
8998
+ productId: PRODUCT_ID,
8999
+ status: 'needs_review',
9000
+ hardeningResult: {
9001
+ passed: false,
9002
+ blockingFindings: 1,
9003
+ blocking: [
9004
+ {
9005
+ tool: 'betterleaks',
9006
+ severity: 'critical',
9007
+ path: 'src/config.rs',
9008
+ line: 12,
9009
+ message: 'aws-key: AWS key in src/config.rs',
9010
+ },
9011
+ ],
9012
+ },
9013
+ },
9014
+ },
9015
+ },
9016
+ });
9017
+
9018
+ const run = parseToolResult(await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID } }));
9019
+
9020
+ expect(run.hardeningNote).toBe(
9021
+ 'Hardening ended with 1 blocking finding it could not fix: aws-key: AWS key in src/config.rs (betterleaks, src/config.rs:12).'
9022
+ );
9023
+ });
9024
+
8907
9025
  it('says what the API says when it names the product holding a repository', async () => {
8908
9026
  const { client, fetchSpy } = await bootMcpPair({
8909
9027
  capabilities: ALL_CAPABILITIES,
@@ -222,6 +222,24 @@ export function runOutcome(run: Record<string, unknown>): { disposition: string;
222
222
  return { disposition, outcome: sentence[disposition] ?? status };
223
223
  }
224
224
 
225
+ /**
226
+ * What hardening left blocking, in a sentence: the findings the run names (PRO-2817), or, for a run reported before
227
+ * it recorded them, how many.
228
+ */
229
+ function hardeningSaid(blocking: number, named: Record<string, unknown>[]): string {
230
+ const count = `Hardening ended with ${blocking} blocking finding${blocking === 1 ? '' : 's'} it could not fix`;
231
+ if (named.length === 0) return `${count}; the run records how many, not which.`;
232
+ const shown = named.slice(0, 3).map((finding) => {
233
+ const message = String(finding['message']);
234
+ const where = finding['line'] ? `${String(finding['path'])}:${String(finding['line'])}` : String(finding['path']);
235
+ // Always where it is, though a scanner's title may repeat the path: a substring test would drop a real one.
236
+ return `${message} (${String(finding['tool'])}, ${where})`;
237
+ });
238
+ const rest = blocking - shown.length;
239
+ const recorded = named.length > shown.length ? `, ${named.length} recorded in taiga_get_run` : '';
240
+ return `${count}: ${shown.join('; ')}${rest > 0 ? `; and ${rest} more${recorded}` : ''}.`;
241
+ }
242
+
225
243
  /**
226
244
  * What each failedReason means for the next move (PRO-2800): reviewers met dispatch_rejected and agent_reported as
227
245
  * bare enums. As @taiga/types documents them.
@@ -247,14 +265,15 @@ export function readableRun(run: Record<string, unknown>, opts: { row?: boolean
247
265
  const { prDraft, error, reviewEscalation, runtime: _runtime, ...rest } = run;
248
266
  const said = runOutcome(run);
249
267
  const means = FAILED_REASON_MEANS[String(run['failedReason'])];
250
- const hardening = run['hardeningResult'] as { blockingFindings?: unknown } | null | undefined;
268
+ const hardening = run['hardeningResult'] as { blockingFindings?: unknown; blocking?: unknown } | null | undefined;
251
269
  const blocking = typeof hardening?.blockingFindings === 'number' ? hardening.blockingFindings : 0;
270
+ const named = Array.isArray(hardening?.blocking) ? (hardening.blocking as Record<string, unknown>[]) : [];
252
271
  return {
253
272
  ...rest,
254
273
  ...(means ? { failedReasonMeans: means } : {}),
255
274
  ...(blocking > 0
256
275
  ? {
257
- hardeningNote: `Hardening ended with ${blocking} blocking finding${blocking === 1 ? '' : 's'} it could not fix; the run records how many, not which.`,
276
+ hardeningNote: hardeningSaid(blocking, named),
258
277
  }
259
278
  : {}),
260
279
  // A row's outcome says the error and the handback; the full read keeps them whole.
@@ -306,6 +306,7 @@ const SETUP_TASK_FIELDS = [
306
306
  'inboxItemId',
307
307
  'inboxItemStatus',
308
308
  'createdAt',
309
+ 'stale',
309
310
  ] as const;
310
311
 
311
312
  /** The API's count across every page, on the first page; nothing from an API that does not send one. */
@@ -350,6 +351,9 @@ export function sameKeyOnce(rows: Row[]): Row[] {
350
351
  Object.assign(kept, row, { alsoFiledAs: [...also, record(was)] });
351
352
  } else {
352
353
  kept['alsoFiledAs'] = [...also, record(row)];
354
+ // Stale only while every open record is (PRO-2819): one raised after the code's documents were derived
355
+ // is a live ask, whichever record leads.
356
+ if (isOpen(row) && row['stale'] === undefined) delete kept['stale'];
353
357
  }
354
358
  }
355
359
  return out;
@@ -471,7 +475,9 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
471
475
  'deploy fails without it), wiring (generated values back to providers) or followup (can wait until after ' +
472
476
  'launch). Runs do not wait for them. One product or all, oldest first; includeDone: true adds done and ' +
473
477
  'dismissed. A variable filed twice lists the others under alsoFiledAs; totalCount counts records, and a ' +
474
- 'page that folded some says recordsOnPage. Unlike ' +
478
+ 'page that folded some says recordsOnPage. stale on an open task says no document derived from the code ' +
479
+ 'since it was raised mentions its key: it may not apply any more, so check before asking anyone to set ' +
480
+ 'it. Unlike ' +
475
481
  'taiga_list_action_required, which lists the same work as items to resolve.',
476
482
  inputSchema: z.object({
477
483
  productId: productId.optional(),