@taiga-ai/mcp-server 0.24.0-rc.462 → 0.25.0-dev.463
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 +100 -111
- package/package.json +1 -1
- package/src/client.ts +0 -8
- package/src/server.integration.test.ts +67 -32
- package/src/tools/catalogue.test.ts +2 -1
- package/src/tools/catalogue.ts +1 -1
- package/src/tools/context.ts +24 -4
- package/src/tools/documents.ts +27 -52
- package/src/tools/inbox.ts +4 -2
- package/src/tools/maintenance.ts +2 -0
- package/src/tools/product-documents.ts +1 -1
- package/src/tools/products.ts +22 -31
- package/src/tools/prompts.ts +1 -1
- package/src/tools/readiness.ts +2 -2
- package/src/tools/resources.ts +2 -2
- package/src/tools/sections.ts +1 -1
- package/taiga-ai-mcp-server-0.25.0-dev.463.tgz +0 -0
- package/taiga-ai-mcp-server-0.24.0-rc.462.tgz +0 -0
package/dist/index.js
CHANGED
|
@@ -202,13 +202,6 @@ var TaigaClient = class {
|
|
|
202
202
|
async publishTenantDocument(type) {
|
|
203
203
|
return this.request("POST", `/tenants/current/documents/${seg(type)}/publish`);
|
|
204
204
|
}
|
|
205
|
-
async listFactoryDocuments(factoryId, opts) {
|
|
206
|
-
const params = new URLSearchParams();
|
|
207
|
-
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
208
|
-
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
209
|
-
const qs = params.toString();
|
|
210
|
-
return this.request("GET", `/factories/${seg(factoryId)}/documents${qs ? `?${qs}` : ""}`);
|
|
211
|
-
}
|
|
212
205
|
async getFactoryDocument(factoryId, type) {
|
|
213
206
|
return this.request("GET", `/factories/${seg(factoryId)}/documents/${seg(type)}`);
|
|
214
207
|
}
|
|
@@ -2251,7 +2244,7 @@ function setupNextStep(product) {
|
|
|
2251
2244
|
if (isImportedProduct(product)) switch (importStatusOf(product)) {
|
|
2252
2245
|
case "analyzing":
|
|
2253
2246
|
case "generating_docs": return "The import is running and derives the documents from the code (taiga_wait_for_job follows its job). Then review them, and taiga_finish_discovery opens the product.";
|
|
2254
|
-
case "complete": return "The import is done: review the reality documents (
|
|
2247
|
+
case "complete": return "The import is done: review the reality documents (taiga_get_product_document without type), then taiga_finish_discovery opens the product.";
|
|
2255
2248
|
case "failed":
|
|
2256
2249
|
case "cancelled": return "The last import did not finish: taiga_start_import runs it again.";
|
|
2257
2250
|
default: return "Link its repository if it has none (taiga_list_linkable_repositories, taiga_link_repository), then taiga_start_import: the import derives the documents from the code and runs by itself.";
|
|
@@ -2735,7 +2728,7 @@ function sectionReadResult(doc, asked, label, options = {}) {
|
|
|
2735
2728
|
const found = Object.keys(controls).length > 0 ? { controls } : {};
|
|
2736
2729
|
const asControl = options.controlIds ? void 0 : unknownSections.find((id) => isControlId(id));
|
|
2737
2730
|
const policy = asControl ? policyOfControl(asControl) : void 0;
|
|
2738
|
-
const controlNote = asControl ? { note: policy ? `${asControl} is a control of ${policy}: read it with taiga_get_document (type: ${policy}, sections: ["${asControl}"]).` : `${asControl} looks like a policy control id, which only a policy resolves:
|
|
2731
|
+
const controlNote = asControl ? { note: policy ? `${asControl} is a control of ${policy}: read it with taiga_get_document (type: ${policy}, sections: ["${asControl}"]).` : `${asControl} looks like a policy control id, which only a policy resolves: taiga_get_document without type lists them.` } : {};
|
|
2739
2732
|
if (known.length === 0) {
|
|
2740
2733
|
const { data: _data, ...meta } = cut.doc;
|
|
2741
2734
|
return toolResult({
|
|
@@ -2826,8 +2819,8 @@ function nextStepFor(state) {
|
|
|
2826
2819
|
case "running": return "The import is running and derives the documents from the code; taiga_wait_for_job follows it.";
|
|
2827
2820
|
case "failed": return "The last import did not finish: taiga_start_import runs it again.";
|
|
2828
2821
|
case "complete":
|
|
2829
|
-
if (state.missing && state.missing.length > 0) return `The import finished but ${named(state.missing)} ${state.missing.length === 1 ? "is" : "are"} not published:
|
|
2830
|
-
return `The import is done: review the reality documents (
|
|
2822
|
+
if (state.missing && state.missing.length > 0) return `The import finished but ${named(state.missing)} ${state.missing.length === 1 ? "is" : "are"} not published: taiga_get_product_document without type shows them, taiga_publish_product_document publishes a draft.`;
|
|
2823
|
+
return `The import is done: review the reality documents (taiga_get_product_document without type), then ${finish}`;
|
|
2831
2824
|
default: return "taiga_start_import derives the documents from the code and runs by itself (paid agent work: ask first).";
|
|
2832
2825
|
}
|
|
2833
2826
|
}
|
|
@@ -3120,7 +3113,7 @@ async function resolveProductRef(client, ref, accepted, usage) {
|
|
|
3120
3113
|
return { product: (await client.getProduct(ref.productId)).data };
|
|
3121
3114
|
} catch (error) {
|
|
3122
3115
|
if (!isMissOrRefusal(error)) throw error;
|
|
3123
|
-
return refusal(apiFailureCode(error), error instanceof TaigaApiError && error.status === 404 ? "No product with that ID the key can see
|
|
3116
|
+
return refusal(apiFailureCode(error), error instanceof TaigaApiError && error.status === 404 ? "No product with that ID the key can see. taiga_list_products (query finds one by name) shows the ones it can; a slug or git remote URL names one too." : `No product with that ID the key can see (${apiFailureText(error)}); taiga_list_products shows the ones it can.`);
|
|
3124
3117
|
}
|
|
3125
3118
|
if (ref.slug) {
|
|
3126
3119
|
const [product] = (await client.listProducts({
|
|
@@ -3242,38 +3235,34 @@ function registerProductTools(server, client) {
|
|
|
3242
3235
|
readiness
|
|
3243
3236
|
} : resolved.product);
|
|
3244
3237
|
}));
|
|
3245
|
-
server.registerTool("taiga_list_product_documents", {
|
|
3246
|
-
title: "List product documents",
|
|
3247
|
-
description: `List a product's documents on both timelines (type, name, version, draft state, review and update times); defaultOrigin is the product's own. ${ORIGIN_MEANING.plan} ${ORIGIN_MEANING.reality} An empty side says why; an unlisted side is null with a note. taiga_get_product_document reads one.`,
|
|
3248
|
-
inputSchema: z.object({ productId }),
|
|
3249
|
-
annotations: READ_ONLY
|
|
3250
|
-
}, (params) => runTool(async () => {
|
|
3251
|
-
const { unlistable: _unlistable, ...timelines } = await documentTimelines(client, params.productId);
|
|
3252
|
-
return toolResult(timelines);
|
|
3253
|
-
}));
|
|
3254
3238
|
server.registerTool("taiga_get_product_document", {
|
|
3255
3239
|
title: "Get product document",
|
|
3256
|
-
description: "Read one product document from one timeline
|
|
3240
|
+
description: "Read one product document from one timeline, or without type list both timelines (type, version, draft and review state; defaultOrigin is the product's own; an empty side says why). plan (specification, architecture, threat_model, technology_decisions) says what to build; reality says what the code does, so read it before changing code. Omit origin for the product's own timeline. sections: [] returns the outline (ids, sizes); then name the sections you need (unknownSections names missing ids). A miss says which documents exist.",
|
|
3257
3241
|
inputSchema: z.object({
|
|
3258
3242
|
productId,
|
|
3259
|
-
type: z.enum(PRODUCT_DOCUMENT_TYPES).describe("Document type
|
|
3243
|
+
type: z.enum(PRODUCT_DOCUMENT_TYPES).optional().describe("Document type; omit to list what the product has on both timelines"),
|
|
3260
3244
|
origin: originInput,
|
|
3261
3245
|
sections: sectionsInput
|
|
3262
3246
|
}),
|
|
3263
3247
|
annotations: READ_ONLY
|
|
3264
3248
|
}, (params) => runTool(async () => {
|
|
3249
|
+
if (params.type === void 0) {
|
|
3250
|
+
if (params.sections || params.origin) return toolError("VALIDATION_ERROR", "Name the document (type) to read one timeline or its sections.");
|
|
3251
|
+
const { unlistable: _unlistable, ...timelines } = await documentTimelines(client, params.productId);
|
|
3252
|
+
return toolResult(timelines);
|
|
3253
|
+
}
|
|
3254
|
+
const type = params.type;
|
|
3265
3255
|
let product;
|
|
3266
3256
|
if (!params.origin) product = (await client.getProduct(params.productId)).data;
|
|
3267
|
-
const origin = params.origin ?? defaultOriginFor(product,
|
|
3257
|
+
const origin = params.origin ?? defaultOriginFor(product, type);
|
|
3268
3258
|
try {
|
|
3269
|
-
const doc = stripProvenance((await client.getProductDocument(params.productId,
|
|
3259
|
+
const doc = stripProvenance((await client.getProductDocument(params.productId, type, origin)).data);
|
|
3270
3260
|
if (!params.sections) return toolResult(doc);
|
|
3271
|
-
return sectionReadResult(doc, params.sections, `${origin} ${
|
|
3261
|
+
return sectionReadResult(doc, params.sections, `${origin} ${type}`);
|
|
3272
3262
|
} catch (error) {
|
|
3273
3263
|
if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
|
|
3274
3264
|
product ??= (await client.getProduct(params.productId)).data;
|
|
3275
|
-
|
|
3276
|
-
return toolError("NOT_FOUND", describeMiss(params.type, origin, timelines, product));
|
|
3265
|
+
return toolError("NOT_FOUND", describeMiss(type, origin, await documentTimelines(client, params.productId, product), product));
|
|
3277
3266
|
}
|
|
3278
3267
|
}));
|
|
3279
3268
|
}
|
|
@@ -3319,7 +3308,7 @@ function sectionIdFrom(readTool) {
|
|
|
3319
3308
|
return z.string().min(1).describe(`Section id -- the keys of \`data.sections\` in ${readTool}, e.g. summary, scope, apiDesign. An id that does not exist yet adds a section.`);
|
|
3320
3309
|
}
|
|
3321
3310
|
const sectionContentInput = z.string().describe("The full new Markdown text of the section; it replaces the old text");
|
|
3322
|
-
const tenantDocumentType = z.enum(TENANT_DOCUMENT_TYPES).describe(
|
|
3311
|
+
const tenantDocumentType = z.enum(TENANT_DOCUMENT_TYPES).describe("Document type: the policy_* ones are policies");
|
|
3323
3312
|
/** Everything the model needs back from a write, without the content it already holds. */
|
|
3324
3313
|
function documentSummary(doc) {
|
|
3325
3314
|
const { data, ...rest } = doc;
|
|
@@ -3378,42 +3367,42 @@ async function publishDraft(scope) {
|
|
|
3378
3367
|
});
|
|
3379
3368
|
}
|
|
3380
3369
|
function registerDocumentTools(server, client) {
|
|
3381
|
-
if (client.hasCapability("tenant:documents:read")) {
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3370
|
+
if (client.hasCapability("tenant:documents:read")) server.registerTool("taiga_get_document", {
|
|
3371
|
+
title: "Get company document",
|
|
3372
|
+
description: `Read one of the organization's documents by type, or without type list them all (type, name, version, draft state). The ${POLICY_TYPES.length} policies (${named$1(POLICY_TYPES)}) set the rules for every product; the others (${named$1(OTHER_TENANT_DOCUMENT_TYPES)}) carry goals, standards, voice and instructions. Policies are long: pass sections: [] for the outline, then the section ids you need, or a control id such as SEC-5.1; unknownSections names ids it lacks. A miss says which documents are published and whether a draft exists. A section's source (template, overlay-<id>, cloud-preset:<providers>) and priority (core: every organization keeps it) do not change what it requires.`,
|
|
3373
|
+
inputSchema: z.object({
|
|
3374
|
+
type: tenantDocumentType.optional(),
|
|
3375
|
+
sections: policySectionsInput,
|
|
3376
|
+
...pagination
|
|
3377
|
+
}),
|
|
3378
|
+
annotations: READ_ONLY
|
|
3379
|
+
}, (params) => runTool(async () => {
|
|
3380
|
+
if (params.type === void 0) {
|
|
3381
|
+
if (params.sections) return toolError("VALIDATION_ERROR", "Name the document (type) to read its sections.");
|
|
3382
|
+
const result = await client.listTenantDocuments({
|
|
3383
|
+
limit: params.limit,
|
|
3384
|
+
cursor: params.cursor
|
|
3385
|
+
});
|
|
3389
3386
|
return toolResult(page(summaries(result.data), result.nextCursor));
|
|
3390
|
-
}
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
if (all.status === "fulfilled") {
|
|
3410
|
-
const published = publishedList(all.value.data);
|
|
3411
|
-
parts.push(published ? `Published documents: ${published}.` : "Nothing is published yet.");
|
|
3412
|
-
} else parts.push(failedLookup("The published list", all.reason));
|
|
3413
|
-
return toolError("NOT_FOUND", parts.join(" "));
|
|
3414
|
-
}
|
|
3415
|
-
}));
|
|
3416
|
-
}
|
|
3387
|
+
}
|
|
3388
|
+
const type = params.type;
|
|
3389
|
+
if (params.limit !== void 0 || params.cursor !== void 0) return toolError("VALIDATION_ERROR", "limit and cursor page the list; omit type to list, or drop them to read.");
|
|
3390
|
+
try {
|
|
3391
|
+
const doc = stripProvenance((await client.getTenantDocument(type)).data);
|
|
3392
|
+
return params.sections ? sectionReadResult(doc, params.sections, type, { controlIds: POLICY_TYPES.includes(type) }) : toolResult(doc);
|
|
3393
|
+
} catch (error) {
|
|
3394
|
+
if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
|
|
3395
|
+
const [draft, all] = await Promise.allSettled([client.getTenantDocumentDraft(type), client.listTenantDocuments({ limit: 100 })]);
|
|
3396
|
+
const parts = [`No published ${type}.`];
|
|
3397
|
+
if (draft.status === "fulfilled") parts.push(draft.value.data ? "A draft exists; taiga_publish_document publishes it once the person asks." : "There is no draft either.");
|
|
3398
|
+
else parts.push(failedLookup("The draft", draft.reason));
|
|
3399
|
+
if (all.status === "fulfilled") {
|
|
3400
|
+
const published = publishedList(all.value.data);
|
|
3401
|
+
parts.push(published ? `Published documents: ${published}.` : "Nothing is published yet.");
|
|
3402
|
+
} else parts.push(failedLookup("The published list", all.reason));
|
|
3403
|
+
return toolError("NOT_FOUND", parts.join(" "));
|
|
3404
|
+
}
|
|
3405
|
+
}));
|
|
3417
3406
|
if (client.hasCapability("tenant:documents:read") && client.hasCapability("tenant:documents:write")) {
|
|
3418
3407
|
const tenantScope = (type) => ({
|
|
3419
3408
|
getDraft: () => client.getTenantDocumentDraft(type),
|
|
@@ -3438,41 +3427,27 @@ function registerDocumentTools(server, client) {
|
|
|
3438
3427
|
annotations: PUBLISH
|
|
3439
3428
|
}, (params) => runTool(() => publishDraft(tenantScope(params.type))));
|
|
3440
3429
|
}
|
|
3441
|
-
if (client.hasCapability("factories:read")) {
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
}, (params) => runTool(async () => {
|
|
3463
|
-
try {
|
|
3464
|
-
return toolResult(stripProvenance((await client.getFactoryDocument(params.factoryId, params.type)).data));
|
|
3465
|
-
} catch (error) {
|
|
3466
|
-
if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
|
|
3467
|
-
await client.getFactory(params.factoryId);
|
|
3468
|
-
const detail = await client.getFactoryDocumentDraft(params.factoryId, params.type).then((draft) => draft.data ? "A draft exists; taiga_publish_factory_document publishes it once the person asks." : "There is no draft either: write one with taiga_update_factory_document, then publish it when the person asks.").catch((reason) => {
|
|
3469
|
-
if (reason instanceof TaigaApiError && reason.status === 404) throw reason;
|
|
3470
|
-
return failedLookup("The draft", reason);
|
|
3471
|
-
});
|
|
3472
|
-
return toolError("NOT_FOUND", `This factory has no published ${params.type} yet. ${detail}`);
|
|
3473
|
-
}
|
|
3474
|
-
}));
|
|
3475
|
-
}
|
|
3430
|
+
if (client.hasCapability("factories:read")) server.registerTool("taiga_get_factory_document", {
|
|
3431
|
+
title: "Get factory instructions",
|
|
3432
|
+
description: "Read a factory's instructions: the domain context, tech preferences and conventions its products follow. If nothing is published, it says whether a draft exists and how to write one.",
|
|
3433
|
+
inputSchema: z.object({
|
|
3434
|
+
factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it"),
|
|
3435
|
+
type: z.enum(FACTORY_DOCUMENT_TYPES).default("factory_instructions").describe("Document type")
|
|
3436
|
+
}),
|
|
3437
|
+
annotations: READ_ONLY
|
|
3438
|
+
}, (params) => runTool(async () => {
|
|
3439
|
+
try {
|
|
3440
|
+
return toolResult(stripProvenance((await client.getFactoryDocument(params.factoryId, params.type)).data));
|
|
3441
|
+
} catch (error) {
|
|
3442
|
+
if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
|
|
3443
|
+
await client.getFactory(params.factoryId);
|
|
3444
|
+
const detail = await client.getFactoryDocumentDraft(params.factoryId, params.type).then((draft) => draft.data ? "A draft exists; taiga_publish_factory_document publishes it once the person asks." : "There is no draft either: write one with taiga_update_factory_document, then publish it when the person asks.").catch((reason) => {
|
|
3445
|
+
if (reason instanceof TaigaApiError && reason.status === 404) throw reason;
|
|
3446
|
+
return failedLookup("The draft", reason);
|
|
3447
|
+
});
|
|
3448
|
+
return toolError("NOT_FOUND", `This factory has no published ${params.type} yet. ${detail}`);
|
|
3449
|
+
}
|
|
3450
|
+
}));
|
|
3476
3451
|
if (client.hasCapability("factories:read") && client.hasCapability("factories:documents:write")) {
|
|
3477
3452
|
const factoryScope = (factoryId, type) => ({
|
|
3478
3453
|
getDraft: () => client.getFactoryDocumentDraft(factoryId, type),
|
|
@@ -4953,7 +4928,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4953
4928
|
ignored: z.boolean().describe("true ignores the finding, false reopens it"),
|
|
4954
4929
|
reason: z.enum(MAINTENANCE_IGNORE_REASONS).optional().describe("Why it is ignored; required when ignoring"),
|
|
4955
4930
|
note: z.string().trim().max(280).optional().describe("A short justification, kept for audit"),
|
|
4956
|
-
expiresAt: z.string().datetime({ offset: true }).optional().describe("When the ignore lapses and the finding resurfaces if it still exists (ISO 8601, in the future)")
|
|
4931
|
+
expiresAt: z.string().datetime({ offset: true }).meta({ pattern: void 0 }).optional().describe("When the ignore lapses and the finding resurfaces if it still exists (ISO 8601, in the future)")
|
|
4957
4932
|
}),
|
|
4958
4933
|
annotations: OPERATE
|
|
4959
4934
|
}, (params) => runTool(async () => {
|
|
@@ -5046,14 +5021,14 @@ function readWith(row, backlogRead) {
|
|
|
5046
5021
|
case "backlog_item": return backlogRead ? "taiga_get_plan (initiativeId: subjectId)" : void 0;
|
|
5047
5022
|
case "product_document": {
|
|
5048
5023
|
const type = /^[^:]+:([a-z_]+):decision:/.exec(String(row["subjectId"]))?.[1];
|
|
5049
|
-
return type ? `taiga_get_product_document (productId, type: ${type}); the decision's text is this row with full: true` : "
|
|
5024
|
+
return type ? `taiga_get_product_document (productId, type: ${type}); the decision's text is this row with full: true` : "taiga_get_product_document (productId, no type)";
|
|
5050
5025
|
}
|
|
5051
5026
|
case "inbox_task": return "taiga_list_setup_tasks (productId)";
|
|
5052
5027
|
case "product": return {
|
|
5053
5028
|
review_backlog: backlogRead ? "taiga_list_initiatives (productId)" : void 0,
|
|
5054
5029
|
review_maintenance: "taiga_list_findings (productId)",
|
|
5055
5030
|
review_policy: "taiga_get_policy_standing (productId)",
|
|
5056
|
-
review_doc: "
|
|
5031
|
+
review_doc: "taiga_get_product_document (productId, no type)"
|
|
5057
5032
|
}[String(row["kind"])];
|
|
5058
5033
|
default: return;
|
|
5059
5034
|
}
|
|
@@ -5366,7 +5341,7 @@ function registerInboxTools(server, client) {
|
|
|
5366
5341
|
"done",
|
|
5367
5342
|
"dismissed"
|
|
5368
5343
|
]).optional().describe("What became of it"),
|
|
5369
|
-
snoozedUntil: z.string().datetime().nullable().optional().describe("Hide it until this time (ISO 8601 in UTC, ending in Z); null wakes it now")
|
|
5344
|
+
snoozedUntil: z.string().datetime().meta({ pattern: void 0 }).nullable().optional().describe("Hide it until this time (ISO 8601 in UTC, ending in Z); null wakes it now")
|
|
5370
5345
|
}),
|
|
5371
5346
|
annotations: OPERATE
|
|
5372
5347
|
}, (params) => runTool(async () => {
|
|
@@ -5399,7 +5374,7 @@ const REPOSITORY_SUMMARY_FIELDS = [
|
|
|
5399
5374
|
"isManaged"
|
|
5400
5375
|
];
|
|
5401
5376
|
/** How many of the latest runs to carry; taiga_list_runs pages the rest. */
|
|
5402
|
-
const BUILDS_IN_CONTEXT =
|
|
5377
|
+
const BUILDS_IN_CONTEXT = 5;
|
|
5403
5378
|
/** A run as the bundle carries it: enough to say what was built and where; taiga_get_run has the rest. */
|
|
5404
5379
|
const CONTEXT_BUILD_FIELDS = [
|
|
5405
5380
|
"id",
|
|
@@ -5418,6 +5393,7 @@ const CONTEXT_BUILD_FIELDS = [
|
|
|
5418
5393
|
"prDraft",
|
|
5419
5394
|
"reviewEscalation",
|
|
5420
5395
|
"error",
|
|
5396
|
+
"hardeningResult",
|
|
5421
5397
|
"createdAt"
|
|
5422
5398
|
];
|
|
5423
5399
|
/**
|
|
@@ -5586,7 +5562,7 @@ async function documentsPart(client, product, depth) {
|
|
|
5586
5562
|
}
|
|
5587
5563
|
} else if (isNotFound$1(read.reason)) {
|
|
5588
5564
|
out[type] = null;
|
|
5589
|
-
notes.push(`No published ${origin} ${type};
|
|
5565
|
+
notes.push(`No published ${origin} ${type}; taiga_get_product_document without type shows what the product has.`);
|
|
5590
5566
|
} else throw read.reason;
|
|
5591
5567
|
});
|
|
5592
5568
|
return {
|
|
@@ -5669,8 +5645,21 @@ async function repositoriesPart(links) {
|
|
|
5669
5645
|
async function buildsPart(client, productId) {
|
|
5670
5646
|
const rows = (await client.listBuildRuns(productId, { limit: BUILDS_IN_CONTEXT })).data;
|
|
5671
5647
|
const states = await initiativeStates(client, rows);
|
|
5672
|
-
return pickFields(rows, CONTEXT_BUILD_FIELDS).map(readableRow).map((row) => withInitiativeState(row, states));
|
|
5648
|
+
return pickFields(rows, CONTEXT_BUILD_FIELDS).map(readableRow).map((row) => withInitiativeState(row, states)).map((row) => pickFields([row], CONTEXT_RUN_SAID)[0]);
|
|
5673
5649
|
}
|
|
5650
|
+
/** A run in the bundle: which one, for what, how it ended, and its pull request. taiga_get_run has the rest. */
|
|
5651
|
+
const CONTEXT_RUN_SAID = [
|
|
5652
|
+
"id",
|
|
5653
|
+
"runNumber",
|
|
5654
|
+
"backlogItemId",
|
|
5655
|
+
"targetTitle",
|
|
5656
|
+
"disposition",
|
|
5657
|
+
"outcome",
|
|
5658
|
+
"failedReasonMeans",
|
|
5659
|
+
"hardeningNote",
|
|
5660
|
+
"prUrl",
|
|
5661
|
+
"createdAt"
|
|
5662
|
+
];
|
|
5674
5663
|
/**
|
|
5675
5664
|
* What waits on a person for this product, counted as taiga_list_action_required's summary counts it (PRO-2800):
|
|
5676
5665
|
* the reviewing agent called four tools to answer "how does it stand", and two later reviews found this count and
|
|
@@ -5767,7 +5756,7 @@ function registerContextTools(server, client) {
|
|
|
5767
5756
|
if (!client.hasCapability("products:read")) return;
|
|
5768
5757
|
server.registerTool("taiga_get_product_context", {
|
|
5769
5758
|
title: "Get product context",
|
|
5770
|
-
description: `Read where a product stands in one call; the first read for any product question. Name it by ID, slug or git remote URL. Returns product (in setup, readiness and next move), factory with its instructions, specification and architecture outlines (section ids, sizes), policies with section counts, open initiatives with a count per board column (byColumn; taiga_list_initiatives status: 'all' has the done and canceled), repositories, the latest
|
|
5759
|
+
description: `Read where a product stands in one call; the first read for any product question. Name it by ID, slug or git remote URL. Returns product (in setup, readiness and next move), factory with its instructions, specification and architecture outlines (section ids, sizes), policies with section counts, open initiatives with a count per board column (byColumn; taiga_list_initiatives status: 'all' has the done and canceled), repositories, the latest five runs (taiga_list_runs has more), and counts for Action Required, open findings and policy violations. A part that failed is under unavailable, one the key cannot read under notGranted; neither shows as empty. documents: budget returns whole sections up to about ${DOCUMENT_BUDGET / 1e3}k characters each, full returns all.`,
|
|
5771
5760
|
inputSchema: z.object({
|
|
5772
5761
|
productId: productId.optional(),
|
|
5773
5762
|
slug: slug.optional(),
|
|
@@ -6252,7 +6241,7 @@ function registerObserveTools(server, client) {
|
|
|
6252
6241
|
//#endregion
|
|
6253
6242
|
//#region src/tools/product-documents.ts
|
|
6254
6243
|
const EDITABLE_TYPES = PRODUCT_DOCUMENT_TYPES.filter((type) => type !== "codebase_snapshot");
|
|
6255
|
-
const documentType = z.enum(EDITABLE_TYPES).describe("Document type --
|
|
6244
|
+
const documentType = z.enum(EDITABLE_TYPES).describe("Document type -- taiga_get_product_document without type shows which ones the product has");
|
|
6256
6245
|
/** The documents a person reviews: the discovery chain, not the snapshot or the product's own instructions. */
|
|
6257
6246
|
const reviewableType = z.enum(REVIEWABLE_PRODUCT_DOCUMENT_TYPES).describe("Document type -- one of the discovery documents a person reviews");
|
|
6258
6247
|
/** The documents an agent writes from other documents; the same table the worker reads its inputs from. */
|
|
@@ -20979,7 +20968,7 @@ function registerPrompts(server, client) {
|
|
|
20979
20968
|
client.hasCapability("factories:read") ? client.getFactoryDocument(String(product.factoryId), "factory_instructions") : Promise.reject(new NotGranted("factories:read")),
|
|
20980
20969
|
client.hasCapability("tenant:documents:read") ? client.listTenantDocuments({ limit: 100 }) : Promise.reject(new NotGranted("tenant:documents:read"))
|
|
20981
20970
|
]);
|
|
20982
|
-
const architectureText = architecture.status === "fulfilled" ? fence("markdown", renderDocument(stripProvenance(architecture.value.data)).text) : isNotFound(architecture.reason) ? `(No published ${origin} architecture;
|
|
20971
|
+
const architectureText = architecture.status === "fulfilled" ? fence("markdown", renderDocument(stripProvenance(architecture.value.data)).text) : isNotFound(architecture.reason) ? `(No published ${origin} architecture; taiga_get_product_document without type shows what exists.)` : unreadable("The architecture", architecture.reason);
|
|
20983
20972
|
const instructionsText_ = instructions.status === "fulfilled" ? fence("markdown", instructionsText(sectionsOf(instructions.value.data.data) ?? {}) ?? "(The factory instructions are empty.)") : instructions.reason instanceof NotGranted ? instructions.reason.line("The factory instructions") : isNotFound(instructions.reason) ? "(This factory has not published instructions.)" : unreadable("The factory instructions", instructions.reason);
|
|
20984
20973
|
const policiesText = policies.status === "fulfilled" ? fence("text", policies.value.data.filter((row) => POLICY_TYPES.includes(String(row.type)) && row.isDraft === false).map((row) => `- ${String(row.type)}: ${Object.keys(sectionsOf(row.data ?? {}) ?? {}).join(", ")}`).join("\n") || "(No policy is published.)") : policies.reason instanceof NotGranted ? policies.reason.line("The policies") : unreadable("The policies", policies.reason);
|
|
20985
20974
|
return message([
|
|
@@ -21339,7 +21328,7 @@ function registerResources(server, client) {
|
|
|
21339
21328
|
return requireOneOf("Document type", value, PRODUCT_DOCUMENT_TYPES);
|
|
21340
21329
|
};
|
|
21341
21330
|
const read = async (uri, productId, type, origin) => {
|
|
21342
|
-
const result = await served(client.getProductDocument(productId, type, origin), `No published ${origin} ${type} for this product, or the key cannot see it;
|
|
21331
|
+
const result = await served(client.getProductDocument(productId, type, origin), `No published ${origin} ${type} for this product, or the key cannot see it; taiga_get_product_document without type shows what exists`);
|
|
21343
21332
|
return contents(uri.href, {
|
|
21344
21333
|
name: productDocumentName(type),
|
|
21345
21334
|
...result.data
|
|
@@ -21356,7 +21345,7 @@ function registerResources(server, client) {
|
|
|
21356
21345
|
});
|
|
21357
21346
|
server.registerResource("product-document", new ResourceTemplate("taiga://products/{productId}/documents/{type}", { list: void 0 }), {
|
|
21358
21347
|
title: "Product document",
|
|
21359
|
-
description: "A product document from the product's own timeline (reality for an imported product, plan otherwise; the documents a person writes are always on the plan side).
|
|
21348
|
+
description: "A product document from the product's own timeline (reality for an imported product, plan otherwise; the documents a person writes are always on the plan side). taiga_get_product_document without type shows what exists; add ?origin=plan or ?origin=reality to read the other side. Markdown for a section map, JSON otherwise (the read says which)."
|
|
21360
21349
|
}, async (uri, variables) => {
|
|
21361
21350
|
const productId = requireId("Product ID", String(variables["productId"]));
|
|
21362
21351
|
const type = documentType(String(variables["type"]));
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -345,14 +345,6 @@ export class TaigaClient {
|
|
|
345
345
|
// Factory Documents
|
|
346
346
|
// ---------------------------------------------------------------------------
|
|
347
347
|
|
|
348
|
-
async listFactoryDocuments(factoryId: string, opts?: { limit?: number | undefined; cursor?: string | undefined }) {
|
|
349
|
-
const params = new URLSearchParams();
|
|
350
|
-
if (opts?.limit) params.set('limit', String(opts.limit));
|
|
351
|
-
if (opts?.cursor) params.set('cursor', opts.cursor);
|
|
352
|
-
const qs = params.toString();
|
|
353
|
-
return this.request<Page<FactoryDocument>>('GET', `/factories/${seg(factoryId)}/documents${qs ? `?${qs}` : ''}`);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
348
|
async getFactoryDocument(factoryId: string, type: string) {
|
|
357
349
|
return this.request<One<FactoryDocument>>('GET', `/factories/${seg(factoryId)}/documents/${seg(type)}`);
|
|
358
350
|
}
|
|
@@ -66,15 +66,12 @@ const ALL_TOOL_NAMES = [
|
|
|
66
66
|
'taiga_ignore_finding',
|
|
67
67
|
'taiga_link_repository',
|
|
68
68
|
'taiga_list_action_required',
|
|
69
|
-
'taiga_list_documents',
|
|
70
69
|
'taiga_list_environments',
|
|
71
70
|
'taiga_list_factories',
|
|
72
|
-
'taiga_list_factory_documents',
|
|
73
71
|
'taiga_list_findings',
|
|
74
72
|
'taiga_list_initiatives',
|
|
75
73
|
'taiga_list_knowledge',
|
|
76
74
|
'taiga_list_linkable_repositories',
|
|
77
|
-
'taiga_list_product_documents',
|
|
78
75
|
'taiga_list_product_repositories',
|
|
79
76
|
'taiga_list_products',
|
|
80
77
|
'taiga_list_repository_branches',
|
|
@@ -326,9 +323,7 @@ describe('MCP server integration', () => {
|
|
|
326
323
|
const names = toolNames(tools);
|
|
327
324
|
|
|
328
325
|
expect(names).toContain('taiga_list_factories');
|
|
329
|
-
expect(names).toContain('taiga_list_factory_documents');
|
|
330
326
|
expect(names).toContain('taiga_get_factory_document');
|
|
331
|
-
expect(names).not.toContain('taiga_list_documents');
|
|
332
327
|
expect(names).not.toContain('taiga_update_document');
|
|
333
328
|
});
|
|
334
329
|
|
|
@@ -338,7 +333,6 @@ describe('MCP server integration', () => {
|
|
|
338
333
|
const { tools } = await client.listTools();
|
|
339
334
|
const names = toolNames(tools);
|
|
340
335
|
|
|
341
|
-
expect(names).toContain('taiga_list_documents');
|
|
342
336
|
expect(names).toContain('taiga_get_document');
|
|
343
337
|
expect(names).not.toContain('taiga_update_document');
|
|
344
338
|
});
|
|
@@ -399,9 +393,7 @@ describe('MCP server integration', () => {
|
|
|
399
393
|
|
|
400
394
|
// teams:read tools present
|
|
401
395
|
expect(names).toContain('taiga_list_factories');
|
|
402
|
-
expect(names).toContain('taiga_list_factory_documents');
|
|
403
396
|
// tenant:documents:read tools present
|
|
404
|
-
expect(names).toContain('taiga_list_documents');
|
|
405
397
|
// write tools absent
|
|
406
398
|
expect(names).not.toContain('taiga_update_document');
|
|
407
399
|
});
|
|
@@ -438,7 +430,6 @@ describe('MCP server integration', () => {
|
|
|
438
430
|
'taiga_list_products',
|
|
439
431
|
'taiga_get_product',
|
|
440
432
|
'taiga_get_product_context',
|
|
441
|
-
'taiga_list_product_documents',
|
|
442
433
|
'taiga_get_product_document',
|
|
443
434
|
'taiga_list_runs',
|
|
444
435
|
'taiga_get_run',
|
|
@@ -467,8 +458,8 @@ describe('MCP server integration', () => {
|
|
|
467
458
|
],
|
|
468
459
|
'vcs:manage': ['taiga_link_repository', 'taiga_set_repository_branch', 'taiga_unlink_repository'],
|
|
469
460
|
'backlog:read': ['taiga_list_initiatives', 'taiga_get_initiative', 'taiga_get_plan', 'taiga_wait_for_initiative'],
|
|
470
|
-
'factories:read': ['taiga_list_factories', '
|
|
471
|
-
'tenant:documents:read': ['
|
|
461
|
+
'factories:read': ['taiga_list_factories', 'taiga_get_factory_document'],
|
|
462
|
+
'tenant:documents:read': ['taiga_get_document'],
|
|
472
463
|
'customDocuments:read': ['taiga_list_knowledge', 'taiga_get_knowledge'],
|
|
473
464
|
};
|
|
474
465
|
const PAIRED_CONTRACT: Record<string, string[]> = {
|
|
@@ -562,7 +553,7 @@ describe('MCP server integration', () => {
|
|
|
562
553
|
routes: { 'GET /v1/tenants/current/documents': { data: [doc], nextCursor: null } },
|
|
563
554
|
});
|
|
564
555
|
|
|
565
|
-
const result = await client.callTool({ name: '
|
|
556
|
+
const result = await client.callTool({ name: 'taiga_get_document', arguments: {} });
|
|
566
557
|
|
|
567
558
|
expect(parseToolResult(result).data).toEqual([
|
|
568
559
|
{
|
|
@@ -1292,7 +1283,7 @@ describe('MCP server integration', () => {
|
|
|
1292
1283
|
});
|
|
1293
1284
|
|
|
1294
1285
|
const result = await client.callTool({
|
|
1295
|
-
name: '
|
|
1286
|
+
name: 'taiga_get_product_document',
|
|
1296
1287
|
arguments: { productId: PRODUCT_ID },
|
|
1297
1288
|
});
|
|
1298
1289
|
|
|
@@ -1426,7 +1417,7 @@ describe('MCP server integration', () => {
|
|
|
1426
1417
|
arguments: { productId: PRODUCT_ID, type: 'architecture' },
|
|
1427
1418
|
});
|
|
1428
1419
|
const list = await client.callTool({
|
|
1429
|
-
name: '
|
|
1420
|
+
name: 'taiga_get_product_document',
|
|
1430
1421
|
arguments: { productId: PRODUCT_ID },
|
|
1431
1422
|
});
|
|
1432
1423
|
|
|
@@ -1565,7 +1556,7 @@ describe('MCP server integration', () => {
|
|
|
1565
1556
|
});
|
|
1566
1557
|
|
|
1567
1558
|
const result = await client.callTool({
|
|
1568
|
-
name: '
|
|
1559
|
+
name: 'taiga_get_product_document',
|
|
1569
1560
|
arguments: { productId: PRODUCT_ID },
|
|
1570
1561
|
});
|
|
1571
1562
|
|
|
@@ -1604,7 +1595,7 @@ describe('MCP server integration', () => {
|
|
|
1604
1595
|
});
|
|
1605
1596
|
|
|
1606
1597
|
const parsed = parseToolResult(
|
|
1607
|
-
await client.callTool({ name: '
|
|
1598
|
+
await client.callTool({ name: 'taiga_get_product_document', arguments: { productId: PRODUCT_ID } })
|
|
1608
1599
|
);
|
|
1609
1600
|
|
|
1610
1601
|
expect(parsed.plan[0].outdated).toBe(false);
|
|
@@ -1623,7 +1614,7 @@ describe('MCP server integration', () => {
|
|
|
1623
1614
|
});
|
|
1624
1615
|
|
|
1625
1616
|
const parsed = parseToolResult(
|
|
1626
|
-
await client.callTool({ name: '
|
|
1617
|
+
await client.callTool({ name: 'taiga_get_product_document', arguments: { productId: PRODUCT_ID } })
|
|
1627
1618
|
);
|
|
1628
1619
|
|
|
1629
1620
|
expect(parsed.plan[0]).not.toHaveProperty('outdated');
|
|
@@ -1642,7 +1633,7 @@ describe('MCP server integration', () => {
|
|
|
1642
1633
|
});
|
|
1643
1634
|
|
|
1644
1635
|
const list = parseToolResult(
|
|
1645
|
-
await client.callTool({ name: '
|
|
1636
|
+
await client.callTool({ name: 'taiga_get_product_document', arguments: { productId: PRODUCT_ID } })
|
|
1646
1637
|
);
|
|
1647
1638
|
const miss = parseToolResult(
|
|
1648
1639
|
await client.callTool({
|
|
@@ -1941,7 +1932,7 @@ describe('MCP server integration', () => {
|
|
|
1941
1932
|
nextCursor: null,
|
|
1942
1933
|
},
|
|
1943
1934
|
[`GET /v1/products/${PRODUCT_ID}/repositories`]: { data: [repo], nextCursor: null },
|
|
1944
|
-
[`GET /v1/products/${PRODUCT_ID}/builds?limit=
|
|
1935
|
+
[`GET /v1/products/${PRODUCT_ID}/builds?limit=5`]: { data: [run], nextCursor: null },
|
|
1945
1936
|
[`GET /v1/products/${PRODUCT_ID}/inbox`]: {
|
|
1946
1937
|
data: [
|
|
1947
1938
|
{ id: 'i1', kind: 'review_pr', status: 'open' },
|
|
@@ -2003,7 +1994,7 @@ describe('MCP server integration', () => {
|
|
|
2003
1994
|
});
|
|
2004
1995
|
expect(bundle.documents.architecture).toBeNull();
|
|
2005
1996
|
expect(bundle.documents.notes).toEqual([
|
|
2006
|
-
'No published plan architecture;
|
|
1997
|
+
'No published plan architecture; taiga_get_product_document without type shows what the product has.',
|
|
2007
1998
|
]);
|
|
2008
1999
|
expect(bundle.policies).toEqual([{ type: 'policy_security', name: 'Security Policy', version: 3, sections: 2 }]);
|
|
2009
2000
|
expect(bundle.initiatives.items.map((row: { id: string }) => row.id)).toEqual([ITEM_ID]);
|
|
@@ -2026,9 +2017,9 @@ describe('MCP server integration', () => {
|
|
|
2026
2017
|
});
|
|
2027
2018
|
expect(bundle.runs).toEqual([
|
|
2028
2019
|
{
|
|
2020
|
+
// The disposition says how it ended; status, the raw word, is taiga_get_run's (PRO-2800).
|
|
2029
2021
|
id: BUILD_ID,
|
|
2030
2022
|
runNumber: 1,
|
|
2031
|
-
status: 'completed',
|
|
2032
2023
|
targetTitle: 'Add login',
|
|
2033
2024
|
disposition: 'unmerged',
|
|
2034
2025
|
outcome: 'Settled without a merged pull request.',
|
|
@@ -2364,13 +2355,7 @@ describe('MCP server integration', () => {
|
|
|
2364
2355
|
it.each([
|
|
2365
2356
|
['taiga_list_factories', {}, '/v1/factories', ['factories:read']],
|
|
2366
2357
|
['taiga_list_products', {}, '/v1/products', ['products:read']],
|
|
2367
|
-
['
|
|
2368
|
-
[
|
|
2369
|
-
'taiga_list_factory_documents',
|
|
2370
|
-
{ factoryId: FACTORY_ID },
|
|
2371
|
-
`/v1/factories/${FACTORY_ID}/documents`,
|
|
2372
|
-
['factories:read'],
|
|
2373
|
-
],
|
|
2358
|
+
['taiga_get_document', {}, '/v1/tenants/current/documents', ['tenant:documents:read']],
|
|
2374
2359
|
[
|
|
2375
2360
|
'taiga_list_initiatives',
|
|
2376
2361
|
{ productId: PRODUCT_ID, status: 'all' },
|
|
@@ -2567,7 +2552,7 @@ describe('MCP server integration', () => {
|
|
|
2567
2552
|
expect(String((own as Error).message)).toContain('403 FORBIDDEN: Not a member of this product');
|
|
2568
2553
|
expect(named).toMatchObject({ code: -32602 });
|
|
2569
2554
|
expect(String((named as Error).message)).toContain(
|
|
2570
|
-
'
|
|
2555
|
+
'taiga_get_product_document without type shows what exists (403 FORBIDDEN'
|
|
2571
2556
|
);
|
|
2572
2557
|
});
|
|
2573
2558
|
|
|
@@ -2640,7 +2625,7 @@ describe('MCP server integration', () => {
|
|
|
2640
2625
|
[`GET /v1/products/${PRODUCT_ID}/backlog?status=backlog&limit=100`]: { data: [], nextCursor: null },
|
|
2641
2626
|
[`GET /v1/products/${PRODUCT_ID}/backlog?status=in_progress&limit=100`]: { data: [item], nextCursor: null },
|
|
2642
2627
|
[`GET /v1/products/${PRODUCT_ID}/repositories`]: { data: [], nextCursor: null },
|
|
2643
|
-
[`GET /v1/products/${PRODUCT_ID}/builds?limit=
|
|
2628
|
+
[`GET /v1/products/${PRODUCT_ID}/builds?limit=5`]: { data: [], nextCursor: null },
|
|
2644
2629
|
'GET /v1/products?slug=web&limit=1': { data: [product], nextCursor: null },
|
|
2645
2630
|
};
|
|
2646
2631
|
|
|
@@ -2731,7 +2716,9 @@ describe('MCP server integration', () => {
|
|
|
2731
2716
|
.catch((e: unknown) => e);
|
|
2732
2717
|
|
|
2733
2718
|
expect(failure).toMatchObject({ code: -32602 });
|
|
2734
|
-
expect(String((failure as Error).message)).toContain(
|
|
2719
|
+
expect(String((failure as Error).message)).toContain(
|
|
2720
|
+
'taiga_list_products (query finds one by name) shows the ones it can; a slug or git remote URL names one too.'
|
|
2721
|
+
);
|
|
2735
2722
|
});
|
|
2736
2723
|
|
|
2737
2724
|
it('should refuse a session with no product named', async () => {
|
|
@@ -8427,7 +8414,7 @@ describe('what the sixth agent review found (PRO-2800)', () => {
|
|
|
8427
8414
|
expect(plan.summary.length).toBeLessThan(240);
|
|
8428
8415
|
expect(doc).not.toHaveProperty('status');
|
|
8429
8416
|
expect(doc).not.toHaveProperty('blocking');
|
|
8430
|
-
expect(doc.readWith).toBe('
|
|
8417
|
+
expect(doc.readWith).toBe('taiga_get_product_document (productId, no type)');
|
|
8431
8418
|
expect(plan.blocking).toBe(true);
|
|
8432
8419
|
expect(plan.readWith).toBe('taiga_get_plan (initiativeId: subjectId)');
|
|
8433
8420
|
expect(task.status).toBe('in_progress');
|
|
@@ -8606,6 +8593,9 @@ describe('what the seventh agent review found (PRO-2800)', () => {
|
|
|
8606
8593
|
const refused = await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: 'my item' } });
|
|
8607
8594
|
|
|
8608
8595
|
expect(listing).not.toMatch(/"pattern":"\^\(\[0-9a-fA-F\]\{8\}/);
|
|
8596
|
+
// Nor the date-time one: a pattern left is a short one that says something (a slug, a file extension).
|
|
8597
|
+
const patterns = [...listing.matchAll(/"pattern":"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1]!);
|
|
8598
|
+
expect(patterns.filter((pattern) => pattern.length > 60)).toEqual([]);
|
|
8609
8599
|
expect(listing).toMatch(/"format":"uuid"/);
|
|
8610
8600
|
expect(JSON.stringify(refused.content)).toMatch(/An initiative ID is a UUID/);
|
|
8611
8601
|
});
|
|
@@ -8813,4 +8803,49 @@ describe('what the seventh agent review found (PRO-2800)', () => {
|
|
|
8813
8803
|
expect(critical).not.toHaveProperty('totalNote');
|
|
8814
8804
|
expect(secrets).not.toHaveProperty('totalNote');
|
|
8815
8805
|
});
|
|
8806
|
+
|
|
8807
|
+
it('reads a document by type and lists them without one, refusing sections with nothing to read', async () => {
|
|
8808
|
+
const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
|
|
8809
|
+
|
|
8810
|
+
const company = parseToolResult(await client.callTool({ name: 'taiga_get_document', arguments: { sections: [] } }));
|
|
8811
|
+
const product = parseToolResult(
|
|
8812
|
+
await client.callTool({
|
|
8813
|
+
name: 'taiga_get_product_document',
|
|
8814
|
+
arguments: { productId: PRODUCT_ID, origin: 'plan' },
|
|
8815
|
+
})
|
|
8816
|
+
);
|
|
8817
|
+
|
|
8818
|
+
expect(company).toMatchObject({ error: 'VALIDATION_ERROR' });
|
|
8819
|
+
expect(product).toMatchObject({ error: 'VALIDATION_ERROR' });
|
|
8820
|
+
});
|
|
8821
|
+
|
|
8822
|
+
it("keeps a run's hardening note in the compact context row", async () => {
|
|
8823
|
+
const { client } = await bootMcpPair({
|
|
8824
|
+
capabilities: ['products:read'],
|
|
8825
|
+
routes: {
|
|
8826
|
+
[`GET /v1/products/${PRODUCT_ID}`]: {
|
|
8827
|
+
data: { id: PRODUCT_ID, name: 'Web', status: 'active', factoryId: FACTORY_ID, settings: {} },
|
|
8828
|
+
},
|
|
8829
|
+
[`/v1/products/${PRODUCT_ID}/builds`]: {
|
|
8830
|
+
data: [
|
|
8831
|
+
{
|
|
8832
|
+
id: BUILD_ID,
|
|
8833
|
+
status: 'completed',
|
|
8834
|
+
prMerged: true,
|
|
8835
|
+
prNumber: 3,
|
|
8836
|
+
hardeningResult: { passed: false, blockingFindings: 2 },
|
|
8837
|
+
},
|
|
8838
|
+
],
|
|
8839
|
+
nextCursor: null,
|
|
8840
|
+
},
|
|
8841
|
+
},
|
|
8842
|
+
});
|
|
8843
|
+
|
|
8844
|
+
const bundle = parseToolResult(
|
|
8845
|
+
await client.callTool({ name: 'taiga_get_product_context', arguments: { productId: PRODUCT_ID } })
|
|
8846
|
+
);
|
|
8847
|
+
|
|
8848
|
+
expect(bundle.runs[0].hardeningNote).toMatch(/^Hardening ended with 2 blocking findings/);
|
|
8849
|
+
expect(bundle.runs[0]).not.toHaveProperty('hardeningResult');
|
|
8850
|
+
});
|
|
8816
8851
|
});
|
|
@@ -102,8 +102,9 @@ describe('tool descriptions against the catalogue', () => {
|
|
|
102
102
|
|
|
103
103
|
it('names every policy where the company documents are explained', async () => {
|
|
104
104
|
const tools = await allTools();
|
|
105
|
-
|
|
105
|
+
// One tool lists and reads the company documents.
|
|
106
106
|
const get = tools.find((t) => t.name === 'taiga_get_document')!;
|
|
107
|
+
const list = get;
|
|
107
108
|
const typeEnum = (get.inputSchema.properties as Record<string, { enum?: string[] }>)['type']!.enum;
|
|
108
109
|
|
|
109
110
|
for (const type of POLICY_TYPES) {
|
package/src/tools/catalogue.ts
CHANGED
|
@@ -182,7 +182,7 @@ export function setupNextStep(
|
|
|
182
182
|
);
|
|
183
183
|
case 'complete':
|
|
184
184
|
return (
|
|
185
|
-
'The import is done: review the reality documents (
|
|
185
|
+
'The import is done: review the reality documents (taiga_get_product_document without type), then ' +
|
|
186
186
|
'taiga_finish_discovery opens the product.'
|
|
187
187
|
);
|
|
188
188
|
case 'failed':
|
package/src/tools/context.ts
CHANGED
|
@@ -56,7 +56,7 @@ const REPOSITORY_SUMMARY_FIELDS = [
|
|
|
56
56
|
] as const;
|
|
57
57
|
|
|
58
58
|
/** How many of the latest runs to carry; taiga_list_runs pages the rest. */
|
|
59
|
-
const BUILDS_IN_CONTEXT =
|
|
59
|
+
const BUILDS_IN_CONTEXT = 5;
|
|
60
60
|
|
|
61
61
|
/** A run as the bundle carries it: enough to say what was built and where; taiga_get_run has the rest. */
|
|
62
62
|
const CONTEXT_BUILD_FIELDS = [
|
|
@@ -76,6 +76,8 @@ const CONTEXT_BUILD_FIELDS = [
|
|
|
76
76
|
'prDraft',
|
|
77
77
|
'reviewEscalation',
|
|
78
78
|
'error',
|
|
79
|
+
// Read for hardeningNote; the compact row drops it.
|
|
80
|
+
'hardeningResult',
|
|
79
81
|
'createdAt',
|
|
80
82
|
] as const;
|
|
81
83
|
|
|
@@ -240,7 +242,7 @@ async function documentsPart(client: TaigaClient, product: WithoutTenant<Product
|
|
|
240
242
|
}
|
|
241
243
|
} else if (isNotFound(read.reason)) {
|
|
242
244
|
out[type] = null;
|
|
243
|
-
notes.push(`No published ${origin} ${type};
|
|
245
|
+
notes.push(`No published ${origin} ${type}; taiga_get_product_document without type shows what the product has.`);
|
|
244
246
|
} else throw read.reason;
|
|
245
247
|
});
|
|
246
248
|
return { ...out, notes };
|
|
@@ -337,11 +339,29 @@ async function repositoriesPart(links: () => ReturnType<TaigaClient['listProduct
|
|
|
337
339
|
async function buildsPart(client: TaigaClient, productId: string) {
|
|
338
340
|
const rows = (await client.listBuildRuns(productId, { limit: BUILDS_IN_CONTEXT })).data as Record<string, unknown>[];
|
|
339
341
|
const states = await initiativeStates(client, rows);
|
|
342
|
+
// The fields the outcome is made from are read, then the row is cut to what the bundle says: each run with its
|
|
343
|
+
// two-sentence outcome and every PR field was half the context, repeated by taiga_list_runs (PRO-2800).
|
|
340
344
|
return pickFields(rows, CONTEXT_BUILD_FIELDS)
|
|
341
345
|
.map(readableRow)
|
|
342
|
-
.map((row) => withInitiativeState(row, states))
|
|
346
|
+
.map((row) => withInitiativeState(row, states))
|
|
347
|
+
.map((row) => pickFields([row], CONTEXT_RUN_SAID)[0]!);
|
|
343
348
|
}
|
|
344
349
|
|
|
350
|
+
/** A run in the bundle: which one, for what, how it ended, and its pull request. taiga_get_run has the rest. */
|
|
351
|
+
const CONTEXT_RUN_SAID = [
|
|
352
|
+
'id',
|
|
353
|
+
'runNumber',
|
|
354
|
+
'backlogItemId',
|
|
355
|
+
'targetTitle',
|
|
356
|
+
'disposition',
|
|
357
|
+
'outcome',
|
|
358
|
+
// Rare and short, and what the outcome sentence does not say.
|
|
359
|
+
'failedReasonMeans',
|
|
360
|
+
'hardeningNote',
|
|
361
|
+
'prUrl',
|
|
362
|
+
'createdAt',
|
|
363
|
+
] as const;
|
|
364
|
+
|
|
345
365
|
/**
|
|
346
366
|
* What waits on a person for this product, counted as taiga_list_action_required's summary counts it (PRO-2800):
|
|
347
367
|
* the reviewing agent called four tools to answer "how does it stand", and two later reviews found this count and
|
|
@@ -466,7 +486,7 @@ export function registerContextTools(server: McpServer, client: TaigaClient) {
|
|
|
466
486
|
'or git remote URL. Returns product (in setup, readiness and next move), factory with its instructions, ' +
|
|
467
487
|
'specification and architecture outlines (section ids, sizes), policies with section counts, open ' +
|
|
468
488
|
"initiatives with a count per board column (byColumn; taiga_list_initiatives status: 'all' has the done " +
|
|
469
|
-
'and canceled), repositories, the latest
|
|
489
|
+
'and canceled), repositories, the latest five runs (taiga_list_runs has more), and counts for Action Required, open findings and ' +
|
|
470
490
|
'policy violations. A part that failed is under unavailable, one the key cannot read under notGranted; ' +
|
|
471
491
|
`neither shows as empty. documents: budget returns whole sections up to about ${DOCUMENT_BUDGET / 1000}k ` +
|
|
472
492
|
'characters each, full returns all.',
|
package/src/tools/documents.ts
CHANGED
|
@@ -75,7 +75,8 @@ export const sectionContentInput = z
|
|
|
75
75
|
.describe('The full new Markdown text of the section; it replaces the old text');
|
|
76
76
|
const tenantDocumentType = z
|
|
77
77
|
.enum(TENANT_DOCUMENT_TYPES)
|
|
78
|
-
|
|
78
|
+
// The enum lists every type; the description only says which are policies.
|
|
79
|
+
.describe('Document type: the policy_* ones are policies');
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
82
|
* The read, save and publish calls for one document scope. The tools are the
|
|
@@ -150,52 +151,48 @@ export function registerDocumentTools(server: McpServer, client: TaigaClient) {
|
|
|
150
151
|
// ── Company-level policy documents ──────────────────────────────────
|
|
151
152
|
|
|
152
153
|
if (client.hasCapability('tenant:documents:read')) {
|
|
153
|
-
server.registerTool(
|
|
154
|
-
'taiga_list_documents',
|
|
155
|
-
{
|
|
156
|
-
title: 'List company documents',
|
|
157
|
-
description:
|
|
158
|
-
"List the organization's documents as summaries (type, name, description, version, draft state). The " +
|
|
159
|
-
`${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) set the security, data privacy, SDLC, ` +
|
|
160
|
-
'operations, architecture and compliance rules for every product; the others ' +
|
|
161
|
-
`(${named(OTHER_TENANT_DOCUMENT_TYPES)}) carry goals, standards, voice and standing instructions. ` +
|
|
162
|
-
'taiga_get_document reads one.',
|
|
163
|
-
inputSchema: z.object({
|
|
164
|
-
...pagination,
|
|
165
|
-
}),
|
|
166
|
-
annotations: READ_ONLY,
|
|
167
|
-
},
|
|
168
|
-
(params) =>
|
|
169
|
-
runTool(async () => {
|
|
170
|
-
const result = await client.listTenantDocuments(params);
|
|
171
|
-
return toolResult(page(summaries(result.data), result.nextCursor));
|
|
172
|
-
})
|
|
173
|
-
);
|
|
174
|
-
|
|
175
154
|
server.registerTool(
|
|
176
155
|
'taiga_get_document',
|
|
177
156
|
{
|
|
178
157
|
title: 'Get company document',
|
|
179
158
|
description:
|
|
180
|
-
"Read one of the organization's documents
|
|
159
|
+
"Read one of the organization's documents by type, or without type list them all (type, name, version, " +
|
|
160
|
+
`draft state). The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) set the rules for every ` +
|
|
161
|
+
`product; the others (${named(OTHER_TENANT_DOCUMENT_TYPES)}) carry goals, standards, voice and ` +
|
|
162
|
+
'instructions. Policies are long: pass ' +
|
|
181
163
|
'sections: [] for the outline, then the section ids you need, or a control id such as SEC-5.1; ' +
|
|
182
164
|
'unknownSections names ids it lacks. A miss says which documents are published and whether a draft ' +
|
|
183
165
|
"exists. A section's source (template, overlay-<id>, cloud-preset:<providers>) and priority (core: every " +
|
|
184
166
|
'organization keeps it) do not change what it requires.',
|
|
185
167
|
inputSchema: z.object({
|
|
186
|
-
type: tenantDocumentType,
|
|
168
|
+
type: tenantDocumentType.optional(),
|
|
187
169
|
sections: policySectionsInput,
|
|
170
|
+
...pagination,
|
|
188
171
|
}),
|
|
189
172
|
annotations: READ_ONLY,
|
|
190
173
|
},
|
|
191
174
|
(params) =>
|
|
192
175
|
runTool(async () => {
|
|
176
|
+
// One tool for the list and the read (PRO-2800): the catalogue's weight is the cost every review ranks
|
|
177
|
+
// first, and a list tool beside each read was the read without a type.
|
|
178
|
+
if (params.type === undefined) {
|
|
179
|
+
if (params.sections) return toolError('VALIDATION_ERROR', 'Name the document (type) to read its sections.');
|
|
180
|
+
const result = await client.listTenantDocuments({ limit: params.limit, cursor: params.cursor });
|
|
181
|
+
return toolResult(page(summaries(result.data), result.nextCursor));
|
|
182
|
+
}
|
|
183
|
+
const type = params.type;
|
|
184
|
+
if (params.limit !== undefined || params.cursor !== undefined) {
|
|
185
|
+
return toolError(
|
|
186
|
+
'VALIDATION_ERROR',
|
|
187
|
+
'limit and cursor page the list; omit type to list, or drop them to read.'
|
|
188
|
+
);
|
|
189
|
+
}
|
|
193
190
|
try {
|
|
194
|
-
const result = await client.getTenantDocument(
|
|
191
|
+
const result = await client.getTenantDocument(type);
|
|
195
192
|
const doc = stripProvenance(result.data);
|
|
196
193
|
return params.sections
|
|
197
|
-
? sectionReadResult(doc, params.sections,
|
|
198
|
-
controlIds: (POLICY_TYPES as readonly string[]).includes(
|
|
194
|
+
? sectionReadResult(doc, params.sections, type, {
|
|
195
|
+
controlIds: (POLICY_TYPES as readonly string[]).includes(type),
|
|
199
196
|
})
|
|
200
197
|
: toolResult(doc);
|
|
201
198
|
} catch (error) {
|
|
@@ -203,10 +200,10 @@ export function registerDocumentTools(server: McpServer, client: TaigaClient) {
|
|
|
203
200
|
// Best effort, each part on its own: a failed lookup must not turn
|
|
204
201
|
// a miss into an outage, nor blank the part that did answer.
|
|
205
202
|
const [draft, all] = await Promise.allSettled([
|
|
206
|
-
client.getTenantDocumentDraft(
|
|
203
|
+
client.getTenantDocumentDraft(type),
|
|
207
204
|
client.listTenantDocuments({ limit: 100 }),
|
|
208
205
|
]);
|
|
209
|
-
const parts = [`No published ${
|
|
206
|
+
const parts = [`No published ${type}.`];
|
|
210
207
|
if (draft.status === 'fulfilled') {
|
|
211
208
|
parts.push(
|
|
212
209
|
draft.value.data
|
|
@@ -277,28 +274,6 @@ export function registerDocumentTools(server: McpServer, client: TaigaClient) {
|
|
|
277
274
|
// ── Factory instructions ────────────────────────────────────────────
|
|
278
275
|
|
|
279
276
|
if (client.hasCapability('factories:read')) {
|
|
280
|
-
server.registerTool(
|
|
281
|
-
'taiga_list_factory_documents',
|
|
282
|
-
{
|
|
283
|
-
title: 'List factory documents',
|
|
284
|
-
description:
|
|
285
|
-
"List a factory's documents, published and draft (isDraft), as summaries (type, name, version). The only " +
|
|
286
|
-
`type is ${named(FACTORY_DOCUMENT_TYPES)}: standing guidance (domain context, tech preferences, ` +
|
|
287
|
-
'conventions) for every product in the factory. An empty list means no publication or draft exists. ' +
|
|
288
|
-
'taiga_get_factory_document reads it.',
|
|
289
|
-
inputSchema: z.object({
|
|
290
|
-
factoryId: idInput('factory', 'taiga_list_factories (query finds one by name) gives it'),
|
|
291
|
-
...pagination,
|
|
292
|
-
}),
|
|
293
|
-
annotations: READ_ONLY,
|
|
294
|
-
},
|
|
295
|
-
(params) =>
|
|
296
|
-
runTool(async () => {
|
|
297
|
-
const result = await client.listFactoryDocuments(params.factoryId, params);
|
|
298
|
-
return toolResult(page(summaries(result.data), result.nextCursor));
|
|
299
|
-
})
|
|
300
|
-
);
|
|
301
|
-
|
|
302
277
|
server.registerTool(
|
|
303
278
|
'taiga_get_factory_document',
|
|
304
279
|
{
|
package/src/tools/inbox.ts
CHANGED
|
@@ -63,7 +63,7 @@ function readWith(row: Row, backlogRead: boolean): string | undefined {
|
|
|
63
63
|
const type = /^[^:]+:([a-z_]+):decision:/.exec(String(row['subjectId']))?.[1];
|
|
64
64
|
return type
|
|
65
65
|
? `taiga_get_product_document (productId, type: ${type}); the decision's text is this row with full: true`
|
|
66
|
-
: '
|
|
66
|
+
: 'taiga_get_product_document (productId, no type)';
|
|
67
67
|
}
|
|
68
68
|
case 'inbox_task':
|
|
69
69
|
return 'taiga_list_setup_tasks (productId)';
|
|
@@ -73,7 +73,7 @@ function readWith(row: Row, backlogRead: boolean): string | undefined {
|
|
|
73
73
|
review_backlog: backlogRead ? 'taiga_list_initiatives (productId)' : undefined,
|
|
74
74
|
review_maintenance: 'taiga_list_findings (productId)',
|
|
75
75
|
review_policy: 'taiga_get_policy_standing (productId)',
|
|
76
|
-
review_doc: '
|
|
76
|
+
review_doc: 'taiga_get_product_document (productId, no type)',
|
|
77
77
|
}[String(row['kind'])];
|
|
78
78
|
default:
|
|
79
79
|
return undefined;
|
|
@@ -517,6 +517,8 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
|
517
517
|
snoozedUntil: z
|
|
518
518
|
.string()
|
|
519
519
|
.datetime()
|
|
520
|
+
// format: date-time says it; the leap-year pattern beside it ran to hundreds of characters (PRO-2800).
|
|
521
|
+
.meta({ pattern: undefined })
|
|
520
522
|
.nullable()
|
|
521
523
|
.optional()
|
|
522
524
|
.describe('Hide it until this time (ISO 8601 in UTC, ending in Z); null wakes it now'),
|
package/src/tools/maintenance.ts
CHANGED
|
@@ -429,6 +429,8 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
|
|
|
429
429
|
expiresAt: z
|
|
430
430
|
.string()
|
|
431
431
|
.datetime({ offset: true })
|
|
432
|
+
// format: date-time says it; the leap-year pattern beside it ran to hundreds of characters (PRO-2800).
|
|
433
|
+
.meta({ pattern: undefined })
|
|
432
434
|
.optional()
|
|
433
435
|
.describe('When the ignore lapses and the finding resurfaces if it still exists (ISO 8601, in the future)'),
|
|
434
436
|
}),
|
|
@@ -34,7 +34,7 @@ const EDITABLE_TYPES = PRODUCT_DOCUMENT_TYPES.filter((type): type is EditableTyp
|
|
|
34
34
|
|
|
35
35
|
const documentType = z
|
|
36
36
|
.enum(EDITABLE_TYPES)
|
|
37
|
-
.describe('Document type --
|
|
37
|
+
.describe('Document type -- taiga_get_product_document without type shows which ones the product has');
|
|
38
38
|
|
|
39
39
|
/** The documents a person reviews: the discovery chain, not the snapshot or the product's own instructions. */
|
|
40
40
|
const reviewableType = z
|
package/src/tools/products.ts
CHANGED
|
@@ -279,7 +279,7 @@ export async function resolveProductRef(
|
|
|
279
279
|
return refusal(
|
|
280
280
|
apiFailureCode(error),
|
|
281
281
|
error instanceof TaigaApiError && error.status === 404
|
|
282
|
-
? 'No product with that ID the key can see
|
|
282
|
+
? 'No product with that ID the key can see. taiga_list_products (query finds one by name) shows the ones it can; a slug or git remote URL names one too.'
|
|
283
283
|
: `No product with that ID the key can see (${apiFailureText(error)}); taiga_list_products shows the ones it can.`
|
|
284
284
|
);
|
|
285
285
|
}
|
|
@@ -470,41 +470,23 @@ export function registerProductTools(server: McpServer, client: TaigaClient) {
|
|
|
470
470
|
})
|
|
471
471
|
);
|
|
472
472
|
|
|
473
|
-
server.registerTool(
|
|
474
|
-
'taiga_list_product_documents',
|
|
475
|
-
{
|
|
476
|
-
title: 'List product documents',
|
|
477
|
-
description:
|
|
478
|
-
"List a product's documents on both timelines (type, name, version, draft state, review and update " +
|
|
479
|
-
`times); defaultOrigin is the product's own. ${ORIGIN_MEANING.plan} ${ORIGIN_MEANING.reality} ` +
|
|
480
|
-
'An empty side says why; an unlisted side is null with a note. ' +
|
|
481
|
-
'taiga_get_product_document reads one.',
|
|
482
|
-
inputSchema: z.object({ productId }),
|
|
483
|
-
annotations: READ_ONLY,
|
|
484
|
-
},
|
|
485
|
-
(params) =>
|
|
486
|
-
runTool(async () => {
|
|
487
|
-
// `unlistable` is how a miss message is built; the list says the
|
|
488
|
-
// same in its notes and a null side.
|
|
489
|
-
const { unlistable: _unlistable, ...timelines } = await documentTimelines(client, params.productId);
|
|
490
|
-
return toolResult(timelines);
|
|
491
|
-
})
|
|
492
|
-
);
|
|
493
|
-
|
|
494
473
|
server.registerTool(
|
|
495
474
|
'taiga_get_product_document',
|
|
496
475
|
{
|
|
497
476
|
title: 'Get product document',
|
|
498
477
|
description:
|
|
499
|
-
'Read one product document from one timeline
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
478
|
+
'Read one product document from one timeline, or without type list both timelines (type, version, ' +
|
|
479
|
+
"draft and review state; defaultOrigin is the product's own; an empty side says why). plan " +
|
|
480
|
+
'(specification, architecture, threat_model, technology_decisions) says what to build; reality says ' +
|
|
481
|
+
"what the code does, so read it before changing code. Omit origin for the product's own timeline. " +
|
|
482
|
+
'sections: [] returns the outline (ids, sizes); then name the sections you need (unknownSections names ' +
|
|
483
|
+
'missing ids). A miss says which documents exist.',
|
|
503
484
|
inputSchema: z.object({
|
|
504
485
|
productId,
|
|
505
486
|
type: z
|
|
506
487
|
.enum(PRODUCT_DOCUMENT_TYPES)
|
|
507
|
-
.
|
|
488
|
+
.optional()
|
|
489
|
+
.describe('Document type; omit to list what the product has on both timelines'),
|
|
508
490
|
origin: originInput,
|
|
509
491
|
sections: sectionsInput,
|
|
510
492
|
}),
|
|
@@ -512,16 +494,25 @@ export function registerProductTools(server: McpServer, client: TaigaClient) {
|
|
|
512
494
|
},
|
|
513
495
|
(params) =>
|
|
514
496
|
runTool(async () => {
|
|
497
|
+
// Without a type, the list of both timelines (PRO-2800): one tool where a list tool stood beside it.
|
|
498
|
+
if (params.type === undefined) {
|
|
499
|
+
if (params.sections || params.origin) {
|
|
500
|
+
return toolError('VALIDATION_ERROR', 'Name the document (type) to read one timeline or its sections.');
|
|
501
|
+
}
|
|
502
|
+
const { unlistable: _unlistable, ...timelines } = await documentTimelines(client, params.productId);
|
|
503
|
+
return toolResult(timelines);
|
|
504
|
+
}
|
|
505
|
+
const type = params.type;
|
|
515
506
|
// The product's own timeline when none is asked for; the API's
|
|
516
507
|
// default is plan, which is the wrong side of an imported product.
|
|
517
508
|
let product: ProductSettingsView | undefined;
|
|
518
509
|
if (!params.origin) product = (await client.getProduct(params.productId)).data;
|
|
519
|
-
const origin = params.origin ?? defaultOriginFor(product,
|
|
510
|
+
const origin = params.origin ?? defaultOriginFor(product, type);
|
|
520
511
|
try {
|
|
521
|
-
const result = await client.getProductDocument(params.productId,
|
|
512
|
+
const result = await client.getProductDocument(params.productId, type, origin);
|
|
522
513
|
const doc = stripProvenance(result.data);
|
|
523
514
|
if (!params.sections) return toolResult(doc);
|
|
524
|
-
return sectionReadResult(doc, params.sections, `${origin} ${
|
|
515
|
+
return sectionReadResult(doc, params.sections, `${origin} ${type}`);
|
|
525
516
|
} catch (error) {
|
|
526
517
|
if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
|
|
527
518
|
// A product the key cannot see is a product miss, not a document
|
|
@@ -530,7 +521,7 @@ export function registerProductTools(server: McpServer, client: TaigaClient) {
|
|
|
530
521
|
// propagates, as the API failing.
|
|
531
522
|
product ??= (await client.getProduct(params.productId)).data;
|
|
532
523
|
const timelines = await documentTimelines(client, params.productId, product);
|
|
533
|
-
return toolError('NOT_FOUND', describeMiss(
|
|
524
|
+
return toolError('NOT_FOUND', describeMiss(type, origin, timelines, product));
|
|
534
525
|
}
|
|
535
526
|
})
|
|
536
527
|
);
|
package/src/tools/prompts.ts
CHANGED
|
@@ -148,7 +148,7 @@ export function registerPrompts(server: McpServer, client: TaigaClient) {
|
|
|
148
148
|
architecture.status === 'fulfilled'
|
|
149
149
|
? fenced('markdown', renderDocument(stripProvenance(architecture.value.data)).text)
|
|
150
150
|
: isNotFound(architecture.reason)
|
|
151
|
-
? `(No published ${origin} architecture;
|
|
151
|
+
? `(No published ${origin} architecture; taiga_get_product_document without type shows what exists.)`
|
|
152
152
|
: unreadable('The architecture', architecture.reason);
|
|
153
153
|
const instructionsText_ =
|
|
154
154
|
instructions.status === 'fulfilled'
|
package/src/tools/readiness.ts
CHANGED
|
@@ -144,10 +144,10 @@ function nextStepFor(state: {
|
|
|
144
144
|
if (state.missing && state.missing.length > 0) {
|
|
145
145
|
return (
|
|
146
146
|
`The import finished but ${named(state.missing)} ${state.missing.length === 1 ? 'is' : 'are'} not ` +
|
|
147
|
-
'published:
|
|
147
|
+
'published: taiga_get_product_document without type shows them, taiga_publish_product_document publishes a draft.'
|
|
148
148
|
);
|
|
149
149
|
}
|
|
150
|
-
return `The import is done: review the reality documents (
|
|
150
|
+
return `The import is done: review the reality documents (taiga_get_product_document without type), then ${finish}`;
|
|
151
151
|
default:
|
|
152
152
|
return 'taiga_start_import derives the documents from the code and runs by itself (paid agent work: ask first).';
|
|
153
153
|
}
|
package/src/tools/resources.ts
CHANGED
|
@@ -142,7 +142,7 @@ export function registerResources(server: McpServer, client: TaigaClient) {
|
|
|
142
142
|
const read = async (uri: URL, productId: string, type: ProductDocumentType, origin: ProductDocumentOrigin) => {
|
|
143
143
|
const result = await served(
|
|
144
144
|
client.getProductDocument(productId, type, origin),
|
|
145
|
-
`No published ${origin} ${type} for this product, or the key cannot see it;
|
|
145
|
+
`No published ${origin} ${type} for this product, or the key cannot see it; taiga_get_product_document without type shows what exists`
|
|
146
146
|
);
|
|
147
147
|
return contents(uri.href, { name: productDocumentName(type), ...result.data });
|
|
148
148
|
};
|
|
@@ -173,7 +173,7 @@ export function registerResources(server: McpServer, client: TaigaClient) {
|
|
|
173
173
|
title: 'Product document',
|
|
174
174
|
description:
|
|
175
175
|
"A product document from the product's own timeline (reality for an imported product, plan otherwise; " +
|
|
176
|
-
'the documents a person writes are always on the plan side).
|
|
176
|
+
'the documents a person writes are always on the plan side). taiga_get_product_document without type shows what ' +
|
|
177
177
|
'exists; add ?origin=plan or ?origin=reality to read the other side. Markdown for a section map, JSON ' +
|
|
178
178
|
'otherwise (the read says which).',
|
|
179
179
|
},
|
package/src/tools/sections.ts
CHANGED
|
@@ -272,7 +272,7 @@ export function sectionReadResult(
|
|
|
272
272
|
? {
|
|
273
273
|
note: policy
|
|
274
274
|
? `${asControl} is a control of ${policy}: read it with taiga_get_document (type: ${policy}, sections: ["${asControl}"]).`
|
|
275
|
-
: `${asControl} looks like a policy control id, which only a policy resolves:
|
|
275
|
+
: `${asControl} looks like a policy control id, which only a policy resolves: taiga_get_document without type lists them.`,
|
|
276
276
|
}
|
|
277
277
|
: {};
|
|
278
278
|
if (known.length === 0) {
|
|
Binary file
|
|
Binary file
|