@taiga-ai/mcp-server 0.3.0-dev.298 → 0.4.0-dev.300
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 +84 -92
- package/package.json +2 -2
- package/src/client.test.ts +14 -14
- package/src/client.ts +58 -65
- package/src/index.ts +9 -9
- package/src/server.integration.test.ts +37 -37
- package/src/tools/backlogs.ts +1 -1
- package/src/tools/documents.ts +30 -28
- package/src/tools/index.ts +4 -4
- package/src/tools/{projects.ts → products.ts} +34 -26
- package/taiga-ai-mcp-server-0.4.0-dev.300.tgz +0 -0
- package/taiga-ai-mcp-server-0.3.0-dev.298.tgz +0 -0
package/dist/index.js
CHANGED
|
@@ -48,15 +48,15 @@ var TaigaClient = class {
|
|
|
48
48
|
if (!this.introspection) return false;
|
|
49
49
|
return this.introspection.capabilities.includes(capability);
|
|
50
50
|
}
|
|
51
|
-
async
|
|
51
|
+
async listProducts(opts) {
|
|
52
52
|
const params = new URLSearchParams();
|
|
53
|
-
if (opts?.
|
|
53
|
+
if (opts?.factoryId) params.set("factoryId", opts.factoryId);
|
|
54
54
|
if (opts?.status) params.set("status", opts.status);
|
|
55
55
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
56
56
|
const qs = params.toString();
|
|
57
57
|
return this.request("GET", `/products${qs ? `?${qs}` : ""}`);
|
|
58
58
|
}
|
|
59
|
-
async
|
|
59
|
+
async getProduct(id) {
|
|
60
60
|
return this.request("GET", `/products/${id}`);
|
|
61
61
|
}
|
|
62
62
|
async listTenantDocuments(opts) {
|
|
@@ -71,103 +71,95 @@ var TaigaClient = class {
|
|
|
71
71
|
async saveTenantDocumentDraft(type, body) {
|
|
72
72
|
return this.request("PUT", `/tenants/current/documents/${type}/draft`, body);
|
|
73
73
|
}
|
|
74
|
-
async
|
|
74
|
+
async listFactoryDocuments(factoryId, opts) {
|
|
75
75
|
const params = new URLSearchParams();
|
|
76
76
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
77
77
|
const qs = params.toString();
|
|
78
|
-
return this.request("GET", `/factories/${
|
|
78
|
+
return this.request("GET", `/factories/${factoryId}/documents${qs ? `?${qs}` : ""}`);
|
|
79
79
|
}
|
|
80
|
-
async
|
|
81
|
-
return this.request("GET", `/factories/${
|
|
80
|
+
async getFactoryDocument(factoryId, type) {
|
|
81
|
+
return this.request("GET", `/factories/${factoryId}/documents/${type}`);
|
|
82
82
|
}
|
|
83
|
-
async
|
|
84
|
-
return this.request("PUT", `/factories/${
|
|
83
|
+
async saveFactoryDocumentDraft(factoryId, type, body) {
|
|
84
|
+
return this.request("PUT", `/factories/${factoryId}/documents/${type}/draft`, body);
|
|
85
85
|
}
|
|
86
|
-
async
|
|
86
|
+
async listProductDocuments(productId, opts) {
|
|
87
87
|
const params = new URLSearchParams();
|
|
88
88
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
89
89
|
const qs = params.toString();
|
|
90
|
-
return this.request("GET", `/products/${
|
|
90
|
+
return this.request("GET", `/products/${productId}/documents${qs ? `?${qs}` : ""}`);
|
|
91
91
|
}
|
|
92
|
-
async
|
|
93
|
-
return this.request("GET", `/products/${
|
|
92
|
+
async getProductDocument(productId, type) {
|
|
93
|
+
return this.request("GET", `/products/${productId}/documents/${type}`);
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
|
-
* Fetch the full context for a
|
|
97
|
-
*
|
|
96
|
+
* Fetch the full context for a product in parallel: product metadata,
|
|
97
|
+
* its factory's instructions and the product documents.
|
|
98
98
|
* Returns a structured bundle optimized for coding agent consumption.
|
|
99
99
|
*/
|
|
100
|
-
async
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const [
|
|
104
|
-
const
|
|
100
|
+
async getProductContext(productId) {
|
|
101
|
+
const product = await this.request("GET", `/products/${productId}`);
|
|
102
|
+
const factoryId = product.data.factoryId;
|
|
103
|
+
const [productDocs, factoryDocs] = await Promise.all([this.listProductDocuments(productId, { limit: 20 }).catch(() => ({ data: [] })), factoryId ? this.listFactoryDocuments(factoryId, { limit: 20 }).catch(() => ({ data: [] })) : Promise.resolve({ data: [] })]);
|
|
104
|
+
const keyProductDocTypes = [
|
|
105
105
|
"specification",
|
|
106
106
|
"architecture",
|
|
107
107
|
"technology_decisions"
|
|
108
108
|
];
|
|
109
|
-
const
|
|
110
|
-
"domain_context",
|
|
111
|
-
"tech_preferences",
|
|
112
|
-
"architecture_constraints"
|
|
113
|
-
];
|
|
109
|
+
const keyFactoryDocTypes = ["team_instructions"];
|
|
114
110
|
const docFetches = [];
|
|
115
|
-
for (const docType of
|
|
111
|
+
for (const docType of keyProductDocTypes) if (productDocs.data.some((d) => d.type === docType && !d.isDraft)) docFetches.push(this.getProductDocument(productId, docType).then((r) => ({
|
|
116
112
|
type: docType,
|
|
117
|
-
scope: "
|
|
113
|
+
scope: "product",
|
|
118
114
|
data: r.data
|
|
119
115
|
})).catch(() => ({
|
|
120
116
|
type: docType,
|
|
121
|
-
scope: "
|
|
117
|
+
scope: "product",
|
|
122
118
|
data: null
|
|
123
119
|
})));
|
|
124
|
-
if (
|
|
125
|
-
for (const docType of
|
|
120
|
+
if (factoryId) {
|
|
121
|
+
for (const docType of keyFactoryDocTypes) if (factoryDocs.data.some((d) => d.type === docType && !d.isDraft)) docFetches.push(this.getFactoryDocument(factoryId, docType).then((r) => ({
|
|
126
122
|
type: docType,
|
|
127
|
-
scope: "
|
|
123
|
+
scope: "factory",
|
|
128
124
|
data: r.data
|
|
129
125
|
})).catch(() => ({
|
|
130
126
|
type: docType,
|
|
131
|
-
scope: "
|
|
127
|
+
scope: "factory",
|
|
132
128
|
data: null
|
|
133
129
|
})));
|
|
134
130
|
}
|
|
135
131
|
const fetchedDocs = await Promise.all(docFetches);
|
|
132
|
+
const summarize = (docs) => docs.map((d) => ({
|
|
133
|
+
type: d.type,
|
|
134
|
+
name: d.name,
|
|
135
|
+
version: d.version,
|
|
136
|
+
isDraft: d.isDraft
|
|
137
|
+
}));
|
|
136
138
|
return {
|
|
137
|
-
|
|
139
|
+
product: product.data,
|
|
138
140
|
documents: {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
product: Object.fromEntries(fetchedDocs.filter((d) => d.scope === "product" && d.data).map((d) => [d.type, d.data])),
|
|
142
|
+
factory: Object.fromEntries(fetchedDocs.filter((d) => d.scope === "factory" && d.data).map((d) => [d.type, d.data]))
|
|
141
143
|
},
|
|
142
144
|
available_documents: {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
name: d.name,
|
|
146
|
-
version: d.version,
|
|
147
|
-
isDraft: d.isDraft
|
|
148
|
-
})),
|
|
149
|
-
team: teamDocs.data.map((d) => ({
|
|
150
|
-
type: d.type,
|
|
151
|
-
name: d.name,
|
|
152
|
-
version: d.version,
|
|
153
|
-
isDraft: d.isDraft
|
|
154
|
-
}))
|
|
145
|
+
product: summarize(productDocs.data),
|
|
146
|
+
factory: summarize(factoryDocs.data)
|
|
155
147
|
}
|
|
156
148
|
};
|
|
157
149
|
}
|
|
158
|
-
async listBacklogItems(
|
|
150
|
+
async listBacklogItems(productId, opts) {
|
|
159
151
|
const params = new URLSearchParams();
|
|
160
152
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
161
153
|
const qs = params.toString();
|
|
162
|
-
return this.request("GET", `/products/${
|
|
154
|
+
return this.request("GET", `/products/${productId}/backlog${qs ? `?${qs}` : ""}`);
|
|
163
155
|
}
|
|
164
156
|
async getBacklogItem(itemId) {
|
|
165
157
|
return this.request("GET", `/backlog/${itemId}`);
|
|
166
158
|
}
|
|
167
|
-
async listEnvironments(
|
|
168
|
-
return this.request("GET", `/products/${
|
|
159
|
+
async listEnvironments(productId) {
|
|
160
|
+
return this.request("GET", `/products/${productId}/environments`);
|
|
169
161
|
}
|
|
170
|
-
async
|
|
162
|
+
async listFactories(opts) {
|
|
171
163
|
const params = new URLSearchParams();
|
|
172
164
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
173
165
|
const qs = params.toString();
|
|
@@ -203,58 +195,58 @@ function summarizeList(items) {
|
|
|
203
195
|
});
|
|
204
196
|
}
|
|
205
197
|
//#endregion
|
|
206
|
-
//#region src/tools/
|
|
207
|
-
function
|
|
208
|
-
if (client.hasCapability("teams:read")) server.tool("
|
|
198
|
+
//#region src/tools/products.ts
|
|
199
|
+
function registerProductTools(server, client) {
|
|
200
|
+
if (client.hasCapability("teams:read")) server.tool("taiga_list_factories", "List all factories in the organization. A factory is the group that owns a set of products. Use this to find factory IDs for reading factory instructions. Factory instructions carry the factory's standing guidance: domain context, tech preferences, and the conventions its products follow.", { limit: z.number().min(1).max(100).optional().describe("Max results (default 25)") }, {
|
|
209
201
|
readOnlyHint: true,
|
|
210
202
|
openWorldHint: false
|
|
211
203
|
}, async (params) => {
|
|
212
|
-
const result = await client.
|
|
204
|
+
const result = await client.listFactories(params);
|
|
213
205
|
return { content: [{
|
|
214
206
|
type: "text",
|
|
215
207
|
text: JSON.stringify(result.data, null, 2)
|
|
216
208
|
}] };
|
|
217
209
|
});
|
|
218
210
|
if (client.hasCapability("projects:read")) {
|
|
219
|
-
server.tool("
|
|
220
|
-
|
|
211
|
+
server.tool("taiga_list_products", "List products in the organization. Use this to find the product you are working on so you can read its specification, architecture, and threat model. Can filter by factory; a status filter also exists but no longer distinguishes anything (see its description).", {
|
|
212
|
+
factoryId: z.string().uuid().optional().describe("Filter by factory ID -- get factory IDs from taiga_list_factories"),
|
|
221
213
|
status: z.enum([
|
|
222
214
|
"draft",
|
|
223
215
|
"active",
|
|
224
216
|
"archived"
|
|
225
|
-
]).optional().describe("Filter by status. Taiga no longer surfaces
|
|
217
|
+
]).optional().describe("Filter by status. Taiga no longer surfaces product status in its UI (PRO-543), so every product is currently \"draft\" and filtering by \"active\" or \"archived\" returns nothing. Omit this unless you are working with data set through the API directly."),
|
|
226
218
|
limit: z.number().min(1).max(100).optional().describe("Max results (default 25)")
|
|
227
219
|
}, {
|
|
228
220
|
readOnlyHint: true,
|
|
229
221
|
openWorldHint: false
|
|
230
222
|
}, async (params) => {
|
|
231
|
-
const result = await client.
|
|
223
|
+
const result = await client.listProducts(params);
|
|
232
224
|
return { content: [{
|
|
233
225
|
type: "text",
|
|
234
226
|
text: JSON.stringify(result.data, null, 2)
|
|
235
227
|
}] };
|
|
236
228
|
});
|
|
237
|
-
server.tool("
|
|
238
|
-
|
|
229
|
+
server.tool("taiga_list_product_documents", "List documents for a specific product (names and types only). Product documents are generated during intake: specification (requirements), architecture (system design), technology decisions, data flow, privacy assessment, threat model (security risks), risk register, user flow, and service blueprint. Use taiga_get_product_document to read the full content.", {
|
|
230
|
+
productId: z.string().uuid().describe("Product ID -- get from taiga_list_products"),
|
|
239
231
|
limit: z.number().min(1).max(100).optional().describe("Max results (default 25)")
|
|
240
232
|
}, {
|
|
241
233
|
readOnlyHint: true,
|
|
242
234
|
openWorldHint: false
|
|
243
235
|
}, async (params) => {
|
|
244
|
-
const summaries = summarizeList((await client.
|
|
236
|
+
const summaries = summarizeList((await client.listProductDocuments(params.productId, { limit: params.limit })).data);
|
|
245
237
|
return { content: [{
|
|
246
238
|
type: "text",
|
|
247
239
|
text: JSON.stringify(summaries, null, 2)
|
|
248
240
|
}] };
|
|
249
241
|
});
|
|
250
|
-
server.tool("
|
|
251
|
-
|
|
252
|
-
type: z.string().describe("Document type -- get available types from
|
|
242
|
+
server.tool("taiga_get_product_document", "Get the full content of a specific product 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, service_blueprint, ui_direction, codebase_snapshot. The product instructions document is `project_instructions` (its stored type keeps the old name).", {
|
|
243
|
+
productId: z.string().uuid().describe("Product ID -- get from taiga_list_products"),
|
|
244
|
+
type: z.string().describe("Document type -- get available types from taiga_list_product_documents. Common: specification, architecture, threat_model, technology_decisions")
|
|
253
245
|
}, {
|
|
254
246
|
readOnlyHint: true,
|
|
255
247
|
openWorldHint: false
|
|
256
248
|
}, async (params) => {
|
|
257
|
-
const result = await client.
|
|
249
|
+
const result = await client.getProductDocument(params.productId, params.type);
|
|
258
250
|
return { content: [{
|
|
259
251
|
type: "text",
|
|
260
252
|
text: JSON.stringify(result.data, null, 2)
|
|
@@ -266,7 +258,7 @@ function registerProjectTools(server, client) {
|
|
|
266
258
|
//#region src/tools/documents.ts
|
|
267
259
|
function registerDocumentTools(server, client) {
|
|
268
260
|
if (client.hasCapability("tenant:documents:read")) {
|
|
269
|
-
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
|
|
261
|
+
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 factories and products. 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)") }, {
|
|
270
262
|
readOnlyHint: true,
|
|
271
263
|
openWorldHint: false
|
|
272
264
|
}, async (params) => {
|
|
@@ -304,36 +296,36 @@ function registerDocumentTools(server, client) {
|
|
|
304
296
|
}] };
|
|
305
297
|
});
|
|
306
298
|
if (client.hasCapability("teams:read")) {
|
|
307
|
-
server.tool("
|
|
308
|
-
|
|
299
|
+
server.tool("taiga_list_factory_documents", "List documents for a specific factory (names and types only). A factory has one document: its instructions, the factory's free-form standing guidance (domain context, tech preferences, conventions) that applies to every product it owns. Use taiga_get_factory_document to read the full content.", {
|
|
300
|
+
factoryId: z.string().uuid().describe("Factory ID -- get from taiga_list_factories"),
|
|
309
301
|
limit: z.number().min(1).max(100).optional().describe("Max results (default 25)")
|
|
310
302
|
}, {
|
|
311
303
|
readOnlyHint: true,
|
|
312
304
|
openWorldHint: false
|
|
313
305
|
}, async (params) => {
|
|
314
|
-
const summaries = summarizeList((await client.
|
|
306
|
+
const summaries = summarizeList((await client.listFactoryDocuments(params.factoryId, { limit: params.limit })).data);
|
|
315
307
|
return { content: [{
|
|
316
308
|
type: "text",
|
|
317
309
|
text: JSON.stringify(summaries, null, 2)
|
|
318
310
|
}] };
|
|
319
311
|
});
|
|
320
|
-
server.tool("
|
|
321
|
-
|
|
322
|
-
type: z.string().describe("Document type -- get available types from
|
|
312
|
+
server.tool("taiga_get_factory_document", "Get the full content of a factory's instructions. Use this when you need the domain context, tech preferences, or conventions the factory expects every product it owns to follow.", {
|
|
313
|
+
factoryId: z.string().uuid().describe("Factory ID -- get from taiga_list_factories"),
|
|
314
|
+
type: z.string().describe("Document type -- get available types from taiga_list_factory_documents. The factory instructions document is `team_instructions` (its stored type keeps the old name).")
|
|
323
315
|
}, {
|
|
324
316
|
readOnlyHint: true,
|
|
325
317
|
openWorldHint: false
|
|
326
318
|
}, async (params) => {
|
|
327
|
-
const result = await client.
|
|
319
|
+
const result = await client.getFactoryDocument(params.factoryId, params.type);
|
|
328
320
|
return { content: [{
|
|
329
321
|
type: "text",
|
|
330
322
|
text: JSON.stringify(result.data, null, 2)
|
|
331
323
|
}] };
|
|
332
324
|
});
|
|
333
325
|
}
|
|
334
|
-
if (client.hasCapability("teams:read") && client.hasCapability("tenant:documents:write")) server.tool("
|
|
335
|
-
|
|
336
|
-
type: z.string().describe("Document type (
|
|
326
|
+
if (client.hasCapability("teams:read") && client.hasCapability("tenant:documents:write")) server.tool("taiga_update_factory_document", "Update the draft of a factory's instructions. Only updates the draft -- does not publish. Use this when you notice the factory conventions, domain context, or tech preferences need updating while working on the codebase.", {
|
|
327
|
+
factoryId: z.string().uuid().describe("Factory ID -- get from taiga_list_factories"),
|
|
328
|
+
type: z.string().describe("Document type (`team_instructions`)"),
|
|
337
329
|
data: z.record(z.string(), z.unknown()).describe("Document content to save as JSON")
|
|
338
330
|
}, {
|
|
339
331
|
readOnlyHint: false,
|
|
@@ -341,8 +333,8 @@ function registerDocumentTools(server, client) {
|
|
|
341
333
|
idempotentHint: true,
|
|
342
334
|
openWorldHint: false
|
|
343
335
|
}, async (params) => {
|
|
344
|
-
const {
|
|
345
|
-
const result = await client.
|
|
336
|
+
const { factoryId, type, ...body } = params;
|
|
337
|
+
const result = await client.saveFactoryDocumentDraft(factoryId, type, body);
|
|
346
338
|
return { content: [{
|
|
347
339
|
type: "text",
|
|
348
340
|
text: JSON.stringify(result.data, null, 2)
|
|
@@ -356,11 +348,11 @@ function registerDocumentTools(server, client) {
|
|
|
356
348
|
* Only tools matching the key's scopes are registered -- the AI assistant
|
|
357
349
|
* never sees tools it can't use.
|
|
358
350
|
*
|
|
359
|
-
* Beta scope: Knowledge base access (policies and
|
|
360
|
-
* Post-beta:
|
|
351
|
+
* Beta scope: Knowledge base access (policies and factory instructions).
|
|
352
|
+
* Post-beta: Products, backlog items, environments, composite tools.
|
|
361
353
|
*/
|
|
362
354
|
function registerAllTools(server, client) {
|
|
363
|
-
|
|
355
|
+
registerProductTools(server, client);
|
|
364
356
|
registerDocumentTools(server, client);
|
|
365
357
|
}
|
|
366
358
|
//#endregion
|
|
@@ -394,20 +386,20 @@ async function main() {
|
|
|
394
386
|
"Security, data privacy, SDLC, architecture, and compliance rules that apply to all code.",
|
|
395
387
|
"Read the relevant policy BEFORE writing code that touches security, data handling, or infrastructure.",
|
|
396
388
|
"",
|
|
397
|
-
"**
|
|
398
|
-
"
|
|
399
|
-
"
|
|
389
|
+
"**Factory instructions** (taiga_list_factory_documents / taiga_get_factory_document):",
|
|
390
|
+
"A factory's standing guidance: domain context (ubiquitous language, entities, business rules),",
|
|
391
|
+
"tech preferences (approved stack, patterns), and conventions. Read these to understand HOW this factory builds software.",
|
|
400
392
|
"",
|
|
401
|
-
"**
|
|
393
|
+
"**Product documents** (taiga_list_product_documents / taiga_get_product_document):",
|
|
402
394
|
"Specification (what to build), architecture (how it's designed), threat model (security risks),",
|
|
403
|
-
"technology decisions, and data flow. Read these to understand the specific
|
|
395
|
+
"technology decisions, and data flow. Read these to understand the specific product.",
|
|
404
396
|
"",
|
|
405
|
-
"Workflow: Start with
|
|
406
|
-
"
|
|
397
|
+
"Workflow: Start with taiga_list_factories to find your factory, then read its instructions.",
|
|
398
|
+
"For product-specific work, use taiga_list_products to find the product,",
|
|
407
399
|
"then read its specification and architecture.",
|
|
408
400
|
"",
|
|
409
|
-
"When you notice a policy or
|
|
410
|
-
"(taiga_update_document,
|
|
401
|
+
"When you notice a policy or factory instructions are outdated, use the update tools",
|
|
402
|
+
"(taiga_update_document, taiga_update_factory_document) to fix it.",
|
|
411
403
|
"This keeps the knowledge base accurate for everyone."
|
|
412
404
|
].join("\n") });
|
|
413
405
|
registerAllTools(server, client);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taiga-ai/mcp-server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for Taiga -- gives AI assistants access to documents and
|
|
3
|
+
"version": "0.4.0-dev.300",
|
|
4
|
+
"description": "MCP server for Taiga -- gives AI assistants access to documents and products",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"bin": {
|
package/src/client.test.ts
CHANGED
|
@@ -54,7 +54,7 @@ describe('TaigaClient', () => {
|
|
|
54
54
|
const client = makeClient({ apiKey: 'taiga_sk_live_secret42' });
|
|
55
55
|
|
|
56
56
|
// Act
|
|
57
|
-
await client.
|
|
57
|
+
await client.listProducts();
|
|
58
58
|
|
|
59
59
|
// Assert
|
|
60
60
|
const headers = spy.mock.calls[0][1].headers;
|
|
@@ -64,7 +64,7 @@ describe('TaigaClient', () => {
|
|
|
64
64
|
it('should identify itself as taiga-ai-mcp-server in every request', async () => {
|
|
65
65
|
const spy = stubFetch(200, { data: [] });
|
|
66
66
|
|
|
67
|
-
await makeClient().
|
|
67
|
+
await makeClient().listProducts();
|
|
68
68
|
|
|
69
69
|
expect(spy.mock.calls[0][1].headers['User-Agent']).toBe(`taiga-ai-mcp-server/${pkg.version}`);
|
|
70
70
|
});
|
|
@@ -79,7 +79,7 @@ describe('TaigaClient', () => {
|
|
|
79
79
|
stubFetch(403, { error: 'Forbidden' });
|
|
80
80
|
const client = makeClient();
|
|
81
81
|
|
|
82
|
-
await expect(client.
|
|
82
|
+
await expect(client.listProducts()).rejects.toThrow(/403.*Forbidden/);
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
it('should reject with status code even when response body is unreadable', async () => {
|
|
@@ -92,13 +92,13 @@ describe('TaigaClient', () => {
|
|
|
92
92
|
})
|
|
93
93
|
);
|
|
94
94
|
|
|
95
|
-
await expect(makeClient().
|
|
95
|
+
await expect(makeClient().listProducts()).rejects.toThrow(/500/);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
it('should reject when fetch itself throws (network failure)', async () => {
|
|
99
99
|
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
|
|
100
100
|
|
|
101
|
-
await expect(makeClient().
|
|
101
|
+
await expect(makeClient().listProducts()).rejects.toThrow(/ECONNREFUSED/);
|
|
102
102
|
});
|
|
103
103
|
});
|
|
104
104
|
|
|
@@ -111,7 +111,7 @@ describe('TaigaClient', () => {
|
|
|
111
111
|
const spy = stubFetch(200, { data: [] });
|
|
112
112
|
const client = makeClient({ baseUrl: 'https://api.test.com///' });
|
|
113
113
|
|
|
114
|
-
await client.
|
|
114
|
+
await client.listProducts();
|
|
115
115
|
|
|
116
116
|
expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/products');
|
|
117
117
|
});
|
|
@@ -119,7 +119,7 @@ describe('TaigaClient', () => {
|
|
|
119
119
|
it('should omit query string when no optional params are provided', async () => {
|
|
120
120
|
const spy = stubFetch(200, { data: [], nextCursor: null });
|
|
121
121
|
|
|
122
|
-
await makeClient().
|
|
122
|
+
await makeClient().listProducts();
|
|
123
123
|
|
|
124
124
|
expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/products');
|
|
125
125
|
});
|
|
@@ -127,7 +127,7 @@ describe('TaigaClient', () => {
|
|
|
127
127
|
it('should include all optional filters as query params', async () => {
|
|
128
128
|
const spy = stubFetch(200, { data: [], nextCursor: null });
|
|
129
129
|
|
|
130
|
-
await makeClient().
|
|
130
|
+
await makeClient().listProducts({ factoryId: 't1', status: 'active', limit: 10 });
|
|
131
131
|
|
|
132
132
|
const url = spy.mock.calls[0][0] as string;
|
|
133
133
|
expect(url).toContain('factoryId=t1');
|
|
@@ -135,7 +135,7 @@ describe('TaigaClient', () => {
|
|
|
135
135
|
expect(url).toContain('limit=10');
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
it('should nest backlog items under the
|
|
138
|
+
it('should nest backlog items under the product path', async () => {
|
|
139
139
|
const spy = stubFetch(200, { data: [], nextCursor: null });
|
|
140
140
|
|
|
141
141
|
await makeClient().listBacklogItems('proj-1', { limit: 20 });
|
|
@@ -145,7 +145,7 @@ describe('TaigaClient', () => {
|
|
|
145
145
|
expect(url).toContain('limit=20');
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
it('should nest environments under the
|
|
148
|
+
it('should nest environments under the product path', async () => {
|
|
149
149
|
const spy = stubFetch(200, { data: [] });
|
|
150
150
|
|
|
151
151
|
await makeClient().listEnvironments('proj-1');
|
|
@@ -206,10 +206,10 @@ describe('TaigaClient', () => {
|
|
|
206
206
|
// -------------------------------------------------------------------------
|
|
207
207
|
|
|
208
208
|
describe('API contracts', () => {
|
|
209
|
-
it('should GET a single
|
|
209
|
+
it('should GET a single product by id', async () => {
|
|
210
210
|
const spy = stubFetch(200, { data: { id: 'p1', name: 'Alpha' } });
|
|
211
211
|
|
|
212
|
-
const result = await makeClient().
|
|
212
|
+
const result = await makeClient().getProduct('p1');
|
|
213
213
|
|
|
214
214
|
expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/products/p1');
|
|
215
215
|
expect(result.data).toEqual({ id: 'p1', name: 'Alpha' });
|
|
@@ -243,10 +243,10 @@ describe('TaigaClient', () => {
|
|
|
243
243
|
expect(spy.mock.calls[0][0]).toBe('https://api.test.com/v1/backlog/item-1');
|
|
244
244
|
});
|
|
245
245
|
|
|
246
|
-
it('should GET
|
|
246
|
+
it('should GET factories with optional limit', async () => {
|
|
247
247
|
const spy = stubFetch(200, { data: [], nextCursor: null });
|
|
248
248
|
|
|
249
|
-
await makeClient().
|
|
249
|
+
await makeClient().listFactories({ limit: 50 });
|
|
250
250
|
|
|
251
251
|
expect(spy.mock.calls[0][0]).toContain('/v1/factories?limit=50');
|
|
252
252
|
});
|
package/src/client.ts
CHANGED
|
@@ -57,19 +57,19 @@ export class TaigaClient {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// ---------------------------------------------------------------------------
|
|
60
|
-
//
|
|
60
|
+
// Products
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
62
62
|
|
|
63
|
-
async
|
|
63
|
+
async listProducts(opts?: { factoryId?: string; status?: string; limit?: number }) {
|
|
64
64
|
const params = new URLSearchParams();
|
|
65
|
-
if (opts?.
|
|
65
|
+
if (opts?.factoryId) params.set('factoryId', opts.factoryId);
|
|
66
66
|
if (opts?.status) params.set('status', opts.status);
|
|
67
67
|
if (opts?.limit) params.set('limit', String(opts.limit));
|
|
68
68
|
const qs = params.toString();
|
|
69
69
|
return this.request<{ data: unknown[]; nextCursor: string | null }>('GET', `/products${qs ? `?${qs}` : ''}`);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
async
|
|
72
|
+
async getProduct(id: string) {
|
|
73
73
|
return this.request<{ data: unknown }>('GET', `/products/${id}`);
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -96,138 +96,131 @@ export class TaigaClient {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// ---------------------------------------------------------------------------
|
|
99
|
-
//
|
|
99
|
+
// Factory Documents
|
|
100
100
|
// ---------------------------------------------------------------------------
|
|
101
101
|
|
|
102
|
-
async
|
|
102
|
+
async listFactoryDocuments(factoryId: string, opts?: { limit?: number }) {
|
|
103
103
|
const params = new URLSearchParams();
|
|
104
104
|
if (opts?.limit) params.set('limit', String(opts.limit));
|
|
105
105
|
const qs = params.toString();
|
|
106
106
|
return this.request<{ data: unknown[]; nextCursor: string | null }>(
|
|
107
107
|
'GET',
|
|
108
|
-
`/factories/${
|
|
108
|
+
`/factories/${factoryId}/documents${qs ? `?${qs}` : ''}`
|
|
109
109
|
);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
async
|
|
113
|
-
return this.request<{ data: unknown }>('GET', `/factories/${
|
|
112
|
+
async getFactoryDocument(factoryId: string, type: string) {
|
|
113
|
+
return this.request<{ data: unknown }>('GET', `/factories/${factoryId}/documents/${type}`);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
async
|
|
117
|
-
return this.request<{ data: unknown }>('PUT', `/factories/${
|
|
116
|
+
async saveFactoryDocumentDraft(factoryId: string, type: string, body: { data: Record<string, unknown> }) {
|
|
117
|
+
return this.request<{ data: unknown }>('PUT', `/factories/${factoryId}/documents/${type}/draft`, body);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// ---------------------------------------------------------------------------
|
|
121
|
-
//
|
|
121
|
+
// Product Documents
|
|
122
122
|
// ---------------------------------------------------------------------------
|
|
123
123
|
|
|
124
|
-
async
|
|
124
|
+
async listProductDocuments(productId: string, opts?: { limit?: number }) {
|
|
125
125
|
const params = new URLSearchParams();
|
|
126
126
|
if (opts?.limit) params.set('limit', String(opts.limit));
|
|
127
127
|
const qs = params.toString();
|
|
128
128
|
return this.request<{ data: unknown[]; nextCursor: string | null }>(
|
|
129
129
|
'GET',
|
|
130
|
-
`/products/${
|
|
130
|
+
`/products/${productId}/documents${qs ? `?${qs}` : ''}`
|
|
131
131
|
);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
async
|
|
135
|
-
return this.request<{ data: unknown }>('GET', `/products/${
|
|
134
|
+
async getProductDocument(productId: string, type: string) {
|
|
135
|
+
return this.request<{ data: unknown }>('GET', `/products/${productId}/documents/${type}`);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
// ---------------------------------------------------------------------------
|
|
139
|
-
//
|
|
139
|
+
// Factory Documents
|
|
140
140
|
// ---------------------------------------------------------------------------
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
* Fetch the full context for a
|
|
144
|
-
*
|
|
143
|
+
* Fetch the full context for a product in parallel: product metadata,
|
|
144
|
+
* its factory's instructions and the product documents.
|
|
145
145
|
* Returns a structured bundle optimized for coding agent consumption.
|
|
146
146
|
*/
|
|
147
|
-
async
|
|
148
|
-
// First fetch the
|
|
149
|
-
const
|
|
150
|
-
const
|
|
147
|
+
async getProductContext(productId: string): Promise<Record<string, unknown>> {
|
|
148
|
+
// First fetch the product to get its factory
|
|
149
|
+
const product = await this.request<{ data: Record<string, unknown> }>('GET', `/products/${productId}`);
|
|
150
|
+
const factoryId = product.data.factoryId as string | undefined;
|
|
151
151
|
|
|
152
152
|
// Fetch the document lists in parallel.
|
|
153
|
-
const [
|
|
154
|
-
this.
|
|
155
|
-
|
|
156
|
-
? this.
|
|
153
|
+
const [productDocs, factoryDocs] = await Promise.all([
|
|
154
|
+
this.listProductDocuments(productId, { limit: 20 }).catch(() => ({ data: [] })),
|
|
155
|
+
factoryId
|
|
156
|
+
? this.listFactoryDocuments(factoryId, { limit: 20 }).catch(() => ({ data: [] }))
|
|
157
157
|
: Promise.resolve({ data: [] }),
|
|
158
158
|
]);
|
|
159
159
|
|
|
160
160
|
// Fetch full content for key documents (in parallel)
|
|
161
|
-
const
|
|
162
|
-
|
|
161
|
+
const keyProductDocTypes = ['specification', 'architecture', 'technology_decisions'];
|
|
162
|
+
// The factory instructions document; its stored type keeps the old name until PRO-1671.
|
|
163
|
+
const keyFactoryDocTypes = ['team_instructions'];
|
|
163
164
|
|
|
164
165
|
const docFetches: Promise<{ type: string; scope: string; data: unknown }>[] = [];
|
|
165
166
|
|
|
166
|
-
for (const docType of
|
|
167
|
-
const exists = (
|
|
167
|
+
for (const docType of keyProductDocTypes) {
|
|
168
|
+
const exists = (productDocs.data as Record<string, unknown>[]).some((d) => d.type === docType && !d.isDraft);
|
|
168
169
|
if (exists) {
|
|
169
170
|
docFetches.push(
|
|
170
|
-
this.
|
|
171
|
-
.then((r) => ({ type: docType, scope: '
|
|
172
|
-
.catch(() => ({ type: docType, scope: '
|
|
171
|
+
this.getProductDocument(productId, docType)
|
|
172
|
+
.then((r) => ({ type: docType, scope: 'product', data: r.data }))
|
|
173
|
+
.catch(() => ({ type: docType, scope: 'product', data: null }))
|
|
173
174
|
);
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
if (
|
|
178
|
-
for (const docType of
|
|
179
|
-
const exists = (
|
|
178
|
+
if (factoryId) {
|
|
179
|
+
for (const docType of keyFactoryDocTypes) {
|
|
180
|
+
const exists = (factoryDocs.data as Record<string, unknown>[]).some((d) => d.type === docType && !d.isDraft);
|
|
180
181
|
if (exists) {
|
|
181
182
|
docFetches.push(
|
|
182
|
-
this.
|
|
183
|
-
.then((r) => ({ type: docType, scope: '
|
|
184
|
-
.catch(() => ({ type: docType, scope: '
|
|
183
|
+
this.getFactoryDocument(factoryId, docType)
|
|
184
|
+
.then((r) => ({ type: docType, scope: 'factory', data: r.data }))
|
|
185
|
+
.catch(() => ({ type: docType, scope: 'factory', data: null }))
|
|
185
186
|
);
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
const fetchedDocs = await Promise.all(docFetches);
|
|
192
|
+
const summarize = (docs: Record<string, unknown>[]) =>
|
|
193
|
+
docs.map((d) => ({ type: d.type, name: d.name, version: d.version, isDraft: d.isDraft }));
|
|
191
194
|
|
|
192
195
|
// Assemble context bundle
|
|
193
|
-
|
|
194
|
-
|
|
196
|
+
return {
|
|
197
|
+
product: product.data,
|
|
195
198
|
documents: {
|
|
196
|
-
|
|
197
|
-
fetchedDocs.filter((d) => d.scope === '
|
|
199
|
+
product: Object.fromEntries(
|
|
200
|
+
fetchedDocs.filter((d) => d.scope === 'product' && d.data).map((d) => [d.type, d.data])
|
|
201
|
+
),
|
|
202
|
+
factory: Object.fromEntries(
|
|
203
|
+
fetchedDocs.filter((d) => d.scope === 'factory' && d.data).map((d) => [d.type, d.data])
|
|
198
204
|
),
|
|
199
|
-
team: Object.fromEntries(fetchedDocs.filter((d) => d.scope === 'team' && d.data).map((d) => [d.type, d.data])),
|
|
200
205
|
},
|
|
201
206
|
available_documents: {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
name: d.name,
|
|
205
|
-
version: d.version,
|
|
206
|
-
isDraft: d.isDraft,
|
|
207
|
-
})),
|
|
208
|
-
team: (teamDocs.data as Record<string, unknown>[]).map((d) => ({
|
|
209
|
-
type: d.type,
|
|
210
|
-
name: d.name,
|
|
211
|
-
version: d.version,
|
|
212
|
-
isDraft: d.isDraft,
|
|
213
|
-
})),
|
|
207
|
+
product: summarize(productDocs.data as Record<string, unknown>[]),
|
|
208
|
+
factory: summarize(factoryDocs.data as Record<string, unknown>[]),
|
|
214
209
|
},
|
|
215
210
|
};
|
|
216
|
-
|
|
217
|
-
return context;
|
|
218
211
|
}
|
|
219
212
|
|
|
220
213
|
// ---------------------------------------------------------------------------
|
|
221
214
|
// Backlog
|
|
222
215
|
// ---------------------------------------------------------------------------
|
|
223
216
|
|
|
224
|
-
async listBacklogItems(
|
|
217
|
+
async listBacklogItems(productId: string, opts?: { limit?: number }) {
|
|
225
218
|
const params = new URLSearchParams();
|
|
226
219
|
if (opts?.limit) params.set('limit', String(opts.limit));
|
|
227
220
|
const qs = params.toString();
|
|
228
221
|
return this.request<{ data: unknown[]; nextCursor: string | null }>(
|
|
229
222
|
'GET',
|
|
230
|
-
`/products/${
|
|
223
|
+
`/products/${productId}/backlog${qs ? `?${qs}` : ''}`
|
|
231
224
|
);
|
|
232
225
|
}
|
|
233
226
|
|
|
@@ -239,15 +232,15 @@ export class TaigaClient {
|
|
|
239
232
|
// Environments
|
|
240
233
|
// ---------------------------------------------------------------------------
|
|
241
234
|
|
|
242
|
-
async listEnvironments(
|
|
243
|
-
return this.request<{ data: unknown[] }>('GET', `/products/${
|
|
235
|
+
async listEnvironments(productId: string) {
|
|
236
|
+
return this.request<{ data: unknown[] }>('GET', `/products/${productId}/environments`);
|
|
244
237
|
}
|
|
245
238
|
|
|
246
239
|
// ---------------------------------------------------------------------------
|
|
247
|
-
//
|
|
240
|
+
// Factories
|
|
248
241
|
// ---------------------------------------------------------------------------
|
|
249
242
|
|
|
250
|
-
async
|
|
243
|
+
async listFactories(opts?: { limit?: number }) {
|
|
251
244
|
const params = new URLSearchParams();
|
|
252
245
|
if (opts?.limit) params.set('limit', String(opts.limit));
|
|
253
246
|
const qs = params.toString();
|
package/src/index.ts
CHANGED
|
@@ -43,20 +43,20 @@ async function main() {
|
|
|
43
43
|
'Security, data privacy, SDLC, architecture, and compliance rules that apply to all code.',
|
|
44
44
|
'Read the relevant policy BEFORE writing code that touches security, data handling, or infrastructure.',
|
|
45
45
|
'',
|
|
46
|
-
'**
|
|
47
|
-
'
|
|
48
|
-
'
|
|
46
|
+
'**Factory instructions** (taiga_list_factory_documents / taiga_get_factory_document):',
|
|
47
|
+
"A factory's standing guidance: domain context (ubiquitous language, entities, business rules),",
|
|
48
|
+
'tech preferences (approved stack, patterns), and conventions. Read these to understand HOW this factory builds software.',
|
|
49
49
|
'',
|
|
50
|
-
'**
|
|
50
|
+
'**Product documents** (taiga_list_product_documents / taiga_get_product_document):',
|
|
51
51
|
"Specification (what to build), architecture (how it's designed), threat model (security risks),",
|
|
52
|
-
'technology decisions, and data flow. Read these to understand the specific
|
|
52
|
+
'technology decisions, and data flow. Read these to understand the specific product.',
|
|
53
53
|
'',
|
|
54
|
-
|
|
55
|
-
'
|
|
54
|
+
'Workflow: Start with taiga_list_factories to find your factory, then read its instructions.',
|
|
55
|
+
'For product-specific work, use taiga_list_products to find the product,',
|
|
56
56
|
'then read its specification and architecture.',
|
|
57
57
|
'',
|
|
58
|
-
'When you notice a policy or
|
|
59
|
-
'(taiga_update_document,
|
|
58
|
+
'When you notice a policy or factory instructions are outdated, use the update tools',
|
|
59
|
+
'(taiga_update_document, taiga_update_factory_document) to fix it.',
|
|
60
60
|
'This keeps the knowledge base accurate for everyone.',
|
|
61
61
|
].join('\n'),
|
|
62
62
|
}
|
|
@@ -24,15 +24,15 @@ const ALL_CAPABILITIES = [
|
|
|
24
24
|
*/
|
|
25
25
|
const ALL_TOOL_NAMES = [
|
|
26
26
|
'taiga_get_document',
|
|
27
|
-
'
|
|
28
|
-
'
|
|
27
|
+
'taiga_get_factory_document',
|
|
28
|
+
'taiga_get_product_document',
|
|
29
29
|
'taiga_list_documents',
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
30
|
+
'taiga_list_factories',
|
|
31
|
+
'taiga_list_factory_documents',
|
|
32
|
+
'taiga_list_product_documents',
|
|
33
|
+
'taiga_list_products',
|
|
34
34
|
'taiga_update_document',
|
|
35
|
-
'
|
|
35
|
+
'taiga_update_factory_document',
|
|
36
36
|
];
|
|
37
37
|
|
|
38
38
|
function makeFetchRouter(routes: Record<string, unknown>) {
|
|
@@ -146,15 +146,15 @@ describe('MCP server integration', () => {
|
|
|
146
146
|
await expect(client.listTools()).rejects.toThrow(/Method not found/);
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
it('should expose only
|
|
149
|
+
it('should expose only factory tools for a teams:read-only key', async () => {
|
|
150
150
|
const { client } = await bootMcpPair({ capabilities: ['teams:read'] });
|
|
151
151
|
|
|
152
152
|
const { tools } = await client.listTools();
|
|
153
153
|
const names = toolNames(tools);
|
|
154
154
|
|
|
155
|
-
expect(names).toContain('
|
|
156
|
-
expect(names).toContain('
|
|
157
|
-
expect(names).toContain('
|
|
155
|
+
expect(names).toContain('taiga_list_factories');
|
|
156
|
+
expect(names).toContain('taiga_list_factory_documents');
|
|
157
|
+
expect(names).toContain('taiga_get_factory_document');
|
|
158
158
|
expect(names).not.toContain('taiga_list_documents');
|
|
159
159
|
expect(names).not.toContain('taiga_update_document');
|
|
160
160
|
});
|
|
@@ -181,7 +181,7 @@ describe('MCP server integration', () => {
|
|
|
181
181
|
expect(names).toContain('taiga_update_document');
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
it('should expose
|
|
184
|
+
it('should expose factory document write when both teams:read and documents:write are granted', async () => {
|
|
185
185
|
const { client } = await bootMcpPair({
|
|
186
186
|
capabilities: ['teams:read', 'tenant:documents:write'],
|
|
187
187
|
});
|
|
@@ -189,8 +189,8 @@ describe('MCP server integration', () => {
|
|
|
189
189
|
const { tools } = await client.listTools();
|
|
190
190
|
const names = toolNames(tools);
|
|
191
191
|
|
|
192
|
-
expect(names).toContain('
|
|
193
|
-
expect(names).toContain('
|
|
192
|
+
expect(names).toContain('taiga_update_factory_document');
|
|
193
|
+
expect(names).toContain('taiga_list_factories');
|
|
194
194
|
});
|
|
195
195
|
|
|
196
196
|
it('should combine capabilities additively', async () => {
|
|
@@ -202,8 +202,8 @@ describe('MCP server integration', () => {
|
|
|
202
202
|
const names = toolNames(tools);
|
|
203
203
|
|
|
204
204
|
// teams:read tools present
|
|
205
|
-
expect(names).toContain('
|
|
206
|
-
expect(names).toContain('
|
|
205
|
+
expect(names).toContain('taiga_list_factories');
|
|
206
|
+
expect(names).toContain('taiga_list_factory_documents');
|
|
207
207
|
// tenant:documents:read tools present
|
|
208
208
|
expect(names).toContain('taiga_list_documents');
|
|
209
209
|
// write tools absent
|
|
@@ -231,8 +231,8 @@ describe('MCP server integration', () => {
|
|
|
231
231
|
|
|
232
232
|
describe('capability-tool contract', () => {
|
|
233
233
|
const CONTRACT: Record<string, string[]> = {
|
|
234
|
-
'projects:read': ['
|
|
235
|
-
'teams:read': ['
|
|
234
|
+
'projects:read': ['taiga_list_products', 'taiga_list_product_documents', 'taiga_get_product_document'],
|
|
235
|
+
'teams:read': ['taiga_list_factories', 'taiga_list_factory_documents', 'taiga_get_factory_document'],
|
|
236
236
|
'tenant:documents:read': ['taiga_list_documents', 'taiga_get_document'],
|
|
237
237
|
'tenant:documents:write': ['taiga_update_document'],
|
|
238
238
|
};
|
|
@@ -258,21 +258,21 @@ describe('MCP server integration', () => {
|
|
|
258
258
|
// -------------------------------------------------------------------------
|
|
259
259
|
|
|
260
260
|
describe('tool execution', () => {
|
|
261
|
-
it('should return
|
|
262
|
-
const
|
|
261
|
+
it('should return factory list as JSON text content', async () => {
|
|
262
|
+
const factories = [
|
|
263
263
|
{ id: 't1', name: 'Product' },
|
|
264
264
|
{ id: 't2', name: 'Platform' },
|
|
265
265
|
];
|
|
266
266
|
|
|
267
267
|
const { client } = await bootMcpPair({
|
|
268
268
|
capabilities: ALL_CAPABILITIES,
|
|
269
|
-
routes: { 'GET /v1/factories': { data:
|
|
269
|
+
routes: { 'GET /v1/factories': { data: factories, nextCursor: null } },
|
|
270
270
|
});
|
|
271
271
|
|
|
272
|
-
const result = await client.callTool({ name: '
|
|
272
|
+
const result = await client.callTool({ name: 'taiga_list_factories', arguments: {} });
|
|
273
273
|
|
|
274
274
|
expect(result.content).toHaveLength(1);
|
|
275
|
-
expect(parseToolResult(result)).toEqual(
|
|
275
|
+
expect(parseToolResult(result)).toEqual(factories);
|
|
276
276
|
});
|
|
277
277
|
|
|
278
278
|
it('should return document content through MCP protocol', async () => {
|
|
@@ -307,39 +307,39 @@ describe('MCP server integration', () => {
|
|
|
307
307
|
expect(putCall![0]).toContain(`/v1/tenants/current/documents/${docType}/draft`);
|
|
308
308
|
});
|
|
309
309
|
|
|
310
|
-
it('should send PUT when updating a
|
|
311
|
-
const
|
|
310
|
+
it('should send PUT when updating a factory document draft', async () => {
|
|
311
|
+
const factoryId = '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b';
|
|
312
312
|
const docType = 'domain_context';
|
|
313
313
|
const updated = { type: docType, name: 'Domain Context' };
|
|
314
314
|
|
|
315
315
|
const { client, fetchSpy } = await bootMcpPair({
|
|
316
316
|
capabilities: ALL_CAPABILITIES,
|
|
317
|
-
routes: { [`PUT /v1/factories/${
|
|
317
|
+
routes: { [`PUT /v1/factories/${factoryId}/documents/${docType}/draft`]: { data: updated } },
|
|
318
318
|
});
|
|
319
319
|
|
|
320
320
|
await client.callTool({
|
|
321
|
-
name: '
|
|
322
|
-
arguments: {
|
|
321
|
+
name: 'taiga_update_factory_document',
|
|
322
|
+
arguments: { factoryId, type: docType, data: { sections: { overview: 'Updated' } } },
|
|
323
323
|
});
|
|
324
324
|
|
|
325
325
|
const putCall = fetchSpy.mock.calls.find(
|
|
326
326
|
(c: unknown[]) => (c[1] as RequestInit)?.method === 'PUT' && (c[0] as string).includes('/factories/')
|
|
327
327
|
);
|
|
328
328
|
expect(putCall).toBeDefined();
|
|
329
|
-
expect(putCall![0]).toContain(`/v1/factories/${
|
|
329
|
+
expect(putCall![0]).toContain(`/v1/factories/${factoryId}/documents/${docType}/draft`);
|
|
330
330
|
});
|
|
331
331
|
|
|
332
|
-
it('should return
|
|
333
|
-
const
|
|
332
|
+
it('should return factories through MCP protocol', async () => {
|
|
333
|
+
const factories = [{ id: 't1', name: 'Engineering' }];
|
|
334
334
|
|
|
335
335
|
const { client } = await bootMcpPair({
|
|
336
336
|
capabilities: ALL_CAPABILITIES,
|
|
337
|
-
routes: { 'GET /v1/factories': { data:
|
|
337
|
+
routes: { 'GET /v1/factories': { data: factories, nextCursor: null } },
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
-
const result = await client.callTool({ name: '
|
|
340
|
+
const result = await client.callTool({ name: 'taiga_list_factories', arguments: {} });
|
|
341
341
|
|
|
342
|
-
expect(parseToolResult(result)).toEqual(
|
|
342
|
+
expect(parseToolResult(result)).toEqual(factories);
|
|
343
343
|
});
|
|
344
344
|
});
|
|
345
345
|
|
|
@@ -380,7 +380,7 @@ describe('MCP server integration', () => {
|
|
|
380
380
|
await Promise.all([server.connect(st), mcpClient.connect(ct)]);
|
|
381
381
|
|
|
382
382
|
// Act
|
|
383
|
-
const result = await mcpClient.callTool({ name: '
|
|
383
|
+
const result = await mcpClient.callTool({ name: 'taiga_list_products', arguments: {} });
|
|
384
384
|
|
|
385
385
|
// Assert -- the MCP SDK marks tool errors with isError
|
|
386
386
|
expect(result.isError).toBe(true);
|
|
@@ -392,9 +392,9 @@ describe('MCP server integration', () => {
|
|
|
392
392
|
routes: {},
|
|
393
393
|
});
|
|
394
394
|
|
|
395
|
-
//
|
|
395
|
+
// taiga_get_product requires a UUID, not a plain string
|
|
396
396
|
const result = await client.callTool({
|
|
397
|
-
name: '
|
|
397
|
+
name: 'taiga_get_product',
|
|
398
398
|
arguments: { id: 'not-a-uuid' },
|
|
399
399
|
});
|
|
400
400
|
|
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, factory instructions).
|
|
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
|
@@ -11,7 +11,7 @@ export function registerDocumentTools(server: McpServer, client: TaigaClient) {
|
|
|
11
11
|
'taiga_list_documents',
|
|
12
12
|
'List company-level policy documents (names and types only). ' +
|
|
13
13
|
'Policies define security, data privacy, SDLC, operations, architecture, and compliance rules ' +
|
|
14
|
-
'that apply across all
|
|
14
|
+
'that apply across all factories and products. ' +
|
|
15
15
|
'Use taiga_get_document with the document type to read the full policy content.',
|
|
16
16
|
{
|
|
17
17
|
limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
|
|
@@ -72,23 +72,23 @@ export function registerDocumentTools(server: McpServer, client: TaigaClient) {
|
|
|
72
72
|
);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
// ──
|
|
75
|
+
// ── Factory instructions ────────────────────────────────────────────
|
|
76
|
+
// Capability strings still say teams until the database rename (PRO-1671).
|
|
76
77
|
|
|
77
78
|
if (client.hasCapability('teams:read')) {
|
|
78
79
|
server.tool(
|
|
79
|
-
'
|
|
80
|
-
'List documents for a specific
|
|
81
|
-
|
|
82
|
-
'tech preferences
|
|
83
|
-
'
|
|
84
|
-
'Use taiga_get_team_document to read the full content.',
|
|
80
|
+
'taiga_list_factory_documents',
|
|
81
|
+
'List documents for a specific factory (names and types only). ' +
|
|
82
|
+
"A factory has one document: its instructions, the factory's free-form standing guidance " +
|
|
83
|
+
'(domain context, tech preferences, conventions) that applies to every product it owns. ' +
|
|
84
|
+
'Use taiga_get_factory_document to read the full content.',
|
|
85
85
|
{
|
|
86
|
-
|
|
86
|
+
factoryId: z.string().uuid().describe('Factory ID -- get from taiga_list_factories'),
|
|
87
87
|
limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
|
|
88
88
|
},
|
|
89
89
|
{ readOnlyHint: true, openWorldHint: false },
|
|
90
90
|
async (params) => {
|
|
91
|
-
const result = await client.
|
|
91
|
+
const result = await client.listFactoryDocuments(params.factoryId, { limit: params.limit });
|
|
92
92
|
const summaries = summarizeList(result.data as Record<string, unknown>[]);
|
|
93
93
|
return {
|
|
94
94
|
content: [{ type: 'text' as const, text: JSON.stringify(summaries, null, 2) }],
|
|
@@ -97,24 +97,22 @@ export function registerDocumentTools(server: McpServer, client: TaigaClient) {
|
|
|
97
97
|
);
|
|
98
98
|
|
|
99
99
|
server.tool(
|
|
100
|
-
'
|
|
101
|
-
|
|
102
|
-
'Use this when you need the
|
|
103
|
-
'
|
|
104
|
-
'Types: team_charter, definition_of_done, tech_preferences, ' +
|
|
105
|
-
'architecture_constraints, slo_targets, domain_context.',
|
|
100
|
+
'taiga_get_factory_document',
|
|
101
|
+
"Get the full content of a factory's instructions. " +
|
|
102
|
+
'Use this when you need the domain context, tech preferences, or conventions ' +
|
|
103
|
+
'the factory expects every product it owns to follow.',
|
|
106
104
|
{
|
|
107
|
-
|
|
105
|
+
factoryId: z.string().uuid().describe('Factory ID -- get from taiga_list_factories'),
|
|
108
106
|
type: z
|
|
109
107
|
.string()
|
|
110
108
|
.describe(
|
|
111
|
-
'Document type -- get available types from
|
|
112
|
-
'
|
|
109
|
+
'Document type -- get available types from taiga_list_factory_documents. ' +
|
|
110
|
+
'The factory instructions document is `team_instructions` (its stored type keeps the old name).'
|
|
113
111
|
),
|
|
114
112
|
},
|
|
115
113
|
{ readOnlyHint: true, openWorldHint: false },
|
|
116
114
|
async (params) => {
|
|
117
|
-
const result = await client.
|
|
115
|
+
const result = await client.getFactoryDocument(params.factoryId, params.type);
|
|
118
116
|
return {
|
|
119
117
|
content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
|
|
120
118
|
};
|
|
@@ -122,22 +120,26 @@ export function registerDocumentTools(server: McpServer, client: TaigaClient) {
|
|
|
122
120
|
);
|
|
123
121
|
}
|
|
124
122
|
|
|
125
|
-
//
|
|
123
|
+
// Factory document write requires both teams:read (to find the factory) and tenant:documents:write
|
|
126
124
|
if (client.hasCapability('teams:read') && client.hasCapability('tenant:documents:write')) {
|
|
127
125
|
server.tool(
|
|
128
|
-
'
|
|
129
|
-
|
|
130
|
-
'Only updates the draft -- does not publish. Use this when you notice
|
|
126
|
+
'taiga_update_factory_document',
|
|
127
|
+
"Update the draft of a factory's instructions. " +
|
|
128
|
+
'Only updates the draft -- does not publish. Use this when you notice the factory conventions, ' +
|
|
131
129
|
'domain context, or tech preferences need updating while working on the codebase.',
|
|
132
130
|
{
|
|
133
|
-
|
|
134
|
-
type: z.string().describe('Document type (
|
|
131
|
+
factoryId: z.string().uuid().describe('Factory ID -- get from taiga_list_factories'),
|
|
132
|
+
type: z.string().describe('Document type (`team_instructions`)'),
|
|
135
133
|
data: z.record(z.string(), z.unknown()).describe('Document content to save as JSON'),
|
|
136
134
|
},
|
|
137
135
|
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
138
136
|
async (params) => {
|
|
139
|
-
const {
|
|
140
|
-
const result = await client.
|
|
137
|
+
const { factoryId, type, ...body } = params;
|
|
138
|
+
const result = await client.saveFactoryDocumentDraft(
|
|
139
|
+
factoryId,
|
|
140
|
+
type,
|
|
141
|
+
body as { data: Record<string, unknown> }
|
|
142
|
+
);
|
|
141
143
|
return {
|
|
142
144
|
content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
|
|
143
145
|
};
|
package/src/tools/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { TaigaClient } from '../client.js';
|
|
3
|
-
import {
|
|
3
|
+
import { registerProductTools } from './products.js';
|
|
4
4
|
import { registerDocumentTools } from './documents.js';
|
|
5
5
|
import { registerBacklogTools } from './backlogs.js';
|
|
6
6
|
|
|
@@ -9,11 +9,11 @@ import { registerBacklogTools } from './backlogs.js';
|
|
|
9
9
|
* Only tools matching the key's scopes are registered -- the AI assistant
|
|
10
10
|
* never sees tools it can't use.
|
|
11
11
|
*
|
|
12
|
-
* Beta scope: Knowledge base access (policies and
|
|
13
|
-
* Post-beta:
|
|
12
|
+
* Beta scope: Knowledge base access (policies and factory instructions).
|
|
13
|
+
* Post-beta: Products, backlog items, environments, composite tools.
|
|
14
14
|
*/
|
|
15
15
|
export function registerAllTools(server: McpServer, client: TaigaClient): void {
|
|
16
|
-
|
|
16
|
+
registerProductTools(server, client);
|
|
17
17
|
registerDocumentTools(server, client);
|
|
18
18
|
registerBacklogTools(server, client);
|
|
19
19
|
}
|
|
@@ -3,19 +3,21 @@ import { z } from 'zod';
|
|
|
3
3
|
import type { TaigaClient } from '../client.js';
|
|
4
4
|
import { summarizeList } from './summarize.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// Capability strings still say teams/projects until the database rename (PRO-1671).
|
|
7
|
+
export function registerProductTools(server: McpServer, client: TaigaClient) {
|
|
7
8
|
if (client.hasCapability('teams:read')) {
|
|
8
9
|
server.tool(
|
|
9
|
-
'
|
|
10
|
-
'List all
|
|
11
|
-
'
|
|
12
|
-
'
|
|
10
|
+
'taiga_list_factories',
|
|
11
|
+
'List all factories in the organization. A factory is the group that owns a set of products. ' +
|
|
12
|
+
'Use this to find factory IDs for reading factory instructions. ' +
|
|
13
|
+
"Factory instructions carry the factory's standing guidance: domain context, tech preferences, " +
|
|
14
|
+
'and the conventions its products follow.',
|
|
13
15
|
{
|
|
14
16
|
limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
|
|
15
17
|
},
|
|
16
18
|
{ readOnlyHint: true, openWorldHint: false },
|
|
17
19
|
async (params) => {
|
|
18
|
-
const result = await client.
|
|
20
|
+
const result = await client.listFactories(params);
|
|
19
21
|
return {
|
|
20
22
|
content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
|
|
21
23
|
};
|
|
@@ -25,25 +27,29 @@ export function registerProjectTools(server: McpServer, client: TaigaClient) {
|
|
|
25
27
|
|
|
26
28
|
if (client.hasCapability('projects:read')) {
|
|
27
29
|
server.tool(
|
|
28
|
-
'
|
|
29
|
-
'List
|
|
30
|
+
'taiga_list_products',
|
|
31
|
+
'List products in the organization. Use this to find the product you are working on ' +
|
|
30
32
|
'so you can read its specification, architecture, and threat model. ' +
|
|
31
|
-
'Can filter by
|
|
33
|
+
'Can filter by factory; a status filter also exists but no longer distinguishes anything (see its description).',
|
|
32
34
|
{
|
|
33
|
-
|
|
35
|
+
factoryId: z
|
|
36
|
+
.string()
|
|
37
|
+
.uuid()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('Filter by factory ID -- get factory IDs from taiga_list_factories'),
|
|
34
40
|
status: z
|
|
35
41
|
.enum(['draft', 'active', 'archived'])
|
|
36
42
|
.optional()
|
|
37
43
|
.describe(
|
|
38
|
-
'Filter by status. Taiga no longer surfaces
|
|
39
|
-
'
|
|
44
|
+
'Filter by status. Taiga no longer surfaces product status in its UI (PRO-543), so every ' +
|
|
45
|
+
'product is currently "draft" and filtering by "active" or "archived" returns nothing. ' +
|
|
40
46
|
'Omit this unless you are working with data set through the API directly.'
|
|
41
47
|
),
|
|
42
48
|
limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
|
|
43
49
|
},
|
|
44
50
|
{ readOnlyHint: true, openWorldHint: false },
|
|
45
51
|
async (params) => {
|
|
46
|
-
const result = await client.
|
|
52
|
+
const result = await client.listProducts(params);
|
|
47
53
|
return {
|
|
48
54
|
content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
|
|
49
55
|
};
|
|
@@ -51,19 +57,19 @@ export function registerProjectTools(server: McpServer, client: TaigaClient) {
|
|
|
51
57
|
);
|
|
52
58
|
|
|
53
59
|
server.tool(
|
|
54
|
-
'
|
|
55
|
-
'List documents for a specific
|
|
56
|
-
'
|
|
60
|
+
'taiga_list_product_documents',
|
|
61
|
+
'List documents for a specific product (names and types only). ' +
|
|
62
|
+
'Product documents are generated during intake: specification (requirements), ' +
|
|
57
63
|
'architecture (system design), technology decisions, data flow, privacy assessment, ' +
|
|
58
|
-
'threat model (security risks), risk register, and
|
|
59
|
-
'Use
|
|
64
|
+
'threat model (security risks), risk register, user flow, and service blueprint. ' +
|
|
65
|
+
'Use taiga_get_product_document to read the full content.',
|
|
60
66
|
{
|
|
61
|
-
|
|
67
|
+
productId: z.string().uuid().describe('Product ID -- get from taiga_list_products'),
|
|
62
68
|
limit: z.number().min(1).max(100).optional().describe('Max results (default 25)'),
|
|
63
69
|
},
|
|
64
70
|
{ readOnlyHint: true, openWorldHint: false },
|
|
65
71
|
async (params) => {
|
|
66
|
-
const result = await client.
|
|
72
|
+
const result = await client.listProductDocuments(params.productId, { limit: params.limit });
|
|
67
73
|
const summaries = summarizeList(result.data as Record<string, unknown>[]);
|
|
68
74
|
return {
|
|
69
75
|
content: [{ type: 'text' as const, text: JSON.stringify(summaries, null, 2) }],
|
|
@@ -72,25 +78,27 @@ export function registerProjectTools(server: McpServer, client: TaigaClient) {
|
|
|
72
78
|
);
|
|
73
79
|
|
|
74
80
|
server.tool(
|
|
75
|
-
'
|
|
76
|
-
'Get the full content of a specific
|
|
81
|
+
'taiga_get_product_document',
|
|
82
|
+
'Get the full content of a specific product document. ' +
|
|
77
83
|
'Use this when you need to understand what to build (specification), ' +
|
|
78
84
|
'how the system is designed (architecture), what security risks exist (threat model), ' +
|
|
79
85
|
'or what technology choices were made (technology_decisions). ' +
|
|
80
86
|
'Types: specification, architecture, technology_decisions, data_flow, ' +
|
|
81
|
-
'privacy_assessment, threat_model, risk_register, user_flow
|
|
87
|
+
'privacy_assessment, threat_model, risk_register, user_flow, service_blueprint, ' +
|
|
88
|
+
'ui_direction, codebase_snapshot. The product instructions document is ' +
|
|
89
|
+
'`project_instructions` (its stored type keeps the old name).',
|
|
82
90
|
{
|
|
83
|
-
|
|
91
|
+
productId: z.string().uuid().describe('Product ID -- get from taiga_list_products'),
|
|
84
92
|
type: z
|
|
85
93
|
.string()
|
|
86
94
|
.describe(
|
|
87
|
-
'Document type -- get available types from
|
|
95
|
+
'Document type -- get available types from taiga_list_product_documents. ' +
|
|
88
96
|
'Common: specification, architecture, threat_model, technology_decisions'
|
|
89
97
|
),
|
|
90
98
|
},
|
|
91
99
|
{ readOnlyHint: true, openWorldHint: false },
|
|
92
100
|
async (params) => {
|
|
93
|
-
const result = await client.
|
|
101
|
+
const result = await client.getProductDocument(params.productId, params.type);
|
|
94
102
|
return {
|
|
95
103
|
content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }],
|
|
96
104
|
};
|
|
Binary file
|
|
Binary file
|