@taiga-ai/mcp-server 0.2.3-dev.267 → 0.2.3-dev.60
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 +495 -62
- package/package.json +8 -6
- package/scripts/build.mjs +37 -0
- package/scripts/prepare-publish.cjs +15 -7
- package/src/client.ts +13 -5
- package/src/index.ts +19 -7
- package/src/server.integration.test.ts +99 -45
- package/src/tools/documents.ts +1 -1
- package/src/tools/index.ts +5 -1
- package/taiga-ai-mcp-server-0.2.3-dev.60.tgz +0 -0
- 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/taiga-ai-mcp-server-0.2.3-dev.267.tgz +0 -0
package/dist/index.js
CHANGED
|
@@ -1,67 +1,500 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from
|
|
3
|
-
import { StdioServerTransport } from
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region ../../packages/feature-flags/dist/index.js
|
|
6
|
+
const featureFlags = Object.freeze({ skills: false });
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/client.ts
|
|
9
|
+
/**
|
|
10
|
+
* HTTP client for the Taiga API.
|
|
11
|
+
* Used by the MCP server to proxy tool calls to the backend.
|
|
12
|
+
*/
|
|
13
|
+
var TaigaClient = class {
|
|
14
|
+
baseUrl;
|
|
15
|
+
apiKey;
|
|
16
|
+
introspection = null;
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
19
|
+
this.apiKey = opts.apiKey;
|
|
20
|
+
}
|
|
21
|
+
async request(method, path, body) {
|
|
22
|
+
const url = `${this.baseUrl}/v1${path}`;
|
|
23
|
+
const headers = {
|
|
24
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
"User-Agent": "taiga-ai-mcp-server/0.0.1"
|
|
27
|
+
};
|
|
28
|
+
const response = await fetch(url, {
|
|
29
|
+
method,
|
|
30
|
+
headers,
|
|
31
|
+
body: body ? JSON.stringify(body) : void 0
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
const text = await response.text().catch(() => "");
|
|
35
|
+
throw new Error(`Taiga API ${method} ${path} failed (${response.status}): ${text}`);
|
|
36
|
+
}
|
|
37
|
+
return response.json();
|
|
38
|
+
}
|
|
39
|
+
async introspect() {
|
|
40
|
+
const result = await this.request("GET", "/auth/introspect");
|
|
41
|
+
this.introspection = result.data;
|
|
42
|
+
return result.data;
|
|
43
|
+
}
|
|
44
|
+
getIntrospection() {
|
|
45
|
+
return this.introspection;
|
|
46
|
+
}
|
|
47
|
+
hasCapability(capability) {
|
|
48
|
+
if (!this.introspection) return false;
|
|
49
|
+
return this.introspection.capabilities.includes(capability);
|
|
50
|
+
}
|
|
51
|
+
async listProjects(opts) {
|
|
52
|
+
const params = new URLSearchParams();
|
|
53
|
+
if (opts?.teamId) params.set("teamId", opts.teamId);
|
|
54
|
+
if (opts?.status) params.set("status", opts.status);
|
|
55
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
56
|
+
const qs = params.toString();
|
|
57
|
+
return this.request("GET", `/projects${qs ? `?${qs}` : ""}`);
|
|
58
|
+
}
|
|
59
|
+
async getProject(id) {
|
|
60
|
+
return this.request("GET", `/projects/${id}`);
|
|
61
|
+
}
|
|
62
|
+
async listTenantDocuments(opts) {
|
|
63
|
+
const params = new URLSearchParams();
|
|
64
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
65
|
+
const qs = params.toString();
|
|
66
|
+
return this.request("GET", `/tenants/current/documents${qs ? `?${qs}` : ""}`);
|
|
67
|
+
}
|
|
68
|
+
async getTenantDocument(type) {
|
|
69
|
+
return this.request("GET", `/tenants/current/documents/${type}`);
|
|
70
|
+
}
|
|
71
|
+
async saveTenantDocumentDraft(type, body) {
|
|
72
|
+
return this.request("PUT", `/tenants/current/documents/${type}/draft`, body);
|
|
73
|
+
}
|
|
74
|
+
async listTeamDocuments(teamId, opts) {
|
|
75
|
+
const params = new URLSearchParams();
|
|
76
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
77
|
+
const qs = params.toString();
|
|
78
|
+
return this.request("GET", `/teams/${teamId}/documents${qs ? `?${qs}` : ""}`);
|
|
79
|
+
}
|
|
80
|
+
async getTeamDocument(teamId, type) {
|
|
81
|
+
return this.request("GET", `/teams/${teamId}/documents/${type}`);
|
|
82
|
+
}
|
|
83
|
+
async saveTeamDocumentDraft(teamId, type, body) {
|
|
84
|
+
return this.request("PUT", `/teams/${teamId}/documents/${type}/draft`, body);
|
|
85
|
+
}
|
|
86
|
+
async listProjectDocuments(projectId, opts) {
|
|
87
|
+
const params = new URLSearchParams();
|
|
88
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
89
|
+
const qs = params.toString();
|
|
90
|
+
return this.request("GET", `/projects/${projectId}/documents${qs ? `?${qs}` : ""}`);
|
|
91
|
+
}
|
|
92
|
+
async getProjectDocument(projectId, type) {
|
|
93
|
+
return this.request("GET", `/projects/${projectId}/documents/${type}`);
|
|
94
|
+
}
|
|
95
|
+
async listSkills(opts) {
|
|
96
|
+
const params = new URLSearchParams();
|
|
97
|
+
if (opts?.scope) params.set("scope", opts.scope);
|
|
98
|
+
if (opts?.scopeId) params.set("scopeId", opts.scopeId);
|
|
99
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
100
|
+
const qs = params.toString();
|
|
101
|
+
return this.request("GET", `/agent-skills${qs ? `?${qs}` : ""}`);
|
|
102
|
+
}
|
|
103
|
+
async getSkill(id) {
|
|
104
|
+
return this.request("GET", `/agent-skills/${id}`);
|
|
105
|
+
}
|
|
106
|
+
async updateSkill(id, body) {
|
|
107
|
+
return this.request("PATCH", `/agent-skills/${id}`, body);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Fetch the full context for a project in parallel: project metadata,
|
|
111
|
+
* team documents, project documents, and team skills.
|
|
112
|
+
* Returns a structured bundle optimized for coding agent consumption.
|
|
113
|
+
*/
|
|
114
|
+
async getProjectContext(projectId) {
|
|
115
|
+
const project = await this.request("GET", `/projects/${projectId}`);
|
|
116
|
+
const teamId = project.data.teamId;
|
|
117
|
+
const [projectDocs, teamDocs, skills] = await Promise.all([
|
|
118
|
+
this.listProjectDocuments(projectId, { limit: 20 }).catch(() => ({ data: [] })),
|
|
119
|
+
teamId ? this.listTeamDocuments(teamId, { limit: 20 }).catch(() => ({ data: [] })) : Promise.resolve({ data: [] }),
|
|
120
|
+
featureFlags.skills ? this.listSkills({ limit: 100 }).catch(() => ({ data: [] })) : Promise.resolve({ data: [] })
|
|
121
|
+
]);
|
|
122
|
+
const keyProjectDocTypes = [
|
|
123
|
+
"specification",
|
|
124
|
+
"architecture",
|
|
125
|
+
"technology_decisions"
|
|
126
|
+
];
|
|
127
|
+
const keyTeamDocTypes = [
|
|
128
|
+
"domain_context",
|
|
129
|
+
"tech_preferences",
|
|
130
|
+
"architecture_constraints"
|
|
131
|
+
];
|
|
132
|
+
const docFetches = [];
|
|
133
|
+
for (const docType of keyProjectDocTypes) if (projectDocs.data.some((d) => d.type === docType && !d.isDraft)) docFetches.push(this.getProjectDocument(projectId, docType).then((r) => ({
|
|
134
|
+
type: docType,
|
|
135
|
+
scope: "project",
|
|
136
|
+
data: r.data
|
|
137
|
+
})).catch(() => ({
|
|
138
|
+
type: docType,
|
|
139
|
+
scope: "project",
|
|
140
|
+
data: null
|
|
141
|
+
})));
|
|
142
|
+
if (teamId) {
|
|
143
|
+
for (const docType of keyTeamDocTypes) if (teamDocs.data.some((d) => d.type === docType && !d.isDraft)) docFetches.push(this.getTeamDocument(teamId, docType).then((r) => ({
|
|
144
|
+
type: docType,
|
|
145
|
+
scope: "team",
|
|
146
|
+
data: r.data
|
|
147
|
+
})).catch(() => ({
|
|
148
|
+
type: docType,
|
|
149
|
+
scope: "team",
|
|
150
|
+
data: null
|
|
151
|
+
})));
|
|
152
|
+
}
|
|
153
|
+
const fetchedDocs = await Promise.all(docFetches);
|
|
154
|
+
const context = {
|
|
155
|
+
project: project.data,
|
|
156
|
+
documents: {
|
|
157
|
+
project: Object.fromEntries(fetchedDocs.filter((d) => d.scope === "project" && d.data).map((d) => [d.type, d.data])),
|
|
158
|
+
team: Object.fromEntries(fetchedDocs.filter((d) => d.scope === "team" && d.data).map((d) => [d.type, d.data]))
|
|
159
|
+
},
|
|
160
|
+
available_documents: {
|
|
161
|
+
project: projectDocs.data.map((d) => ({
|
|
162
|
+
type: d.type,
|
|
163
|
+
name: d.name,
|
|
164
|
+
version: d.version,
|
|
165
|
+
isDraft: d.isDraft
|
|
166
|
+
})),
|
|
167
|
+
team: teamDocs.data.map((d) => ({
|
|
168
|
+
type: d.type,
|
|
169
|
+
name: d.name,
|
|
170
|
+
version: d.version,
|
|
171
|
+
isDraft: d.isDraft
|
|
172
|
+
}))
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
if (featureFlags.skills) context.skills_summary = skills.data.filter((s) => !s.teamId || s.teamId === teamId).map((s) => ({
|
|
176
|
+
id: s.id,
|
|
177
|
+
name: s.name,
|
|
178
|
+
slug: s.slug,
|
|
179
|
+
description: s.description
|
|
180
|
+
}));
|
|
181
|
+
return context;
|
|
182
|
+
}
|
|
183
|
+
async listBacklogItems(projectId, opts) {
|
|
184
|
+
const params = new URLSearchParams();
|
|
185
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
186
|
+
const qs = params.toString();
|
|
187
|
+
return this.request("GET", `/projects/${projectId}/backlog${qs ? `?${qs}` : ""}`);
|
|
188
|
+
}
|
|
189
|
+
async getBacklogItem(itemId) {
|
|
190
|
+
return this.request("GET", `/backlog/${itemId}`);
|
|
191
|
+
}
|
|
192
|
+
async listEnvironments(projectId) {
|
|
193
|
+
return this.request("GET", `/projects/${projectId}/environments`);
|
|
194
|
+
}
|
|
195
|
+
async listTeams(opts) {
|
|
196
|
+
const params = new URLSearchParams();
|
|
197
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
198
|
+
const qs = params.toString();
|
|
199
|
+
return this.request("GET", `/teams${qs ? `?${qs}` : ""}`);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region src/tools/summarize.ts
|
|
204
|
+
/**
|
|
205
|
+
* Strip heavy content from list responses to avoid context window bloat.
|
|
206
|
+
*
|
|
207
|
+
* List endpoints return summaries (id, name, description, metadata).
|
|
208
|
+
* Get endpoints return the full content.
|
|
209
|
+
*
|
|
210
|
+
* This keeps list calls cheap (~100 tokens per item) and lets the AI
|
|
211
|
+
* selectively load full content only for the items it needs.
|
|
212
|
+
*/
|
|
213
|
+
/** Fields to strip from list responses -- these contain the bulk content. */
|
|
214
|
+
const HEAVY_FIELDS = [
|
|
215
|
+
"content",
|
|
216
|
+
"data",
|
|
217
|
+
"sections"
|
|
218
|
+
];
|
|
219
|
+
/**
|
|
220
|
+
* Remove heavy content fields from each item in an array.
|
|
221
|
+
* Preserves all metadata (id, name, description, type, dates, etc).
|
|
222
|
+
*/
|
|
223
|
+
function summarizeList(items) {
|
|
224
|
+
return items.map((item) => {
|
|
225
|
+
const summary = { ...item };
|
|
226
|
+
for (const field of HEAVY_FIELDS) if (field in summary) delete summary[field];
|
|
227
|
+
return summary;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region src/tools/projects.ts
|
|
232
|
+
function registerProjectTools(server, client) {
|
|
233
|
+
if (client.hasCapability("teams:read")) server.tool("taiga_list_teams", "List all teams in the organization. Use this to find team IDs for reading team documents, skills, and understanding team structure. Teams own domain context, tech preferences, and architecture constraints.", { limit: z.number().min(1).max(100).optional().describe("Max results (default 25)") }, {
|
|
234
|
+
readOnlyHint: true,
|
|
235
|
+
openWorldHint: false
|
|
236
|
+
}, async (params) => {
|
|
237
|
+
const result = await client.listTeams(params);
|
|
238
|
+
return { content: [{
|
|
239
|
+
type: "text",
|
|
240
|
+
text: JSON.stringify(result.data, null, 2)
|
|
241
|
+
}] };
|
|
242
|
+
});
|
|
243
|
+
if (client.hasCapability("projects:read")) {
|
|
244
|
+
server.tool("taiga_list_projects", "List projects in the organization. Use this to find the project you are working on so you can read its specification, architecture, and threat model. Can filter by team or status.", {
|
|
245
|
+
teamId: z.string().uuid().optional().describe("Filter by team ID -- get team IDs from taiga_list_teams"),
|
|
246
|
+
status: z.enum([
|
|
247
|
+
"draft",
|
|
248
|
+
"active",
|
|
249
|
+
"archived"
|
|
250
|
+
]).optional().describe("Filter by status (draft = intake in progress, active = development)"),
|
|
251
|
+
limit: z.number().min(1).max(100).optional().describe("Max results (default 25)")
|
|
252
|
+
}, {
|
|
253
|
+
readOnlyHint: true,
|
|
254
|
+
openWorldHint: false
|
|
255
|
+
}, async (params) => {
|
|
256
|
+
const result = await client.listProjects(params);
|
|
257
|
+
return { content: [{
|
|
258
|
+
type: "text",
|
|
259
|
+
text: JSON.stringify(result.data, null, 2)
|
|
260
|
+
}] };
|
|
261
|
+
});
|
|
262
|
+
server.tool("taiga_list_project_documents", "List documents for a specific project (names and types only). Project documents are generated during intake: specification (requirements), architecture (system design), technology decisions, data flow, privacy assessment, threat model (security risks), risk register, and user flow. Use taiga_get_project_document to read the full content.", {
|
|
263
|
+
projectId: z.string().uuid().describe("Project ID -- get from taiga_list_projects"),
|
|
264
|
+
limit: z.number().min(1).max(100).optional().describe("Max results (default 25)")
|
|
265
|
+
}, {
|
|
266
|
+
readOnlyHint: true,
|
|
267
|
+
openWorldHint: false
|
|
268
|
+
}, async (params) => {
|
|
269
|
+
const summaries = summarizeList((await client.listProjectDocuments(params.projectId, { limit: params.limit })).data);
|
|
270
|
+
return { content: [{
|
|
271
|
+
type: "text",
|
|
272
|
+
text: JSON.stringify(summaries, null, 2)
|
|
273
|
+
}] };
|
|
274
|
+
});
|
|
275
|
+
server.tool("taiga_get_project_document", "Get the full content of a specific project document. Use this when you need to understand what to build (specification), how the system is designed (architecture), what security risks exist (threat model), or what technology choices were made (technology_decisions). Types: specification, architecture, technology_decisions, data_flow, privacy_assessment, threat_model, risk_register, user_flow.", {
|
|
276
|
+
projectId: z.string().uuid().describe("Project ID -- get from taiga_list_projects"),
|
|
277
|
+
type: z.string().describe("Document type -- get available types from taiga_list_project_documents. Common: specification, architecture, threat_model, technology_decisions")
|
|
278
|
+
}, {
|
|
279
|
+
readOnlyHint: true,
|
|
280
|
+
openWorldHint: false
|
|
281
|
+
}, async (params) => {
|
|
282
|
+
const result = await client.getProjectDocument(params.projectId, params.type);
|
|
283
|
+
return { content: [{
|
|
284
|
+
type: "text",
|
|
285
|
+
text: JSON.stringify(result.data, null, 2)
|
|
286
|
+
}] };
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
//#endregion
|
|
291
|
+
//#region src/tools/documents.ts
|
|
292
|
+
function registerDocumentTools(server, client) {
|
|
293
|
+
if (client.hasCapability("tenant:documents:read")) {
|
|
294
|
+
server.tool("taiga_list_documents", "List company-level policy documents (names and types only). Policies define security, data privacy, SDLC, operations, architecture, and compliance rules that apply across all teams and projects. Use taiga_get_document with the document type to read the full policy content.", { limit: z.number().min(1).max(100).optional().describe("Max results (default 25)") }, {
|
|
295
|
+
readOnlyHint: true,
|
|
296
|
+
openWorldHint: false
|
|
297
|
+
}, async (params) => {
|
|
298
|
+
const summaries = summarizeList((await client.listTenantDocuments(params)).data);
|
|
299
|
+
return { content: [{
|
|
300
|
+
type: "text",
|
|
301
|
+
text: JSON.stringify(summaries, null, 2)
|
|
302
|
+
}] };
|
|
303
|
+
});
|
|
304
|
+
server.tool("taiga_get_document", "Get the full content of a company policy document. Use this when you need to understand security requirements, data handling rules, architecture standards, or compliance controls that apply to your code. Types: policy_security, policy_data, policy_sdlc, policy_ops, policy_tic, policy_risk, policy_architecture, policy_supply_chain, policy_cloud_governance, policy_acceptable_use, policy_ip.", { type: z.string().describe("Document type -- get available types from taiga_list_documents. Common: policy_security, policy_data, policy_sdlc, policy_architecture") }, {
|
|
305
|
+
readOnlyHint: true,
|
|
306
|
+
openWorldHint: false
|
|
307
|
+
}, async (params) => {
|
|
308
|
+
const result = await client.getTenantDocument(params.type);
|
|
309
|
+
return { content: [{
|
|
310
|
+
type: "text",
|
|
311
|
+
text: JSON.stringify(result.data, null, 2)
|
|
312
|
+
}] };
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
if (client.hasCapability("tenant:documents:write")) server.tool("taiga_update_document", "Update the draft content of a company policy document. Only updates the draft -- does not publish. Use this when you notice a policy is outdated or needs corrections while reviewing code.", {
|
|
316
|
+
type: z.string().describe("Document type (e.g. policy_security, policy_sdlc)"),
|
|
317
|
+
data: z.record(z.string(), z.unknown()).describe("Document content to save as JSON")
|
|
318
|
+
}, {
|
|
319
|
+
readOnlyHint: false,
|
|
320
|
+
destructiveHint: false,
|
|
321
|
+
idempotentHint: true,
|
|
322
|
+
openWorldHint: false
|
|
323
|
+
}, async (params) => {
|
|
324
|
+
const { type, ...body } = params;
|
|
325
|
+
const result = await client.saveTenantDocumentDraft(type, body);
|
|
326
|
+
return { content: [{
|
|
327
|
+
type: "text",
|
|
328
|
+
text: JSON.stringify(result.data, null, 2)
|
|
329
|
+
}] };
|
|
330
|
+
});
|
|
331
|
+
if (client.hasCapability("teams:read")) {
|
|
332
|
+
server.tool("taiga_list_team_documents", "List documents for a specific team (names and types only). Team documents define how the team works: domain context (entities, ubiquitous language), tech preferences (approved stack), architecture constraints, SLO targets, definition of done, and team charter. Use taiga_get_team_document to read the full content.", {
|
|
333
|
+
teamId: z.string().uuid().describe("Team ID -- get from taiga_list_teams"),
|
|
334
|
+
limit: z.number().min(1).max(100).optional().describe("Max results (default 25)")
|
|
335
|
+
}, {
|
|
336
|
+
readOnlyHint: true,
|
|
337
|
+
openWorldHint: false
|
|
338
|
+
}, async (params) => {
|
|
339
|
+
const summaries = summarizeList((await client.listTeamDocuments(params.teamId, { limit: params.limit })).data);
|
|
340
|
+
return { content: [{
|
|
341
|
+
type: "text",
|
|
342
|
+
text: JSON.stringify(summaries, null, 2)
|
|
343
|
+
}] };
|
|
344
|
+
});
|
|
345
|
+
server.tool("taiga_get_team_document", "Get the full content of a specific team document. Use this when you need the team domain context (ubiquitous language, entities, business rules), tech preferences (approved stack, patterns), or architecture constraints. Types: team_charter, definition_of_done, tech_preferences, architecture_constraints, slo_targets, domain_context.", {
|
|
346
|
+
teamId: z.string().uuid().describe("Team ID -- get from taiga_list_teams"),
|
|
347
|
+
type: z.string().describe("Document type -- get available types from taiga_list_team_documents. Most useful: domain_context (entities, language), tech_preferences (stack, patterns)")
|
|
348
|
+
}, {
|
|
349
|
+
readOnlyHint: true,
|
|
350
|
+
openWorldHint: false
|
|
351
|
+
}, async (params) => {
|
|
352
|
+
const result = await client.getTeamDocument(params.teamId, params.type);
|
|
353
|
+
return { content: [{
|
|
354
|
+
type: "text",
|
|
355
|
+
text: JSON.stringify(result.data, null, 2)
|
|
356
|
+
}] };
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (client.hasCapability("teams:read") && client.hasCapability("tenant:documents:write")) server.tool("taiga_update_team_document", "Update the draft content of a team document. Only updates the draft -- does not publish. Use this when you notice team conventions, domain context, or tech preferences need updating while working on the codebase.", {
|
|
360
|
+
teamId: z.string().uuid().describe("Team ID -- get from taiga_list_teams"),
|
|
361
|
+
type: z.string().describe("Document type (e.g. domain_context, tech_preferences)"),
|
|
362
|
+
data: z.record(z.string(), z.unknown()).describe("Document content to save as JSON")
|
|
363
|
+
}, {
|
|
364
|
+
readOnlyHint: false,
|
|
365
|
+
destructiveHint: false,
|
|
366
|
+
idempotentHint: true,
|
|
367
|
+
openWorldHint: false
|
|
368
|
+
}, async (params) => {
|
|
369
|
+
const { teamId, type, ...body } = params;
|
|
370
|
+
const result = await client.saveTeamDocumentDraft(teamId, type, body);
|
|
371
|
+
return { content: [{
|
|
372
|
+
type: "text",
|
|
373
|
+
text: JSON.stringify(result.data, null, 2)
|
|
374
|
+
}] };
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region src/tools/skills.ts
|
|
379
|
+
function registerSkillTools(server, client) {
|
|
380
|
+
if (client.hasCapability("tenant:documents:read")) {
|
|
381
|
+
server.tool("taiga_list_skills", "List agent skills available in the organization (names and descriptions only). Skills are reusable knowledge modules that guide coding: coding standards, security requirements, architecture patterns, database conventions, etc. Use taiga_get_skill with the skill ID to read the full content.", { limit: z.number().min(1).max(100).optional().describe("Max results (default 25)") }, {
|
|
382
|
+
readOnlyHint: true,
|
|
383
|
+
openWorldHint: false
|
|
384
|
+
}, async (params) => {
|
|
385
|
+
const summaries = summarizeList((await client.listSkills(params)).data);
|
|
386
|
+
return { content: [{
|
|
387
|
+
type: "text",
|
|
388
|
+
text: JSON.stringify(summaries, null, 2)
|
|
389
|
+
}] };
|
|
390
|
+
});
|
|
391
|
+
server.tool("taiga_get_skill", "Get the full content of a specific agent skill. Use this when you need detailed coding guidelines, patterns, or conventions for a specific topic (e.g., security requirements, API design, testing strategy). Returns the complete markdown content with code examples and best practices.", { id: z.string().uuid().describe("Skill ID -- get from taiga_list_skills") }, {
|
|
392
|
+
readOnlyHint: true,
|
|
393
|
+
openWorldHint: false
|
|
394
|
+
}, async (params) => {
|
|
395
|
+
const result = await client.getSkill(params.id);
|
|
396
|
+
return { content: [{
|
|
397
|
+
type: "text",
|
|
398
|
+
text: JSON.stringify(result.data, null, 2)
|
|
399
|
+
}] };
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
if (client.hasCapability("tenant:documents:write")) server.tool("taiga_update_skill", "Update an agent skill (coding standards, patterns, conventions). Use this when you notice a skill is outdated, incomplete, or needs corrections while working on the codebase. Changes are saved immediately.", {
|
|
403
|
+
id: z.string().uuid().describe("Skill ID -- get from taiga_list_skills"),
|
|
404
|
+
name: z.string().optional().describe("Updated skill name"),
|
|
405
|
+
description: z.string().optional().describe("Updated skill description"),
|
|
406
|
+
content: z.string().optional().describe("Updated skill content (markdown)")
|
|
407
|
+
}, {
|
|
408
|
+
readOnlyHint: false,
|
|
409
|
+
destructiveHint: false,
|
|
410
|
+
idempotentHint: true,
|
|
411
|
+
openWorldHint: false
|
|
412
|
+
}, async (params) => {
|
|
413
|
+
const { id, ...body } = params;
|
|
414
|
+
const result = await client.updateSkill(id, body);
|
|
415
|
+
return { content: [{
|
|
416
|
+
type: "text",
|
|
417
|
+
text: JSON.stringify(result.data, null, 2)
|
|
418
|
+
}] };
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region src/tools/index.ts
|
|
423
|
+
/**
|
|
424
|
+
* Register all MCP tools based on the API key's effective capabilities.
|
|
425
|
+
* Only tools matching the key's scopes are registered -- the AI assistant
|
|
426
|
+
* never sees tools it can't use.
|
|
427
|
+
*
|
|
428
|
+
* Beta scope: Knowledge base access (policies, skills, team documents).
|
|
429
|
+
* Post-beta: Projects, backlog items, environments, composite tools.
|
|
430
|
+
*/
|
|
431
|
+
function registerAllTools(server, client) {
|
|
432
|
+
registerProjectTools(server, client);
|
|
433
|
+
registerDocumentTools(server, client);
|
|
434
|
+
if (featureFlags.skills) registerSkillTools(server, client);
|
|
435
|
+
}
|
|
436
|
+
//#endregion
|
|
437
|
+
//#region src/index.ts
|
|
438
|
+
const DEFAULT_API_URL = "https://api.taiga.app";
|
|
7
439
|
async function main() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
await server.connect(transport);
|
|
440
|
+
const apiKey = process.env.TAIGA_API_KEY;
|
|
441
|
+
if (!apiKey) {
|
|
442
|
+
console.error("Error: TAIGA_API_KEY environment variable is required.");
|
|
443
|
+
console.error("Create an API key at https://app.taiga.app/settings/api-keys");
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
const client = new TaigaClient({
|
|
447
|
+
baseUrl: process.env.TAIGA_API_URL ?? DEFAULT_API_URL,
|
|
448
|
+
apiKey
|
|
449
|
+
});
|
|
450
|
+
const introspection = await client.introspect().catch((error) => {
|
|
451
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
452
|
+
console.error(`Error: Failed to validate API key: ${message}`);
|
|
453
|
+
console.error("Check that your TAIGA_API_KEY is valid and not expired.");
|
|
454
|
+
process.exit(1);
|
|
455
|
+
});
|
|
456
|
+
const server = new McpServer({
|
|
457
|
+
name: "taiga",
|
|
458
|
+
version: "0.0.1"
|
|
459
|
+
}, { instructions: [
|
|
460
|
+
"You have access to Taiga -- a policy-aware software development platform.",
|
|
461
|
+
"Taiga provides your organization's engineering knowledge base:",
|
|
462
|
+
"",
|
|
463
|
+
"**Company policies** (taiga_list_documents / taiga_get_document):",
|
|
464
|
+
"Security, data privacy, SDLC, architecture, and compliance rules that apply to all code.",
|
|
465
|
+
"Read the relevant policy BEFORE writing code that touches security, data handling, or infrastructure.",
|
|
466
|
+
"",
|
|
467
|
+
"**Team documents** (taiga_list_team_documents / taiga_get_team_document):",
|
|
468
|
+
"Domain context (ubiquitous language, entities, business rules), tech preferences (approved stack, patterns),",
|
|
469
|
+
"architecture constraints, and SLO targets. Read these to understand HOW this team builds software.",
|
|
470
|
+
"",
|
|
471
|
+
"**Project documents** (taiga_list_project_documents / taiga_get_project_document):",
|
|
472
|
+
"Specification (what to build), architecture (how it's designed), threat model (security risks),",
|
|
473
|
+
"technology decisions, and data flow. Read these to understand the specific project context.",
|
|
474
|
+
"",
|
|
475
|
+
...featureFlags.skills ? [
|
|
476
|
+
"**Skills** (taiga_list_skills / taiga_get_skill):",
|
|
477
|
+
"Coding standards, security requirements, API design patterns, testing strategy, database patterns,",
|
|
478
|
+
"and more. These are detailed engineering guidelines with code examples.",
|
|
479
|
+
""
|
|
480
|
+
] : [],
|
|
481
|
+
"Workflow: Start with taiga_list_teams to find your team, then read the team's domain_context",
|
|
482
|
+
"and tech_preferences. For project-specific work, use taiga_list_projects to find the project,",
|
|
483
|
+
featureFlags.skills ? "then read its specification and architecture. Use skills for coding patterns." : "then read its specification and architecture.",
|
|
484
|
+
"",
|
|
485
|
+
featureFlags.skills ? "When you notice a policy, skill, or team document is outdated, use the update tools" : "When you notice a policy or team document is outdated, use the update tools",
|
|
486
|
+
featureFlags.skills ? "(taiga_update_document, taiga_update_skill, taiga_update_team_document) to fix it." : "(taiga_update_document, taiga_update_team_document) to fix it.",
|
|
487
|
+
"This keeps the knowledge base accurate for everyone."
|
|
488
|
+
].join("\n") });
|
|
489
|
+
registerAllTools(server, client);
|
|
490
|
+
const toolCount = introspection.capabilities.length;
|
|
491
|
+
console.error(`Taiga MCP server started (${toolCount} capabilities, user: ${introspection.email}, role: ${introspection.role})`);
|
|
492
|
+
const transport = new StdioServerTransport();
|
|
493
|
+
await server.connect(transport);
|
|
63
494
|
}
|
|
64
495
|
main().catch((error) => {
|
|
65
|
-
|
|
66
|
-
|
|
496
|
+
console.error("Fatal error:", error);
|
|
497
|
+
process.exit(1);
|
|
67
498
|
});
|
|
499
|
+
//#endregion
|
|
500
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taiga-ai/mcp-server",
|
|
3
|
-
"version": "0.2.3-dev.
|
|
3
|
+
"version": "0.2.3-dev.60",
|
|
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",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
8
7
|
"bin": {
|
|
9
8
|
"taiga-mcp-server": "./dist/index.js"
|
|
10
9
|
},
|
|
11
10
|
"exports": {
|
|
12
11
|
".": {
|
|
13
|
-
"types": "./dist/index.d.ts",
|
|
14
12
|
"import": "./dist/index.js",
|
|
15
13
|
"default": "./dist/index.js"
|
|
16
14
|
}
|
|
17
15
|
},
|
|
18
16
|
"scripts": {
|
|
19
|
-
"build": "tsc",
|
|
17
|
+
"build": "tsc --noEmit && node scripts/build.mjs",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
20
19
|
"dev": "tsc --watch",
|
|
21
|
-
"lint": "
|
|
20
|
+
"lint": "oxlint src",
|
|
21
|
+
"format": "oxfmt --check \"**/*.{ts,tsx,js,jsx,mjs,cjs,json,md}\"",
|
|
22
|
+
"format:fix": "oxfmt \"**/*.{ts,tsx,js,jsx,mjs,cjs,json,md}\"",
|
|
22
23
|
"test": "vitest run"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
@@ -27,8 +28,9 @@
|
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"@types/node": "^22.19.17",
|
|
31
|
+
"rolldown": "^1.0.3",
|
|
30
32
|
"typescript": "^5.9.3",
|
|
31
|
-
"vitest": "^4.1.
|
|
33
|
+
"vitest": "^4.1.8"
|
|
32
34
|
},
|
|
33
35
|
"publishConfig": {
|
|
34
36
|
"access": "public"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { rolldown } from 'rolldown';
|
|
3
|
+
|
|
4
|
+
const OUTFILE = 'dist/index.js';
|
|
5
|
+
const SHEBANG = '#!/usr/bin/env node';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Bundle the MCP server into a single self-contained ESM file.
|
|
9
|
+
*
|
|
10
|
+
* The published npm artifact must not reference private workspace packages, so
|
|
11
|
+
* `@taiga/*` (notably `@taiga/feature-flags`, a runtime value) is inlined. The
|
|
12
|
+
* genuinely-published deps — the MCP SDK and zod — stay external and are
|
|
13
|
+
* installed normally by consumers. Node built-ins are external via platform.
|
|
14
|
+
*/
|
|
15
|
+
const external = [/^@modelcontextprotocol\/sdk(\/|$)/, /^zod(\/|$)/, /^node:/];
|
|
16
|
+
|
|
17
|
+
const bundle = await rolldown({
|
|
18
|
+
input: 'src/index.ts',
|
|
19
|
+
platform: 'node',
|
|
20
|
+
external,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await bundle.write({
|
|
24
|
+
file: OUTFILE,
|
|
25
|
+
format: 'esm',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await bundle.close();
|
|
29
|
+
|
|
30
|
+
// Guarantee the bin shebang survived bundling. Rolldown preserves the entry's
|
|
31
|
+
// shebang today, but bundlers commonly strip them — without a `#!` line the
|
|
32
|
+
// published bin fails to exec under `npx` even when chmod +x. This makes the
|
|
33
|
+
// contract explicit and robust to future bundler changes (no-op if present).
|
|
34
|
+
const code = readFileSync(OUTFILE, 'utf8');
|
|
35
|
+
if (!code.startsWith('#!')) {
|
|
36
|
+
writeFileSync(OUTFILE, `${SHEBANG}\n${code}`);
|
|
37
|
+
}
|