@taiga-ai/mcp-server 0.4.0 → 0.5.0-rc.389
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 +393 -153
- package/package.json +2 -2
- package/src/client.test.ts +78 -16
- package/src/client.ts +124 -21
- package/src/config.test.ts +24 -0
- package/src/config.ts +25 -0
- package/src/index.ts +7 -9
- package/src/server.integration.test.ts +407 -34
- package/src/tools/documents.ts +245 -97
- package/src/tools/errors.ts +48 -0
- package/src/tools/inputs.test.ts +19 -0
- package/src/tools/inputs.ts +13 -0
- package/src/tools/products.ts +87 -73
- package/src/tools/sections.test.ts +58 -0
- package/src/tools/sections.ts +38 -0
- package/taiga-ai-mcp-server-0.5.0-rc.389.tgz +0 -0
- package/tsconfig.json +8 -5
- package/taiga-ai-mcp-server-0.4.0.tgz +0 -0
package/src/tools/products.ts
CHANGED
|
@@ -1,106 +1,120 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { TaigaClient } from '../client.js';
|
|
4
|
+
import { runTool, toolResult } from './errors.js';
|
|
5
|
+
import { documentType } from './inputs.js';
|
|
4
6
|
import { summarizeList } from './summarize.js';
|
|
5
7
|
|
|
8
|
+
const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;
|
|
9
|
+
|
|
6
10
|
export function registerProductTools(server: McpServer, client: TaigaClient) {
|
|
7
11
|
if (client.hasCapability('factories:read')) {
|
|
8
|
-
server.
|
|
12
|
+
server.registerTool(
|
|
9
13
|
'taiga_list_factories',
|
|
10
|
-
'List all factories in the organization. A factory is the group that owns a set of products. ' +
|
|
11
|
-
'Use this to find factory IDs for reading factory instructions. ' +
|
|
12
|
-
"Factory instructions carry the factory's standing guidance: domain context, tech preferences, " +
|
|
13
|
-
'and the conventions its products follow.',
|
|
14
14
|
{
|
|
15
|
-
|
|
15
|
+
title: 'List factories',
|
|
16
|
+
description:
|
|
17
|
+
'List all factories in the organization. A factory is the group that owns a set of products. ' +
|
|
18
|
+
'Use this to find factory IDs for reading factory instructions. ' +
|
|
19
|
+
"Factory instructions carry the factory's standing guidance: domain context, tech preferences, " +
|
|
20
|
+
'and the conventions its products follow.',
|
|
21
|
+
inputSchema: {
|
|
22
|
+
limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
|
|
23
|
+
},
|
|
24
|
+
annotations: READ_ONLY,
|
|
16
25
|
},
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
};
|
|
23
|
-
}
|
|
26
|
+
(params) =>
|
|
27
|
+
runTool(async () => {
|
|
28
|
+
const result = await client.listFactories(params);
|
|
29
|
+
return toolResult(result.data);
|
|
30
|
+
})
|
|
24
31
|
);
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
if (client.hasCapability('products:read')) {
|
|
28
|
-
server.
|
|
35
|
+
server.registerTool(
|
|
29
36
|
'taiga_list_products',
|
|
30
|
-
'List products in the organization. Use this to find the product you are working on ' +
|
|
31
|
-
'so you can read its specification, architecture, and threat model. ' +
|
|
32
|
-
'Can filter by factory; a status filter also exists but no longer distinguishes anything (see its description).',
|
|
33
37
|
{
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
title: 'List products',
|
|
39
|
+
description:
|
|
40
|
+
'List products in the organization. Use this to find the product you are working on ' +
|
|
41
|
+
'so you can read its specification, architecture, and threat model. ' +
|
|
42
|
+
'Can filter by factory; a status filter also exists but no longer distinguishes anything (see its description).',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
factoryId: z
|
|
45
|
+
.string()
|
|
46
|
+
.uuid()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('Filter by factory ID -- get factory IDs from taiga_list_factories'),
|
|
49
|
+
status: z
|
|
50
|
+
.enum(['draft', 'active', 'archived'])
|
|
51
|
+
.optional()
|
|
52
|
+
.describe(
|
|
53
|
+
'Filter by status. Taiga no longer surfaces product status in its UI (PRO-543), so every ' +
|
|
54
|
+
'product is currently "draft" and filtering by "active" or "archived" returns nothing. ' +
|
|
55
|
+
'Omit this unless you are working with data set through the API directly.'
|
|
56
|
+
),
|
|
57
|
+
limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
|
|
58
|
+
},
|
|
59
|
+
annotations: READ_ONLY,
|
|
48
60
|
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
};
|
|
55
|
-
}
|
|
61
|
+
(params) =>
|
|
62
|
+
runTool(async () => {
|
|
63
|
+
const result = await client.listProducts(params);
|
|
64
|
+
return toolResult(result.data);
|
|
65
|
+
})
|
|
56
66
|
);
|
|
57
67
|
|
|
58
|
-
server.
|
|
68
|
+
server.registerTool(
|
|
59
69
|
'taiga_list_product_documents',
|
|
60
|
-
'List documents for a specific product (names and types only). ' +
|
|
61
|
-
'Product documents are generated during intake: specification (requirements), ' +
|
|
62
|
-
'architecture (system design), technology decisions, data flow, privacy assessment, ' +
|
|
63
|
-
'threat model (security risks), risk register, user flow, and service blueprint. ' +
|
|
64
|
-
'Use taiga_get_product_document to read the full content.',
|
|
65
70
|
{
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
title: 'List product documents',
|
|
72
|
+
description:
|
|
73
|
+
'List documents for a specific product (names and types only). ' +
|
|
74
|
+
'Product documents are generated during intake: specification (requirements), ' +
|
|
75
|
+
'architecture (system design), technology decisions, data flow, privacy assessment, ' +
|
|
76
|
+
'threat model (security risks), risk register, user flow, and service blueprint. ' +
|
|
77
|
+
'Use taiga_get_product_document to read the full content.',
|
|
78
|
+
inputSchema: {
|
|
79
|
+
productId: z.string().uuid().describe('Product ID -- get from taiga_list_products'),
|
|
80
|
+
limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
|
|
81
|
+
},
|
|
82
|
+
annotations: READ_ONLY,
|
|
68
83
|
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
};
|
|
76
|
-
}
|
|
84
|
+
(params) =>
|
|
85
|
+
runTool(async () => {
|
|
86
|
+
const result = await client.listProductDocuments(params.productId, { limit: params.limit });
|
|
87
|
+
const summaries = summarizeList(result.data as Record<string, unknown>[]);
|
|
88
|
+
return toolResult(summaries);
|
|
89
|
+
})
|
|
77
90
|
);
|
|
78
91
|
|
|
79
|
-
server.
|
|
92
|
+
server.registerTool(
|
|
80
93
|
'taiga_get_product_document',
|
|
81
|
-
'Get the full content of a specific product document. ' +
|
|
82
|
-
'Use this when you need to understand what to build (specification), ' +
|
|
83
|
-
'how the system is designed (architecture), what security risks exist (threat model), ' +
|
|
84
|
-
'or what technology choices were made (technology_decisions). ' +
|
|
85
|
-
'Types: specification, architecture, technology_decisions, data_flow, ' +
|
|
86
|
-
'privacy_assessment, threat_model, risk_register, user_flow, service_blueprint, ' +
|
|
87
|
-
"ui_direction, codebase_snapshot, and product_instructions (the product's standing instructions).",
|
|
88
94
|
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
.
|
|
92
|
-
|
|
95
|
+
title: 'Get product document',
|
|
96
|
+
description:
|
|
97
|
+
'Get the full content of a specific product document. ' +
|
|
98
|
+
'Use this when you need to understand what to build (specification), ' +
|
|
99
|
+
'how the system is designed (architecture), what security risks exist (threat model), ' +
|
|
100
|
+
'or what technology choices were made (technology_decisions). ' +
|
|
101
|
+
'Types: specification, architecture, technology_decisions, data_flow, ' +
|
|
102
|
+
'privacy_assessment, threat_model, risk_register, user_flow, service_blueprint, ' +
|
|
103
|
+
"ui_direction, codebase_snapshot, and product_instructions (the product's standing instructions).",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
productId: z.string().uuid().describe('Product ID -- get from taiga_list_products'),
|
|
106
|
+
type: documentType.describe(
|
|
93
107
|
'Document type -- get available types from taiga_list_product_documents. ' +
|
|
94
108
|
'Common: specification, architecture, threat_model, technology_decisions'
|
|
95
109
|
),
|
|
110
|
+
},
|
|
111
|
+
annotations: READ_ONLY,
|
|
96
112
|
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
};
|
|
103
|
-
}
|
|
113
|
+
(params) =>
|
|
114
|
+
runTool(async () => {
|
|
115
|
+
const result = await client.getProductDocument(params.productId, params.type);
|
|
116
|
+
return toolResult(result.data);
|
|
117
|
+
})
|
|
104
118
|
);
|
|
105
119
|
}
|
|
106
120
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { applySectionEdit, sectionContent, sectionsOf } from './sections.js';
|
|
3
|
+
|
|
4
|
+
describe('sectionsOf', () => {
|
|
5
|
+
it('returns the sections map of a section-shaped document', () => {
|
|
6
|
+
expect(sectionsOf({ sections: { a: { content: 'x' } } })).toEqual({ a: { content: 'x' } });
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('returns null for documents that keep their content elsewhere', () => {
|
|
10
|
+
expect(sectionsOf({ components: {} })).toBeNull();
|
|
11
|
+
expect(sectionsOf({ sections: 'text' })).toBeNull();
|
|
12
|
+
expect(sectionsOf({ sections: ['a'] })).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('sectionContent', () => {
|
|
17
|
+
it('reads object and bare-string sections alike', () => {
|
|
18
|
+
expect(sectionContent({ a: { content: 'x' } }, 'a')).toBe('x');
|
|
19
|
+
expect(sectionContent({ a: 'plain' }, 'a')).toBe('plain');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('is empty for a section without text and null for a missing or null one', () => {
|
|
23
|
+
expect(sectionContent({ a: { priority: 'core' } }, 'a')).toBe('');
|
|
24
|
+
expect(sectionContent({}, 'a')).toBeNull();
|
|
25
|
+
expect(sectionContent({ a: null }, 'a')).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('applySectionEdit', () => {
|
|
30
|
+
const sections = {
|
|
31
|
+
summary: { source: 'template', priority: 'core', content: 'old', derivedFrom: ['doc-1'] },
|
|
32
|
+
scope: 'bare string',
|
|
33
|
+
list: ['not', 'a', 'section'],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
it('replaces the text and keeps the section metadata and the other sections', () => {
|
|
37
|
+
expect(applySectionEdit(sections, 'summary', 'new')).toEqual({
|
|
38
|
+
summary: { source: 'template', priority: 'core', content: 'new', derivedFrom: ['doc-1'] },
|
|
39
|
+
scope: 'bare string',
|
|
40
|
+
list: ['not', 'a', 'section'],
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('turns a bare-string or array-valued section into an object with the new text', () => {
|
|
45
|
+
expect(applySectionEdit(sections, 'scope', 'new').scope).toEqual({ content: 'new' });
|
|
46
|
+
expect(applySectionEdit(sections, 'list', 'new').list).toEqual({ content: 'new' });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('adds a section that did not exist', () => {
|
|
50
|
+
expect(applySectionEdit(sections, 'retention', 'keep 90 days').retention).toEqual({ content: 'keep 90 days' });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('does not mutate its input', () => {
|
|
54
|
+
applySectionEdit(sections, 'summary', 'new');
|
|
55
|
+
|
|
56
|
+
expect(sections.summary.content).toBe('old');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a section edit changes a document's `data`.
|
|
3
|
+
*
|
|
4
|
+
* Policies, organization instructions and factory instructions keep their
|
|
5
|
+
* content under `data.sections`, one entry per section id. An edit replaces
|
|
6
|
+
* that section's text and nothing else: `priority` (ordering), `source` and
|
|
7
|
+
* `derivedFrom` (which reference documents shaped it, PRO-2158) stay, so the
|
|
8
|
+
* next derivation does not re-add what an earlier one already brought in.
|
|
9
|
+
* Same rule as the web editor (apps/web/src/features/organization/lib/
|
|
10
|
+
* policySectionEdits.ts); the two must agree or a draft saved from one
|
|
11
|
+
* surface loses what the other preserved.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** A section as stored: an object with `content` and metadata, or (older rows) a bare string. */
|
|
15
|
+
export type Sections = Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
18
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The `sections` map of a document, or null when the document is not section-shaped. */
|
|
22
|
+
export function sectionsOf(data: Record<string, unknown>): Sections | null {
|
|
23
|
+
return isRecord(data.sections) ? data.sections : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The text of one section, or null when the section does not exist. */
|
|
27
|
+
export function sectionContent(sections: Sections, sectionId: string): string | null {
|
|
28
|
+
const section = sections[sectionId];
|
|
29
|
+
if (section === undefined || section === null) return null;
|
|
30
|
+
if (typeof section === 'string') return section;
|
|
31
|
+
return isRecord(section) && typeof section.content === 'string' ? section.content : '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The map with one section's text replaced (or the section added); every other section untouched. */
|
|
35
|
+
export function applySectionEdit(sections: Sections, sectionId: string, content: string): Sections {
|
|
36
|
+
const prior = sections[sectionId];
|
|
37
|
+
return { ...sections, [sectionId]: isRecord(prior) ? { ...prior, content } : { content } };
|
|
38
|
+
}
|
|
Binary file
|
package/tsconfig.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
|
+
"extends": "@tsconfig/strictest/tsconfig.json",
|
|
2
3
|
"compilerOptions": {
|
|
3
4
|
"target": "ES2022",
|
|
4
5
|
"module": "ESNext",
|
|
5
6
|
"moduleResolution": "bundler",
|
|
6
7
|
"declaration": true,
|
|
7
8
|
"declarationMap": true,
|
|
8
|
-
"strict": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
9
|
"outDir": "./dist",
|
|
11
10
|
"rootDir": "./src",
|
|
12
|
-
"esModuleInterop": true,
|
|
13
11
|
"resolveJsonModule": true,
|
|
14
|
-
"types": ["node"]
|
|
12
|
+
"types": ["node"],
|
|
13
|
+
|
|
14
|
+
// Preset flags this package still fails (PRO-2509). Each line is deleted
|
|
15
|
+
// by the issue named beside it; no new line is added here.
|
|
16
|
+
"noPropertyAccessFromIndexSignature": false // PRO-2519
|
|
15
17
|
},
|
|
16
18
|
"include": ["src"],
|
|
17
|
-
"exclude": ["node_modules", "dist"]
|
|
19
|
+
"exclude": ["node_modules", "dist"],
|
|
20
|
+
"references": [{ "path": "../../packages/types" }]
|
|
18
21
|
}
|
|
Binary file
|