@taiga-ai/mcp-server 0.17.0-rc.442 → 0.18.0-dev.444
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 +209 -85
- package/package.json +1 -1
- package/src/client.ts +15 -2
- package/src/server.integration.test.ts +404 -37
- package/src/tools/autonomy.ts +2 -2
- package/src/tools/backlogs.ts +67 -8
- package/src/tools/catalogue.test.ts +15 -1
- package/src/tools/context.ts +31 -18
- package/src/tools/documents.ts +13 -10
- package/src/tools/hints.ts +1 -1
- package/src/tools/inbox.ts +24 -5
- package/src/tools/index.ts +23 -2
- package/src/tools/instructions.ts +7 -6
- package/src/tools/knowledge.ts +3 -3
- package/src/tools/maintenance.ts +5 -3
- package/src/tools/observe.ts +29 -13
- package/src/tools/product-documents.ts +4 -5
- package/src/tools/products.ts +7 -11
- package/src/tools/repositories.ts +47 -16
- package/src/tools/sections.ts +17 -5
- package/src/tools/setup.ts +8 -8
- package/taiga-ai-mcp-server-0.18.0-dev.444.tgz +0 -0
- package/taiga-ai-mcp-server-0.17.0-rc.442.tgz +0 -0
package/src/tools/backlogs.ts
CHANGED
|
@@ -66,6 +66,63 @@ const LIFECYCLE_MEANING =
|
|
|
66
66
|
'resume_build taiga_resume_backlog_item, retry_build a fresh run (start over, in the web), review_pr and ' +
|
|
67
67
|
'view_pr the pull request. needsHumanCause is set only when the line stopped on it.';
|
|
68
68
|
|
|
69
|
+
/** What each plan failure cause means for whoever reads it next (PLAN_FAILURE_CAUSES in @taiga/types). */
|
|
70
|
+
const PLAN_FAILURE_MEANING: Record<string, string> = {
|
|
71
|
+
agent_error: 'The planning agent failed. Planning again is the usual next step.',
|
|
72
|
+
worker_lost: 'Nobody was running the planning any more (the worker stopped). Planning again is the usual next step.',
|
|
73
|
+
cancelled: 'A person stopped the planning (by says who). Ask before planning it again.',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Who or what ended a failed plan, and when, from the initiative's history
|
|
78
|
+
* (PRO-2780): planFailureCause alone said "cancelled" with nobody behind it.
|
|
79
|
+
* Best effort: a history the API will not give leaves the item as it was.
|
|
80
|
+
*/
|
|
81
|
+
async function planFailureOf(
|
|
82
|
+
client: TaigaClient,
|
|
83
|
+
itemId: string,
|
|
84
|
+
itemCause: unknown
|
|
85
|
+
): Promise<Record<string, unknown> | null> {
|
|
86
|
+
let events: Record<string, unknown>[];
|
|
87
|
+
try {
|
|
88
|
+
events = (await client.listBacklogItemActivity(itemId)).data as Record<string, unknown>[];
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
// Only a failure of the latest attempt: one before the last plan_started
|
|
94
|
+
// belongs to an earlier attempt, even when it failed for the same reason.
|
|
95
|
+
let failed: Record<string, unknown> | undefined;
|
|
96
|
+
for (const event of [...events].reverse()) {
|
|
97
|
+
if (event.kind !== 'turn') continue;
|
|
98
|
+
if (event.trigger === 'plan_started') break;
|
|
99
|
+
if (event.trigger === 'plan_failed') {
|
|
100
|
+
failed = event;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!failed) return null;
|
|
105
|
+
const actor = (failed.actor ?? {}) as { type?: unknown; name?: unknown };
|
|
106
|
+
const data = (failed.data ?? {}) as { cause?: unknown };
|
|
107
|
+
const cause = typeof data.cause === 'string' ? data.cause : null;
|
|
108
|
+
// The event is recorded best effort: one that disagrees with the item's own
|
|
109
|
+
// cause belongs to an earlier failure, and its who and when are not this one's.
|
|
110
|
+
if (typeof itemCause === 'string' && cause !== itemCause) {
|
|
111
|
+
return {
|
|
112
|
+
cause: itemCause,
|
|
113
|
+
meaning: PLAN_FAILURE_MEANING[itemCause] ?? 'Planning ended without a plan.',
|
|
114
|
+
at: null,
|
|
115
|
+
by: null,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
cause,
|
|
120
|
+
meaning: (cause && PLAN_FAILURE_MEANING[cause]) ?? 'Planning ended without a plan.',
|
|
121
|
+
at: failed.occurredAt ?? null,
|
|
122
|
+
by: typeof actor.name === 'string' ? actor.name : (actor.type ?? null),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
69
126
|
const planId = idInput('plan', "taiga_get_plan gives the initiative's active plan and its ID");
|
|
70
127
|
|
|
71
128
|
const itemId = idInput('backlog item', 'taiga_list_backlog_items (query finds one by title) gives it');
|
|
@@ -189,6 +246,7 @@ function registerBacklogReads(server: McpServer, client: TaigaClient) {
|
|
|
189
246
|
'Get one backlog item in full: description, acceptance criteria (and which are met), technical ' +
|
|
190
247
|
'context (patterns, constraints, and the assumptions to verify before starting), dependencies and source documents. Read this before ' +
|
|
191
248
|
'implementing an item, and read its runs with taiga_list_builds to see what the coding agent already did. ' +
|
|
249
|
+
'A failed plan carries planFailure: the cause, what it means, when, and by whom. ' +
|
|
192
250
|
LIFECYCLE_MEANING,
|
|
193
251
|
inputSchema: z.object({
|
|
194
252
|
itemId,
|
|
@@ -197,8 +255,10 @@ function registerBacklogReads(server: McpServer, client: TaigaClient) {
|
|
|
197
255
|
},
|
|
198
256
|
(params) =>
|
|
199
257
|
runTool(async () => {
|
|
200
|
-
const
|
|
201
|
-
return toolResult(
|
|
258
|
+
const item = (await client.getBacklogItem(params.itemId)).data as Record<string, unknown>;
|
|
259
|
+
if (item.planningStatus !== 'failed') return toolResult(item);
|
|
260
|
+
const planFailure = await planFailureOf(client, params.itemId, item.planFailureCause);
|
|
261
|
+
return toolResult(planFailure ? { ...item, planFailure } : item);
|
|
202
262
|
})
|
|
203
263
|
);
|
|
204
264
|
|
|
@@ -341,8 +401,7 @@ function registerPlanAgain(server: McpServer, client: TaigaClient) {
|
|
|
341
401
|
'building, reviewing). Also refused while a dependency is still in flight or a UI direction has no ' +
|
|
342
402
|
'chosen concept, unless force says to plan anyway, which only the person decides. Anywhere off the ' +
|
|
343
403
|
'Build station, put the initiative in the queue instead (taiga_queue_backlog_item): the queue is the ' +
|
|
344
|
-
'only way to start work. To send a plan back with direction, taiga_reject_plan. Starts a planner run
|
|
345
|
-
'ask the person first.',
|
|
404
|
+
'only way to start work. To send a plan back with direction, taiga_reject_plan. Starts a planner run.',
|
|
346
405
|
inputSchema: z.object({
|
|
347
406
|
itemId,
|
|
348
407
|
force: z
|
|
@@ -486,7 +545,7 @@ function registerReviewGate(server: McpServer, client: TaigaClient) {
|
|
|
486
545
|
"Set whether an initiative's plan is built as soon as it lands (true) or waits for a person to read it and " +
|
|
487
546
|
"approve or reject it (false); null follows the product's default. Standing: it applies to every plan the " +
|
|
488
547
|
'initiative gets, including one being written now. Opening the gate on a plan that is already waiting ' +
|
|
489
|
-
'starts its build
|
|
548
|
+
'starts its build at once. Returns the initiative with the gate that now applies.',
|
|
490
549
|
inputSchema: z.object({ itemId, autoBuild: autoBuildInput }),
|
|
491
550
|
annotations: OPERATE_SETTING,
|
|
492
551
|
},
|
|
@@ -515,7 +574,7 @@ function registerResume(server: McpServer, client: TaigaClient) {
|
|
|
515
574
|
'way to carry work on: a run is the record of an attempt, and decisions about the work are made on the ' +
|
|
516
575
|
'initiative. Refused (CONFLICT) when nothing is flagged or the cause has no resume: a plan that failed ' +
|
|
517
576
|
'with needsHumanCause null (lifecycle.cta retry_plan, planFailureCause says why) is planned again with ' +
|
|
518
|
-
'taiga_plan_again. Starts paid work
|
|
577
|
+
'taiga_plan_again. Starts paid work.',
|
|
519
578
|
inputSchema: z.object({ itemId }),
|
|
520
579
|
annotations: OPERATE,
|
|
521
580
|
},
|
|
@@ -532,7 +591,7 @@ function registerPlanVerdicts(server: McpServer, client: TaigaClient) {
|
|
|
532
591
|
"Approve an initiative's plan: its build starts now, as the approver, the same start the Start run " +
|
|
533
592
|
'button makes. Read the plan first with taiga_get_plan and name its ID; an approval of a plan that ' +
|
|
534
593
|
'was replaced since is refused (CONFLICT), as is one while a new plan is being generated or a build is ' +
|
|
535
|
-
'already running. Starts a coding run that pushes commits and opens a pull request
|
|
594
|
+
'already running. Starts a coding run that pushes commits and opens a pull request.',
|
|
536
595
|
inputSchema: z.object({ planId }),
|
|
537
596
|
annotations: OPERATE,
|
|
538
597
|
},
|
|
@@ -546,7 +605,7 @@ function registerPlanVerdicts(server: McpServer, client: TaigaClient) {
|
|
|
546
605
|
description:
|
|
547
606
|
"Send an initiative's plan back with direction: a new plan is generated with the comment as reviewer " +
|
|
548
607
|
'feedback and the rejected plan as its starting point, which supersedes it when the new one lands. ' +
|
|
549
|
-
'Read the plan first with taiga_get_plan and name its ID. Starts a planner run
|
|
608
|
+
'Read the plan first with taiga_get_plan and name its ID. Starts a planner run.',
|
|
550
609
|
inputSchema: z.object({
|
|
551
610
|
planId,
|
|
552
611
|
comment: z
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
TENANT_DOCUMENT_TYPES,
|
|
10
10
|
} from '@taiga/types';
|
|
11
11
|
import { TaigaClient } from '../client.js';
|
|
12
|
-
import { registerAllTools } from './index.js';
|
|
12
|
+
import { registerAllTools, writeNotice } from './index.js';
|
|
13
13
|
import { INSTRUCTIONS_HEAD, INSTRUCTIONS_HEAD_LIMIT, SERVER_INSTRUCTIONS } from './instructions.js';
|
|
14
14
|
import { PRODUCT_DOCUMENT_NAMES } from './catalogue.js';
|
|
15
15
|
|
|
@@ -150,3 +150,17 @@ describe('tool descriptions against the catalogue', () => {
|
|
|
150
150
|
}
|
|
151
151
|
});
|
|
152
152
|
});
|
|
153
|
+
|
|
154
|
+
// PRO-2778: the notice is what tells the model a tool writes, so a tool must say whether it does.
|
|
155
|
+
describe('writeNotice', () => {
|
|
156
|
+
it('refuses a tool whose annotations do not say whether it is read-only', () => {
|
|
157
|
+
expect(() => writeNotice('taiga_x', undefined)).toThrow(/taiga_x: annotations must say/);
|
|
158
|
+
expect(() => writeNotice('taiga_x', {})).toThrow(/annotations must say/);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('says nothing for a read, and what a write does otherwise', () => {
|
|
162
|
+
expect(writeNotice('r', { readOnlyHint: true })).toBe('');
|
|
163
|
+
expect(writeNotice('w', { readOnlyHint: false })).toBe('Writes. ');
|
|
164
|
+
expect(writeNotice('o', { readOnlyHint: false, destructiveHint: true })).toMatch(/^Acts on paid or shared work/);
|
|
165
|
+
});
|
|
166
|
+
});
|
package/src/tools/context.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { apiFailureText, isNetworkFailure, networkFailureDetail, runTool, toolError, toolResult } from './errors.js';
|
|
15
15
|
import { productId, remoteUrl, slug } from './inputs.js';
|
|
16
16
|
import { PRODUCT_SUMMARY_FIELDS, resolveProductRef } from './products.js';
|
|
17
|
-
import { budgetSections, instructionsText, isRecord, sectionsOf, stripProvenance } from './sections.js';
|
|
17
|
+
import { budgetSections, instructionsText, isRecord, sectionContent, sectionsOf, stripProvenance } from './sections.js';
|
|
18
18
|
import { itemRows, pickFields } from './summarize.js';
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -87,8 +87,14 @@ function withoutCitations(doc: Record<string, unknown>): { doc: Record<string, u
|
|
|
87
87
|
return { doc: { ...doc, data: { ...(doc.data as Record<string, unknown>), sections: kept } }, dropped };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/**
|
|
91
|
-
|
|
90
|
+
/**
|
|
91
|
+
* How much of the two documents the bundle carries. The outline by default
|
|
92
|
+
* (PRO-2778): the whole sections that fit a budget made the opening read run
|
|
93
|
+
* to 43k characters for an active product, most of it prose a "where does it
|
|
94
|
+
* stand" question never reads.
|
|
95
|
+
*/
|
|
96
|
+
export const DOCUMENT_DEPTHS = ['outline', 'budget', 'full'] as const;
|
|
97
|
+
export type DocumentDepth = (typeof DOCUMENT_DEPTHS)[number];
|
|
92
98
|
|
|
93
99
|
/** Pages of 100 the backlog part follows before it stops and says so. */
|
|
94
100
|
const BACKLOG_PAGE_LIMIT = 10;
|
|
@@ -156,7 +162,7 @@ async function factoryPart(client: TaigaClient, factoryId: string) {
|
|
|
156
162
|
|
|
157
163
|
/**
|
|
158
164
|
* The specification and architecture from the product's own timeline: their
|
|
159
|
-
* whole sections up to a budget
|
|
165
|
+
* outline by default, the whole sections up to a budget or in full when
|
|
160
166
|
* asked. Null, with a note, where unpublished.
|
|
161
167
|
*/
|
|
162
168
|
async function documentsPart(client: TaigaClient, product: Record<string, unknown>, depth: DocumentDepth) {
|
|
@@ -195,8 +201,15 @@ async function documentsPart(client: TaigaClient, product: Record<string, unknow
|
|
|
195
201
|
} else out[type] = cut.doc;
|
|
196
202
|
} else {
|
|
197
203
|
const sections = sectionsOf((doc.data as Record<string, unknown>) ?? {});
|
|
198
|
-
if (sections)
|
|
199
|
-
|
|
204
|
+
if (sections) {
|
|
205
|
+
// Each id with the length of its text, so the next read is sized before it is
|
|
206
|
+
// made (a reality section's citations come on top).
|
|
207
|
+
const outline = Object.keys(sections).map((id) => ({
|
|
208
|
+
id,
|
|
209
|
+
characters: sectionContent(sections, id)?.length ?? 0,
|
|
210
|
+
}));
|
|
211
|
+
out[type] = { version: doc.version, sections: outline };
|
|
212
|
+
} else {
|
|
200
213
|
out[type] = { version: doc.version, sections: null };
|
|
201
214
|
notes.push(`The ${origin} ${type} is not made of sections; read it with taiga_get_product_document.`);
|
|
202
215
|
}
|
|
@@ -340,8 +353,8 @@ export async function productContext(
|
|
|
340
353
|
notGranted: notGranted.map((part) => `${part} (needs ${PARTS[part]})`),
|
|
341
354
|
unavailable,
|
|
342
355
|
readNext:
|
|
343
|
-
'Read a policy with taiga_get_document (sections: the ids listed here, for only those),
|
|
344
|
-
'document with taiga_get_product_document,
|
|
356
|
+
'Read a policy with taiga_get_document (sections: the ids listed here, for only those), the ' +
|
|
357
|
+
"specification's or architecture's sections or another product document with taiga_get_product_document, " +
|
|
345
358
|
'a backlog item with taiga_get_backlog_item (taiga_list_backlog_items pages a truncated backlog) and a run ' +
|
|
346
359
|
'with taiga_get_build.',
|
|
347
360
|
};
|
|
@@ -368,20 +381,21 @@ export function registerContextTools(server: McpServer, client: TaigaClient) {
|
|
|
368
381
|
'every published policy as its section ids, the open backlog items, the linked repositories and the ' +
|
|
369
382
|
'latest runs. Name the product by ID, slug or the git remote URL of the checkout. A part the API refused ' +
|
|
370
383
|
'or failed is named under unavailable with what it said; a part the key cannot read is named under ' +
|
|
371
|
-
'notGranted; neither is shown as empty. The two documents
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
'
|
|
375
|
-
'characters says how large the result is. Call this
|
|
384
|
+
'notGranted; neither is shown as empty. The two documents come as their outline by default: each ' +
|
|
385
|
+
'section id with its size, to read with taiga_get_product_document naming the sections. Before writing ' +
|
|
386
|
+
`code, documents: budget brings as many whole sections as fit, about ${DOCUMENT_BUDGET / 1000}k ` +
|
|
387
|
+
'characters each, naming the ones left out; full brings them whole, tens of thousands of tokens on a ' +
|
|
388
|
+
'large product and more than some clients accept. characters says how large the result is. Call this ' +
|
|
389
|
+
'first, then read what you need.',
|
|
376
390
|
inputSchema: z.object({
|
|
377
391
|
productId: productId.optional(),
|
|
378
392
|
slug: slug.optional(),
|
|
379
393
|
remoteUrl: remoteUrl.optional(),
|
|
380
|
-
|
|
381
|
-
.
|
|
394
|
+
documents: z
|
|
395
|
+
.enum(DOCUMENT_DEPTHS)
|
|
382
396
|
.optional()
|
|
383
397
|
.describe(
|
|
384
|
-
'
|
|
398
|
+
'The specification and architecture as outline (section ids and sizes, the default), budget (the whole sections that fit) or full'
|
|
385
399
|
),
|
|
386
400
|
}),
|
|
387
401
|
annotations: READ_ONLY,
|
|
@@ -390,8 +404,7 @@ export function registerContextTools(server: McpServer, client: TaigaClient) {
|
|
|
390
404
|
runTool(async () => {
|
|
391
405
|
const resolved = await resolveProductRef(client, params, ['productId', 'slug', 'remoteUrl']);
|
|
392
406
|
if ('refusal' in resolved) return toolError(resolved.refusal.code, resolved.refusal.message);
|
|
393
|
-
|
|
394
|
-
return productContext(client, resolved.product, { documents });
|
|
407
|
+
return productContext(client, resolved.product, { documents: params.documents ?? 'outline' });
|
|
395
408
|
})
|
|
396
409
|
);
|
|
397
410
|
}
|
package/src/tools/documents.ts
CHANGED
|
@@ -11,7 +11,14 @@ import {
|
|
|
11
11
|
named,
|
|
12
12
|
} from './catalogue.js';
|
|
13
13
|
import { runTool, toolError, toolResult } from './errors.js';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
applySectionEdit,
|
|
16
|
+
sectionContent,
|
|
17
|
+
sectionReadResult,
|
|
18
|
+
sectionsInput,
|
|
19
|
+
sectionsOf,
|
|
20
|
+
stripProvenance,
|
|
21
|
+
} from './sections.js';
|
|
15
22
|
import { page, pickFields } from './summarize.js';
|
|
16
23
|
|
|
17
24
|
/**
|
|
@@ -170,17 +177,13 @@ export function registerDocumentTools(server: McpServer, client: TaigaClient) {
|
|
|
170
177
|
'Use this when you need the security requirements, data handling rules, architecture standards ' +
|
|
171
178
|
'or compliance controls that apply to your code, or the goals, standards and voice the ' +
|
|
172
179
|
'organization has set down. A policy runs to thousands of tokens: name the sections you need (the ' +
|
|
173
|
-
'ids taiga_get_product_context lists for every policy
|
|
174
|
-
'rest with their sizes and any id the document lacks
|
|
175
|
-
'says which documents are published and whether a
|
|
180
|
+
'ids taiga_get_product_context lists for every policy, or sections: [] for the outline) and the read ' +
|
|
181
|
+
'carries only those, listing the rest with their sizes and any id the document lacks ' +
|
|
182
|
+
'(unknownSections). A read that finds no document says which documents are published and whether a ' +
|
|
183
|
+
'draft exists.',
|
|
176
184
|
inputSchema: z.object({
|
|
177
185
|
type: tenantDocumentType,
|
|
178
|
-
sections:
|
|
179
|
-
.array(z.string().min(1).max(200))
|
|
180
|
-
.min(1)
|
|
181
|
-
.max(50)
|
|
182
|
-
.optional()
|
|
183
|
-
.describe('Only these sections, by id; omit for the whole document'),
|
|
186
|
+
sections: sectionsInput,
|
|
184
187
|
}),
|
|
185
188
|
annotations: READ_ONLY,
|
|
186
189
|
},
|
package/src/tools/hints.ts
CHANGED
|
@@ -21,7 +21,7 @@ const BY_ARGUMENT: Record<string, string> = {
|
|
|
21
21
|
factoryId: 'taiga_list_factories lists the factories the key can see.',
|
|
22
22
|
findingId: "taiga_list_findings lists a product's findings.",
|
|
23
23
|
violationId: "taiga_get_policy_standing lists a product's policy violations.",
|
|
24
|
-
environmentId: "
|
|
24
|
+
environmentId: "taiga_list_environments lists the product's environments with their IDs; a product ID is not one.",
|
|
25
25
|
knowledgeId: 'taiga_list_knowledge lists the knowledge files.',
|
|
26
26
|
productId:
|
|
27
27
|
'taiga_list_products lists the products the key can see; a name is not an ID (taiga_get_product takes a slug).',
|
package/src/tools/inbox.ts
CHANGED
|
@@ -51,6 +51,11 @@ const SETUP_TASK_FIELDS = [
|
|
|
51
51
|
'createdAt',
|
|
52
52
|
] as const;
|
|
53
53
|
|
|
54
|
+
/** The API's count across every page, on the first page; nothing from an API that does not send one. */
|
|
55
|
+
function totalOf(result: { totalCount?: unknown }): { totalCount?: number } {
|
|
56
|
+
return typeof result.totalCount === 'number' ? { totalCount: result.totalCount } : {};
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
55
60
|
if (!client.hasCapability('products:read')) return;
|
|
56
61
|
|
|
@@ -66,7 +71,8 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
|
66
71
|
'can open (each row names its product), or one product. category gate is the reviews, setup the setup items; resolved: true reads the ' +
|
|
67
72
|
"archive of done and dismissed ones. The inbox is shared: it is the team's, not one person's. " +
|
|
68
73
|
'An item snoozed until later is left out until then, as the page leaves it out (snoozedHidden counts ' +
|
|
69
|
-
'them).
|
|
74
|
+
'them). The first page says how many there are on every page (totalCount, snoozed ones included). ' +
|
|
75
|
+
'taiga_list_setup_tasks has the setup tasks with what exactly to set.',
|
|
70
76
|
inputSchema: z.object({
|
|
71
77
|
productId: productId.optional(),
|
|
72
78
|
category: z.enum(['gate', 'setup']).optional().describe('gate: things to review; setup: things to set up'),
|
|
@@ -80,7 +86,11 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
|
80
86
|
// The route coerces any value to a boolean, so "false" would read the
|
|
81
87
|
// archive: send resolved only when it is asked for.
|
|
82
88
|
const { resolved, ...rest } = params;
|
|
83
|
-
const result = await client.listInbox({
|
|
89
|
+
const result = await client.listInbox({
|
|
90
|
+
...rest,
|
|
91
|
+
...(resolved ? { resolved: true } : {}),
|
|
92
|
+
...(params.cursor === undefined ? { withTotal: true } : {}),
|
|
93
|
+
});
|
|
84
94
|
// The API returns snoozed items, as they are not done; the page hides
|
|
85
95
|
// one until its snooze ends, and so does this, so an agent does not
|
|
86
96
|
// act on what the team put off.
|
|
@@ -93,6 +103,7 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
|
93
103
|
const hidden = rows.length - due.length;
|
|
94
104
|
return toolResult({
|
|
95
105
|
...page(pickFields(due, ITEM_FIELDS), result.nextCursor),
|
|
106
|
+
...totalOf(result),
|
|
96
107
|
...(hidden > 0 ? { snoozedHidden: hidden } : {}),
|
|
97
108
|
});
|
|
98
109
|
})
|
|
@@ -110,7 +121,8 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
|
110
121
|
'providers; followup: billing approvals, extra permissions and the like that can wait until after launch). ' +
|
|
111
122
|
'A build does not wait for any of them. Across every product the key can open (each row names its ' +
|
|
112
123
|
'product), or one. The tasks still to do, oldest first, the order they depend on each other; ' +
|
|
113
|
-
'includeDone: true adds the done and dismissed ones.'
|
|
124
|
+
'includeDone: true adds the done and dismissed ones. The first page says how many there are on every ' +
|
|
125
|
+
'page (totalCount).',
|
|
114
126
|
inputSchema: z.object({
|
|
115
127
|
productId: productId.optional(),
|
|
116
128
|
includeDone: z.boolean().optional().describe('Also the tasks already done or dismissed (default false)'),
|
|
@@ -121,8 +133,15 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
|
121
133
|
(params) =>
|
|
122
134
|
runTool(async () => {
|
|
123
135
|
const { includeDone, ...rest } = params;
|
|
124
|
-
const result = await client.listSetupTasks({
|
|
125
|
-
|
|
136
|
+
const result = await client.listSetupTasks({
|
|
137
|
+
...rest,
|
|
138
|
+
...(includeDone ? {} : { openOnly: true }),
|
|
139
|
+
...(params.cursor === undefined ? { withTotal: true } : {}),
|
|
140
|
+
});
|
|
141
|
+
return toolResult({
|
|
142
|
+
...page(pickFields(result.data as Row[], SETUP_TASK_FIELDS), result.nextCursor),
|
|
143
|
+
...totalOf(result),
|
|
144
|
+
});
|
|
126
145
|
})
|
|
127
146
|
);
|
|
128
147
|
|
package/src/tools/index.ts
CHANGED
|
@@ -32,7 +32,11 @@ import { DEFAULT_WAIT_OPTIONS, registerWaitTools, type WaitOptions } from './wai
|
|
|
32
32
|
*/
|
|
33
33
|
function withStrictInputs(server: McpServer): McpServer {
|
|
34
34
|
const register = server.registerTool.bind(server) as (...args: unknown[]) => unknown;
|
|
35
|
-
server.registerTool = ((
|
|
35
|
+
server.registerTool = ((
|
|
36
|
+
name: string,
|
|
37
|
+
config: { inputSchema?: unknown; description?: string; annotations?: ToolAnnotations },
|
|
38
|
+
callback: unknown
|
|
39
|
+
) => {
|
|
36
40
|
const schema = config.inputSchema;
|
|
37
41
|
if (schema !== undefined && !(schema instanceof z.ZodObject)) {
|
|
38
42
|
throw new Error(`Tool ${name}: inputSchema must be a z.object() so unknown arguments can be refused`);
|
|
@@ -42,11 +46,28 @@ function withStrictInputs(server: McpServer): McpServer {
|
|
|
42
46
|
const handler = callback as (args: unknown, ctx: unknown) => unknown;
|
|
43
47
|
const withHint = (args: unknown, ctx: unknown) =>
|
|
44
48
|
notFoundHints.run(notFoundHint(name, args), () => handler(args, ctx));
|
|
45
|
-
|
|
49
|
+
const described = { ...config, description: `${writeNotice(name, config.annotations)}${config.description ?? ''}` };
|
|
50
|
+
return register(name, schema ? { ...described, inputSchema: schema.strict() } : described, withHint);
|
|
46
51
|
}) as unknown as McpServer['registerTool'];
|
|
47
52
|
return server;
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
type ToolAnnotations = { readOnlyHint?: boolean; destructiveHint?: boolean };
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* What a tool's annotations say, in its description's first words (PRO-2778).
|
|
59
|
+
* Annotations are for the client: Claude Code does not show them to the
|
|
60
|
+
* model, so an agent read 79 tools without knowing which ones write. A tool
|
|
61
|
+
* with none is refused here, so no tool reaches the catalogue unclassified.
|
|
62
|
+
*/
|
|
63
|
+
export function writeNotice(name: string, annotations: ToolAnnotations | undefined): string {
|
|
64
|
+
if (!annotations || annotations.readOnlyHint === undefined) {
|
|
65
|
+
throw new Error(`Tool ${name}: annotations must say whether it is read-only`);
|
|
66
|
+
}
|
|
67
|
+
if (annotations.readOnlyHint) return '';
|
|
68
|
+
return annotations.destructiveHint ? 'Acts on paid or shared work: ask the person first. ' : 'Writes. ';
|
|
69
|
+
}
|
|
70
|
+
|
|
50
71
|
/**
|
|
51
72
|
* Register all MCP tools, resources and prompts based on the API key's effective capabilities.
|
|
52
73
|
* Only tools matching the key's scopes are registered -- the AI assistant
|
|
@@ -25,15 +25,16 @@ export const INSTRUCTIONS_HEAD = [
|
|
|
25
25
|
'**Start here.** Call taiga_get_product_context first, naming the product by its git remote URL inside a',
|
|
26
26
|
'checkout (git remote get-url origin), else by slug or ID (taiga_list_products finds one by name with',
|
|
27
27
|
'query). It returns the product (and, while it is in setup, nextStep: what it waits for), its factory',
|
|
28
|
-
'instructions, its specification and architecture, every policy as its section ids, the
|
|
29
|
-
'the repositories and the latest runs. Then read only what you need:
|
|
30
|
-
|
|
31
|
-
'(taiga_list_action_required) for what
|
|
28
|
+
'instructions, the outline of its specification and architecture, every policy as its section ids, the',
|
|
29
|
+
'open initiatives, the repositories and the latest runs. Then read only what you need: sections by id with',
|
|
30
|
+
"taiga_get_document or taiga_get_product_document (sections: [] is any document's outline), an",
|
|
31
|
+
'initiative with taiga_get_backlog_item. Check Action Required (taiga_list_action_required) for what',
|
|
32
|
+
'waits on a person.',
|
|
32
33
|
'',
|
|
33
34
|
'**Before you write.** Tell the person what you are about to do and wait for their yes before a tool that',
|
|
34
35
|
'starts paid agent work (queueing, approving, resuming, requesting, importing, generating), interrupts it',
|
|
35
|
-
'(stopping, pausing) or changes what others read (publishing, resolving, settings, repositories).
|
|
36
|
-
'that prompts on destructive tools does part of
|
|
36
|
+
'(stopping, pausing) or changes what others read (publishing, resolving, settings, repositories). Such a',
|
|
37
|
+
'tool says so first in its description; a client that prompts on destructive tools does part of it.',
|
|
37
38
|
'',
|
|
38
39
|
'**Answers.** A refusal carries the API reason: say it as it is. NOT_FOUND names the tool that lists the',
|
|
39
40
|
'right IDs; an ID is a UUID, never a name. An empty result says why when it has a reason.',
|
package/src/tools/knowledge.ts
CHANGED
|
@@ -183,7 +183,7 @@ export function registerKnowledgeTools(server: McpServer, client: TaigaClient) {
|
|
|
183
183
|
'file is processed before agents can read it; taiga_list_knowledge says when it is ready. PDFs and ' +
|
|
184
184
|
'spreadsheets are added in the web. The same content already filed at that level comes back as that ' +
|
|
185
185
|
'file, with deduplicated: true: it was not created here, so do not delete it as if it were. Every agent at that level reads it from then on, and processing it ' +
|
|
186
|
-
'is paid work
|
|
186
|
+
'is paid work.',
|
|
187
187
|
inputSchema: z.object({
|
|
188
188
|
...scopeInputs,
|
|
189
189
|
fileName: z
|
|
@@ -225,7 +225,7 @@ export function registerKnowledgeTools(server: McpServer, client: TaigaClient) {
|
|
|
225
225
|
title: 'Retry processing a knowledge file',
|
|
226
226
|
description:
|
|
227
227
|
'Retry processing a knowledge file that failed (status failed): until it is processed, nothing reads it. ' +
|
|
228
|
-
'Processing is paid work
|
|
228
|
+
'Processing is paid work.',
|
|
229
229
|
inputSchema: z.object({ knowledgeId }),
|
|
230
230
|
annotations: OPERATE,
|
|
231
231
|
},
|
|
@@ -244,7 +244,7 @@ export function registerKnowledgeTools(server: McpServer, client: TaigaClient) {
|
|
|
244
244
|
description:
|
|
245
245
|
'Delete a knowledge file that is stale or wrong: stale reference material is worse than none, because ' +
|
|
246
246
|
'it is confidently specific, and nothing expires on its own. Agents stop reading it at once. It cannot ' +
|
|
247
|
-
'be undone
|
|
247
|
+
'be undone. Organization knowledge takes an owner or admin.',
|
|
248
248
|
inputSchema: z.object({ knowledgeId }),
|
|
249
249
|
annotations: OPERATE,
|
|
250
250
|
},
|
package/src/tools/maintenance.ts
CHANGED
|
@@ -171,7 +171,8 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
|
|
|
171
171
|
"each with its violations (critical and high only, each citing the policy's sentence and the document " +
|
|
172
172
|
'passage that contradicts it), what is being done about each (an initiative raised, a fix in progress, ' +
|
|
173
173
|
'awaiting verification), and when the documents were derived and assessed, led by a summary of the ' +
|
|
174
|
-
'counts. A control
|
|
174
|
+
'counts. A control breached by scanner findings counts them by tier (findingsByTier): a breach of ' +
|
|
175
|
+
'routine findings only, such as secrets in test fixtures, is not an urgent one. A control with no violation ' +
|
|
175
176
|
'is not listed, and it is not "passing" either: there is no such state. Pass full: true for every ' +
|
|
176
177
|
"violation's evidence and the resolved ones. Fix a control with taiga_fix_policy_violation.",
|
|
177
178
|
inputSchema: z.object({
|
|
@@ -219,6 +220,7 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
|
|
|
219
220
|
'state',
|
|
220
221
|
'divergenceCount',
|
|
221
222
|
'findingCount',
|
|
223
|
+
'findingsByTier',
|
|
222
224
|
'plannedCount',
|
|
223
225
|
]),
|
|
224
226
|
violations: pickFields(violations, VIOLATION_FIELDS),
|
|
@@ -283,7 +285,7 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
|
|
|
283
285
|
'dependency, every open advisory on that package at that installed version in one version bump; for ' +
|
|
284
286
|
'code or infrastructure, every location the same rule flagged. Absent for a secret only in history, ' +
|
|
285
287
|
'anything in a submodule, and a dependency with no fixed version (remediableBy says upstream_only); the ' +
|
|
286
|
-
'API refuses those. Starts paid work
|
|
288
|
+
'API refuses those. Starts paid work.',
|
|
287
289
|
inputSchema: z.object({
|
|
288
290
|
productId,
|
|
289
291
|
findingId,
|
|
@@ -334,7 +336,7 @@ export function registerMaintenanceTools(server: McpServer, client: TaigaClient)
|
|
|
334
336
|
'against it that has no initiative yet, or a single violation (violationId) when the control has more ' +
|
|
335
337
|
"than one. Taiga raises one initiative and adds it to the end of the product's queue, exactly as a " +
|
|
336
338
|
'finding fix does. The violation stays listed while the work is under way: only the next assessment ' +
|
|
337
|
-
'can record that the documents no longer show it. Starts paid work
|
|
339
|
+
'can record that the documents no longer show it. Starts paid work.',
|
|
338
340
|
inputSchema: z.object({
|
|
339
341
|
productId,
|
|
340
342
|
controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
|
package/src/tools/observe.ts
CHANGED
|
@@ -39,9 +39,9 @@ const DEPLOYMENT_FIELDS = [
|
|
|
39
39
|
|
|
40
40
|
const ENVIRONMENT_FIELDS = ['id', 'name', 'slug', 'type', 'ordinal', 'status', 'isProtected', 'url'] as const;
|
|
41
41
|
|
|
42
|
-
const environmentId = idInput('environment', '
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const environmentId = idInput('environment', 'taiga_list_environments lists them; a product ID is not one');
|
|
43
|
+
/** The windows the pages offer, as the schema's own values rather than a number checked afterwards. */
|
|
44
|
+
const windowDays = z.literal([30, 60, 90]).optional().describe('Days to look back: 30, 60 or 90 (default 30)');
|
|
45
45
|
|
|
46
46
|
/** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
|
|
47
47
|
const isApiFailure = (error: unknown) => error instanceof TaigaApiError || isNetworkFailure(error);
|
|
@@ -129,6 +129,28 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
|
|
|
129
129
|
})
|
|
130
130
|
);
|
|
131
131
|
|
|
132
|
+
server.registerTool(
|
|
133
|
+
'taiga_list_environments',
|
|
134
|
+
{
|
|
135
|
+
title: "List a product's environments",
|
|
136
|
+
description:
|
|
137
|
+
"The places a product's software runs (development, staging, production or its own ladder), each with " +
|
|
138
|
+
'its ID, type, order and App URL. The environment IDs taiga_get_delivery_metrics, taiga_get_monitoring ' +
|
|
139
|
+
'and taiga_update_environment take. taiga_get_deployments lists them too, beside what runs where.',
|
|
140
|
+
inputSchema: z.object({ productId }),
|
|
141
|
+
annotations: READ_ONLY,
|
|
142
|
+
},
|
|
143
|
+
(params) =>
|
|
144
|
+
runTool(async () => {
|
|
145
|
+
const { rows, more } = await allEnvironments(client, params.productId);
|
|
146
|
+
return toolResult({
|
|
147
|
+
data: pickFields(rows, ENVIRONMENT_FIELDS),
|
|
148
|
+
...(more ? { more: 'More environments than this answer reads; the rest were left out.' } : {}),
|
|
149
|
+
...(rows.length === 0 ? { note: 'No environment is defined yet; taiga_define_environment adds one.' } : {}),
|
|
150
|
+
});
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
|
|
132
154
|
server.registerTool(
|
|
133
155
|
'taiga_get_delivery_metrics',
|
|
134
156
|
{
|
|
@@ -141,15 +163,12 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
|
|
|
141
163
|
'rework rate, which DORA has published none for. Failures and recoveries are read from deployments, not ' +
|
|
142
164
|
'incidents. A trend built on fewer than ten deployments is approximate; a metric no deployment fed says ' +
|
|
143
165
|
'so instead of showing a number. Previews and checks do not count.',
|
|
144
|
-
inputSchema: z.object({ productId, environmentId, windowDays
|
|
166
|
+
inputSchema: z.object({ productId, environmentId, windowDays }),
|
|
145
167
|
annotations: READ_ONLY,
|
|
146
168
|
},
|
|
147
169
|
(params) =>
|
|
148
170
|
runTool(async () => {
|
|
149
171
|
const days = params.windowDays ?? 30;
|
|
150
|
-
if (![30, 60, 90].includes(days)) {
|
|
151
|
-
return toolError('VALIDATION_ERROR', 'windowDays is 30, 60 or 90.');
|
|
152
|
-
}
|
|
153
172
|
return toolResult((await client.getDeliveryMetrics(params.productId, params.environmentId, days)).data);
|
|
154
173
|
})
|
|
155
174
|
);
|
|
@@ -169,7 +188,7 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
|
|
|
169
188
|
inputSchema: z.object({
|
|
170
189
|
productId,
|
|
171
190
|
environmentId: environmentId.optional(),
|
|
172
|
-
windowDays
|
|
191
|
+
windowDays,
|
|
173
192
|
}),
|
|
174
193
|
annotations: READ_ONLY,
|
|
175
194
|
},
|
|
@@ -189,9 +208,6 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
|
|
|
189
208
|
});
|
|
190
209
|
}
|
|
191
210
|
const days = params.windowDays ?? 30;
|
|
192
|
-
if (![30, 60, 90].includes(days)) {
|
|
193
|
-
return toolError('VALIDATION_ERROR', 'windowDays is 30, 60 or 90, the windows the page offers.');
|
|
194
|
-
}
|
|
195
211
|
const env = params.environmentId;
|
|
196
212
|
const [uptime, experience, clientHealth] = await Promise.all([
|
|
197
213
|
part(() => client.getMonitoringSignal(params.productId, 'uptime', env, days)),
|
|
@@ -240,7 +256,7 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
|
|
|
240
256
|
'read where the work will run. Describing one gives Taiga no access to it: the pipeline deploys, Taiga ' +
|
|
241
257
|
'learns what happened from source control. Member-level work. Give it its App URL afterwards with ' +
|
|
242
258
|
'taiga_update_environment, which is all monitoring needs to start checking. Changes the ' +
|
|
243
|
-
"product's setup for everybody
|
|
259
|
+
"product's setup for everybody.",
|
|
244
260
|
inputSchema: z.object({
|
|
245
261
|
productId,
|
|
246
262
|
name: z.string().trim().min(1).max(100).describe('What the team calls it, e.g. staging'),
|
|
@@ -264,7 +280,7 @@ export function registerObserveTools(server: McpServer, client: TaigaClient) {
|
|
|
264
280
|
'Rename an environment, change its kind, or set its public App URL (null clears it). The App URL is all ' +
|
|
265
281
|
"monitoring needs: once it is saved, Taiga checks the app's availability every few minutes on its own. " +
|
|
266
282
|
'Marking an environment production makes it the one delivery metrics report on. Changes the ' +
|
|
267
|
-
"product's setup for everybody
|
|
283
|
+
"product's setup for everybody.",
|
|
268
284
|
inputSchema: z.object({
|
|
269
285
|
environmentId,
|
|
270
286
|
name: z.string().trim().min(1).max(100).optional().describe('A new name'),
|