@taiga-ai/mcp-server 0.0.1-dev.59 → 0.0.1-dev.62
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/client.d.ts +36 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +102 -0
- package/dist/index.js +30 -1
- package/dist/server.integration.test.js +66 -56
- package/dist/tools/backlogs.d.ts +1 -1
- package/dist/tools/backlogs.d.ts.map +1 -1
- package/dist/tools/backlogs.js +5 -32
- package/dist/tools/documents.d.ts.map +1 -1
- package/dist/tools/documents.js +74 -12
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -0
- package/dist/tools/projects.d.ts.map +1 -1
- package/dist/tools/projects.js +46 -15
- package/dist/tools/skills.d.ts.map +1 -1
- package/dist/tools/skills.js +30 -10
- package/dist/tools/summarize.d.ts +15 -0
- package/dist/tools/summarize.d.ts.map +1 -0
- package/dist/tools/summarize.js +26 -0
- package/package.json +1 -1
- package/src/client.ts +130 -0
- package/src/index.ts +37 -5
- package/src/server.integration.test.ts +68 -56
- package/src/tools/backlogs.ts +6 -49
- package/src/tools/documents.ts +101 -9
- package/src/tools/index.ts +3 -0
- package/src/tools/projects.ts +57 -14
- package/src/tools/skills.ts +37 -8
- package/src/tools/summarize.ts +30 -0
package/dist/tools/projects.js
CHANGED
|
@@ -1,30 +1,61 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { summarizeList } from './summarize.js';
|
|
2
3
|
export function registerProjectTools(server, client) {
|
|
3
|
-
if (client.hasCapability('
|
|
4
|
-
server.tool('
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
if (client.hasCapability('teams:read')) {
|
|
5
|
+
server.tool('taiga_list_teams', 'List all teams in the organization. Use this to find team IDs ' +
|
|
6
|
+
'for reading team documents, skills, and understanding team structure. ' +
|
|
7
|
+
'Teams own domain context, tech preferences, and architecture constraints.', {
|
|
7
8
|
limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
|
|
8
|
-
}, async (params) => {
|
|
9
|
-
const result = await client.
|
|
9
|
+
}, { readOnlyHint: true, openWorldHint: false }, async (params) => {
|
|
10
|
+
const result = await client.listTeams(params);
|
|
10
11
|
return {
|
|
11
12
|
content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
|
|
12
13
|
};
|
|
13
14
|
});
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
}
|
|
16
|
+
if (client.hasCapability('projects:read')) {
|
|
17
|
+
server.tool('taiga_list_projects', 'List projects in the organization. Use this to find the project you are working on ' +
|
|
18
|
+
'so you can read its specification, architecture, and threat model. ' +
|
|
19
|
+
'Can filter by team or status.', {
|
|
20
|
+
teamId: z.string().uuid().optional().describe('Filter by team ID -- get team IDs from taiga_list_teams'),
|
|
21
|
+
status: z
|
|
22
|
+
.enum(['draft', 'active', 'archived'])
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Filter by status (draft = intake in progress, active = development)'),
|
|
25
|
+
limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
|
|
26
|
+
}, { readOnlyHint: true, openWorldHint: false }, async (params) => {
|
|
27
|
+
const result = await client.listProjects(params);
|
|
18
28
|
return {
|
|
19
29
|
content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
|
|
20
30
|
};
|
|
21
31
|
});
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
server.tool('taiga_list_project_documents', 'List documents for a specific project (names and types only). ' +
|
|
33
|
+
'Project documents are generated during intake: specification (requirements), ' +
|
|
34
|
+
'architecture (system design), technology decisions, data flow, privacy assessment, ' +
|
|
35
|
+
'threat model (security risks), risk register, and user flow. ' +
|
|
36
|
+
'Use taiga_get_project_document to read the full content.', {
|
|
37
|
+
projectId: z.string().uuid().describe('Project ID -- get from taiga_list_projects'),
|
|
25
38
|
limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
|
|
26
|
-
}, async (params) => {
|
|
27
|
-
const result = await client.
|
|
39
|
+
}, { readOnlyHint: true, openWorldHint: false }, async (params) => {
|
|
40
|
+
const result = await client.listProjectDocuments(params.projectId, { limit: params.limit });
|
|
41
|
+
const summaries = summarizeList(result.data);
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: 'text', text: JSON.stringify(summaries, null, 2) }],
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
server.tool('taiga_get_project_document', 'Get the full content of a specific project document. ' +
|
|
47
|
+
'Use this when you need to understand what to build (specification), ' +
|
|
48
|
+
'how the system is designed (architecture), what security risks exist (threat model), ' +
|
|
49
|
+
'or what technology choices were made (technology_decisions). ' +
|
|
50
|
+
'Types: specification, architecture, technology_decisions, data_flow, ' +
|
|
51
|
+
'privacy_assessment, threat_model, risk_register, user_flow.', {
|
|
52
|
+
projectId: z.string().uuid().describe('Project ID -- get from taiga_list_projects'),
|
|
53
|
+
type: z
|
|
54
|
+
.string()
|
|
55
|
+
.describe('Document type -- get available types from taiga_list_project_documents. ' +
|
|
56
|
+
'Common: specification, architecture, threat_model, technology_decisions'),
|
|
57
|
+
}, { readOnlyHint: true, openWorldHint: false }, async (params) => {
|
|
58
|
+
const result = await client.getProjectDocument(params.projectId, params.type);
|
|
28
59
|
return {
|
|
29
60
|
content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
|
|
30
61
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/tools/skills.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/tools/skills.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,QA8DxE"}
|
package/dist/tools/skills.js
CHANGED
|
@@ -1,25 +1,45 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { summarizeList } from './summarize.js';
|
|
2
3
|
export function registerSkillTools(server, client) {
|
|
3
|
-
// Skills require tenant:documents:read as they're part of the knowledge base
|
|
4
4
|
if (client.hasCapability('tenant:documents:read')) {
|
|
5
|
-
server.tool('taiga_list_skills', 'List agent skills available in the organization. ' +
|
|
6
|
-
'Skills are reusable knowledge modules that guide
|
|
7
|
-
'architecture patterns,
|
|
5
|
+
server.tool('taiga_list_skills', 'List agent skills available in the organization (names and descriptions only). ' +
|
|
6
|
+
'Skills are reusable knowledge modules that guide coding: coding standards, ' +
|
|
7
|
+
'security requirements, architecture patterns, database conventions, etc. ' +
|
|
8
|
+
'Use taiga_get_skill with the skill ID to read the full content.', {
|
|
8
9
|
limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
|
|
9
|
-
}, async (params) => {
|
|
10
|
+
}, { readOnlyHint: true, openWorldHint: false }, async (params) => {
|
|
10
11
|
const result = await client.listSkills(params);
|
|
12
|
+
const summaries = summarizeList(result.data);
|
|
11
13
|
return {
|
|
12
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
14
|
+
content: [{ type: 'text', text: JSON.stringify(summaries, null, 2) }],
|
|
13
15
|
};
|
|
14
16
|
});
|
|
15
|
-
server.tool('taiga_get_skill', 'Get the full content of a specific agent skill
|
|
16
|
-
'
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
server.tool('taiga_get_skill', 'Get the full content of a specific agent skill. ' +
|
|
18
|
+
'Use this when you need detailed coding guidelines, patterns, or conventions ' +
|
|
19
|
+
'for a specific topic (e.g., security requirements, API design, testing strategy). ' +
|
|
20
|
+
'Returns the complete markdown content with code examples and best practices.', {
|
|
21
|
+
id: z.string().uuid().describe('Skill ID -- get from taiga_list_skills'),
|
|
22
|
+
}, { readOnlyHint: true, openWorldHint: false }, async (params) => {
|
|
19
23
|
const result = await client.getSkill(params.id);
|
|
20
24
|
return {
|
|
21
25
|
content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
|
|
22
26
|
};
|
|
23
27
|
});
|
|
24
28
|
}
|
|
29
|
+
if (client.hasCapability('tenant:documents:write')) {
|
|
30
|
+
server.tool('taiga_update_skill', 'Update an agent skill (coding standards, patterns, conventions). ' +
|
|
31
|
+
'Use this when you notice a skill is outdated, incomplete, or needs corrections ' +
|
|
32
|
+
'while working on the codebase. Changes are saved immediately.', {
|
|
33
|
+
id: z.string().uuid().describe('Skill ID -- get from taiga_list_skills'),
|
|
34
|
+
name: z.string().optional().describe('Updated skill name'),
|
|
35
|
+
description: z.string().optional().describe('Updated skill description'),
|
|
36
|
+
content: z.string().optional().describe('Updated skill content (markdown)'),
|
|
37
|
+
}, { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async (params) => {
|
|
38
|
+
const { id, ...body } = params;
|
|
39
|
+
const result = await client.updateSkill(id, body);
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
25
45
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip heavy content from list responses to avoid context window bloat.
|
|
3
|
+
*
|
|
4
|
+
* List endpoints return summaries (id, name, description, metadata).
|
|
5
|
+
* Get endpoints return the full content.
|
|
6
|
+
*
|
|
7
|
+
* This keeps list calls cheap (~100 tokens per item) and lets the AI
|
|
8
|
+
* selectively load full content only for the items it needs.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Remove heavy content fields from each item in an array.
|
|
12
|
+
* Preserves all metadata (id, name, description, type, dates, etc).
|
|
13
|
+
*/
|
|
14
|
+
export declare function summarizeList<T extends Record<string, unknown>>(items: T[]): Omit<T, 'content' | 'data' | 'sections'>[];
|
|
15
|
+
//# sourceMappingURL=summarize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summarize.d.ts","sourceRoot":"","sources":["../../src/tools/summarize.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7D,KAAK,EAAE,CAAC,EAAE,GACT,IAAI,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,UAAU,CAAC,EAAE,CAU5C"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip heavy content from list responses to avoid context window bloat.
|
|
3
|
+
*
|
|
4
|
+
* List endpoints return summaries (id, name, description, metadata).
|
|
5
|
+
* Get endpoints return the full content.
|
|
6
|
+
*
|
|
7
|
+
* This keeps list calls cheap (~100 tokens per item) and lets the AI
|
|
8
|
+
* selectively load full content only for the items it needs.
|
|
9
|
+
*/
|
|
10
|
+
/** Fields to strip from list responses -- these contain the bulk content. */
|
|
11
|
+
const HEAVY_FIELDS = ['content', 'data', 'sections'];
|
|
12
|
+
/**
|
|
13
|
+
* Remove heavy content fields from each item in an array.
|
|
14
|
+
* Preserves all metadata (id, name, description, type, dates, etc).
|
|
15
|
+
*/
|
|
16
|
+
export function summarizeList(items) {
|
|
17
|
+
return items.map((item) => {
|
|
18
|
+
const summary = { ...item };
|
|
19
|
+
for (const field of HEAVY_FIELDS) {
|
|
20
|
+
if (field in summary) {
|
|
21
|
+
delete summary[field];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return summary;
|
|
25
|
+
});
|
|
26
|
+
}
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -94,6 +94,46 @@ export class TaigaClient {
|
|
|
94
94
|
return this.request<{ data: unknown }>('PUT', `/tenants/current/documents/${type}/draft`, body);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Team Documents
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
async listTeamDocuments(teamId: string, opts?: { limit?: number }) {
|
|
102
|
+
const params = new URLSearchParams();
|
|
103
|
+
if (opts?.limit) params.set('limit', String(opts.limit));
|
|
104
|
+
const qs = params.toString();
|
|
105
|
+
return this.request<{ data: unknown[]; nextCursor: string | null }>(
|
|
106
|
+
'GET',
|
|
107
|
+
`/teams/${teamId}/documents${qs ? `?${qs}` : ''}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async getTeamDocument(teamId: string, type: string) {
|
|
112
|
+
return this.request<{ data: unknown }>('GET', `/teams/${teamId}/documents/${type}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async saveTeamDocumentDraft(teamId: string, type: string, body: { data: Record<string, unknown> }) {
|
|
116
|
+
return this.request<{ data: unknown }>('PUT', `/teams/${teamId}/documents/${type}/draft`, body);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Project Documents
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
async listProjectDocuments(projectId: string, opts?: { limit?: number }) {
|
|
124
|
+
const params = new URLSearchParams();
|
|
125
|
+
if (opts?.limit) params.set('limit', String(opts.limit));
|
|
126
|
+
const qs = params.toString();
|
|
127
|
+
return this.request<{ data: unknown[]; nextCursor: string | null }>(
|
|
128
|
+
'GET',
|
|
129
|
+
`/projects/${projectId}/documents${qs ? `?${qs}` : ''}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async getProjectDocument(projectId: string, type: string) {
|
|
134
|
+
return this.request<{ data: unknown }>('GET', `/projects/${projectId}/documents/${type}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
97
137
|
// ---------------------------------------------------------------------------
|
|
98
138
|
// Skills (agent skills)
|
|
99
139
|
// ---------------------------------------------------------------------------
|
|
@@ -111,6 +151,96 @@ export class TaigaClient {
|
|
|
111
151
|
return this.request<{ data: unknown }>('GET', `/agent-skills/${id}`);
|
|
112
152
|
}
|
|
113
153
|
|
|
154
|
+
async updateSkill(id: string, body: { name?: string; description?: string; content?: string }) {
|
|
155
|
+
return this.request<{ data: unknown }>('PATCH', `/agent-skills/${id}`, body);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Team Documents
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Fetch the full context for a project in parallel: project metadata,
|
|
164
|
+
* team documents, project documents, and team skills.
|
|
165
|
+
* Returns a structured bundle optimized for coding agent consumption.
|
|
166
|
+
*/
|
|
167
|
+
async getProjectContext(projectId: string): Promise<Record<string, unknown>> {
|
|
168
|
+
// First fetch the project to get the teamId
|
|
169
|
+
const project = await this.request<{ data: Record<string, unknown> }>('GET', `/projects/${projectId}`);
|
|
170
|
+
const teamId = project.data.teamId as string | undefined;
|
|
171
|
+
|
|
172
|
+
// Fetch everything in parallel
|
|
173
|
+
const [projectDocs, teamDocs, skills] = await Promise.all([
|
|
174
|
+
this.listProjectDocuments(projectId, { limit: 20 }).catch(() => ({ data: [] })),
|
|
175
|
+
teamId
|
|
176
|
+
? this.listTeamDocuments(teamId, { limit: 20 }).catch(() => ({ data: [] }))
|
|
177
|
+
: Promise.resolve({ data: [] }),
|
|
178
|
+
this.listSkills({ limit: 100 }).catch(() => ({ data: [] })),
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
// Fetch full content for key documents (in parallel)
|
|
182
|
+
const keyProjectDocTypes = ['specification', 'architecture', 'technology_decisions'];
|
|
183
|
+
const keyTeamDocTypes = ['domain_context', 'tech_preferences', 'architecture_constraints'];
|
|
184
|
+
|
|
185
|
+
const docFetches: Promise<{ type: string; scope: string; data: unknown }>[] = [];
|
|
186
|
+
|
|
187
|
+
for (const docType of keyProjectDocTypes) {
|
|
188
|
+
const exists = (projectDocs.data as Record<string, unknown>[]).some((d) => d.type === docType && !d.isDraft);
|
|
189
|
+
if (exists) {
|
|
190
|
+
docFetches.push(
|
|
191
|
+
this.getProjectDocument(projectId, docType)
|
|
192
|
+
.then((r) => ({ type: docType, scope: 'project', data: r.data }))
|
|
193
|
+
.catch(() => ({ type: docType, scope: 'project', data: null }))
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (teamId) {
|
|
199
|
+
for (const docType of keyTeamDocTypes) {
|
|
200
|
+
const exists = (teamDocs.data as Record<string, unknown>[]).some((d) => d.type === docType && !d.isDraft);
|
|
201
|
+
if (exists) {
|
|
202
|
+
docFetches.push(
|
|
203
|
+
this.getTeamDocument(teamId, docType)
|
|
204
|
+
.then((r) => ({ type: docType, scope: 'team', data: r.data }))
|
|
205
|
+
.catch(() => ({ type: docType, scope: 'team', data: null }))
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const fetchedDocs = await Promise.all(docFetches);
|
|
212
|
+
|
|
213
|
+
// Assemble context bundle
|
|
214
|
+
const context: Record<string, unknown> = {
|
|
215
|
+
project: project.data,
|
|
216
|
+
documents: {
|
|
217
|
+
project: Object.fromEntries(
|
|
218
|
+
fetchedDocs.filter((d) => d.scope === 'project' && d.data).map((d) => [d.type, d.data])
|
|
219
|
+
),
|
|
220
|
+
team: Object.fromEntries(fetchedDocs.filter((d) => d.scope === 'team' && d.data).map((d) => [d.type, d.data])),
|
|
221
|
+
},
|
|
222
|
+
available_documents: {
|
|
223
|
+
project: (projectDocs.data as Record<string, unknown>[]).map((d) => ({
|
|
224
|
+
type: d.type,
|
|
225
|
+
name: d.name,
|
|
226
|
+
version: d.version,
|
|
227
|
+
isDraft: d.isDraft,
|
|
228
|
+
})),
|
|
229
|
+
team: (teamDocs.data as Record<string, unknown>[]).map((d) => ({
|
|
230
|
+
type: d.type,
|
|
231
|
+
name: d.name,
|
|
232
|
+
version: d.version,
|
|
233
|
+
isDraft: d.isDraft,
|
|
234
|
+
})),
|
|
235
|
+
},
|
|
236
|
+
skills_summary: (skills.data as Record<string, unknown>[])
|
|
237
|
+
.filter((s) => !s.teamId || s.teamId === teamId)
|
|
238
|
+
.map((s) => ({ id: s.id, name: s.name, slug: s.slug, description: s.description })),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return context;
|
|
242
|
+
}
|
|
243
|
+
|
|
114
244
|
// ---------------------------------------------------------------------------
|
|
115
245
|
// Backlog
|
|
116
246
|
// ---------------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -27,11 +27,43 @@ async function main() {
|
|
|
27
27
|
process.exit(1);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
// Create MCP server with dynamic tool registration
|
|
31
|
-
const server = new McpServer(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
// Create MCP server with dynamic tool registration and instructions
|
|
31
|
+
const server = new McpServer(
|
|
32
|
+
{
|
|
33
|
+
name: 'taiga',
|
|
34
|
+
version: '0.0.1',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
instructions: [
|
|
38
|
+
'You have access to Taiga -- a policy-aware software development platform.',
|
|
39
|
+
"Taiga provides your organization's engineering knowledge base:",
|
|
40
|
+
'',
|
|
41
|
+
'**Company policies** (taiga_list_documents / taiga_get_document):',
|
|
42
|
+
'Security, data privacy, SDLC, architecture, and compliance rules that apply to all code.',
|
|
43
|
+
'Read the relevant policy BEFORE writing code that touches security, data handling, or infrastructure.',
|
|
44
|
+
'',
|
|
45
|
+
'**Team documents** (taiga_list_team_documents / taiga_get_team_document):',
|
|
46
|
+
'Domain context (ubiquitous language, entities, business rules), tech preferences (approved stack, patterns),',
|
|
47
|
+
'architecture constraints, and SLO targets. Read these to understand HOW this team builds software.',
|
|
48
|
+
'',
|
|
49
|
+
'**Project documents** (taiga_list_project_documents / taiga_get_project_document):',
|
|
50
|
+
"Specification (what to build), architecture (how it's designed), threat model (security risks),",
|
|
51
|
+
'technology decisions, and data flow. Read these to understand the specific project context.',
|
|
52
|
+
'',
|
|
53
|
+
'**Skills** (taiga_list_skills / taiga_get_skill):',
|
|
54
|
+
'Coding standards, security requirements, API design patterns, testing strategy, database patterns,',
|
|
55
|
+
'and more. These are detailed engineering guidelines with code examples.',
|
|
56
|
+
'',
|
|
57
|
+
"Workflow: Start with taiga_list_teams to find your team, then read the team's domain_context",
|
|
58
|
+
'and tech_preferences. For project-specific work, use taiga_list_projects to find the project,',
|
|
59
|
+
'then read its specification and architecture. Use skills for coding patterns.',
|
|
60
|
+
'',
|
|
61
|
+
'When you notice a policy, skill, or team document is outdated, use the update tools',
|
|
62
|
+
'(taiga_update_document, taiga_update_skill, taiga_update_team_document) to fix it.',
|
|
63
|
+
'This keeps the knowledge base accurate for everyone.',
|
|
64
|
+
].join('\n'),
|
|
65
|
+
}
|
|
66
|
+
);
|
|
35
67
|
|
|
36
68
|
registerAllTools(server, client);
|
|
37
69
|
|
|
@@ -20,17 +20,19 @@ const ALL_CAPABILITIES = [
|
|
|
20
20
|
|
|
21
21
|
/** All tool names the server can register (sorted for deterministic comparison). */
|
|
22
22
|
const ALL_TOOL_NAMES = [
|
|
23
|
-
'taiga_get_backlog_item',
|
|
24
23
|
'taiga_get_document',
|
|
25
|
-
'
|
|
24
|
+
'taiga_get_project_document',
|
|
26
25
|
'taiga_get_skill',
|
|
27
|
-
'
|
|
26
|
+
'taiga_get_team_document',
|
|
28
27
|
'taiga_list_documents',
|
|
29
|
-
'
|
|
28
|
+
'taiga_list_project_documents',
|
|
30
29
|
'taiga_list_projects',
|
|
31
30
|
'taiga_list_skills',
|
|
31
|
+
'taiga_list_team_documents',
|
|
32
32
|
'taiga_list_teams',
|
|
33
33
|
'taiga_update_document',
|
|
34
|
+
'taiga_update_skill',
|
|
35
|
+
'taiga_update_team_document',
|
|
34
36
|
];
|
|
35
37
|
|
|
36
38
|
function makeFetchRouter(routes: Record<string, unknown>) {
|
|
@@ -127,7 +129,7 @@ describe('MCP server integration', () => {
|
|
|
127
129
|
// -------------------------------------------------------------------------
|
|
128
130
|
|
|
129
131
|
describe('tool discovery', () => {
|
|
130
|
-
it('should expose all
|
|
132
|
+
it('should expose all 13 tools when API key has full capabilities', async () => {
|
|
131
133
|
const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
|
|
132
134
|
|
|
133
135
|
const { tools } = await client.listTools();
|
|
@@ -144,17 +146,17 @@ describe('MCP server integration', () => {
|
|
|
144
146
|
await expect(client.listTools()).rejects.toThrow(/Method not found/);
|
|
145
147
|
});
|
|
146
148
|
|
|
147
|
-
it('should expose only
|
|
148
|
-
const { client } = await bootMcpPair({ capabilities: ['
|
|
149
|
+
it('should expose only team tools for a teams:read-only key', async () => {
|
|
150
|
+
const { client } = await bootMcpPair({ capabilities: ['teams:read'] });
|
|
149
151
|
|
|
150
152
|
const { tools } = await client.listTools();
|
|
151
153
|
const names = toolNames(tools);
|
|
152
154
|
|
|
153
|
-
expect(names).toContain('
|
|
154
|
-
expect(names).toContain('
|
|
155
|
-
expect(names).toContain('
|
|
156
|
-
expect(names).not.toContain('taiga_list_teams');
|
|
155
|
+
expect(names).toContain('taiga_list_teams');
|
|
156
|
+
expect(names).toContain('taiga_list_team_documents');
|
|
157
|
+
expect(names).toContain('taiga_get_team_document');
|
|
157
158
|
expect(names).not.toContain('taiga_list_documents');
|
|
159
|
+
expect(names).not.toContain('taiga_update_document');
|
|
158
160
|
});
|
|
159
161
|
|
|
160
162
|
it('should expose read-only document tools without write capability', async () => {
|
|
@@ -170,43 +172,47 @@ describe('MCP server integration', () => {
|
|
|
170
172
|
expect(names).not.toContain('taiga_update_document');
|
|
171
173
|
});
|
|
172
174
|
|
|
173
|
-
it('should expose
|
|
175
|
+
it('should expose write tools when documents:write is also granted', async () => {
|
|
174
176
|
const { client } = await bootMcpPair({
|
|
175
177
|
capabilities: ['tenant:documents:read', 'tenant:documents:write'],
|
|
176
178
|
});
|
|
177
179
|
|
|
178
180
|
const { tools } = await client.listTools();
|
|
181
|
+
const names = toolNames(tools);
|
|
179
182
|
|
|
180
|
-
expect(
|
|
183
|
+
expect(names).toContain('taiga_update_document');
|
|
184
|
+
expect(names).toContain('taiga_update_skill');
|
|
181
185
|
});
|
|
182
186
|
|
|
183
|
-
it('should expose
|
|
184
|
-
const { client } = await bootMcpPair({
|
|
187
|
+
it('should expose team document write when both teams:read and documents:write are granted', async () => {
|
|
188
|
+
const { client } = await bootMcpPair({
|
|
189
|
+
capabilities: ['teams:read', 'tenant:documents:write'],
|
|
190
|
+
});
|
|
185
191
|
|
|
186
192
|
const { tools } = await client.listTools();
|
|
187
193
|
const names = toolNames(tools);
|
|
188
194
|
|
|
189
|
-
expect(names).toContain('
|
|
190
|
-
expect(names).toContain('
|
|
191
|
-
expect(names).not.toContain('taiga_list_environments');
|
|
195
|
+
expect(names).toContain('taiga_update_team_document');
|
|
196
|
+
expect(names).toContain('taiga_list_teams');
|
|
192
197
|
});
|
|
193
198
|
|
|
194
199
|
it('should combine capabilities additively', async () => {
|
|
195
200
|
const { client } = await bootMcpPair({
|
|
196
|
-
capabilities: ['
|
|
201
|
+
capabilities: ['teams:read', 'tenant:documents:read'],
|
|
197
202
|
});
|
|
198
203
|
|
|
199
204
|
const { tools } = await client.listTools();
|
|
200
205
|
const names = toolNames(tools);
|
|
201
206
|
|
|
202
|
-
//
|
|
203
|
-
expect(names).toContain('
|
|
204
|
-
expect(names).toContain('
|
|
205
|
-
//
|
|
206
|
-
expect(names).toContain('
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
expect(names).not.toContain('
|
|
207
|
+
// teams:read tools present
|
|
208
|
+
expect(names).toContain('taiga_list_teams');
|
|
209
|
+
expect(names).toContain('taiga_list_team_documents');
|
|
210
|
+
// tenant:documents:read tools present
|
|
211
|
+
expect(names).toContain('taiga_list_documents');
|
|
212
|
+
expect(names).toContain('taiga_list_skills');
|
|
213
|
+
// write tools absent
|
|
214
|
+
expect(names).not.toContain('taiga_update_document');
|
|
215
|
+
expect(names).not.toContain('taiga_update_skill');
|
|
210
216
|
});
|
|
211
217
|
|
|
212
218
|
it('should provide a description and input schema for every tool', async () => {
|
|
@@ -230,11 +236,10 @@ describe('MCP server integration', () => {
|
|
|
230
236
|
|
|
231
237
|
describe('capability-tool contract', () => {
|
|
232
238
|
const CONTRACT: Record<string, string[]> = {
|
|
233
|
-
'projects:read': ['taiga_list_projects', '
|
|
234
|
-
'teams:read': ['taiga_list_teams'],
|
|
239
|
+
'projects:read': ['taiga_list_projects', 'taiga_list_project_documents', 'taiga_get_project_document'],
|
|
240
|
+
'teams:read': ['taiga_list_teams', 'taiga_list_team_documents', 'taiga_get_team_document'],
|
|
235
241
|
'tenant:documents:read': ['taiga_list_documents', 'taiga_get_document', 'taiga_list_skills', 'taiga_get_skill'],
|
|
236
|
-
'tenant:documents:write': ['taiga_update_document'],
|
|
237
|
-
'backlog:read': ['taiga_list_backlog_items', 'taiga_get_backlog_item'],
|
|
242
|
+
'tenant:documents:write': ['taiga_update_document', 'taiga_update_skill'],
|
|
238
243
|
};
|
|
239
244
|
|
|
240
245
|
for (const [capability, expectedTools] of Object.entries(CONTRACT)) {
|
|
@@ -258,38 +263,40 @@ describe('MCP server integration', () => {
|
|
|
258
263
|
// -------------------------------------------------------------------------
|
|
259
264
|
|
|
260
265
|
describe('tool execution', () => {
|
|
261
|
-
it('should return
|
|
262
|
-
const
|
|
263
|
-
{ id: '
|
|
264
|
-
{ id: '
|
|
266
|
+
it('should return team list as JSON text content', async () => {
|
|
267
|
+
const teams = [
|
|
268
|
+
{ id: 't1', name: 'Product' },
|
|
269
|
+
{ id: 't2', name: 'Platform' },
|
|
265
270
|
];
|
|
266
271
|
|
|
267
272
|
const { client } = await bootMcpPair({
|
|
268
273
|
capabilities: ALL_CAPABILITIES,
|
|
269
|
-
routes: { 'GET /v1/
|
|
274
|
+
routes: { 'GET /v1/teams': { data: teams, nextCursor: null } },
|
|
270
275
|
});
|
|
271
276
|
|
|
272
|
-
const result = await client.callTool({ name: '
|
|
277
|
+
const result = await client.callTool({ name: 'taiga_list_teams', arguments: {} });
|
|
273
278
|
|
|
274
279
|
expect(result.content).toHaveLength(1);
|
|
275
|
-
expect(parseToolResult(result)).toEqual(
|
|
280
|
+
expect(parseToolResult(result)).toEqual(teams);
|
|
276
281
|
});
|
|
277
282
|
|
|
278
|
-
it('should
|
|
279
|
-
const
|
|
280
|
-
const
|
|
283
|
+
it('should send PATCH when updating a skill', async () => {
|
|
284
|
+
const skillId = '00000000-0000-0000-0000-000000000001';
|
|
285
|
+
const updated = { id: skillId, name: 'Updated Skill', content: '# Updated' };
|
|
281
286
|
|
|
282
|
-
const { client } = await bootMcpPair({
|
|
287
|
+
const { client, fetchSpy } = await bootMcpPair({
|
|
283
288
|
capabilities: ALL_CAPABILITIES,
|
|
284
|
-
routes: { [`
|
|
289
|
+
routes: { [`PATCH /v1/agent-skills/${skillId}`]: { data: updated } },
|
|
285
290
|
});
|
|
286
291
|
|
|
287
|
-
|
|
288
|
-
name: '
|
|
289
|
-
arguments: { id:
|
|
292
|
+
await client.callTool({
|
|
293
|
+
name: 'taiga_update_skill',
|
|
294
|
+
arguments: { id: skillId, content: '# Updated' },
|
|
290
295
|
});
|
|
291
296
|
|
|
292
|
-
|
|
297
|
+
const patchCall = fetchSpy.mock.calls.find((c: unknown[]) => (c[1] as RequestInit)?.method === 'PATCH');
|
|
298
|
+
expect(patchCall).toBeDefined();
|
|
299
|
+
expect(patchCall![0]).toContain(`/v1/agent-skills/${skillId}`);
|
|
293
300
|
});
|
|
294
301
|
|
|
295
302
|
it('should return document content through MCP protocol', async () => {
|
|
@@ -324,21 +331,26 @@ describe('MCP server integration', () => {
|
|
|
324
331
|
expect(putCall![0]).toContain(`/v1/tenants/current/documents/${docType}/draft`);
|
|
325
332
|
});
|
|
326
333
|
|
|
327
|
-
it('should
|
|
328
|
-
const
|
|
329
|
-
const
|
|
334
|
+
it('should send PUT when updating a team document draft', async () => {
|
|
335
|
+
const teamId = '00000000-0000-0000-0000-000000000001';
|
|
336
|
+
const docType = 'domain_context';
|
|
337
|
+
const updated = { type: docType, name: 'Domain Context' };
|
|
330
338
|
|
|
331
|
-
const { client } = await bootMcpPair({
|
|
339
|
+
const { client, fetchSpy } = await bootMcpPair({
|
|
332
340
|
capabilities: ALL_CAPABILITIES,
|
|
333
|
-
routes: { [`
|
|
341
|
+
routes: { [`PUT /v1/teams/${teamId}/documents/${docType}/draft`]: { data: updated } },
|
|
334
342
|
});
|
|
335
343
|
|
|
336
|
-
|
|
337
|
-
name: '
|
|
338
|
-
arguments: {
|
|
344
|
+
await client.callTool({
|
|
345
|
+
name: 'taiga_update_team_document',
|
|
346
|
+
arguments: { teamId, type: docType, data: { sections: { overview: 'Updated' } } },
|
|
339
347
|
});
|
|
340
348
|
|
|
341
|
-
|
|
349
|
+
const putCall = fetchSpy.mock.calls.find(
|
|
350
|
+
(c: unknown[]) => (c[1] as RequestInit)?.method === 'PUT' && (c[0] as string).includes('/teams/')
|
|
351
|
+
);
|
|
352
|
+
expect(putCall).toBeDefined();
|
|
353
|
+
expect(putCall![0]).toContain(`/v1/teams/${teamId}/documents/${docType}/draft`);
|
|
342
354
|
});
|
|
343
355
|
|
|
344
356
|
it('should return skills through MCP protocol', async () => {
|