@taiga-ai/mcp-server 0.24.0-rc.462 → 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 +107 -116
- package/package.json +1 -1
- package/src/client.ts +0 -8
- package/src/server.integration.test.ts +118 -32
- package/src/tools/catalogue.test.ts +2 -1
- package/src/tools/catalogue.ts +1 -1
- package/src/tools/context.ts +24 -4
- package/src/tools/documents.ts +27 -52
- package/src/tools/inbox.ts +4 -2
- package/src/tools/maintenance.ts +8 -4
- package/src/tools/product-documents.ts +1 -1
- package/src/tools/products.ts +22 -31
- package/src/tools/prompts.ts +1 -1
- package/src/tools/readiness.ts +2 -2
- package/src/tools/repositories.ts +7 -2
- package/src/tools/resources.ts +2 -2
- package/src/tools/sections.ts +1 -1
- package/taiga-ai-mcp-server-0.25.0-dev.465.tgz +0 -0
- package/taiga-ai-mcp-server-0.24.0-rc.462.tgz +0 -0
|
@@ -66,15 +66,12 @@ const ALL_TOOL_NAMES = [
|
|
|
66
66
|
'taiga_ignore_finding',
|
|
67
67
|
'taiga_link_repository',
|
|
68
68
|
'taiga_list_action_required',
|
|
69
|
-
'taiga_list_documents',
|
|
70
69
|
'taiga_list_environments',
|
|
71
70
|
'taiga_list_factories',
|
|
72
|
-
'taiga_list_factory_documents',
|
|
73
71
|
'taiga_list_findings',
|
|
74
72
|
'taiga_list_initiatives',
|
|
75
73
|
'taiga_list_knowledge',
|
|
76
74
|
'taiga_list_linkable_repositories',
|
|
77
|
-
'taiga_list_product_documents',
|
|
78
75
|
'taiga_list_product_repositories',
|
|
79
76
|
'taiga_list_products',
|
|
80
77
|
'taiga_list_repository_branches',
|
|
@@ -326,9 +323,7 @@ describe('MCP server integration', () => {
|
|
|
326
323
|
const names = toolNames(tools);
|
|
327
324
|
|
|
328
325
|
expect(names).toContain('taiga_list_factories');
|
|
329
|
-
expect(names).toContain('taiga_list_factory_documents');
|
|
330
326
|
expect(names).toContain('taiga_get_factory_document');
|
|
331
|
-
expect(names).not.toContain('taiga_list_documents');
|
|
332
327
|
expect(names).not.toContain('taiga_update_document');
|
|
333
328
|
});
|
|
334
329
|
|
|
@@ -338,7 +333,6 @@ describe('MCP server integration', () => {
|
|
|
338
333
|
const { tools } = await client.listTools();
|
|
339
334
|
const names = toolNames(tools);
|
|
340
335
|
|
|
341
|
-
expect(names).toContain('taiga_list_documents');
|
|
342
336
|
expect(names).toContain('taiga_get_document');
|
|
343
337
|
expect(names).not.toContain('taiga_update_document');
|
|
344
338
|
});
|
|
@@ -399,9 +393,7 @@ describe('MCP server integration', () => {
|
|
|
399
393
|
|
|
400
394
|
// teams:read tools present
|
|
401
395
|
expect(names).toContain('taiga_list_factories');
|
|
402
|
-
expect(names).toContain('taiga_list_factory_documents');
|
|
403
396
|
// tenant:documents:read tools present
|
|
404
|
-
expect(names).toContain('taiga_list_documents');
|
|
405
397
|
// write tools absent
|
|
406
398
|
expect(names).not.toContain('taiga_update_document');
|
|
407
399
|
});
|
|
@@ -438,7 +430,6 @@ describe('MCP server integration', () => {
|
|
|
438
430
|
'taiga_list_products',
|
|
439
431
|
'taiga_get_product',
|
|
440
432
|
'taiga_get_product_context',
|
|
441
|
-
'taiga_list_product_documents',
|
|
442
433
|
'taiga_get_product_document',
|
|
443
434
|
'taiga_list_runs',
|
|
444
435
|
'taiga_get_run',
|
|
@@ -467,8 +458,8 @@ describe('MCP server integration', () => {
|
|
|
467
458
|
],
|
|
468
459
|
'vcs:manage': ['taiga_link_repository', 'taiga_set_repository_branch', 'taiga_unlink_repository'],
|
|
469
460
|
'backlog:read': ['taiga_list_initiatives', 'taiga_get_initiative', 'taiga_get_plan', 'taiga_wait_for_initiative'],
|
|
470
|
-
'factories:read': ['taiga_list_factories', '
|
|
471
|
-
'tenant:documents:read': ['
|
|
461
|
+
'factories:read': ['taiga_list_factories', 'taiga_get_factory_document'],
|
|
462
|
+
'tenant:documents:read': ['taiga_get_document'],
|
|
472
463
|
'customDocuments:read': ['taiga_list_knowledge', 'taiga_get_knowledge'],
|
|
473
464
|
};
|
|
474
465
|
const PAIRED_CONTRACT: Record<string, string[]> = {
|
|
@@ -562,7 +553,7 @@ describe('MCP server integration', () => {
|
|
|
562
553
|
routes: { 'GET /v1/tenants/current/documents': { data: [doc], nextCursor: null } },
|
|
563
554
|
});
|
|
564
555
|
|
|
565
|
-
const result = await client.callTool({ name: '
|
|
556
|
+
const result = await client.callTool({ name: 'taiga_get_document', arguments: {} });
|
|
566
557
|
|
|
567
558
|
expect(parseToolResult(result).data).toEqual([
|
|
568
559
|
{
|
|
@@ -1292,7 +1283,7 @@ describe('MCP server integration', () => {
|
|
|
1292
1283
|
});
|
|
1293
1284
|
|
|
1294
1285
|
const result = await client.callTool({
|
|
1295
|
-
name: '
|
|
1286
|
+
name: 'taiga_get_product_document',
|
|
1296
1287
|
arguments: { productId: PRODUCT_ID },
|
|
1297
1288
|
});
|
|
1298
1289
|
|
|
@@ -1426,7 +1417,7 @@ describe('MCP server integration', () => {
|
|
|
1426
1417
|
arguments: { productId: PRODUCT_ID, type: 'architecture' },
|
|
1427
1418
|
});
|
|
1428
1419
|
const list = await client.callTool({
|
|
1429
|
-
name: '
|
|
1420
|
+
name: 'taiga_get_product_document',
|
|
1430
1421
|
arguments: { productId: PRODUCT_ID },
|
|
1431
1422
|
});
|
|
1432
1423
|
|
|
@@ -1565,7 +1556,7 @@ describe('MCP server integration', () => {
|
|
|
1565
1556
|
});
|
|
1566
1557
|
|
|
1567
1558
|
const result = await client.callTool({
|
|
1568
|
-
name: '
|
|
1559
|
+
name: 'taiga_get_product_document',
|
|
1569
1560
|
arguments: { productId: PRODUCT_ID },
|
|
1570
1561
|
});
|
|
1571
1562
|
|
|
@@ -1604,7 +1595,7 @@ describe('MCP server integration', () => {
|
|
|
1604
1595
|
});
|
|
1605
1596
|
|
|
1606
1597
|
const parsed = parseToolResult(
|
|
1607
|
-
await client.callTool({ name: '
|
|
1598
|
+
await client.callTool({ name: 'taiga_get_product_document', arguments: { productId: PRODUCT_ID } })
|
|
1608
1599
|
);
|
|
1609
1600
|
|
|
1610
1601
|
expect(parsed.plan[0].outdated).toBe(false);
|
|
@@ -1623,7 +1614,7 @@ describe('MCP server integration', () => {
|
|
|
1623
1614
|
});
|
|
1624
1615
|
|
|
1625
1616
|
const parsed = parseToolResult(
|
|
1626
|
-
await client.callTool({ name: '
|
|
1617
|
+
await client.callTool({ name: 'taiga_get_product_document', arguments: { productId: PRODUCT_ID } })
|
|
1627
1618
|
);
|
|
1628
1619
|
|
|
1629
1620
|
expect(parsed.plan[0]).not.toHaveProperty('outdated');
|
|
@@ -1642,7 +1633,7 @@ describe('MCP server integration', () => {
|
|
|
1642
1633
|
});
|
|
1643
1634
|
|
|
1644
1635
|
const list = parseToolResult(
|
|
1645
|
-
await client.callTool({ name: '
|
|
1636
|
+
await client.callTool({ name: 'taiga_get_product_document', arguments: { productId: PRODUCT_ID } })
|
|
1646
1637
|
);
|
|
1647
1638
|
const miss = parseToolResult(
|
|
1648
1639
|
await client.callTool({
|
|
@@ -1941,7 +1932,7 @@ describe('MCP server integration', () => {
|
|
|
1941
1932
|
nextCursor: null,
|
|
1942
1933
|
},
|
|
1943
1934
|
[`GET /v1/products/${PRODUCT_ID}/repositories`]: { data: [repo], nextCursor: null },
|
|
1944
|
-
[`GET /v1/products/${PRODUCT_ID}/builds?limit=
|
|
1935
|
+
[`GET /v1/products/${PRODUCT_ID}/builds?limit=5`]: { data: [run], nextCursor: null },
|
|
1945
1936
|
[`GET /v1/products/${PRODUCT_ID}/inbox`]: {
|
|
1946
1937
|
data: [
|
|
1947
1938
|
{ id: 'i1', kind: 'review_pr', status: 'open' },
|
|
@@ -2003,7 +1994,7 @@ describe('MCP server integration', () => {
|
|
|
2003
1994
|
});
|
|
2004
1995
|
expect(bundle.documents.architecture).toBeNull();
|
|
2005
1996
|
expect(bundle.documents.notes).toEqual([
|
|
2006
|
-
'No published plan architecture;
|
|
1997
|
+
'No published plan architecture; taiga_get_product_document without type shows what the product has.',
|
|
2007
1998
|
]);
|
|
2008
1999
|
expect(bundle.policies).toEqual([{ type: 'policy_security', name: 'Security Policy', version: 3, sections: 2 }]);
|
|
2009
2000
|
expect(bundle.initiatives.items.map((row: { id: string }) => row.id)).toEqual([ITEM_ID]);
|
|
@@ -2026,9 +2017,9 @@ describe('MCP server integration', () => {
|
|
|
2026
2017
|
});
|
|
2027
2018
|
expect(bundle.runs).toEqual([
|
|
2028
2019
|
{
|
|
2020
|
+
// The disposition says how it ended; status, the raw word, is taiga_get_run's (PRO-2800).
|
|
2029
2021
|
id: BUILD_ID,
|
|
2030
2022
|
runNumber: 1,
|
|
2031
|
-
status: 'completed',
|
|
2032
2023
|
targetTitle: 'Add login',
|
|
2033
2024
|
disposition: 'unmerged',
|
|
2034
2025
|
outcome: 'Settled without a merged pull request.',
|
|
@@ -2364,13 +2355,7 @@ describe('MCP server integration', () => {
|
|
|
2364
2355
|
it.each([
|
|
2365
2356
|
['taiga_list_factories', {}, '/v1/factories', ['factories:read']],
|
|
2366
2357
|
['taiga_list_products', {}, '/v1/products', ['products:read']],
|
|
2367
|
-
['
|
|
2368
|
-
[
|
|
2369
|
-
'taiga_list_factory_documents',
|
|
2370
|
-
{ factoryId: FACTORY_ID },
|
|
2371
|
-
`/v1/factories/${FACTORY_ID}/documents`,
|
|
2372
|
-
['factories:read'],
|
|
2373
|
-
],
|
|
2358
|
+
['taiga_get_document', {}, '/v1/tenants/current/documents', ['tenant:documents:read']],
|
|
2374
2359
|
[
|
|
2375
2360
|
'taiga_list_initiatives',
|
|
2376
2361
|
{ productId: PRODUCT_ID, status: 'all' },
|
|
@@ -2567,7 +2552,7 @@ describe('MCP server integration', () => {
|
|
|
2567
2552
|
expect(String((own as Error).message)).toContain('403 FORBIDDEN: Not a member of this product');
|
|
2568
2553
|
expect(named).toMatchObject({ code: -32602 });
|
|
2569
2554
|
expect(String((named as Error).message)).toContain(
|
|
2570
|
-
'
|
|
2555
|
+
'taiga_get_product_document without type shows what exists (403 FORBIDDEN'
|
|
2571
2556
|
);
|
|
2572
2557
|
});
|
|
2573
2558
|
|
|
@@ -2640,7 +2625,7 @@ describe('MCP server integration', () => {
|
|
|
2640
2625
|
[`GET /v1/products/${PRODUCT_ID}/backlog?status=backlog&limit=100`]: { data: [], nextCursor: null },
|
|
2641
2626
|
[`GET /v1/products/${PRODUCT_ID}/backlog?status=in_progress&limit=100`]: { data: [item], nextCursor: null },
|
|
2642
2627
|
[`GET /v1/products/${PRODUCT_ID}/repositories`]: { data: [], nextCursor: null },
|
|
2643
|
-
[`GET /v1/products/${PRODUCT_ID}/builds?limit=
|
|
2628
|
+
[`GET /v1/products/${PRODUCT_ID}/builds?limit=5`]: { data: [], nextCursor: null },
|
|
2644
2629
|
'GET /v1/products?slug=web&limit=1': { data: [product], nextCursor: null },
|
|
2645
2630
|
};
|
|
2646
2631
|
|
|
@@ -2731,7 +2716,9 @@ describe('MCP server integration', () => {
|
|
|
2731
2716
|
.catch((e: unknown) => e);
|
|
2732
2717
|
|
|
2733
2718
|
expect(failure).toMatchObject({ code: -32602 });
|
|
2734
|
-
expect(String((failure as Error).message)).toContain(
|
|
2719
|
+
expect(String((failure as Error).message)).toContain(
|
|
2720
|
+
'taiga_list_products (query finds one by name) shows the ones it can; a slug or git remote URL names one too.'
|
|
2721
|
+
);
|
|
2735
2722
|
});
|
|
2736
2723
|
|
|
2737
2724
|
it('should refuse a session with no product named', async () => {
|
|
@@ -8427,7 +8414,7 @@ describe('what the sixth agent review found (PRO-2800)', () => {
|
|
|
8427
8414
|
expect(plan.summary.length).toBeLessThan(240);
|
|
8428
8415
|
expect(doc).not.toHaveProperty('status');
|
|
8429
8416
|
expect(doc).not.toHaveProperty('blocking');
|
|
8430
|
-
expect(doc.readWith).toBe('
|
|
8417
|
+
expect(doc.readWith).toBe('taiga_get_product_document (productId, no type)');
|
|
8431
8418
|
expect(plan.blocking).toBe(true);
|
|
8432
8419
|
expect(plan.readWith).toBe('taiga_get_plan (initiativeId: subjectId)');
|
|
8433
8420
|
expect(task.status).toBe('in_progress');
|
|
@@ -8606,6 +8593,9 @@ describe('what the seventh agent review found (PRO-2800)', () => {
|
|
|
8606
8593
|
const refused = await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: 'my item' } });
|
|
8607
8594
|
|
|
8608
8595
|
expect(listing).not.toMatch(/"pattern":"\^\(\[0-9a-fA-F\]\{8\}/);
|
|
8596
|
+
// Nor the date-time one: a pattern left is a short one that says something (a slug, a file extension).
|
|
8597
|
+
const patterns = [...listing.matchAll(/"pattern":"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1]!);
|
|
8598
|
+
expect(patterns.filter((pattern) => pattern.length > 60)).toEqual([]);
|
|
8609
8599
|
expect(listing).toMatch(/"format":"uuid"/);
|
|
8610
8600
|
expect(JSON.stringify(refused.content)).toMatch(/An initiative ID is a UUID/);
|
|
8611
8601
|
});
|
|
@@ -8813,4 +8803,100 @@ describe('what the seventh agent review found (PRO-2800)', () => {
|
|
|
8813
8803
|
expect(critical).not.toHaveProperty('totalNote');
|
|
8814
8804
|
expect(secrets).not.toHaveProperty('totalNote');
|
|
8815
8805
|
});
|
|
8806
|
+
|
|
8807
|
+
it('reads a document by type and lists them without one, refusing sections with nothing to read', async () => {
|
|
8808
|
+
const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
|
|
8809
|
+
|
|
8810
|
+
const company = parseToolResult(await client.callTool({ name: 'taiga_get_document', arguments: { sections: [] } }));
|
|
8811
|
+
const product = parseToolResult(
|
|
8812
|
+
await client.callTool({
|
|
8813
|
+
name: 'taiga_get_product_document',
|
|
8814
|
+
arguments: { productId: PRODUCT_ID, origin: 'plan' },
|
|
8815
|
+
})
|
|
8816
|
+
);
|
|
8817
|
+
|
|
8818
|
+
expect(company).toMatchObject({ error: 'VALIDATION_ERROR' });
|
|
8819
|
+
expect(product).toMatchObject({ error: 'VALIDATION_ERROR' });
|
|
8820
|
+
});
|
|
8821
|
+
|
|
8822
|
+
it("keeps a run's hardening note in the compact context row", async () => {
|
|
8823
|
+
const { client } = await bootMcpPair({
|
|
8824
|
+
capabilities: ['products:read'],
|
|
8825
|
+
routes: {
|
|
8826
|
+
[`GET /v1/products/${PRODUCT_ID}`]: {
|
|
8827
|
+
data: { id: PRODUCT_ID, name: 'Web', status: 'active', factoryId: FACTORY_ID, settings: {} },
|
|
8828
|
+
},
|
|
8829
|
+
[`/v1/products/${PRODUCT_ID}/builds`]: {
|
|
8830
|
+
data: [
|
|
8831
|
+
{
|
|
8832
|
+
id: BUILD_ID,
|
|
8833
|
+
status: 'completed',
|
|
8834
|
+
prMerged: true,
|
|
8835
|
+
prNumber: 3,
|
|
8836
|
+
hardeningResult: { passed: false, blockingFindings: 2 },
|
|
8837
|
+
},
|
|
8838
|
+
],
|
|
8839
|
+
nextCursor: null,
|
|
8840
|
+
},
|
|
8841
|
+
},
|
|
8842
|
+
});
|
|
8843
|
+
|
|
8844
|
+
const bundle = parseToolResult(
|
|
8845
|
+
await client.callTool({ name: 'taiga_get_product_context', arguments: { productId: PRODUCT_ID } })
|
|
8846
|
+
);
|
|
8847
|
+
|
|
8848
|
+
expect(bundle.runs[0].hardeningNote).toMatch(/^Hardening ended with 2 blocking findings/);
|
|
8849
|
+
expect(bundle.runs[0]).not.toHaveProperty('hardeningResult');
|
|
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
|
+
});
|
|
8816
8902
|
});
|
|
@@ -102,8 +102,9 @@ describe('tool descriptions against the catalogue', () => {
|
|
|
102
102
|
|
|
103
103
|
it('names every policy where the company documents are explained', async () => {
|
|
104
104
|
const tools = await allTools();
|
|
105
|
-
|
|
105
|
+
// One tool lists and reads the company documents.
|
|
106
106
|
const get = tools.find((t) => t.name === 'taiga_get_document')!;
|
|
107
|
+
const list = get;
|
|
107
108
|
const typeEnum = (get.inputSchema.properties as Record<string, { enum?: string[] }>)['type']!.enum;
|
|
108
109
|
|
|
109
110
|
for (const type of POLICY_TYPES) {
|
package/src/tools/catalogue.ts
CHANGED
|
@@ -182,7 +182,7 @@ export function setupNextStep(
|
|
|
182
182
|
);
|
|
183
183
|
case 'complete':
|
|
184
184
|
return (
|
|
185
|
-
'The import is done: review the reality documents (
|
|
185
|
+
'The import is done: review the reality documents (taiga_get_product_document without type), then ' +
|
|
186
186
|
'taiga_finish_discovery opens the product.'
|
|
187
187
|
);
|
|
188
188
|
case 'failed':
|
package/src/tools/context.ts
CHANGED
|
@@ -56,7 +56,7 @@ const REPOSITORY_SUMMARY_FIELDS = [
|
|
|
56
56
|
] as const;
|
|
57
57
|
|
|
58
58
|
/** How many of the latest runs to carry; taiga_list_runs pages the rest. */
|
|
59
|
-
const BUILDS_IN_CONTEXT =
|
|
59
|
+
const BUILDS_IN_CONTEXT = 5;
|
|
60
60
|
|
|
61
61
|
/** A run as the bundle carries it: enough to say what was built and where; taiga_get_run has the rest. */
|
|
62
62
|
const CONTEXT_BUILD_FIELDS = [
|
|
@@ -76,6 +76,8 @@ const CONTEXT_BUILD_FIELDS = [
|
|
|
76
76
|
'prDraft',
|
|
77
77
|
'reviewEscalation',
|
|
78
78
|
'error',
|
|
79
|
+
// Read for hardeningNote; the compact row drops it.
|
|
80
|
+
'hardeningResult',
|
|
79
81
|
'createdAt',
|
|
80
82
|
] as const;
|
|
81
83
|
|
|
@@ -240,7 +242,7 @@ async function documentsPart(client: TaigaClient, product: WithoutTenant<Product
|
|
|
240
242
|
}
|
|
241
243
|
} else if (isNotFound(read.reason)) {
|
|
242
244
|
out[type] = null;
|
|
243
|
-
notes.push(`No published ${origin} ${type};
|
|
245
|
+
notes.push(`No published ${origin} ${type}; taiga_get_product_document without type shows what the product has.`);
|
|
244
246
|
} else throw read.reason;
|
|
245
247
|
});
|
|
246
248
|
return { ...out, notes };
|
|
@@ -337,11 +339,29 @@ async function repositoriesPart(links: () => ReturnType<TaigaClient['listProduct
|
|
|
337
339
|
async function buildsPart(client: TaigaClient, productId: string) {
|
|
338
340
|
const rows = (await client.listBuildRuns(productId, { limit: BUILDS_IN_CONTEXT })).data as Record<string, unknown>[];
|
|
339
341
|
const states = await initiativeStates(client, rows);
|
|
342
|
+
// The fields the outcome is made from are read, then the row is cut to what the bundle says: each run with its
|
|
343
|
+
// two-sentence outcome and every PR field was half the context, repeated by taiga_list_runs (PRO-2800).
|
|
340
344
|
return pickFields(rows, CONTEXT_BUILD_FIELDS)
|
|
341
345
|
.map(readableRow)
|
|
342
|
-
.map((row) => withInitiativeState(row, states))
|
|
346
|
+
.map((row) => withInitiativeState(row, states))
|
|
347
|
+
.map((row) => pickFields([row], CONTEXT_RUN_SAID)[0]!);
|
|
343
348
|
}
|
|
344
349
|
|
|
350
|
+
/** A run in the bundle: which one, for what, how it ended, and its pull request. taiga_get_run has the rest. */
|
|
351
|
+
const CONTEXT_RUN_SAID = [
|
|
352
|
+
'id',
|
|
353
|
+
'runNumber',
|
|
354
|
+
'backlogItemId',
|
|
355
|
+
'targetTitle',
|
|
356
|
+
'disposition',
|
|
357
|
+
'outcome',
|
|
358
|
+
// Rare and short, and what the outcome sentence does not say.
|
|
359
|
+
'failedReasonMeans',
|
|
360
|
+
'hardeningNote',
|
|
361
|
+
'prUrl',
|
|
362
|
+
'createdAt',
|
|
363
|
+
] as const;
|
|
364
|
+
|
|
345
365
|
/**
|
|
346
366
|
* What waits on a person for this product, counted as taiga_list_action_required's summary counts it (PRO-2800):
|
|
347
367
|
* the reviewing agent called four tools to answer "how does it stand", and two later reviews found this count and
|
|
@@ -466,7 +486,7 @@ export function registerContextTools(server: McpServer, client: TaigaClient) {
|
|
|
466
486
|
'or git remote URL. Returns product (in setup, readiness and next move), factory with its instructions, ' +
|
|
467
487
|
'specification and architecture outlines (section ids, sizes), policies with section counts, open ' +
|
|
468
488
|
"initiatives with a count per board column (byColumn; taiga_list_initiatives status: 'all' has the done " +
|
|
469
|
-
'and canceled), repositories, the latest
|
|
489
|
+
'and canceled), repositories, the latest five runs (taiga_list_runs has more), and counts for Action Required, open findings and ' +
|
|
470
490
|
'policy violations. A part that failed is under unavailable, one the key cannot read under notGranted; ' +
|
|
471
491
|
`neither shows as empty. documents: budget returns whole sections up to about ${DOCUMENT_BUDGET / 1000}k ` +
|
|
472
492
|
'characters each, full returns all.',
|
package/src/tools/documents.ts
CHANGED
|
@@ -75,7 +75,8 @@ export const sectionContentInput = z
|
|
|
75
75
|
.describe('The full new Markdown text of the section; it replaces the old text');
|
|
76
76
|
const tenantDocumentType = z
|
|
77
77
|
.enum(TENANT_DOCUMENT_TYPES)
|
|
78
|
-
|
|
78
|
+
// The enum lists every type; the description only says which are policies.
|
|
79
|
+
.describe('Document type: the policy_* ones are policies');
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
82
|
* The read, save and publish calls for one document scope. The tools are the
|
|
@@ -150,52 +151,48 @@ export function registerDocumentTools(server: McpServer, client: TaigaClient) {
|
|
|
150
151
|
// ── Company-level policy documents ──────────────────────────────────
|
|
151
152
|
|
|
152
153
|
if (client.hasCapability('tenant:documents:read')) {
|
|
153
|
-
server.registerTool(
|
|
154
|
-
'taiga_list_documents',
|
|
155
|
-
{
|
|
156
|
-
title: 'List company documents',
|
|
157
|
-
description:
|
|
158
|
-
"List the organization's documents as summaries (type, name, description, version, draft state). The " +
|
|
159
|
-
`${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) set the security, data privacy, SDLC, ` +
|
|
160
|
-
'operations, architecture and compliance rules for every product; the others ' +
|
|
161
|
-
`(${named(OTHER_TENANT_DOCUMENT_TYPES)}) carry goals, standards, voice and standing instructions. ` +
|
|
162
|
-
'taiga_get_document reads one.',
|
|
163
|
-
inputSchema: z.object({
|
|
164
|
-
...pagination,
|
|
165
|
-
}),
|
|
166
|
-
annotations: READ_ONLY,
|
|
167
|
-
},
|
|
168
|
-
(params) =>
|
|
169
|
-
runTool(async () => {
|
|
170
|
-
const result = await client.listTenantDocuments(params);
|
|
171
|
-
return toolResult(page(summaries(result.data), result.nextCursor));
|
|
172
|
-
})
|
|
173
|
-
);
|
|
174
|
-
|
|
175
154
|
server.registerTool(
|
|
176
155
|
'taiga_get_document',
|
|
177
156
|
{
|
|
178
157
|
title: 'Get company document',
|
|
179
158
|
description:
|
|
180
|
-
"Read one of the organization's documents
|
|
159
|
+
"Read one of the organization's documents by type, or without type list them all (type, name, version, " +
|
|
160
|
+
`draft state). The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) set the rules for every ` +
|
|
161
|
+
`product; the others (${named(OTHER_TENANT_DOCUMENT_TYPES)}) carry goals, standards, voice and ` +
|
|
162
|
+
'instructions. Policies are long: pass ' +
|
|
181
163
|
'sections: [] for the outline, then the section ids you need, or a control id such as SEC-5.1; ' +
|
|
182
164
|
'unknownSections names ids it lacks. A miss says which documents are published and whether a draft ' +
|
|
183
165
|
"exists. A section's source (template, overlay-<id>, cloud-preset:<providers>) and priority (core: every " +
|
|
184
166
|
'organization keeps it) do not change what it requires.',
|
|
185
167
|
inputSchema: z.object({
|
|
186
|
-
type: tenantDocumentType,
|
|
168
|
+
type: tenantDocumentType.optional(),
|
|
187
169
|
sections: policySectionsInput,
|
|
170
|
+
...pagination,
|
|
188
171
|
}),
|
|
189
172
|
annotations: READ_ONLY,
|
|
190
173
|
},
|
|
191
174
|
(params) =>
|
|
192
175
|
runTool(async () => {
|
|
176
|
+
// One tool for the list and the read (PRO-2800): the catalogue's weight is the cost every review ranks
|
|
177
|
+
// first, and a list tool beside each read was the read without a type.
|
|
178
|
+
if (params.type === undefined) {
|
|
179
|
+
if (params.sections) return toolError('VALIDATION_ERROR', 'Name the document (type) to read its sections.');
|
|
180
|
+
const result = await client.listTenantDocuments({ limit: params.limit, cursor: params.cursor });
|
|
181
|
+
return toolResult(page(summaries(result.data), result.nextCursor));
|
|
182
|
+
}
|
|
183
|
+
const type = params.type;
|
|
184
|
+
if (params.limit !== undefined || params.cursor !== undefined) {
|
|
185
|
+
return toolError(
|
|
186
|
+
'VALIDATION_ERROR',
|
|
187
|
+
'limit and cursor page the list; omit type to list, or drop them to read.'
|
|
188
|
+
);
|
|
189
|
+
}
|
|
193
190
|
try {
|
|
194
|
-
const result = await client.getTenantDocument(
|
|
191
|
+
const result = await client.getTenantDocument(type);
|
|
195
192
|
const doc = stripProvenance(result.data);
|
|
196
193
|
return params.sections
|
|
197
|
-
? sectionReadResult(doc, params.sections,
|
|
198
|
-
controlIds: (POLICY_TYPES as readonly string[]).includes(
|
|
194
|
+
? sectionReadResult(doc, params.sections, type, {
|
|
195
|
+
controlIds: (POLICY_TYPES as readonly string[]).includes(type),
|
|
199
196
|
})
|
|
200
197
|
: toolResult(doc);
|
|
201
198
|
} catch (error) {
|
|
@@ -203,10 +200,10 @@ export function registerDocumentTools(server: McpServer, client: TaigaClient) {
|
|
|
203
200
|
// Best effort, each part on its own: a failed lookup must not turn
|
|
204
201
|
// a miss into an outage, nor blank the part that did answer.
|
|
205
202
|
const [draft, all] = await Promise.allSettled([
|
|
206
|
-
client.getTenantDocumentDraft(
|
|
203
|
+
client.getTenantDocumentDraft(type),
|
|
207
204
|
client.listTenantDocuments({ limit: 100 }),
|
|
208
205
|
]);
|
|
209
|
-
const parts = [`No published ${
|
|
206
|
+
const parts = [`No published ${type}.`];
|
|
210
207
|
if (draft.status === 'fulfilled') {
|
|
211
208
|
parts.push(
|
|
212
209
|
draft.value.data
|
|
@@ -277,28 +274,6 @@ export function registerDocumentTools(server: McpServer, client: TaigaClient) {
|
|
|
277
274
|
// ── Factory instructions ────────────────────────────────────────────
|
|
278
275
|
|
|
279
276
|
if (client.hasCapability('factories:read')) {
|
|
280
|
-
server.registerTool(
|
|
281
|
-
'taiga_list_factory_documents',
|
|
282
|
-
{
|
|
283
|
-
title: 'List factory documents',
|
|
284
|
-
description:
|
|
285
|
-
"List a factory's documents, published and draft (isDraft), as summaries (type, name, version). The only " +
|
|
286
|
-
`type is ${named(FACTORY_DOCUMENT_TYPES)}: standing guidance (domain context, tech preferences, ` +
|
|
287
|
-
'conventions) for every product in the factory. An empty list means no publication or draft exists. ' +
|
|
288
|
-
'taiga_get_factory_document reads it.',
|
|
289
|
-
inputSchema: z.object({
|
|
290
|
-
factoryId: idInput('factory', 'taiga_list_factories (query finds one by name) gives it'),
|
|
291
|
-
...pagination,
|
|
292
|
-
}),
|
|
293
|
-
annotations: READ_ONLY,
|
|
294
|
-
},
|
|
295
|
-
(params) =>
|
|
296
|
-
runTool(async () => {
|
|
297
|
-
const result = await client.listFactoryDocuments(params.factoryId, params);
|
|
298
|
-
return toolResult(page(summaries(result.data), result.nextCursor));
|
|
299
|
-
})
|
|
300
|
-
);
|
|
301
|
-
|
|
302
277
|
server.registerTool(
|
|
303
278
|
'taiga_get_factory_document',
|
|
304
279
|
{
|
package/src/tools/inbox.ts
CHANGED
|
@@ -63,7 +63,7 @@ function readWith(row: Row, backlogRead: boolean): string | undefined {
|
|
|
63
63
|
const type = /^[^:]+:([a-z_]+):decision:/.exec(String(row['subjectId']))?.[1];
|
|
64
64
|
return type
|
|
65
65
|
? `taiga_get_product_document (productId, type: ${type}); the decision's text is this row with full: true`
|
|
66
|
-
: '
|
|
66
|
+
: 'taiga_get_product_document (productId, no type)';
|
|
67
67
|
}
|
|
68
68
|
case 'inbox_task':
|
|
69
69
|
return 'taiga_list_setup_tasks (productId)';
|
|
@@ -73,7 +73,7 @@ function readWith(row: Row, backlogRead: boolean): string | undefined {
|
|
|
73
73
|
review_backlog: backlogRead ? 'taiga_list_initiatives (productId)' : undefined,
|
|
74
74
|
review_maintenance: 'taiga_list_findings (productId)',
|
|
75
75
|
review_policy: 'taiga_get_policy_standing (productId)',
|
|
76
|
-
review_doc: '
|
|
76
|
+
review_doc: 'taiga_get_product_document (productId, no type)',
|
|
77
77
|
}[String(row['kind'])];
|
|
78
78
|
default:
|
|
79
79
|
return undefined;
|
|
@@ -517,6 +517,8 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
|
517
517
|
snoozedUntil: z
|
|
518
518
|
.string()
|
|
519
519
|
.datetime()
|
|
520
|
+
// format: date-time says it; the leap-year pattern beside it ran to hundreds of characters (PRO-2800).
|
|
521
|
+
.meta({ pattern: undefined })
|
|
520
522
|
.nullable()
|
|
521
523
|
.optional()
|
|
522
524
|
.describe('Hide it until this time (ISO 8601 in UTC, ending in Z); null wakes it now'),
|
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
|
|
@@ -429,6 +431,8 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
|
|
|
429
431
|
expiresAt: z
|
|
430
432
|
.string()
|
|
431
433
|
.datetime({ offset: true })
|
|
434
|
+
// format: date-time says it; the leap-year pattern beside it ran to hundreds of characters (PRO-2800).
|
|
435
|
+
.meta({ pattern: undefined })
|
|
432
436
|
.optional()
|
|
433
437
|
.describe('When the ignore lapses and the finding resurfaces if it still exists (ISO 8601, in the future)'),
|
|
434
438
|
}),
|
|
@@ -460,8 +464,8 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
|
|
|
460
464
|
"Raise an initiative that fixes a maintenance finding, at the end of the product's line; it ends in a " +
|
|
461
465
|
"pull request a person merges, and the initiative's review gate applies. wholeGroup: true fixes the " +
|
|
462
466
|
'group in one initiative: every open advisory on that package version in one bump, or every location the ' +
|
|
463
|
-
'same rule flagged. Refused for a secret only in history,
|
|
464
|
-
'
|
|
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).',
|
|
465
469
|
inputSchema: z.object({
|
|
466
470
|
productId,
|
|
467
471
|
findingId,
|
|
@@ -34,7 +34,7 @@ const EDITABLE_TYPES = PRODUCT_DOCUMENT_TYPES.filter((type): type is EditableTyp
|
|
|
34
34
|
|
|
35
35
|
const documentType = z
|
|
36
36
|
.enum(EDITABLE_TYPES)
|
|
37
|
-
.describe('Document type --
|
|
37
|
+
.describe('Document type -- taiga_get_product_document without type shows which ones the product has');
|
|
38
38
|
|
|
39
39
|
/** The documents a person reviews: the discovery chain, not the snapshot or the product's own instructions. */
|
|
40
40
|
const reviewableType = z
|