@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), breached controls and initiativeId of a fix under way. Omit status for open findings. taiga_get_finding reads one.",
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, anything in a submodule, and remediableBy upstream_only.",
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
- const name = /^(\S+\/\S+) is already linked to another product\.?$/.exec(error.apiMessage ?? "")?.[1];
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 (taiga_get_product with its URL says which). No connection means GitHub is not connected yet; someone who can install the app does that once, in the web.",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.25.0-dev.463",
3
+ "version": "0.25.0-dev.465",
4
4
  "description": "MCP server for Taiga -- gives AI assistants access to documents and products",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
  });
@@ -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 (upstream_only: no fixed ' +
213
- 'version), breached controls and initiativeId of a fix under way. Omit status for open findings. taiga_get_finding reads one.',
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, anything in a submodule, and remediableBy ' +
466
- 'upstream_only.',
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
- const name = /^(\S+\/\S+) is already linked to another product\.?$/.exec(error.apiMessage ?? '')?.[1];
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 (taiga_get_product with its URL says which). No connection means GitHub is not ' +
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'),