@taiga-ai/mcp-server 0.26.0-rc.510 → 0.26.0-rc.514

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 +72 -6
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -356,6 +356,17 @@ var TaigaClient = class {
356
356
  async getFinding(id) {
357
357
  return this.request("GET", `/maintenance/findings/${seg(id)}`);
358
358
  }
359
+ /** The package inventory (PRO-2862); 404 where the `licenses` feature is not enabled. */
360
+ async listPackages(productId, opts) {
361
+ return this.request("GET", `/products/${seg(productId)}/maintenance/packages${query$1({ ...opts })}`);
362
+ }
363
+ async getLicenseSummary(productId) {
364
+ return this.request("GET", `/products/${seg(productId)}/maintenance/licenses`);
365
+ }
366
+ /** A repository's CycloneDX 1.5 SBOM (PRO-2894), the document itself. */
367
+ async getSbom(productId, repositoryId) {
368
+ return this.request("GET", `/products/${seg(productId)}/maintenance/repositories/${seg(repositoryId)}/sbom`);
369
+ }
359
370
  async updateFinding(id, body) {
360
371
  return this.request("PATCH", `/maintenance/findings/${seg(id)}`, body);
361
372
  }
@@ -3924,6 +3935,10 @@ async function publishDraft(scope) {
3924
3935
  ...published.warnings ? { warnings: published.warnings } : {}
3925
3936
  });
3926
3937
  }
3938
+ /** 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. */
3939
+ function notControls(sections) {
3940
+ return sections.filter((id) => !isControlId(id.toUpperCase()));
3941
+ }
3927
3942
  /** The policies the section ids name when every one is a control id (SEC-5.1), else none. */
