@taiga-ai/mcp-server 0.20.0-dev.454 → 0.21.0-dev.455
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 +435 -183
- package/package.json +1 -1
- package/src/server.integration.test.ts +355 -68
- package/src/tools/autonomy.ts +9 -11
- package/src/tools/backlogs.ts +84 -81
- package/src/tools/builds.ts +96 -28
- package/src/tools/catalogue.test.ts +5 -8
- package/src/tools/catalogue.ts +5 -8
- package/src/tools/context.ts +21 -20
- package/src/tools/documents.ts +34 -43
- package/src/tools/errors.ts +2 -1
- package/src/tools/inbox.ts +19 -33
- package/src/tools/index.ts +42 -7
- package/src/tools/inputs.ts +1 -1
- package/src/tools/instructions.ts +17 -19
- package/src/tools/jobs.ts +5 -6
- package/src/tools/knowledge.ts +17 -23
- package/src/tools/maintenance.ts +129 -82
- package/src/tools/observe.ts +30 -39
- package/src/tools/product-documents.ts +20 -26
- package/src/tools/products.ts +23 -73
- package/src/tools/prompts.ts +1 -1
- package/src/tools/queue.ts +10 -12
- package/src/tools/repositories.ts +20 -25
- package/src/tools/sections.ts +41 -6
- package/src/tools/setup.ts +100 -54
- package/src/tools/summarize.ts +4 -1
- package/src/tools/wait.ts +13 -22
- package/taiga-ai-mcp-server-0.21.0-dev.455.tgz +0 -0
- package/taiga-ai-mcp-server-0.20.0-dev.454.tgz +0 -0
package/src/tools/setup.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Product } from '@taiga/types';
|
|
|
5
5
|
import { TaigaApiError, type TaigaClient, type WithoutTenant } from '../client.js';
|
|
6
6
|
import { OPERATE, OPERATE_SETTING, READ_ONLY, WRITE } from './annotations.js';
|
|
7
7
|
import { isImportedProduct } from './catalogue.js';
|
|
8
|
-
import { runTool, toolError, toolResult } from './errors.js';
|
|
8
|
+
import { isNetworkFailure, runTool, toolError, toolResult } from './errors.js';
|
|
9
9
|
import { setupReadiness } from './readiness.js';
|
|
10
10
|
import { withStop } from './hints.js';
|
|
11
11
|
import { idInput, productId } from './inputs.js';
|
|
@@ -65,6 +65,54 @@ const MISSING_DOCUMENTS_HOW =
|
|
|
65
65
|
|
|
66
66
|
type ProductRow = WithoutTenant<Product>;
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Refuse a name already taken (PRO-2800): the API appends -2 to the slug and
|
|
70
|
+
* creates a second one, so a retried call or two agents at once left
|
|
71
|
+
* duplicates nobody asked for. allowDuplicateName says the person means it.
|
|
72
|
+
*/
|
|
73
|
+
const allowDuplicateName = z
|
|
74
|
+
.boolean()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe('Create it even when one with the same name exists (default false: refused, naming the existing one)');
|
|
77
|
+
|
|
78
|
+
/** The same name, as a person would read it: case and surrounding space aside. */
|
|
79
|
+
const sameName = (a: string, b: string) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
80
|
+
|
|
81
|
+
/** Pages read looking for a taken name; past this the create goes ahead. */
|
|
82
|
+
const NAME_PAGES = 5;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The first row with this name, across pages. Best effort: a list the API
|
|
86
|
+
* refuses or fails leaves the check out rather than the create.
|
|
87
|
+
*/
|
|
88
|
+
async function firstNamed<T extends { id: string; name: string; slug?: string }>(
|
|
89
|
+
name: string,
|
|
90
|
+
read: (cursor: string | undefined) => Promise<{ data: T[]; nextCursor: string | null }>
|
|
91
|
+
): Promise<T | null> {
|
|
92
|
+
let cursor: string | undefined;
|
|
93
|
+
try {
|
|
94
|
+
for (let pageIndex = 0; pageIndex < NAME_PAGES; pageIndex += 1) {
|
|
95
|
+
const result = await read(cursor);
|
|
96
|
+
const same = result.data.find((row) => sameName(row.name, name));
|
|
97
|
+
if (same) return same;
|
|
98
|
+
if (!result.nextCursor) return null;
|
|
99
|
+
cursor = result.nextCursor;
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function duplicateName(kind: 'factory' | 'product', existing: { id: string; name: string; slug?: string }) {
|
|
108
|
+
return toolError(
|
|
109
|
+
'DUPLICATE_NAME',
|
|
110
|
+
`A ${kind} named "${existing.name}" already exists (${existing.id}). Use it, choose another name, or pass ` +
|
|
111
|
+
'allowDuplicateName: true if the person wants a second one.',
|
|
112
|
+
{ existingId: existing.id, ...(existing.slug ? { existingSlug: existing.slug } : {}) }
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
68
116
|
/** A product row the model can act on: how it started, and while in setup what it still needs. */
|
|
69
117
|
async function productRow(client: TaigaClient, product: ProductRow) {
|
|
70
118
|
const { settings: _settings, ...rest } = pickFields([product], PRODUCT_FIELDS)[0]!;
|
|
@@ -118,17 +166,23 @@ export function registerSetupTools(server: McpServer, client: TaigaClient) {
|
|
|
118
166
|
{
|
|
119
167
|
title: 'Create a factory',
|
|
120
168
|
description:
|
|
121
|
-
'Create a factory: a group of products
|
|
122
|
-
'
|
|
123
|
-
'
|
|
124
|
-
|
|
125
|
-
|
|
169
|
+
'Create a factory: a group of products sharing knowledge, one instructions document and members. Owners ' +
|
|
170
|
+
'and admins only; members are added in the web. Then add products with taiga_create_product or ' +
|
|
171
|
+
'taiga_move_product.',
|
|
172
|
+
inputSchema: z.object({
|
|
173
|
+
name: z.string().trim().min(1).max(255).describe('The factory name'),
|
|
174
|
+
allowDuplicateName: allowDuplicateName,
|
|
175
|
+
}),
|
|
126
176
|
annotations: WRITE,
|
|
127
177
|
},
|
|
128
178
|
(params) =>
|
|
129
|
-
runTool(async () =>
|
|
130
|
-
|
|
131
|
-
|
|
179
|
+
runTool(async () => {
|
|
180
|
+
if (!params.allowDuplicateName) {
|
|
181
|
+
const same = await firstNamed(params.name, async (cursor) => client.listFactories({ limit: 100, cursor }));
|
|
182
|
+
if (same) return duplicateName('factory', same);
|
|
183
|
+
}
|
|
184
|
+
return toolResult(pickFields([(await client.createFactory({ name: params.name })).data], FACTORY_FIELDS)[0]);
|
|
185
|
+
})
|
|
132
186
|
);
|
|
133
187
|
|
|
134
188
|
server.registerTool(
|
|
@@ -136,8 +190,8 @@ export function registerSetupTools(server: McpServer, client: TaigaClient) {
|
|
|
136
190
|
{
|
|
137
191
|
title: 'Rename a factory',
|
|
138
192
|
description:
|
|
139
|
-
'Rename a factory. Its
|
|
140
|
-
'
|
|
193
|
+
'Rename a factory. Its slug follows the new name, so links to it change; products, members, knowledge ' +
|
|
194
|
+
'and instructions stay as they are.',
|
|
141
195
|
inputSchema: z.object({ factoryId, name: z.string().trim().min(1).max(255).describe('The new name') }),
|
|
142
196
|
annotations: OPERATE_SETTING,
|
|
143
197
|
},
|
|
@@ -154,10 +208,9 @@ export function registerSetupTools(server: McpServer, client: TaigaClient) {
|
|
|
154
208
|
{
|
|
155
209
|
title: 'Preview moving a product to another factory',
|
|
156
210
|
description:
|
|
157
|
-
|
|
158
|
-
"
|
|
159
|
-
|
|
160
|
-
'to the person, before taiga_move_product.',
|
|
211
|
+
"Show who gains and who loses access if a product moves to another factory: the source factory's members " +
|
|
212
|
+
"lose it, the target's gain it; people in both, the product's own members, and owners and admins keep " +
|
|
213
|
+
'it, named with why. Show it to the person before taiga_move_product.',
|
|
161
214
|
inputSchema: z.object({ productId, factoryId: factoryId.describe('The factory to move it to') }),
|
|
162
215
|
annotations: READ_ONLY,
|
|
163
216
|
},
|
|
@@ -170,11 +223,10 @@ export function registerSetupTools(server: McpServer, client: TaigaClient) {
|
|
|
170
223
|
{
|
|
171
224
|
title: 'Move a product to another factory',
|
|
172
225
|
description:
|
|
173
|
-
'Move a product to another factory. Access follows the factory, so this changes who can reach
|
|
174
|
-
"
|
|
175
|
-
'
|
|
176
|
-
'
|
|
177
|
-
'line first and then moves, which interrupts that work.',
|
|
226
|
+
'Move a product to another factory. Access follows the factory, so this changes who can reach it ' +
|
|
227
|
+
"(taiga_preview_product_move says who). The product's own knowledge moves with it; factory knowledge and " +
|
|
228
|
+
'instructions stay. Refused (WORK_IN_FLIGHT) while a plan or run is in flight; then ask the person: ' +
|
|
229
|
+
'stopLine: true stops the line first, interrupting that work.',
|
|
178
230
|
inputSchema: z.object({
|
|
179
231
|
productId,
|
|
180
232
|
factoryId: factoryId.describe('The factory to move it to'),
|
|
@@ -227,21 +279,19 @@ export function registerSetupTools(server: McpServer, client: TaigaClient) {
|
|
|
227
279
|
{
|
|
228
280
|
title: 'Create a product',
|
|
229
281
|
description:
|
|
230
|
-
'Create a product in a factory,
|
|
231
|
-
'later: scratch (
|
|
232
|
-
'
|
|
233
|
-
'
|
|
234
|
-
'
|
|
235
|
-
'
|
|
236
|
-
'one is off, deploymentNotes says in a sentence how that half is done today. Material the product should ' +
|
|
237
|
-
'be built from is worth adding before Discovery starts (taiga_add_knowledge with the productId): what is ' +
|
|
238
|
-
'added first shapes the specification.',
|
|
282
|
+
'Create a product in a factory, as New product does, and return it. Choose now how it starts; it cannot ' +
|
|
283
|
+
'change later: scratch (projectType greenfield), where Discovery is a conversation in the web that ' +
|
|
284
|
+
'writes the specification, or import (brownfield), where Taiga drafts the documents from a repository: ' +
|
|
285
|
+
'taiga_link_repository (owners and admins only), then taiga_start_import. Infrastructure code and CI/CD ' +
|
|
286
|
+
'pipelines follow the organization unless set here; when one is off, deploymentNotes says how that half ' +
|
|
287
|
+
'is done today. Add source material first with taiga_add_knowledge (productId): Discovery writes from it.',
|
|
239
288
|
inputSchema: z.object({
|
|
240
289
|
factoryId,
|
|
241
290
|
name: z.string().trim().min(1).max(255).describe('The product name'),
|
|
242
291
|
start: z.enum(['scratch', 'import']).describe('scratch: describe it in Discovery; import: from a repository'),
|
|
243
292
|
...buildScope,
|
|
244
293
|
deploymentNotes: deploymentNotes.optional(),
|
|
294
|
+
allowDuplicateName: allowDuplicateName,
|
|
245
295
|
}),
|
|
246
296
|
annotations: WRITE,
|
|
247
297
|
},
|
|
@@ -249,6 +299,10 @@ export function registerSetupTools(server: McpServer, client: TaigaClient) {
|
|
|
249
299
|
runTool(async () => {
|
|
250
300
|
const contradiction = notesWithNothingToDescribe(params);
|
|
251
301
|
if (contradiction) return contradiction;
|
|
302
|
+
if (!params.allowDuplicateName) {
|
|
303
|
+
const same = await firstNamed(params.name, async (cursor) => client.listProducts({ limit: 100, cursor }));
|
|
304
|
+
if (same) return duplicateName('product', same);
|
|
305
|
+
}
|
|
252
306
|
const product = (
|
|
253
307
|
await client.createProduct({
|
|
254
308
|
name: params.name,
|
|
@@ -268,11 +322,10 @@ export function registerSetupTools(server: McpServer, client: TaigaClient) {
|
|
|
268
322
|
{
|
|
269
323
|
title: "Change a product's settings",
|
|
270
324
|
description:
|
|
271
|
-
"Change a product's name or what Taiga builds for it. A new name changes the slug, so links
|
|
272
|
-
'
|
|
273
|
-
'
|
|
274
|
-
|
|
275
|
-
'product started). Later plans read the new answer; work already planned is not redone. Autonomy is ' +
|
|
325
|
+
"Change a product's name or what Taiga builds for it. A new name changes the slug, so links change. " +
|
|
326
|
+
'Infrastructure code and CI/CD pipelines: null returns one to the organization default; deploymentNotes ' +
|
|
327
|
+
'says how a half Taiga does not build is done; emitDesignStandards: null follows the default for how the ' +
|
|
328
|
+
'product started. Later plans read the change; planned work is not redone. Autonomy is ' +
|
|
276
329
|
'taiga_set_product_autonomy.',
|
|
277
330
|
inputSchema: z.object({
|
|
278
331
|
productId,
|
|
@@ -306,11 +359,9 @@ export function registerSetupTools(server: McpServer, client: TaigaClient) {
|
|
|
306
359
|
{
|
|
307
360
|
title: "Refresh a product's documents from its code",
|
|
308
361
|
description:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
'
|
|
312
|
-
'again; unchanged sections are not republished. Runs an agent in the background and returns the job; ' +
|
|
313
|
-
'taiga_wait_for_job says when it is done.',
|
|
362
|
+
'Derive the reality documents again from the code on the branch Taiga works on (effectiveBranch): all, ' +
|
|
363
|
+
'or the ones named. For a document the import misread, refresh it instead of importing again; unchanged ' +
|
|
364
|
+
'sections are not republished. Returns the job; taiga_wait_for_job says when it is done.',
|
|
314
365
|
inputSchema: z.object({
|
|
315
366
|
productId,
|
|
316
367
|
docTypes: z
|
|
@@ -331,14 +382,11 @@ export function registerSetupTools(server: McpServer, client: TaigaClient) {
|
|
|
331
382
|
{
|
|
332
383
|
title: 'Finish Discovery',
|
|
333
384
|
description:
|
|
334
|
-
"Finish a product's Discovery
|
|
335
|
-
'
|
|
336
|
-
'
|
|
337
|
-
'
|
|
338
|
-
'
|
|
339
|
-
'and opens the rest of the product; for an ' +
|
|
340
|
-
'imported product it is also when the first initiatives are planned. A locked document changes only ' +
|
|
341
|
-
'after taiga_reopen_discovery. Ask the person first: it is their decision that the documents are right.',
|
|
385
|
+
"Finish a product's Discovery: it locks the documents and opens the rest of the product; for an imported " +
|
|
386
|
+
'product the first initiatives are planned. Refused (CONFLICT) with details.reason: documents_incomplete ' +
|
|
387
|
+
'(details.missingDocuments lists them and how they are made), not_in_setup (already finished), or a ' +
|
|
388
|
+
'required check not yet stamped (stamped in the web). A locked document changes only after ' +
|
|
389
|
+
'taiga_reopen_discovery. The person decides the documents are right: ask first.',
|
|
342
390
|
inputSchema: z.object({ productId }),
|
|
343
391
|
annotations: OPERATE,
|
|
344
392
|
},
|
|
@@ -380,13 +428,11 @@ export function registerSetupTools(server: McpServer, client: TaigaClient) {
|
|
|
380
428
|
{
|
|
381
429
|
title: 'Import a codebase into a product',
|
|
382
430
|
description:
|
|
383
|
-
'Start
|
|
384
|
-
'derives the full document set from the code,
|
|
385
|
-
'
|
|
386
|
-
|
|
387
|
-
'
|
|
388
|
-
'linked repository, or while an import runs. Runs agents in the background and returns the job; ' +
|
|
389
|
-
'taiga_wait_for_job says when it is done (it takes a while).',
|
|
431
|
+
'Start importing a product created for import, and return the job. Taiga reads the linked repository and ' +
|
|
432
|
+
'derives the full document set from the code, citing the files. Then the scanners sweep it, the ' +
|
|
433
|
+
'documents are assessed against policies, and the first initiatives are planned when Discovery finishes. ' +
|
|
434
|
+
'Refused for a scratch product, without a linked repository, or while an import runs. taiga_wait_for_job ' +
|
|
435
|
+
'says when it is done (it takes a while).',
|
|
390
436
|
inputSchema: z.object({ productId }),
|
|
391
437
|
annotations: OPERATE,
|
|
392
438
|
},
|
package/src/tools/summarize.ts
CHANGED
|
@@ -61,7 +61,9 @@ export async function searchPages<T>(
|
|
|
61
61
|
fetchPage: (cursor: string | undefined) => Promise<{ data: T[]; nextCursor: string | null }>,
|
|
62
62
|
query: string,
|
|
63
63
|
text: (row: T) => unknown[],
|
|
64
|
-
limit: number
|
|
64
|
+
limit: number,
|
|
65
|
+
/** A further condition a row must meet, besides the query. */
|
|
66
|
+
keep: (row: T) => boolean = () => true
|
|
65
67
|
): Promise<{ rows: T[]; more: boolean }> {
|
|
66
68
|
const needle = query.toLowerCase();
|
|
67
69
|
const rows: T[] = [];
|
|
@@ -70,6 +72,7 @@ export async function searchPages<T>(
|
|
|
70
72
|
const result = await fetchPage(cursor);
|
|
71
73
|
const page = result.data;
|
|
72
74
|
for (const [at, row] of page.entries()) {
|
|
75
|
+
if (!keep(row)) continue;
|
|
73
76
|
if (!text(row).some((value) => typeof value === 'string' && value.toLowerCase().includes(needle))) continue;
|
|
74
77
|
rows.push(row);
|
|
75
78
|
// Full: stop reading. \`more\` then means "there may be more", not
|
package/src/tools/wait.ts
CHANGED
|
@@ -360,17 +360,12 @@ export function registerWaitTools(server: McpServer, client: TaigaClient, option
|
|
|
360
360
|
{
|
|
361
361
|
title: 'Wait for an initiative',
|
|
362
362
|
description:
|
|
363
|
-
'Wait
|
|
364
|
-
'
|
|
365
|
-
'
|
|
366
|
-
'
|
|
367
|
-
'
|
|
368
|
-
'
|
|
369
|
-
'initiative, or a landed plan whose review gate cannot be read; stoppedBecause says which), or at the ' +
|
|
370
|
-
'timeout with reached: false: ' +
|
|
371
|
-
'call again to keep waiting. Conditions are states, not events: an initiative whose latest run is already ' +
|
|
372
|
-
'settled answers run_settled at once; item is null when not even the first read answered in time. ' +
|
|
373
|
-
'Planning takes minutes and a build up to an hour.',
|
|
363
|
+
'Wait until an initiative reaches a condition instead of polling: changed, planned, your_turn (waits on ' +
|
|
364
|
+
'a person), pr_open (open now; merged or closed does not count), run_settled (latest run stopped moving) ' +
|
|
365
|
+
'or done. Returns reached: true when it ' +
|
|
366
|
+
'holds; early with stoppedBecause when only a person can move it (a flag, failed plan, plan or pull ' +
|
|
367
|
+
'request awaiting review, a cancel); or reached: false at the timeout: call again. A condition already ' +
|
|
368
|
+
'true answers at once. Planning takes minutes, a run up to an hour.',
|
|
374
369
|
inputSchema: z.object({
|
|
375
370
|
initiativeId: idInput('initiative', 'taiga_list_initiatives (query finds one by title) gives it'),
|
|
376
371
|
until: z.enum(ITEM_CONDITIONS).describe('What to wait for'),
|
|
@@ -425,13 +420,10 @@ export function registerWaitTools(server: McpServer, client: TaigaClient, option
|
|
|
425
420
|
{
|
|
426
421
|
title: 'Wait for a run',
|
|
427
422
|
description:
|
|
428
|
-
'Wait
|
|
429
|
-
'
|
|
430
|
-
'
|
|
431
|
-
'
|
|
432
|
-
'changed waits on, since a person resuming the run is a change), or at the timeout with reached: false: ' +
|
|
433
|
-
'call again to keep waiting. Conditions are states: a run already settled answers settled at once; run ' +
|
|
434
|
-
'is null when not even the first read answered in time.',
|
|
423
|
+
'Wait until a run reaches a condition: changed, step_done (one more step finished), pr_open (open now) ' +
|
|
424
|
+
'or settled (in review, completed, failed, stopped). Returns reached: true when it holds; early with ' +
|
|
425
|
+
'stoppedBecause if the run settled without it; or reached: false at the timeout: call again. A settled ' +
|
|
426
|
+
'run answers settled at once; run is null if the first read timed out.',
|
|
435
427
|
inputSchema: z.object({
|
|
436
428
|
runId: idInput('run', 'taiga_list_runs gives it'),
|
|
437
429
|
until: z.enum(BUILD_CONDITIONS).describe('What to wait for'),
|
|
@@ -468,10 +460,9 @@ export function registerWaitTools(server: McpServer, client: TaigaClient, option
|
|
|
468
460
|
{
|
|
469
461
|
title: 'Wait for an agent job',
|
|
470
462
|
description:
|
|
471
|
-
'Wait until an agent job finishes (completed, failed or cancelled)
|
|
472
|
-
|
|
473
|
-
'
|
|
474
|
-
'jobs take a few minutes.',
|
|
463
|
+
'Wait until an agent job finishes (completed, failed or cancelled). Returns the job as taiga_get_job ' +
|
|
464
|
+
"reads it, with an initiative request's outcome, and reached: true; or reached: false at the timeout: " +
|
|
465
|
+
'call again to keep waiting. Most jobs take a few minutes.',
|
|
475
466
|
inputSchema: z.object({
|
|
476
467
|
jobId: idInput('job', 'the tool that started the job returned it'),
|
|
477
468
|
timeoutSeconds,
|
|
Binary file
|
|
Binary file
|