@taiga-ai/mcp-server 0.26.0-rc.504 → 0.26.0-rc.514
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +140 -19
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -356,6 +356,17 @@ var TaigaClient = class {
|
|
|
356
356
|
async getFinding(id) {
|
|
357
357
|
return this.request("GET", `/maintenance/findings/${seg(id)}`);
|
|
358
358
|
}
|
|
359
|
+
/** The package inventory (PRO-2862); 404 where the `licenses` feature is not enabled. */
|
|
360
|
+
async listPackages(productId, opts) {
|
|
361
|
+
return this.request("GET", `/products/${seg(productId)}/maintenance/packages${query$1({ ...opts })}`);
|
|
362
|
+
}
|
|
363
|
+
async getLicenseSummary(productId) {
|
|
364
|
+
return this.request("GET", `/products/${seg(productId)}/maintenance/licenses`);
|
|
365
|
+
}
|
|
366
|
+
/** A repository's CycloneDX 1.5 SBOM (PRO-2894), the document itself. */
|
|
367
|
+
async getSbom(productId, repositoryId) {
|
|
368
|
+
return this.request("GET", `/products/${seg(productId)}/maintenance/repositories/${seg(repositoryId)}/sbom`);
|
|
369
|
+
}
|
|
359
370
|
async updateFinding(id, body) {
|
|
360
371
|
return this.request("PATCH", `/maintenance/findings/${seg(id)}`, body);
|
|
361
372
|
}
|
|
@@ -1457,6 +1468,24 @@ function hardeningCoverage(result) {
|
|
|
1457
1468
|
gaps
|
|
1458
1469
|
};
|
|
1459
1470
|
}
|
|
1471
|
+
//#endregion
|
|
1472
|
+
//#region ../../packages/types/dist/inbox.js
|
|
1473
|
+
/** What action is required. Drives icon, copy, and completion logic. */
|
|
1474
|
+
const INBOX_ITEM_KINDS = [
|
|
1475
|
+
"review_plan",
|
|
1476
|
+
"review_backlog",
|
|
1477
|
+
"review_doc",
|
|
1478
|
+
"review_pr",
|
|
1479
|
+
"review_maintenance",
|
|
1480
|
+
"review_policy",
|
|
1481
|
+
"setup_env_var",
|
|
1482
|
+
"setup_secret",
|
|
1483
|
+
"setup_third_party",
|
|
1484
|
+
"setup_dns",
|
|
1485
|
+
"setup_billing",
|
|
1486
|
+
"setup_cloud_permission",
|
|
1487
|
+
"setup_manual"
|
|
1488
|
+
];
|
|
1460
1489
|
z.discriminatedUnion("kind", [
|
|
1461
1490
|
z.object({
|
|
1462
1491
|
type: z.literal("narrative"),
|
|
@@ -2443,7 +2472,7 @@ const DOCUMENT_INPUTS = Object.freeze({
|
|
|
2443
2472
|
* reads as deleted) and fields they explain would simply be missing. So it
|
|
2444
2473
|
* refuses to start, and says what to do.
|
|
2445
2474
|
*/
|
|
2446
|
-
function apiLevelProblem(introspection, apiUrl, required =
|
|
2475
|
+
function apiLevelProblem(introspection, apiUrl, required = 3, version = SERVER_VERSION) {
|
|
2447
2476
|
const offered = introspection.mcpApiLevel ?? 0;
|
|
2448
2477
|
if (offered >= required) return null;
|
|
2449
2478
|
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.`;
|
|
@@ -2934,7 +2963,11 @@ async function runTool(handler) {
|
|
|
2934
2963
|
if (error instanceof TaigaApiError) {
|
|
2935
2964
|
const code = error.code ?? CODE_BY_STATUS[error.status] ?? "API_ERROR";
|
|
2936
2965
|
const worded = inProductWording(error.apiMessage ?? `Taiga API answered ${error.status}.`);
|
|
2937
|
-
return toolError(code, error.status === 404 ? withNotFoundHint(worded) :
|
|
2966
|
+
return toolError(code, error.status === 404 ? withNotFoundHint(worded) : [
|
|
2967
|
+
502,
|
|
2968
|
+
503,
|
|
2969
|
+
504
|
|
2970
|
+
].includes(error.status) ? `${withStop(worded)} This is usually temporary: a read may answer on a retry; after a write, check whether it took effect before repeating it.` : worded, error.details);
|
|
2938
2971
|
}
|
|
2939
2972
|
if (isNetworkFailure(error)) return toolError("NETWORK_ERROR", `Could not reach the Taiga API: ${networkFailureDetail(error)}. Check TAIGA_API_URL.`);
|
|
2940
2973
|
throw error;
|
|
@@ -3902,10 +3935,21 @@ async function publishDraft(scope) {
|
|
|
3902
3935
|
...published.warnings ? { warnings: published.warnings } : {}
|
|
3903
3936
|
});
|
|
3904
3937
|
}
|
|
3938
|
+
/** The section ids that are not control ids at all. GDPR-1.1 is a control id, so it is not among them, though it names no single policy. */
|
|
3939
|
+
function notControls(sections) {
|
|
3940
|
+
return sections.filter((id) => !isControlId(id.toUpperCase()));
|
|
3941
|
+
}
|
|
3942
|
+
/** The policies the section ids name when every one is a control id (SEC-5.1), else none. */
|
|
3943
|
+
function policiesNamed(sections) {
|
|
3944
|
+
if (sections.length === 0) return [];
|
|
3945
|
+
const types = sections.map((id) => isControlId(id.toUpperCase()) ? policyNamedByControl(id.toUpperCase()) : void 0);
|
|
3946
|
+
if (types.some((type) => type === void 0)) return [];
|
|
3947
|
+
return [...new Set(types)];
|
|
3948
|
+
}
|
|
3905
3949
|
function registerDocumentTools(server, client) {
|
|
3906
3950
|
if (client.hasCapability("tenant:documents:read")) server.registerTool("taiga_get_organization_document", {
|
|
3907
3951
|
title: "Get an organization document",
|
|
3908
|
-
description: `Read the organization's own documents, which apply to every product. Two answers: without type, the list (type, name, version, draft state); with type, that document. The ${POLICY_TYPES.length} policies (${named$1(POLICY_TYPES)}) set the rules for every product; the others (${named$1(OTHER_TENANT_DOCUMENT_TYPES)}) carry goals, standards, voice and instructions. Policies are long: pass sections: [] for the outline, then the section ids you need, or a control id such as SEC-5.1; unknownSections names ids it lacks. A miss says which documents are published and whether a draft exists. A section's source (template, overlay-<id>, cloud-preset:<providers>) and priority (core: every organization keeps it) do not change what it requires.`,
|
|
3952
|
+
description: `Read the organization's own documents, which apply to every product. Two answers: without type, the list (type, name, version, draft state); with type, that document. The ${POLICY_TYPES.length} policies (${named$1(POLICY_TYPES)}) set the rules for every product; the others (${named$1(OTHER_TENANT_DOCUMENT_TYPES)}) carry goals, standards, voice and instructions. Policies are long: pass sections: [] for the outline, then the section ids you need, or a control id such as SEC-5.1 (which names its policy, so type may be left out); unknownSections names ids it lacks. A miss says which documents are published and whether a draft exists. A section's source (template, overlay-<id>, cloud-preset:<providers>) and priority (core: every organization keeps it) do not change what it requires.`,
|
|
3909
3953
|
inputSchema: z.object({
|
|
3910
3954
|
type: tenantDocumentType.optional(),
|
|
3911
3955
|
sections: policySectionsInput,
|
|
@@ -3913,19 +3957,25 @@ function registerDocumentTools(server, client) {
|
|
|
3913
3957
|
}),
|
|
3914
3958
|
annotations: READ_ONLY
|
|
3915
3959
|
}, (params) => runTool(async () => {
|
|
3916
|
-
|
|
3917
|
-
|
|
3960
|
+
const named = params.type === void 0 && params.sections ? policiesNamed(params.sections) : [];
|
|
3961
|
+
const args = params.type === void 0 && named.length === 1 ? {
|
|
3962
|
+
...params,
|
|
3963
|
+
type: named[0],
|
|
3964
|
+
sections: params.sections?.map((id) => id.toUpperCase())
|
|
3965
|
+
} : params;
|
|
3966
|
+
if (args.type === void 0) {
|
|
3967
|
+
if (args.sections) return toolError("VALIDATION_ERROR", named.length > 1 ? `Those control ids are in different policies (${named.join(", ")}): read one type at a time.` : notControls(args.sections).length > 0 && notControls(args.sections).length < args.sections.length ? `Not a control id: ${notControls(args.sections).join(", ")}. A control id such as SEC-5.1 names its policy; any other section id needs type.` : "Name the document (type) to read its sections; a control id such as SEC-5.1 names its policy itself, but one of a family that spans policies (GDPR, NIS2) does not.");
|
|
3918
3968
|
const result = await client.listTenantDocuments({
|
|
3919
|
-
limit:
|
|
3920
|
-
cursor:
|
|
3969
|
+
limit: args.limit,
|
|
3970
|
+
cursor: args.cursor
|
|
3921
3971
|
});
|
|
3922
3972
|
return toolResult(page(summaries(result.data), result.nextCursor));
|
|
3923
3973
|
}
|
|
3924
|
-
const type =
|
|
3925
|
-
if (
|
|
3974
|
+
const type = args.type;
|
|
3975
|
+
if (args.limit !== void 0 || args.cursor !== void 0) return toolError("VALIDATION_ERROR", params.type === void 0 ? "limit and cursor page the list, which takes no sections; drop them to read the control." : "limit and cursor page the list; omit type to list, or drop them to read.");
|
|
3926
3976
|
try {
|
|
3927
3977
|
const doc = stripProvenance((await client.getTenantDocument(type)).data);
|
|
3928
|
-
return
|
|
3978
|
+
return args.sections ? sectionReadResult(doc, args.sections, type, { controlIds: POLICY_TYPES.includes(type) }) : toolResult(doc);
|
|
3929
3979
|
} catch (error) {
|
|
3930
3980
|
if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
|
|
3931
3981
|
const [draft, all] = await Promise.allSettled([client.getTenantDocumentDraft(type), client.listTenantDocuments({ limit: 100 })]);
|
|
@@ -4672,7 +4722,7 @@ function waitingOn(rows, station) {
|
|
|
4672
4722
|
}
|
|
4673
4723
|
const STATION_NAMED = 3;
|
|
4674
4724
|
/** Who holds the station, or null when the key cannot read the backlog or the read failed. */
|
|
4675
|
-
async function stationHolders(client, product,
|
|
4725
|
+
async function stationHolders(client, product, except) {
|
|
4676
4726
|
if (!client.hasCapability("backlog:read")) return null;
|
|
4677
4727
|
try {
|
|
4678
4728
|
const columns = await Promise.all(["plan", "build"].map((state) => client.listBacklogItems(product, {
|
|
@@ -4680,7 +4730,7 @@ async function stationHolders(client, product, head) {
|
|
|
4680
4730
|
order: "newest",
|
|
4681
4731
|
limit: 100
|
|
4682
4732
|
})));
|
|
4683
|
-
const rows = columns.flatMap((page) => page.data).filter((row) => row.id
|
|
4733
|
+
const rows = columns.flatMap((page) => page.data).filter((row) => !except.has(row.id));
|
|
4684
4734
|
const capped = columns.some((page) => page.nextCursor !== null);
|
|
4685
4735
|
return {
|
|
4686
4736
|
count: rows.length,
|
|
@@ -4756,10 +4806,14 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
4756
4806
|
});
|
|
4757
4807
|
const note = unnamed === 0 ? null : !canName ? "Entries carry initiativeId only: naming them needs backlog:read, which this key lacks." : entries.length > NAMED_ENTRIES ? `Only the first ${NAMED_ENTRIES} entries are named; the rest carry initiativeId only.` : "An entry whose initiative could not be read carries initiativeId only; taiga_get_initiative reads it.";
|
|
4758
4808
|
const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
|
|
4809
|
+
const idle = rows.length > 0 && blockingIndex(rows, (row) => row.entryState) < 0;
|
|
4810
|
+
const station = rows.length > 0 || options.name === "all" ? await stationHolders(client, product, new Set(idle ? [rows[0].backlogItemId] : rows.map((row) => row.backlogItemId))) : null;
|
|
4811
|
+
const offLine = !idle && station && station.count > 0 ? { offTheLine: `${station.atLeast ? "At least " : ""}${station.count} initiative${station.count === 1 ? " is" : "s are"} in Plan or Build without a queue entry (${station.titles.map((title) => `"${title}"`).join(", ")}${station.count > station.titles.length ? ", and more" : ""}): moved there by hand or waiting on a pull request review. Each holds the station like the active entry; taiga_list_initiatives with column: 'plan' or 'build' lists them.` } : {};
|
|
4759
4812
|
return {
|
|
4760
4813
|
linePaused: paused === null ? null : typeof paused === "string",
|
|
4761
4814
|
entries: rows.map(({ lifecycle: _lifecycle, runId: _runId, ...row }) => row),
|
|
4762
|
-
waitingOn: line ?? waitingOn(rows,
|
|
4815
|
+
waitingOn: line ?? waitingOn(rows, idle ? station : null),
|
|
4816
|
+
...offLine,
|
|
4763
4817
|
...note ? { note } : {}
|
|
4764
4818
|
};
|
|
4765
4819
|
}
|
|
@@ -5363,6 +5417,19 @@ function registerPlanVerdicts(server, client) {
|
|
|
5363
5417
|
* next sweep no longer detects it, so there is no tool for either.
|
|
5364
5418
|
*/
|
|
5365
5419
|
/** A finding as a list row: enough to decide whether to open, fix or ignore it. */
|
|
5420
|
+
const PACKAGE_ROW_FIELDS = [
|
|
5421
|
+
"id",
|
|
5422
|
+
"ecosystem",
|
|
5423
|
+
"packageName",
|
|
5424
|
+
"version",
|
|
5425
|
+
"purl",
|
|
5426
|
+
"licenses",
|
|
5427
|
+
"scope",
|
|
5428
|
+
"relation",
|
|
5429
|
+
"sourcePath",
|
|
5430
|
+
"productRepositoryId",
|
|
5431
|
+
"detectedAt"
|
|
5432
|
+
];
|
|
5366
5433
|
const FINDING_ROW_FIELDS = [
|
|
5367
5434
|
"id",
|
|
5368
5435
|
"kind",
|
|
@@ -5436,7 +5503,7 @@ function findingRows(findings, extra = []) {
|
|
|
5436
5503
|
"dependencyGroups",
|
|
5437
5504
|
"dependencyRelation",
|
|
5438
5505
|
...extra
|
|
5439
|
-
]).map((row) => withDependencyScope(row)).map(withFixability);
|
|
5506
|
+
]).map((row) => withDependencyScope(row)).map(withFixability).map((row) => Object.fromEntries(Object.entries(row).filter(([key, value]) => value !== null || key === "ignoreExpiresAt" && row["status"] === "ignored")));
|
|
5440
5507
|
}
|
|
5441
5508
|
/** The parts of an advisory record an agent reads; the rest (every affected range, every reference) is full: true. */
|
|
5442
5509
|
const ADVISORY_KEPT = [
|
|
@@ -5544,9 +5611,10 @@ function registerMaintenanceTools(server, client) {
|
|
|
5544
5611
|
if (!client.hasCapability("products:read")) return;
|
|
5545
5612
|
server.registerTool("taiga_list_findings", {
|
|
5546
5613
|
title: "List maintenance findings",
|
|
5547
|
-
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. taiga_get_finding reads one.",
|
|
5614
|
+
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.",
|
|
5548
5615
|
inputSchema: z.object({
|
|
5549
5616
|
productId,
|
|
5617
|
+
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)"),
|
|
5550
5618
|
status: z.enum(MAINTENANCE_FINDING_STATUSES).optional().describe("Only findings in this status; omit for the ones needing action"),
|
|
5551
5619
|
severity: z.enum(MAINTENANCE_FINDING_SEVERITIES).optional().describe("Only this severity"),
|
|
5552
5620
|
kind: z.enum(MAINTENANCE_FINDING_KINDS).optional().describe("Only this kind of finding"),
|
|
@@ -5554,7 +5622,21 @@ function registerMaintenanceTools(server, client) {
|
|
|
5554
5622
|
}),
|
|
5555
5623
|
annotations: READ_ONLY
|
|
5556
5624
|
}, (params) => runTool(async () => {
|
|
5557
|
-
const { productId: id, ...filters } = params;
|
|
5625
|
+
const { productId: id, controlId, ...filters } = params;
|
|
5626
|
+
if (controlId) {
|
|
5627
|
+
if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", "controlId reads every page itself; drop the cursor.");
|
|
5628
|
+
const wanted = controlId.toUpperCase();
|
|
5629
|
+
const found = await searchPages((cursor) => client.listFindings(id, {
|
|
5630
|
+
...filters,
|
|
5631
|
+
limit: 100,
|
|
5632
|
+
cursor
|
|
5633
|
+
}), "", () => [""], params.limit ?? 25, (row) => Array.isArray(row["controls"]) && row["controls"].some((c) => String(c).toUpperCase() === wanted));
|
|
5634
|
+
return toolResult({
|
|
5635
|
+
...page(findingRows(found.rows), null),
|
|
5636
|
+
...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." } : {},
|
|
5637
|
+
...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.` } : {}
|
|
5638
|
+
});
|
|
5639
|
+
}
|
|
5558
5640
|
const [result, sweep] = await Promise.all([client.listFindings(id, filters), params.cursor === void 0 ? sweepOverview(client, id).catch((error) => {
|
|
5559
5641
|
if (error instanceof TaigaApiError || isNetworkFailure(error)) return "unread";
|
|
5560
5642
|
throw error;
|
|
@@ -5569,6 +5651,35 @@ function registerMaintenanceTools(server, client) {
|
|
|
5569
5651
|
...params.status === void 0 && params.severity === void 0 && params.kind === void 0 && typeof result.totalCount === "number" && sweep && sweep !== "unread" && typeof sweep["openFindings"] === "number" && sweep["openFindings"] !== result.totalCount ? { totalNote: "totalCount includes findings with a fix under way (remediating) and ignored findings whose ignore has expired; sweep.openFindings counts open ones only." } : {}
|
|
5570
5652
|
});
|
|
5571
5653
|
}));
|
|
5654
|
+
server.registerTool("taiga_list_packages", {
|
|
5655
|
+
title: "List the package inventory",
|
|
5656
|
+
description: "Every package a product's repositories resolve, vulnerable or not, as the latest sweep read it: name, version, ecosystem, SPDX licenses, scope (dev / runtime / unknown), relation (direct / transitive / unknown) and the lockfile. The first page opens with licenses: each license string across the inventory, how many packages carry it and its standing against the allowlist (allowed, not_allowed, unknown), plus the allowlist and when each repository was last inventoried. Filter by one license string (UNKNOWN for the ones the data source could not name), a name substring or a repository. A license outside the allowlist is also a license_violation finding in taiga_list_findings. Where the licenses feature is not enabled the product answers NOT_FOUND. taiga_get_sbom downloads one repository as CycloneDX.",
|
|
5657
|
+
inputSchema: z.object({
|
|
5658
|
+
productId,
|
|
5659
|
+
license: z.string().min(1).max(4096).optional().describe("Only packages carrying exactly this SPDX string"),
|
|
5660
|
+
search: z.string().min(1).max(255).optional().describe("Case-insensitive substring of the package name"),
|
|
5661
|
+
productRepositoryId: idInput("repository", "taiga_list_product_repositories gives it", "only this repository").optional(),
|
|
5662
|
+
...pagination
|
|
5663
|
+
}),
|
|
5664
|
+
annotations: READ_ONLY
|
|
5665
|
+
}, (params) => runTool(async () => {
|
|
5666
|
+
const { productId: id, ...filters } = params;
|
|
5667
|
+
const [result, summary] = await Promise.all([client.listPackages(id, filters), params.cursor === void 0 ? client.getLicenseSummary(id) : Promise.resolve(null)]);
|
|
5668
|
+
return toolResult({
|
|
5669
|
+
...summary ? { licenses: summary.data } : {},
|
|
5670
|
+
...page(pickFields(result.data, PACKAGE_ROW_FIELDS), result.nextCursor),
|
|
5671
|
+
...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {}
|
|
5672
|
+
});
|
|
5673
|
+
}));
|
|
5674
|
+
server.registerTool("taiga_get_sbom", {
|
|
5675
|
+
title: "Download a repository SBOM",
|
|
5676
|
+
description: "One repository's software bill of materials as a CycloneDX 1.5 JSON document, rendered from the stored package inventory: metadata (timestamp, tools, the repository), every package with its purl, license and scope, and the packages the repository declares directly. The whole document is returned, so for a large repository the answer is large; read taiga_list_packages when a summary is enough. NOT_FOUND where the licenses feature is not enabled, and for a repository no sweep has inventoried yet.",
|
|
5677
|
+
inputSchema: z.object({
|
|
5678
|
+
productId,
|
|
5679
|
+
repositoryId: idInput("repository", "taiga_list_product_repositories gives it")
|
|
5680
|
+
}),
|
|
5681
|
+
annotations: READ_ONLY
|
|
5682
|
+
}, (params) => runTool(async () => toolResult(await client.getSbom(params.productId, params.repositoryId))));
|
|
5572
5683
|
server.registerTool("taiga_get_finding", {
|
|
5573
5684
|
title: "Get a maintenance finding",
|
|
5574
5685
|
description: "Read one maintenance finding: exploit likelihood (EPSS, known-exploited list), the advisory summarized (full: true for its whole record: every affected range and reference) and fixed version, code location, breached policy controls, why it ranks where it does, history (reintroduced, previously resolved) and the remediation under way. fixable: false with fixNote says taiga_fix_finding would refuse it. A secret's value is never stored, only the rule and location.",
|
|
@@ -5788,6 +5899,9 @@ function readWith(row, backlogRead) {
|
|
|
5788
5899
|
* were the same things.
|
|
5789
5900
|
*/
|
|
5790
5901
|
const KIND_SAID = { review_backlog: "review_initiatives" };
|
|
5902
|
+
/** The kinds as the rows say them, for the kind filter, and the way back to the API's word. */
|
|
5903
|
+
const KINDS_SAID = INBOX_ITEM_KINDS.map((kind) => KIND_SAID[kind] ?? kind);
|
|
5904
|
+
const KIND_FROM_SAID = Object.fromEntries(Object.entries(KIND_SAID).map(([api, said]) => [said, api]));
|
|
5791
5905
|
const SUBJECT_SAID = {
|
|
5792
5906
|
backlog_item: "initiative",
|
|
5793
5907
|
build_run: "run",
|
|
@@ -6060,15 +6174,17 @@ function registerInboxTools(server, client) {
|
|
|
6060
6174
|
inputSchema: z.object({
|
|
6061
6175
|
productId: productId.optional(),
|
|
6062
6176
|
category: z.enum(["gate", "setup"]).optional().describe("gate: things to review; setup: things to set up"),
|
|
6177
|
+
kind: z.enum(KINDS_SAID).optional().describe("Only this kind, such as review_plan or review_pr, as the rows name it"),
|
|
6063
6178
|
resolved: z.boolean().optional().describe("Read the resolved archive (done and dismissed) instead"),
|
|
6064
6179
|
full: z.boolean().optional().describe("Carry each item's metadata, such as a document decision's whole text (default false)"),
|
|
6065
6180
|
...pagination
|
|
6066
6181
|
}),
|
|
6067
6182
|
annotations: READ_ONLY
|
|
6068
6183
|
}, (params) => runTool(async () => {
|
|
6069
|
-
const { resolved, full, ...rest } = params;
|
|
6184
|
+
const { resolved, full, kind, ...rest } = params;
|
|
6070
6185
|
const query = {
|
|
6071
6186
|
...rest,
|
|
6187
|
+
...kind ? { kind: KIND_FROM_SAID[kind] ?? kind } : {},
|
|
6072
6188
|
...resolved ? { resolved: true } : {}
|
|
6073
6189
|
};
|
|
6074
6190
|
const first = params.cursor === void 0;
|
|
@@ -6503,10 +6619,15 @@ async function standingPart(client, productId) {
|
|
|
6503
6619
|
const standing = (await client.getPolicyStanding(productId)).data;
|
|
6504
6620
|
const summary = standingSummary(standing);
|
|
6505
6621
|
const state = standing.assessment?.state;
|
|
6622
|
+
const controls = Array.isArray(standing.controls) ? standing.controls : [];
|
|
6623
|
+
const withAny = (key) => controls.filter((control) => Number(control[key] ?? 0) > 0).length;
|
|
6506
6624
|
return {
|
|
6507
6625
|
openViolations: summary.violations,
|
|
6508
6626
|
bySeverity: summary.bySeverity,
|
|
6509
|
-
|
|
6627
|
+
controlsBreached: withAny("findingCount"),
|
|
6628
|
+
controlsDiverged: withAny("divergenceCount"),
|
|
6629
|
+
controlsMet: summary.controlsMet,
|
|
6630
|
+
controlsNotApplicable: summary.controlsNotApplicable,
|
|
6510
6631
|
...state === "never" || state === "failed" ? { note: `The assessment ${state === "never" ? "has never run" : "failed"}: no violations here is not an all-clear.` } : {}
|
|
6511
6632
|
};
|
|
6512
6633
|
}
|
|
@@ -6864,7 +6985,7 @@ const ENVIRONMENT_FIELDS = [
|
|
|
6864
6985
|
*/
|
|
6865
6986
|
function deploymentLines(rows) {
|
|
6866
6987
|
return rows.map((row) => {
|
|
6867
|
-
const repository = row.metadata?.
|
|
6988
|
+
const repository = row.metadata?.repositoryFullName;
|
|
6868
6989
|
return {
|
|
6869
6990
|
...pickFields([row], DEPLOYMENT_FIELDS)[0],
|
|
6870
6991
|
repository: typeof repository === "string" ? repository : null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taiga-ai/mcp-server",
|
|
3
|
-
"version": "0.26.0-rc.
|
|
3
|
+
"version": "0.26.0-rc.514",
|
|
4
4
|
"description": "Taiga for AI coding assistants: read products, policies, initiatives and runs, and operate the line, over MCP",
|
|
5
5
|
"mcpName": "ga.tai/mcp-server",
|
|
6
6
|
"type": "module",
|