@taiga-ai/mcp-server 0.0.1-dev.51

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.
Files changed (40) hide show
  1. package/dist/client.d.ts +73 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +126 -0
  4. package/dist/client.test.d.ts +2 -0
  5. package/dist/client.test.d.ts.map +1 -0
  6. package/dist/client.test.js +203 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +38 -0
  10. package/dist/server.integration.test.d.ts +2 -0
  11. package/dist/server.integration.test.d.ts.map +1 -0
  12. package/dist/server.integration.test.js +348 -0
  13. package/dist/tools/backlogs.d.ts +4 -0
  14. package/dist/tools/backlogs.d.ts.map +1 -0
  15. package/dist/tools/backlogs.js +33 -0
  16. package/dist/tools/documents.d.ts +4 -0
  17. package/dist/tools/documents.d.ts.map +1 -0
  18. package/dist/tools/documents.js +35 -0
  19. package/dist/tools/index.d.ts +9 -0
  20. package/dist/tools/index.d.ts.map +1 -0
  21. package/dist/tools/index.js +15 -0
  22. package/dist/tools/projects.d.ts +4 -0
  23. package/dist/tools/projects.d.ts.map +1 -0
  24. package/dist/tools/projects.js +33 -0
  25. package/dist/tools/skills.d.ts +4 -0
  26. package/dist/tools/skills.d.ts.map +1 -0
  27. package/dist/tools/skills.js +25 -0
  28. package/package.json +36 -0
  29. package/scripts/prepare-publish.cjs +33 -0
  30. package/src/client.test.ts +277 -0
  31. package/src/client.ts +150 -0
  32. package/src/index.ts +51 -0
  33. package/src/server.integration.test.ts +429 -0
  34. package/src/tools/backlogs.ts +53 -0
  35. package/src/tools/documents.ts +55 -0
  36. package/src/tools/index.ts +18 -0
  37. package/src/tools/projects.ts +53 -0
  38. package/src/tools/skills.ts +39 -0
  39. package/tsconfig.json +17 -0
  40. package/vitest.config.ts +9 -0
