@taiga-ai/mcp-server 0.7.0-dev.395 → 0.8.0-dev.397
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 +383 -33
- package/package.json +1 -1
- package/src/client.ts +20 -4
- package/src/server.integration.test.ts +448 -9
- package/src/tools/backlogs.ts +8 -8
- package/src/tools/builds.ts +8 -8
- package/src/tools/context.ts +280 -0
- package/src/tools/documents.ts +7 -6
- package/src/tools/errors.ts +20 -3
- package/src/tools/index.ts +2 -0
- package/src/tools/inputs.ts +32 -0
- package/src/tools/instructions.ts +6 -2
- package/src/tools/products.ts +88 -40
- package/src/tools/sections.test.ts +18 -1
- package/src/tools/sections.ts +17 -0
- package/src/tools/summarize.ts +5 -0
- package/taiga-ai-mcp-server-0.8.0-dev.397.tgz +0 -0
- package/taiga-ai-mcp-server-0.7.0-dev.395.tgz +0 -0
package/dist/index.js
CHANGED
|
@@ -106,9 +106,13 @@ var TaigaClient = class {
|
|
|
106
106
|
async getProduct(id) {
|
|
107
107
|
return this.request("GET", `/products/${seg(id)}`);
|
|
108
108
|
}
|
|
109
|
+
async listProductRepositories(productId) {
|
|
110
|
+
return this.request("GET", `/products/${seg(productId)}/repositories`);
|
|
111
|
+
}
|
|
109
112
|
async listTenantDocuments(opts) {
|
|
110
113
|
const params = new URLSearchParams();
|
|
111
114
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
115
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
112
116
|
const qs = params.toString();
|
|
113
117
|
return this.request("GET", `/tenants/current/documents${qs ? `?${qs}` : ""}`);
|
|
114
118
|
}
|
|
@@ -128,6 +132,7 @@ var TaigaClient = class {
|
|
|
128
132
|
async listFactoryDocuments(factoryId, opts) {
|
|
129
133
|
const params = new URLSearchParams();
|
|
130
134
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
135
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
131
136
|
const qs = params.toString();
|
|
132
137
|
return this.request("GET", `/factories/${seg(factoryId)}/documents${qs ? `?${qs}` : ""}`);
|
|
133
138
|
}
|
|
@@ -162,6 +167,7 @@ var TaigaClient = class {
|
|
|
162
167
|
if (opts?.category) params.set("category", opts.category);
|
|
163
168
|
if (opts?.includeArchived) params.set("includeArchived", "true");
|
|
164
169
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
170
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
165
171
|
const qs = params.toString();
|
|
166
172
|
return this.request("GET", `/products/${seg(productId)}/backlog${qs ? `?${qs}` : ""}`);
|
|
167
173
|
}
|
|
@@ -172,6 +178,7 @@ var TaigaClient = class {
|
|
|
172
178
|
const params = new URLSearchParams();
|
|
173
179
|
if (opts?.backlogItemId) params.set("backlogItemId", opts.backlogItemId);
|
|
174
180
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
181
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
175
182
|
const qs = params.toString();
|
|
176
183
|
return this.request("GET", `/products/${seg(productId)}/builds${qs ? `?${qs}` : ""}`);
|
|
177
184
|
}
|
|
@@ -187,6 +194,7 @@ var TaigaClient = class {
|
|
|
187
194
|
async listFactories(opts) {
|
|
188
195
|
const params = new URLSearchParams();
|
|
189
196
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
197
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
190
198
|
const qs = params.toString();
|
|
191
199
|
return this.request("GET", `/factories${qs ? `?${qs}` : ""}`);
|
|
192
200
|
}
|
|
@@ -219,6 +227,17 @@ const READ_ONLY = {
|
|
|
219
227
|
openWorldHint: false
|
|
220
228
|
};
|
|
221
229
|
//#endregion
|
|
230
|
+
//#region src/tools/inputs.ts
|
|
231
|
+
/** The inputs several tools share, so a product or a page is named the same way everywhere. */
|
|
232
|
+
const productId = z.string().uuid().describe("Product ID -- taiga_get_product_context, taiga_resolve_product or taiga_list_products give it");
|
|
233
|
+
/** The page inputs every list tool takes; `nextCursor` of one page is the `cursor` of the next. */
|
|
234
|
+
const pagination = {
|
|
235
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max results (default 25)"),
|
|
236
|
+
cursor: z.string().max(256).optional().describe("The nextCursor of the previous page, to read the next one; omit for the first page")
|
|
237
|
+
};
|
|
238
|
+
const slug = z.string().max(63).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "A slug is lowercase words joined by hyphens").describe("Product slug, as it appears in Taiga URLs");
|
|
239
|
+
const remoteUrl = z.string().min(1).max(2048).describe("The git remote URL of a checkout (git remote get-url origin): https://, git@host:owner/repo.git or ssh://");
|
|
240
|
+
//#endregion
|
|
222
241
|
//#region ../../packages/types/dist/tenants.js
|
|
223
242
|
const TENANT_DOCUMENT_TYPES = [
|
|
224
243
|
"policy_security",
|
|
@@ -436,6 +455,36 @@ const PRODUCT_DOCUMENT_DISPLAY_NAMES = {
|
|
|
436
455
|
service_blueprint: "Service Blueprint"
|
|
437
456
|
};
|
|
438
457
|
const PRODUCT_DOCUMENT_ORIGINS = ["plan", "reality"];
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region ../../packages/types/dist/backlog.js
|
|
460
|
+
/** Dispositions that end an item's pipeline life: settled work neither blocks
|
|
461
|
+
* dependants nor counts toward completion denominators. */
|
|
462
|
+
const SETTLED_BACKLOG_STATUSES = ["done", "canceled"];
|
|
463
|
+
/** Finished work: settled by a person or a merge, or archived. The one
|
|
464
|
+
* predicate every "can this still move / plan / build" guard reads. */
|
|
465
|
+
function isSettledOrArchived(item) {
|
|
466
|
+
return item.archivedAt != null || SETTLED_BACKLOG_STATUSES.includes(item.status);
|
|
467
|
+
}
|
|
468
|
+
const TURN_EVENT_TRIGGERS = [
|
|
469
|
+
"defined",
|
|
470
|
+
"plan_started",
|
|
471
|
+
"plan_completed",
|
|
472
|
+
"plan_failed",
|
|
473
|
+
"plan_approved",
|
|
474
|
+
"plan_rejected",
|
|
475
|
+
"build_started",
|
|
476
|
+
"pr_opened",
|
|
477
|
+
"pr_ready",
|
|
478
|
+
"build_completed",
|
|
479
|
+
"build_failed",
|
|
480
|
+
"build_stopped",
|
|
481
|
+
"pr_merged",
|
|
482
|
+
"pr_closed",
|
|
483
|
+
"edited",
|
|
484
|
+
"disposition_changed",
|
|
485
|
+
"archived",
|
|
486
|
+
"unarchived"
|
|
487
|
+
];
|
|
439
488
|
/**
|
|
440
489
|
* The two blocking states are NOT the same kind of blocker, and collapsing them
|
|
441
490
|
* into one list is what caused a bug in each direction (PRO-1028, PRO-1039).
|
|
@@ -690,6 +739,7 @@ function runDisposition(status) {
|
|
|
690
739
|
const BUILD_RUN_ACTIVE_STATUSES = BUILD_RUN_STATUSES.filter((s) => runDisposition(s) === "in_flight" && s !== "pending");
|
|
691
740
|
BUILD_RUN_STATUSES.filter((s) => runDisposition(s) !== "in_flight");
|
|
692
741
|
[...BUILD_RUN_ACTIVE_STATUSES.filter((s) => s !== "reviewing")];
|
|
742
|
+
[...TURN_EVENT_TRIGGERS];
|
|
693
743
|
//#endregion
|
|
694
744
|
//#region ../../packages/types/dist/maintenance.js
|
|
695
745
|
/**
|
|
@@ -1088,6 +1138,19 @@ function toolError(error, message) {
|
|
|
1088
1138
|
isError: true
|
|
1089
1139
|
};
|
|
1090
1140
|
}
|
|
1141
|
+
/**
|
|
1142
|
+
* fetch rejects with a TypeError carrying a `cause` when the API cannot be
|
|
1143
|
+
* reached; a TypeError from our own code carries none and is a bug to surface.
|
|
1144
|
+
*/
|
|
1145
|
+
function isNetworkFailure(error) {
|
|
1146
|
+
return error instanceof TypeError && "cause" in error;
|
|
1147
|
+
}
|
|
1148
|
+
/** What went wrong on the wire: undici's message is the constant "fetch failed", the cause says why. */
|
|
1149
|
+
function networkFailureDetail(error) {
|
|
1150
|
+
const cause = error.cause;
|
|
1151
|
+
const why = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : null;
|
|
1152
|
+
return why ? `${error.message} (${why})` : error.message;
|
|
1153
|
+
}
|
|
1091
1154
|
const CODE_BY_STATUS = {
|
|
1092
1155
|
400: "VALIDATION_FAILED",
|
|
1093
1156
|
401: "UNAUTHENTICATED",
|
|
@@ -1108,7 +1171,7 @@ async function runTool(handler) {
|
|
|
1108
1171
|
return await handler();
|
|
1109
1172
|
} catch (error) {
|
|
1110
1173
|
if (error instanceof TaigaApiError) return toolError(error.code ?? CODE_BY_STATUS[error.status] ?? "API_ERROR", error.apiMessage ?? `Taiga API answered ${error.status}.`);
|
|
1111
|
-
if (error
|
|
1174
|
+
if (isNetworkFailure(error)) return toolError("NETWORK_ERROR", `Could not reach the Taiga API: ${networkFailureDetail(error)}. Check TAIGA_API_URL.`);
|
|
1112
1175
|
throw error;
|
|
1113
1176
|
}
|
|
1114
1177
|
}
|
|
@@ -1225,6 +1288,19 @@ function stripProvenance(doc) {
|
|
|
1225
1288
|
}
|
|
1226
1289
|
};
|
|
1227
1290
|
}
|
|
1291
|
+
/**
|
|
1292
|
+
* The free-form text of an instructions document, the way the API hands it
|
|
1293
|
+
* to the agents (factory-instructions.ts: every non-empty section, in order,
|
|
1294
|
+
* joined by a blank line). Null when there is nothing to read.
|
|
1295
|
+
*/
|
|
1296
|
+
function instructionsText(sections) {
|
|
1297
|
+
const parts = [];
|
|
1298
|
+
for (const section of Object.values(sections)) {
|
|
1299
|
+
const content = isRecord(section) ? section.content : void 0;
|
|
1300
|
+
if (typeof content === "string" && content.trim()) parts.push(content.trim());
|
|
1301
|
+
}
|
|
1302
|
+
return parts.join("\n\n").trim() || null;
|
|
1303
|
+
}
|
|
1228
1304
|
//#endregion
|
|
1229
1305
|
//#region src/tools/summarize.ts
|
|
1230
1306
|
/**
|
|
@@ -1239,6 +1315,13 @@ function pickFields(items, fields) {
|
|
|
1239
1315
|
return picked;
|
|
1240
1316
|
});
|
|
1241
1317
|
}
|
|
1318
|
+
/** The page a list tool returns: the rows the model asked for and where the next page starts. */
|
|
1319
|
+
function page(data, nextCursor) {
|
|
1320
|
+
return {
|
|
1321
|
+
data,
|
|
1322
|
+
nextCursor: nextCursor ?? null
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1242
1325
|
//#endregion
|
|
1243
1326
|
//#region src/tools/products.ts
|
|
1244
1327
|
/** What a product row needs to say for the model to pick one and read it in full. */
|
|
@@ -1274,12 +1357,12 @@ const PAGE_SIZE = 100;
|
|
|
1274
1357
|
* matches is not a resolution the model can act on anyway.
|
|
1275
1358
|
*/
|
|
1276
1359
|
const RESOLVE_PAGE_LIMIT = 10;
|
|
1277
|
-
const productId = z.string().uuid().describe("Product ID -- get from taiga_resolve_product (from a git remote) or taiga_list_products");
|
|
1278
1360
|
const originInput = z.enum(PRODUCT_DOCUMENT_ORIGINS).optional().describe(`Which timeline. ${ORIGIN_MEANING.plan} ${ORIGIN_MEANING.reality} Omit it to read the product's own timeline: reality for an imported product, plan for every other, and always plan for the documents a person writes (Look & Feel, Product Instructions).`);
|
|
1279
1361
|
/**
|
|
1280
1362
|
* Every product linked to the repository, page by page, until the API runs
|
|
1281
|
-
* out or the cap is hit
|
|
1282
|
-
*
|
|
1363
|
+
* out or the cap is hit, in which case the list says it is truncated. A page
|
|
1364
|
+
* that FAILS is different: it fails the call, like every other tool, rather
|
|
1365
|
+
* than being folded into a truncated list that would read as a complete one.
|
|
1283
1366
|
*/
|
|
1284
1367
|
async function linkedProducts(client, repository, repositoryHost) {
|
|
1285
1368
|
const products = [];
|
|
@@ -1379,14 +1462,46 @@ function describeMiss(type, wanted, timelines, product) {
|
|
|
1379
1462
|
}
|
|
1380
1463
|
return parts.join(" ");
|
|
1381
1464
|
}
|
|
1465
|
+
/**
|
|
1466
|
+
* One product from whichever name the caller has: its ID, its slug (through
|
|
1467
|
+
* the list's slug filter) or the git remote of a checkout (through the
|
|
1468
|
+
* repository filter). Exactly one of the three; a remote linked to several
|
|
1469
|
+
* products is ambiguous and says so.
|
|
1470
|
+
*/
|
|
1471
|
+
async function resolveProductRef(client, ref, accepted) {
|
|
1472
|
+
if ([
|
|
1473
|
+
ref.productId,
|
|
1474
|
+
ref.slug,
|
|
1475
|
+
ref.remoteUrl
|
|
1476
|
+
].filter((v) => v !== void 0).length !== 1) return { error: toolError("VALIDATION_FAILED", `Name the product exactly once: ${accepted.join(" or ")}. Two names could mean two products.`) };
|
|
1477
|
+
if (ref.productId) return { product: (await client.getProduct(ref.productId)).data };
|
|
1478
|
+
if (ref.slug) {
|
|
1479
|
+
const [product] = (await client.listProducts({
|
|
1480
|
+
slug: ref.slug,
|
|
1481
|
+
limit: 1
|
|
1482
|
+
})).data;
|
|
1483
|
+
if (!product) return { error: toolError("NOT_FOUND", `No product you can see has the slug "${ref.slug}". taiga_list_products shows the ones you can.`) };
|
|
1484
|
+
return { product };
|
|
1485
|
+
}
|
|
1486
|
+
const remote = parseRemote(ref.remoteUrl);
|
|
1487
|
+
if (!remote) return { error: toolError("INVALID_REMOTE", `"${ref.remoteUrl}" is not a remote of the form host/owner/repo. Pass the URL git remote get-url origin prints.`) };
|
|
1488
|
+
const { products } = await linkedProducts(client, remote.repository, remote.host);
|
|
1489
|
+
if (products.length === 0) return { error: toolError("NOT_FOUND", `No product you can see is linked to ${remote.repository} on ${remote.host}. Link the repository to its product in Taiga, or find the product with taiga_list_products.`) };
|
|
1490
|
+
if (products.length > 1) {
|
|
1491
|
+
const names = pickFields(products, PRODUCT_SUMMARY_FIELDS).map((p) => `${String(p.name)} (${String(p.id)})`).join(", ");
|
|
1492
|
+
return { error: toolError("AMBIGUOUS", `${remote.repository} is linked to several products: ${names}. Call again with the productId of the one you mean.`) };
|
|
1493
|
+
}
|
|
1494
|
+
return { product: products[0] };
|
|
1495
|
+
}
|
|
1382
1496
|
function registerProductTools(server, client) {
|
|
1383
1497
|
if (client.hasCapability("factories:read")) server.registerTool("taiga_list_factories", {
|
|
1384
1498
|
title: "List factories",
|
|
1385
1499
|
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.",
|
|
1386
|
-
inputSchema: {
|
|
1500
|
+
inputSchema: { ...pagination },
|
|
1387
1501
|
annotations: READ_ONLY
|
|
1388
1502
|
}, (params) => runTool(async () => {
|
|
1389
|
-
|
|
1503
|
+
const result = await client.listFactories(params);
|
|
1504
|
+
return toolResult(page(result.data, result.nextCursor));
|
|
1390
1505
|
}));
|
|
1391
1506
|
if (client.hasCapability("products:read")) {
|
|
1392
1507
|
server.registerTool("taiga_list_products", {
|
|
@@ -1395,35 +1510,29 @@ function registerProductTools(server, client) {
|
|
|
1395
1510
|
inputSchema: {
|
|
1396
1511
|
factoryId: z.string().uuid().optional().describe("Filter by factory ID -- get factory IDs from taiga_list_factories"),
|
|
1397
1512
|
status: z.enum(PRODUCT_STATUSES).optional().describe("Filter by status: draft until the product finishes setup in Taiga, active after that, archived when retired."),
|
|
1398
|
-
|
|
1513
|
+
...pagination
|
|
1399
1514
|
},
|
|
1400
1515
|
annotations: READ_ONLY
|
|
1401
1516
|
}, (params) => runTool(async () => {
|
|
1402
|
-
|
|
1517
|
+
const result = await client.listProducts(params);
|
|
1518
|
+
return toolResult(page(pickFields(result.data, PRODUCT_SUMMARY_FIELDS), result.nextCursor));
|
|
1403
1519
|
}));
|
|
1404
1520
|
server.registerTool("taiga_get_product", {
|
|
1405
1521
|
title: "Get product",
|
|
1406
1522
|
description: "Get one product in full, by ID or by slug: name, slug, factory, what Taiga owns for it (application, infrastructure, deployment) and its settings; the list and resolve tools carry summaries. Use taiga_list_product_documents for its specification and design, taiga_list_backlog_items for its work and taiga_list_builds for what the coding agent did.",
|
|
1407
1523
|
inputSchema: {
|
|
1408
1524
|
productId: productId.optional(),
|
|
1409
|
-
slug:
|
|
1525
|
+
slug: slug.optional()
|
|
1410
1526
|
},
|
|
1411
1527
|
annotations: READ_ONLY
|
|
1412
1528
|
}, (params) => runTool(async () => {
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
if (!params.slug) return toolError("VALIDATION_FAILED", "Pass productId or slug.");
|
|
1416
|
-
const [product] = (await client.listProducts({
|
|
1417
|
-
slug: params.slug,
|
|
1418
|
-
limit: 1
|
|
1419
|
-
})).data;
|
|
1420
|
-
if (!product) return toolError("NOT_FOUND", `No product you can see has the slug "${params.slug}". taiga_list_products shows the ones you can.`);
|
|
1421
|
-
return toolResult(product);
|
|
1529
|
+
const resolved = await resolveProductRef(client, params, ["productId", "slug"]);
|
|
1530
|
+
return "error" in resolved ? resolved.error : toolResult(resolved.product);
|
|
1422
1531
|
}));
|
|
1423
1532
|
server.registerTool("taiga_resolve_product", {
|
|
1424
1533
|
title: "Resolve product from git remote",
|
|
1425
|
-
description: "Find the Taiga product a git checkout belongs to from its remote URL (the output of `git remote get-url origin`).
|
|
1426
|
-
inputSchema: { remoteUrl
|
|
1534
|
+
description: "Find the Taiga product a git checkout belongs to from its remote URL (the output of `git remote get-url origin`). taiga_get_product_context takes the same URL and returns the whole context at once; use this when you only need the ID. Returns every product the repository is linked to (several when the organization shares repositories; `truncated` is true past 1000); NOT_FOUND means nobody linked it, or the key cannot see the product.",
|
|
1535
|
+
inputSchema: { remoteUrl },
|
|
1427
1536
|
annotations: READ_ONLY
|
|
1428
1537
|
}, (params) => runTool(async () => {
|
|
1429
1538
|
const remote = parseRemote(params.remoteUrl);
|
|
@@ -1562,11 +1671,11 @@ function registerDocumentTools(server, client) {
|
|
|
1562
1671
|
server.registerTool("taiga_list_documents", {
|
|
1563
1672
|
title: "List company documents",
|
|
1564
1673
|
description: `List the organization's documents as summaries (type, name, description, version, draft state). The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) define the security, data privacy, SDLC, operations, architecture and compliance rules that apply across all factories and products; the other documents (${named(OTHER_TENANT_DOCUMENT_TYPES)}) carry goals, standards, voice and standing instructions. Use taiga_get_document with the type to read one in full.`,
|
|
1565
|
-
inputSchema: {
|
|
1674
|
+
inputSchema: { ...pagination },
|
|
1566
1675
|
annotations: READ_ONLY
|
|
1567
1676
|
}, (params) => runTool(async () => {
|
|
1568
1677
|
const result = await client.listTenantDocuments(params);
|
|
1569
|
-
return toolResult(summaries(result.data));
|
|
1678
|
+
return toolResult(page(summaries(result.data), result.nextCursor));
|
|
1570
1679
|
}));
|
|
1571
1680
|
server.registerTool("taiga_get_document", {
|
|
1572
1681
|
title: "Get company document",
|
|
@@ -1620,12 +1729,12 @@ function registerDocumentTools(server, client) {
|
|
|
1620
1729
|
description: `List a factory's documents, published and draft, as summaries (type, name, version, draft state). A factory has one document type, ${named(FACTORY_DOCUMENT_TYPES)}: its free-form standing guidance (domain context, tech preferences, conventions) that applies to every product it owns. Drafts are listed too (isDraft true); an empty list means neither a publication nor a draft exists. Use taiga_get_factory_document to read the full content.`,
|
|
1621
1730
|
inputSchema: {
|
|
1622
1731
|
factoryId: z.string().uuid().describe("Factory ID -- get from taiga_list_factories"),
|
|
1623
|
-
|
|
1732
|
+
...pagination
|
|
1624
1733
|
},
|
|
1625
1734
|
annotations: READ_ONLY
|
|
1626
1735
|
}, (params) => runTool(async () => {
|
|
1627
|
-
const result = await client.listFactoryDocuments(params.factoryId,
|
|
1628
|
-
return toolResult(summaries(result.data));
|
|
1736
|
+
const result = await client.listFactoryDocuments(params.factoryId, params);
|
|
1737
|
+
return toolResult(page(summaries(result.data), result.nextCursor));
|
|
1629
1738
|
}));
|
|
1630
1739
|
server.registerTool("taiga_get_factory_document", {
|
|
1631
1740
|
title: "Get factory instructions",
|
|
@@ -1704,7 +1813,7 @@ function registerBacklogTools(server, client) {
|
|
|
1704
1813
|
title: "List backlog items",
|
|
1705
1814
|
description: "List a product's backlog items (initiatives): titles, category, status and where each one is on the line (backlog, todo, queue, plan, build, done). Use this to find the item you are working on, then read its acceptance criteria and technical context with taiga_get_backlog_item. Archived items are left out unless asked for.",
|
|
1706
1815
|
inputSchema: {
|
|
1707
|
-
productId
|
|
1816
|
+
productId,
|
|
1708
1817
|
status: z.enum([
|
|
1709
1818
|
"backlog",
|
|
1710
1819
|
"in_progress",
|
|
@@ -1724,11 +1833,12 @@ function registerBacklogTools(server, client) {
|
|
|
1724
1833
|
"improvement"
|
|
1725
1834
|
]).optional().describe("Only this category"),
|
|
1726
1835
|
includeArchived: z.boolean().optional().describe("Include archived items (default false)"),
|
|
1727
|
-
|
|
1836
|
+
...pagination
|
|
1728
1837
|
},
|
|
1729
1838
|
annotations: READ_ONLY
|
|
1730
1839
|
}, (params) => runTool(async () => {
|
|
1731
|
-
|
|
1840
|
+
const result = await client.listBacklogItems(params.productId, params);
|
|
1841
|
+
return toolResult(page(pickFields(result.data, BACKLOG_SUMMARY_FIELDS), result.nextCursor));
|
|
1732
1842
|
}));
|
|
1733
1843
|
server.registerTool("taiga_get_backlog_item", {
|
|
1734
1844
|
title: "Get backlog item",
|
|
@@ -1771,13 +1881,14 @@ function registerBuildTools(server, client) {
|
|
|
1771
1881
|
title: "List build runs",
|
|
1772
1882
|
description: "List the coding agent's runs for a product, newest first: status, branch, pull request and step progress. Use this to see what Taiga already built or is building, then read one run with taiga_get_build. Filter by backlog item to follow a single initiative.",
|
|
1773
1883
|
inputSchema: {
|
|
1774
|
-
productId
|
|
1884
|
+
productId,
|
|
1775
1885
|
backlogItemId: z.string().uuid().optional().describe("Only this backlog item's runs -- get from taiga_list_backlog_items"),
|
|
1776
|
-
|
|
1886
|
+
...pagination
|
|
1777
1887
|
},
|
|
1778
1888
|
annotations: READ_ONLY
|
|
1779
1889
|
}, (params) => runTool(async () => {
|
|
1780
|
-
|
|
1890
|
+
const result = await client.listBuildRuns(params.productId, params);
|
|
1891
|
+
return toolResult(page(pickFields(result.data, BUILD_SUMMARY_FIELDS), result.nextCursor));
|
|
1781
1892
|
}));
|
|
1782
1893
|
server.registerTool("taiga_get_build", {
|
|
1783
1894
|
title: "Get build run",
|
|
@@ -1793,6 +1904,240 @@ function registerBuildTools(server, client) {
|
|
|
1793
1904
|
}));
|
|
1794
1905
|
}
|
|
1795
1906
|
//#endregion
|
|
1907
|
+
//#region src/tools/context.ts
|
|
1908
|
+
/**
|
|
1909
|
+
* One call at the start of a coding session: everything the model would
|
|
1910
|
+
* otherwise collect in eight, fanned out here. Each part stands on its own
|
|
1911
|
+
* (Promise.allSettled): a part the API refuses or fails is named under
|
|
1912
|
+
* `unavailable` with what the API said, a part the key's capabilities do not
|
|
1913
|
+
* cover is named under `notGranted`, and neither is ever shown as empty. A
|
|
1914
|
+
* failure that is not the API's (a bug in this code) is not downgraded; it
|
|
1915
|
+
* fails the call so it surfaces.
|
|
1916
|
+
*/
|
|
1917
|
+
/** The two documents a coding session starts from; the rest are a read away. */
|
|
1918
|
+
const CONTEXT_DOCUMENT_TYPES = ["specification", "architecture"];
|
|
1919
|
+
const REPOSITORY_SUMMARY_FIELDS = [
|
|
1920
|
+
"id",
|
|
1921
|
+
"provider",
|
|
1922
|
+
"repositoryName",
|
|
1923
|
+
"repositoryUrl",
|
|
1924
|
+
"effectiveBranch",
|
|
1925
|
+
"isManaged"
|
|
1926
|
+
];
|
|
1927
|
+
/** How many of the latest runs to carry; taiga_list_builds pages the rest. */
|
|
1928
|
+
const BUILDS_IN_CONTEXT = 10;
|
|
1929
|
+
/** Pages of 100 the backlog part follows before it stops and says so. */
|
|
1930
|
+
const BACKLOG_PAGE_LIMIT = 10;
|
|
1931
|
+
const PARTS = {
|
|
1932
|
+
factory: "factories:read",
|
|
1933
|
+
documents: "products:read",
|
|
1934
|
+
policies: "tenant:documents:read",
|
|
1935
|
+
backlog: "backlog:read",
|
|
1936
|
+
repositories: "products:read",
|
|
1937
|
+
builds: "products:read"
|
|
1938
|
+
};
|
|
1939
|
+
/** What the API said when a part failed, in the words the bundle reports. */
|
|
1940
|
+
function unavailableReason(reason) {
|
|
1941
|
+
if (reason instanceof TaigaApiError) return `${reason.status} ${reason.code ?? ""}`.trim() + (reason.apiMessage ? `: ${reason.apiMessage}` : "");
|
|
1942
|
+
return `the API could not be reached: ${networkFailureDetail(reason)}`;
|
|
1943
|
+
}
|
|
1944
|
+
/** Whether a failed part is the API's doing; anything else is ours and propagates. */
|
|
1945
|
+
function isApiFailure(reason) {
|
|
1946
|
+
return reason instanceof TaigaApiError || isNetworkFailure(reason);
|
|
1947
|
+
}
|
|
1948
|
+
function isNotFound(reason) {
|
|
1949
|
+
return reason instanceof TaigaApiError && reason.status === 404;
|
|
1950
|
+
}
|
|
1951
|
+
/** The factory's standing guidance in full; null, with a note, when none is published or it is not section-shaped. */
|
|
1952
|
+
async function factoryPart(client, factoryId) {
|
|
1953
|
+
const factory = (await client.getFactory(factoryId)).data;
|
|
1954
|
+
const base = {
|
|
1955
|
+
id: factory.id,
|
|
1956
|
+
name: factory.name,
|
|
1957
|
+
slug: factory.slug
|
|
1958
|
+
};
|
|
1959
|
+
try {
|
|
1960
|
+
const doc = (await client.getFactoryDocument(factoryId, "factory_instructions")).data;
|
|
1961
|
+
const sections = sectionsOf(doc.data);
|
|
1962
|
+
if (!sections) return {
|
|
1963
|
+
...base,
|
|
1964
|
+
instructions: null,
|
|
1965
|
+
instructionsVersion: doc.version,
|
|
1966
|
+
note: "The published instructions are not made of sections; read them with taiga_get_factory_document."
|
|
1967
|
+
};
|
|
1968
|
+
return {
|
|
1969
|
+
...base,
|
|
1970
|
+
instructions: instructionsText(sections),
|
|
1971
|
+
instructionsVersion: doc.version
|
|
1972
|
+
};
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
if (!isNotFound(error)) throw error;
|
|
1975
|
+
return {
|
|
1976
|
+
...base,
|
|
1977
|
+
instructions: null,
|
|
1978
|
+
instructionsVersion: null,
|
|
1979
|
+
note: "This factory has not published instructions yet."
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* The specification and architecture from the product's own timeline: in full
|
|
1985
|
+
* by default, or as section ids when the caller wants a small bundle. Null,
|
|
1986
|
+
* with a note, where unpublished.
|
|
1987
|
+
*/
|
|
1988
|
+
async function documentsPart(client, product, full) {
|
|
1989
|
+
const productId = String(product.id);
|
|
1990
|
+
const origin = defaultOriginFor(product);
|
|
1991
|
+
const reads = await Promise.allSettled(CONTEXT_DOCUMENT_TYPES.map((type) => client.getProductDocument(productId, type, origin)));
|
|
1992
|
+
const out = { origin };
|
|
1993
|
+
const notes = [];
|
|
1994
|
+
CONTEXT_DOCUMENT_TYPES.forEach((type, index) => {
|
|
1995
|
+
const read = reads[index];
|
|
1996
|
+
if (read.status === "fulfilled") {
|
|
1997
|
+
const doc = stripProvenance(read.value.data);
|
|
1998
|
+
if (full) out[type] = doc;
|
|
1999
|
+
else {
|
|
2000
|
+
const sections = sectionsOf(doc.data ?? {});
|
|
2001
|
+
if (sections) out[type] = {
|
|
2002
|
+
version: doc.version,
|
|
2003
|
+
sections: Object.keys(sections)
|
|
2004
|
+
};
|
|
2005
|
+
else {
|
|
2006
|
+
out[type] = {
|
|
2007
|
+
version: doc.version,
|
|
2008
|
+
sections: null
|
|
2009
|
+
};
|
|
2010
|
+
notes.push(`The ${origin} ${type} is not made of sections; read it with taiga_get_product_document.`);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
} else if (isNotFound(read.reason)) {
|
|
2014
|
+
out[type] = null;
|
|
2015
|
+
notes.push(`No published ${origin} ${type}; taiga_list_product_documents shows what the product has.`);
|
|
2016
|
+
} else throw read.reason;
|
|
2017
|
+
});
|
|
2018
|
+
return {
|
|
2019
|
+
...out,
|
|
2020
|
+
notes
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
/** Every published policy as its section ids, so the model knows which one to read with taiga_get_document. */
|
|
2024
|
+
async function policiesPart(client) {
|
|
2025
|
+
return (await client.listTenantDocuments({ limit: 100 })).data.filter((row) => POLICY_TYPES.includes(String(row.type)) && row.isDraft === false).map((row) => ({
|
|
2026
|
+
type: row.type,
|
|
2027
|
+
name: row.name,
|
|
2028
|
+
version: row.version,
|
|
2029
|
+
sections: Object.keys(sectionsOf(row.data ?? {}) ?? {})
|
|
2030
|
+
}));
|
|
2031
|
+
}
|
|
2032
|
+
/** The statuses an item is open in; the API's filter takes one at a time. */
|
|
2033
|
+
const OPEN_BACKLOG_STATUSES = ["backlog", "in_progress"];
|
|
2034
|
+
/**
|
|
2035
|
+
* The items still open: one status-filtered read per open status (the API
|
|
2036
|
+
* leaves archived items out), each paged in full up to a cap the result
|
|
2037
|
+
* reports. The list is oldest first and the open items are the newest, so a
|
|
2038
|
+
* single page would miss exactly the ones that matter. Rows carrying a
|
|
2039
|
+
* retired status (readable for a rolling deploy) match neither filter;
|
|
2040
|
+
* the settled check below keeps the shape honest for whatever comes back.
|
|
2041
|
+
*/
|
|
2042
|
+
async function backlogPart(client, productId) {
|
|
2043
|
+
const open = /* @__PURE__ */ new Map();
|
|
2044
|
+
let truncated = false;
|
|
2045
|
+
for (const status of OPEN_BACKLOG_STATUSES) {
|
|
2046
|
+
let cursor;
|
|
2047
|
+
for (let pageIndex = 0;; pageIndex += 1) {
|
|
2048
|
+
if (pageIndex === BACKLOG_PAGE_LIMIT) {
|
|
2049
|
+
truncated = true;
|
|
2050
|
+
break;
|
|
2051
|
+
}
|
|
2052
|
+
const result = await client.listBacklogItems(productId, {
|
|
2053
|
+
status,
|
|
2054
|
+
limit: 100,
|
|
2055
|
+
cursor
|
|
2056
|
+
});
|
|
2057
|
+
for (const row of result.data) if (!isSettledOrArchived({
|
|
2058
|
+
status: String(row.status),
|
|
2059
|
+
archivedAt: row.archivedAt ?? null
|
|
2060
|
+
})) open.set(String(row.id), row);
|
|
2061
|
+
if (!result.nextCursor) break;
|
|
2062
|
+
cursor = result.nextCursor;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
return {
|
|
2066
|
+
items: pickFields([...open.values()], BACKLOG_SUMMARY_FIELDS),
|
|
2067
|
+
truncated
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* The linked repositories. The route serves one page of 100 and takes no
|
|
2072
|
+
* cursor, so past that the bundle says the list is cut rather than complete.
|
|
2073
|
+
*/
|
|
2074
|
+
async function repositoriesPart(client, productId) {
|
|
2075
|
+
const result = await client.listProductRepositories(productId);
|
|
2076
|
+
return {
|
|
2077
|
+
items: pickFields(result.data, REPOSITORY_SUMMARY_FIELDS),
|
|
2078
|
+
truncated: result.nextCursor !== null
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
async function buildsPart(client, productId) {
|
|
2082
|
+
const rows = (await client.listBuildRuns(productId, { limit: BUILDS_IN_CONTEXT })).data;
|
|
2083
|
+
return pickFields(rows, BUILD_SUMMARY_FIELDS);
|
|
2084
|
+
}
|
|
2085
|
+
async function productContext(client, product, options) {
|
|
2086
|
+
const productId = String(product.id);
|
|
2087
|
+
const granted = (part) => client.hasCapability(PARTS[part]);
|
|
2088
|
+
const notGranted = Object.keys(PARTS).filter((part) => !granted(part));
|
|
2089
|
+
const fetchers = {
|
|
2090
|
+
factory: () => factoryPart(client, String(product.factoryId)),
|
|
2091
|
+
documents: () => documentsPart(client, product, options.fullDocuments),
|
|
2092
|
+
policies: () => policiesPart(client),
|
|
2093
|
+
backlog: () => backlogPart(client, productId),
|
|
2094
|
+
repositories: () => repositoriesPart(client, productId),
|
|
2095
|
+
builds: () => buildsPart(client, productId)
|
|
2096
|
+
};
|
|
2097
|
+
const parts = Object.keys(fetchers).filter(granted);
|
|
2098
|
+
const settled = await Promise.allSettled(parts.map((part) => fetchers[part]()));
|
|
2099
|
+
const bundle = { product: {
|
|
2100
|
+
...pickFields([product], PRODUCT_SUMMARY_FIELDS)[0],
|
|
2101
|
+
projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
|
|
2102
|
+
defaultOrigin: defaultOriginFor(product)
|
|
2103
|
+
} };
|
|
2104
|
+
const unavailable = {};
|
|
2105
|
+
parts.forEach((part, index) => {
|
|
2106
|
+
const result = settled[index];
|
|
2107
|
+
if (result.status === "fulfilled") bundle[part] = result.value;
|
|
2108
|
+
else if (isApiFailure(result.reason)) unavailable[part] = unavailableReason(result.reason);
|
|
2109
|
+
else throw result.reason;
|
|
2110
|
+
});
|
|
2111
|
+
return toolResult({
|
|
2112
|
+
...bundle,
|
|
2113
|
+
notGranted: notGranted.map((part) => `${part} (needs ${PARTS[part]})`),
|
|
2114
|
+
unavailable,
|
|
2115
|
+
readNext: "Read a policy in full with taiga_get_document, another product document with taiga_get_product_document, a backlog item with taiga_get_backlog_item and a run with taiga_get_build."
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
function registerContextTools(server, client) {
|
|
2119
|
+
if (!client.hasCapability("products:read")) return;
|
|
2120
|
+
server.registerTool("taiga_get_product_context", {
|
|
2121
|
+
title: "Get product context",
|
|
2122
|
+
description: "Everything a coding session starts from, in one call: the product and which document timeline is its own, its factory with the instructions in full, the specification and architecture from that timeline, every published policy as its section ids, the open backlog items, the linked repositories and the latest runs. Name the product by ID, slug or the git remote URL of the checkout. A part the API refused or failed is named under unavailable with what it said; a part the key cannot read is named under notGranted; neither is shown as empty. The two documents come in full, which on a large product is several thousand tokens; pass fullDocuments: false for their section ids only. Call this first, then read what you need in full.",
|
|
2123
|
+
inputSchema: {
|
|
2124
|
+
productId: productId.optional(),
|
|
2125
|
+
slug: slug.optional(),
|
|
2126
|
+
remoteUrl: remoteUrl.optional(),
|
|
2127
|
+
fullDocuments: z.boolean().default(true).describe("Carry the specification and architecture in full (default) or as section ids only")
|
|
2128
|
+
},
|
|
2129
|
+
annotations: READ_ONLY
|
|
2130
|
+
}, (params) => runTool(async () => {
|
|
2131
|
+
const resolved = await resolveProductRef(client, params, [
|
|
2132
|
+
"productId",
|
|
2133
|
+
"slug",
|
|
2134
|
+
"remoteUrl"
|
|
2135
|
+
]);
|
|
2136
|
+
if ("error" in resolved) return resolved.error;
|
|
2137
|
+
return productContext(client, resolved.product, { fullDocuments: params.fullDocuments });
|
|
2138
|
+
}));
|
|
2139
|
+
}
|
|
2140
|
+
//#endregion
|
|
1796
2141
|
//#region src/tools/index.ts
|
|
1797
2142
|
/**
|
|
1798
2143
|
* Register all MCP tools based on the API key's effective capabilities.
|
|
@@ -1804,6 +2149,7 @@ function registerAllTools(server, client) {
|
|
|
1804
2149
|
registerDocumentTools(server, client);
|
|
1805
2150
|
registerBacklogTools(server, client);
|
|
1806
2151
|
registerBuildTools(server, client);
|
|
2152
|
+
registerContextTools(server, client);
|
|
1807
2153
|
}
|
|
1808
2154
|
//#endregion
|
|
1809
2155
|
//#region src/tools/instructions.ts
|
|
@@ -1852,8 +2198,12 @@ const SERVER_INSTRUCTIONS = [
|
|
|
1852
2198
|
"`priority: core` marks the sections every organization keeps. Neither changes what the section",
|
|
1853
2199
|
"requires of you.",
|
|
1854
2200
|
"",
|
|
1855
|
-
"Workflow:
|
|
1856
|
-
"
|
|
2201
|
+
"Workflow: start with taiga_get_product_context, naming the product by its git remote URL inside a checkout",
|
|
2202
|
+
"(git remote get-url origin), else by slug or ID from taiga_list_products. It returns the product, the",
|
|
2203
|
+
"factory instructions, the specification and architecture, every policy as its section ids, the open backlog",
|
|
2204
|
+
"items, the repositories and the latest runs in one call, and names any part it could not read. Then read",
|
|
2205
|
+
"what you need in full: a policy with taiga_get_document, a backlog item with taiga_get_backlog_item.",
|
|
2206
|
+
"Without it, use taiga_list_factories and taiga_list_products, then read the factory instructions,",
|
|
1857
2207
|
"the product's specification and architecture, and the backlog item you are working on. A document",
|
|
1858
2208
|
"read that finds nothing tells you what does exist and which tool reads or writes it.",
|
|
1859
2209
|
"",
|
package/package.json
CHANGED