@taiga-ai/mcp-server 0.4.0-rc.345 → 0.5.0-dev.388

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
@@ -7,6 +7,49 @@ const SERVER_VERSION = createRequire(import.meta.url)("../package.json").version
7
7
  //#endregion
8
8
  //#region src/client.ts
9
9
  /**
10
+ * A non-2xx answer from the Taiga API. Carries the status and the API's own
11
+ * `code` and `message` so a tool can hand the model something it can act on
12
+ * (a 400 from the publish gate names the section that still holds template
13
+ * residue) instead of one opaque string.
14
+ */
15
+ var TaigaApiError = class extends Error {
16
+ status;
17
+ code;
18
+ apiMessage;
19
+ constructor(method, path, status, body) {
20
+ const parsed = parseErrorBody(body);
21
+ super(`Taiga API ${method} ${path} failed (${status}): ${parsed.message ?? body}`);
22
+ this.name = "TaigaApiError";
23
+ this.status = status;
24
+ this.code = parsed.code;
25
+ this.apiMessage = parsed.message;
26
+ }
27
+ };
28
+ /**
29
+ * One path segment. Every id and type a tool passes is validated at the
30
+ * tool boundary; this is the second wall. Encoding keeps a slash inside the
31
+ * segment, and the dot segments are refused outright: encodeURIComponent
32
+ * leaves "." and ".." as they are, and the URL parser in fetch would then
33
+ * collapse `/documents/../publish` into a different route.
34
+ */
35
+ function seg(value) {
36
+ if (value === "" || value === "." || value === "..") throw new TypeError(`Invalid path segment: ${JSON.stringify(value)}`);
37
+ return encodeURIComponent(value);
38
+ }
39
+ function parseErrorBody(body) {
40
+ try {
41
+ const json = JSON.parse(body);
42
+ const message = typeof json.message === "string" ? json.message : void 0;
43
+ const details = Array.isArray(json.details) ? json.details.map((d) => [d.instancePath, d.message].filter((x) => typeof x === "string" && x).join(" ")).filter(Boolean) : [];
44
+ return {
45
+ ...typeof json.code === "string" ? { code: json.code } : {},
46
+ ...message ? { message: details.length ? `${message}: ${details.join("; ")}` : message } : {}
47
+ };
48
+ } catch {
49
+ return {};
50
+ }
51
+ }
52
+ /**
10
53
  * HTTP client for the Taiga API.
11
54
  * Used by the MCP server to proxy tool calls to the backend.
12
55
  */
@@ -22,17 +65,17 @@ var TaigaClient = class {
22
65
  const url = `${this.baseUrl}/v1${path}`;
23
66
  const headers = {
24
67
  Authorization: `Bearer ${this.apiKey}`,
25
- "Content-Type": "application/json",
26
68
  "User-Agent": `taiga-ai-mcp-server/${SERVER_VERSION}`
27
69
  };
70
+ if (body !== void 0) headers["Content-Type"] = "application/json";
28
71
  const response = await fetch(url, {
29
72
  method,
30
73
  headers,
31
- body: body ? JSON.stringify(body) : void 0
74
+ body: body === void 0 ? null : JSON.stringify(body)
32
75
  });
33
76
  if (!response.ok) {
34
77
  const text = await response.text().catch(() => "");
35
- throw new Error(`Taiga API ${method} ${path} failed (${response.status}): ${text}`);
78
+ throw new TaigaApiError(method, path, response.status, text);
36
79
  }
37
80
  return response.json();
38
81
  }
@@ -57,7 +100,7 @@ var TaigaClient = class {
57
100
  return this.request("GET", `/products${qs ? `?${qs}` : ""}`);
58
101
  }
59
102
  async getProduct(id) {
60
- return this.request("GET", `/products/${id}`);
103
+ return this.request("GET", `/products/${seg(id)}`);
61
104
  }
