@taiga-ai/mcp-server 0.25.0-dev.463 → 0.25.0-dev.465
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
CHANGED
|
@@ -4815,7 +4815,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4815
4815
|
if (!client.hasCapability("products:read")) return;
|
|
4816
4816
|
server.registerTool("taiga_list_findings", {
|
|
4817
4817
|
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
|
|
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 (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
4819
|
inputSchema: z.object({
|
|
4820
4820
|
productId,
|
|
4821
4821
|
status: z.enum(MAINTENANCE_FINDING_STATUSES).optional().describe("Only findings in this status; omit for the ones needing action"),
|
|
@@ -4944,7 +4944,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4944
4944
|
}));
|
|
4945
4945
|
server.registerTool("taiga_fix_finding", {
|
|
4946
4946
|
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,
|
|
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, remediableBy upstream_only (code in a submodule), and a dependency with no fixed version yet (fixable: false).",
|
|
4948
4948
|
inputSchema: z.object({
|
|
4949
4949
|
productId,
|
|
4950
4950
|
findingId,
|
|
@@ -21059,7 +21059,8 @@ const GITHUB_REPOSITORY_FIELDS = [
|
|
|
21059
21059
|
"defaultBranch",
|
|
21060
21060
|
"description",
|
|
21061
21061
|
"language",
|
|
21062
|
-
"linked"
|
|
21062
|
+
"linked",
|
|
21063
|
+
"linkedProduct"
|
|
21063
21064
|
];
|
|
21064
21065
|
const repositoryId = idInput("repository link", "taiga_list_product_repositories gives it; a linkable repository id is not one");
|
|
21065
21066
|
/** Round-robin over lists: the first of each, then the second of each, and so on. */
|
|
@@ -21075,7 +21076,8 @@ function interleave(lists) {
|
|
|
21075
21076
|
*/
|
|
21076
21077
|
async function holderOf(client, error, productId) {
|
|
21077
21078
|
if (!(error instanceof TaigaApiError) || error.status !== 409) return null;
|
|
21078
|
-
|
|
21079
|
+
if (error.details?.["holder"]) return null;
|
|
21080
|
+
const name = /^(\S+\/\S+) is already linked to another product\./.exec(error.apiMessage ?? "")?.[1];
|
|
21079
21081
|
if (!name) return null;
|
|
21080
21082
|
try {
|
|
21081
21083
|
const holders = (await client.listProducts({
|
|
@@ -21137,7 +21139,7 @@ function registerRepositoryTools(server, client) {
|
|
|
21137
21139
|
}
|
|
21138
21140
|
if (client.hasCapability("vcs:read")) server.registerTool("taiga_list_linkable_repositories", {
|
|
21139
21141
|
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
|
|
21142
|
+
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
21143
|
inputSchema: z.object({
|
|
21142
21144
|
query: z.string().trim().min(1).max(256).optional().describe("Part of the repository name"),
|
|
21143
21145
|
account: z.string().trim().min(1).max(140).optional().describe("The GitHub account login (the owner in owner/repo), case-insensitive"),
|
package/package.json
CHANGED
|
@@ -8848,4 +8848,55 @@ describe('what the seventh agent review found (PRO-2800)', () => {
|
|
|
8848
8848
|
expect(bundle.runs[0].hardeningNote).toMatch(/^Hardening ended with 2 blocking findings/);
|
|
8849
8849
|
expect(bundle.runs[0]).not.toHaveProperty('hardeningResult');
|
|
8850
8850
|
});
|
|
8851
|
+
|
|
8852
|
+
it('says what the API says when it names the product holding a repository', async () => {
|
|
8853
|
+
const { client, fetchSpy } = await bootMcpPair({
|
|
8854
|
+
capabilities: ALL_CAPABILITIES,
|
|
8855
|
+
routes: {
|
|
8856
|
+
[`POST /v1/products/${PRODUCT_ID}/repositories`]: refusal(
|
|
8857
|
+
409,
|
|
8858
|
+
'CONFLICT',
|
|
8859
|
+
'taiga-sandbox/import-test-chi is already linked to Orders. A repository belongs to one product; unlink it there first.',
|
|
8860
|
+
{ holder: { productId: PRODUCT_ID_2, name: 'Orders' } }
|
|
8861
|
+
),
|
|
8862
|
+
},
|
|
8863
|
+
});
|
|
8864
|
+
|
|
8865
|
+
const result = parseToolResult(
|
|
8866
|
+
await client.callTool({
|
|
8867
|
+
name: 'taiga_link_repository',
|
|
8868
|
+
arguments: { productId: PRODUCT_ID, linkableRepositoryId: '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4f07' },
|
|
8869
|
+
})
|
|
8870
|
+
);
|
|
8871
|
+
|
|
8872
|
+
expect(result.message).toMatch(/already linked to Orders\./);
|
|
8873
|
+
expect(result.details.holder).toEqual({ productId: PRODUCT_ID_2, name: 'Orders' });
|
|
8874
|
+
// No second lookup when the API named it.
|
|
8875
|
+
expect(fetchSpy.mock.calls.some((call: unknown[]) => String(call[0]).includes('/v1/products?'))).toBe(false);
|
|
8876
|
+
});
|
|
8877
|
+
|
|
8878
|
+
it('passes on the product holding a linked repository when the API names it', async () => {
|
|
8879
|
+
const INSTALL = '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4f0b';
|
|
8880
|
+
const { client } = await bootMcpPair({
|
|
8881
|
+
capabilities: ALL_CAPABILITIES,
|
|
8882
|
+
routes: {
|
|
8883
|
+
'GET /v1/github/installations': { data: [{ id: INSTALL, accountLogin: 'acme' }], nextCursor: null },
|
|
8884
|
+
[`/v1/github/installations/${INSTALL}/repositories`]: {
|
|
8885
|
+
data: [
|
|
8886
|
+
{
|
|
8887
|
+
id: '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4f0c',
|
|
8888
|
+
fullName: 'acme/web',
|
|
8889
|
+
linked: true,
|
|
8890
|
+
linkedProduct: { id: PRODUCT_ID_2, name: 'Orders' },
|
|
8891
|
+
},
|
|
8892
|
+
],
|
|
8893
|
+
nextCursor: null,
|
|
8894
|
+
},
|
|
8895
|
+
},
|
|
8896
|
+
});
|
|
8897
|
+
|
|
8898
|
+
const result = parseToolResult(await client.callTool({ name: 'taiga_list_linkable_repositories', arguments: {} }));
|
|
8899
|
+
|
|
8900
|
+
expect(result.repositories.data[0].linkedProduct).toEqual({ id: PRODUCT_ID_2, name: 'Orders' });
|
|
8901
|
+
});
|
|
8851
8902
|
});
|
package/src/tools/maintenance.ts
CHANGED
|
@@ -209,8 +209,10 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
|
|
|
209
209
|
"List what maintenance sweeps found in a product's repository: dependencies, secrets, insecure code " +
|
|
210
210
|
'(sast) and infrastructure. The first page opens with sweep (open counts, latest sweeps), so an old or ' +
|
|
211
211
|
'failed sweep is not read as an all-clear. Rows: severity, tier (act_now: known exploited; urgent: ' +
|
|
212
|
-
'likely and serious; elevated: one of the two; routine), location, remediableBy (
|
|
213
|
-
'
|
|
212
|
+
'likely and serious; elevated: one of the two; routine), location, remediableBy (coding_agent: a run here ' +
|
|
213
|
+
'can fix it; upstream_only: the code is in a submodule, so the fix belongs in its own repository), fixable: false ' +
|
|
214
|
+
'when no fixed version is known yet, breached controls and initiativeId of a fix under way. Omit status ' +
|
|
215
|
+
'for open findings. taiga_get_finding reads one.',
|
|
214
216
|
inputSchema: z.object({
|
|
215
217
|
productId,
|
|
216
218
|
status: z
|
|
@@ -462,8 +464,8 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
|
|
|
462
464
|
"Raise an initiative that fixes a maintenance finding, at the end of the product's line; it ends in a " +
|
|
463
465
|
"pull request a person merges, and the initiative's review gate applies. wholeGroup: true fixes the " +
|
|
464
466
|
'group in one initiative: every open advisory on that package version in one bump, or every location the ' +
|
|
465
|
-
'same rule flagged. Refused for a secret only in history,
|
|
466
|
-
'
|
|
467
|
+
'same rule flagged. Refused for a secret only in history, remediableBy upstream_only (code in a ' +
|
|
468
|
+
'submodule), and a dependency with no fixed version yet (fixable: false).',
|
|
467
469
|
inputSchema: z.object({
|
|
468
470
|
productId,
|
|
469
471
|
findingId,
|
|
@@ -49,6 +49,8 @@ const GITHUB_REPOSITORY_FIELDS = [
|
|
|
49
49
|
'language',
|
|
50
50
|
// A product already links it (PRO-2800); absent from an API that predates the field.
|
|
51
51
|
'linked',
|
|
52
|
+
// Which product holds it, to a key that sees every product (PRO-2822).
|
|
53
|
+
'linkedProduct',
|
|
52
54
|
] as const;
|
|
53
55
|
|
|
54
56
|
const repositoryId = idInput(
|
|
@@ -72,7 +74,10 @@ export function interleave<T>(lists: T[][]): T[] {
|
|
|
72
74
|
*/
|
|
73
75
|
async function holderOf(client: TaigaClient, error: unknown, productId: string) {
|
|
74
76
|
if (!(error instanceof TaigaApiError) || error.status !== 409) return null;
|
|
75
|
-
|
|
77
|
+
// An API that names the holder (PRO-2807) says it itself; an older one, or a key that cannot see every product,
|
|
78
|
+
// says "another product", and this lists the ones the key can see.
|
|
79
|
+
if (error.details?.['holder']) return null;
|
|
80
|
+
const name = /^(\S+\/\S+) is already linked to another product\./.exec(error.apiMessage ?? '')?.[1];
|
|
76
81
|
if (!name) return null;
|
|
77
82
|
try {
|
|
78
83
|
// The refusal names the repository only, not its host, and the same name can live on github.com and on an
|
|
@@ -162,7 +167,7 @@ export function registerRepositoryTools(server: McpServer, client: TaigaClient)
|
|
|
162
167
|
"List the repositories the organization's GitHub connection can reach, to link one with " +
|
|
163
168
|
"taiga_link_repository (it takes the row's id as linkableRepositoryId). query searches by name; account (login, owner, or " +
|
|
164
169
|
'installationId) reads one account, else rows come from every account in turn. linked: true means a ' +
|
|
165
|
-
'product already links it
|
|
170
|
+
'product already links it, and linkedProduct names it to a key that sees every product. No connection means GitHub is not ' +
|
|
166
171
|
'connected yet; someone who can install the app does that once, in the web.',
|
|
167
172
|
inputSchema: z.object({
|
|
168
173
|
query: z.string().trim().min(1).max(256).optional().describe('Part of the repository name'),
|
|
Binary file
|
|
Binary file
|