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

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 +146 -88
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2688,7 +2688,7 @@ function setupNextStep(product) {
2688
2688
  if (product?.status !== "draft") return null;
2689
2689
  if (isImportedProduct(product)) switch (importStatusOf(product)) {
2690
2690
  case "analyzing":
2691
- case "generating_docs": return `The import is running and derives the documents from the code (taiga_wait_for_job follows its job). Then review them, and ${FINISH_DISCOVERY}`;
2691
+ case "generating_docs": return `The import is running and derives the documents from the code (taiga_wait (jobId) follows its job). Then review them, and ${FINISH_DISCOVERY}`;
2692
2692
  case "complete": return "The import is done: review the reality documents (taiga_get_product_document without type), then taiga_finish_discovery opens the product and starts planning its first initiatives (paid agent work: ask the person first).";
2693
2693
  case "failed":
2694
2694
  case "cancelled": return "The last import did not finish: taiga_start_import runs it again.";
@@ -2724,7 +2724,7 @@ const BY_ARGUMENT = {
2724
2724
  factoryId: "taiga_list_factories lists the factories the key can see.",
2725
2725
  findingId: "taiga_list_findings lists a product's findings.",
2726
2726
  violationId: "taiga_get_policy_standing lists a product's policy violations.",
2727
- environmentId: "taiga_list_environments lists the product's environments with their IDs; a product ID is not one.",
2727
+ environmentId: "taiga_get_deployments lists the product's environments with their IDs; a product ID is not one.",
2728
2728
  knowledgeId: "taiga_list_knowledge lists the knowledge files.",
2729
2729
  productId: "taiga_list_products lists the products the key can see; a name is not an ID (taiga_get_product takes a slug)."
2730
2730
  };
@@ -3308,7 +3308,7 @@ function nextStepFor(state) {
3308
3308
  if (state.linked === false) return "Link its repository (taiga_list_linkable_repositories, then taiga_link_repository), then taiga_start_import.";
3309
3309
  if (state.linked === null && state.importState === "not started") return setupNextStep(state.product);
3310
3310
  switch (state.importState) {
3311
- case "running": return "The import is running and derives the documents from the code; taiga_wait_for_job follows it.";
3311
+ case "running": return "The import is running and derives the documents from the code; taiga_wait (jobId) follows it.";
3312
3312
  case "failed": return "The last import did not finish: taiga_start_import runs it again.";
3313
3313
  case "complete":
3314
3314
  if (state.missing && state.missing.length > 0) return `The import finished but ${named(state.missing)} ${state.missing.length === 1 ? "is" : "are"} not published: taiga_get_product_document without type shows them, taiga_publish_product_document publishes a draft.`;
@@ -3787,7 +3787,7 @@ function registerProductTools(server, client) {
3787
3787
  }));
3788
3788
  server.registerTool("taiga_get_product", {
3789
3789
  title: "Get product",
3790
- description: "Read one product and its settings by ID, slug or git remote URL (git remote get-url origin; a URL linked to more than one product is refused with their IDs): name, factory, kind, autonomy, what Taiga builds (application, infrastructure, deployment) and network access, unsetMeans saying what a null setting follows. In setup, readiness says whether it can finish now and what is missing (repository, import, unpublished documents, unstamped checks), plus nextStep. For initiatives, runs and findings, use taiga_get_product_context.",
3790
+ description: "Read one product's settings only (for where it stands, use taiga_get_product_context), by ID, slug or git remote URL (git remote get-url origin; a URL linked to more than one product is refused with their IDs): name, factory, kind, autonomy, what Taiga builds (application, infrastructure, deployment) and network access, unsetMeans saying what a null setting follows. In setup, readiness says whether it can finish now and what is missing (repository, import, unpublished documents, unstamped checks), plus nextStep. For initiatives, runs and findings, use taiga_get_product_context.",
3791
3791
  inputSchema: z.object({
3792
3792
  productId: productId.optional(),
3793
3793
  slug: slug.optional(),
@@ -4139,7 +4139,7 @@ function registerJobTools(server, client) {
4139
4139
  if (!client.hasCapability("products:read")) return;
4140
4140
  server.registerTool("taiga_get_job", {
4141
4141
  title: "Get an agent job",
4142
- description: "Read one agent job, such as a taiga_request_initiatives request: status (pending, processing, completed, failed, cancelled), progress and any error. A finished initiative request lists the initiatives it added, or why none (already covered, not readable as work, outside the product, or not allowed by a policy, with the rule it conflicts with and how to decide otherwise). taiga_wait_for_job waits for it.",
4142
+ description: "Read one agent job, such as a taiga_request_initiatives request: status (pending, processing, completed, failed, cancelled), progress and any error. A finished initiative request lists the initiatives it added, or why none (already covered, not readable as work, outside the product, or not allowed by a policy, with the rule it conflicts with and how to decide otherwise). taiga_wait (jobId) waits for it.",
4143
4143
  inputSchema: z.object({ jobId: idInput("job", "the tool that started the job returned it") }),
4144
4144
  annotations: READ_ONLY
4145
4145
  }, (params) => runTool(async () => toolResult(jobReport((await client.getAgentJob(params.jobId)).data))));
@@ -4221,6 +4221,20 @@ const RUN_OUTCOME_FIELDS = [
4221
4221
  * stalled in the hardening pass after the steps and the retry continues it.
4222
4222
  * Null for a run still going or that finished.
4223
4223
  */
4224
+ /**
4225
+ * The line of a failed step's error that says why (PRO-2941): a review found the reason buried in a
4226
+ * 2,000-character dump of git and lint output, behind runtime warnings. The first line that reads as an error,
4227
+ * else the first line; capped. Null when no step failed with an error.
4228
+ */
4229
+ function failureCause(steps) {
4230
+ const failed = steps.find((step) => step["status"] === "failed" && typeof step["error"] === "string");
4231
+ if (!failed) return null;
4232
+ const lines = String(failed["error"]).split("\n").map((line) => line.trim()).filter((line) => line && !/^\(node:\d+\)|^\(Use `node|^[⋯✔↓]/u.test(line));
4233
+ const telling = lines.find((line) => /\berror\b|cannot|not found|denied|refused|no such/iu.test(line)) ?? lines.find((line) => /fail|✖/iu.test(line)) ?? lines[0];
4234
+ if (!telling) return null;
4235
+ const said = telling.length > 200 ? `${telling.slice(0, 197)}...` : telling;
4236
+ return `step ${String(failed["stepOrder"])}: ${said}`;
4237
+ }
4224
4238
  function stoppedIn(run, steps) {
4225
4239
  if (run.prMerged === true || run.status !== "failed" && run.status !== "stopped") return null;
4226
4240
  if (!steps.some((step) => step.status !== "planned")) return "before the first step started, while setting up the run (cloning, preparing the workspace)";
@@ -4625,7 +4639,7 @@ function registerBuildTools(server, client) {
4625
4639
  }));
4626
4640
  server.registerTool("taiga_get_run", {
4627
4641
  title: "Get run",
4628
- description: "Read one coding-agent run: status and outcome (one sentence), pull request, hardening, and each step with its status, commit, files, verification and headline. scanCoverage names security scanners that did not run, and why. verification is Taiga's check after a step (tests, lint, build); verificationNote says when one failed throughout. A failed or stopped run has stoppedIn and failedReasonMeans. full: true adds the complete instructions and reports. To resume: taiga_resume_initiative; to stop: move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
4642
+ description: "Read one coding-agent run: status and outcome (one sentence), pull request, hardening, and each step with its status, commit, files, verification and headline. scanCoverage names security scanners that did not run, and why. verification is Taiga's check after a step (tests, lint, build); verificationNote says when one failed throughout. A failed or stopped run has stoppedIn, failedReasonMeans and cause (the line of the failed step's error that says why). full: true adds the complete instructions and reports. To resume: taiga_resume_initiative; to stop: move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
4629
4643
  inputSchema: z.object({
4630
4644
  runId,
4631
4645
  full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
@@ -4636,7 +4650,11 @@ function registerBuildTools(server, client) {
4636
4650
  const runRow = run.data;
4637
4651
  const rows = steps.data.map((step) => neverStarted(step));
4638
4652
  const where = stoppedIn(runRow, rows);
4639
- const stop = where ? { stoppedIn: where } : {};
4653
+ const cause = failureCause(rows);
4654
+ const stop = {
4655
+ ...where ? { stoppedIn: where } : {},
4656
+ ...cause ? { cause } : {}
4657
+ };
4640
4658
  const states = await initiativeStates(client, [runRow]);
4641
4659
  const readable = withInitiativeState(readableRun(runRow), states);
4642
4660
  const note = verificationNote(rows);
@@ -4818,7 +4836,7 @@ async function queueView(client, product, options = { name: "all" }) {
4818
4836
  return {
4819
4837
  linePaused: paused === null ? null : typeof paused === "string",
4820
4838
  entries: rows.map(({ lifecycle: _lifecycle, runId: _runId, ...row }) => row),
4821
- waitingOn: line ?? waitingOn(rows, idle ? station : null),
4839
+ waitingOn: line ?? waitingOn(rows, idle ? station : null) ?? (rows.length === 0 && "offTheLine" in offLine ? `Nothing is queued. ${offLine.offTheLine}` : null),
4822
4840
  ...offLine,
4823
4841
  ...note ? { note } : {}
4824
4842
  };
@@ -5247,7 +5265,7 @@ function registerBacklogReads(server, client) {
5247
5265
  function registerRequest(server, client) {
5248
5266
  server.registerTool("taiga_request_initiatives", {
5249
5267
  title: "Request new initiatives",
5250
- description: "Ask an agent to turn described work into initiatives in the product's Backlog, and return the job. One piece of work becomes one initiative, a program an ordered set with dependencies. It may add none and say why (already covered, not work, outside the product, or a named policy forbids it). Omit title and description for the one next initiative the product most needs. taiga_wait_for_job gives the outcome. One request per product at a time (CONFLICT). Nothing goes on the line.",
5268
+ description: "Ask an agent to turn described work into initiatives in the product's Backlog, and return the job. One piece of work becomes one initiative, a program an ordered set with dependencies. It may add none and say why (already covered, not work, outside the product, or a named policy forbids it). Omit title and description for the one next initiative the product most needs. taiga_wait (jobId) gives the outcome. One request per product at a time (CONFLICT). Nothing goes on the line.",
5251
5269
  inputSchema: z.object({
5252
5270
  productId,
5253
5271
  title: z.string().max(500).optional().describe("What the work is, in a line"),
@@ -5484,12 +5502,31 @@ function groupKey(row) {
5484
5502
  const rule = typeof location?.ruleId === "string" ? location.ruleId : null;
5485
5503
  return rule ? `${String(row["kind"])}|${String(row["severity"])}|${status}|${link}|${rule}` : null;
5486
5504
  }
5487
- /** Where one finding is, in a few characters: its path and line, or its package. */
5505
+ /**
5506
+ * Where one finding is, in a few characters: its file and line (and the resource a scanner names), or its package.
5507
+ * A review read seven "CKV_GHA_7" where the location keeps no path: the file is in sourcePaths.
5508
+ */
5488
5509
  function whereOf(row) {
5489
5510
  const location = row["location"];
5490
- if (typeof location?.path === "string") return location.line ? `${location.path}:${String(location.line)}` : location.path;
5511
+ const paths = Array.isArray(row["sourcePaths"]) ? row["sourcePaths"] : [];
5512
+ const file = typeof location?.path === "string" ? location.path : typeof paths[0] === "string" ? paths[0] : null;
5513
+ if (file) {
5514
+ const at = location?.line ? `${file}:${String(location.line)}` : file;
5515
+ return typeof location?.resource === "string" ? `${at} ${location.resource}` : at;
5516
+ }
5491
5517
  return [row["packageName"], row["currentVersion"]].filter(Boolean).join(" ") || String(row["id"]);
5492
5518
  }
5519
+ /** The words every member's title opens with, at a word boundary; empty when they share fewer than 20 characters. */
5520
+ function sharedTitle(titles) {
5521
+ let prefix = titles[0] ?? "";
5522
+ for (const title of titles.slice(1)) {
5523
+ let at = 0;
5524
+ while (at < prefix.length && prefix[at] === title[at]) at += 1;
5525
+ prefix = prefix.slice(0, at);
5526
+ }
5527
+ const cut = (prefix.length === (titles[0] ?? "").length ? prefix : prefix.slice(0, Math.max(0, prefix.lastIndexOf(" ")))).trim().replace(/[\s,(:;-]+$/, "");
5528
+ return cut.length >= 20 ? cut : "";
5529
+ }
5493
5530
  /**
5494
5531
  * Findings of one rule or license verdict on a page, folded into one row (PRO-2941): a review read seven
5495
5532
  * CKV_GHA_7 rows and fifteen sharp license rows, identical but for where. The first stands for them, with
@@ -5517,12 +5554,14 @@ function grouped(rows, fold) {
5517
5554
  if (members.length === 1) return members[0];
5518
5555
  const where = members.map(whereOf);
5519
5556
  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))));
5557
+ const shared = Object.fromEntries(Object.entries(first).filter(([key, value]) => key !== "id" && key !== "title" && members.every((member) => JSON.stringify(member[key]) === JSON.stringify(value))));
5521
5558
  const location = first["location"];
5522
- const said = first["kind"] === "license_violation" ? licenseVerdict(first) : typeof location?.ruleId === "string" ? location.ruleId : String(first["title"]);
5559
+ const words = sharedTitle(members.map((member) => String(member["title"] ?? "")));
5560
+ const rule = typeof location?.ruleId === "string" ? location.ruleId : null;
5561
+ const said = first["kind"] === "license_violation" ? licenseVerdict(first) : words ? rule && !words.includes(rule) ? `${rule}: ${words}` : words : rule ?? String(first["title"]);
5523
5562
  return {
5524
5563
  ...shared,
5525
- ...shared["title"] === void 0 ? { title: said } : {},
5564
+ title: said,
5526
5565
  count: members.length,
5527
5566
  findingIds: members.map((member) => member["id"]),
5528
5567
  where: where.slice(0, GROUP_WHERE),
@@ -5661,8 +5700,11 @@ function standingSummary(standing) {
5661
5700
  bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
5662
5701
  }
5663
5702
  const stated = (state) => controls.filter((c) => c["state"] === state).length;
5703
+ const withAny = (key) => controls.filter((control) => Number(control[key] ?? 0) > 0).length;
5664
5704
  return {
5665
- controls: listed.length,
5705
+ controlsWithViolations: listed.length,
5706
+ controlsBreached: withAny("findingCount"),
5707
+ controlsDiverged: withAny("divergenceCount"),
5666
5708
  controlsInPolicies: controls.length,
5667
5709
  controlsMet: stated("met"),
5668
5710
  controlsNotApplicable: stated("not_applicable"),
@@ -6148,6 +6190,8 @@ function isDue(row, now) {
6148
6190
  }
6149
6191
  /** Pages read for the summary; past this it says it counted only those. */
6150
6192
  const SUMMARY_PAGE = 100;
6193
+ /** Rows an org-wide first page carries when the caller names no limit: the summary answers the rest. */
6194
+ const ORG_WIDE_ROWS = 10;
6151
6195
  /** Pages the summary reads before it says it stopped: 1,000 items. */
6152
6196
  const SUMMARY_PAGES = 10;
6153
6197
  /** Every item the summary counts, up to SUMMARY_PAGES pages; nextCursor set when it stopped short. Null on failure. */
@@ -6269,8 +6313,10 @@ function registerInboxTools(server, client) {
6269
6313
  ...resolved ? { resolved: true } : {}
6270
6314
  };
6271
6315
  const first = params.cursor === void 0;
6316
+ const limit = params.limit ?? (params.productId ? void 0 : ORG_WIDE_ROWS);
6272
6317
  const result = await client.listInbox({
6273
6318
  ...query,
6319
+ ...limit !== void 0 ? { limit } : {},
6274
6320
  ...first ? { withTotal: true } : {}
6275
6321
  });
6276
6322
  const overview = !first || resolved ? null : !result.nextCursor ? result : await everyItem(client, query);
@@ -6309,7 +6355,7 @@ function registerInboxTools(server, client) {
6309
6355
  ...checked.unchecked > 0 ? (() => {
6310
6356
  const onPage = due.filter((row) => checked.uncheckedIds.has(String(row["id"]))).length;
6311
6357
  const beyond = checked.unchecked - onPage;
6312
- return { staleNote: `${[...onPage > 0 ? [`${onPage} review${onPage === 1 ? "" : "s"} on this page could not be checked for whether the subject moved on: those rows say staleChecked: false, and their readWith read tells`] : [], ...beyond > 0 ? [`${beyond} review${beyond === 1 ? "" : "s"} counted in the summary but not on this page could not be checked either, so the summary counts them open`] : []].join("; ")}.` };
6358
+ return { staleNote: `${[...onPage > 0 ? [`${onPage} review${onPage === 1 ? "" : "s"} on this page could not be checked for whether the subject moved on: those rows say staleChecked: false, and their readWith read tells`] : [], ...beyond > 0 ? [`${beyond} review${beyond === 1 ? "" : "s"} past this page could not be checked, so the summary counts them as open`] : []].join("; ")}.` };
6313
6359
  })() : {}
6314
6360
  });
6315
6361
  }));
@@ -6700,13 +6746,12 @@ async function standingPart(client, productId) {
6700
6746
  const standing = (await client.getPolicyStanding(productId)).data;
6701
6747
  const summary = standingSummary(standing);
6702
6748
  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;
6705
6749
  return {
6706
6750
  openViolations: summary.violations,
6707
6751
  bySeverity: summary.bySeverity,
6708
- controlsBreached: withAny("findingCount"),
6709
- controlsDiverged: withAny("divergenceCount"),
6752
+ controlsWithViolations: summary.controlsWithViolations,
6753
+ controlsBreached: summary.controlsBreached,
6754
+ controlsDiverged: summary.controlsDiverged,
6710
6755
  controlsMet: summary.controlsMet,
6711
6756
  controlsNotApplicable: summary.controlsNotApplicable,
6712
6757
  ...state === "never" || state === "failed" ? { note: `The assessment ${state === "never" ? "has never run" : "failed"}: no violations here is not an all-clear.` } : {}
@@ -7073,7 +7118,7 @@ function deploymentLines(rows) {
7073
7118
  };
7074
7119
  });
7075
7120
  }
7076
- const environmentId = idInput("environment", "taiga_list_environments lists them; a product ID is not one");
7121
+ const environmentId = idInput("environment", "taiga_get_deployments lists them; a product ID is not one");
7077
7122
  /** The windows the pages offer, as the schema's own values rather than a number checked afterwards. */
7078
7123
  const windowDays = z.literal([
7079
7124
  30,
@@ -7083,11 +7128,11 @@ const windowDays = z.literal([
7083
7128
  /** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
7084
7129
  const isApiFailure = (error) => error instanceof TaigaApiError || isNetworkFailure(error);
7085
7130
  /** A part that may fail on its own without failing the read: null and the reason, or the value. */
7086
- async function part(read) {
7131
+ async function part(read, rethrow = () => false) {
7087
7132
  try {
7088
7133
  return { value: await read() };
7089
7134
  } catch (error) {
7090
- if (!isApiFailure(error)) throw error;
7135
+ if (!isApiFailure(error) || rethrow(error)) throw error;
7091
7136
  return { unavailable: error instanceof TaigaApiError ? `${error.status}: ${error.apiMessage ?? ""}`.trim() : "unreachable" };
7092
7137
  }
7093
7138
  }
@@ -7118,7 +7163,7 @@ function registerObserveTools(server, client) {
7118
7163
  if (!client.hasCapability("products:read")) return;
7119
7164
  server.registerTool("taiga_get_deployments", {
7120
7165
  title: "Get what runs where",
7121
- description: "Read what runs in each of a product's environments and its recent deployments, newest first: commit, pull request, status, deployer and, on failure, the provider's error. What runs is the latest successful deployment, so a newer failure shows beside it. History is mirrored from the provider, including deployments made outside Taiga; syncStatus says when it last synced. environmentId narrows to one environment. Taiga never deploys.",
7166
+ description: "Read a product's environments with their IDs, what runs in each, and its recent deployments, newest first: commit, pull request, status, deployer and, on failure, the provider's error. What runs is the latest successful deployment, so a newer failure shows beside it. History is mirrored from the provider, including deployments made outside Taiga; syncStatus says when it last synced. environmentId narrows to one environment. Taiga never deploys.",
7122
7167
  inputSchema: z.object({
7123
7168
  productId,
7124
7169
  environmentId: environmentId.optional(),
@@ -7126,25 +7171,31 @@ function registerObserveTools(server, client) {
7126
7171
  }),
7127
7172
  annotations: READ_ONLY
7128
7173
  }, (params) => runTool(async () => {
7129
- const [environments, current, history, sync] = await Promise.all([
7174
+ const [environments, currentRead, historyRead, sync] = await Promise.all([
7130
7175
  allEnvironments(client, params.productId),
7131
- client.getCurrentDeployments(params.productId),
7132
- client.listDeployments(params.productId, {
7176
+ part(() => client.getCurrentDeployments(params.productId)),
7177
+ part(() => client.listDeployments(params.productId, {
7133
7178
  environmentId: params.environmentId,
7134
7179
  limit: params.limit ?? 10,
7135
7180
  cursor: params.cursor
7136
- }),
7181
+ }), (error) => Boolean(params.environmentId) && error instanceof TaigaApiError && error.status === 404),
7137
7182
  part(() => client.getDeploymentSyncStatus(params.productId))
7138
7183
  ]);
7139
- const now = current.data;
7140
- const note = environments.rows.length === 0 ? "This product has no environments yet: an environment is defined with taiga_create_environment, and its deployments are mirrored from the provider once it is." : history.data.length === 0 && !params.cursor ? "No deployments recorded yet: none was mirrored from the provider (syncStatus says when it last synced), or nothing has been deployed." : null;
7141
- const live = new Set([...now.current ?? [], ...now.deploying ?? []].map((row) => row.id));
7184
+ const now = "value" in currentRead ? currentRead.value.data : null;
7185
+ const history = "value" in historyRead ? historyRead.value : null;
7186
+ const note = environments.rows.length === 0 ? "This product has no environments yet: an environment is defined with taiga_create_environment, and its deployments are mirrored from the provider once it is." : history && history.data.length === 0 && !params.cursor ? "No deployments recorded yet: none was mirrored from the provider (syncStatus says when it last synced), or nothing has been deployed." : null;
7187
+ const live = new Set([...now?.current ?? [], ...now?.deploying ?? []].map((row) => row.id));
7188
+ const unavailable = {
7189
+ ..."unavailable" in currentRead ? { running: currentRead.unavailable } : {},
7190
+ ..."unavailable" in historyRead ? { history: historyRead.unavailable } : {}
7191
+ };
7142
7192
  return toolResult({
7143
7193
  environments: pickFields(environments.rows, ENVIRONMENT_FIELDS),
7144
7194
  ...environments.more ? { moreEnvironments: true } : {},
7145
- running: deploymentLines(now.current ?? []),
7146
- deploying: deploymentLines(now.deploying ?? []),
7147
- history: page(deploymentLines(history.data.filter((row) => !live.has(row.id))), history.nextCursor),
7195
+ running: now ? deploymentLines(now.current ?? []) : null,
7196
+ deploying: now ? deploymentLines(now.deploying ?? []) : null,
7197
+ history: history ? page(deploymentLines(history.data.filter((row) => !live.has(row.id))), history.nextCursor) : null,
7198
+ ...Object.keys(unavailable).length > 0 ? { unavailable } : {},
7148
7199
  ..."value" in sync ? { syncStatus: sync.value.data } : {
7149
7200
  syncStatus: null,
7150
7201
  syncUnavailable: sync.unavailable
@@ -7152,19 +7203,6 @@ function registerObserveTools(server, client) {
7152
7203
  ...note ? { note } : {}
7153
7204
  });
7154
7205
  }));
7155
- server.registerTool("taiga_list_environments", {
7156
- title: "List a product's environments",
7157
- description: "List a product's environments with ID, type, order and App URL. These are the environment IDs taiga_get_delivery_metrics, taiga_get_monitoring and taiga_update_environment take. taiga_get_deployments lists them too, with what runs where.",
7158
- inputSchema: z.object({ productId }),
7159
- annotations: READ_ONLY
7160
- }, (params) => runTool(async () => {
7161
- const { rows, more } = await allEnvironments(client, params.productId);
7162
- return toolResult({
7163
- data: pickFields(rows, ENVIRONMENT_FIELDS),
7164
- ...more ? { more: "More environments than this answer reads; the rest were left out." } : {},
7165
- ...rows.length === 0 ? { note: "No environment is defined yet; taiga_create_environment adds one." } : {}
7166
- });
7167
- }));
7168
7206
  server.registerTool("taiga_get_delivery_metrics", {
7169
7207
  title: "Get delivery metrics",
7170
7208
  description: "Read DORA's five delivery metrics for one environment (the page shows production) over 30, 60 or 90 days: deployment frequency, lead time, failed deployment recovery time, change-failure rate and rework rate, each with its trend against the window before and a performance band (none for rework rate). Computed from deployments, not incidents; previews and checks do not count. Under ten deployments a trend is approximate; a metric with no data says so instead of a number. Without environmentId, the product's production environment when it has exactly one.",
@@ -7184,7 +7222,7 @@ function registerObserveTools(server, client) {
7184
7222
  note: "This product has no environments, so there is nothing to measure: an environment is defined with taiga_create_environment, and its deployments are mirrored from the provider once it is."
7185
7223
  });
7186
7224
  const production = all.filter((env) => env.type === "production");
7187
- if (production.length !== 1 || more) return toolError("VALIDATION_ERROR", `Pass environmentId: the product has ${more ? "more environments than one read holds, and " : ""}${production.length === 0 ? "no" : production.length} production environment${production.length === 1 ? "" : "s"}${more ? " among the first" : ""}. taiga_list_environments lists them with their IDs.`, { environments: all.map((env) => ({
7225
+ if (production.length !== 1 || more) return toolError("VALIDATION_ERROR", `Pass environmentId: the product has ${more ? "more environments than one read holds, and " : ""}${production.length === 0 ? "no" : production.length} production environment${production.length === 1 ? "" : "s"}${more ? " among the first" : ""}. taiga_get_deployments lists them with their IDs.`, { environments: all.map((env) => ({
7188
7226
  id: env.id,
7189
7227
  name: env.name,
7190
7228
  type: env.type
@@ -7313,7 +7351,7 @@ function registerProductDocumentWrites(server, client) {
7313
7351
  }, (params) => runTool(async () => toolResult((await client.markProductDocumentReviewed(params.productId, params.type)).data)));
7314
7352
  if (canRead) server.registerTool("taiga_regenerate_product_document", {
7315
7353
  title: "Regenerate a product document",
7316
- description: "Regenerate one product document from the documents it derives from, when it is outdated or needs another pass, and return the job. The result is published and documents derived from it are marked outdated. The Specification (from the Discovery conversation) and the Look & Feel (from its concepts) are made in the web. Documents Discovery requires regenerate only while Discovery is open; the optional Service Blueprint any time. taiga_wait_for_job says when it is done.",
7354
+ description: "Regenerate one product document from the documents it derives from, when it is outdated or needs another pass, and return the job. The result is published and documents derived from it are marked outdated. The Specification (from the Discovery conversation) and the Look & Feel (from its concepts) are made in the web. Documents Discovery requires regenerate only while Discovery is open; the optional Service Blueprint any time. taiga_wait (jobId) says when it is done.",
7317
7355
  inputSchema: z.object({
7318
7356
  productId,
7319
7357
  type: generatedType
@@ -22658,7 +22696,7 @@ function registerSetupTools(server, client) {
22658
22696
  }));
22659
22697
  server.registerTool("taiga_refresh_reality_documents", {
22660
22698
  title: "Refresh a product's documents from its code",
22661
- description: "Derive the reality documents again from the code on the branch Taiga works on (effectiveBranch): all, or the ones named. For a document the import misread, refresh it instead of importing again; unchanged sections are not republished. Returns the job; taiga_wait_for_job says when it is done.",
22699
+ description: "Derive the reality documents again from the code on the branch Taiga works on (effectiveBranch): all, or the ones named. For a document the import misread, refresh it instead of importing again; unchanged sections are not republished. Returns the job; taiga_wait (jobId) says when it is done.",
22662
22700
  inputSchema: z.object({
22663
22701
  productId,
22664
22702
  docTypes: z.array(z.enum(REALITY_DERIVED_DOCUMENT_TYPES)).min(1).max(REALITY_DERIVED_DOCUMENT_TYPES.length).optional().describe("The documents to refresh; omit for all")
@@ -22687,7 +22725,7 @@ function registerSetupTools(server, client) {
22687
22725
  if (!client.hasCapability("products:read")) return;
22688
22726
  server.registerTool("taiga_start_import", {
22689
22727
  title: "Import a codebase into a product",
22690
- description: "Start importing a product created for import, and return the job. Taiga reads the linked repository and derives the full document set from the code, citing the files. Then the scanners sweep it, the documents are assessed against policies, and the first initiatives are planned when Discovery finishes. Refused for a scratch product, without a linked repository, or while an import runs. taiga_wait_for_job says when it is done (it takes a while).",
22728
+ description: "Start importing a product created for import, and return the job. Taiga reads the linked repository and derives the full document set from the code, citing the files. Then the scanners sweep it, the documents are assessed against policies, and the first initiatives are planned when Discovery finishes. Refused for a scratch product, without a linked repository, or while an import runs. taiga_wait (jobId) says when it is done (it takes a while).",
22691
22729
  inputSchema: z.object({ productId }),
22692
22730
  annotations: OPERATE
22693
22731
  }, (params) => runTool(async () => {
@@ -22954,20 +22992,25 @@ function judgeBuild(until) {
22954
22992
  return null;
22955
22993
  };
22956
22994
  }
22995
+ /** What each target may be waited for. A job has one condition, finished, so it takes none. */
22996
+ const UNTIL_FOR = {
22997
+ initiative: ITEM_CONDITIONS,
22998
+ run: BUILD_CONDITIONS
22999
+ };
23000
+ const ALL_CONDITIONS = [.../* @__PURE__ */ new Set([...ITEM_CONDITIONS, ...BUILD_CONDITIONS])];
23001
+ /**
23002
+ * One tool for waiting (PRO-2941): three wait tools, one per thing waited on, were two more names in a catalogue
23003
+ * every review ranked too large. The ID says what is waited on; until says for what, refused when it does not
23004
+ * apply to that thing.
23005
+ */
22957
23006
  function registerWaitTools(server, client, options) {
22958
- if (client.hasCapability("backlog:read")) server.registerTool("taiga_wait_for_initiative", {
22959
- title: "Wait for an initiative",
22960
- description: "Wait until an initiative reaches a condition instead of polling: changed, planned, your_turn (waits on a person), pr_open (open now; merged or closed does not count), run_settled (latest run stopped moving) or done. Returns reached: true when it holds; early with stoppedBecause when only a person can move it (a flag, failed plan, plan or pull request awaiting review, a cancel); or reached: false at the timeout: call again. A condition already true answers at once. Planning takes minutes, a run up to an hour.",
22961
- inputSchema: z.object({
22962
- initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it"),
22963
- until: z.enum(ITEM_CONDITIONS).describe("What to wait for"),
22964
- timeoutSeconds
22965
- }),
22966
- annotations: READ_ONLY
22967
- }, (params, ctx) => runTool(async () => {
23007
+ const items = client.hasCapability("backlog:read");
23008
+ const runs = client.hasCapability("products:read");
23009
+ if (!items && !runs) return;
23010
+ const waitInitiative = async (target, until, params, ctx) => {
22968
23011
  const gate = { product: void 0 };
22969
23012
  const read = async (signal) => {
22970
- const item = (await client.getBacklogItem(params.initiativeId, signal)).data;
23013
+ const item = (await client.getBacklogItem(target, signal)).data;
22971
23014
  if (gate.product === void 0 && typeof item.autoBuild !== "boolean") gate.product = client.hasCapability("products:read") ? await client.getProduct(String(item.productId), signal).then((p) => {
22972
23015
  const value = p.data.autoBuild;
22973
23016
  return typeof value === "boolean" ? value : true;
@@ -22977,48 +23020,31 @@ function registerWaitTools(server, client, options) {
22977
23020
  }) : null;
22978
23021
  return item;
22979
23022
  };
22980
- const { now, ...outcome } = await waitFor(read, judgeItem(params.until, gate), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
23023
+ const { now, ...outcome } = await waitFor(read, judgeItem(until, gate), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
22981
23024
  progressToken: ctx.mcpReq._meta?.progressToken,
22982
23025
  notify: ctx.mcpReq.notify,
22983
23026
  signal: ctx.mcpReq.signal
22984
23027
  });
22985
23028
  return toolResult({
22986
- until: params.until,
23029
+ until,
22987
23030
  ...outcome,
22988
23031
  item: now ? itemReport(now) : null
22989
23032
  });
22990
- }));
22991
- if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_run", {
22992
- title: "Wait for a run",
22993
- description: "Wait until a run reaches a condition: changed, step_done (one more step finished), pr_open (open now) or settled (in review, completed, failed, stopped). Returns reached: true when it holds; early with stoppedBecause if the run settled without it; or reached: false at the timeout: call again. A settled run answers settled at once; run is null if the first read timed out.",
22994
- inputSchema: z.object({
22995
- runId: idInput("run", "taiga_list_runs gives it"),
22996
- until: z.enum(BUILD_CONDITIONS).describe("What to wait for"),
22997
- timeoutSeconds
22998
- }),
22999
- annotations: READ_ONLY
23000
- }, (params, ctx) => runTool(async () => {
23001
- const { now, ...outcome } = await waitFor(async (signal) => (await client.getBuildRun(params.runId, signal)).data, judgeBuild(params.until), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
23033
+ };
23034
+ const waitRun = async (target, until, params, ctx) => {
23035
+ const { now, ...outcome } = await waitFor(async (signal) => (await client.getBuildRun(target, signal)).data, judgeBuild(until), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
23002
23036
  progressToken: ctx.mcpReq._meta?.progressToken,
23003
23037
  notify: ctx.mcpReq.notify,
23004
23038
  signal: ctx.mcpReq.signal
23005
23039
  });
23006
23040
  return toolResult({
23007
- until: params.until,
23041
+ until,
23008
23042
  ...outcome,
23009
23043
  run: now ? readableRow(pickFields([now], RUN_REPORT_FIELDS)[0]) : null
23010
23044
  });
23011
- }));
23012
- if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_job", {
23013
- title: "Wait for an agent job",
23014
- description: "Wait until an agent job finishes (completed, failed or cancelled). Returns the job as taiga_get_job reads it, with an initiative request's outcome, and reached: true; or reached: false at the timeout: call again to keep waiting. Most jobs take a few minutes.",
23015
- inputSchema: z.object({
23016
- jobId: idInput("job", "the tool that started the job returned it"),
23017
- timeoutSeconds
23018
- }),
23019
- annotations: READ_ONLY
23020
- }, (params, ctx) => runTool(async () => {
23021
- const { now, ...outcome } = await waitFor(async (signal) => (await client.getAgentJob(params.jobId, signal)).data, (job) => SETTLED_JOB_STATUSES.has(String(job.status)) ? { reached: true } : null, (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
23045
+ };
23046
+ const waitJob = async (target, params, ctx) => {
23047
+ const { now, ...outcome } = await waitFor(async (signal) => (await client.getAgentJob(target, signal)).data, (job) => SETTLED_JOB_STATUSES.has(String(job.status)) ? { reached: true } : null, (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
23022
23048
  progressToken: ctx.mcpReq._meta?.progressToken,
23023
23049
  notify: ctx.mcpReq.notify,
23024
23050
  signal: ctx.mcpReq.signal
@@ -23027,6 +23053,38 @@ function registerWaitTools(server, client, options) {
23027
23053
  ...outcome,
23028
23054
  job: now ? jobReport(now) : null
23029
23055
  });
23056
+ };
23057
+ server.registerTool("taiga_wait", {
23058
+ title: "Wait for an initiative, run or job",
23059
+ description: "Wait instead of polling, for one of: an initiative (initiativeId) to reach until: changed, planned, your_turn (waits on a person), pr_open (open now; merged or closed does not count), run_settled (latest run stopped moving) or done; a run (runId) to reach until: changed, step_done, pr_open or settled (in review, completed, failed, stopped); or an agent job (jobId) to finish, which takes no until. Returns reached: true when it holds; early with stoppedBecause when it cannot happen without a person; or reached: false at the timeout: call again. A condition already true answers at once. Planning takes minutes, a run up to an hour, most jobs a few minutes.",
23060
+ inputSchema: z.object({
23061
+ initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it").optional(),
23062
+ runId: idInput("run", "taiga_list_runs gives it").optional(),
23063
+ jobId: idInput("job", "the tool that started the job returned it").optional(),
23064
+ until: z.enum(ALL_CONDITIONS).optional().describe("What to wait for: an initiative and a run need one"),
23065
+ timeoutSeconds
23066
+ }),
23067
+ annotations: READ_ONLY
23068
+ }, (params, ctx) => runTool(async () => {
23069
+ if ([
23070
+ "initiativeId",
23071
+ "runId",
23072
+ "jobId"
23073
+ ].filter((key) => params[key] !== void 0).length !== 1) return toolError("VALIDATION_ERROR", "Name exactly one of initiativeId, runId or jobId: the thing to wait on.");
23074
+ if (params.jobId !== void 0) {
23075
+ if (params.until !== void 0) return toolError("VALIDATION_ERROR", "A job is waited on until it finishes: leave until out.");
23076
+ if (!runs) return toolError("FORBIDDEN", "Waiting on a job needs products:read, which this key lacks.");
23077
+ return waitJob(params.jobId, params, ctx);
23078
+ }
23079
+ const kind = params.initiativeId !== void 0 ? "initiative" : "run";
23080
+ const allowed = UNTIL_FOR[kind];
23081
+ if (params.until === void 0 || !allowed.includes(params.until)) return toolError("VALIDATION_ERROR", `Waiting on ${kind === "initiative" ? "an initiative" : "a run"} takes until: ${allowed.join(", ")}.`);
23082
+ if (kind === "initiative") {
23083
+ if (!items) return toolError("FORBIDDEN", "Waiting on an initiative needs backlog:read, which this key lacks.");
23084
+ return waitInitiative(params.initiativeId, params.until, params, ctx);
23085
+ }
23086
+ if (!runs) return toolError("FORBIDDEN", "Waiting on a run needs products:read, which this key lacks.");
23087
+ return waitRun(params.runId, params.until, params, ctx);
23030
23088
  }));
23031
23089
  }
23032
23090
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.26.0-dev.515",
3
+ "version": "0.26.0-dev.518",
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",