62
105
  async listTenantDocuments(opts) {
63
106
  const params = new URLSearchParams();
@@ -66,31 +109,45 @@ var TaigaClient = class {
66
109
  return this.request("GET", `/tenants/current/documents${qs ? `?${qs}` : ""}`);
67
110
  }
68
111
  async getTenantDocument(type) {
69
- return this.request("GET", `/tenants/current/documents/${type}`);
112
+ return this.request("GET", `/tenants/current/documents/${seg(type)}`);
113
+ }
114
+ /** The shared draft for a document type, or `{ data: null }` when none exists. */
115
+ async getTenantDocumentDraft(type) {
116
+ return this.request("GET", `/tenants/current/documents/${seg(type)}/draft`);
70
117
  }
71
118
  async saveTenantDocumentDraft(type, body) {
72
- return this.request("PUT", `/tenants/current/documents/${type}/draft`, body);
119
+ return this.request("PUT", `/tenants/current/documents/${seg(type)}/draft`, body);
120
+ }
121
+ async publishTenantDocument(type) {
122
+ return this.request("POST", `/tenants/current/documents/${seg(type)}/publish`);
73
123
  }
74
124
  async listFactoryDocuments(factoryId, opts) {
75
125
  const params = new URLSearchParams();
76
126
  if (opts?.limit) params.set("limit", String(opts.limit));
77
127
  const qs = params.toString();
78
- return this.request("GET", `/factories/${factoryId}/documents${qs ? `?${qs}` : ""}`);
128
+ return this.request("GET", `/factories/${seg(factoryId)}/documents${qs ? `?${qs}` : ""}`);
79
129
  }
80
130
  async getFactoryDocument(factoryId, type) {
81
- return this.request("GET", `/factories/${factoryId}/documents/${type}`);
131
+ return this.request("GET", `/factories/${seg(factoryId)}/documents/${seg(type)}`);
132
+ }
133
+ /** The shared draft for a factory document type, or `{ data: null }` when none exists. */
134
+ async getFactoryDocumentDraft(factoryId, type) {
135
+ return this.request("GET", `/factories/${seg(factoryId)}/documents/${seg(type)}/draft`);
82
136
  }
83
137
  async saveFactoryDocumentDraft(factoryId, type, body) {
84
- return this.request("PUT", `/factories/${factoryId}/documents/${type}/draft`, body);
138
+ return this.request("PUT", `/factories/${seg(factoryId)}/documents/${seg(type)}/draft`, body);
139
+ }
140
+ async publishFactoryDocument(factoryId, type) {
141
+ return this.request("POST", `/factories/${seg(factoryId)}/documents/${seg(type)}/publish`);
85
142
  }
86
143
  async listProductDocuments(productId, opts) {
87
144
  const params = new URLSearchParams();
88
145
  if (opts?.limit) params.set("limit", String(opts.limit));
89
146
  const qs = params.toString();
90
- return this.request("GET", `/products/${productId}/documents${qs ? `?${qs}` : ""}`);
147
+ return this.request("GET", `/products/${seg(productId)}/documents${qs ? `?${qs}` : ""}`);
91
148
  }
92
149
  async getProductDocument(productId, type) {
93
- return this.request("GET", `/products/${productId}/documents/${type}`);
150
+ return this.request("GET", `/products/${seg(productId)}/documents/${seg(type)}`);
94
151
  }
95
152
  /**
96
153
  * Fetch the full context for a product in parallel: product metadata,
@@ -98,7 +155,7 @@ var TaigaClient = class {
98
155
  * Returns a structured bundle optimized for coding agent consumption.
99
156
  */
100
157
  async getProductContext(productId) {
101
- const product = await this.request("GET", `/products/${productId}`);
158
+ const product = await this.request("GET", `/products/${seg(productId)}`);
102
159
  const factoryId = product.data.factoryId;
103
160
  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
161
  const keyProductDocTypes = [
@@ -151,13 +208,13 @@ var TaigaClient = class {
151
208
  const params = new URLSearchParams();
152
209
  if (opts?.limit) params.set("limit", String(opts.limit));
153
210
  const qs = params.toString();
154
- return this.request("GET", `/products/${productId}/backlog${qs ? `?${qs}` : ""}`);
211
+ return this.request("GET", `/products/${seg(productId)}/backlog${qs ? `?${qs}` : ""}`);
155
212
  }
156
213
  async getBacklogItem(itemId) {
157
- return this.request("GET", `/backlog/${itemId}`);
214
+ return this.request("GET", `/backlog/${seg(itemId)}`);
158
215
  }
159
216
  async listEnvironments(productId) {
160
- return this.request("GET", `/products/${productId}/environments`);
217
+ return this.request("GET", `/products/${seg(productId)}/environments`);
161
218
  }
162
219
  async listFactories(opts) {
163
220
  const params = new URLSearchParams();
@@ -167,6 +224,82 @@ var TaigaClient = class {
167
224
  }
168
225
  };
169
226
  //#endregion
227
+ //#region src/config.ts
228
+ const envSchema = z.object({
229
+ TAIGA_API_KEY: z.string().min(1),
230
+ TAIGA_API_URL: z.string().default("https://api.taiga.app")
231
+ });
232
+ /** Null when TAIGA_API_KEY is missing or empty: it is the one setting with no default. */
233
+ function readConfig() {
234
+ const parsed = envSchema.safeParse(process.env);
235
+ if (!parsed.success) return null;
236
+ return {
237
+ apiKey: parsed.data.TAIGA_API_KEY,
238
+ apiUrl: parsed.data.TAIGA_API_URL
239
+ };
240
+ }
241
+ //#endregion
242
+ //#region src/tools/errors.ts
243
+ /** A successful tool result: one JSON text block the model reads. */
244
+ function toolResult(value) {
245
+ return { content: [{
246
+ type: "text",
247
+ text: JSON.stringify(value, null, 2)
248
+ }] };
249
+ }
250
+ /**
251
+ * A failed tool result the model can branch on: a stable code and a message
252
+ * that says what to do next. Returned, not thrown, so the client sees
253
+ * `isError` rather than a protocol error.
254
+ */
255
+ function toolError(error, message) {
256
+ return {
257
+ content: [{
258
+ type: "text",
259
+ text: JSON.stringify({
260
+ error,
261
+ message
262
+ })
263
+ }],
264
+ isError: true
265
+ };
266
+ }
267
+ const CODE_BY_STATUS = {
268
+ 400: "VALIDATION_FAILED",
269
+ 401: "UNAUTHENTICATED",
270
+ 403: "FORBIDDEN",
271
+ 404: "NOT_FOUND",
272
+ 409: "CONFLICT",
273
+ 429: "RATE_LIMITED"
274
+ };
275
+ /**
276
+ * Run a tool handler and turn a failure into a structured error. An API
277
+ * refusal keeps the API's own code when it sent one and its message whole:
278
+ * on a refused publish that names the section holding template residue,
279
+ * which is what the model needs to fix the draft. A failed connection is
280
+ * structured too, so every failure the model sees has the same shape.
281
+ */
282
+ async function runTool(handler) {
283
+ try {
284
+ return await handler();
285
+ } catch (error) {
286
+ if (error instanceof TaigaApiError) return toolError(error.code ?? CODE_BY_STATUS[error.status] ?? "API_ERROR", error.apiMessage ?? `Taiga API answered ${error.status}.`);
287
+ if (error instanceof TypeError) return toolError("NETWORK_ERROR", `Could not reach the Taiga API: ${error.message}. Check TAIGA_API_URL.`);
288
+ throw error;
289
+ }
290
+ }
291
+ //#endregion
292
+ //#region src/tools/inputs.ts
293
+ /**
294
+ * A document type as the API names one: policy_security, factory_instructions,
295
+ * specification. An identifier, never a path: the value is interpolated into
296
+ * the route, and a type that carried a slash would reach a different
297
+ * document's route than the tool's description promises. The API's own enum
298
+ * stays the source of truth for which identifiers exist (it answers 400 with
299
+ * the valid list); this only rules out what could not be a type at all.
300
+ */
301
+ const documentType = z.string().regex(/^[a-z][a-z0-9_]{0,63}$/, "A document type is a lowercase identifier such as policy_security");
302
+ //#endregion
170
303
  //#region src/tools/summarize.ts
171
304
  /**
172
305
  * Strip heavy content from list responses to avoid context window bloat.
@@ -196,150 +329,257 @@ function summarizeList(items) {
196
329
  }
197
330
  //#endregion
198
331
  //#region src/tools/products.ts
332
+ const READ_ONLY$1 = {
333
+ readOnlyHint: true,
334
+ destructiveHint: false,
335
+ idempotentHint: true,
336
+ openWorldHint: false
337
+ };
199
338
  function registerProductTools(server, client) {
200
- if (client.hasCapability("factories: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)") }, {
201
- readOnlyHint: true,
202
- openWorldHint: false
203
- }, async (params) => {
204
- const result = await client.listFactories(params);
205
- return { content: [{
206
- type: "text",
207
- text: JSON.stringify(result.data, null, 2)
208
- }] };
209
- });
339
+ if (client.hasCapability("factories:read")) server.registerTool("taiga_list_factories", {
340
+ title: "List factories",
341
+ description: "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.",
342
+ inputSchema: { limit: z.number().min(1).max(100).optional().describe("Max results (default 25)") },
343
+ annotations: READ_ONLY$1
344
+ }, (params) => runTool(async () => {
345
+ return toolResult((await client.listFactories(params)).data);
346
+ }));
210
347
  if (client.hasCapability("products:read")) {
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"),
213
- status: z.enum([
214
- "draft",
215
- "active",
216
- "archived"
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."),
218
- limit: z.number().min(1).max(100).optional().describe("Max results (default 25)")
219
- }, {
220
- readOnlyHint: true,
221
- openWorldHint: false
222
- }, async (params) => {
223
- const result = await client.listProducts(params);
224
- return { content: [{
225
- type: "text",
226
- text: JSON.stringify(result.data, null, 2)
227
- }] };
228
- });
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"),
231
- limit: z.number().min(1).max(100).optional().describe("Max results (default 25)")
232
- }, {
233
- readOnlyHint: true,
234
- openWorldHint: false
235
- }, async (params) => {
236
- const summaries = summarizeList((await client.listProductDocuments(params.productId, { limit: params.limit })).data);
237
- return { content: [{
238
- type: "text",
239
- text: JSON.stringify(summaries, null, 2)
240
- }] };
241
- });
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, and product_instructions (the product's standing instructions).", {
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")
245
- }, {
246
- readOnlyHint: true,
247
- openWorldHint: false
248
- }, async (params) => {
249
- const result = await client.getProductDocument(params.productId, params.type);
250
- return { content: [{
251
- type: "text",
252
- text: JSON.stringify(result.data, null, 2)
253
- }] };
254
- });
348
+ server.registerTool("taiga_list_products", {
349
+ title: "List products",
350
+ description: "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).",
351
+ inputSchema: {
352
+ factoryId: z.string().uuid().optional().describe("Filter by factory ID -- get factory IDs from taiga_list_factories"),
353
+ status: z.enum([
354
+ "draft",
355
+ "active",
356
+ "archived"
357
+ ]).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."),
358
+ limit: z.number().min(1).max(100).optional().describe("Max results (default 25)")
359
+ },
360
+ annotations: READ_ONLY$1
361
+ }, (params) => runTool(async () => {
362
+ return toolResult((await client.listProducts(params)).data);
363
+ }));
364
+ server.registerTool("taiga_list_product_documents", {
365
+ title: "List product documents",
366
+ description: "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.",
367
+ inputSchema: {
368
+ productId: z.string().uuid().describe("Product ID -- get from taiga_list_products"),
369
+ limit: z.number().min(1).max(100).optional().describe("Max results (default 25)")
370
+ },
371
+ annotations: READ_ONLY$1
372
+ }, (params) => runTool(async () => {
373
+ return toolResult(summarizeList((await client.listProductDocuments(params.productId, { limit: params.limit })).data));
374
+ }));
375
+ server.registerTool("taiga_get_product_document", {
376
+ title: "Get product document",
377
+ description: "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, and product_instructions (the product's standing instructions).",
378
+ inputSchema: {
379
+ productId: z.string().uuid().describe("Product ID -- get from taiga_list_products"),
380
+ type: documentType.describe("Document type -- get available types from taiga_list_product_documents. Common: specification, architecture, threat_model, technology_decisions")
381
+ },
382
+ annotations: READ_ONLY$1
383
+ }, (params) => runTool(async () => {
384
+ return toolResult((await client.getProductDocument(params.productId, params.type)).data);
385
+ }));
255
386
  }
256
387
  }
257
388
  //#endregion
389
+ //#region src/tools/sections.ts
390
+ function isRecord(value) {
391
+ return typeof value === "object" && value !== null && !Array.isArray(value);
392
+ }
393
+ /** The `sections` map of a document, or null when the document is not section-shaped. */
394
+ function sectionsOf(data) {
395
+ return isRecord(data.sections) ? data.sections : null;
396
+ }
397
+ /** The text of one section, or null when the section does not exist. */
398
+ function sectionContent(sections, sectionId) {
399
+ const section = sections[sectionId];
400
+ if (section === void 0 || section === null) return null;
401
+ if (typeof section === "string") return section;
402
+ return isRecord(section) && typeof section.content === "string" ? section.content : "";
403
+ }
404
+ /** The map with one section's text replaced (or the section added); every other section untouched. */
405
+ function applySectionEdit(sections, sectionId, content) {
406
+ const prior = sections[sectionId];
407
+ return {
408
+ ...sections,
409
+ [sectionId]: isRecord(prior) ? {
410
+ ...prior,
411
+ content
412
+ } : { content }
413
+ };
414
+ }
415
+ //#endregion
258
416
  //#region src/tools/documents.ts
417
+ const READ_ONLY = {
418
+ readOnlyHint: true,
419
+ destructiveHint: false,
420
+ idempotentHint: true,
421
+ openWorldHint: false
422
+ };
423
+ const DRAFT_WRITE = {
424
+ readOnlyHint: false,
425
+ destructiveHint: false,
426
+ idempotentHint: true,
427
+ openWorldHint: false
428
+ };
429
+ const PUBLISH = {
430
+ readOnlyHint: false,
431
+ destructiveHint: true,
432
+ idempotentHint: false,
433
+ openWorldHint: false
434
+ };
435
+ const sectionId = z.string().min(1).describe("Section id -- the keys of `data.sections` in taiga_get_document, e.g. summary, scope, apiDesign. An id that does not exist yet adds a section.");
436
+ const sectionContentInput = z.string().describe("The full new Markdown text of the section; it replaces the old text");
437
+ const tenantDocumentType = documentType.describe("Document type (e.g. policy_security, policy_sdlc) -- see taiga_list_documents");
438
+ /** Everything the model needs back from a write, without the content it already holds. */
439
+ function documentSummary(doc) {
440
+ const { data, ...rest } = doc;
441
+ return {
442
+ ...rest,
443
+ sections: Object.keys(sectionsOf(data) ?? {})
444
+ };
445
+ }
446
+ /**
447
+ * The document a section edit starts from: the draft when one exists, else
448
+ * the published version, else (instructions documents start empty and
449
+ * unpublished, PRO-502) an empty section map. The API seeds the draft the
450
+ * same way on the first save, so the merge happens on what it would hold.
451
+ */
452
+ async function editBase(scope) {
453
+ const draft = (await scope.getDraft()).data;
454
+ if (draft) return draft.data;
455
+ try {
456
+ return (await scope.getPublished()).data.data;
457
+ } catch (error) {
458
+ if (error instanceof TaigaApiError && error.status === 404) return { sections: {} };
459
+ throw error;
460
+ }
461
+ }
462
+ /**
463
+ * Edit one section of a document's draft; the edit keeps every other section
464
+ * and the section's own metadata, see sections.ts.
465
+ */
466
+ async function updateSection(scope, type, id, content) {
467
+ const base = await editBase(scope);
468
+ const sections = sectionsOf(base);
469
+ if (!sections) return toolError("UNSUPPORTED_DOCUMENT", `Document ${type} is not made of sections and cannot be edited section by section. Edit it in the Taiga app.`);
470
+ const saved = await scope.saveDraft({
471
+ ...base,
472
+ sections: applySectionEdit(sections, id, content)
473
+ });
474
+ return toolResult({
475
+ ...documentSummary(saved.data),
476
+ section: {
477
+ id,
478
+ content: sectionContent(sectionsOf(saved.data.data) ?? {}, id)
479
+ }
480
+ });
481
+ }
482
+ /** Publish the draft; the API answers 404 when there is none and 400 when the residue gate refuses it. */
483
+ async function publishDraft(scope) {
484
+ return toolResult(documentSummary((await scope.publish()).data));
485
+ }
259
486
  function registerDocumentTools(server, client) {
260
487
  if (client.hasCapability("tenant:documents:read")) {
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)") }, {
262
- readOnlyHint: true,
263
- openWorldHint: false
264
- }, async (params) => {
265
- const summaries = summarizeList((await client.listTenantDocuments(params)).data);
266
- return { content: [{
267
- type: "text",
268
- text: JSON.stringify(summaries, null, 2)
269
- }] };
270
- });
271
- server.tool("taiga_get_document", "Get the full content of a company policy document. Use this when you need to understand security requirements, data handling rules, architecture standards, or compliance controls that apply to your code. Types: policy_security, policy_data, policy_sdlc, policy_ops, policy_tic, policy_risk, policy_architecture, policy_supply_chain, policy_cloud_governance, policy_acceptable_use, policy_ip.", { type: z.string().describe("Document type -- get available types from taiga_list_documents. Common: policy_security, policy_data, policy_sdlc, policy_architecture") }, {
272
- readOnlyHint: true,
273
- openWorldHint: false
274
- }, async (params) => {
275
- const result = await client.getTenantDocument(params.type);
276
- return { content: [{
277
- type: "text",
278
- text: JSON.stringify(result.data, null, 2)
279
- }] };
488
+ server.registerTool("taiga_list_documents", {
489
+ title: "List company documents",
490
+ description: "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.",
491
+ inputSchema: { limit: z.number().min(1).max(100).optional().describe("Max results (default 25)") },
492
+ annotations: READ_ONLY
493
+ }, (params) => runTool(async () => {
494
+ return toolResult(summarizeList((await client.listTenantDocuments(params)).data));
495
+ }));
496
+ server.registerTool("taiga_get_document", {
497
+ title: "Get company document",
498
+ description: "Get the full content of a company policy document. Use this when you need to understand security requirements, data handling rules, architecture standards, or compliance controls that apply to your code. Types: policy_security, policy_data, policy_sdlc, policy_ops, policy_tic, policy_risk, policy_architecture, policy_supply_chain, policy_cloud_governance, policy_acceptable_use, policy_ip.",
499
+ inputSchema: { type: documentType.describe("Document type -- get available types from taiga_list_documents. Common: policy_security, policy_data, policy_sdlc, policy_architecture") },
500
+ annotations: READ_ONLY
501
+ }, (params) => runTool(async () => {
502
+ return toolResult((await client.getTenantDocument(params.type)).data);
503
+ }));
504
+ }
505
+ if (client.hasCapability("tenant:documents:read") && client.hasCapability("tenant:documents:write")) {
506
+ const tenantScope = (type) => ({
507
+ getDraft: () => client.getTenantDocumentDraft(type),
508
+ getPublished: () => client.getTenantDocument(type),
509
+ saveDraft: (data) => client.saveTenantDocumentDraft(type, { data }),
510
+ publish: () => client.publishTenantDocument(type)
280
511
  });
512
+ server.registerTool("taiga_update_document", {
513
+ title: "Update a section of a company document draft",
514
+ description: "Replace the text of one section of a company policy document, in its draft. Every other section stays as it is. Does not publish: people and agents keep reading the published version until taiga_publish_document is called. Read the document with taiga_get_document first to see its sections and their current text. Use this when you notice a policy is outdated or needs corrections while reviewing code.",
515
+ inputSchema: {
516
+ type: tenantDocumentType,
517
+ sectionId,
518
+ content: sectionContentInput
519
+ },
520
+ annotations: DRAFT_WRITE
521
+ }, (params) => runTool(() => updateSection(tenantScope(params.type), params.type, params.sectionId, params.content)));
522
+ server.registerTool("taiga_publish_document", {
523
+ title: "Publish a company document draft",
524
+ description: "Publish a company policy document's draft as its next version, the one every agent and person reads from then on. Only publish when the person asked for it; save edits with taiga_update_document first. A refused publish names what to fix in the draft.",
525
+ inputSchema: { type: tenantDocumentType },
526
+ annotations: PUBLISH
527
+ }, (params) => runTool(() => publishDraft(tenantScope(params.type))));
281
528
  }
282
- if (client.hasCapability("tenant:documents:write")) server.tool("taiga_update_document", "Update the draft content of a company policy document. Only updates the draft -- does not publish. Use this when you notice a policy is outdated or needs corrections while reviewing code.", {
283
- type: z.string().describe("Document type (e.g. policy_security, policy_sdlc)"),
284
- data: z.record(z.string(), z.unknown()).describe("Document content to save as JSON")
285
- }, {
286
- readOnlyHint: false,
287
- destructiveHint: false,
288
- idempotentHint: true,
289
- openWorldHint: false
290
- }, async (params) => {
291
- const { type, ...body } = params;
292
- const result = await client.saveTenantDocumentDraft(type, body);
293
- return { content: [{
294
- type: "text",
295
- text: JSON.stringify(result.data, null, 2)
296
- }] };
297
- });
298
529
  if (client.hasCapability("factories:read")) {
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"),
301
- limit: z.number().min(1).max(100).optional().describe("Max results (default 25)")
302
- }, {
303
- readOnlyHint: true,
304
- openWorldHint: false
305
- }, async (params) => {
306
- const summaries = summarizeList((await client.listFactoryDocuments(params.factoryId, { limit: params.limit })).data);
307
- return { content: [{
308
- type: "text",
309
- text: JSON.stringify(summaries, null, 2)
310
- }] };
311
- });
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 `factory_instructions`.")
315
- }, {
316
- readOnlyHint: true,
317
- openWorldHint: false
318
- }, async (params) => {
319
- const result = await client.getFactoryDocument(params.factoryId, params.type);
320
- return { content: [{
321
- type: "text",
322
- text: JSON.stringify(result.data, null, 2)
323
- }] };
530
+ server.registerTool("taiga_list_factory_documents", {
531
+ title: "List factory documents",
532
+ description: "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.",
533
+ inputSchema: {
534
+ factoryId: z.string().uuid().describe("Factory ID -- get from taiga_list_factories"),
535
+ limit: z.number().min(1).max(100).optional().describe("Max results (default 25)")
536
+ },
537
+ annotations: READ_ONLY
538
+ }, (params) => runTool(async () => {
539
+ return toolResult(summarizeList((await client.listFactoryDocuments(params.factoryId, { limit: params.limit })).data));
540
+ }));
541
+ server.registerTool("taiga_get_factory_document", {
542
+ title: "Get factory instructions",
543
+ description: "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.",
544
+ inputSchema: {
545
+ factoryId: z.string().uuid().describe("Factory ID -- get from taiga_list_factories"),
546
+ type: documentType.describe("Document type -- get available types from taiga_list_factory_documents. The factory instructions document is `factory_instructions`.")
547
+ },
548
+ annotations: READ_ONLY
549
+ }, (params) => runTool(async () => {
550
+ return toolResult((await client.getFactoryDocument(params.factoryId, params.type)).data);
551
+ }));
552
+ }
553
+ if (client.hasCapability("factories:read") && client.hasCapability("factories:documents:write")) {
554
+ const factoryScope = (factoryId, type) => ({
555
+ getDraft: () => client.getFactoryDocumentDraft(factoryId, type),
556
+ getPublished: () => client.getFactoryDocument(factoryId, type),
557
+ saveDraft: (data) => client.saveFactoryDocumentDraft(factoryId, type, { data }),
558
+ publish: () => client.publishFactoryDocument(factoryId, type)
324
559
  });
560
+ const factoryId = z.string().uuid().describe("Factory ID -- get from taiga_list_factories");
561
+ const factoryDocumentType = documentType.describe("Document type (`factory_instructions`)");
562
+ server.registerTool("taiga_update_factory_document", {
563
+ title: "Update a section of factory instructions",
564
+ description: "Replace the text of one section of a factory's instructions, in its draft. The instructions document has one section, `instructions`. Does not publish: products keep following the published version until taiga_publish_factory_document is called. Read the current text with taiga_get_factory_document first. Use this when the factory conventions, domain context or tech preferences need updating while working on the codebase.",
565
+ inputSchema: {
566
+ factoryId,
567
+ type: factoryDocumentType,
568
+ sectionId: sectionId.default("instructions"),
569
+ content: sectionContentInput
570
+ },
571
+ annotations: DRAFT_WRITE
572
+ }, (params) => runTool(() => updateSection(factoryScope(params.factoryId, params.type), params.type, params.sectionId, params.content)));
573
+ server.registerTool("taiga_publish_factory_document", {
574
+ title: "Publish factory instructions draft",
575
+ description: "Publish a factory's instructions draft as the next version every product of the factory follows. Only publish when the person asked for it; save edits with taiga_update_factory_document first.",
576
+ inputSchema: {
577
+ factoryId,
578
+ type: factoryDocumentType
579
+ },
580
+ annotations: PUBLISH
581
+ }, (params) => runTool(() => publishDraft(factoryScope(params.factoryId, params.type))));
325
582
  }
326
- if (client.hasCapability("factories: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 (`factory_instructions`)"),
329
- data: z.record(z.string(), z.unknown()).describe("Document content to save as JSON")
330
- }, {
331
- readOnlyHint: false,
332
- destructiveHint: false,
333
- idempotentHint: true,
334
- openWorldHint: false
335
- }, async (params) => {
336
- const { factoryId, type, ...body } = params;
337
- const result = await client.saveFactoryDocumentDraft(factoryId, type, body);
338
- return { content: [{
339
- type: "text",
340
- text: JSON.stringify(result.data, null, 2)
341
- }] };
342
- });
343
583
  }
