@taiga-ai/mcp-server 0.26.0-dev.511 → 0.26.0-dev.516
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 +273 -101
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -321,6 +321,7 @@ var TaigaClient = class {
|
|
|
321
321
|
async listBuildRuns(productId, opts) {
|
|
322
322
|
const params = new URLSearchParams();
|
|
323
323
|
if (opts?.backlogItemId) params.set("backlogItemId", opts.backlogItemId);
|
|
324
|
+
if (opts?.status?.length) params.set("status", opts.status.join(","));
|
|
324
325
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
325
326
|
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
326
327
|
const qs = params.toString();
|
|
@@ -330,6 +331,7 @@ var TaigaClient = class {
|
|
|
330
331
|
async listBuildRunActivity(opts) {
|
|
331
332
|
const params = new URLSearchParams();
|
|
332
333
|
if (opts?.active) params.set("active", "true");
|
|
334
|
+
if (opts?.status?.length) params.set("status", opts.status.join(","));
|
|
333
335
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
334
336
|
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
335
337
|
const qs = params.toString();
|
|
@@ -356,6 +358,17 @@ var TaigaClient = class {
|
|
|
356
358
|
async getFinding(id) {
|
|
357
359
|
return this.request("GET", `/maintenance/findings/${seg(id)}`);
|
|
358
360
|
}
|
|
361
|
+
/** The package inventory (PRO-2862); 404 where the `licenses` feature is not enabled. */
|
|
362
|
+
async listPackages(productId, opts) {
|
|
363
|
+
return this.request("GET", `/products/${seg(productId)}/maintenance/packages${query$1({ ...opts })}`);
|
|
364
|
+
}
|
|
365
|
+
async getLicenseSummary(productId) {
|
|
366
|
+
return this.request("GET", `/products/${seg(productId)}/maintenance/licenses`);
|
|
367
|
+
}
|
|
368
|
+
/** A repository's CycloneDX 1.5 SBOM (PRO-2894), the document itself. */
|
|
369
|
+
async getSbom(productId, repositoryId) {
|
|
370
|
+
return this.request("GET", `/products/${seg(productId)}/maintenance/repositories/${seg(repositoryId)}/sbom`);
|
|
371
|
+
}
|
|
359
372
|
async updateFinding(id, body) {
|
|
360
373
|
return this.request("PATCH", `/maintenance/findings/${seg(id)}`, body);
|
|
361
374
|
}
|
|
@@ -2461,7 +2474,7 @@ const DOCUMENT_INPUTS = Object.freeze({
|
|
|
2461
2474
|
* reads as deleted) and fields they explain would simply be missing. So it
|
|
2462
2475
|
* refuses to start, and says what to do.
|
|
2463
2476
|
*/
|
|
2464
|
-
function apiLevelProblem(introspection, apiUrl, required =
|
|
2477
|
+
function apiLevelProblem(introspection, apiUrl, required = 4, version = SERVER_VERSION) {
|
|
2465
2478
|
const offered = introspection.mcpApiLevel ?? 0;
|
|
2466
2479
|
if (offered >= required) return null;
|
|
2467
2480
|
return `The Taiga API at ${apiUrl} is older than @taiga-ai/mcp-server ${version} needs (API level ${offered}, needs ${required}). Run the version published for it, or wait for that API to be updated; an earlier release of this package works against it.`;
|
|
@@ -2675,7 +2688,7 @@ function setupNextStep(product) {
|
|
|
2675
2688
|
if (product?.status !== "draft") return null;
|
|
2676
2689
|
if (isImportedProduct(product)) switch (importStatusOf(product)) {
|
|
2677
2690
|
case "analyzing":
|
|
2678
|
-
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}`;
|
|
2679
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).";
|
|
2680
2693
|
case "failed":
|
|
2681
2694
|
case "cancelled": return "The last import did not finish: taiga_start_import runs it again.";
|
|
@@ -2703,21 +2716,22 @@ const BY_ARGUMENT = {
|
|
|
2703
2716
|
initiativeId: ITEM_ID_HINT,
|
|
2704
2717
|
runId: "taiga_list_runs lists the runs, of one product or of every product the key can see.",
|
|
2705
2718
|
planId: "taiga_get_plan gives the initiative's active plan and its ID.",
|
|
2706
|
-
|
|
2707
|
-
|
|
2719
|
+
linkId: "taiga_list_product_repositories lists the product's repository links; a linkable repository id is not a link ID.",
|
|
2720
|
+
replacesLinkId: "taiga_list_product_repositories lists the product's repository links.",
|
|
2708
2721
|
linkableRepositoryId: "taiga_list_linkable_repositories lists the repositories the GitHub connection reaches, with their ids.",
|
|
2709
2722
|
installationId: "taiga_list_linkable_repositories lists the connected GitHub accounts.",
|
|
2710
2723
|
jobId: "A job ID is what the tool that started the job returned (taiga_request_initiatives, taiga_start_import).",
|
|
2711
2724
|
factoryId: "taiga_list_factories lists the factories the key can see.",
|
|
2712
2725
|
findingId: "taiga_list_findings lists a product's findings.",
|
|
2713
2726
|
violationId: "taiga_get_policy_standing lists a product's policy violations.",
|
|
2714
|
-
environmentId: "
|
|
2727
|
+
environmentId: "taiga_get_deployments lists the product's environments with their IDs; a product ID is not one.",
|
|
2715
2728
|
knowledgeId: "taiga_list_knowledge lists the knowledge files.",
|
|
2716
2729
|
productId: "taiga_list_products lists the products the key can see; a name is not an ID (taiga_get_product takes a slug)."
|
|
2717
2730
|
};
|
|
2718
2731
|
/** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
|
|
2719
2732
|
const BY_TOOL = {
|
|
2720
2733
|
taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
|
|
2734
|
+
taiga_get_sbom: () => "An SBOM exists once a sweep has inventoried that repository, where the licenses feature is enabled; taiga_list_packages (linkId) says whether it has packages, and taiga_list_product_repositories lists the links.",
|
|
2721
2735
|
taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_initiative's situation says whether one is being planned or failed, and names the call that asks for one (taiga_queue_initiative off the line, taiga_plan_again for a failed plan)." : ITEM_ID_HINT
|
|
2722
2736
|
};
|
|
2723
2737
|
/**
|
|
@@ -2730,8 +2744,8 @@ const MISSING_THING = {
|
|
|
2730
2744
|
initiativeId: /backlog item|initiative/i,
|
|
2731
2745
|
runId: /\b(build )?run\b/i,
|
|
2732
2746
|
planId: /\bplan\b/i,
|
|
2733
|
-
|
|
2734
|
-
|
|
2747
|
+
linkId: /repository link|\blink\b|repository/i,
|
|
2748
|
+
replacesLinkId: /repository link|\blink\b/i,
|
|
2735
2749
|
linkableRepositoryId: /github repository/i,
|
|
2736
2750
|
installationId: /installation|account/i,
|
|
2737
2751
|
jobId: /\bjob\b/i,
|
|
@@ -2812,6 +2826,7 @@ function ownArgumentLike(key, own) {
|
|
|
2812
2826
|
const PRODUCT_WORDS = {
|
|
2813
2827
|
backlogItemId: "initiativeId",
|
|
2814
2828
|
buildRunId: "runId",
|
|
2829
|
+
productRepositoryId: "linkId",
|
|
2815
2830
|
latestBuild: "latestRun"
|
|
2816
2831
|
};
|
|
2817
2832
|
/**
|
|
@@ -2854,7 +2869,7 @@ function inProductWords(value) {
|
|
|
2854
2869
|
}
|
|
2855
2870
|
/** The API's 404 sentence in the product's words: "Backlog item not found" is an initiative not found. */
|
|
2856
2871
|
function inProductWording(said) {
|
|
2857
|
-
return said.replace(/^Backlog item not found/i, "Initiative not found").replace(/^Build run step not found/i, "Run step not found").replace(/^Build run not found/i, "Run not found").replace(/^Custom document not found/i, "Knowledge file not found").replace(/^(Branch "[^"]*" does not exist in \S+?)\.?$/i, "$1. taiga_list_repository_branches lists the branches it has.").replace(/^This product already has a repository\. Change it instead of linking another\./i, "This product already has a repository. Pass
|
|
2872
|
+
return said.replace(/^Backlog item not found/i, "Initiative not found").replace(/^Build run step not found/i, "Run step not found").replace(/^Build run not found/i, "Run not found").replace(/^Custom document not found/i, "Knowledge file not found").replace(/^(Branch "[^"]*" does not exist in \S+?)\.?$/i, "$1. taiga_list_repository_branches lists the branches it has.").replace(/^This product already has a repository\. Change it instead of linking another\./i, "This product already has a repository. Pass replacesLinkId to swap it in one step, or taiga_unlink_repository first (taiga_list_product_repositories gives its ID).");
|
|
2858
2873
|
}
|
|
2859
2874
|
/**
|
|
2860
2875
|
* A float without its binary noise: six significant digits. Delivery metrics
|
|
@@ -3293,7 +3308,7 @@ function nextStepFor(state) {
|
|
|
3293
3308
|
if (state.linked === false) return "Link its repository (taiga_list_linkable_repositories, then taiga_link_repository), then taiga_start_import.";
|
|
3294
3309
|
if (state.linked === null && state.importState === "not started") return setupNextStep(state.product);
|
|
3295
3310
|
switch (state.importState) {
|
|
3296
|
-
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.";
|
|
3297
3312
|
case "failed": return "The last import did not finish: taiga_start_import runs it again.";
|
|
3298
3313
|
case "complete":
|
|
3299
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.`;
|
|
@@ -3772,7 +3787,7 @@ function registerProductTools(server, client) {
|
|
|
3772
3787
|
}));
|
|
3773
3788
|
server.registerTool("taiga_get_product", {
|
|
3774
3789
|
title: "Get product",
|
|
3775
|
-
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.",
|
|
3776
3791
|
inputSchema: z.object({
|
|
3777
3792
|
productId: productId.optional(),
|
|
3778
3793
|
slug: slug.optional(),
|
|
@@ -3924,6 +3939,10 @@ async function publishDraft(scope) {
|
|
|
3924
3939
|
...published.warnings ? { warnings: published.warnings } : {}
|
|
3925
3940
|
});
|
|
3926
3941
|
}
|
|
3942
|
+
/** The section ids that are not control ids at all. GDPR-1.1 is a control id, so it is not among them, though it names no single policy. */
|
|
3943
|
+
function notControls(sections) {
|
|
3944
|
+
return sections.filter((id) => !isControlId(id.toUpperCase()));
|
|
3945
|
+
}
|
|
3927
3946
|
/** The policies the section ids name when every one is a control id (SEC-5.1), else none. */
|
|
3928
3947
|
function policiesNamed(sections) {
|
|
3929
3948
|
if (sections.length === 0) return [];
|
|
@@ -3949,7 +3968,7 @@ function registerDocumentTools(server, client) {
|
|
|
3949
3968
|
sections: params.sections?.map((id) => id.toUpperCase())
|
|
3950
3969
|
} : params;
|
|
3951
3970
|
if (args.type === void 0) {
|
|
3952
|
-
if (args.sections) return toolError("VALIDATION_ERROR", named.length > 1 ? `Those control ids are in different policies (${named.join(", ")}): read one type at a time.` : "Name the document (type) to read its sections; a control id such as SEC-5.1 names its policy itself.");
|
|
3971
|
+
if (args.sections) return toolError("VALIDATION_ERROR", named.length > 1 ? `Those control ids are in different policies (${named.join(", ")}): read one type at a time.` : notControls(args.sections).length > 0 && notControls(args.sections).length < args.sections.length ? `Not a control id: ${notControls(args.sections).join(", ")}. A control id such as SEC-5.1 names its policy; any other section id needs type.` : "Name the document (type) to read its sections; a control id such as SEC-5.1 names its policy itself, but one of a family that spans policies (GDPR, NIS2) does not.");
|
|
3953
3972
|
const result = await client.listTenantDocuments({
|
|
3954
3973
|
limit: args.limit,
|
|
3955
3974
|
cursor: args.cursor
|
|
@@ -4120,7 +4139,7 @@ function registerJobTools(server, client) {
|
|
|
4120
4139
|
if (!client.hasCapability("products:read")) return;
|
|
4121
4140
|
server.registerTool("taiga_get_job", {
|
|
4122
4141
|
title: "Get an agent job",
|
|
4123
|
-
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.",
|
|
4124
4143
|
inputSchema: z.object({ jobId: idInput("job", "the tool that started the job returned it") }),
|
|
4125
4144
|
annotations: READ_ONLY
|
|
4126
4145
|
}, (params) => runTool(async () => toolResult(jobReport((await client.getAgentJob(params.jobId)).data))));
|
|
@@ -4578,11 +4597,12 @@ function registerBuildTools(server, client) {
|
|
|
4578
4597
|
if (!client.hasCapability("products:read")) return;
|
|
4579
4598
|
server.registerTool("taiga_list_runs", {
|
|
4580
4599
|
title: "List runs",
|
|
4581
|
-
description: "List coding agent runs, newest first: status, branch, pull request, step progress, actions and outcome, one sentence saying how it stands: running, waiting for a person to review and merge, merged, failed, stopped, its pull request closed unmerged, or nobody waiting on it because its initiative ended. Without productId, every product's runs; active: true keeps running ones. taiga_get_run reads one; carry work on from the initiative (taiga_resume_initiative).",
|
|
4600
|
+
description: "List coding agent runs, newest first: status, branch, pull request, step progress, actions and outcome, one sentence saying how it stands: running, waiting for a person to review and merge, merged, failed, stopped, its pull request closed unmerged, or nobody waiting on it because its initiative ended. Without productId, every product's runs; active: true keeps running ones, and status narrows to some statuses (status: [\"failed\", \"stopped\"] for what failed). taiga_get_run reads one; carry work on from the initiative (taiga_resume_initiative).",
|
|
4582
4601
|
inputSchema: z.object({
|
|
4583
4602
|
productId: productId.optional(),
|
|
4584
4603
|
initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it").optional().describe("Only this initiative's runs -- get from taiga_list_initiatives"),
|
|
4585
4604
|
active: z.boolean().optional().describe("Only runs the agent is executing now (cloning through reviewing; a queued pending run is not yet executing and is left out); for the org-wide list"),
|
|
4605
|
+
status: z.array(z.enum(BUILD_RUN_STATUSES)).min(1).optional().describe("Only runs in these statuses, such as [\"failed\", \"stopped\"]"),
|
|
4586
4606
|
...pagination
|
|
4587
4607
|
}),
|
|
4588
4608
|
annotations: READ_ONLY
|
|
@@ -4593,6 +4613,7 @@ function registerBuildTools(server, client) {
|
|
|
4593
4613
|
if (productIdOf) {
|
|
4594
4614
|
const result = await client.listBuildRuns(productIdOf, {
|
|
4595
4615
|
backlogItemId: params.initiativeId,
|
|
4616
|
+
status: params.status,
|
|
4596
4617
|
limit: params.limit,
|
|
4597
4618
|
cursor: params.cursor
|
|
4598
4619
|
});
|
|
@@ -4707,7 +4728,7 @@ function waitingOn(rows, station) {
|
|
|
4707
4728
|
}
|
|
4708
4729
|
const STATION_NAMED = 3;
|
|
4709
4730
|
/** Who holds the station, or null when the key cannot read the backlog or the read failed. */
|
|
4710
|
-
async function stationHolders(client, product,
|
|
4731
|
+
async function stationHolders(client, product, except) {
|
|
4711
4732
|
if (!client.hasCapability("backlog:read")) return null;
|
|
4712
4733
|
try {
|
|
4713
4734
|
const columns = await Promise.all(["plan", "build"].map((state) => client.listBacklogItems(product, {
|
|
@@ -4715,7 +4736,7 @@ async function stationHolders(client, product, head) {
|
|
|
4715
4736
|
order: "newest",
|
|
4716
4737
|
limit: 100
|
|
4717
4738
|
})));
|
|
4718
|
-
const rows = columns.flatMap((page) => page.data).filter((row) => row.id
|
|
4739
|
+
const rows = columns.flatMap((page) => page.data).filter((row) => !except.has(row.id));
|
|
4719
4740
|
const capped = columns.some((page) => page.nextCursor !== null);
|
|
4720
4741
|
return {
|
|
4721
4742
|
count: rows.length,
|
|
@@ -4791,10 +4812,14 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
4791
4812
|
});
|
|
4792
4813
|
const note = unnamed === 0 ? null : !canName ? "Entries carry initiativeId only: naming them needs backlog:read, which this key lacks." : entries.length > NAMED_ENTRIES ? `Only the first ${NAMED_ENTRIES} entries are named; the rest carry initiativeId only.` : "An entry whose initiative could not be read carries initiativeId only; taiga_get_initiative reads it.";
|
|
4793
4814
|
const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
|
|
4815
|
+
const idle = rows.length > 0 && blockingIndex(rows, (row) => row.entryState) < 0;
|
|
4816
|
+
const station = rows.length > 0 || options.name === "all" ? await stationHolders(client, product, new Set(idle ? [rows[0].backlogItemId] : rows.map((row) => row.backlogItemId))) : null;
|
|
4817
|
+
const offLine = !idle && station && station.count > 0 ? { offTheLine: `${station.atLeast ? "At least " : ""}${station.count} initiative${station.count === 1 ? " is" : "s are"} in Plan or Build without a queue entry (${station.titles.map((title) => `"${title}"`).join(", ")}${station.count > station.titles.length ? ", and more" : ""}): moved there by hand or waiting on a pull request review. Each holds the station like the active entry; taiga_list_initiatives with column: 'plan' or 'build' lists them.` } : {};
|
|
4794
4818
|
return {
|
|
4795
4819
|
linePaused: paused === null ? null : typeof paused === "string",
|
|
4796
4820
|
entries: rows.map(({ lifecycle: _lifecycle, runId: _runId, ...row }) => row),
|
|
4797
|
-
waitingOn: line ?? waitingOn(rows,
|
|
4821
|
+
waitingOn: line ?? waitingOn(rows, idle ? station : null),
|
|
4822
|
+
...offLine,
|
|
4798
4823
|
...note ? { note } : {}
|
|
4799
4824
|
};
|
|
4800
4825
|
}
|
|
@@ -5222,7 +5247,7 @@ function registerBacklogReads(server, client) {
|
|
|
5222
5247
|
function registerRequest(server, client) {
|
|
5223
5248
|
server.registerTool("taiga_request_initiatives", {
|
|
5224
5249
|
title: "Request new initiatives",
|
|
5225
|
-
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.
|
|
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 (jobId) gives the outcome. One request per product at a time (CONFLICT). Nothing goes on the line.",
|
|
5226
5251
|
inputSchema: z.object({
|
|
5227
5252
|
productId,
|
|
5228
5253
|
title: z.string().max(500).optional().describe("What the work is, in a line"),
|
|
@@ -5398,6 +5423,19 @@ function registerPlanVerdicts(server, client) {
|
|
|
5398
5423
|
* next sweep no longer detects it, so there is no tool for either.
|
|
5399
5424
|
*/
|
|
5400
5425
|
/** A finding as a list row: enough to decide whether to open, fix or ignore it. */
|
|
5426
|
+
const PACKAGE_ROW_FIELDS = [
|
|
5427
|
+
"id",
|
|
5428
|
+
"ecosystem",
|
|
5429
|
+
"packageName",
|
|
5430
|
+
"version",
|
|
5431
|
+
"purl",
|
|
5432
|
+
"licenses",
|
|
5433
|
+
"scope",
|
|
5434
|
+
"relation",
|
|
5435
|
+
"sourcePath",
|
|
5436
|
+
"productRepositoryId",
|
|
5437
|
+
"detectedAt"
|
|
5438
|
+
];
|
|
5401
5439
|
const FINDING_ROW_FIELDS = [
|
|
5402
5440
|
"id",
|
|
5403
5441
|
"kind",
|
|
@@ -5421,6 +5459,77 @@ const FINDING_ROW_FIELDS = [
|
|
|
5421
5459
|
"ignoreReason",
|
|
5422
5460
|
"ignoreExpiresAt"
|
|
5423
5461
|
];
|
|
5462
|
+
/** Locations a folded row names before it says how many more there are. */
|
|
5463
|
+
const GROUP_WHERE = 5;
|
|
5464
|
+
/** What makes two findings one rule's: its rule id for code, infrastructure and secrets, its license verdict for a license. */
|
|
5465
|
+
/** The scanner's title cap (deps.py _COLUMN_CAP): a title cut there ends in "...". */
|
|
5466
|
+
const TITLE_CAP = 300;
|
|
5467
|
+
/**
|
|
5468
|
+
* The license verdict of a license finding's title: what follows "<package> <version>: ". A title the scanner cut
|
|
5469
|
+
* at its cap has no whole verdict, so it folds with nothing: two cut titles could share a prefix and differ.
|
|
5470
|
+
*/
|
|
5471
|
+
function licenseVerdict(row) {
|
|
5472
|
+
const title = String(row["title"] ?? "");
|
|
5473
|
+
if (title.length >= TITLE_CAP && title.endsWith("...")) return "";
|
|
5474
|
+
return title.split(": ").slice(1).join(": ");
|
|
5475
|
+
}
|
|
5476
|
+
function groupKey(row) {
|
|
5477
|
+
const status = String(row["status"] ?? "");
|
|
5478
|
+
const link = String(row["productRepositoryId"] ?? "");
|
|
5479
|
+
if (row["kind"] === "license_violation") {
|
|
5480
|
+
const verdict = licenseVerdict(row);
|
|
5481
|
+
return verdict ? `license|${status}|${link}|${verdict}` : null;
|
|
5482
|
+
}
|
|
5483
|
+
const location = row["location"];
|
|
5484
|
+
const rule = typeof location?.ruleId === "string" ? location.ruleId : null;
|
|
5485
|
+
return rule ? `${String(row["kind"])}|${String(row["severity"])}|${status}|${link}|${rule}` : null;
|
|
5486
|
+
}
|
|
5487
|
+
/** Where one finding is, in a few characters: its path and line, or its package. */
|
|
5488
|
+
function whereOf(row) {
|
|
5489
|
+
const location = row["location"];
|
|
5490
|
+
if (typeof location?.path === "string") return location.line ? `${location.path}:${String(location.line)}` : location.path;
|
|
5491
|
+
return [row["packageName"], row["currentVersion"]].filter(Boolean).join(" ") || String(row["id"]);
|
|
5492
|
+
}
|
|
5493
|
+
/**
|
|
5494
|
+
* Findings of one rule or license verdict on a page, folded into one row (PRO-2941): a review read seven
|
|
5495
|
+
* CKV_GHA_7 rows and fifteen sharp license rows, identical but for where. The first stands for them, with
|
|
5496
|
+
* count, every findingId (what fix and ignore take) and the first few locations.
|
|
5497
|
+
*/
|
|
5498
|
+
function grouped(rows, fold) {
|
|
5499
|
+
if (!fold) return rows;
|
|
5500
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5501
|
+
const order = [];
|
|
5502
|
+
for (const row of rows) {
|
|
5503
|
+
const key = groupKey(row);
|
|
5504
|
+
if (!key) {
|
|
5505
|
+
order.push(row);
|
|
5506
|
+
continue;
|
|
5507
|
+
}
|
|
5508
|
+
if (!groups.has(key)) {
|
|
5509
|
+
groups.set(key, []);
|
|
5510
|
+
order.push(key);
|
|
5511
|
+
}
|
|
5512
|
+
groups.get(key).push(row);
|
|
5513
|
+
}
|
|
5514
|
+
return order.map((entry) => {
|
|
5515
|
+
if (typeof entry !== "string") return entry;
|
|
5516
|
+
const members = groups.get(entry);
|
|
5517
|
+
if (members.length === 1) return members[0];
|
|
5518
|
+
const where = members.map(whereOf);
|
|
5519
|
+
const first = members[0];
|
|
5520
|
+
const shared = Object.fromEntries(Object.entries(first).filter(([key, value]) => key !== "id" && members.every((member) => JSON.stringify(member[key]) === JSON.stringify(value))));
|
|
5521
|
+
const location = first["location"];
|
|
5522
|
+
const said = first["kind"] === "license_violation" ? licenseVerdict(first) : typeof location?.ruleId === "string" ? location.ruleId : String(first["title"]);
|
|
5523
|
+
return {
|
|
5524
|
+
...shared,
|
|
5525
|
+
...shared["title"] === void 0 ? { title: said } : {},
|
|
5526
|
+
count: members.length,
|
|
5527
|
+
findingIds: members.map((member) => member["id"]),
|
|
5528
|
+
where: where.slice(0, GROUP_WHERE),
|
|
5529
|
+
...where.length > GROUP_WHERE ? { moreWhere: where.length - GROUP_WHERE } : {}
|
|
5530
|
+
};
|
|
5531
|
+
});
|
|
5532
|
+
}
|
|
5424
5533
|
/**
|
|
5425
5534
|
* Whether taiga_fix_finding would take a finding, said on the row (PRO-2800): three reviews found a dependency
|
|
5426
5535
|
* marked remediableBy coding_agent with no fixed version, which the API refuses to fix. The API's own refusals,
|
|
@@ -5579,13 +5688,14 @@ function registerMaintenanceTools(server, client) {
|
|
|
5579
5688
|
if (!client.hasCapability("products:read")) return;
|
|
5580
5689
|
server.registerTool("taiga_list_findings", {
|
|
5581
5690
|
title: "List maintenance findings",
|
|
5582
|
-
description: "List what maintenance sweeps found in a product's repository: dependencies, secrets, insecure code (sast) and infrastructure. The first page opens with sweep (open counts, latest sweeps), so an old or failed sweep is not read as an all-clear. Rows: severity, tier (act_now: known exploited; urgent: likely and serious; elevated: one of the two; routine), location, remediableBy (coding_agent: a run here can fix it; upstream_only: the code is in a submodule, so the fix belongs in its own repository), fixable: false when no fixed version is known yet, breached controls and initiativeId of a fix under way. Omit status for open findings. controlId lists the findings that breach one control. taiga_get_finding reads one.",
|
|
5691
|
+
description: "List what maintenance sweeps found in a product's repository: dependencies, secrets, insecure code (sast) and infrastructure. The first page opens with sweep (open counts, latest sweeps), so an old or failed sweep is not read as an all-clear. Rows: severity, tier (act_now: known exploited; urgent: likely and serious; elevated: one of the two; routine), location, remediableBy (coding_agent: a run here can fix it; upstream_only: the code is in a submodule, so the fix belongs in its own repository), fixable: false when no fixed version is known yet, breached controls and initiativeId of a fix under way. Omit status for open findings. controlId lists the findings that breach one control. Findings of one rule or license verdict in one repository fold into one row per page, with count, findingIds (what fix and ignore take), where, and only the fields they share; group: false lists each. taiga_get_finding reads one.",
|
|
5583
5692
|
inputSchema: z.object({
|
|
5584
5693
|
productId,
|
|
5585
5694
|
controlId: z.string().trim().min(1).max(40).optional().describe("Only the findings that breach this control, such as SEC-5.1 (one page, searched across all)"),
|
|
5586
5695
|
status: z.enum(MAINTENANCE_FINDING_STATUSES).optional().describe("Only findings in this status; omit for the ones needing action"),
|
|
5587
5696
|
severity: z.enum(MAINTENANCE_FINDING_SEVERITIES).optional().describe("Only this severity"),
|
|
5588
5697
|
kind: z.enum(MAINTENANCE_FINDING_KINDS).optional().describe("Only this kind of finding"),
|
|
5698
|
+
group: z.boolean().optional().describe("Fold findings of one rule or license on this page into one row (default true); false lists each"),
|
|
5589
5699
|
...pagination
|
|
5590
5700
|
}),
|
|
5591
5701
|
annotations: READ_ONLY
|
|
@@ -5600,7 +5710,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
5600
5710
|
cursor
|
|
5601
5711
|
}), "", () => [""], params.limit ?? 25, (row) => Array.isArray(row["controls"]) && row["controls"].some((c) => String(c).toUpperCase() === wanted));
|
|
5602
5712
|
return toolResult({
|
|
5603
|
-
...page(findingRows(found.rows), null),
|
|
5713
|
+
...page(grouped(findingRows(found.rows), params.group !== false), null),
|
|
5604
5714
|
...found.more ? { more: found.rows.length < (params.limit ?? 25) ? `Only the first 1000 findings were searched; narrow by status, severity or kind to reach the rest.` : "There may be more than the limit carries: raise the limit, or narrow by status, severity or kind." } : {},
|
|
5605
5715
|
...found.rows.length === 0 && !found.more ? { note: `No ${params.status ? `${params.status} ` : "open "}finding breaches ${wanted}. That is no all-clear on its own: taiga_list_findings without controlId says when the product was last swept.` } : {}
|
|
5606
5716
|
});
|
|
@@ -5614,11 +5724,43 @@ function registerMaintenanceTools(server, client) {
|
|
|
5614
5724
|
sweep: null,
|
|
5615
5725
|
sweepNote: "The sweep overview could not be read: an empty list here is not an all-clear."
|
|
5616
5726
|
} : sweep ? { sweep } : {},
|
|
5617
|
-
...page(findingRows(result.data), result.nextCursor),
|
|
5727
|
+
...page(grouped(findingRows(result.data), params.group !== false), result.nextCursor),
|
|
5618
5728
|
...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {},
|
|
5619
5729
|
...params.status === void 0 && params.severity === void 0 && params.kind === void 0 && typeof result.totalCount === "number" && sweep && sweep !== "unread" && typeof sweep["openFindings"] === "number" && sweep["openFindings"] !== result.totalCount ? { totalNote: "totalCount includes findings with a fix under way (remediating) and ignored findings whose ignore has expired; sweep.openFindings counts open ones only." } : {}
|
|
5620
5730
|
});
|
|
5621
5731
|
}));
|
|
5732
|
+
server.registerTool("taiga_list_packages", {
|
|
5733
|
+
title: "List the package inventory",
|
|
5734
|
+
description: "Every package a product's repositories resolve, vulnerable or not, as the latest sweep read it: name, version, ecosystem, SPDX licenses, scope (dev / runtime / unknown), relation (direct / transitive / unknown) and the lockfile. The first page opens with licenses: each license string across the inventory, how many packages carry it and its standing against the allowlist (allowed, not_allowed, unknown), plus the allowlist and when each repository was last inventoried. Filter by one license string (UNKNOWN for the ones the data source could not name), a name substring or a repository. A license outside the allowlist is also a license_violation finding in taiga_list_findings. Where the licenses feature is not enabled the product answers NOT_FOUND. taiga_get_sbom downloads one repository as CycloneDX.",
|
|
5735
|
+
inputSchema: z.object({
|
|
5736
|
+
productId,
|
|
5737
|
+
license: z.string().min(1).max(4096).optional().describe("Only packages carrying exactly this SPDX string"),
|
|
5738
|
+
search: z.string().min(1).max(255).optional().describe("Case-insensitive substring of the package name"),
|
|
5739
|
+
linkId: idInput("repository link", "taiga_list_product_repositories gives it", "only this repository").optional(),
|
|
5740
|
+
...pagination
|
|
5741
|
+
}),
|
|
5742
|
+
annotations: READ_ONLY
|
|
5743
|
+
}, (params) => runTool(async () => {
|
|
5744
|
+
const { productId: id, linkId: link, ...filters } = params;
|
|
5745
|
+
const [result, summary] = await Promise.all([client.listPackages(id, {
|
|
5746
|
+
...filters,
|
|
5747
|
+
...link ? { productRepositoryId: link } : {}
|
|
5748
|
+
}), params.cursor === void 0 ? client.getLicenseSummary(id) : Promise.resolve(null)]);
|
|
5749
|
+
return toolResult({
|
|
5750
|
+
...summary ? { licenses: summary.data } : {},
|
|
5751
|
+
...page(pickFields(result.data, PACKAGE_ROW_FIELDS), result.nextCursor),
|
|
5752
|
+
...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {}
|
|
5753
|
+
});
|
|
5754
|
+
}));
|
|
5755
|
+
server.registerTool("taiga_get_sbom", {
|
|
5756
|
+
title: "Download a repository SBOM",
|
|
5757
|
+
description: "One repository's software bill of materials as a CycloneDX 1.5 JSON document, rendered from the stored package inventory: metadata (timestamp, tools, the repository), every package with its purl, license and scope, and the packages the repository declares directly. The whole document is returned, so for a large repository the answer is large; read taiga_list_packages when a summary is enough. NOT_FOUND where the licenses feature is not enabled, and for a repository no sweep has inventoried yet.",
|
|
5758
|
+
inputSchema: z.object({
|
|
5759
|
+
productId,
|
|
5760
|
+
linkId: idInput("repository link", "taiga_list_product_repositories gives it")
|
|
5761
|
+
}),
|
|
5762
|
+
annotations: READ_ONLY
|
|
5763
|
+
}, (params) => runTool(async () => toolResult(await client.getSbom(params.productId, params.linkId))));
|
|
5622
5764
|
server.registerTool("taiga_get_finding", {
|
|
5623
5765
|
title: "Get a maintenance finding",
|
|
5624
5766
|
description: "Read one maintenance finding: exploit likelihood (EPSS, known-exploited list), the advisory summarized (full: true for its whole record: every affected range and reference) and fixed version, code location, breached policy controls, why it ranks where it does, history (reintroduced, previously resolved) and the remediation under way. fixable: false with fixNote says taiga_fix_finding would refuse it. A secret's value is never stored, only the rule and location.",
|
|
@@ -6558,10 +6700,15 @@ async function standingPart(client, productId) {
|
|
|
6558
6700
|
const standing = (await client.getPolicyStanding(productId)).data;
|
|
6559
6701
|
const summary = standingSummary(standing);
|
|
6560
6702
|
const state = standing.assessment?.state;
|
|
6703
|
+
const controls = Array.isArray(standing.controls) ? standing.controls : [];
|
|
6704
|
+
const withAny = (key) => controls.filter((control) => Number(control[key] ?? 0) > 0).length;
|
|
6561
6705
|
return {
|
|
6562
6706
|
openViolations: summary.violations,
|
|
6563
6707
|
bySeverity: summary.bySeverity,
|
|
6564
|
-
|
|
6708
|
+
controlsBreached: withAny("findingCount"),
|
|
6709
|
+
controlsDiverged: withAny("divergenceCount"),
|
|
6710
|
+
controlsMet: summary.controlsMet,
|
|
6711
|
+
controlsNotApplicable: summary.controlsNotApplicable,
|
|
6565
6712
|
...state === "never" || state === "failed" ? { note: `The assessment ${state === "never" ? "has never run" : "failed"}: no violations here is not an all-clear.` } : {}
|
|
6566
6713
|
};
|
|
6567
6714
|
}
|
|
@@ -6926,7 +7073,7 @@ function deploymentLines(rows) {
|
|
|
6926
7073
|
};
|
|
6927
7074
|
});
|
|
6928
7075
|
}
|
|
6929
|
-
const environmentId = idInput("environment", "
|
|
7076
|
+
const environmentId = idInput("environment", "taiga_get_deployments lists them; a product ID is not one");
|
|
6930
7077
|
/** The windows the pages offer, as the schema's own values rather than a number checked afterwards. */
|
|
6931
7078
|
const windowDays = z.literal([
|
|
6932
7079
|
30,
|
|
@@ -6936,11 +7083,11 @@ const windowDays = z.literal([
|
|
|
6936
7083
|
/** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
|
|
6937
7084
|
const isApiFailure = (error) => error instanceof TaigaApiError || isNetworkFailure(error);
|
|
6938
7085
|
/** A part that may fail on its own without failing the read: null and the reason, or the value. */
|
|
6939
|
-
async function part(read) {
|
|
7086
|
+
async function part(read, rethrow = () => false) {
|
|
6940
7087
|
try {
|
|
6941
7088
|
return { value: await read() };
|
|
6942
7089
|
} catch (error) {
|
|
6943
|
-
if (!isApiFailure(error)) throw error;
|
|
7090
|
+
if (!isApiFailure(error) || rethrow(error)) throw error;
|
|
6944
7091
|
return { unavailable: error instanceof TaigaApiError ? `${error.status}: ${error.apiMessage ?? ""}`.trim() : "unreachable" };
|
|
6945
7092
|
}
|
|
6946
7093
|
}
|
|
@@ -6971,7 +7118,7 @@ function registerObserveTools(server, client) {
|
|
|
6971
7118
|
if (!client.hasCapability("products:read")) return;
|
|
6972
7119
|
server.registerTool("taiga_get_deployments", {
|
|
6973
7120
|
title: "Get what runs where",
|
|
6974
|
-
description: "Read what runs in each
|
|
7121
|
+
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.",
|
|
6975
7122
|
inputSchema: z.object({
|
|
6976
7123
|
productId,
|
|
6977
7124
|
environmentId: environmentId.optional(),
|
|
@@ -6979,25 +7126,31 @@ function registerObserveTools(server, client) {
|
|
|
6979
7126
|
}),
|
|
6980
7127
|
annotations: READ_ONLY
|
|
6981
7128
|
}, (params) => runTool(async () => {
|
|
6982
|
-
const [environments,
|
|
7129
|
+
const [environments, currentRead, historyRead, sync] = await Promise.all([
|
|
6983
7130
|
allEnvironments(client, params.productId),
|
|
6984
|
-
client.getCurrentDeployments(params.productId),
|
|
6985
|
-
client.listDeployments(params.productId, {
|
|
7131
|
+
part(() => client.getCurrentDeployments(params.productId)),
|
|
7132
|
+
part(() => client.listDeployments(params.productId, {
|
|
6986
7133
|
environmentId: params.environmentId,
|
|
6987
7134
|
limit: params.limit ?? 10,
|
|
6988
7135
|
cursor: params.cursor
|
|
6989
|
-
}),
|
|
7136
|
+
}), (error) => Boolean(params.environmentId) && error instanceof TaigaApiError && error.status === 404),
|
|
6990
7137
|
part(() => client.getDeploymentSyncStatus(params.productId))
|
|
6991
7138
|
]);
|
|
6992
|
-
const now =
|
|
6993
|
-
const
|
|
6994
|
-
const
|
|
7139
|
+
const now = "value" in currentRead ? currentRead.value.data : null;
|
|
7140
|
+
const history = "value" in historyRead ? historyRead.value : null;
|
|
7141
|
+
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;
|
|
7142
|
+
const live = new Set([...now?.current ?? [], ...now?.deploying ?? []].map((row) => row.id));
|
|
7143
|
+
const unavailable = {
|
|
7144
|
+
..."unavailable" in currentRead ? { running: currentRead.unavailable } : {},
|
|
7145
|
+
..."unavailable" in historyRead ? { history: historyRead.unavailable } : {}
|
|
7146
|
+
};
|
|
6995
7147
|
return toolResult({
|
|
6996
7148
|
environments: pickFields(environments.rows, ENVIRONMENT_FIELDS),
|
|
6997
7149
|
...environments.more ? { moreEnvironments: true } : {},
|
|
6998
|
-
running: deploymentLines(now.current ?? []),
|
|
6999
|
-
deploying: deploymentLines(now.deploying ?? []),
|
|
7000
|
-
history: page(deploymentLines(history.data.filter((row) => !live.has(row.id))), history.nextCursor),
|
|
7150
|
+
running: now ? deploymentLines(now.current ?? []) : null,
|
|
7151
|
+
deploying: now ? deploymentLines(now.deploying ?? []) : null,
|
|
7152
|
+
history: history ? page(deploymentLines(history.data.filter((row) => !live.has(row.id))), history.nextCursor) : null,
|
|
7153
|
+
...Object.keys(unavailable).length > 0 ? { unavailable } : {},
|
|
7001
7154
|
..."value" in sync ? { syncStatus: sync.value.data } : {
|
|
7002
7155
|
syncStatus: null,
|
|
7003
7156
|
syncUnavailable: sync.unavailable
|
|
@@ -7005,19 +7158,6 @@ function registerObserveTools(server, client) {
|
|
|
7005
7158
|
...note ? { note } : {}
|
|
7006
7159
|
});
|
|
7007
7160
|
}));
|
|
7008
|
-
server.registerTool("taiga_list_environments", {
|
|
7009
|
-
title: "List a product's environments",
|
|
7010
|
-
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.",
|
|
7011
|
-
inputSchema: z.object({ productId }),
|
|
7012
|
-
annotations: READ_ONLY
|
|
7013
|
-
}, (params) => runTool(async () => {
|
|
7014
|
-
const { rows, more } = await allEnvironments(client, params.productId);
|
|
7015
|
-
return toolResult({
|
|
7016
|
-
data: pickFields(rows, ENVIRONMENT_FIELDS),
|
|
7017
|
-
...more ? { more: "More environments than this answer reads; the rest were left out." } : {},
|
|
7018
|
-
...rows.length === 0 ? { note: "No environment is defined yet; taiga_create_environment adds one." } : {}
|
|
7019
|
-
});
|
|
7020
|
-
}));
|
|
7021
7161
|
server.registerTool("taiga_get_delivery_metrics", {
|
|
7022
7162
|
title: "Get delivery metrics",
|
|
7023
7163
|
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.",
|
|
@@ -7037,7 +7177,7 @@ function registerObserveTools(server, client) {
|
|
|
7037
7177
|
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."
|
|
7038
7178
|
});
|
|
7039
7179
|
const production = all.filter((env) => env.type === "production");
|
|
7040
|
-
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" : ""}.
|
|
7180
|
+
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) => ({
|
|
7041
7181
|
id: env.id,
|
|
7042
7182
|
name: env.name,
|
|
7043
7183
|
type: env.type
|
|
@@ -7166,7 +7306,7 @@ function registerProductDocumentWrites(server, client) {
|
|
|
7166
7306
|
}, (params) => runTool(async () => toolResult((await client.markProductDocumentReviewed(params.productId, params.type)).data)));
|
|
7167
7307
|
if (canRead) server.registerTool("taiga_regenerate_product_document", {
|
|
7168
7308
|
title: "Regenerate a product document",
|
|
7169
|
-
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.
|
|
7309
|
+
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.",
|
|
7170
7310
|
inputSchema: z.object({
|
|
7171
7311
|
productId,
|
|
7172
7312
|
type: generatedType
|
|
@@ -21942,7 +22082,11 @@ const GITHUB_REPOSITORY_FIELDS = [
|
|
|
21942
22082
|
"linked",
|
|
21943
22083
|
"linkedProduct"
|
|
21944
22084
|
];
|
|
21945
|
-
|
|
22085
|
+
/**
|
|
22086
|
+
* A product's link to a repository: one name for it in every tool (PRO-2941). Reviews met it as repositoryId
|
|
22087
|
+
* here, productRepositoryId on findings and packages, and linkableRepositoryId (a different thing) beside both.
|
|
22088
|
+
*/
|
|
22089
|
+
const linkId = idInput("repository link", "taiga_list_product_repositories gives it (its id); a linkable repository id is not one");
|
|
21946
22090
|
/** Round-robin over lists: the first of each, then the second of each, and so on. */
|
|
21947
22091
|
function interleave(lists) {
|
|
21948
22092
|
const out = [];
|
|
@@ -22006,11 +22150,11 @@ function registerRepositoryTools(server, client) {
|
|
|
22006
22150
|
description: "List the branches of a product's linked repository, each with whether it is protected, to choose the one Taiga works on (taiga_set_repository_branch). truncated: true means there are more than listed.",
|
|
22007
22151
|
inputSchema: z.object({
|
|
22008
22152
|
productId,
|
|
22009
|
-
|
|
22153
|
+
linkId
|
|
22010
22154
|
}),
|
|
22011
22155
|
annotations: READ_ONLY
|
|
22012
22156
|
}, (params) => runTool(async () => {
|
|
22013
|
-
const result = await client.listRepositoryBranches(params.productId, params.
|
|
22157
|
+
const result = await client.listRepositoryBranches(params.productId, params.linkId);
|
|
22014
22158
|
return toolResult({
|
|
22015
22159
|
data: result.data,
|
|
22016
22160
|
truncated: result.truncated
|
|
@@ -22066,11 +22210,11 @@ function registerRepositoryTools(server, client) {
|
|
|
22066
22210
|
if (!client.hasCapability("vcs:manage")) return;
|
|
22067
22211
|
server.registerTool("taiga_link_repository", {
|
|
22068
22212
|
title: "Link a repository to a product",
|
|
22069
|
-
description: "Link a GitHub repository to a product (taiga_list_linkable_repositories gives its id).
|
|
22213
|
+
description: "Link a GitHub repository to a product (taiga_list_linkable_repositories gives its id). replacesLinkId swaps out that link in one step (the same one changes nothing). Whether a product may link more than one repository depends on the organization; when it may not, a second link is refused and the refusal says to pass replacesLinkId. A managed link (isManaged) cannot be replaced or unlinked. Once linked, planning reads its code, runs open pull requests in it, sweeps scan it, and its deployment history is imported.",
|
|
22070
22214
|
inputSchema: z.object({
|
|
22071
22215
|
productId,
|
|
22072
22216
|
linkableRepositoryId: idInput("linkable repository", "taiga_list_linkable_repositories gives it (a row's id; not GitHub's numeric id, not a link ID)"),
|
|
22073
|
-
|
|
22217
|
+
replacesLinkId: linkId.optional().describe("The link this one replaces")
|
|
22074
22218
|
}),
|
|
22075
22219
|
annotations: OPERATE
|
|
22076
22220
|
}, (params) => runTool(async () => {
|
|
@@ -22078,7 +22222,7 @@ function registerRepositoryTools(server, client) {
|
|
|
22078
22222
|
try {
|
|
22079
22223
|
linked = await client.linkRepository(params.productId, {
|
|
22080
22224
|
githubRepositoryId: params.linkableRepositoryId,
|
|
22081
|
-
...params.
|
|
22225
|
+
...params.replacesLinkId ? { replacesRepositoryId: params.replacesLinkId } : {}
|
|
22082
22226
|
});
|
|
22083
22227
|
} catch (error) {
|
|
22084
22228
|
const holder = await holderOf(client, error, params.productId);
|
|
@@ -22095,14 +22239,14 @@ function registerRepositoryTools(server, client) {
|
|
|
22095
22239
|
description: "Choose the branch Taiga branches from and opens pull requests against (taiga_list_repository_branches lists them), or null for the repository's default. A protected branch is accepted, but Taiga may not be able to merge into it; the result says when it is protected. Optionally sets a label for the link.",
|
|
22096
22240
|
inputSchema: z.object({
|
|
22097
22241
|
productId,
|
|
22098
|
-
|
|
22242
|
+
linkId,
|
|
22099
22243
|
baseBranch: z.string().trim().min(1).max(255).nullable().optional().describe("The branch; null for the default"),
|
|
22100
22244
|
label: z.string().trim().min(1).max(100).nullable().optional().describe("A label for the link; null clears it")
|
|
22101
22245
|
}),
|
|
22102
22246
|
annotations: OPERATE_SETTING
|
|
22103
22247
|
}, (params) => runTool(async () => {
|
|
22104
22248
|
if (params.baseBranch === void 0 && params.label === void 0) return toolError("VALIDATION_ERROR", "Set baseBranch, label or both.");
|
|
22105
|
-
const updated = (await client.updateRepository(params.productId, params.
|
|
22249
|
+
const updated = (await client.updateRepository(params.productId, params.linkId, {
|
|
22106
22250
|
...params.baseBranch !== void 0 ? { baseBranch: params.baseBranch } : {},
|
|
22107
22251
|
...params.label !== void 0 ? { label: params.label } : {}
|
|
22108
22252
|
})).data;
|
|
@@ -22114,16 +22258,16 @@ function registerRepositoryTools(server, client) {
|
|
|
22114
22258
|
}));
|
|
22115
22259
|
server.registerTool("taiga_unlink_repository", {
|
|
22116
22260
|
title: "Unlink a repository from a product",
|
|
22117
|
-
description: "Unlink a repository from a product. The product keeps its documents and initiatives, but nothing is planned against code, built or swept. The repository itself is untouched; one Taiga created for the product cannot be unlinked. To swap it, use taiga_link_repository with
|
|
22261
|
+
description: "Unlink a repository from a product. The product keeps its documents and initiatives, but nothing is planned against code, built or swept. The repository itself is untouched; one Taiga created for the product cannot be unlinked. To swap it, use taiga_link_repository with replacesLinkId.",
|
|
22118
22262
|
inputSchema: z.object({
|
|
22119
22263
|
productId,
|
|
22120
|
-
|
|
22264
|
+
linkId
|
|
22121
22265
|
}),
|
|
22122
22266
|
annotations: OPERATE
|
|
22123
22267
|
}, (params) => runTool(async () => {
|
|
22124
|
-
await client.unlinkRepository(params.productId, params.
|
|
22268
|
+
await client.unlinkRepository(params.productId, params.linkId);
|
|
22125
22269
|
return toolResult({
|
|
22126
|
-
|
|
22270
|
+
linkId: params.linkId,
|
|
22127
22271
|
unlinked: true
|
|
22128
22272
|
});
|
|
22129
22273
|
}));
|
|
@@ -22507,7 +22651,7 @@ function registerSetupTools(server, client) {
|
|
|
22507
22651
|
}));
|
|
22508
22652
|
server.registerTool("taiga_refresh_reality_documents", {
|
|
22509
22653
|
title: "Refresh a product's documents from its code",
|
|
22510
|
-
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;
|
|
22654
|
+
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.",
|
|
22511
22655
|
inputSchema: z.object({
|
|
22512
22656
|
productId,
|
|
22513
22657
|
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")
|
|
@@ -22536,7 +22680,7 @@ function registerSetupTools(server, client) {
|
|
|
22536
22680
|
if (!client.hasCapability("products:read")) return;
|
|
22537
22681
|
server.registerTool("taiga_start_import", {
|
|
22538
22682
|
title: "Import a codebase into a product",
|
|
22539
|
-
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.
|
|
22683
|
+
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).",
|
|
22540
22684
|
inputSchema: z.object({ productId }),
|
|
22541
22685
|
annotations: OPERATE
|
|
22542
22686
|
}, (params) => runTool(async () => {
|
|
@@ -22803,20 +22947,25 @@ function judgeBuild(until) {
|
|
|
22803
22947
|
return null;
|
|
22804
22948
|
};
|
|
22805
22949
|
}
|
|
22950
|
+
/** What each target may be waited for. A job has one condition, finished, so it takes none. */
|
|
22951
|
+
const UNTIL_FOR = {
|
|
22952
|
+
initiative: ITEM_CONDITIONS,
|
|
22953
|
+
run: BUILD_CONDITIONS
|
|
22954
|
+
};
|
|
22955
|
+
const ALL_CONDITIONS = [.../* @__PURE__ */ new Set([...ITEM_CONDITIONS, ...BUILD_CONDITIONS])];
|
|
22956
|
+
/**
|
|
22957
|
+
* One tool for waiting (PRO-2941): three wait tools, one per thing waited on, were two more names in a catalogue
|
|
22958
|
+
* every review ranked too large. The ID says what is waited on; until says for what, refused when it does not
|
|
22959
|
+
* apply to that thing.
|
|
22960
|
+
*/
|
|
22806
22961
|
function registerWaitTools(server, client, options) {
|
|
22807
|
-
|
|
22808
|
-
|
|
22809
|
-
|
|
22810
|
-
|
|
22811
|
-
initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it"),
|
|
22812
|
-
until: z.enum(ITEM_CONDITIONS).describe("What to wait for"),
|
|
22813
|
-
timeoutSeconds
|
|
22814
|
-
}),
|
|
22815
|
-
annotations: READ_ONLY
|
|
22816
|
-
}, (params, ctx) => runTool(async () => {
|
|
22962
|
+
const items = client.hasCapability("backlog:read");
|
|
22963
|
+
const runs = client.hasCapability("products:read");
|
|
22964
|
+
if (!items && !runs) return;
|
|
22965
|
+
const waitInitiative = async (target, until, params, ctx) => {
|
|
22817
22966
|
const gate = { product: void 0 };
|
|
22818
22967
|
const read = async (signal) => {
|
|
22819
|
-
const item = (await client.getBacklogItem(
|
|
22968
|
+
const item = (await client.getBacklogItem(target, signal)).data;
|
|
22820
22969
|
if (gate.product === void 0 && typeof item.autoBuild !== "boolean") gate.product = client.hasCapability("products:read") ? await client.getProduct(String(item.productId), signal).then((p) => {
|
|
22821
22970
|
const value = p.data.autoBuild;
|
|
22822
22971
|
return typeof value === "boolean" ? value : true;
|
|
@@ -22826,48 +22975,31 @@ function registerWaitTools(server, client, options) {
|
|
|
22826
22975
|
}) : null;
|
|
22827
22976
|
return item;
|
|
22828
22977
|
};
|
|
22829
|
-
const { now, ...outcome } = await waitFor(read, judgeItem(
|
|
22978
|
+
const { now, ...outcome } = await waitFor(read, judgeItem(until, gate), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
|
|
22830
22979
|
progressToken: ctx.mcpReq._meta?.progressToken,
|
|
22831
22980
|
notify: ctx.mcpReq.notify,
|
|
22832
22981
|
signal: ctx.mcpReq.signal
|
|
22833
22982
|
});
|
|
22834
22983
|
return toolResult({
|
|
22835
|
-
until
|
|
22984
|
+
until,
|
|
22836
22985
|
...outcome,
|
|
22837
22986
|
item: now ? itemReport(now) : null
|
|
22838
22987
|
});
|
|
22839
|
-
}
|
|
22840
|
-
|
|
22841
|
-
|
|
22842
|
-
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.",
|
|
22843
|
-
inputSchema: z.object({
|
|
22844
|
-
runId: idInput("run", "taiga_list_runs gives it"),
|
|
22845
|
-
until: z.enum(BUILD_CONDITIONS).describe("What to wait for"),
|
|
22846
|
-
timeoutSeconds
|
|
22847
|
-
}),
|
|
22848
|
-
annotations: READ_ONLY
|
|
22849
|
-
}, (params, ctx) => runTool(async () => {
|
|
22850
|
-
const { now, ...outcome } = await waitFor(async (signal) => (await client.getBuildRun(params.runId, signal)).data, judgeBuild(params.until), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
|
|
22988
|
+
};
|
|
22989
|
+
const waitRun = async (target, until, params, ctx) => {
|
|
22990
|
+
const { now, ...outcome } = await waitFor(async (signal) => (await client.getBuildRun(target, signal)).data, judgeBuild(until), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
|
|
22851
22991
|
progressToken: ctx.mcpReq._meta?.progressToken,
|
|
22852
22992
|
notify: ctx.mcpReq.notify,
|
|
22853
22993
|
signal: ctx.mcpReq.signal
|
|
22854
22994
|
});
|
|
22855
22995
|
return toolResult({
|
|
22856
|
-
until
|
|
22996
|
+
until,
|
|
22857
22997
|
...outcome,
|
|
22858
22998
|
run: now ? readableRow(pickFields([now], RUN_REPORT_FIELDS)[0]) : null
|
|
22859
22999
|
});
|
|
22860
|
-
}
|
|
22861
|
-
|
|
22862
|
-
|
|
22863
|
-
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.",
|
|
22864
|
-
inputSchema: z.object({
|
|
22865
|
-
jobId: idInput("job", "the tool that started the job returned it"),
|
|
22866
|
-
timeoutSeconds
|
|
22867
|
-
}),
|
|
22868
|
-
annotations: READ_ONLY
|
|
22869
|
-
}, (params, ctx) => runTool(async () => {
|
|
22870
|
-
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, {
|
|
23000
|
+
};
|
|
23001
|
+
const waitJob = async (target, params, ctx) => {
|
|
23002
|
+
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, {
|
|
22871
23003
|
progressToken: ctx.mcpReq._meta?.progressToken,
|
|
22872
23004
|
notify: ctx.mcpReq.notify,
|
|
22873
23005
|
signal: ctx.mcpReq.signal
|
|
@@ -22876,6 +23008,38 @@ function registerWaitTools(server, client, options) {
|
|
|
22876
23008
|
...outcome,
|
|
22877
23009
|
job: now ? jobReport(now) : null
|
|
22878
23010
|
});
|
|
23011
|
+
};
|
|
23012
|
+
server.registerTool("taiga_wait", {
|
|
23013
|
+
title: "Wait for an initiative, run or job",
|
|
23014
|
+
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.",
|
|
23015
|
+
inputSchema: z.object({
|
|
23016
|
+
initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it").optional(),
|
|
23017
|
+
runId: idInput("run", "taiga_list_runs gives it").optional(),
|
|
23018
|
+
jobId: idInput("job", "the tool that started the job returned it").optional(),
|
|
23019
|
+
until: z.enum(ALL_CONDITIONS).optional().describe("What to wait for: an initiative and a run need one"),
|
|
23020
|
+
timeoutSeconds
|
|
23021
|
+
}),
|
|
23022
|
+
annotations: READ_ONLY
|
|
23023
|
+
}, (params, ctx) => runTool(async () => {
|
|
23024
|
+
if ([
|
|
23025
|
+
"initiativeId",
|
|
23026
|
+
"runId",
|
|
23027
|
+
"jobId"
|
|
23028
|
+
].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.");
|
|
23029
|
+
if (params.jobId !== void 0) {
|
|
23030
|
+
if (params.until !== void 0) return toolError("VALIDATION_ERROR", "A job is waited on until it finishes: leave until out.");
|
|
23031
|
+
if (!runs) return toolError("FORBIDDEN", "Waiting on a job needs products:read, which this key lacks.");
|
|
23032
|
+
return waitJob(params.jobId, params, ctx);
|
|
23033
|
+
}
|
|
23034
|
+
const kind = params.initiativeId !== void 0 ? "initiative" : "run";
|
|
23035
|
+
const allowed = UNTIL_FOR[kind];
|
|
23036
|
+
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(", ")}.`);
|
|
23037
|
+
if (kind === "initiative") {
|
|
23038
|
+
if (!items) return toolError("FORBIDDEN", "Waiting on an initiative needs backlog:read, which this key lacks.");
|
|
23039
|
+
return waitInitiative(params.initiativeId, params.until, params, ctx);
|
|
23040
|
+
}
|
|
23041
|
+
if (!runs) return toolError("FORBIDDEN", "Waiting on a run needs products:read, which this key lacks.");
|
|
23042
|
+
return waitRun(params.runId, params.until, params, ctx);
|
|
22879
23043
|
}));
|
|
22880
23044
|
}
|
|
22881
23045
|
//#endregion
|
|
@@ -22933,6 +23097,12 @@ function refusedArguments(args) {
|
|
|
22933
23097
|
}));
|
|
22934
23098
|
return toolError("VALIDATION_ERROR", `Invalid arguments for ${tool}. ${lines.map((line) => line.argument ? `${line.argument}: ${line.message}` : line.message).join(" ")}`, { arguments: lines });
|
|
22935
23099
|
}
|
|
23100
|
+
/** Argument names this release replaced, and what replaced them. */
|
|
23101
|
+
const RENAMED_ARGUMENTS = {
|
|
23102
|
+
repositoryId: "linkId",
|
|
23103
|
+
replacesRepositoryId: "replacesLinkId",
|
|
23104
|
+
productRepositoryId: "linkId"
|
|
23105
|
+
};
|
|
22936
23106
|
/**
|
|
22937
23107
|
* A refused call says what to do next (PRO-2841), as a 404 already does. The
|
|
22938
23108
|
* SDK validates the arguments before the handler runs, where the 404 hints
|
|
@@ -22947,6 +23117,8 @@ function withRecoveryHints(tool, schema, inputs, isRead) {
|
|
|
22947
23117
|
const hinted = (issue) => {
|
|
22948
23118
|
if (issue.code === "unrecognized_keys" && issue.keys?.length) {
|
|
22949
23119
|
const pointers = issue.keys.map((key) => {
|
|
23120
|
+
const renamed = RENAMED_ARGUMENTS[key];
|
|
23121
|
+
if (renamed && (inputs.get(tool) ?? []).includes(renamed)) return `${tool} takes ${renamed}, which replaced ${key}.`;
|
|
22950
23122
|
const own = ownArgumentLike(key, inputs.get(tool) ?? []);
|
|
22951
23123
|
if (own) return `${tool} takes ${own}, spelled that way.`;
|
|
22952
23124
|
const other = closestToolTaking(key, tool, inputs, isRead);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taiga-ai/mcp-server",
|
|
3
|
-
"version": "0.26.0-dev.
|
|
3
|
+
"version": "0.26.0-dev.516",
|
|
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",
|