3928
3943
  function policiesNamed(sections) {
3929
3944
  if (sections.length === 0) return [];
@@ -3949,7 +3964,7 @@ function registerDocumentTools(server, client) {
3949
3964
  sections: params.sections?.map((id) => id.toUpperCase())
3950
3965
  } : params;
3951
3966
  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.");
3967
+ 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
3968
  const result = await client.listTenantDocuments({
3954
3969
  limit: args.limit,
3955
3970
  cursor: args.cursor
@@ -4707,7 +4722,7 @@ function waitingOn(rows, station) {
4707
4722
  }
4708
4723
  const STATION_NAMED = 3;
4709
4724
  /** Who holds the station, or null when the key cannot read the backlog or the read failed. */
4710
- async function stationHolders(client, product, head) {
4725
+ async function stationHolders(client, product, except) {
4711
4726
  if (!client.hasCapability("backlog:read")) return null;
4712
4727
  try {
4713
4728
  const columns = await Promise.all(["plan", "build"].map((state) => client.listBacklogItems(product, {
@@ -4715,7 +4730,7 @@ async function stationHolders(client, product, head) {
4715
4730
  order: "newest",
4716
4731
  limit: 100
4717
4732
  })));
4718
- const rows = columns.flatMap((page) => page.data).filter((row) => row.id !== head);
4733
+ const rows = columns.flatMap((page) => page.data).filter((row) => !except.has(row.id));
4719
4734
  const capped = columns.some((page) => page.nextCursor !== null);
4720
4735
  return {
4721
4736
  count: rows.length,
@@ -4791,10 +4806,14 @@ async function queueView(client, product, options = { name: "all" }) {
4791
4806
  });
4792
4807
  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
4808
  const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
4809
+ const idle = rows.length > 0 && blockingIndex(rows, (row) => row.entryState) < 0;
4810
+ const station = rows.length > 0 || options.name === "all" ? await stationHolders(client, product, new Set(idle ? [rows[0].backlogItemId] : rows.map((row) => row.backlogItemId))) : null;
4811
+ 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
4812
  return {
4795
4813
  linePaused: paused === null ? null : typeof paused === "string",
4796
4814
  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),
4815
+ waitingOn: line ?? waitingOn(rows, idle ? station : null),
4816
+ ...offLine,
4798
4817
  ...note ? { note } : {}
4799
4818
  };
4800
4819
  }
@@ -5398,6 +5417,19 @@ function registerPlanVerdicts(server, client) {
5398
5417
  * next sweep no longer detects it, so there is no tool for either.
5399
5418
  */
5400
5419
  /** A finding as a list row: enough to decide whether to open, fix or ignore it. */
5420
+ const PACKAGE_ROW_FIELDS = [
5421
+ "id",
5422
+ "ecosystem",
5423
+ "packageName",
5424
+ "version",
5425
+ "purl",
5426
+ "licenses",
5427
+ "scope",
5428
+ "relation",
5429
+ "sourcePath",
5430
+ "productRepositoryId",
5431
+ "detectedAt"
5432
+ ];
5401
5433
  const FINDING_ROW_FIELDS = [
5402
5434
  "id",
5403
5435
  "kind",
@@ -5619,6 +5651,35 @@ function registerMaintenanceTools(server, client) {
5619
5651
  ...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
5652
  });
5621
5653
  }));
5654
+ server.registerTool("taiga_list_packages", {
5655
+ title: "List the package inventory",
5656
+ 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.",
5657
+ inputSchema: z.object({
5658
+ productId,
5659
+ license: z.string().min(1).max(4096).optional().describe("Only packages carrying exactly this SPDX string"),
5660
+ search: z.string().min(1).max(255).optional().describe("Case-insensitive substring of the package name"),
5661
+ productRepositoryId: idInput("repository", "taiga_list_product_repositories gives it", "only this repository").optional(),
5662
+ ...pagination
5663
+ }),
5664
+ annotations: READ_ONLY
5665
+ }, (params) => runTool(async () => {
5666
+ const { productId: id, ...filters } = params;
5667
+ const [result, summary] = await Promise.all([client.listPackages(id, filters), params.cursor === void 0 ? client.getLicenseSummary(id) : Promise.resolve(null)]);
5668
+ return toolResult({
5669
+ ...summary ? { licenses: summary.data } : {},
5670
+ ...page(pickFields(result.data, PACKAGE_ROW_FIELDS), result.nextCursor),
5671
+ ...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {}
5672
+ });
5673
+ }));
5674
+ server.registerTool("taiga_get_sbom", {
5675
+ title: "Download a repository SBOM",
5676
+ 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.",
5677
+ inputSchema: z.object({
5678
+ productId,
5679
+ repositoryId: idInput("repository", "taiga_list_product_repositories gives it")
5680
+ }),
5681
+ annotations: READ_ONLY
5682
+ }, (params) => runTool(async () => toolResult(await client.getSbom(params.productId, params.repositoryId))));
5622
5683
  server.registerTool("taiga_get_finding", {
5623
5684
  title: "Get a maintenance finding",
5624
5685
  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 +6619,15 @@ async function standingPart(client, productId) {
6558
6619
  const standing = (await client.getPolicyStanding(productId)).data;
6559
6620
  const summary = standingSummary(standing);
6560
6621
  const state = standing.assessment?.state;
6622
+ const controls = Array.isArray(standing.controls) ? standing.controls : [];
6623
+ const withAny = (key) => controls.filter((control) => Number(control[key] ?? 0) > 0).length;
6561
6624
  return {
6562
6625
  openViolations: summary.violations,
6563
6626
  bySeverity: summary.bySeverity,
6564
- controlsViolated: summary.controls,
6627
+ controlsBreached: withAny("findingCount"),
6628
+ controlsDiverged: withAny("divergenceCount"),
6629
+ controlsMet: summary.controlsMet,
6630
+ controlsNotApplicable: summary.controlsNotApplicable,
6565
6631
  ...state === "never" || state === "failed" ? { note: `The assessment ${state === "never" ? "has never run" : "failed"}: no violations here is not an all-clear.` } : {}
6566
6632
  };
6567
6633
  }
@@ -6919,7 +6985,7 @@ const ENVIRONMENT_FIELDS = [
6919
6985
  */
6920
6986
  function deploymentLines(rows) {
6921
6987
  return rows.map((row) => {
6922
- const repository = row.metadata?.["repositoryFullName"];
6988
+ const repository = row.metadata?.repositoryFullName;
6923
6989
  return {
6924
6990
  ...pickFields([row], DEPLOYMENT_FIELDS)[0],
6925
6991
  repository: typeof repository === "string" ? repository : null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.26.0-rc.510",
3
+ "version": "0.26.0-rc.514",
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",