@taiga-ai/mcp-server 0.25.0-rc.464 → 0.25.0-rc.472
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 +129 -85
- package/package.json +1 -1
- package/src/client.ts +15 -9
- package/src/server.integration.test.ts +287 -63
- package/src/tools/builds.ts +62 -12
- package/src/tools/inbox.ts +7 -1
- package/src/tools/maintenance.ts +9 -4
- package/src/tools/repositories.ts +7 -2
- package/src/tools/sections.ts +3 -16
- package/src/tools/setup.ts +56 -64
- package/taiga-ai-mcp-server-0.25.0-rc.472.tgz +0 -0
- package/taiga-ai-mcp-server-0.25.0-rc.464.tgz +0 -0
package/dist/index.js
CHANGED
|
@@ -670,6 +670,29 @@ const POLICY_TYPE_BY_CONTROL_PREFIX = {
|
|
|
670
670
|
SCS: "policy_supply_chain",
|
|
671
671
|
CLD: "policy_cloud_governance"
|
|
672
672
|
};
|
|
673
|
+
/** The policy document a control id belongs to, or undefined for an unknown prefix. */
|
|
674
|
+
function policyTypeForControl(controlId) {
|
|
675
|
+
return POLICY_TYPE_BY_CONTROL_PREFIX[controlId.split("-")[0] ?? ""];
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* The policy a control id points a reader to (PRO-2809): its rightmost base
|
|
679
|
+
* prefix. An overlay family names its host policy last: TEL-TIC-1.1 is a
|
|
680
|
+
* telecom control in policy_tic, HC-FI-DATA-1.1 a Finnish healthcare control in
|
|
681
|
+
* policy_data, ARC-AWS-5 an AWS preset in policy_architecture. A regulatory
|
|
682
|
+
* family with no base prefix (GDPR, NIS2, AIACT) spans several policies and
|
|
683
|
+
* names none.
|
|
684
|
+
*
|
|
685
|
+
* For saying where to read a control, not for deciding who holds it:
|
|
686
|
+
* {@link policyTypeForControl} keeps an overlay control with the document it is
|
|
687
|
+
* written in, whatever its id says.
|
|
688
|
+
*/
|
|
689
|
+
function policyNamedByControl(controlId) {
|
|
690
|
+
const prefixes = controlId.split("-").filter((part) => /^[A-Z]/.test(part));
|
|
691
|
+
for (let at = prefixes.length - 1; at >= 0; at -= 1) {
|
|
692
|
+
const owner = POLICY_TYPE_BY_CONTROL_PREFIX[prefixes[at]];
|
|
693
|
+
if (owner) return owner;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
673
696
|
/**
|
|
674
697
|
* The shape of a control id in a policy's control table: `SEC-3.4`, and the
|
|
675
698
|
* overlay families too (`TEL-TIC-1.1`, `HC-FI-DATA-1.1`, `GDPR-1.1`). One
|
|
@@ -2663,17 +2686,6 @@ const sectionsInput = z.array(z.string().min(1).max(200)).max(50).optional().des
|
|
|
2663
2686
|
/** The sections input of a company document, where a policy control id also names the sections that state it. */
|
|
2664
2687
|
const policySectionsInput = sectionsInput.describe("Only these sections, by id, or a policy control id (SEC-5.1) for the sections that state it; [] for the outline (under outline: every section id and its size, no text); omit for the whole document");
|
|
2665
2688
|
/**
|
|
2666
|
-
* The policy that states a control: the rightmost policy prefix in its id, so an overlay family's
|
|
2667
|
-
* TEL-TIC-1.1 is policy_tic and HC-FI-DATA-1.1 policy_data, as the overlays file them.
|
|
2668
|
-
*/
|
|
2669
|
-
function policyOfControl(id) {
|
|
2670
|
-
const prefixes = id.split("-").filter((part) => /^[A-Z]/.test(part));
|
|
2671
|
-
for (const prefix of prefixes.reverse()) {
|
|
2672
|
-
const policy = POLICY_TYPE_BY_CONTROL_PREFIX[prefix];
|
|
2673
|
-
if (policy) return policy;
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
/**
|
|
2677
2689
|
* The sections that state each control id asked for (PRO-2800): policy
|
|
2678
2690
|
* standing cites SEC-5.1, and the section holding it has an id of its own
|
|
2679
2691
|
* (secretsManagement) an agent could only guess. A control id is found in
|
|
@@ -2699,7 +2711,7 @@ function withControls(outline, sections, policyType) {
|
|
|
2699
2711
|
return outline.map((entry) => {
|
|
2700
2712
|
const text = sectionContent(sections, entry.id) ?? "";
|
|
2701
2713
|
const ids = [...new Set([...text.matchAll(pattern)].filter((match) => !/^_?Reserved_?/i.test(match[2] ?? "")).map((match) => match[1]).filter((id) => {
|
|
2702
|
-
const owner =
|
|
2714
|
+
const owner = policyTypeForControl(id);
|
|
2703
2715
|
return owner === void 0 || owner === policyType;
|
|
2704
2716
|
}))];
|
|
2705
2717
|
return ids.length > 0 ? {
|
|
@@ -2727,7 +2739,7 @@ function sectionReadResult(doc, asked, label, options = {}) {
|
|
|
2727
2739
|
const known = ids.filter((id) => all.includes(id));
|
|
2728
2740
|
const found = Object.keys(controls).length > 0 ? { controls } : {};
|
|
2729
2741
|
const asControl = options.controlIds ? void 0 : unknownSections.find((id) => isControlId(id));
|
|
2730
|
-
const policy = asControl ?
|
|
2742
|
+
const policy = asControl ? policyNamedByControl(asControl) : void 0;
|
|
2731
2743
|
const controlNote = asControl ? { note: policy ? `${asControl} is a control of ${policy}: read it with taiga_get_document (type: ${policy}, sections: ["${asControl}"]).` : `${asControl} looks like a policy control id, which only a policy resolves: taiga_get_document without type lists them.` } : {};
|
|
2732
2744
|
if (known.length === 0) {
|
|
2733
2745
|
const { data: _data, ...meta } = cut.doc;
|
|
@@ -3729,6 +3741,22 @@ function runOutcome(run) {
|
|
|
3729
3741
|
};
|
|
3730
3742
|
}
|
|
3731
3743
|
/**
|
|
3744
|
+
* What hardening left blocking, in a sentence: the findings the run names (PRO-2817), or, for a run reported before
|
|
3745
|
+
* it recorded them, how many.
|
|
3746
|
+
*/
|
|
3747
|
+
function hardeningSaid(blocking, named) {
|
|
3748
|
+
const count = `Hardening ended with ${blocking} blocking finding${blocking === 1 ? "" : "s"} it could not fix`;
|
|
3749
|
+
if (named.length === 0) return `${count}; the run records how many, not which.`;
|
|
3750
|
+
const shown = named.slice(0, 3).map((finding) => {
|
|
3751
|
+
const message = String(finding["message"]);
|
|
3752
|
+
const where = finding["line"] ? `${String(finding["path"])}:${String(finding["line"])}` : String(finding["path"]);
|
|
3753
|
+
return `${message} (${String(finding["tool"])}, ${where})`;
|
|
3754
|
+
});
|
|
3755
|
+
const rest = blocking - shown.length;
|
|
3756
|
+
const recorded = named.length > shown.length ? `, ${named.length} recorded in taiga_get_run` : "";
|
|
3757
|
+
return `${count}: ${shown.join("; ")}${rest > 0 ? `; and ${rest} more${recorded}` : ""}.`;
|
|
3758
|
+
}
|
|
3759
|
+
/**
|
|
3732
3760
|
* What each failedReason means for the next move (PRO-2800): reviewers met dispatch_rejected and agent_reported as
|
|
3733
3761
|
* bare enums. As @taiga/types documents them.
|
|
3734
3762
|
*/
|
|
@@ -3753,10 +3781,11 @@ function readableRun(run, opts = {}) {
|
|
|
3753
3781
|
const means = FAILED_REASON_MEANS[String(run["failedReason"])];
|
|
3754
3782
|
const hardening = run["hardeningResult"];
|
|
3755
3783
|
const blocking = typeof hardening?.blockingFindings === "number" ? hardening.blockingFindings : 0;
|
|
3784
|
+
const named = Array.isArray(hardening?.blocking) ? hardening.blocking : [];
|
|
3756
3785
|
return {
|
|
3757
3786
|
...rest,
|
|
3758
3787
|
...means ? { failedReasonMeans: means } : {},
|
|
3759
|
-
...blocking > 0 ? { hardeningNote:
|
|
3788
|
+
...blocking > 0 ? { hardeningNote: hardeningSaid(blocking, named) } : {},
|
|
3760
3789
|
...opts.row || error === void 0 ? {} : { error },
|
|
3761
3790
|
...opts.row || reviewEscalation === void 0 ? {} : { reviewEscalation },
|
|
3762
3791
|
...typeof rest["prNumber"] === "number" || rest["prUrl"] ? { prDraft } : {},
|
|
@@ -3769,20 +3798,27 @@ const INITIATIVE_CHECKS = 10;
|
|
|
3769
3798
|
* The status of the initiative behind each run still waiting on its pull
|
|
3770
3799
|
* request (PRO-2800). A reviewing agent read a run of a canceled initiative as
|
|
3771
3800
|
* "waiting for a person to review and merge" while Action Required called the
|
|
3772
|
-
* same review stale.
|
|
3773
|
-
*
|
|
3774
|
-
*
|
|
3801
|
+
* same review stale.
|
|
3802
|
+
*
|
|
3803
|
+
* The run says it itself since PRO-2808 (initiativeClosed: done, canceled,
|
|
3804
|
+
* archived, deleted, or null while live), for every key that can read it.
|
|
3805
|
+
* Against an API from before that, each initiative is read: a 404 is
|
|
3806
|
+
* 'deleted', and an initiative past the cap, one whose read failed, and every
|
|
3807
|
+
* one without backlog:read is 'unchecked', which withInitiativeState adds to
|
|
3808
|
+
* the row's outcome.
|
|
3775
3809
|
*/
|
|
3776
3810
|
async function initiativeStates(client, rows) {
|
|
3777
3811
|
const states = /* @__PURE__ */ new Map();
|
|
3778
3812
|
const waiting = rows.filter((row) => ["needs_review", "completed"].includes(String(row["status"])) && row["prMerged"] !== true);
|
|
3779
|
-
const
|
|
3813
|
+
const told = waiting.filter((row) => "initiativeClosed" in row);
|
|
3814
|
+
for (const row of told) if (typeof row["backlogItemId"] === "string") states.set(row["backlogItemId"], typeof row["initiativeClosed"] === "string" ? row["initiativeClosed"] : "live");
|
|
3815
|
+
const all = [...new Set(waiting.filter((row) => !("initiativeClosed" in row)).map((row) => row["backlogItemId"]).filter((id) => typeof id === "string" && !states.has(id)))];
|
|
3780
3816
|
const ids = client.hasCapability("backlog:read") ? all.slice(0, INITIATIVE_CHECKS) : [];
|
|
3781
3817
|
for (const id of all) if (!ids.includes(id)) states.set(id, "unchecked");
|
|
3782
3818
|
await Promise.all(ids.map(async (id) => {
|
|
3783
3819
|
try {
|
|
3784
3820
|
const item = (await client.getBacklogItem(id)).data;
|
|
3785
|
-
states.set(id, String(item["status"] ?? item["state"]));
|
|
3821
|
+
states.set(id, item["archivedAt"] ? "archived" : String(item["status"] ?? item["state"]));
|
|
3786
3822
|
} catch (error) {
|
|
3787
3823
|
if (error instanceof TaigaApiError && error.status === 404) return void states.set(id, "deleted");
|
|
3788
3824
|
if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
|
|
@@ -3791,7 +3827,18 @@ async function initiativeStates(client, rows) {
|
|
|
3791
3827
|
}));
|
|
3792
3828
|
return states;
|
|
3793
3829
|
}
|
|
3794
|
-
/**
|
|
3830
|
+
/**
|
|
3831
|
+
* How each way an initiative ends reads on a run it left waiting: the same set
|
|
3832
|
+
* Action Required closes a run's review for (PRO-2806). A done one's waiting
|
|
3833
|
+
* run is not the one that landed it: that run is merged and never waiting.
|
|
3834
|
+
*/
|
|
3835
|
+
const INITIATIVE_ENDED = {
|
|
3836
|
+
done: "was marked done",
|
|
3837
|
+
canceled: "was canceled",
|
|
3838
|
+
archived: "was archived",
|
|
3839
|
+
deleted: "was deleted"
|
|
3840
|
+
};
|
|
3841
|
+
/** A readable run row that also says when its initiative ended while its pull request waited. */
|
|
3795
3842
|
function withInitiativeState(row, states) {
|
|
3796
3843
|
const state = states.get(String(row["backlogItemId"]));
|
|
3797
3844
|
if (!["gated", "unmerged"].includes(String(row["disposition"]))) return row;
|
|
@@ -3799,14 +3846,15 @@ function withInitiativeState(row, states) {
|
|
|
3799
3846
|
...row,
|
|
3800
3847
|
outcome: `${String(row["outcome"])} Whether its initiative still wants it was not checked.`
|
|
3801
3848
|
};
|
|
3802
|
-
|
|
3849
|
+
const ended = state ? INITIATIVE_ENDED[state] : void 0;
|
|
3850
|
+
if (!ended) return row;
|
|
3803
3851
|
const pr = typeof row["prNumber"] === "number" ? `PR #${row["prNumber"]}` : "its pull request";
|
|
3804
3852
|
const open = row["prState"] === "open" ? ` ${pr} is still open on the forge.` : "";
|
|
3805
3853
|
return {
|
|
3806
3854
|
...row,
|
|
3807
3855
|
disposition: "abandoned",
|
|
3808
3856
|
actions: [],
|
|
3809
|
-
outcome: `Its initiative
|
|
3857
|
+
outcome: `Its initiative ${ended}, so nobody is waiting on this run.${open}`
|
|
3810
3858
|
};
|
|
3811
3859
|
}
|
|
3812
3860
|
/** A step that changed only prose: no code of its own could have broken a test. */
|
|
@@ -3913,7 +3961,7 @@ function registerBuildTools(server, client) {
|
|
|
3913
3961
|
if (!client.hasCapability("products:read")) return;
|
|
3914
3962
|
server.registerTool("taiga_list_runs", {
|
|
3915
3963
|
title: "List runs",
|
|
3916
|
-
description: "List coding agent runs, newest first: branch, pull request, step progress, outcome (one sentence), actions and disposition: in_flight, gated (a person owes a review and merge), succeeded (merged), failed, stopped, rejected (pull request closed unmerged), unmerged, or abandoned (initiative canceled or deleted). 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).",
|
|
3964
|
+
description: "List coding agent runs, newest first: branch, pull request, step progress, outcome (one sentence), actions and disposition: in_flight, gated (a person owes a review and merge), succeeded (merged), failed, stopped, rejected (pull request closed unmerged), unmerged, or abandoned (its initiative ended: done, canceled, archived or deleted). 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).",
|
|
3917
3965
|
inputSchema: z.object({
|
|
3918
3966
|
productId: productId.optional(),
|
|
3919
3967
|
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; needs productId"),
|
|
@@ -4815,7 +4863,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4815
4863
|
if (!client.hasCapability("products:read")) return;
|
|
4816
4864
|
server.registerTool("taiga_list_findings", {
|
|
4817
4865
|
title: "List maintenance findings",
|
|
4818
|
-
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 (upstream_only: no fixed version
|
|
4866
|
+
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.",
|
|
4819
4867
|
inputSchema: z.object({
|
|
4820
4868
|
productId,
|
|
4821
4869
|
status: z.enum(MAINTENANCE_FINDING_STATUSES).optional().describe("Only findings in this status; omit for the ones needing action"),
|
|
@@ -4861,7 +4909,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4861
4909
|
}));
|
|
4862
4910
|
server.registerTool("taiga_get_policy_standing", {
|
|
4863
4911
|
title: "Get a product's policy standing",
|
|
4864
|
-
description: "How a product stands against the organization's published policies: summary counts, when the documents were assessed, and each violated control with its violations (critical and high only, citing the policy sentence and the contradicting passage) and the work on each. state: breached (open scanner findings map to it, by findingsByTier; routine-only is not urgent) or diverged (the documents contradict it). live: false means the violation's initiative ended without fixing it: it needs a person or a new fix. An unlisted control is not \"passing\" (summary.controlsWithNothingRecorded counts them). full: true adds evidence and resolved ones. Fix with taiga_fix_policy_violation.",
|
|
4912
|
+
description: "How a product stands against the organization's published policies: summary counts, when the documents were assessed, and each violated control with its violations (critical and high only, citing the policy sentence and the contradicting passage) and the work on each. state: breached (open scanner findings map to it, by findingsByTier; routine-only is not urgent) or diverged (the documents contradict it). live: false means the violation's initiative ended without fixing it: it needs a person or a new fix. raisedFor: false means the link is the backlog planner's: it said an initiative a person asked it for would close the violation, which may be a dedicated fix or broader work; true means a fix was raised for it (taiga_fix_policy_violation or the sweep). An unlisted control is not \"passing\" (summary.controlsWithNothingRecorded counts them). full: true adds evidence and resolved ones. Fix with taiga_fix_policy_violation.",
|
|
4865
4913
|
inputSchema: z.object({
|
|
4866
4914
|
productId,
|
|
4867
4915
|
full: z.boolean().optional().describe("Carry each violation's evidence and policy excerpt, cut to about 1,500 characters each, and up to 20 resolved violations (default false)"),
|
|
@@ -4944,7 +4992,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4944
4992
|
}));
|
|
4945
4993
|
server.registerTool("taiga_fix_finding", {
|
|
4946
4994
|
title: "Fix a finding",
|
|
4947
|
-
description: "Raise an initiative that fixes a maintenance finding, at the end of the product's line; it ends in a pull request a person merges, and the initiative's review gate applies. wholeGroup: true fixes the group in one initiative: every open advisory on that package version in one bump, or every location the same rule flagged. Refused for a secret only in history,
|
|
4995
|
+
description: "Raise an initiative that fixes a maintenance finding, at the end of the product's line; it ends in a pull request a person merges, and the initiative's review gate applies. wholeGroup: true fixes the group in one initiative: every open advisory on that package version in one bump, or every location the same rule flagged. Refused for a secret only in history, remediableBy upstream_only (code in a submodule), and a dependency with no fixed version yet (fixable: false).",
|
|
4948
4996
|
inputSchema: z.object({
|
|
4949
4997
|
productId,
|
|
4950
4998
|
findingId,
|
|
@@ -5196,7 +5244,8 @@ const SETUP_TASK_FIELDS = [
|
|
|
5196
5244
|
"providerActionUrl",
|
|
5197
5245
|
"inboxItemId",
|
|
5198
5246
|
"inboxItemStatus",
|
|
5199
|
-
"createdAt"
|
|
5247
|
+
"createdAt",
|
|
5248
|
+
"stale"
|
|
5200
5249
|
];
|
|
5201
5250
|
/** The API's count across every page, on the first page; nothing from an API that does not send one. */
|
|
5202
5251
|
function totalOf(result) {
|
|
@@ -5236,7 +5285,10 @@ function sameKeyOnce(rows) {
|
|
|
5236
5285
|
const { alsoFiledAs: _also, ...was } = kept;
|
|
5237
5286
|
for (const field of Object.keys(kept)) delete kept[field];
|
|
5238
5287
|
Object.assign(kept, row, { alsoFiledAs: [...also, record(was)] });
|
|
5239
|
-
} else
|
|
5288
|
+
} else {
|
|
5289
|
+
kept["alsoFiledAs"] = [...also, record(row)];
|
|
5290
|
+
if (isOpen(row) && row["stale"] === void 0) delete kept["stale"];
|
|
5291
|
+
}
|
|
5240
5292
|
}
|
|
5241
5293
|
return out;
|
|
5242
5294
|
}
|
|
@@ -5311,7 +5363,7 @@ function registerInboxTools(server, client) {
|
|
|
5311
5363
|
}));
|
|
5312
5364
|
server.registerTool("taiga_list_setup_tasks", {
|
|
5313
5365
|
title: "List setup tasks",
|
|
5314
|
-
description: "List what a person must set up for a product (secrets, variables, accounts), with exactly what and where: key (the name to create; Taiga never stores values), the provider page, and phase: prereq (the deploy fails without it), wiring (generated values back to providers) or followup (can wait until after launch). Runs do not wait for them. One product or all, oldest first; includeDone: true adds done and dismissed. A variable filed twice lists the others under alsoFiledAs; totalCount counts records, and a page that folded some says recordsOnPage. Unlike taiga_list_action_required, which lists the same work as items to resolve.",
|
|
5366
|
+
description: "List what a person must set up for a product (secrets, variables, accounts), with exactly what and where: key (the name to create; Taiga never stores values), the provider page, and phase: prereq (the deploy fails without it), wiring (generated values back to providers) or followup (can wait until after launch). Runs do not wait for them. One product or all, oldest first; includeDone: true adds done and dismissed. A variable filed twice lists the others under alsoFiledAs; totalCount counts records, and a page that folded some says recordsOnPage. stale on an open task says no document derived from the code since it was raised mentions its key: it may not apply any more, so check before asking anyone to set it. Unlike taiga_list_action_required, which lists the same work as items to resolve.",
|
|
5315
5367
|
inputSchema: z.object({
|
|
5316
5368
|
productId: productId.optional(),
|
|
5317
5369
|
includeDone: z.boolean().optional().describe("Also the tasks already done or dismissed (default false)"),
|
|
@@ -21059,7 +21111,8 @@ const GITHUB_REPOSITORY_FIELDS = [
|
|
|
21059
21111
|
"defaultBranch",
|
|
21060
21112
|
"description",
|
|
21061
21113
|
"language",
|
|
21062
|
-
"linked"
|
|
21114
|
+
"linked",
|
|
21115
|
+
"linkedProduct"
|
|
21063
21116
|
];
|
|
21064
21117
|
const repositoryId = idInput("repository link", "taiga_list_product_repositories gives it; a linkable repository id is not one");
|
|
21065
21118
|
/** Round-robin over lists: the first of each, then the second of each, and so on. */
|
|
@@ -21075,7 +21128,8 @@ function interleave(lists) {
|
|
|
21075
21128
|
*/
|
|
21076
21129
|
async function holderOf(client, error, productId) {
|
|
21077
21130
|
if (!(error instanceof TaigaApiError) || error.status !== 409) return null;
|
|
21078
|
-
|
|
21131
|
+
if (error.details?.["holder"]) return null;
|
|
21132
|
+
const name = /^(\S+\/\S+) is already linked to another product\./.exec(error.apiMessage ?? "")?.[1];
|
|
21079
21133
|
if (!name) return null;
|
|
21080
21134
|
try {
|
|
21081
21135
|
const holders = (await client.listProducts({
|
|
@@ -21137,7 +21191,7 @@ function registerRepositoryTools(server, client) {
|
|
|
21137
21191
|
}
|
|
21138
21192
|
if (client.hasCapability("vcs:read")) server.registerTool("taiga_list_linkable_repositories", {
|
|
21139
21193
|
title: "Find a repository to link",
|
|
21140
|
-
description: "List the repositories the organization's GitHub connection can reach, to link one with taiga_link_repository (it takes the row's id as linkableRepositoryId). query searches by name; account (login, owner, or installationId) reads one account, else rows come from every account in turn. linked: true means a product already links it
|
|
21194
|
+
description: "List the repositories the organization's GitHub connection can reach, to link one with taiga_link_repository (it takes the row's id as linkableRepositoryId). query searches by name; account (login, owner, or installationId) reads one account, else rows come from every account in turn. linked: true means a product already links it, and linkedProduct names it to a key that sees every product. No connection means GitHub is not connected yet; someone who can install the app does that once, in the web.",
|
|
21141
21195
|
inputSchema: z.object({
|
|
21142
21196
|
query: z.string().trim().min(1).max(256).optional().describe("Part of the repository name"),
|
|
21143
21197
|
account: z.string().trim().min(1).max(140).optional().describe("The GitHub account login (the owner in owner/repo), case-insensitive"),
|
|
@@ -21406,41 +21460,29 @@ const MISSING_DOCUMENTS_HOW = "An imported product gets them from its import (ta
|
|
|
21406
21460
|
/**
|
|
21407
21461
|
* Refuse a name already taken (PRO-2800): the API appends -2 to the slug and
|
|
21408
21462
|
* creates a second one, so a retried call or two agents at once left
|
|
21409
|
-
* duplicates nobody asked for.
|
|
21463
|
+
* duplicates nobody asked for. The API checks it in the create's transaction
|
|
21464
|
+
* (PRO-2808), so names past any page and two creates at once are caught too.
|
|
21465
|
+
* allowDuplicateName says the person means it.
|
|
21410
21466
|
*/
|
|
21411
21467
|
const allowDuplicateName = z.boolean().optional().describe("Create it even when one with the same name exists (default false: refused, naming the existing one)");
|
|
21412
|
-
/** The same name, as a person would read it: case and surrounding space aside. */
|
|
21413
|
-
const sameName = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
21414
|
-
/** Pages read looking for a taken name; past this the create goes ahead. */
|
|
21415
|
-
const NAME_PAGES = 5;
|
|
21416
21468
|
/**
|
|
21417
|
-
* The
|
|
21418
|
-
*
|
|
21419
|
-
|
|
21420
|
-
|
|
21421
|
-
|
|
21422
|
-
|
|
21423
|
-
|
|
21424
|
-
|
|
21425
|
-
|
|
21426
|
-
|
|
21427
|
-
|
|
21428
|
-
|
|
21429
|
-
|
|
21430
|
-
|
|
21431
|
-
|
|
21432
|
-
|
|
21433
|
-
|
|
21434
|
-
}
|
|
21435
|
-
/**
|
|
21436
|
-
* The refusal names the one that exists and leaves the choice to the person: another session may have made it, so
|
|
21437
|
-
* "use it" would hand the agent someone else's work (PRO-2800, review 6).
|
|
21438
|
-
*/
|
|
21439
|
-
function duplicateName(kind, existing, where) {
|
|
21440
|
-
const made = typeof existing.createdAt === "string" ? `, created ${existing.createdAt}` : "";
|
|
21441
|
-
return toolError("DUPLICATE_NAME", `A ${kind} named "${existing.name}" already exists ${where} (${existing.id}${made}). It may not be this person's: ask whether they meant that one, want another name, or want a second (allowDuplicateName: true).`, {
|
|
21442
|
-
existingId: existing.id,
|
|
21443
|
-
...existing.slug ? { existingSlug: existing.slug } : {}
|
|
21469
|
+
* The API's refusal of a taken name, as the tool's: it names the one that
|
|
21470
|
+
* exists when the caller may read it, and leaves the choice to the person.
|
|
21471
|
+
* Another session may have made it, so "use it" would hand the agent someone
|
|
21472
|
+
* else's work (PRO-2800, review 6). Anything else is not this refusal.
|
|
21473
|
+
*/
|
|
21474
|
+
function duplicateName(kind, error, where) {
|
|
21475
|
+
if (!(error instanceof TaigaApiError) || error.status !== 409 || error.details?.["reason"] !== "duplicate_name") return null;
|
|
21476
|
+
const said = (key) => typeof error.details?.[key] === "string" ? error.details[key] : void 0;
|
|
21477
|
+
const [existingId, existingSlug, name, createdAt] = [
|
|
21478
|
+
said("existingId"),
|
|
21479
|
+
said("existingSlug"),
|
|
21480
|
+
said("existingName"),
|
|
21481
|
+
said("existingCreatedAt")
|
|
21482
|
+
];
|
|
21483
|
+
return toolError("DUPLICATE_NAME", `${name ? `A ${kind} named "${name}"` : `A ${kind} with this name`} already exists ${where}${existingId ? ` (${existingId}${createdAt ? `, created ${createdAt}` : ""})` : ""}. ${existingId ? "It may not be this person's: ask whether they meant that one, want another name, or want a second" : "The caller cannot read it: ask whether they want another name or a second"} (allowDuplicateName: true).`, {
|
|
21484
|
+
...existingId ? { existingId } : {},
|
|
21485
|
+
...existingSlug ? { existingSlug } : {}
|
|
21444
21486
|
});
|
|
21445
21487
|
}
|
|
21446
21488
|
/** A product row the model can act on: how it started, and while in setup what it still needs. */
|
|
@@ -21481,14 +21523,16 @@ function registerSetupTools(server, client) {
|
|
|
21481
21523
|
}),
|
|
21482
21524
|
annotations: WRITE
|
|
21483
21525
|
}, (params) => runTool(async () => {
|
|
21484
|
-
|
|
21485
|
-
|
|
21486
|
-
|
|
21487
|
-
|
|
21488
|
-
}));
|
|
21489
|
-
|
|
21526
|
+
try {
|
|
21527
|
+
return toolResult(pickFields([(await client.createFactory({
|
|
21528
|
+
name: params.name,
|
|
21529
|
+
refuseDuplicateName: !params.allowDuplicateName
|
|
21530
|
+
})).data], FACTORY_FIELDS)[0]);
|
|
21531
|
+
} catch (error) {
|
|
21532
|
+
const refused = duplicateName("factory", error, "in this organization");
|
|
21533
|
+
if (refused) return refused;
|
|
21534
|
+
throw error;
|
|
21490
21535
|
}
|
|
21491
|
-
return toolResult(pickFields([(await client.createFactory({ name: params.name })).data], FACTORY_FIELDS)[0]);
|
|
21492
21536
|
}));
|
|
21493
21537
|
server.registerTool("taiga_rename_factory", {
|
|
21494
21538
|
title: "Rename a factory",
|
|
@@ -21553,22 +21597,22 @@ function registerSetupTools(server, client) {
|
|
|
21553
21597
|
}, (params) => runTool(async () => {
|
|
21554
21598
|
const contradiction = notesWithNothingToDescribe(params);
|
|
21555
21599
|
if (contradiction) return contradiction;
|
|
21556
|
-
|
|
21557
|
-
|
|
21600
|
+
let product;
|
|
21601
|
+
try {
|
|
21602
|
+
product = (await client.createProduct({
|
|
21603
|
+
name: params.name,
|
|
21558
21604
|
factoryId: params.factoryId,
|
|
21559
|
-
|
|
21560
|
-
|
|
21561
|
-
|
|
21562
|
-
|
|
21605
|
+
...params.start === "import" ? { projectType: "brownfield" } : {},
|
|
21606
|
+
...params.ownsInfrastructure !== void 0 ? { ownsInfrastructure: params.ownsInfrastructure } : {},
|
|
21607
|
+
...params.ownsDeployment !== void 0 ? { ownsDeployment: params.ownsDeployment } : {},
|
|
21608
|
+
...params.deploymentNotes !== void 0 ? { deploymentNotes: params.deploymentNotes } : {},
|
|
21609
|
+
refuseDuplicateName: !params.allowDuplicateName
|
|
21610
|
+
})).data;
|
|
21611
|
+
} catch (error) {
|
|
21612
|
+
const refused = duplicateName("product", error, "in this factory");
|
|
21613
|
+
if (refused) return refused;
|
|
21614
|
+
throw error;
|
|
21563
21615
|
}
|
|
21564
|
-
const product = (await client.createProduct({
|
|
21565
|
-
name: params.name,
|
|
21566
|
-
factoryId: params.factoryId,
|
|
21567
|
-
...params.start === "import" ? { projectType: "brownfield" } : {},
|
|
21568
|
-
...params.ownsInfrastructure !== void 0 ? { ownsInfrastructure: params.ownsInfrastructure } : {},
|
|
21569
|
-
...params.ownsDeployment !== void 0 ? { ownsDeployment: params.ownsDeployment } : {},
|
|
21570
|
-
...params.deploymentNotes !== void 0 ? { deploymentNotes: params.deploymentNotes } : {}
|
|
21571
|
-
})).data;
|
|
21572
21616
|
return toolResult(await productRow(client, product));
|
|
21573
21617
|
}));
|
|
21574
21618
|
server.registerTool("taiga_update_product", {
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -44,6 +44,8 @@ import type {
|
|
|
44
44
|
SetupTaskListEntry,
|
|
45
45
|
TenantDocument,
|
|
46
46
|
UptimeReport,
|
|
47
|
+
CreateFactoryInput,
|
|
48
|
+
CreateProductInput,
|
|
47
49
|
} from '@taiga/types';
|
|
48
50
|
import { SERVER_VERSION } from './version.js';
|
|
49
51
|
|
|
@@ -844,7 +846,7 @@ export class TaigaClient {
|
|
|
844
846
|
return this.request<Page<Factory>>('GET', `/factories${qs ? `?${qs}` : ''}`);
|
|
845
847
|
}
|
|
846
848
|
|
|
847
|
-
async createFactory(body:
|
|
849
|
+
async createFactory(body: CreateFactoryInput) {
|
|
848
850
|
return this.request<One<Factory>>('POST', '/factories', body);
|
|
849
851
|
}
|
|
850
852
|
|
|
@@ -856,14 +858,18 @@ export class TaigaClient {
|
|
|
856
858
|
// Setting a product up
|
|
857
859
|
// ---------------------------------------------------------------------------
|
|
858
860
|
|
|
859
|
-
async createProduct(
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
861
|
+
async createProduct(
|
|
862
|
+
body: Pick<
|
|
863
|
+
CreateProductInput,
|
|
864
|
+
| 'name'
|
|
865
|
+
| 'factoryId'
|
|
866
|
+
| 'projectType'
|
|
867
|
+
| 'ownsInfrastructure'
|
|
868
|
+
| 'ownsDeployment'
|
|
869
|
+
| 'deploymentNotes'
|
|
870
|
+
| 'refuseDuplicateName'
|
|
871
|
+
>
|
|
872
|
+
) {
|
|
867
873
|
return this.request<One<Product>>('POST', '/products', body);
|
|
868
874
|
}
|
|
869
875
|
|