@taiga-ai/mcp-server 0.3.0-dev.298 → 0.4.0-dev.299

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 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 listProjects(opts) {
51
+ async listProducts(opts) {
52
52
  const params = new URLSearchParams();
53
- if (opts?.teamId) params.set("factoryId", opts.teamId);
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 getProject(id) {
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 listTeamDocuments(teamId, opts) {
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/${teamId}/documents${qs ? `?${qs}` : ""}`);
78
+ return this.request("GET", `/factories/${factoryId}/documents${qs ? `?${qs}` : ""}`);
79
79
  }
80
- async getTeamDocument(teamId, type) {
81
- return this.request("GET", `/factories/${teamId}/documents/${type}`);
80
+ async getFactoryDocument(factoryId, type) {
81
+ return this.request("GET", `/factories/${factoryId}/documents/${type}`);
82
82
  }
83
- async saveTeamDocumentDraft(teamId, type, body) {
84
- return this.request("PUT", `/factories/${teamId}/documents/${type}/draft`, body);
83
+ async saveFactoryDocumentDraft(factoryId, type, body) {
84
+ return this.request("PUT", `/factories/${factoryId}/documents/${type}/draft`, body);
85
85
  }
86
- async listProjectDocuments(projectId, opts) {
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/${projectId}/documents${qs ? `?${qs}` : ""}`);
90
+ return this.request("GET", `/products/${productId}/documents${qs ? `?${qs}` : ""}`);
91
91
  }
92
- async getProjectDocument(projectId, type) {
93
- return this.request("GET", `/products/${projectId}/documents/${type}`);
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 project in parallel: project metadata,
97
- * team documents and project documents.
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 getProjectContext(projectId) {
101
- const project = await this.request("GET", `/products/${projectId}`);
102
- const teamId = project.data.factoryId;
103
- const [projectDocs, teamDocs] = await Promise.all([this.listProjectDocuments(projectId, { limit: 20 }).catch(() => ({ data: [] })), teamId ? this.listTeamDocuments(teamId, { limit: 20 }).catch(() => ({ data: [] })) : Promise.resolve({ data: [] })]);
104
- const keyProjectDocTypes = [
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 keyTeamDocTypes = [
110
- "domain_context",
111
- "tech_preferences",
112
- "architecture_constraints"
113
- ];
109
+ const keyFactoryDocTypes = ["team_instructions"];
114
110
  const docFetches = [];
115
- for (const docType of keyProjectDocTypes) if (projectDocs.data.some((d) => d.type === docType && !d.isDraft)) docFetches.push(this.getProjectDocument(projectId, docType).then((r) => ({
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: "project",
113
+ scope: "product",
118
114
  data: r.data
119
115
  })).catch(() => ({
120
116
  type: docType,
121
- scope: "project",
117
+ scope: "product",
122
118
  data: null
123
119
  })));
124
- if (teamId) {
125
- for (const docType of keyTeamDocTypes) if (teamDocs.data.some((d) => d.type === docType && !d.isDraft)) docFetches.push(this.getTeamDocument(teamId, docType).then((r) => ({
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: "team",
123
+ scope: "factory",
128
124
  data: r.data
129
125
  })).catch(() => ({
130
126
  type: docType,
131
- scope: "team",
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
- project: project.data,
139
+ product: product.data,
138
140
  documents: {
139
- project: Object.fromEntries(fetchedDocs.filter((d) => d.scope === "project" && d.data).map((d) => [d.type, d.data])),
140
- team: Object.fromEntries(fetchedDocs.filter((d) => d.scope === "team" && d.data).map((d) => [d.type, d.data]))
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
- project: projectDocs.data.map((d) => ({
144
- type: d.type,
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(projectId, opts) {
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/${projectId}/backlog${qs ? `?${qs}` : ""}`);
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(projectId) {
168
- return this.request("GET", `/products/${projectId}/environments`);
159
+ async listEnvironments(productId) {
160
+ return this.request("GET", `/products/${productId}/environments`);
169
161
  }
170
- async listTeams(opts) {
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/projects.ts
207
- function registerProjectTools(server, client) {
208
- 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 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)") }, {
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.listTeams(params);
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("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; a status filter also exists but no longer distinguishes anything (see its description).", {
220
- teamId: z.string().uuid().optional().describe("Filter by team ID -- get team IDs from taiga_list_teams"),
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 project status in its UI (PRO-543), so every project is currently \"draft\" and filtering by \"active\" or \"archived\" returns nothing. Omit this unless you are working with data set through the API directly."),
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.listProjects(params);
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("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.", {
238
- projectId: z.string().uuid().describe("Project ID -- get from taiga_list_projects"),
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.listProjectDocuments(params.projectId, { limit: params.limit })).data);
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("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.", {
251
- projectId: z.string().uuid().describe("Project ID -- get from taiga_list_projects"),
252
- type: z.string().describe("Document type -- get available types from taiga_list_project_documents. Common: specification, architecture, threat_model, technology_decisions")
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.getProjectDocument(params.projectId, params.type);
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 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)") }, {
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("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.", {
308
- teamId: z.string().uuid().describe("Team ID -- get from taiga_list_teams"),
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.listTeamDocuments(params.teamId, { limit: params.limit })).data);
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("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.", {
321
- teamId: z.string().uuid().describe("Team ID -- get from taiga_list_teams"),
322
- type: z.string().describe("Document type -- get available types from taiga_list_team_documents. Most useful: domain_context (entities, language), tech_preferences (stack, patterns)")
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.getTeamDocument(params.teamId, params.type);
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("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.", {
335
- teamId: z.string().uuid().describe("Team ID -- get from taiga_list_teams"),
336
- type: z.string().describe("Document type (e.g. domain_context, tech_preferences)"),
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 { teamId, type, ...body } = params;
345
- const result = await client.saveTeamDocumentDraft(teamId, type, body);
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 team documents).
360
- * Post-beta: Projects, backlog items, environments, composite tools.
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
- registerProjectTools(server, client);
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
- "**Team documents** (taiga_list_team_documents / taiga_get_team_document):",
398
- "Domain context (ubiquitous language, entities, business rules), tech preferences (approved stack, patterns),",
399
- "architecture constraints, and SLO targets. Read these to understand HOW this team builds software.",
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
- "**Project documents** (taiga_list_project_documents / taiga_get_project_document):",
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 project context.",
395
+ "technology decisions, and data flow. Read these to understand the specific product.",
404
396
  "",
405
- "Workflow: Start with taiga_list_teams to find your team, then read the team's domain_context",
406
- "and tech_preferences. For project-specific work, use taiga_list_projects to find the project,",
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 team document is outdated, use the update tools",
410
- "(taiga_update_document, taiga_update_team_document) to fix it.",
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.3.0-dev.298",
4
- "description": "MCP server for Taiga -- gives AI assistants access to documents and projects",
3
+ "version": "0.4.0-dev.299",
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": {
@@ -54,7 +54,7 @@ describe('TaigaClient', () => {
54
54
  const client = makeClient({ apiKey: 'taiga_sk_live_secret42' });
55
55
 
56
56
  // Act
57
- await client.listProjects();
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().listProjects();
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.listProjects()).rejects.toThrow(/403.*Forbidden/);
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().listProjects()).rejects.toThrow(/500/);
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().listProjects()).rejects.toThrow(/ECONNREFUSED/);
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.listProjects();
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().listProjects();
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().listProjects({ teamId: 't1', status: 'active', limit: 10 });
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 project path', async () => {
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 project path', async () => {
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 project by id', async () => {
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().getProject('p1');
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 teams with optional limit', async () => {
246
+ it('should GET factories with optional limit', async () => {
247
247
  const spy = stubFetch(200, { data: [], nextCursor: null });
248
248
 
249
- await makeClient().listTeams({ limit: 50 });
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
- // Projects
60
+ // Products
61
61
  // ---------------------------------------------------------------------------
62
62
 
63
- async listProjects(opts?: { teamId?: string; status?: string; limit?: number }) {
63
+ async listProducts(opts?: { factoryId?: string; status?: string; limit?: number }) {
64
64
  const params = new URLSearchParams();
65
- if (opts?.teamId) params.set('factoryId', opts.teamId);
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 getProject(id: string) {
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
- // Team Documents
99
+ // Factory Documents
100
100
  // ---------------------------------------------------------------------------
101
101
 
102
- async listTeamDocuments(teamId: string, opts?: { limit?: number }) {
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/${teamId}/documents${qs ? `?${qs}` : ''}`
108
+ `/factories/${factoryId}/documents${qs ? `?${qs}` : ''}`
109
109
  );
110
110
  }
111
111
 
112
- async getTeamDocument(teamId: string, type: string) {
113
- return this.request<{ data: unknown }>('GET', `/factories/${teamId}/documents/${type}`);
112
+ async getFactoryDocument(factoryId: string, type: string) {
113
+ return this.request<{ data: unknown }>('GET', `/factories/${factoryId}/documents/${type}`);
114
114
  }
115
115
 
116
- async saveTeamDocumentDraft(teamId: string, type: string, body: { data: Record<string, unknown> }) {
117
- return this.request<{ data: unknown }>('PUT', `/factories/${teamId}/documents/${type}/draft`, body);
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
- // Project Documents
121
+ // Product Documents
122
122
  // ---------------------------------------------------------------------------
123
123
 
124
- async listProjectDocuments(projectId: string, opts?: { limit?: number }) {
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/${projectId}/documents${qs ? `?${qs}` : ''}`
130
+ `/products/${productId}/documents${qs ? `?${qs}` : ''}`
131
131
  );
132
132
  }
133
133
 
134
- async getProjectDocument(projectId: string, type: string) {
135
- return this.request<{ data: unknown }>('GET', `/products/${projectId}/documents/${type}`);
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
- // Team Documents
139
+ // Factory Documents
140
140
  // ---------------------------------------------------------------------------
141
141
 
142
142
  /**
143
- * Fetch the full context for a project in parallel: project metadata,
144
- * team documents and project documents.
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 getProjectContext(projectId: string): Promise<Record<string, unknown>> {
148
- // First fetch the project to get its factory (the API's name for a team since PRO-1668)
149
- const project = await this.request<{ data: Record<string, unknown> }>('GET', `/products/${projectId}`);
150
- const teamId = project.data.factoryId as string | undefined;
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 [projectDocs, teamDocs] = await Promise.all([
154
- this.listProjectDocuments(projectId, { limit: 20 }).catch(() => ({ data: [] })),
155
- teamId
156
- ? this.listTeamDocuments(teamId, { limit: 20 }).catch(() => ({ data: [] }))
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 keyProjectDocTypes = ['specification', 'architecture', 'technology_decisions'];
162
- const keyTeamDocTypes = ['domain_context', 'tech_preferences', 'architecture_constraints'];
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 keyProjectDocTypes) {
167
- const exists = (projectDocs.data as Record<string, unknown>[]).some((d) => d.type === docType && !d.isDraft);
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.getProjectDocument(projectId, docType)
171
- .then((r) => ({ type: docType, scope: 'project', data: r.data }))
172
- .catch(() => ({ type: docType, scope: 'project', data: null }))
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 (teamId) {
178
- for (const docType of keyTeamDocTypes) {
179
- const exists = (teamDocs.data as Record<string, unknown>[]).some((d) => d.type === docType && !d.isDraft);
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.getTeamDocument(teamId, docType)
183
- .then((r) => ({ type: docType, scope: 'team', data: r.data }))
184
- .catch(() => ({ type: docType, scope: 'team', data: null }))
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
- const context: Record<string, unknown> = {
194
- project: project.data,
196
+ return {
197
+ product: product.data,
195
198
  documents: {
196
- project: Object.fromEntries(
197
- fetchedDocs.filter((d) => d.scope === 'project' && d.data).map((d) => [d.type, d.data])
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
- project: (projectDocs.data as Record<string, unknown>[]).map((d) => ({
203
- type: d.type,
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(projectId: string, opts?: { limit?: number }) {
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/${projectId}/backlog${qs ? `?${qs}` : ''}`
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(projectId: string) {
243
- return this.request<{ data: unknown[] }>('GET', `/products/${projectId}/environments`);
235
+ async listEnvironments(productId: string) {
236
+ return this.request<{ data: unknown[] }>('GET', `/products/${productId}/environments`);
244
237
  }
245
238
 
246
239
  // ---------------------------------------------------------------------------
247
- // Teams
240
+ // Factories
248
241
  // ---------------------------------------------------------------------------
249
242
 
250
- async listTeams(opts?: { limit?: number }) {
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
- '**Team documents** (taiga_list_team_documents / taiga_get_team_document):',
47
- 'Domain context (ubiquitous language, entities, business rules), tech preferences (approved stack, patterns),',
48
- 'architecture constraints, and SLO targets. Read these to understand HOW this team builds software.',
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
- '**Project documents** (taiga_list_project_documents / taiga_get_project_document):',
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 project context.',
52
+ 'technology decisions, and data flow. Read these to understand the specific product.',
53
53
  '',
54
- "Workflow: Start with taiga_list_teams to find your team, then read the team's domain_context",
55
- 'and tech_preferences. For project-specific work, use taiga_list_projects to find the project,',
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 team document is outdated, use the update tools',
59
- '(taiga_update_document, taiga_update_team_document) to fix it.',
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
- 'taiga_get_project_document',
28
- 'taiga_get_team_document',
27
+ 'taiga_get_factory_document',
28
+ 'taiga_get_product_document',
29
29
  'taiga_list_documents',
30
- 'taiga_list_project_documents',
31
- 'taiga_list_projects',
32
- 'taiga_list_team_documents',
33
- 'taiga_list_teams',
30
+ 'taiga_list_factories',
31
+ 'taiga_list_factory_documents',
32
+ 'taiga_list_product_documents',
33
+ 'taiga_list_products',
34
34
  'taiga_update_document',
35
- 'taiga_update_team_document',
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 team tools for a teams:read-only key', async () => {
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('taiga_list_teams');
156
- expect(names).toContain('taiga_list_team_documents');
157
- expect(names).toContain('taiga_get_team_document');
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 team document write when both teams:read and documents:write are granted', async () => {
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('taiga_update_team_document');
193
- expect(names).toContain('taiga_list_teams');
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('taiga_list_teams');
206
- expect(names).toContain('taiga_list_team_documents');
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': ['taiga_list_projects', 'taiga_list_project_documents', 'taiga_get_project_document'],
235
- 'teams:read': ['taiga_list_teams', 'taiga_list_team_documents', 'taiga_get_team_document'],
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 team list as JSON text content', async () => {
262
- const teams = [
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: teams, nextCursor: null } },
269
+ routes: { 'GET /v1/factories': { data: factories, nextCursor: null } },
270
270
  });
271
271
 
272
- const result = await client.callTool({ name: 'taiga_list_teams', arguments: {} });
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(teams);
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 team document draft', async () => {
311
- const teamId = '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b';
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/${teamId}/documents/${docType}/draft`]: { data: updated } },
317
+ routes: { [`PUT /v1/factories/${factoryId}/documents/${docType}/draft`]: { data: updated } },
318
318
  });
319
319
 
320
320
  await client.callTool({
321
- name: 'taiga_update_team_document',
322
- arguments: { teamId, type: docType, data: { sections: { overview: 'Updated' } } },
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/${teamId}/documents/${docType}/draft`);
329
+ expect(putCall![0]).toContain(`/v1/factories/${factoryId}/documents/${docType}/draft`);
330
330
  });
331
331
 
332
- it('should return teams through MCP protocol', async () => {
333
- const teams = [{ id: 't1', name: 'Engineering' }];
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: teams, nextCursor: null } },
337
+ routes: { 'GET /v1/factories': { data: factories, nextCursor: null } },
338
338
  });
339
339
 
340
- const result = await client.callTool({ name: 'taiga_list_teams', arguments: {} });
340
+ const result = await client.callTool({ name: 'taiga_list_factories', arguments: {} });
341
341
 
342
- expect(parseToolResult(result)).toEqual(teams);
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: 'taiga_list_projects', arguments: {} });
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
- // taiga_get_project requires a UUID, not a plain string
395
+ // taiga_get_product requires a UUID, not a plain string
396
396
  const result = await client.callTool({
397
- name: 'taiga_get_project',
397
+ name: 'taiga_get_product',
398
398
  arguments: { id: 'not-a-uuid' },
399
399
  });
400
400
 
@@ -1,5 +1,5 @@
1
1
  // Backlog tools are reserved for post-beta.
2
- // The beta MCP server focuses on the knowledge base (policies, team docs).
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';
@@ -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 teams and projects. ' +
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
- // ── Team documents ──────────────────────────────────────────────────
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
- 'taiga_list_team_documents',
80
- 'List documents for a specific team (names and types only). ' +
81
- 'Team documents define how the team works: domain context (entities, ubiquitous language), ' +
82
- 'tech preferences (approved stack), architecture constraints, SLO targets, ' +
83
- 'definition of done, and team charter. ' +
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
- teamId: z.string().uuid().describe('Team ID -- get from taiga_list_teams'),
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.listTeamDocuments(params.teamId, { limit: params.limit });
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
- 'taiga_get_team_document',
101
- 'Get the full content of a specific team document. ' +
102
- 'Use this when you need the team domain context (ubiquitous language, entities, business rules), ' +
103
- 'tech preferences (approved stack, patterns), or architecture constraints. ' +
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
- teamId: z.string().uuid().describe('Team ID -- get from taiga_list_teams'),
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 taiga_list_team_documents. ' +
112
- 'Most useful: domain_context (entities, language), tech_preferences (stack, patterns)'
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.getTeamDocument(params.teamId, params.type);
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
- // Team document write requires both teams:read (to find the team) and tenant:documents:write
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
- 'taiga_update_team_document',
129
- 'Update the draft content of a team document. ' +
130
- 'Only updates the draft -- does not publish. Use this when you notice team conventions, ' +
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
- teamId: z.string().uuid().describe('Team ID -- get from taiga_list_teams'),
134
- type: z.string().describe('Document type (e.g. domain_context, tech_preferences)'),
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 { teamId, type, ...body } = params;
140
- const result = await client.saveTeamDocumentDraft(teamId, type, body as { data: Record<string, unknown> });
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
  };
@@ -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 { registerProjectTools } from './projects.js';
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 team documents).
13
- * Post-beta: Projects, backlog items, environments, composite tools.
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
- registerProjectTools(server, client);
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
- export function registerProjectTools(server: McpServer, client: TaigaClient) {
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
- 'taiga_list_teams',
10
- 'List all teams in the organization. Use this to find team IDs ' +
11
- 'for reading team documents and understanding team structure. ' +
12
- 'Teams own domain context, tech preferences, and architecture constraints.',
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.listTeams(params);
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
- 'taiga_list_projects',
29
- 'List projects in the organization. Use this to find the project you are working on ' +
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 team; a status filter also exists but no longer distinguishes anything (see its description).',
33
+ 'Can filter by factory; a status filter also exists but no longer distinguishes anything (see its description).',
32
34
  {
33
- teamId: z.string().uuid().optional().describe('Filter by team ID -- get team IDs from taiga_list_teams'),
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 project status in its UI (PRO-543), so every ' +
39
- 'project is currently "draft" and filtering by "active" or "archived" returns nothing. ' +
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.listProjects(params);
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
- 'taiga_list_project_documents',
55
- 'List documents for a specific project (names and types only). ' +
56
- 'Project documents are generated during intake: specification (requirements), ' +
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 user flow. ' +
59
- 'Use taiga_get_project_document to read the full content.',
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
- projectId: z.string().uuid().describe('Project ID -- get from taiga_list_projects'),
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.listProjectDocuments(params.projectId, { limit: params.limit });
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
- 'taiga_get_project_document',
76
- 'Get the full content of a specific project document. ' +
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
- projectId: z.string().uuid().describe('Project ID -- get from taiga_list_projects'),
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 taiga_list_project_documents. ' +
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.getProjectDocument(params.projectId, params.type);
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
  };