344
584
  //#endregion
345
585
  //#region src/tools/index.ts
@@ -357,17 +597,16 @@ function registerAllTools(server, client) {
357
597
  }
358
598
  //#endregion
359
599
  //#region src/index.ts
360
- const DEFAULT_API_URL = "https://api.taiga.app";
361
600
  async function main() {
362
- const apiKey = process.env.TAIGA_API_KEY;
363
- if (!apiKey) {
601
+ const config = readConfig();
602
+ if (!config) {
364
603
  console.error("Error: TAIGA_API_KEY environment variable is required.");
365
604
  console.error("Create an API key at https://app.taiga.app/settings/api-keys");
366
605
  process.exit(1);
367
606
  }
368
607
  const client = new TaigaClient({
369
- baseUrl: process.env.TAIGA_API_URL ?? DEFAULT_API_URL,
370
- apiKey
608
+ baseUrl: config.apiUrl,
609
+ apiKey: config.apiKey
371
610
  });
372
611
  const introspection = await client.introspect().catch((error) => {
373
612
  const message = error instanceof Error ? error.message : String(error);
@@ -398,8 +637,9 @@ async function main() {
398
637
  "For product-specific work, use taiga_list_products to find the product,",
399
638
  "then read its specification and architecture.",
400
639
  "",
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.",
640
+ "When you notice a policy or factory instructions are outdated, fix the section with",
641
+ "taiga_update_document or taiga_update_factory_document. That saves a draft nobody follows yet;",
642
+ "publish it with taiga_publish_document or taiga_publish_factory_document only when the person asks.",
403
643
  "This keeps the knowledge base accurate for everyone."
404
644
  ].join("\n") });
405
645
  registerAllTools(server, client);