@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.
- package/dist/index.js +146 -88
- 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 (
|
|
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: "
|
|
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;
|
|
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
|
|
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).
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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"}
|
|
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
|
-
|
|
6709
|
-
|
|
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", "
|
|
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
|
|
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,
|
|
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 =
|
|
7140
|
-
const
|
|
7141
|
-
const
|
|
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" : ""}.
|
|
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.
|
|
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;
|
|
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.
|
|
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
|
-
|
|
22959
|
-
|
|
22960
|
-
|
|
22961
|
-
|
|
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(
|
|
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(
|
|
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
|
|
23029
|
+
until,
|
|
22987
23030
|
...outcome,
|
|
22988
23031
|
item: now ? itemReport(now) : null
|
|
22989
23032
|
});
|
|
22990
|
-
}
|
|
22991
|
-
|
|
22992
|
-
|
|
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
|
|
23041
|
+
until,
|
|
23008
23042
|
...outcome,
|
|
23009
23043
|
run: now ? readableRow(pickFields([now], RUN_REPORT_FIELDS)[0]) : null
|
|
23010
23044
|
});
|
|
23011
|
-
}
|
|
23012
|
-
|
|
23013
|
-
|
|
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.
|
|
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",
|