@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,348 @@
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
+ // Helpers -- thin wrappers, not abstraction layers
9
+ // ---------------------------------------------------------------------------
10
+ /** All capabilities the MCP server understands. */
11
+ const ALL_CAPABILITIES = [
12
+ 'projects:read',
13
+ 'teams:read',
14
+ 'tenant:documents:read',
15
+ 'tenant:documents:write',
16
+ 'backlog:read',
17
+ ];
18
+ /** All tool names the server can register (sorted for deterministic comparison). */
19
+ const ALL_TOOL_NAMES = [
20
+ 'taiga_get_backlog_item',
21
+ 'taiga_get_document',
22
+ 'taiga_get_project',
23
+ 'taiga_get_skill',
24
+ 'taiga_list_backlog_items',
25
+ 'taiga_list_documents',
26
+ 'taiga_list_environments',
27
+ 'taiga_list_projects',
28
+ 'taiga_list_skills',
29
+ 'taiga_list_teams',
30
+ 'taiga_update_document',
31
+ ];
32
+ function makeFetchRouter(routes) {
33
+ return vi.fn().mockImplementation(async (url, init) => {
34
+ const path = new URL(url).pathname;
35
+ const method = init?.method ?? 'GET';
36
+ const key = `${method} ${path}`;
37
+ const body = routes[key] ?? routes[path];
38
+ if (body !== undefined) {
39
+ return {
40
+ ok: true,
41
+ status: 200,
42
+ json: () => Promise.resolve(body),
43
+ text: () => Promise.resolve(JSON.stringify(body)),
44
+ };
45
+ }
46
+ return {
47
+ ok: false,
48
+ status: 404,
49
+ json: () => Promise.resolve({ error: 'Not found' }),
50
+ text: () => Promise.resolve('Not found'),
51
+ };
52
+ });
53
+ }
54
+ function introspectionResponse(capabilities) {
55
+ return {
56
+ data: {
57
+ userId: 'user-1',
58
+ tenantId: 'tenant-1',
59
+ email: 'dev@test.com',
60
+ role: 'member',
61
+ capabilities,
62
+ },
63
+ };
64
+ }
65
+ /**
66
+ * Boots a full MCP Client <-> Server pair connected in-memory.
67
+ * The TaigaClient uses a route-based fetch mock, so no real HTTP is needed.
68
+ * Returns both the MCP client (for protocol calls) and the fetch spy (for
69
+ * verifying outgoing HTTP requests when needed).
70
+ */
71
+ async function bootMcpPair(opts) {
72
+ const allRoutes = {
73
+ '/v1/auth/introspect': introspectionResponse(opts.capabilities),
74
+ ...opts.routes,
75
+ };
76
+ const fetchSpy = makeFetchRouter(allRoutes);
77
+ vi.stubGlobal('fetch', fetchSpy);
78
+ // Arrange -- Taiga client, introspect, register tools
79
+ const taigaClient = new TaigaClient({
80
+ baseUrl: 'https://api.test.com',
81
+ apiKey: 'taiga_sk_live_test',
82
+ });
83
+ await taigaClient.introspect();
84
+ const server = new McpServer({ name: 'taiga-test', version: '0.0.1' });
85
+ registerAllTools(server, taigaClient);
86
+ const client = new Client({ name: 'test-client', version: '0.0.1' });
87
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
88
+ await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
89
+ return { client, fetchSpy };
90
+ }
91
+ /** Extract sorted tool names from a listTools response. */
92
+ function toolNames(tools) {
93
+ return tools.map((t) => t.name).sort();
94
+ }
95
+ /** Extract parsed JSON from the first text content block of a tool call result. */
96
+ function parseToolResult(result) {
97
+ const content = result.content;
98
+ return JSON.parse(content[0].text);
99
+ }
100
+ // ---------------------------------------------------------------------------
101
+ // Tests
102
+ // ---------------------------------------------------------------------------
103
+ describe('MCP server integration', () => {
104
+ beforeEach(() => {
105
+ vi.restoreAllMocks();
106
+ });
107
+ // -------------------------------------------------------------------------
108
+ // Tool discovery -- the AI assistant asks "what tools do you have?"
109
+ // These tests verify the server exposes exactly the right tools based on
110
+ // the API key's capabilities. This is the primary security contract.
111
+ // -------------------------------------------------------------------------
112
+ describe('tool discovery', () => {
113
+ it('should expose all 11 tools when API key has full capabilities', async () => {
114
+ const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
115
+ const { tools } = await client.listTools();
116
+ expect(toolNames(tools)).toEqual(ALL_TOOL_NAMES);
117
+ });
118
+ it('should reject tools/list when API key has no capabilities', async () => {
119
+ // When zero tools are registered, the MCP SDK doesn't advertise
120
+ // the tools capability at all -- the protocol-correct response
121
+ // is "Method not found".
122
+ const { client } = await bootMcpPair({ capabilities: [] });
123
+ await expect(client.listTools()).rejects.toThrow(/Method not found/);
124
+ });
125
+ it('should expose only project tools for a projects:read-only key', async () => {
126
+ const { client } = await bootMcpPair({ capabilities: ['projects:read'] });
127
+ const { tools } = await client.listTools();
128
+ const names = toolNames(tools);
129
+ expect(names).toContain('taiga_list_projects');
130
+ expect(names).toContain('taiga_get_project');
131
+ expect(names).toContain('taiga_list_environments');
132
+ expect(names).not.toContain('taiga_list_teams');
133
+ expect(names).not.toContain('taiga_list_documents');
134
+ });
135
+ it('should expose read-only document tools without write capability', async () => {
136
+ const { client } = await bootMcpPair({ capabilities: ['tenant:documents:read'] });
137
+ const { tools } = await client.listTools();
138
+ const names = toolNames(tools);
139
+ expect(names).toContain('taiga_list_documents');
140
+ expect(names).toContain('taiga_get_document');
141
+ expect(names).toContain('taiga_list_skills');
142
+ expect(names).toContain('taiga_get_skill');
143
+ expect(names).not.toContain('taiga_update_document');
144
+ });
145
+ it('should expose update tool when documents:write is also granted', async () => {
146
+ const { client } = await bootMcpPair({
147
+ capabilities: ['tenant:documents:read', 'tenant:documents:write'],
148
+ });
149
+ const { tools } = await client.listTools();
150
+ expect(toolNames(tools)).toContain('taiga_update_document');
151
+ });
152
+ it('should expose backlog tools only with backlog:read', async () => {
153
+ const { client } = await bootMcpPair({ capabilities: ['backlog:read'] });
154
+ const { tools } = await client.listTools();
155
+ const names = toolNames(tools);
156
+ expect(names).toContain('taiga_list_backlog_items');
157
+ expect(names).toContain('taiga_get_backlog_item');
158
+ expect(names).not.toContain('taiga_list_environments');
159
+ });
160
+ it('should combine capabilities additively', async () => {
161
+ const { client } = await bootMcpPair({
162
+ capabilities: ['projects:read', 'backlog:read'],
163
+ });
164
+ const { tools } = await client.listTools();
165
+ const names = toolNames(tools);
166
+ // projects:read tools present
167
+ expect(names).toContain('taiga_list_projects');
168
+ expect(names).toContain('taiga_list_environments');
169
+ // backlog:read tools present
170
+ expect(names).toContain('taiga_list_backlog_items');
171
+ // unrelated capabilities absent
172
+ expect(names).not.toContain('taiga_list_documents');
173
+ expect(names).not.toContain('taiga_list_teams');
174
+ });
175
+ it('should provide a description and input schema for every tool', async () => {
176
+ const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
177
+ const { tools } = await client.listTools();
178
+ for (const tool of tools) {
179
+ expect(tool.description, `${tool.name} missing description`).toBeTruthy();
180
+ expect(tool.inputSchema, `${tool.name} missing inputSchema`).toBeDefined();
181
+ expect(tool.inputSchema.type).toBe('object');
182
+ }
183
+ });
184
+ });
185
+ // -------------------------------------------------------------------------
186
+ // Capability-tool contract -- parametric tests proving each capability
187
+ // maps to exactly the right set of tools. If someone adds a tool but
188
+ // forgets the capability check, these tests catch it.
189
+ // -------------------------------------------------------------------------
190
+ describe('capability-tool contract', () => {
191
+ const CONTRACT = {
192
+ 'projects:read': ['taiga_list_projects', 'taiga_get_project', 'taiga_list_environments'],
193
+ 'teams:read': ['taiga_list_teams'],
194
+ 'tenant:documents:read': ['taiga_list_documents', 'taiga_get_document', 'taiga_list_skills', 'taiga_get_skill'],
195
+ 'tenant:documents:write': ['taiga_update_document'],
196
+ 'backlog:read': ['taiga_list_backlog_items', 'taiga_get_backlog_item'],
197
+ };
198
+ for (const [capability, expectedTools] of Object.entries(CONTRACT)) {
199
+ it(`${capability} should gate: ${expectedTools.join(', ')}`, async () => {
200
+ const { client } = await bootMcpPair({ capabilities: [capability] });
201
+ const { tools } = await client.listTools();
202
+ const names = toolNames(tools);
203
+ for (const expected of expectedTools) {
204
+ expect(names, `Expected ${expected} for ${capability}`).toContain(expected);
205
+ }
206
+ });
207
+ }
208
+ });
209
+ // -------------------------------------------------------------------------
210
+ // Tool execution -- the AI assistant calls a tool and gets data back.
211
+ // These test the full round-trip: MCP call -> tool handler -> TaigaClient
212
+ // -> (mocked) API -> response back through MCP protocol.
213
+ // -------------------------------------------------------------------------
214
+ describe('tool execution', () => {
215
+ it('should return project list as JSON text content', async () => {
216
+ const projects = [
217
+ { id: 'p1', name: 'Alpha' },
218
+ { id: 'p2', name: 'Beta' },
219
+ ];
220
+ const { client } = await bootMcpPair({
221
+ capabilities: ALL_CAPABILITIES,
222
+ routes: { 'GET /v1/projects': { data: projects, nextCursor: null } },
223
+ });
224
+ const result = await client.callTool({ name: 'taiga_list_projects', arguments: {} });
225
+ expect(result.content).toHaveLength(1);
226
+ expect(parseToolResult(result)).toEqual(projects);
227
+ });
228
+ it('should pass project id through to the API', async () => {
229
+ const projectId = '00000000-0000-0000-0000-000000000001';
230
+ const project = { id: projectId, name: 'Alpha', status: 'active' };
231
+ const { client } = await bootMcpPair({
232
+ capabilities: ALL_CAPABILITIES,
233
+ routes: { [`GET /v1/projects/${projectId}`]: { data: project } },
234
+ });
235
+ const result = await client.callTool({
236
+ name: 'taiga_get_project',
237
+ arguments: { id: projectId },
238
+ });
239
+ expect(parseToolResult(result)).toEqual(project);
240
+ });
241
+ it('should return document content through MCP protocol', async () => {
242
+ const docs = [{ id: 'd1', title: 'Security Policy' }];
243
+ const { client } = await bootMcpPair({
244
+ capabilities: ALL_CAPABILITIES,
245
+ routes: { 'GET /v1/tenants/current/documents': { data: docs, nextCursor: null } },
246
+ });
247
+ const result = await client.callTool({ name: 'taiga_list_documents', arguments: {} });
248
+ expect(parseToolResult(result)).toEqual(docs);
249
+ });
250
+ it('should send PUT when updating a document draft', async () => {
251
+ const docType = 'security_policy';
252
+ const updated = { type: docType, title: 'Updated Policy' };
253
+ const { client, fetchSpy } = await bootMcpPair({
254
+ capabilities: ALL_CAPABILITIES,
255
+ routes: { [`PUT /v1/tenants/current/documents/${docType}/draft`]: { data: updated } },
256
+ });
257
+ await client.callTool({
258
+ name: 'taiga_update_document',
259
+ arguments: { type: docType, data: { title: 'Updated' } },
260
+ });
261
+ const putCall = fetchSpy.mock.calls.find((c) => c[1]?.method === 'PUT');
262
+ expect(putCall).toBeDefined();
263
+ expect(putCall[0]).toContain(`/v1/tenants/current/documents/${docType}/draft`);
264
+ });
265
+ it('should pass projectId to backlog items endpoint', async () => {
266
+ const projectId = '00000000-0000-0000-0000-000000000001';
267
+ const items = [{ id: 'bi1', name: 'Feature X' }];
268
+ const { client } = await bootMcpPair({
269
+ capabilities: ALL_CAPABILITIES,
270
+ routes: { [`GET /v1/projects/${projectId}/backlog`]: { data: items, nextCursor: null } },
271
+ });
272
+ const result = await client.callTool({
273
+ name: 'taiga_list_backlog_items',
274
+ arguments: { projectId },
275
+ });
276
+ expect(parseToolResult(result)).toEqual(items);
277
+ });
278
+ it('should return skills through MCP protocol', async () => {
279
+ const skills = [{ id: 's1', name: 'React Standards' }];
280
+ const { client } = await bootMcpPair({
281
+ capabilities: ALL_CAPABILITIES,
282
+ routes: { 'GET /v1/agent-skills': { data: skills, nextCursor: null } },
283
+ });
284
+ const result = await client.callTool({ name: 'taiga_list_skills', arguments: {} });
285
+ expect(parseToolResult(result)).toEqual(skills);
286
+ });
287
+ it('should return teams through MCP protocol', async () => {
288
+ const teams = [{ id: 't1', name: 'Engineering' }];
289
+ const { client } = await bootMcpPair({
290
+ capabilities: ALL_CAPABILITIES,
291
+ routes: { 'GET /v1/teams': { data: teams, nextCursor: null } },
292
+ });
293
+ const result = await client.callTool({ name: 'taiga_list_teams', arguments: {} });
294
+ expect(parseToolResult(result)).toEqual(teams);
295
+ });
296
+ });
297
+ // -------------------------------------------------------------------------
298
+ // Error handling -- when the API fails, the AI assistant should get a
299
+ // clear error, not a silent success or cryptic crash.
300
+ // -------------------------------------------------------------------------
301
+ describe('error handling', () => {
302
+ it('should surface API errors as MCP error content', async () => {
303
+ // Introspect succeeds, but all other endpoints fail
304
+ const fetchSpy = vi.fn().mockImplementation(async (url) => {
305
+ const path = new URL(url).pathname;
306
+ if (path === '/v1/auth/introspect') {
307
+ return {
308
+ ok: true,
309
+ status: 200,
310
+ json: () => Promise.resolve(introspectionResponse(ALL_CAPABILITIES)),
311
+ text: () => Promise.resolve(''),
312
+ };
313
+ }
314
+ return {
315
+ ok: false,
316
+ status: 500,
317
+ json: () => Promise.resolve({ error: 'Internal Server Error' }),
318
+ text: () => Promise.resolve('Internal Server Error'),
319
+ };
320
+ });
321
+ vi.stubGlobal('fetch', fetchSpy);
322
+ // Arrange
323
+ const taigaClient = new TaigaClient({ baseUrl: 'https://api.test.com', apiKey: 'test' });
324
+ await taigaClient.introspect();
325
+ const server = new McpServer({ name: 'test', version: '0.0.1' });
326
+ registerAllTools(server, taigaClient);
327
+ const mcpClient = new Client({ name: 'test-client', version: '0.0.1' });
328
+ const [ct, st] = InMemoryTransport.createLinkedPair();
329
+ await Promise.all([server.connect(st), mcpClient.connect(ct)]);
330
+ // Act
331
+ const result = await mcpClient.callTool({ name: 'taiga_list_projects', arguments: {} });
332
+ // Assert -- the MCP SDK marks tool errors with isError
333
+ expect(result.isError).toBe(true);
334
+ });
335
+ it('should reject tool calls with invalid parameters', async () => {
336
+ const { client } = await bootMcpPair({
337
+ capabilities: ALL_CAPABILITIES,
338
+ routes: {},
339
+ });
340
+ // taiga_get_project requires a UUID, not a plain string
341
+ const result = await client.callTool({
342
+ name: 'taiga_get_project',
343
+ arguments: { id: 'not-a-uuid' },
344
+ });
345
+ expect(result.isError).toBe(true);
346
+ });
347
+ });
348
+ });
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { TaigaClient } from '../client.js';
3
+ export declare function registerBacklogTools(server: McpServer, client: TaigaClient): void;
4
+ //# sourceMappingURL=backlogs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backlogs.d.ts","sourceRoot":"","sources":["../../src/tools/backlogs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,QAgD1E"}
@@ -0,0 +1,33 @@
1
+ import { z } from 'zod';
2
+ export function registerBacklogTools(server, client) {
3
+ if (client.hasCapability('backlog:read')) {
4
+ server.tool('taiga_list_backlog_items', 'List backlog items (features, tasks, stories) for a project. ' +
5
+ 'Returns item name, status, planning status, and priority.', {
6
+ projectId: z.string().uuid().describe('Project ID'),
7
+ limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
8
+ }, async (params) => {
9
+ const result = await client.listBacklogItems(params.projectId, { limit: params.limit });
10
+ return {
11
+ content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
12
+ };
13
+ });
14
+ server.tool('taiga_get_backlog_item', 'Get detailed information about a specific backlog item, including its active plan and steps.', {
15
+ itemId: z.string().uuid().describe('Backlog item ID'),
16
+ }, async (params) => {
17
+ const result = await client.getBacklogItem(params.itemId);
18
+ return {
19
+ content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
20
+ };
21
+ });
22
+ }
23
+ if (client.hasCapability('projects:read')) {
24
+ server.tool('taiga_list_environments', 'List deployment environments (dev, staging, production) for a project.', {
25
+ projectId: z.string().uuid().describe('Project ID'),
26
+ }, async (params) => {
27
+ const result = await client.listEnvironments(params.projectId);
28
+ return {
29
+ content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
30
+ };
31
+ });
32
+ }
33
+ }
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { TaigaClient } from '../client.js';
3
+ export declare function registerDocumentTools(server: McpServer, client: TaigaClient): void;
4
+ //# sourceMappingURL=documents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../src/tools/documents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,QAkD3E"}
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ export function registerDocumentTools(server, client) {
3
+ if (client.hasCapability('tenant:documents:read')) {
4
+ server.tool('taiga_list_documents', 'List company-level policy documents (security, data privacy, SDLC, etc). ' +
5
+ 'These are the governance documents that guide all projects.', {
6
+ limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
7
+ }, async (params) => {
8
+ const result = await client.listTenantDocuments(params);
9
+ return {
10
+ content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
11
+ };
12
+ });
13
+ server.tool('taiga_get_document', 'Get the full content of a specific policy document by type. ' +
14
+ 'Common types: security_policy, data_privacy_policy, sdlc_policy, architecture_standards.', {
15
+ type: z.string().describe('Document type (e.g. security_policy, sdlc_policy)'),
16
+ }, async (params) => {
17
+ const result = await client.getTenantDocument(params.type);
18
+ return {
19
+ content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
20
+ };
21
+ });
22
+ }
23
+ if (client.hasCapability('tenant:documents:write')) {
24
+ server.tool('taiga_update_document', 'Update the draft content of a policy document. Only updates the draft -- does not publish.', {
25
+ type: z.string().describe('Document type (e.g. security_policy)'),
26
+ data: z.record(z.string(), z.unknown()).describe('Document content to save'),
27
+ }, async (params) => {
28
+ const { type, ...body } = params;
29
+ const result = await client.saveTenantDocumentDraft(type, body);
30
+ return {
31
+ content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
32
+ };
33
+ });
34
+ }
35
+ }
@@ -0,0 +1,9 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { TaigaClient } from '../client.js';
3
+ /**
4
+ * Register all MCP tools based on the API key's effective capabilities.
5
+ * Only tools matching the key's scopes are registered -- the AI assistant
6
+ * never sees tools it can't use.
7
+ */
8
+ export declare function registerAllTools(server: McpServer, client: TaigaClient): void;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAMhD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAK7E"}
@@ -0,0 +1,15 @@
1
+ import { registerProjectTools } from './projects.js';
2
+ import { registerDocumentTools } from './documents.js';
3
+ import { registerBacklogTools } from './backlogs.js';
4
+ import { registerSkillTools } from './skills.js';
5
+ /**
6
+ * Register all MCP tools based on the API key's effective capabilities.
7
+ * Only tools matching the key's scopes are registered -- the AI assistant
8
+ * never sees tools it can't use.
9
+ */
10
+ export function registerAllTools(server, client) {
11
+ registerProjectTools(server, client);
12
+ registerDocumentTools(server, client);
13
+ registerBacklogTools(server, client);
14
+ registerSkillTools(server, client);
15
+ }
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { TaigaClient } from '../client.js';
3
+ export declare function registerProjectTools(server: McpServer, client: TaigaClient): void;
4
+ //# sourceMappingURL=projects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/tools/projects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,QAgD1E"}
@@ -0,0 +1,33 @@
1
+ import { z } from 'zod';
2
+ 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'),
7
+ limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
8
+ }, async (params) => {
9
+ const result = await client.listProjects(params);
10
+ return {
11
+ content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
12
+ };
13
+ });
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);
18
+ return {
19
+ content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
20
+ };
21
+ });
22
+ }
23
+ if (client.hasCapability('teams:read')) {
24
+ server.tool('taiga_list_teams', 'List all teams in the organization.', {
25
+ limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
26
+ }, async (params) => {
27
+ const result = await client.listTeams(params);
28
+ return {
29
+ content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
30
+ };
31
+ });
32
+ }
33
+ }
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { TaigaClient } from '../client.js';
3
+ export declare function registerSkillTools(server: McpServer, client: TaigaClient): void;
4
+ //# sourceMappingURL=skills.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+ export function registerSkillTools(server, client) {
3
+ // Skills require tenant:documents:read as they're part of the knowledge base
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).', {
8
+ limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
9
+ }, async (params) => {
10
+ const result = await client.listSkills(params);
11
+ return {
12
+ content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
13
+ };
14
+ });
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) => {
19
+ const result = await client.getSkill(params.id);
20
+ return {
21
+ content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
22
+ };
23
+ });
24
+ }
25
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@taiga-ai/mcp-server",
3
+ "version": "0.0.1-dev.51",
4
+ "description": "MCP server for Taiga -- gives AI assistants access to documents, skills, and projects",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "taiga-mcp-server": "./dist/index.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "dev": "tsc --watch",
21
+ "lint": "eslint src/",
22
+ "test": "vitest run"
23
+ },
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.12.1",
26
+ "zod": "^3.24.4"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.15.3",
30
+ "typescript": "^5.7.2",
31
+ "vitest": "^4.0.16"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Prepare package.json for npm publish.
5
+ *
6
+ * - Removes `private: true` so npm allows publishing
7
+ * - Sets publishConfig to public access
8
+ * - Strips workspace-only dependencies (@taiga/types is compile-time only)
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ const pkgPath = path.join(__dirname, '..', 'package.json');
15
+
16
+ try {
17
+ const raw = fs.readFileSync(pkgPath, 'utf8');
18
+ const pkg = JSON.parse(raw);
19
+
20
+ delete pkg.private;
21
+ pkg.publishConfig = { access: 'public' };
22
+
23
+ if (pkg.dependencies?.['@taiga/types']) {
24
+ delete pkg.dependencies['@taiga/types'];
25
+ }
26
+
27
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
28
+ console.log('prepare-publish: package.json updated');
29
+ } catch (err) {
30
+ console.error('prepare-publish: failed to update package.json');
31
+ console.error(err.stack || err);
32
+ process.exit(1);
33
+ }