@taiga-ai/mcp-server 0.2.3 → 0.3.0-dev.296
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +419 -62
- package/package.json +13 -11
- package/scripts/build.mjs +37 -0
- package/scripts/prepare-publish.cjs +15 -7
- package/src/client.test.ts +12 -27
- package/src/client.ts +19 -43
- package/src/index.ts +5 -8
- package/src/server.integration.test.ts +13 -50
- package/src/tools/backlogs.ts +1 -1
- package/src/tools/documents.ts +1 -1
- package/src/tools/index.ts +1 -3
- package/src/tools/projects.ts +7 -3
- package/src/version.test.ts +10 -0
- package/src/version.ts +14 -0
- package/taiga-ai-mcp-server-0.3.0-dev.296.tgz +0 -0
- package/tsconfig.json +2 -1
- package/dist/client.d.ts +0 -109
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -228
- package/dist/client.test.d.ts +0 -2
- package/dist/client.test.d.ts.map +0 -1
- package/dist/client.test.js +0 -203
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/server.integration.test.d.ts +0 -2
- package/dist/server.integration.test.d.ts.map +0 -1
- package/dist/server.integration.test.js +0 -358
- package/dist/tools/backlogs.d.ts +0 -4
- package/dist/tools/backlogs.d.ts.map +0 -1
- package/dist/tools/backlogs.js +0 -6
- package/dist/tools/documents.d.ts +0 -4
- package/dist/tools/documents.d.ts.map +0 -1
- package/dist/tools/documents.js +0 -97
- package/dist/tools/index.d.ts +0 -12
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -18
- package/dist/tools/projects.d.ts +0 -4
- package/dist/tools/projects.d.ts.map +0 -1
- package/dist/tools/projects.js +0 -64
- package/dist/tools/skills.d.ts +0 -4
- package/dist/tools/skills.d.ts.map +0 -1
- package/dist/tools/skills.js +0 -45
- package/dist/tools/summarize.d.ts +0 -15
- package/dist/tools/summarize.d.ts.map +0 -1
- package/dist/tools/summarize.js +0 -26
- package/src/tools/skills.ts +0 -68
- package/taiga-ai-mcp-server-0.2.3.tgz +0 -0
package/src/client.test.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { TaigaClient } from './client.js';
|
|
3
4
|
|
|
5
|
+
// Read independently of src/version.ts, so the test checks the real file.
|
|
6
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string };
|
|
7
|
+
|
|
4
8
|
// ---------------------------------------------------------------------------
|
|
5
9
|
// Helpers -- thin wrappers to reduce boilerplate, not to obscure intent
|
|
6
10
|
// ---------------------------------------------------------------------------
|
|
@@ -62,7 +66,7 @@ describe('TaigaClient', () => {
|
|
|
62
66
|
|
|
63
67
|
await makeClient().listProjects();
|
|
64
68
|
|
|
65
|
-
expect(spy.mock.calls[0][1].headers['User-Agent']).
|
|
69
|
+
expect(spy.mock.calls[0][1].headers['User-Agent']).toBe(`taiga-ai-mcp-server/${pkg.version}`);
|
|
66
70
|
});
|
|
67
71
|
});
|
|
68
72
|
|
|
@@ -109,7 +113,7 @@ describe('TaigaClient', () => {
|
|
|
109
113
|
|
|
110
114
|
await client.listProjects();
|
|
111
115
|
|
|
112
|
-
expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/
|
|
116
|
+
expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/products');
|
|
113
117
|
});
|
|
114
118
|
|
|
115
119
|
it('should omit query string when no optional params are provided', async () => {
|
|
@@ -117,7 +121,7 @@ describe('TaigaClient', () => {
|
|
|
117
121
|
|
|
118
122
|
await makeClient().listProjects();
|
|
119
123
|
|
|
120
|
-
expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/
|
|
124
|
+
expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/products');
|
|
121
125
|
});
|
|
122
126
|
|
|
123
127
|
it('should include all optional filters as query params', async () => {
|
|
@@ -126,7 +130,7 @@ describe('TaigaClient', () => {
|
|
|
126
130
|
await makeClient().listProjects({ teamId: 't1', status: 'active', limit: 10 });
|
|
127
131
|
|
|
128
132
|
const url = spy.mock.calls[0][0] as string;
|
|
129
|
-
expect(url).toContain('
|
|
133
|
+
expect(url).toContain('factoryId=t1');
|
|
130
134
|
expect(url).toContain('status=active');
|
|
131
135
|
expect(url).toContain('limit=10');
|
|
132
136
|
});
|
|
@@ -137,7 +141,7 @@ describe('TaigaClient', () => {
|
|
|
137
141
|
await makeClient().listBacklogItems('proj-1', { limit: 20 });
|
|
138
142
|
|
|
139
143
|
const url = spy.mock.calls[0][0] as string;
|
|
140
|
-
expect(url).toContain('/v1/
|
|
144
|
+
expect(url).toContain('/v1/products/proj-1/backlog');
|
|
141
145
|
expect(url).toContain('limit=20');
|
|
142
146
|
});
|
|
143
147
|
|
|
@@ -146,7 +150,7 @@ describe('TaigaClient', () => {
|
|
|
146
150
|
|
|
147
151
|
await makeClient().listEnvironments('proj-1');
|
|
148
152
|
|
|
149
|
-
expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/
|
|
153
|
+
expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/products/proj-1/environments');
|
|
150
154
|
});
|
|
151
155
|
});
|
|
152
156
|
|
|
@@ -207,7 +211,7 @@ describe('TaigaClient', () => {
|
|
|
207
211
|
|
|
208
212
|
const result = await makeClient().getProject('p1');
|
|
209
213
|
|
|
210
|
-
expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/
|
|
214
|
+
expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/products/p1');
|
|
211
215
|
expect(result.data).toEqual({ id: 'p1', name: 'Alpha' });
|
|
212
216
|
});
|
|
213
217
|
|
|
@@ -231,25 +235,6 @@ describe('TaigaClient', () => {
|
|
|
231
235
|
});
|
|
232
236
|
});
|
|
233
237
|
|
|
234
|
-
it('should GET a single skill by id from agent-skills path', async () => {
|
|
235
|
-
const spy = stubFetch(200, { data: { id: 's1' } });
|
|
236
|
-
|
|
237
|
-
await makeClient().getSkill('s1');
|
|
238
|
-
|
|
239
|
-
expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/agent-skills/s1');
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('should include scope filters when listing skills', async () => {
|
|
243
|
-
const spy = stubFetch(200, { data: [], nextCursor: null });
|
|
244
|
-
|
|
245
|
-
await makeClient().listSkills({ scope: 'team', scopeId: 't1', limit: 10 });
|
|
246
|
-
|
|
247
|
-
const url = spy.mock.calls[0][0] as string;
|
|
248
|
-
expect(url).toContain('/v1/agent-skills');
|
|
249
|
-
expect(url).toContain('scope=team');
|
|
250
|
-
expect(url).toContain('scopeId=t1');
|
|
251
|
-
});
|
|
252
|
-
|
|
253
238
|
it('should GET a single backlog item by id (flat path)', async () => {
|
|
254
239
|
const spy = stubFetch(200, { data: { id: 'item-1' } });
|
|
255
240
|
|
|
@@ -263,7 +248,7 @@ describe('TaigaClient', () => {
|
|
|
263
248
|
|
|
264
249
|
await makeClient().listTeams({ limit: 50 });
|
|
265
250
|
|
|
266
|
-
expect(spy.mock.calls[0][0]).toContain('/v1/
|
|
251
|
+
expect(spy.mock.calls[0][0]).toContain('/v1/factories?limit=50');
|
|
267
252
|
});
|
|
268
253
|
|
|
269
254
|
it('should GET documents from tenant path with optional limit', async () => {
|
package/src/client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ApiKeyIntrospection } from '@taiga/types';
|
|
2
|
+
import { SERVER_VERSION } from './version.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* HTTP client for the Taiga API.
|
|
@@ -19,7 +20,7 @@ export class TaigaClient {
|
|
|
19
20
|
const headers: Record<string, string> = {
|
|
20
21
|
Authorization: `Bearer ${this.apiKey}`,
|
|
21
22
|
'Content-Type': 'application/json',
|
|
22
|
-
'User-Agent':
|
|
23
|
+
'User-Agent': `taiga-ai-mcp-server/${SERVER_VERSION}`,
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
const response = await fetch(url, {
|
|
@@ -61,15 +62,15 @@ export class TaigaClient {
|
|
|
61
62
|
|
|
62
63
|
async listProjects(opts?: { teamId?: string; status?: string; limit?: number }) {
|
|
63
64
|
const params = new URLSearchParams();
|
|
64
|
-
if (opts?.teamId) params.set('
|
|
65
|
+
if (opts?.teamId) params.set('factoryId', opts.teamId);
|
|
65
66
|
if (opts?.status) params.set('status', opts.status);
|
|
66
67
|
if (opts?.limit) params.set('limit', String(opts.limit));
|
|
67
68
|
const qs = params.toString();
|
|
68
|
-
return this.request<{ data: unknown[]; nextCursor: string | null }>('GET', `/
|
|
69
|
+
return this.request<{ data: unknown[]; nextCursor: string | null }>('GET', `/products${qs ? `?${qs}` : ''}`);
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
async getProject(id: string) {
|
|
72
|
-
return this.request<{ data: unknown }>('GET', `/
|
|
73
|
+
return this.request<{ data: unknown }>('GET', `/products/${id}`);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
// ---------------------------------------------------------------------------
|
|
@@ -104,16 +105,16 @@ export class TaigaClient {
|
|
|
104
105
|
const qs = params.toString();
|
|
105
106
|
return this.request<{ data: unknown[]; nextCursor: string | null }>(
|
|
106
107
|
'GET',
|
|
107
|
-
`/
|
|
108
|
+
`/factories/${teamId}/documents${qs ? `?${qs}` : ''}`
|
|
108
109
|
);
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
async getTeamDocument(teamId: string, type: string) {
|
|
112
|
-
return this.request<{ data: unknown }>('GET', `/
|
|
113
|
+
return this.request<{ data: unknown }>('GET', `/factories/${teamId}/documents/${type}`);
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
async saveTeamDocumentDraft(teamId: string, type: string, body: { data: Record<string, unknown> }) {
|
|
116
|
-
return this.request<{ data: unknown }>('PUT', `/
|
|
117
|
+
return this.request<{ data: unknown }>('PUT', `/factories/${teamId}/documents/${type}/draft`, body);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
// ---------------------------------------------------------------------------
|
|
@@ -126,33 +127,12 @@ export class TaigaClient {
|
|
|
126
127
|
const qs = params.toString();
|
|
127
128
|
return this.request<{ data: unknown[]; nextCursor: string | null }>(
|
|
128
129
|
'GET',
|
|
129
|
-
`/
|
|
130
|
+
`/products/${projectId}/documents${qs ? `?${qs}` : ''}`
|
|
130
131
|
);
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
async getProjectDocument(projectId: string, type: string) {
|
|
134
|
-
return this.request<{ data: unknown }>('GET', `/
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
// Skills (agent skills)
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
|
|
141
|
-
async listSkills(opts?: { scope?: string; scopeId?: string; limit?: number }) {
|
|
142
|
-
const params = new URLSearchParams();
|
|
143
|
-
if (opts?.scope) params.set('scope', opts.scope);
|
|
144
|
-
if (opts?.scopeId) params.set('scopeId', opts.scopeId);
|
|
145
|
-
if (opts?.limit) params.set('limit', String(opts.limit));
|
|
146
|
-
const qs = params.toString();
|
|
147
|
-
return this.request<{ data: unknown[]; nextCursor: string | null }>('GET', `/agent-skills${qs ? `?${qs}` : ''}`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async getSkill(id: string) {
|
|
151
|
-
return this.request<{ data: unknown }>('GET', `/agent-skills/${id}`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async updateSkill(id: string, body: { name?: string; description?: string; content?: string }) {
|
|
155
|
-
return this.request<{ data: unknown }>('PATCH', `/agent-skills/${id}`, body);
|
|
135
|
+
return this.request<{ data: unknown }>('GET', `/products/${projectId}/documents/${type}`);
|
|
156
136
|
}
|
|
157
137
|
|
|
158
138
|
// ---------------------------------------------------------------------------
|
|
@@ -161,21 +141,20 @@ export class TaigaClient {
|
|
|
161
141
|
|
|
162
142
|
/**
|
|
163
143
|
* Fetch the full context for a project in parallel: project metadata,
|
|
164
|
-
* team documents
|
|
144
|
+
* team documents and project documents.
|
|
165
145
|
* Returns a structured bundle optimized for coding agent consumption.
|
|
166
146
|
*/
|
|
167
147
|
async getProjectContext(projectId: string): Promise<Record<string, unknown>> {
|
|
168
|
-
// First fetch the project to get the
|
|
169
|
-
const project = await this.request<{ data: Record<string, unknown> }>('GET', `/
|
|
170
|
-
const teamId = project.data.
|
|
148
|
+
// First fetch the project to get its factory (the API's name for a team since PRO-1668)
|
|
149
|
+
const project = await this.request<{ data: Record<string, unknown> }>('GET', `/products/${projectId}`);
|
|
150
|
+
const teamId = project.data.factoryId as string | undefined;
|
|
171
151
|
|
|
172
|
-
// Fetch
|
|
173
|
-
const [projectDocs, teamDocs
|
|
152
|
+
// Fetch the document lists in parallel.
|
|
153
|
+
const [projectDocs, teamDocs] = await Promise.all([
|
|
174
154
|
this.listProjectDocuments(projectId, { limit: 20 }).catch(() => ({ data: [] })),
|
|
175
155
|
teamId
|
|
176
156
|
? this.listTeamDocuments(teamId, { limit: 20 }).catch(() => ({ data: [] }))
|
|
177
157
|
: Promise.resolve({ data: [] }),
|
|
178
|
-
this.listSkills({ limit: 100 }).catch(() => ({ data: [] })),
|
|
179
158
|
]);
|
|
180
159
|
|
|
181
160
|
// Fetch full content for key documents (in parallel)
|
|
@@ -233,9 +212,6 @@ export class TaigaClient {
|
|
|
233
212
|
isDraft: d.isDraft,
|
|
234
213
|
})),
|
|
235
214
|
},
|
|
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
215
|
};
|
|
240
216
|
|
|
241
217
|
return context;
|
|
@@ -251,7 +227,7 @@ export class TaigaClient {
|
|
|
251
227
|
const qs = params.toString();
|
|
252
228
|
return this.request<{ data: unknown[]; nextCursor: string | null }>(
|
|
253
229
|
'GET',
|
|
254
|
-
`/
|
|
230
|
+
`/products/${projectId}/backlog${qs ? `?${qs}` : ''}`
|
|
255
231
|
);
|
|
256
232
|
}
|
|
257
233
|
|
|
@@ -264,7 +240,7 @@ export class TaigaClient {
|
|
|
264
240
|
// ---------------------------------------------------------------------------
|
|
265
241
|
|
|
266
242
|
async listEnvironments(projectId: string) {
|
|
267
|
-
return this.request<{ data: unknown[] }>('GET', `/
|
|
243
|
+
return this.request<{ data: unknown[] }>('GET', `/products/${projectId}/environments`);
|
|
268
244
|
}
|
|
269
245
|
|
|
270
246
|
// ---------------------------------------------------------------------------
|
|
@@ -275,6 +251,6 @@ export class TaigaClient {
|
|
|
275
251
|
const params = new URLSearchParams();
|
|
276
252
|
if (opts?.limit) params.set('limit', String(opts.limit));
|
|
277
253
|
const qs = params.toString();
|
|
278
|
-
return this.request<{ data: unknown[]; nextCursor: string | null }>('GET', `/
|
|
254
|
+
return this.request<{ data: unknown[]; nextCursor: string | null }>('GET', `/factories${qs ? `?${qs}` : ''}`);
|
|
279
255
|
}
|
|
280
256
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
5
|
import { TaigaClient } from './client.js';
|
|
6
6
|
import { registerAllTools } from './tools/index.js';
|
|
7
|
+
import { SERVER_VERSION } from './version.js';
|
|
7
8
|
|
|
8
9
|
const DEFAULT_API_URL = 'https://api.taiga.app';
|
|
9
10
|
|
|
@@ -31,7 +32,7 @@ async function main() {
|
|
|
31
32
|
const server = new McpServer(
|
|
32
33
|
{
|
|
33
34
|
name: 'taiga',
|
|
34
|
-
version:
|
|
35
|
+
version: SERVER_VERSION,
|
|
35
36
|
},
|
|
36
37
|
{
|
|
37
38
|
instructions: [
|
|
@@ -50,16 +51,12 @@ async function main() {
|
|
|
50
51
|
"Specification (what to build), architecture (how it's designed), threat model (security risks),",
|
|
51
52
|
'technology decisions, and data flow. Read these to understand the specific project context.',
|
|
52
53
|
'',
|
|
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
54
|
"Workflow: Start with taiga_list_teams to find your team, then read the team's domain_context",
|
|
58
55
|
'and tech_preferences. For project-specific work, use taiga_list_projects to find the project,',
|
|
59
|
-
'then read its specification and architecture.
|
|
56
|
+
'then read its specification and architecture.',
|
|
60
57
|
'',
|
|
61
|
-
'When you notice a policy
|
|
62
|
-
'(taiga_update_document,
|
|
58
|
+
'When you notice a policy or team document is outdated, use the update tools',
|
|
59
|
+
'(taiga_update_document, taiga_update_team_document) to fix it.',
|
|
63
60
|
'This keeps the knowledge base accurate for everyone.',
|
|
64
61
|
].join('\n'),
|
|
65
62
|
}
|
|
@@ -18,20 +18,20 @@ const ALL_CAPABILITIES = [
|
|
|
18
18
|
'backlog:read',
|
|
19
19
|
];
|
|
20
20
|
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Every tool name the server registers for a key with all capabilities,
|
|
23
|
+
* sorted for deterministic comparison.
|
|
24
|
+
*/
|
|
22
25
|
const ALL_TOOL_NAMES = [
|
|
23
26
|
'taiga_get_document',
|
|
24
27
|
'taiga_get_project_document',
|
|
25
|
-
'taiga_get_skill',
|
|
26
28
|
'taiga_get_team_document',
|
|
27
29
|
'taiga_list_documents',
|
|
28
30
|
'taiga_list_project_documents',
|
|
29
31
|
'taiga_list_projects',
|
|
30
|
-
'taiga_list_skills',
|
|
31
32
|
'taiga_list_team_documents',
|
|
32
33
|
'taiga_list_teams',
|
|
33
34
|
'taiga_update_document',
|
|
34
|
-
'taiga_update_skill',
|
|
35
35
|
'taiga_update_team_document',
|
|
36
36
|
];
|
|
37
37
|
|
|
@@ -129,7 +129,7 @@ describe('MCP server integration', () => {
|
|
|
129
129
|
// -------------------------------------------------------------------------
|
|
130
130
|
|
|
131
131
|
describe('tool discovery', () => {
|
|
132
|
-
it('should expose
|
|
132
|
+
it('should expose every tool when API key has full capabilities', async () => {
|
|
133
133
|
const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
|
|
134
134
|
|
|
135
135
|
const { tools } = await client.listTools();
|
|
@@ -167,8 +167,6 @@ describe('MCP server integration', () => {
|
|
|
167
167
|
|
|
168
168
|
expect(names).toContain('taiga_list_documents');
|
|
169
169
|
expect(names).toContain('taiga_get_document');
|
|
170
|
-
expect(names).toContain('taiga_list_skills');
|
|
171
|
-
expect(names).toContain('taiga_get_skill');
|
|
172
170
|
expect(names).not.toContain('taiga_update_document');
|
|
173
171
|
});
|
|
174
172
|
|
|
@@ -181,7 +179,6 @@ describe('MCP server integration', () => {
|
|
|
181
179
|
const names = toolNames(tools);
|
|
182
180
|
|
|
183
181
|
expect(names).toContain('taiga_update_document');
|
|
184
|
-
expect(names).toContain('taiga_update_skill');
|
|
185
182
|
});
|
|
186
183
|
|
|
187
184
|
it('should expose team document write when both teams:read and documents:write are granted', async () => {
|
|
@@ -209,10 +206,8 @@ describe('MCP server integration', () => {
|
|
|
209
206
|
expect(names).toContain('taiga_list_team_documents');
|
|
210
207
|
// tenant:documents:read tools present
|
|
211
208
|
expect(names).toContain('taiga_list_documents');
|
|
212
|
-
expect(names).toContain('taiga_list_skills');
|
|
213
209
|
// write tools absent
|
|
214
210
|
expect(names).not.toContain('taiga_update_document');
|
|
215
|
-
expect(names).not.toContain('taiga_update_skill');
|
|
216
211
|
});
|
|
217
212
|
|
|
218
213
|
it('should provide a description and input schema for every tool', async () => {
|
|
@@ -238,8 +233,8 @@ describe('MCP server integration', () => {
|
|
|
238
233
|
const CONTRACT: Record<string, string[]> = {
|
|
239
234
|
'projects:read': ['taiga_list_projects', 'taiga_list_project_documents', 'taiga_get_project_document'],
|
|
240
235
|
'teams:read': ['taiga_list_teams', 'taiga_list_team_documents', 'taiga_get_team_document'],
|
|
241
|
-
'tenant:documents:read': ['taiga_list_documents', 'taiga_get_document'
|
|
242
|
-
'tenant:documents:write': ['taiga_update_document'
|
|
236
|
+
'tenant:documents:read': ['taiga_list_documents', 'taiga_get_document'],
|
|
237
|
+
'tenant:documents:write': ['taiga_update_document'],
|
|
243
238
|
};
|
|
244
239
|
|
|
245
240
|
for (const [capability, expectedTools] of Object.entries(CONTRACT)) {
|
|
@@ -271,7 +266,7 @@ describe('MCP server integration', () => {
|
|
|
271
266
|
|
|
272
267
|
const { client } = await bootMcpPair({
|
|
273
268
|
capabilities: ALL_CAPABILITIES,
|
|
274
|
-
routes: { 'GET /v1/
|
|
269
|
+
routes: { 'GET /v1/factories': { data: teams, nextCursor: null } },
|
|
275
270
|
});
|
|
276
271
|
|
|
277
272
|
const result = await client.callTool({ name: 'taiga_list_teams', arguments: {} });
|
|
@@ -280,25 +275,6 @@ describe('MCP server integration', () => {
|
|
|
280
275
|
expect(parseToolResult(result)).toEqual(teams);
|
|
281
276
|
});
|
|
282
277
|
|
|
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' };
|
|
286
|
-
|
|
287
|
-
const { client, fetchSpy } = await bootMcpPair({
|
|
288
|
-
capabilities: ALL_CAPABILITIES,
|
|
289
|
-
routes: { [`PATCH /v1/agent-skills/${skillId}`]: { data: updated } },
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
await client.callTool({
|
|
293
|
-
name: 'taiga_update_skill',
|
|
294
|
-
arguments: { id: skillId, content: '# Updated' },
|
|
295
|
-
});
|
|
296
|
-
|
|
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}`);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
278
|
it('should return document content through MCP protocol', async () => {
|
|
303
279
|
const docs = [{ id: 'd1', title: 'Security Policy' }];
|
|
304
280
|
|
|
@@ -332,13 +308,13 @@ describe('MCP server integration', () => {
|
|
|
332
308
|
});
|
|
333
309
|
|
|
334
310
|
it('should send PUT when updating a team document draft', async () => {
|
|
335
|
-
const teamId = '
|
|
311
|
+
const teamId = '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b';
|
|
336
312
|
const docType = 'domain_context';
|
|
337
313
|
const updated = { type: docType, name: 'Domain Context' };
|
|
338
314
|
|
|
339
315
|
const { client, fetchSpy } = await bootMcpPair({
|
|
340
316
|
capabilities: ALL_CAPABILITIES,
|
|
341
|
-
routes: { [`PUT /v1/
|
|
317
|
+
routes: { [`PUT /v1/factories/${teamId}/documents/${docType}/draft`]: { data: updated } },
|
|
342
318
|
});
|
|
343
319
|
|
|
344
320
|
await client.callTool({
|
|
@@ -347,23 +323,10 @@ describe('MCP server integration', () => {
|
|
|
347
323
|
});
|
|
348
324
|
|
|
349
325
|
const putCall = fetchSpy.mock.calls.find(
|
|
350
|
-
(c: unknown[]) => (c[1] as RequestInit)?.method === 'PUT' && (c[0] as string).includes('/
|
|
326
|
+
(c: unknown[]) => (c[1] as RequestInit)?.method === 'PUT' && (c[0] as string).includes('/factories/')
|
|
351
327
|
);
|
|
352
328
|
expect(putCall).toBeDefined();
|
|
353
|
-
expect(putCall![0]).toContain(`/v1/
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it('should return skills through MCP protocol', async () => {
|
|
357
|
-
const skills = [{ id: 's1', name: 'React Standards' }];
|
|
358
|
-
|
|
359
|
-
const { client } = await bootMcpPair({
|
|
360
|
-
capabilities: ALL_CAPABILITIES,
|
|
361
|
-
routes: { 'GET /v1/agent-skills': { data: skills, nextCursor: null } },
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
const result = await client.callTool({ name: 'taiga_list_skills', arguments: {} });
|
|
365
|
-
|
|
366
|
-
expect(parseToolResult(result)).toEqual(skills);
|
|
329
|
+
expect(putCall![0]).toContain(`/v1/factories/${teamId}/documents/${docType}/draft`);
|
|
367
330
|
});
|
|
368
331
|
|
|
369
332
|
it('should return teams through MCP protocol', async () => {
|
|
@@ -371,7 +334,7 @@ describe('MCP server integration', () => {
|
|
|
371
334
|
|
|
372
335
|
const { client } = await bootMcpPair({
|
|
373
336
|
capabilities: ALL_CAPABILITIES,
|
|
374
|
-
routes: { 'GET /v1/
|
|
337
|
+
routes: { 'GET /v1/factories': { data: teams, nextCursor: null } },
|
|
375
338
|
});
|
|
376
339
|
|
|
377
340
|
const result = await client.callTool({ name: 'taiga_list_teams', arguments: {} });
|
package/src/tools/backlogs.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Backlog tools are reserved for post-beta.
|
|
2
|
-
// The beta MCP server focuses on the knowledge base (policies,
|
|
2
|
+
// The beta MCP server focuses on the knowledge base (policies, team docs).
|
|
3
3
|
|
|
4
4
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
5
|
import type { TaigaClient } from '../client.js';
|
package/src/tools/documents.ts
CHANGED
|
@@ -32,7 +32,7 @@ export function registerDocumentTools(server: McpServer, client: TaigaClient) {
|
|
|
32
32
|
'Use this when you need to understand security requirements, data handling rules, ' +
|
|
33
33
|
'architecture standards, or compliance controls that apply to your code. ' +
|
|
34
34
|
'Types: policy_security, policy_data, policy_sdlc, policy_ops, policy_tic, ' +
|
|
35
|
-
'policy_risk, policy_architecture, policy_supply_chain, policy_acceptable_use, policy_ip.',
|
|
35
|
+
'policy_risk, policy_architecture, policy_supply_chain, policy_cloud_governance, policy_acceptable_use, policy_ip.',
|
|
36
36
|
{
|
|
37
37
|
type: z
|
|
38
38
|
.string()
|
package/src/tools/index.ts
CHANGED
|
@@ -3,19 +3,17 @@ import type { TaigaClient } from '../client.js';
|
|
|
3
3
|
import { registerProjectTools } from './projects.js';
|
|
4
4
|
import { registerDocumentTools } from './documents.js';
|
|
5
5
|
import { registerBacklogTools } from './backlogs.js';
|
|
6
|
-
import { registerSkillTools } from './skills.js';
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Register all MCP tools based on the API key's effective capabilities.
|
|
10
9
|
* Only tools matching the key's scopes are registered -- the AI assistant
|
|
11
10
|
* never sees tools it can't use.
|
|
12
11
|
*
|
|
13
|
-
* Beta scope: Knowledge base access (policies
|
|
12
|
+
* Beta scope: Knowledge base access (policies and team documents).
|
|
14
13
|
* Post-beta: Projects, backlog items, environments, composite tools.
|
|
15
14
|
*/
|
|
16
15
|
export function registerAllTools(server: McpServer, client: TaigaClient): void {
|
|
17
16
|
registerProjectTools(server, client);
|
|
18
17
|
registerDocumentTools(server, client);
|
|
19
18
|
registerBacklogTools(server, client);
|
|
20
|
-
registerSkillTools(server, client);
|
|
21
19
|
}
|
package/src/tools/projects.ts
CHANGED
|
@@ -8,7 +8,7 @@ export function registerProjectTools(server: McpServer, client: TaigaClient) {
|
|
|
8
8
|
server.tool(
|
|
9
9
|
'taiga_list_teams',
|
|
10
10
|
'List all teams in the organization. Use this to find team IDs ' +
|
|
11
|
-
'for reading team documents
|
|
11
|
+
'for reading team documents and understanding team structure. ' +
|
|
12
12
|
'Teams own domain context, tech preferences, and architecture constraints.',
|
|
13
13
|
{
|
|
14
14
|
limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
|
|
@@ -28,13 +28,17 @@ export function registerProjectTools(server: McpServer, client: TaigaClient) {
|
|
|
28
28
|
'taiga_list_projects',
|
|
29
29
|
'List projects in the organization. Use this to find the project you are working on ' +
|
|
30
30
|
'so you can read its specification, architecture, and threat model. ' +
|
|
31
|
-
'Can filter by team
|
|
31
|
+
'Can filter by team; a status filter also exists but no longer distinguishes anything (see its description).',
|
|
32
32
|
{
|
|
33
33
|
teamId: z.string().uuid().optional().describe('Filter by team ID -- get team IDs from taiga_list_teams'),
|
|
34
34
|
status: z
|
|
35
35
|
.enum(['draft', 'active', 'archived'])
|
|
36
36
|
.optional()
|
|
37
|
-
.describe(
|
|
37
|
+
.describe(
|
|
38
|
+
'Filter by status. Taiga no longer surfaces project status in its UI (PRO-543), so every ' +
|
|
39
|
+
'project is currently "draft" and filtering by "active" or "archived" returns nothing. ' +
|
|
40
|
+
'Omit this unless you are working with data set through the API directly.'
|
|
41
|
+
),
|
|
38
42
|
limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
|
|
39
43
|
},
|
|
40
44
|
{ readOnlyHint: true, openWorldHint: false },
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { SERVER_VERSION } from './version.js';
|
|
4
|
+
|
|
5
|
+
describe('SERVER_VERSION', () => {
|
|
6
|
+
it('is the version in package.json, not a hand-kept copy', () => {
|
|
7
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string };
|
|
8
|
+
expect(SERVER_VERSION).toBe(pkg.version);
|
|
9
|
+
});
|
|
10
|
+
});
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This package's own version, read from package.json at runtime.
|
|
5
|
+
*
|
|
6
|
+
* Read at runtime rather than inlined at build: the publish workflow sets the
|
|
7
|
+
* final version (`0.3.0-dev.<run>`, `0.3.0-rc.<run>`) after the bundle is
|
|
8
|
+
* built, so only a runtime read reports what was actually published. Both
|
|
9
|
+
* `src/` and the bundled `dist/index.js` sit one level under the package root,
|
|
10
|
+
* and npm always ships package.json.
|
|
11
|
+
*/
|
|
12
|
+
const pkg = createRequire(import.meta.url)('../package.json') as { version: string };
|
|
13
|
+
|
|
14
|
+
export const SERVER_VERSION: string = pkg.version;
|
|
Binary file
|