@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.
@@ -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('projects:read')) {
4
- server.tool('taiga_list_projects', 'List all projects in the organization. Returns project name, status, team, and metadata.', {
5
- teamId: z.string().uuid().optional().describe('Filter by team ID'),
6
- status: z.enum(['draft', 'active', 'archived']).optional().describe('Filter by status'),
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.listProjects(params);
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
- server.tool('taiga_get_project', 'Get detailed information about a specific project by ID.', {
15
- id: z.string().uuid().describe('Project ID'),
16
- }, async (params) => {
17
- const result = await client.getProject(params.id);
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
- if (client.hasCapability('teams:read')) {
24
- server.tool('taiga_list_teams', 'List all teams in the organization.', {
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.listTeams(params);
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;AAEhD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,QAkCxE"}
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"}
@@ -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 AI agents (coding standards, ' +
7
- 'architecture patterns, deployment procedures, etc).', {
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(result.data, null, 2) }],
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 by ID. ' +
16
- 'Returns the skill definition including instructions, context, and metadata.', {
17
- id: z.string().uuid().describe('Skill ID'),
18
- }, async (params) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.0.1-dev.59",
3
+ "version": "0.0.1-dev.62",
4
4
  "description": "MCP server for Taiga -- gives AI assistants access to documents, skills, and projects",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
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
- name: 'taiga',
33
- version: '0.0.1',
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
- 'taiga_get_project',
24
+ 'taiga_get_project_document',
26
25
  'taiga_get_skill',
27
- 'taiga_list_backlog_items',
26
+ 'taiga_get_team_document',
28
27
  'taiga_list_documents',
29
- 'taiga_list_environments',
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 11 tools when API key has full capabilities', async () => {
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 project tools for a projects:read-only key', async () => {
148
- const { client } = await bootMcpPair({ capabilities: ['projects:read'] });
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('taiga_list_projects');
154
- expect(names).toContain('taiga_get_project');
155
- expect(names).toContain('taiga_list_environments');
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 update tool when documents:write is also granted', async () => {
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(toolNames(tools)).toContain('taiga_update_document');
183
+ expect(names).toContain('taiga_update_document');
184
+ expect(names).toContain('taiga_update_skill');
181
185
  });
182
186
 
183
- it('should expose backlog tools only with backlog:read', async () => {
184
- const { client } = await bootMcpPair({ capabilities: ['backlog:read'] });
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('taiga_list_backlog_items');
190
- expect(names).toContain('taiga_get_backlog_item');
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: ['projects:read', 'backlog:read'],
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
- // projects:read tools present
203
- expect(names).toContain('taiga_list_projects');
204
- expect(names).toContain('taiga_list_environments');
205
- // backlog:read tools present
206
- expect(names).toContain('taiga_list_backlog_items');
207
- // unrelated capabilities absent
208
- expect(names).not.toContain('taiga_list_documents');
209
- expect(names).not.toContain('taiga_list_teams');
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', 'taiga_get_project', 'taiga_list_environments'],
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 project list as JSON text content', async () => {
262
- const projects = [
263
- { id: 'p1', name: 'Alpha' },
264
- { id: 'p2', name: 'Beta' },
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/projects': { data: projects, nextCursor: null } },
274
+ routes: { 'GET /v1/teams': { data: teams, nextCursor: null } },
270
275
  });
271
276
 
272
- const result = await client.callTool({ name: 'taiga_list_projects', arguments: {} });
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(projects);
280
+ expect(parseToolResult(result)).toEqual(teams);
276
281
  });
277
282
 
278
- it('should pass project id through to the API', async () => {
279
- const projectId = '00000000-0000-0000-0000-000000000001';
280
- const project = { id: projectId, name: 'Alpha', status: 'active' };
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: { [`GET /v1/projects/${projectId}`]: { data: project } },
289
+ routes: { [`PATCH /v1/agent-skills/${skillId}`]: { data: updated } },
285
290
  });
286
291
 
287
- const result = await client.callTool({
288
- name: 'taiga_get_project',
289
- arguments: { id: projectId },
292
+ await client.callTool({
293
+ name: 'taiga_update_skill',
294
+ arguments: { id: skillId, content: '# Updated' },
290
295
  });
291
296
 
292
- expect(parseToolResult(result)).toEqual(project);
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 pass projectId to backlog items endpoint', async () => {
328
- const projectId = '00000000-0000-0000-0000-000000000001';
329
- const items = [{ id: 'bi1', name: 'Feature X' }];
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: { [`GET /v1/projects/${projectId}/backlog`]: { data: items, nextCursor: null } },
341
+ routes: { [`PUT /v1/teams/${teamId}/documents/${docType}/draft`]: { data: updated } },
334
342
  });
335
343
 
336
- const result = await client.callTool({
337
- name: 'taiga_list_backlog_items',
338
- arguments: { projectId },
344
+ await client.callTool({
345
+ name: 'taiga_update_team_document',
346
+ arguments: { teamId, type: docType, data: { sections: { overview: 'Updated' } } },
339
347
  });
340
348
 
341
- expect(parseToolResult(result)).toEqual(items);
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 () => {