@@ -0,0 +1,429 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
5
+ import { TaigaClient } from './client.js';
6
+ import { registerAllTools } from './tools/index.js';
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Helpers -- thin wrappers, not abstraction layers
10
+ // ---------------------------------------------------------------------------
11
+
12
+ /** All capabilities the MCP server understands. */
13
+ const ALL_CAPABILITIES = [
14
+ 'projects:read',
15
+ 'teams:read',
16
+ 'tenant:documents:read',
17
+ 'tenant:documents:write',
18
+ 'backlog:read',
19
+ ];
20
+
21
+ /** All tool names the server can register (sorted for deterministic comparison). */
22
+ const ALL_TOOL_NAMES = [
23
+ 'taiga_get_backlog_item',
24
+ 'taiga_get_document',
25
+ 'taiga_get_project',
26
+ 'taiga_get_skill',
27
+ 'taiga_list_backlog_items',
28
+ 'taiga_list_documents',
29
+ 'taiga_list_environments',
30
+ 'taiga_list_projects',
31
+ 'taiga_list_skills',
32
+ 'taiga_list_teams',
33
+ 'taiga_update_document',
34
+ ];
35
+
36
+ function makeFetchRouter(routes: Record<string, unknown>) {
37
+ return vi.fn().mockImplementation(async (url: string, init: RequestInit) => {
38
+ const path = new URL(url).pathname;
39
+ const method = init?.method ?? 'GET';
40
+ const key = `${method} ${path}`;
41
+
42
+ const body = routes[key] ?? routes[path];
43
+ if (body !== undefined) {
44
+ return {
45
+ ok: true,
46
+ status: 200,
47
+ json: () => Promise.resolve(body),
48
+ text: () => Promise.resolve(JSON.stringify(body)),
49
+ };
50
+ }
51
+ return {
52
+ ok: false,
53
+ status: 404,
54
+ json: () => Promise.resolve({ error: 'Not found' }),
55
+ text: () => Promise.resolve('Not found'),
56
+ };
57
+ });
58
+ }
59
+
60
+ function introspectionResponse(capabilities: string[]) {
61
+ return {
62
+ data: {
63
+ userId: 'user-1',
64
+ tenantId: 'tenant-1',
65
+ email: 'dev@test.com',
66
+ role: 'member',
67
+ capabilities,
68
+ },
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Boots a full MCP Client <-> Server pair connected in-memory.
74
+ * The TaigaClient uses a route-based fetch mock, so no real HTTP is needed.
75
+ * Returns both the MCP client (for protocol calls) and the fetch spy (for
76
+ * verifying outgoing HTTP requests when needed).
77
+ */
78
+ async function bootMcpPair(opts: { capabilities: string[]; routes?: Record<string, unknown> }) {
79
+ const allRoutes: Record<string, unknown> = {
80
+ '/v1/auth/introspect': introspectionResponse(opts.capabilities),
81
+ ...opts.routes,
82
+ };
83
+ const fetchSpy = makeFetchRouter(allRoutes);
84
+ vi.stubGlobal('fetch', fetchSpy);
85
+
86
+ // Arrange -- Taiga client, introspect, register tools
87
+ const taigaClient = new TaigaClient({
88
+ baseUrl: 'https://api.test.com',
89
+ apiKey: 'taiga_sk_live_test',
90
+ });
91
+ await taigaClient.introspect();
92
+
93
+ const server = new McpServer({ name: 'taiga-test', version: '0.0.1' });
94
+ registerAllTools(server, taigaClient);
95
+
96
+ const client = new Client({ name: 'test-client', version: '0.0.1' });
97
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
98
+ await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
99
+
100
+ return { client, fetchSpy };
101
+ }
102
+
103
+ /** Extract sorted tool names from a listTools response. */
104
+ function toolNames(tools: { name: string }[]) {
105
+ return tools.map((t) => t.name).sort();
106
+ }
107
+
108
+ /** Extract parsed JSON from the first text content block of a tool call result. */
109
+ function parseToolResult(result: Record<string, unknown>) {
110
+ const content = result.content as { type: string; text: string }[];
111
+ return JSON.parse(content[0].text);
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Tests
116
+ // ---------------------------------------------------------------------------
117
+
118
+ describe('MCP server integration', () => {
119
+ beforeEach(() => {
120
+ vi.restoreAllMocks();
121
+ });
122
+
123
+ // -------------------------------------------------------------------------
124
+ // Tool discovery -- the AI assistant asks "what tools do you have?"
125
+ // These tests verify the server exposes exactly the right tools based on
126
+ // the API key's capabilities. This is the primary security contract.
127
+ // -------------------------------------------------------------------------
128
+
129
+ describe('tool discovery', () => {
130
+ it('should expose all 11 tools when API key has full capabilities', async () => {
131
+ const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
132
+
133
+ const { tools } = await client.listTools();
134
+
135
+ expect(toolNames(tools)).toEqual(ALL_TOOL_NAMES);
136
+ });
137
+
138
+ it('should reject tools/list when API key has no capabilities', async () => {
139
+ // When zero tools are registered, the MCP SDK doesn't advertise
140
+ // the tools capability at all -- the protocol-correct response
141
+ // is "Method not found".
142
+ const { client } = await bootMcpPair({ capabilities: [] });
143
+
144
+ await expect(client.listTools()).rejects.toThrow(/Method not found/);
145
+ });
146
+
147
+ it('should expose only project tools for a projects:read-only key', async () => {
148
+ const { client } = await bootMcpPair({ capabilities: ['projects:read'] });
149
+
150
+ const { tools } = await client.listTools();
151
+ const names = toolNames(tools);
152
+
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');
157
+ expect(names).not.toContain('taiga_list_documents');
158
+ });
159
+
160
+ it('should expose read-only document tools without write capability', async () => {
161
+ const { client } = await bootMcpPair({ capabilities: ['tenant:documents:read'] });
162
+
163
+ const { tools } = await client.listTools();
164
+ const names = toolNames(tools);
165
+
166
+ expect(names).toContain('taiga_list_documents');
167
+ expect(names).toContain('taiga_get_document');
168
+ expect(names).toContain('taiga_list_skills');
169
+ expect(names).toContain('taiga_get_skill');
170
+ expect(names).not.toContain('taiga_update_document');
171
+ });
172
+
173
+ it('should expose update tool when documents:write is also granted', async () => {
174
+ const { client } = await bootMcpPair({
175
+ capabilities: ['tenant:documents:read', 'tenant:documents:write'],
176
+ });
177
+
178
+ const { tools } = await client.listTools();
179
+
180
+ expect(toolNames(tools)).toContain('taiga_update_document');
181
+ });
182
+
183
+ it('should expose backlog tools only with backlog:read', async () => {
184
+ const { client } = await bootMcpPair({ capabilities: ['backlog:read'] });
185
+
186
+ const { tools } = await client.listTools();
187
+ const names = toolNames(tools);
188
+
189
+ expect(names).toContain('taiga_list_backlog_items');
190
+ expect(names).toContain('taiga_get_backlog_item');
191
+ expect(names).not.toContain('taiga_list_environments');
192
+ });
193
+
194
+ it('should combine capabilities additively', async () => {
195
+ const { client } = await bootMcpPair({
196
+ capabilities: ['projects:read', 'backlog:read'],
197
+ });
198
+
199
+ const { tools } = await client.listTools();
200
+ const names = toolNames(tools);
201
+
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');
210
+ });
211
+
212
+ it('should provide a description and input schema for every tool', async () => {
213
+ const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
214
+
215
+ const { tools } = await client.listTools();
216
+
217
+ for (const tool of tools) {
218
+ expect(tool.description, `${tool.name} missing description`).toBeTruthy();
219
+ expect(tool.inputSchema, `${tool.name} missing inputSchema`).toBeDefined();
220
+ expect(tool.inputSchema.type).toBe('object');
221
+ }
222
+ });
223
+ });
224
+
225
+ // -------------------------------------------------------------------------
226
+ // Capability-tool contract -- parametric tests proving each capability
227
+ // maps to exactly the right set of tools. If someone adds a tool but
228
+ // forgets the capability check, these tests catch it.
229
+ // -------------------------------------------------------------------------
230
+
231
+ describe('capability-tool contract', () => {
232
+ const CONTRACT: Record<string, string[]> = {
233
+ 'projects:read': ['taiga_list_projects', 'taiga_get_project', 'taiga_list_environments'],
234
+ 'teams:read': ['taiga_list_teams'],
235
+ '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'],
238
+ };
239
+
240
+ for (const [capability, expectedTools] of Object.entries(CONTRACT)) {
241
+ it(`${capability} should gate: ${expectedTools.join(', ')}`, async () => {
242
+ const { client } = await bootMcpPair({ capabilities: [capability] });
243
+
244
+ const { tools } = await client.listTools();
245
+ const names = toolNames(tools);
246
+
247
+ for (const expected of expectedTools) {
248
+ expect(names, `Expected ${expected} for ${capability}`).toContain(expected);
249
+ }
250
+ });
251
+ }
252
+ });
253
+
254
+ // -------------------------------------------------------------------------
255
+ // Tool execution -- the AI assistant calls a tool and gets data back.
256
+ // These test the full round-trip: MCP call -> tool handler -> TaigaClient
257
+ // -> (mocked) API -> response back through MCP protocol.
258
+ // -------------------------------------------------------------------------
259
+
260
+ 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' },
265
+ ];
266
+
267
+ const { client } = await bootMcpPair({
268
+ capabilities: ALL_CAPABILITIES,
269
+ routes: { 'GET /v1/projects': { data: projects, nextCursor: null } },
270
+ });
271
+
272
+ const result = await client.callTool({ name: 'taiga_list_projects', arguments: {} });
273
+
274
+ expect(result.content).toHaveLength(1);
275
+ expect(parseToolResult(result)).toEqual(projects);
276
+ });
277
+
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' };
281
+
282
+ const { client } = await bootMcpPair({
283
+ capabilities: ALL_CAPABILITIES,
284
+ routes: { [`GET /v1/projects/${projectId}`]: { data: project } },
285
+ });
286
+
287
+ const result = await client.callTool({
288
+ name: 'taiga_get_project',
289
+ arguments: { id: projectId },
290
+ });
291
+
292
+ expect(parseToolResult(result)).toEqual(project);
293
+ });
294
+
295
+ it('should return document content through MCP protocol', async () => {
296
+ const docs = [{ id: 'd1', title: 'Security Policy' }];
297
+
298
+ const { client } = await bootMcpPair({
299
+ capabilities: ALL_CAPABILITIES,
300
+ routes: { 'GET /v1/tenants/current/documents': { data: docs, nextCursor: null } },
301
+ });
302
+
303
+ const result = await client.callTool({ name: 'taiga_list_documents', arguments: {} });
304
+
305
+ expect(parseToolResult(result)).toEqual(docs);
306
+ });
307
+
308
+ it('should send PUT when updating a document draft', async () => {
309
+ const docType = 'security_policy';
310
+ const updated = { type: docType, title: 'Updated Policy' };
311
+
312
+ const { client, fetchSpy } = await bootMcpPair({
313
+ capabilities: ALL_CAPABILITIES,
314
+ routes: { [`PUT /v1/tenants/current/documents/${docType}/draft`]: { data: updated } },
315
+ });
316
+
317
+ await client.callTool({
318
+ name: 'taiga_update_document',
319
+ arguments: { type: docType, data: { title: 'Updated' } },
320
+ });
321
+
322
+ const putCall = fetchSpy.mock.calls.find((c: unknown[]) => (c[1] as RequestInit)?.method === 'PUT');
323
+ expect(putCall).toBeDefined();
324
+ expect(putCall![0]).toContain(`/v1/tenants/current/documents/${docType}/draft`);
325
+ });
326
+
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' }];
330
+
331
+ const { client } = await bootMcpPair({
332
+ capabilities: ALL_CAPABILITIES,
333
+ routes: { [`GET /v1/projects/${projectId}/backlog`]: { data: items, nextCursor: null } },
334
+ });
335
+
336
+ const result = await client.callTool({
337
+ name: 'taiga_list_backlog_items',
338
+ arguments: { projectId },
339
+ });
340
+
341
+ expect(parseToolResult(result)).toEqual(items);
342
+ });
343
+
344
+ it('should return skills through MCP protocol', async () => {
345
+ const skills = [{ id: 's1', name: 'React Standards' }];
346
+
347
+ const { client } = await bootMcpPair({
348
+ capabilities: ALL_CAPABILITIES,
349
+ routes: { 'GET /v1/agent-skills': { data: skills, nextCursor: null } },
350
+ });
351
+
352
+ const result = await client.callTool({ name: 'taiga_list_skills', arguments: {} });
353
+
354
+ expect(parseToolResult(result)).toEqual(skills);
355
+ });
356
+
357
+ it('should return teams through MCP protocol', async () => {
358
+ const teams = [{ id: 't1', name: 'Engineering' }];
359
+
360
+ const { client } = await bootMcpPair({
361
+ capabilities: ALL_CAPABILITIES,
362
+ routes: { 'GET /v1/teams': { data: teams, nextCursor: null } },
363
+ });
364
+
365
+ const result = await client.callTool({ name: 'taiga_list_teams', arguments: {} });
366
+
367
+ expect(parseToolResult(result)).toEqual(teams);
368
+ });
369
+ });
370
+
371
+ // -------------------------------------------------------------------------
372
+ // Error handling -- when the API fails, the AI assistant should get a
373
+ // clear error, not a silent success or cryptic crash.
374
+ // -------------------------------------------------------------------------
375
+
376
+ describe('error handling', () => {
377
+ it('should surface API errors as MCP error content', async () => {
378
+ // Introspect succeeds, but all other endpoints fail
379
+ const fetchSpy = vi.fn().mockImplementation(async (url: string) => {
380
+ const path = new URL(url).pathname;
381
+ if (path === '/v1/auth/introspect') {
382
+ return {
383
+ ok: true,
384
+ status: 200,
385
+ json: () => Promise.resolve(introspectionResponse(ALL_CAPABILITIES)),
386
+ text: () => Promise.resolve(''),
387
+ };
388
+ }
389
+ return {
390
+ ok: false,
391
+ status: 500,
392
+ json: () => Promise.resolve({ error: 'Internal Server Error' }),
393
+ text: () => Promise.resolve('Internal Server Error'),
394
+ };
395
+ });
396
+ vi.stubGlobal('fetch', fetchSpy);
397
+
398
+ // Arrange
399
+ const taigaClient = new TaigaClient({ baseUrl: 'https://api.test.com', apiKey: 'test' });
400
+ await taigaClient.introspect();
401
+ const server = new McpServer({ name: 'test', version: '0.0.1' });
402
+ registerAllTools(server, taigaClient);
403
+ const mcpClient = new Client({ name: 'test-client', version: '0.0.1' });
404
+ const [ct, st] = InMemoryTransport.createLinkedPair();
405
+ await Promise.all([server.connect(st), mcpClient.connect(ct)]);
406
+
407
+ // Act
408
+ const result = await mcpClient.callTool({ name: 'taiga_list_projects', arguments: {} });
409
+
410
+ // Assert -- the MCP SDK marks tool errors with isError
411
+ expect(result.isError).toBe(true);
412
+ });
413
+
414
+ it('should reject tool calls with invalid parameters', async () => {
415
+ const { client } = await bootMcpPair({
416
+ capabilities: ALL_CAPABILITIES,
417
+ routes: {},
418
+ });
419
+
420
+ // taiga_get_project requires a UUID, not a plain string
421
+ const result = await client.callTool({
422
+ name: 'taiga_get_project',
423
+ arguments: { id: 'not-a-uuid' },
424
+ });
425
+
426
+ expect(result.isError).toBe(true);
427
+ });
428
+ });
429
+ });
@@ -0,0 +1,53 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import type { TaigaClient } from '../client.js';
4
+
5
+ export function registerBacklogTools(server: McpServer, client: TaigaClient) {
6
+ if (client.hasCapability('backlog:read')) {
7
+ server.tool(
8
+ 'taiga_list_backlog_items',
9
+ 'List backlog items (features, tasks, stories) for a project. ' +
10
+ 'Returns item name, status, planning status, and priority.',
11
+ {
12
+ projectId: z.string().uuid().describe('Project ID'),
13
+ limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
14
+ },
15
+ async (params) => {
16
+ const result = await client.listBacklogItems(params.projectId, { limit: params.limit });
17
+ return {
18
+ content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
19
+ };
20
+ }
21
+ );
22
+
23
+ server.tool(
24
+ 'taiga_get_backlog_item',
25
+ 'Get detailed information about a specific backlog item, including its active plan and steps.',
26
+ {
27
+ itemId: z.string().uuid().describe('Backlog item ID'),
28
+ },
29
+ async (params) => {
30
+ const result = await client.getBacklogItem(params.itemId);
31
+ return {
32
+ content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
33
+ };
34
+ }
35
+ );
36
+ }
37
+
38
+ if (client.hasCapability('projects:read')) {
39
+ server.tool(
40
+ 'taiga_list_environments',
41
+ 'List deployment environments (dev, staging, production) for a project.',
42
+ {
43
+ projectId: z.string().uuid().describe('Project ID'),
44
+ },
45
+ async (params) => {
46
+ const result = await client.listEnvironments(params.projectId);
47
+ return {
48
+ content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
49
+ };
50
+ }
51
+ );
52
+ }
53
+ }
@@ -0,0 +1,55 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import type { TaigaClient } from '../client.js';
4
+
5
+ export function registerDocumentTools(server: McpServer, client: TaigaClient) {
6
+ if (client.hasCapability('tenant:documents:read')) {
7
+ server.tool(
8
+ 'taiga_list_documents',
9
+ 'List company-level policy documents (security, data privacy, SDLC, etc). ' +
10
+ 'These are the governance documents that guide all projects.',
11
+ {
12
+ limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
13
+ },
14
+ async (params) => {
15
+ const result = await client.listTenantDocuments(params);
16
+ return {
17
+ content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
18
+ };
19
+ }
20
+ );
21
+
22
+ server.tool(
23
+ 'taiga_get_document',
24
+ 'Get the full content of a specific policy document by type. ' +
25
+ 'Common types: security_policy, data_privacy_policy, sdlc_policy, architecture_standards.',
26
+ {
27
+ type: z.string().describe('Document type (e.g. security_policy, sdlc_policy)'),
28
+ },
29
+ async (params) => {
30
+ const result = await client.getTenantDocument(params.type);
31
+ return {
32
+ content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
33
+ };
34
+ }
35
+ );
36
+ }
37
+
38
+ if (client.hasCapability('tenant:documents:write')) {
39
+ server.tool(
40
+ 'taiga_update_document',
41
+ 'Update the draft content of a policy document. Only updates the draft -- does not publish.',
42
+ {
43
+ type: z.string().describe('Document type (e.g. security_policy)'),
44
+ data: z.record(z.string(), z.unknown()).describe('Document content to save'),
45
+ },
46
+ async (params) => {
47
+ const { type, ...body } = params;
48
+ const result = await client.saveTenantDocumentDraft(type, body as { data: Record<string, unknown> });
49
+ return {
50
+ content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
51
+ };
52
+ }
53
+ );
54
+ }
55
+ }
@@ -0,0 +1,18 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { TaigaClient } from '../client.js';
3
+ import { registerProjectTools } from './projects.js';
4
+ import { registerDocumentTools } from './documents.js';
5
+ import { registerBacklogTools } from './backlogs.js';
6
+ import { registerSkillTools } from './skills.js';
7
+
8
+ /**
9
+ * Register all MCP tools based on the API key's effective capabilities.
10
+ * Only tools matching the key's scopes are registered -- the AI assistant
11
+ * never sees tools it can't use.
12
+ */
13
+ export function registerAllTools(server: McpServer, client: TaigaClient): void {
14
+ registerProjectTools(server, client);
15
+ registerDocumentTools(server, client);
16
+ registerBacklogTools(server, client);
17
+ registerSkillTools(server, client);
18
+ }
@@ -0,0 +1,53 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import type { TaigaClient } from '../client.js';
4
+
5
+ export function registerProjectTools(server: McpServer, client: TaigaClient) {
6
+ if (client.hasCapability('projects:read')) {
7
+ server.tool(
8
+ 'taiga_list_projects',
9
+ 'List all projects in the organization. Returns project name, status, team, and metadata.',
10
+ {
11
+ teamId: z.string().uuid().optional().describe('Filter by team ID'),
12
+ status: z.enum(['draft', 'active', 'archived']).optional().describe('Filter by status'),
13
+ limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
14
+ },
15
+ async (params) => {
16
+ const result = await client.listProjects(params);
17
+ return {
18
+ content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
19
+ };
20
+ }
21
+ );
22
+
23
+ server.tool(
24
+ 'taiga_get_project',
25
+ 'Get detailed information about a specific project by ID.',
26
+ {
27
+ id: z.string().uuid().describe('Project ID'),
28
+ },
29
+ async (params) => {
30
+ const result = await client.getProject(params.id);
31
+ return {
32
+ content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
33
+ };
34
+ }
35
+ );
36
+ }
37
+
38
+ if (client.hasCapability('teams:read')) {
39
+ server.tool(
40
+ 'taiga_list_teams',
41
+ 'List all teams in the organization.',
42
+ {
43
+ limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
44
+ },
45
+ async (params) => {
46
+ const result = await client.listTeams(params);
47
+ return {
48
+ content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
49
+ };
50
+ }
51
+ );
52
+ }
53
+ }
@@ -0,0 +1,39 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import type { TaigaClient } from '../client.js';
4
+
5
+ export function registerSkillTools(server: McpServer, client: TaigaClient) {
6
+ // Skills require tenant:documents:read as they're part of the knowledge base
7
+ if (client.hasCapability('tenant:documents:read')) {
8
+ server.tool(
9
+ 'taiga_list_skills',
10
+ 'List agent skills available in the organization. ' +
11
+ 'Skills are reusable knowledge modules that guide AI agents (coding standards, ' +
12
+ 'architecture patterns, deployment procedures, etc).',
13
+ {
14
+ limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
15
+ },
16
+ async (params) => {
17
+ const result = await client.listSkills(params);
18
+ return {
19
+ content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
20
+ };
21
+ }
22
+ );
23
+
24
+ server.tool(
25
+ 'taiga_get_skill',
26
+ 'Get the full content of a specific agent skill by ID. ' +
27
+ 'Returns the skill definition including instructions, context, and metadata.',
28
+ {
29
+ id: z.string().uuid().describe('Skill ID'),
30
+ },
31
+ async (params) => {
32
+ const result = await client.getSkill(params.id);
33
+ return {
34
+ content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
35
+ };
36
+ }
37
+ );
38
+ }
39
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "esModuleInterop": true,
13
+ "resolveJsonModule": true
14
+ },
15
+ "include": ["src"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['src/**/*.test.ts'],
6
+ environment: 'node',
7
+ testTimeout: 10_000,
8
+ },
9
+ });