@taiga-ai/mcp-server 0.26.0-dev.509 → 0.26.0-dev.515

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 (2) hide show
  1. package/dist/index.js +187 -28
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -321,6 +321,7 @@ var TaigaClient = class {
321
321
  async listBuildRuns(productId, opts) {
322
322
  const params = new URLSearchParams();
323
323
  if (opts?.backlogItemId) params.set("backlogItemId", opts.backlogItemId);
324
+ if (opts?.status?.length) params.set("status", opts.status.join(","));
324
325
  if (opts?.limit) params.set("limit", String(opts.limit));
325
326
  if (opts?.cursor) params.set("cursor", opts.cursor);
326
327
  const qs = params.toString();
@@ -330,6 +331,7 @@ var TaigaClient = class {
330
331
  async listBuildRunActivity(opts) {
331
332
  const params = new URLSearchParams();
332
333
  if (opts?.active) params.set("active", "true");
334
+ if (opts?.status?.length) params.set("status", opts.status.join(","));
333
335
  if (opts?.limit) params.set("limit", String(opts.limit));
334
336
  if (opts?.cursor) params.set("cursor", opts.cursor);
335
337
  const qs = params.toString();
@@ -356,6 +358,17 @@ var TaigaClient = class {
356
358
  async getFinding(id) {
357
359
  return this.request("GET", `/maintenance/findings/${seg(id)}`);
358
360
  }
361
+ /** The package inventory (PRO-2862); 404 where the `licenses` feature is not enabled. */
362
+ async listPackages(productId, opts) {
363
+ return this.request("GET", `/products/${seg(productId)}/maintenance/packages${query$1({ ...opts })}`);
364
+ }
365
+ async getLicenseSummary(productId) {
366
+ return this.request("GET", `/products/${seg(productId)}/maintenance/licenses`);
367
+ }
368
+ /** A repository's CycloneDX 1.5 SBOM (PRO-2894), the document itself. */
369
+ async getSbom(productId, repositoryId) {
370
+ return this.request("GET", `/products/${seg(productId)}/maintenance/repositories/${seg(repositoryId)}/sbom`);
371
+ }
359
372
  async updateFinding(id, body) {
360
373
  return this.request("PATCH", `/maintenance/findings/${seg(id)}`, body);
361
374
  }
@@ -2461,7 +2474,7 @@ const DOCUMENT_INPUTS = Object.freeze({
2461
2474
  * reads as deleted) and fields they explain would simply be missing. So it
2462
2475
  * refuses to start, and says what to do.
2463
2476
  */
2464
- function apiLevelProblem(introspection, apiUrl, required = 3, version = SERVER_VERSION) {
2477
+ function apiLevelProblem(introspection, apiUrl, required = 4, version = SERVER_VERSION) {
2465
2478
  const offered = introspection.mcpApiLevel ?? 0;
2466
2479
  if (offered >= required) return null;
2467
2480
  return `The Taiga API at ${apiUrl} is older than @taiga-ai/mcp-server ${version} needs (API level ${offered}, needs ${required}). Run the version published for it, or wait for that API to be updated; an earlier release of this package works against it.`;
@@ -2703,8 +2716,8 @@ const BY_ARGUMENT = {
2703
2716
  initiativeId: ITEM_ID_HINT,
2704
2717
  runId: "taiga_list_runs lists the runs, of one product or of every product the key can see.",
2705
2718
  planId: "taiga_get_plan gives the initiative's active plan and its ID.",
2706
- repositoryId: "taiga_list_product_repositories lists the product's repository links; a linkable repository id is not a link ID.",
2707
- replacesRepositoryId: "taiga_list_product_repositories lists the product's repository links.",
2719
+ linkId: "taiga_list_product_repositories lists the product's repository links; a linkable repository id is not a link ID.",
2720
+ replacesLinkId: "taiga_list_product_repositories lists the product's repository links.",
2708
2721
  linkableRepositoryId: "taiga_list_linkable_repositories lists the repositories the GitHub connection reaches, with their ids.",
2709
2722
  installationId: "taiga_list_linkable_repositories lists the connected GitHub accounts.",
2710
2723
  jobId: "A job ID is what the tool that started the job returned (taiga_request_initiatives, taiga_start_import).",
@@ -2718,6 +2731,7 @@ const BY_ARGUMENT = {
2718
2731
  /** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
2719
2732
  const BY_TOOL = {
2720
2733
  taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
2734
+ taiga_get_sbom: () => "An SBOM exists once a sweep has inventoried that repository, where the licenses feature is enabled; taiga_list_packages (linkId) says whether it has packages, and taiga_list_product_repositories lists the links.",
2721
2735
  taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_initiative's situation says whether one is being planned or failed, and names the call that asks for one (taiga_queue_initiative off the line, taiga_plan_again for a failed plan)." : ITEM_ID_HINT
2722
2736
  };
2723
2737
  /**
@@ -2730,8 +2744,8 @@ const MISSING_THING = {
2730
2744
  initiativeId: /backlog item|initiative/i,
2731
2745
  runId: /\b(build )?run\b/i,
2732
2746
  planId: /\bplan\b/i,
2733
- repositoryId: /repository link|\blink\b/i,
2734
- replacesRepositoryId: /repository link|\blink\b/i,
2747
+ linkId: /repository link|\blink\b|repository/i,
2748
+ replacesLinkId: /repository link|\blink\b/i,
2735
2749
  linkableRepositoryId: /github repository/i,
2736
2750
  installationId: /installation|account/i,
2737
2751
  jobId: /\bjob\b/i,
@@ -2812,6 +2826,7 @@ function ownArgumentLike(key, own) {
2812
2826
  const PRODUCT_WORDS = {
2813
2827
  backlogItemId: "initiativeId",
2814
2828
  buildRunId: "runId",
2829
+ productRepositoryId: "linkId",
2815
2830
  latestBuild: "latestRun"
2816
2831
  };
2817
2832
  /**
@@ -2854,7 +2869,7 @@ function inProductWords(value) {
2854
2869
  }
2855
2870
  /** The API's 404 sentence in the product's words: "Backlog item not found" is an initiative not found. */
2856
2871
  function inProductWording(said) {
2857
- return said.replace(/^Backlog item not found/i, "Initiative not found").replace(/^Build run step not found/i, "Run step not found").replace(/^Build run not found/i, "Run not found").replace(/^Custom document not found/i, "Knowledge file not found").replace(/^(Branch "[^"]*" does not exist in \S+?)\.?$/i, "$1. taiga_list_repository_branches lists the branches it has.").replace(/^This product already has a repository\. Change it instead of linking another\./i, "This product already has a repository. Pass replacesRepositoryId to swap it in one step, or taiga_unlink_repository first (taiga_list_product_repositories gives its ID).");
2872
+ return said.replace(/^Backlog item not found/i, "Initiative not found").replace(/^Build run step not found/i, "Run step not found").replace(/^Build run not found/i, "Run not found").replace(/^Custom document not found/i, "Knowledge file not found").replace(/^(Branch "[^"]*" does not exist in \S+?)\.?$/i, "$1. taiga_list_repository_branches lists the branches it has.").replace(/^This product already has a repository\. Change it instead of linking another\./i, "This product already has a repository. Pass replacesLinkId to swap it in one step, or taiga_unlink_repository first (taiga_list_product_repositories gives its ID).");
2858
2873
  }
2859
2874
  /**
2860
2875
  * A float without its binary noise: six significant digits. Delivery metrics
@@ -3924,6 +3939,10 @@ async function publishDraft(scope) {
3924
3939
  ...published.warnings ? { warnings: published.warnings } : {}
3925
3940
  });
3926
3941
  }
3942
+ /** The section ids that are not control ids at all. GDPR-1.1 is a control id, so it is not among them, though it names no single policy. */
3943
+ function notControls(sections) {
3944
+ return sections.filter((id) => !isControlId(id.toUpperCase()));
3945
+ }
3927
3946
  /** The policies the section ids name when every one is a control id (SEC-5.1), else none. */
3928
3947
  function policiesNamed(sections) {
3929
3948
  if (sections.length === 0) return [];
@@ -3949,7 +3968,7 @@ function registerDocumentTools(server, client) {
3949
3968
  sections: params.sections?.map((id) => id.toUpperCase())
3950
3969
  } : params;
3951
3970
  if (args.type === void 0) {
3952
- if (args.sections) return toolError("VALIDATION_ERROR", named.length > 1 ? `Those control ids are in different policies (${named.join(", ")}): read one type at a time.` : "Name the document (type) to read its sections; a control id such as SEC-5.1 names its policy itself.");
3971
+ if (args.sections) return toolError("VALIDATION_ERROR", named.length > 1 ? `Those control ids are in different policies (${named.join(", ")}): read one type at a time.` : notControls(args.sections).length > 0 && notControls(args.sections).length < args.sections.length ? `Not a control id: ${notControls(args.sections).join(", ")}. A control id such as SEC-5.1 names its policy; any other section id needs type.` : "Name the document (type) to read its sections; a control id such as SEC-5.1 names its policy itself, but one of a family that spans policies (GDPR, NIS2) does not.");
3953
3972
  const result = await client.listTenantDocuments({
3954
3973
  limit: args.limit,
3955
3974
  cursor: args.cursor
@@ -4578,11 +4597,12 @@ function registerBuildTools(server, client) {
4578
4597
  if (!client.hasCapability("products:read")) return;
4579
4598
  server.registerTool("taiga_list_runs", {
4580
4599
  title: "List runs",
4581
- description: "List coding agent runs, newest first: status, branch, pull request, step progress, actions and outcome, one sentence saying how it stands: running, waiting for a person to review and merge, merged, failed, stopped, its pull request closed unmerged, or nobody waiting on it because its initiative ended. Without productId, every product's runs; active: true keeps running ones. taiga_get_run reads one; carry work on from the initiative (taiga_resume_initiative).",
4600
+ description: "List coding agent runs, newest first: status, branch, pull request, step progress, actions and outcome, one sentence saying how it stands: running, waiting for a person to review and merge, merged, failed, stopped, its pull request closed unmerged, or nobody waiting on it because its initiative ended. Without productId, every product's runs; active: true keeps running ones, and status narrows to some statuses (status: [\"failed\", \"stopped\"] for what failed). taiga_get_run reads one; carry work on from the initiative (taiga_resume_initiative).",
4582
4601
  inputSchema: z.object({
4583
4602
  productId: productId.optional(),
4584
4603
  initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it").optional().describe("Only this initiative's runs -- get from taiga_list_initiatives"),
4585
4604
  active: z.boolean().optional().describe("Only runs the agent is executing now (cloning through reviewing; a queued pending run is not yet executing and is left out); for the org-wide list"),
4605
+ status: z.array(z.enum(BUILD_RUN_STATUSES)).min(1).optional().describe("Only runs in these statuses, such as [\"failed\", \"stopped\"]"),
4586
4606
  ...pagination
4587
4607
  }),
4588
4608
  annotations: READ_ONLY
@@ -4593,6 +4613,7 @@ function registerBuildTools(server, client) {
4593
4613
  if (productIdOf) {
4594
4614
  const result = await client.listBuildRuns(productIdOf, {
4595
4615
  backlogItemId: params.initiativeId,
4616
+ status: params.status,
4596
4617
  limit: params.limit,
4597
4618
  cursor: params.cursor
4598
4619
  });
@@ -4707,7 +4728,7 @@ function waitingOn(rows, station) {
4707
4728
  }
4708
4729
  const STATION_NAMED = 3;
4709
4730
  /** Who holds the station, or null when the key cannot read the backlog or the read failed. */
4710
- async function stationHolders(client, product, head) {
4731
+ async function stationHolders(client, product, except) {
4711
4732
  if (!client.hasCapability("backlog:read")) return null;
4712
4733
  try {
4713
4734
  const columns = await Promise.all(["plan", "build"].map((state) => client.listBacklogItems(product, {
@@ -4715,7 +4736,7 @@ async function stationHolders(client, product, head) {
4715
4736
  order: "newest",
4716
4737
  limit: 100
4717
4738
  })));
4718
- const rows = columns.flatMap((page) => page.data).filter((row) => row.id !== head);
4739
+ const rows = columns.flatMap((page) => page.data).filter((row) => !except.has(row.id));
4719
4740
  const capped = columns.some((page) => page.nextCursor !== null);
4720
4741
  return {
4721
4742
  count: rows.length,
@@ -4791,10 +4812,14 @@ async function queueView(client, product, options = { name: "all" }) {
4791
4812
  });
4792
4813
  const note = unnamed === 0 ? null : !canName ? "Entries carry initiativeId only: naming them needs backlog:read, which this key lacks." : entries.length > NAMED_ENTRIES ? `Only the first ${NAMED_ENTRIES} entries are named; the rest carry initiativeId only.` : "An entry whose initiative could not be read carries initiativeId only; taiga_get_initiative reads it.";
4793
4814
  const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
4815
+ const idle = rows.length > 0 && blockingIndex(rows, (row) => row.entryState) < 0;
4816
+ const station = rows.length > 0 || options.name === "all" ? await stationHolders(client, product, new Set(idle ? [rows[0].backlogItemId] : rows.map((row) => row.backlogItemId))) : null;
4817
+ const offLine = !idle && station && station.count > 0 ? { offTheLine: `${station.atLeast ? "At least " : ""}${station.count} initiative${station.count === 1 ? " is" : "s are"} in Plan or Build without a queue entry (${station.titles.map((title) => `"${title}"`).join(", ")}${station.count > station.titles.length ? ", and more" : ""}): moved there by hand or waiting on a pull request review. Each holds the station like the active entry; taiga_list_initiatives with column: 'plan' or 'build' lists them.` } : {};
4794
4818
  return {
4795
4819
  linePaused: paused === null ? null : typeof paused === "string",
4796
4820
  entries: rows.map(({ lifecycle: _lifecycle, runId: _runId, ...row }) => row),
4797
- waitingOn: line ?? waitingOn(rows, rows.length > 0 && blockingIndex(rows, (row) => row.entryState) < 0 ? await stationHolders(client, product, rows[0].backlogItemId) : null),
4821
+ waitingOn: line ?? waitingOn(rows, idle ? station : null),
4822
+ ...offLine,
4798
4823
  ...note ? { note } : {}
4799
4824
  };
4800
4825
  }
@@ -5398,6 +5423,19 @@ function registerPlanVerdicts(server, client) {
5398
5423
  * next sweep no longer detects it, so there is no tool for either.
5399
5424
  */
5400
5425
  /** A finding as a list row: enough to decide whether to open, fix or ignore it. */
5426
+ const PACKAGE_ROW_FIELDS = [
5427
+ "id",
5428
+ "ecosystem",
5429
+ "packageName",
5430
+ "version",
5431
+ "purl",
5432
+ "licenses",
5433
+ "scope",
5434
+ "relation",
5435
+ "sourcePath",
5436
+ "productRepositoryId",
5437
+ "detectedAt"
5438
+ ];
5401
5439
  const FINDING_ROW_FIELDS = [
5402
5440
  "id",
5403
5441
  "kind",
@@ -5421,6 +5459,77 @@ const FINDING_ROW_FIELDS = [
5421
5459
  "ignoreReason",
5422
5460
  "ignoreExpiresAt"
5423
5461
  ];
5462
+ /** Locations a folded row names before it says how many more there are. */
5463
+ const GROUP_WHERE = 5;
5464
+ /** What makes two findings one rule's: its rule id for code, infrastructure and secrets, its license verdict for a license. */
5465
+ /** The scanner's title cap (deps.py _COLUMN_CAP): a title cut there ends in "...". */
5466
+ const TITLE_CAP = 300;
5467
+ /**
5468
+ * The license verdict of a license finding's title: what follows "<package> <version>: ". A title the scanner cut
5469
+ * at its cap has no whole verdict, so it folds with nothing: two cut titles could share a prefix and differ.
5470
+ */
5471
+ function licenseVerdict(row) {
5472
+ const title = String(row["title"] ?? "");
5473
+ if (title.length >= TITLE_CAP && title.endsWith("...")) return "";
5474
+ return title.split(": ").slice(1).join(": ");
5475
+ }
5476
+ function groupKey(row) {
5477
+ const status = String(row["status"] ?? "");
5478
+ const link = String(row["productRepositoryId"] ?? "");
5479
+ if (row["kind"] === "license_violation") {
5480
+ const verdict = licenseVerdict(row);
5481
+ return verdict ? `license|${status}|${link}|${verdict}` : null;
5482
+ }
5483
+ const location = row["location"];
5484
+ const rule = typeof location?.ruleId === "string" ? location.ruleId : null;
5485
+ return rule ? `${String(row["kind"])}|${String(row["severity"])}|${status}|${link}|${rule}` : null;
5486
+ }
5487
+ /** Where one finding is, in a few characters: its path and line, or its package. */
5488
+ function whereOf(row) {
5489
+ const location = row["location"];
5490
+ if (typeof location?.path === "string") return location.line ? `${location.path}:${String(location.line)}` : location.path;
5491
+ return [row["packageName"], row["currentVersion"]].filter(Boolean).join(" ") || String(row["id"]);
5492
+ }
5493
+ /**
5494
+ * Findings of one rule or license verdict on a page, folded into one row (PRO-2941): a review read seven
5495
+ * CKV_GHA_7 rows and fifteen sharp license rows, identical but for where. The first stands for them, with
5496
+ * count, every findingId (what fix and ignore take) and the first few locations.
5497
+ */
5498
+ function grouped(rows, fold) {
5499
+ if (!fold) return rows;
5500
+ const groups = /* @__PURE__ */ new Map();
5501
+ const order = [];
5502
+ for (const row of rows) {
5503
+ const key = groupKey(row);
5504
+ if (!key) {
5505
+ order.push(row);
5506
+ continue;
5507
+ }
5508
+ if (!groups.has(key)) {
5509
+ groups.set(key, []);
5510
+ order.push(key);
5511
+ }
5512
+ groups.get(key).push(row);
5513
+ }
5514
+ return order.map((entry) => {
5515
+ if (typeof entry !== "string") return entry;
5516
+ const members = groups.get(entry);
5517
+ if (members.length === 1) return members[0];
5518
+ const where = members.map(whereOf);
5519
+ const first = members[0];
5520
+ const shared = Object.fromEntries(Object.entries(first).filter(([key, value]) => key !== "id" && members.every((member) => JSON.stringify(member[key]) === JSON.stringify(value))));
5521
+ const location = first["location"];
5522
+ const said = first["kind"] === "license_violation" ? licenseVerdict(first) : typeof location?.ruleId === "string" ? location.ruleId : String(first["title"]);
5523
+ return {
5524
+ ...shared,
5525
+ ...shared["title"] === void 0 ? { title: said } : {},
5526
+ count: members.length,
5527
+ findingIds: members.map((member) => member["id"]),
5528
+ where: where.slice(0, GROUP_WHERE),
5529
+ ...where.length > GROUP_WHERE ? { moreWhere: where.length - GROUP_WHERE } : {}
5530
+ };
5531
+ });
5532
+ }
5424
5533
  /**
5425
5534
  * Whether taiga_fix_finding would take a finding, said on the row (PRO-2800): three reviews found a dependency
5426
5535
  * marked remediableBy coding_agent with no fixed version, which the API refuses to fix. The API's own refusals,
@@ -5579,13 +5688,14 @@ function registerMaintenanceTools(server, client) {
5579
5688
  if (!client.hasCapability("products:read")) return;
5580
5689
  server.registerTool("taiga_list_findings", {
5581
5690
  title: "List maintenance findings",
5582
- description: "List what maintenance sweeps found in a product's repository: dependencies, secrets, insecure code (sast) and infrastructure. The first page opens with sweep (open counts, latest sweeps), so an old or failed sweep is not read as an all-clear. Rows: severity, tier (act_now: known exploited; urgent: likely and serious; elevated: one of the two; routine), location, remediableBy (coding_agent: a run here can fix it; upstream_only: the code is in a submodule, so the fix belongs in its own repository), fixable: false when no fixed version is known yet, breached controls and initiativeId of a fix under way. Omit status for open findings. controlId lists the findings that breach one control. taiga_get_finding reads one.",
5691
+ description: "List what maintenance sweeps found in a product's repository: dependencies, secrets, insecure code (sast) and infrastructure. The first page opens with sweep (open counts, latest sweeps), so an old or failed sweep is not read as an all-clear. Rows: severity, tier (act_now: known exploited; urgent: likely and serious; elevated: one of the two; routine), location, remediableBy (coding_agent: a run here can fix it; upstream_only: the code is in a submodule, so the fix belongs in its own repository), fixable: false when no fixed version is known yet, breached controls and initiativeId of a fix under way. Omit status for open findings. controlId lists the findings that breach one control. Findings of one rule or license verdict in one repository fold into one row per page, with count, findingIds (what fix and ignore take), where, and only the fields they share; group: false lists each. taiga_get_finding reads one.",
5583
5692
  inputSchema: z.object({
5584
5693
  productId,
5585
5694
  controlId: z.string().trim().min(1).max(40).optional().describe("Only the findings that breach this control, such as SEC-5.1 (one page, searched across all)"),
5586
5695
  status: z.enum(MAINTENANCE_FINDING_STATUSES).optional().describe("Only findings in this status; omit for the ones needing action"),
5587
5696
  severity: z.enum(MAINTENANCE_FINDING_SEVERITIES).optional().describe("Only this severity"),
5588
5697
  kind: z.enum(MAINTENANCE_FINDING_KINDS).optional().describe("Only this kind of finding"),
5698
+ group: z.boolean().optional().describe("Fold findings of one rule or license on this page into one row (default true); false lists each"),
5589
5699
  ...pagination
5590
5700
  }),
5591
5701
  annotations: READ_ONLY
@@ -5600,7 +5710,7 @@ function registerMaintenanceTools(server, client) {
5600
5710
  cursor
5601
5711
  }), "", () => [""], params.limit ?? 25, (row) => Array.isArray(row["controls"]) && row["controls"].some((c) => String(c).toUpperCase() === wanted));
5602
5712
  return toolResult({
5603
- ...page(findingRows(found.rows), null),
5713
+ ...page(grouped(findingRows(found.rows), params.group !== false), null),
5604
5714
  ...found.more ? { more: found.rows.length < (params.limit ?? 25) ? `Only the first 1000 findings were searched; narrow by status, severity or kind to reach the rest.` : "There may be more than the limit carries: raise the limit, or narrow by status, severity or kind." } : {},
5605
5715
  ...found.rows.length === 0 && !found.more ? { note: `No ${params.status ? `${params.status} ` : "open "}finding breaches ${wanted}. That is no all-clear on its own: taiga_list_findings without controlId says when the product was last swept.` } : {}
5606
5716
  });
@@ -5614,11 +5724,43 @@ function registerMaintenanceTools(server, client) {
5614
5724
  sweep: null,
5615
5725
  sweepNote: "The sweep overview could not be read: an empty list here is not an all-clear."
5616
5726
  } : sweep ? { sweep } : {},
5617
- ...page(findingRows(result.data), result.nextCursor),
5727
+ ...page(grouped(findingRows(result.data), params.group !== false), result.nextCursor),
5618
5728
  ...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {},
5619
5729
  ...params.status === void 0 && params.severity === void 0 && params.kind === void 0 && typeof result.totalCount === "number" && sweep && sweep !== "unread" && typeof sweep["openFindings"] === "number" && sweep["openFindings"] !== result.totalCount ? { totalNote: "totalCount includes findings with a fix under way (remediating) and ignored findings whose ignore has expired; sweep.openFindings counts open ones only." } : {}
5620
5730
  });
5621
5731
  }));
5732
+ server.registerTool("taiga_list_packages", {
5733
+ title: "List the package inventory",
5734
+ description: "Every package a product's repositories resolve, vulnerable or not, as the latest sweep read it: name, version, ecosystem, SPDX licenses, scope (dev / runtime / unknown), relation (direct / transitive / unknown) and the lockfile. The first page opens with licenses: each license string across the inventory, how many packages carry it and its standing against the allowlist (allowed, not_allowed, unknown), plus the allowlist and when each repository was last inventoried. Filter by one license string (UNKNOWN for the ones the data source could not name), a name substring or a repository. A license outside the allowlist is also a license_violation finding in taiga_list_findings. Where the licenses feature is not enabled the product answers NOT_FOUND. taiga_get_sbom downloads one repository as CycloneDX.",
5735
+ inputSchema: z.object({
5736
+ productId,
5737
+ license: z.string().min(1).max(4096).optional().describe("Only packages carrying exactly this SPDX string"),
5738
+ search: z.string().min(1).max(255).optional().describe("Case-insensitive substring of the package name"),
5739
+ linkId: idInput("repository link", "taiga_list_product_repositories gives it", "only this repository").optional(),
5740
+ ...pagination
5741
+ }),
5742
+ annotations: READ_ONLY
5743
+ }, (params) => runTool(async () => {
5744
+ const { productId: id, linkId: link, ...filters } = params;
5745
+ const [result, summary] = await Promise.all([client.listPackages(id, {
5746
+ ...filters,
5747
+ ...link ? { productRepositoryId: link } : {}
5748
+ }), params.cursor === void 0 ? client.getLicenseSummary(id) : Promise.resolve(null)]);
5749
+ return toolResult({
5750
+ ...summary ? { licenses: summary.data } : {},
5751
+ ...page(pickFields(result.data, PACKAGE_ROW_FIELDS), result.nextCursor),
5752
+ ...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {}
5753
+ });
5754
+ }));
5755
+ server.registerTool("taiga_get_sbom", {
5756
+ title: "Download a repository SBOM",
5757
+ description: "One repository's software bill of materials as a CycloneDX 1.5 JSON document, rendered from the stored package inventory: metadata (timestamp, tools, the repository), every package with its purl, license and scope, and the packages the repository declares directly. The whole document is returned, so for a large repository the answer is large; read taiga_list_packages when a summary is enough. NOT_FOUND where the licenses feature is not enabled, and for a repository no sweep has inventoried yet.",
5758
+ inputSchema: z.object({
5759
+ productId,
5760
+ linkId: idInput("repository link", "taiga_list_product_repositories gives it")
5761
+ }),
5762
+ annotations: READ_ONLY
5763
+ }, (params) => runTool(async () => toolResult(await client.getSbom(params.productId, params.linkId))));
5622
5764
  server.registerTool("taiga_get_finding", {
5623
5765
  title: "Get a maintenance finding",
5624
5766
  description: "Read one maintenance finding: exploit likelihood (EPSS, known-exploited list), the advisory summarized (full: true for its whole record: every affected range and reference) and fixed version, code location, breached policy controls, why it ranks where it does, history (reintroduced, previously resolved) and the remediation under way. fixable: false with fixNote says taiga_fix_finding would refuse it. A secret's value is never stored, only the rule and location.",
@@ -6558,10 +6700,15 @@ async function standingPart(client, productId) {
6558
6700
  const standing = (await client.getPolicyStanding(productId)).data;
6559
6701
  const summary = standingSummary(standing);
6560
6702
  const state = standing.assessment?.state;
6703
+ const controls = Array.isArray(standing.controls) ? standing.controls : [];
6704
+ const withAny = (key) => controls.filter((control) => Number(control[key] ?? 0) > 0).length;
6561
6705
  return {
6562
6706
  openViolations: summary.violations,
6563
6707
  bySeverity: summary.bySeverity,
6564
- controlsViolated: summary.controls,
6708
+ controlsBreached: withAny("findingCount"),
6709
+ controlsDiverged: withAny("divergenceCount"),
6710
+ controlsMet: summary.controlsMet,
6711
+ controlsNotApplicable: summary.controlsNotApplicable,
6565
6712
  ...state === "never" || state === "failed" ? { note: `The assessment ${state === "never" ? "has never run" : "failed"}: no violations here is not an all-clear.` } : {}
6566
6713
  };
6567
6714
  }
@@ -6919,7 +7066,7 @@ const ENVIRONMENT_FIELDS = [
6919
7066
  */
6920
7067
  function deploymentLines(rows) {
6921
7068
  return rows.map((row) => {
6922
- const repository = row.metadata?.["repositoryFullName"];
7069
+ const repository = row.metadata?.repositoryFullName;
6923
7070
  return {
6924
7071
  ...pickFields([row], DEPLOYMENT_FIELDS)[0],
6925
7072
  repository: typeof repository === "string" ? repository : null
@@ -21942,7 +22089,11 @@ const GITHUB_REPOSITORY_FIELDS = [
21942
22089
  "linked",
21943
22090
  "linkedProduct"
21944
22091
  ];
21945
- const repositoryId = idInput("repository link", "taiga_list_product_repositories gives it; a linkable repository id is not one");
22092
+ /**
22093
+ * A product's link to a repository: one name for it in every tool (PRO-2941). Reviews met it as repositoryId
22094
+ * here, productRepositoryId on findings and packages, and linkableRepositoryId (a different thing) beside both.
22095
+ */
22096
+ const linkId = idInput("repository link", "taiga_list_product_repositories gives it (its id); a linkable repository id is not one");
21946
22097
  /** Round-robin over lists: the first of each, then the second of each, and so on. */
21947
22098
  function interleave(lists) {
21948
22099
  const out = [];
@@ -22006,11 +22157,11 @@ function registerRepositoryTools(server, client) {
22006
22157
  description: "List the branches of a product's linked repository, each with whether it is protected, to choose the one Taiga works on (taiga_set_repository_branch). truncated: true means there are more than listed.",
22007
22158
  inputSchema: z.object({
22008
22159
  productId,
22009
- repositoryId
22160
+ linkId
22010
22161
  }),
22011
22162
  annotations: READ_ONLY
22012
22163
  }, (params) => runTool(async () => {
22013
- const result = await client.listRepositoryBranches(params.productId, params.repositoryId);
22164
+ const result = await client.listRepositoryBranches(params.productId, params.linkId);
22014
22165
  return toolResult({
22015
22166
  data: result.data,
22016
22167
  truncated: result.truncated
@@ -22066,11 +22217,11 @@ function registerRepositoryTools(server, client) {
22066
22217
  if (!client.hasCapability("vcs:manage")) return;
22067
22218
  server.registerTool("taiga_link_repository", {
22068
22219
  title: "Link a repository to a product",
22069
- description: "Link a GitHub repository to a product (taiga_list_linkable_repositories gives its id). replacesRepositoryId swaps out the one linked now in one step (the same one changes nothing); linking while one is linked without it is refused. A managed link (isManaged) cannot be replaced or unlinked. Once linked, planning reads its code, runs open pull requests in it, sweeps scan it, and its deployment history is imported.",
22220
+ description: "Link a GitHub repository to a product (taiga_list_linkable_repositories gives its id). replacesLinkId swaps out that link in one step (the same one changes nothing). Whether a product may link more than one repository depends on the organization; when it may not, a second link is refused and the refusal says to pass replacesLinkId. A managed link (isManaged) cannot be replaced or unlinked. Once linked, planning reads its code, runs open pull requests in it, sweeps scan it, and its deployment history is imported.",
22070
22221
  inputSchema: z.object({
22071
22222
  productId,
22072
22223
  linkableRepositoryId: idInput("linkable repository", "taiga_list_linkable_repositories gives it (a row's id; not GitHub's numeric id, not a link ID)"),
22073
- replacesRepositoryId: repositoryId.optional().describe("The link this one replaces")
22224
+ replacesLinkId: linkId.optional().describe("The link this one replaces")
22074
22225
  }),
22075
22226
  annotations: OPERATE
22076
22227
  }, (params) => runTool(async () => {
@@ -22078,7 +22229,7 @@ function registerRepositoryTools(server, client) {
22078
22229
  try {
22079
22230
  linked = await client.linkRepository(params.productId, {
22080
22231
  githubRepositoryId: params.linkableRepositoryId,
22081
- ...params.replacesRepositoryId ? { replacesRepositoryId: params.replacesRepositoryId } : {}
22232
+ ...params.replacesLinkId ? { replacesRepositoryId: params.replacesLinkId } : {}
22082
22233
  });
22083
22234
  } catch (error) {
22084
22235
  const holder = await holderOf(client, error, params.productId);
@@ -22095,14 +22246,14 @@ function registerRepositoryTools(server, client) {
22095
22246
  description: "Choose the branch Taiga branches from and opens pull requests against (taiga_list_repository_branches lists them), or null for the repository's default. A protected branch is accepted, but Taiga may not be able to merge into it; the result says when it is protected. Optionally sets a label for the link.",
22096
22247
  inputSchema: z.object({
22097
22248
  productId,
22098
- repositoryId,
22249
+ linkId,
22099
22250
  baseBranch: z.string().trim().min(1).max(255).nullable().optional().describe("The branch; null for the default"),
22100
22251
  label: z.string().trim().min(1).max(100).nullable().optional().describe("A label for the link; null clears it")
22101
22252
  }),
22102
22253
  annotations: OPERATE_SETTING
22103
22254
  }, (params) => runTool(async () => {
22104
22255
  if (params.baseBranch === void 0 && params.label === void 0) return toolError("VALIDATION_ERROR", "Set baseBranch, label or both.");
22105
- const updated = (await client.updateRepository(params.productId, params.repositoryId, {
22256
+ const updated = (await client.updateRepository(params.productId, params.linkId, {
22106
22257
  ...params.baseBranch !== void 0 ? { baseBranch: params.baseBranch } : {},
22107
22258
  ...params.label !== void 0 ? { label: params.label } : {}
22108
22259
  })).data;
@@ -22114,16 +22265,16 @@ function registerRepositoryTools(server, client) {
22114
22265
  }));
22115
22266
  server.registerTool("taiga_unlink_repository", {
22116
22267
  title: "Unlink a repository from a product",
22117
- description: "Unlink a repository from a product. The product keeps its documents and initiatives, but nothing is planned against code, built or swept. The repository itself is untouched; one Taiga created for the product cannot be unlinked. To swap it, use taiga_link_repository with replacesRepositoryId.",
22268
+ description: "Unlink a repository from a product. The product keeps its documents and initiatives, but nothing is planned against code, built or swept. The repository itself is untouched; one Taiga created for the product cannot be unlinked. To swap it, use taiga_link_repository with replacesLinkId.",
22118
22269
  inputSchema: z.object({
22119
22270
  productId,
22120
- repositoryId
22271
+ linkId
22121
22272
  }),
22122
22273
  annotations: OPERATE
22123
22274
  }, (params) => runTool(async () => {
22124
- await client.unlinkRepository(params.productId, params.repositoryId);
22275
+ await client.unlinkRepository(params.productId, params.linkId);
22125
22276
  return toolResult({
22126
- repositoryId: params.repositoryId,
22277
+ linkId: params.linkId,
22127
22278
  unlinked: true
22128
22279
  });
22129
22280
  }));
@@ -22933,6 +23084,12 @@ function refusedArguments(args) {
22933
23084
  }));
22934
23085
  return toolError("VALIDATION_ERROR", `Invalid arguments for ${tool}. ${lines.map((line) => line.argument ? `${line.argument}: ${line.message}` : line.message).join(" ")}`, { arguments: lines });
22935
23086
  }
23087
+ /** Argument names this release replaced, and what replaced them. */
23088
+ const RENAMED_ARGUMENTS = {
23089
+ repositoryId: "linkId",
23090
+ replacesRepositoryId: "replacesLinkId",
23091
+ productRepositoryId: "linkId"
23092
+ };
22936
23093
  /**
22937
23094
  * A refused call says what to do next (PRO-2841), as a 404 already does. The
22938
23095
  * SDK validates the arguments before the handler runs, where the 404 hints
@@ -22947,6 +23104,8 @@ function withRecoveryHints(tool, schema, inputs, isRead) {
22947
23104
  const hinted = (issue) => {
22948
23105
  if (issue.code === "unrecognized_keys" && issue.keys?.length) {
22949
23106
  const pointers = issue.keys.map((key) => {
23107
+ const renamed = RENAMED_ARGUMENTS[key];
23108
+ if (renamed && (inputs.get(tool) ?? []).includes(renamed)) return `${tool} takes ${renamed}, which replaced ${key}.`;
22950
23109
  const own = ownArgumentLike(key, inputs.get(tool) ?? []);
22951
23110
  if (own) return `${tool} takes ${own}, spelled that way.`;
22952
23111
  const other = closestToolTaking(key, tool, inputs, isRead);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.26.0-dev.509",
3
+ "version": "0.26.0-dev.515",
4
4
  "description": "Taiga for AI coding assistants: read products, policies, initiatives and runs, and operate the line, over MCP",
5
5
  "mcpName": "ga.tai/mcp-server",
6
6
  "type": "module",