@taiga-ai/mcp-server 0.18.0-rc.445 → 0.19.0-dev.451
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 +1128 -650
- package/package.json +1 -1
- package/src/client.test.ts +2 -2
- package/src/client.ts +201 -145
- package/src/modern.integration.test.ts +1 -1
- package/src/server.integration.test.ts +782 -243
- package/src/tools/autonomy.ts +3 -5
- package/src/tools/backlogs.ts +182 -72
- package/src/tools/builds.ts +142 -53
- package/src/tools/catalogue.test.ts +28 -22
- package/src/tools/context.ts +126 -56
- package/src/tools/documents.ts +12 -5
- package/src/tools/errors.ts +40 -2
- package/src/tools/hints.ts +37 -9
- package/src/tools/inbox.ts +149 -28
- package/src/tools/index.ts +15 -1
- package/src/tools/instructions.ts +32 -154
- package/src/tools/jobs.ts +6 -5
- package/src/tools/knowledge.ts +4 -6
- package/src/tools/maintenance.ts +53 -32
- package/src/tools/markdown.ts +13 -5
- package/src/tools/observe.ts +32 -14
- package/src/tools/product-documents.ts +4 -6
- package/src/tools/products.ts +32 -27
- package/src/tools/prompts.ts +14 -20
- package/src/tools/queue.ts +47 -26
- package/src/tools/readiness.ts +172 -0
- package/src/tools/repositories.ts +11 -9
- package/src/tools/resources.ts +16 -19
- package/src/tools/sections.test.ts +3 -3
- package/src/tools/sections.ts +57 -16
- package/src/tools/setup.ts +47 -51
- package/src/tools/summarize.test.ts +20 -0
- package/src/tools/summarize.ts +17 -13
- package/src/tools/wait.ts +39 -26
- package/taiga-ai-mcp-server-0.19.0-dev.451.tgz +0 -0
- package/tsconfig.json +1 -5
- package/taiga-ai-mcp-server-0.18.0-rc.445.tgz +0 -0
package/src/tools/inbox.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import type
|
|
3
|
+
import { TaigaApiError, type TaigaClient } from '../client.js';
|
|
4
4
|
import { OPERATE, READ_ONLY } from './annotations.js';
|
|
5
|
-
import { runTool, toolError, toolResult } from './errors.js';
|
|
5
|
+
import { isNetworkFailure, runTool, toolError, toolResult } from './errors.js';
|
|
6
6
|
import { idInput, pagination, productId } from './inputs.js';
|
|
7
7
|
import { page, pickFields } from './summarize.js';
|
|
8
8
|
|
|
@@ -16,6 +16,11 @@ import { page, pickFields } from './summarize.js';
|
|
|
16
16
|
|
|
17
17
|
type Row = Record<string, unknown>;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* An item as a row: what it is, whose product, how long it has waited. Not
|
|
21
|
+
* its metadata by default: a document decision carried its whole text there,
|
|
22
|
+
* and fifty rows came to 57 KB (PRO-2800); full: true brings it.
|
|
23
|
+
*/
|
|
19
24
|
const ITEM_FIELDS = [
|
|
20
25
|
'id',
|
|
21
26
|
'productId',
|
|
@@ -29,11 +34,54 @@ const ITEM_FIELDS = [
|
|
|
29
34
|
'status',
|
|
30
35
|
'blocking',
|
|
31
36
|
'snoozedUntil',
|
|
32
|
-
'metadata',
|
|
33
37
|
'createdAt',
|
|
34
38
|
'resolvedAt',
|
|
35
39
|
] as const;
|
|
36
40
|
|
|
41
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
42
|
+
|
|
43
|
+
/** When an item stopped waiting: its resolution, or now while it is open. */
|
|
44
|
+
function waitedUntil(resolvedAt: unknown, now: number): number {
|
|
45
|
+
const at = typeof resolvedAt === 'string' ? Date.parse(resolvedAt) : NaN;
|
|
46
|
+
return Number.isNaN(at) ? now : at;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A row as an agent reads it: the pull request to open when there is one, and how many days it has waited. */
|
|
50
|
+
function itemRow(row: Row, now: number, full: boolean): Row {
|
|
51
|
+
const picked = pickFields([row], ITEM_FIELDS)[0]!;
|
|
52
|
+
const { snoozedUntil, resolvedAt, ...rest } = picked;
|
|
53
|
+
const metadata = (row['metadata'] ?? {}) as Row;
|
|
54
|
+
const created = typeof row['createdAt'] === 'string' ? Date.parse(row['createdAt']) : NaN;
|
|
55
|
+
return {
|
|
56
|
+
...rest,
|
|
57
|
+
...(snoozedUntil ? { snoozedUntil } : {}),
|
|
58
|
+
...(resolvedAt ? { resolvedAt } : {}),
|
|
59
|
+
...(typeof metadata['prUrl'] === 'string' ? { prUrl: metadata['prUrl'] } : {}),
|
|
60
|
+
// A resolved item waited until it was resolved, not until now.
|
|
61
|
+
...(Number.isNaN(created) ? {} : { waitingDays: Math.floor((waitedUntil(resolvedAt, now) - created) / DAY_MS) }),
|
|
62
|
+
...(full ? { metadata: row['metadata'] } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** What is open, counted by product and kind: the answer to "what needs attention" without reading every row. */
|
|
67
|
+
function summaryOf(rows: Row[]): Row[] {
|
|
68
|
+
const byProduct = new Map<
|
|
69
|
+
string,
|
|
70
|
+
{ productId: string; product: unknown; open: number; byKind: Record<string, number> }
|
|
71
|
+
>();
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
const key = String(row['productId']);
|
|
74
|
+
const entry = byProduct.get(key) ?? { productId: key, product: row['productName'] ?? key, open: 0, byKind: {} };
|
|
75
|
+
entry.open += 1;
|
|
76
|
+
entry.byKind[String(row['kind'])] = (entry.byKind[String(row['kind'])] ?? 0) + 1;
|
|
77
|
+
byProduct.set(key, entry);
|
|
78
|
+
}
|
|
79
|
+
return [...byProduct.values()].sort((a, b) => b.open - a.open);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Pages read for the summary; past this it says it counted only those. */
|
|
83
|
+
const SUMMARY_PAGE = 100;
|
|
84
|
+
|
|
37
85
|
const SETUP_TASK_FIELDS = [
|
|
38
86
|
'id',
|
|
39
87
|
'productId',
|
|
@@ -56,6 +104,48 @@ function totalOf(result: { totalCount?: unknown }): { totalCount?: number } {
|
|
|
56
104
|
return typeof result.totalCount === 'number' ? { totalCount: result.totalCount } : {};
|
|
57
105
|
}
|
|
58
106
|
|
|
107
|
+
/**
|
|
108
|
+
* One row per variable: tasks filed before the API merged a key's categories
|
|
109
|
+
* (PRO-2782) still list JWT_SECRET once as a secret and once as an env_var.
|
|
110
|
+
* The oldest row stays, and names the others under alsoFiledAs, so the
|
|
111
|
+
* duplicates read as one thing to set with several records, not several things.
|
|
112
|
+
*/
|
|
113
|
+
export function sameKeyOnce(rows: Row[]): Row[] {
|
|
114
|
+
const first = new Map<string, Row>();
|
|
115
|
+
const out: Row[] = [];
|
|
116
|
+
const isOpen = (row: Row) => row['inboxItemStatus'] !== 'done' && row['inboxItemStatus'] !== 'dismissed';
|
|
117
|
+
for (const row of rows) {
|
|
118
|
+
if (typeof row['key'] !== 'string' || !row['key']) {
|
|
119
|
+
out.push(row);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const key = `${String(row['productId'])}:${row['key']}`;
|
|
123
|
+
const kept = first.get(key);
|
|
124
|
+
if (!kept) {
|
|
125
|
+
const copy = { ...row };
|
|
126
|
+
first.set(key, copy);
|
|
127
|
+
out.push(copy);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const also = (kept['alsoFiledAs'] as Row[] | undefined) ?? [];
|
|
131
|
+
const record = (from: Row) => ({
|
|
132
|
+
id: from['id'],
|
|
133
|
+
category: from['category'],
|
|
134
|
+
title: from['title'],
|
|
135
|
+
inboxItemStatus: from['inboxItemStatus'],
|
|
136
|
+
});
|
|
137
|
+
if (!isOpen(kept) && isOpen(row)) {
|
|
138
|
+
// An open record of the variable leads: a done one must not make it read as done.
|
|
139
|
+
const { alsoFiledAs: _also, ...was } = kept;
|
|
140
|
+
for (const field of Object.keys(kept)) delete kept[field];
|
|
141
|
+
Object.assign(kept, row, { alsoFiledAs: [...also, record(was)] });
|
|
142
|
+
} else {
|
|
143
|
+
kept['alsoFiledAs'] = [...also, record(row)];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
59
149
|
export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
60
150
|
if (!client.hasCapability('products:read')) return;
|
|
61
151
|
|
|
@@ -64,19 +154,24 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
|
64
154
|
{
|
|
65
155
|
title: 'List what needs a person',
|
|
66
156
|
description:
|
|
67
|
-
'Read Action Required, where work comes back to a person: things to review (a plan,
|
|
68
|
-
'initiatives, a document, a pull request, maintenance findings, new policy violations)
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
157
|
+
'Read Action Required, where work comes back to a person, shared by the team: things to review (a plan, ' +
|
|
158
|
+
'a set of initiatives, a document decision, a pull request, maintenance findings, new policy violations) ' +
|
|
159
|
+
'and things to set up that only a person can (a secret, an environment variable, an account, a package ' +
|
|
160
|
+
'registry, a cloud permission). Across every product the key can open, or one product. The first page ' +
|
|
161
|
+
'opens with summary: what is open, counted by product and kind, so "what needs attention" is one call, ' +
|
|
162
|
+
'and totalCount. Each row says how many days it has waited (waitingDays) and, for a pull request review, ' +
|
|
163
|
+
'the prUrl; an old item is still open, not stale. category gate is the reviews, setup the setup items; ' +
|
|
164
|
+
'resolved: true reads the archive. An item snoozed until later is left out until then (snoozedHidden ' +
|
|
165
|
+
"counts them). full: true adds each item's metadata. Unlike taiga_list_setup_tasks, which lists the setup " +
|
|
166
|
+
'items with exactly what to set and where.',
|
|
76
167
|
inputSchema: z.object({
|
|
77
168
|
productId: productId.optional(),
|
|
78
169
|
category: z.enum(['gate', 'setup']).optional().describe('gate: things to review; setup: things to set up'),
|
|
79
170
|
resolved: z.boolean().optional().describe('Read the resolved archive (done and dismissed) instead'),
|
|
171
|
+
full: z
|
|
172
|
+
.boolean()
|
|
173
|
+
.optional()
|
|
174
|
+
.describe("Carry each item's metadata, such as a document decision's whole text (default false)"),
|
|
80
175
|
...pagination,
|
|
81
176
|
}),
|
|
82
177
|
annotations: READ_ONLY,
|
|
@@ -85,24 +180,47 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
|
85
180
|
runTool(async () => {
|
|
86
181
|
// The route coerces any value to a boolean, so "false" would read the
|
|
87
182
|
// archive: send resolved only when it is asked for.
|
|
88
|
-
const { resolved, ...rest } = params;
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
183
|
+
const { resolved, full, ...rest } = params;
|
|
184
|
+
const query = { ...rest, ...(resolved ? { resolved: true } : {}) };
|
|
185
|
+
const first = params.cursor === undefined;
|
|
186
|
+
const result = await client.listInbox({ ...query, ...(first ? { withTotal: true } : {}) });
|
|
187
|
+
// The summary counts what is open across products, on the first page:
|
|
188
|
+
// from the page itself when it holds everything, else from one read
|
|
189
|
+
// of up to SUMMARY_PAGE items. Best effort: a failed read leaves the
|
|
190
|
+
// summary out, not the page the agent asked for.
|
|
191
|
+
const overview =
|
|
192
|
+
!first || resolved
|
|
193
|
+
? null
|
|
194
|
+
: !result.nextCursor
|
|
195
|
+
? result
|
|
196
|
+
: await client.listInbox({ ...query, limit: SUMMARY_PAGE, cursor: undefined }).catch((error: unknown) => {
|
|
197
|
+
if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
|
|
198
|
+
throw error;
|
|
199
|
+
});
|
|
94
200
|
// The API returns snoozed items, as they are not done; the page hides
|
|
95
201
|
// one until its snooze ends, and so does this, so an agent does not
|
|
96
202
|
// act on what the team put off.
|
|
97
203
|
const now = Date.now();
|
|
204
|
+
const isDue = (row: Row) =>
|
|
205
|
+
!(
|
|
206
|
+
row['status'] === 'snoozed' &&
|
|
207
|
+
typeof row['snoozedUntil'] === 'string' &&
|
|
208
|
+
Date.parse(row['snoozedUntil']) > now
|
|
209
|
+
);
|
|
98
210
|
const rows = result.data as Row[];
|
|
99
|
-
const due = rows.filter(
|
|
100
|
-
(row) =>
|
|
101
|
-
!(row.status === 'snoozed' && typeof row.snoozedUntil === 'string' && Date.parse(row.snoozedUntil) > now)
|
|
102
|
-
);
|
|
211
|
+
const due = rows.filter(isDue);
|
|
103
212
|
const hidden = rows.length - due.length;
|
|
104
213
|
return toolResult({
|
|
105
|
-
...
|
|
214
|
+
...(overview
|
|
215
|
+
? {
|
|
216
|
+
summary: summaryOf((overview.data as Row[]).filter(isDue)),
|
|
217
|
+
...(overview.nextCursor ? { summaryNote: `The summary counts the first ${SUMMARY_PAGE} items.` } : {}),
|
|
218
|
+
}
|
|
219
|
+
: {}),
|
|
220
|
+
...page(
|
|
221
|
+
due.map((row) => itemRow(row, now, full === true)),
|
|
222
|
+
result.nextCursor
|
|
223
|
+
),
|
|
106
224
|
...totalOf(result),
|
|
107
225
|
...(hidden > 0 ? { snoozedHidden: hidden } : {}),
|
|
108
226
|
});
|
|
@@ -122,7 +240,10 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
|
122
240
|
'A build does not wait for any of them. Across every product the key can open (each row names its ' +
|
|
123
241
|
'product), or one. The tasks still to do, oldest first, the order they depend on each other; ' +
|
|
124
242
|
'includeDone: true adds the done and dismissed ones. The first page says how many there are on every ' +
|
|
125
|
-
'page (totalCount).'
|
|
243
|
+
'page (totalCount). One row per variable: a variable filed twice (as a secret and as an environment ' +
|
|
244
|
+
'variable) lists the other records under alsoFiledAs, an open one first; within a page, and totalCount ' +
|
|
245
|
+
'counts every record. Unlike taiga_list_action_required, whose setup ' +
|
|
246
|
+
'items are the same work as rows to resolve, this says exactly what to set and where.',
|
|
126
247
|
inputSchema: z.object({
|
|
127
248
|
productId: productId.optional(),
|
|
128
249
|
includeDone: z.boolean().optional().describe('Also the tasks already done or dismissed (default false)'),
|
|
@@ -139,7 +260,7 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
|
139
260
|
...(params.cursor === undefined ? { withTotal: true } : {}),
|
|
140
261
|
});
|
|
141
262
|
return toolResult({
|
|
142
|
-
...page(pickFields(result.data as Row[], SETUP_TASK_FIELDS), result.nextCursor),
|
|
263
|
+
...page(sameKeyOnce(pickFields(result.data as Row[], SETUP_TASK_FIELDS)), result.nextCursor),
|
|
143
264
|
...totalOf(result),
|
|
144
265
|
});
|
|
145
266
|
})
|
|
@@ -161,7 +282,7 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
|
161
282
|
'document was reviewed, and setting it back to in_progress withdraws that; do it only when the person ' +
|
|
162
283
|
'says they read them. Ask the person before clearing what they have not dealt with.',
|
|
163
284
|
inputSchema: z.object({
|
|
164
|
-
|
|
285
|
+
actionId: idInput('Action Required item', 'taiga_list_action_required gives it'),
|
|
165
286
|
status: z.enum(['in_progress', 'done', 'dismissed']).optional().describe('What became of it'),
|
|
166
287
|
snoozedUntil: z
|
|
167
288
|
.string()
|
|
@@ -177,11 +298,11 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
|
177
298
|
if (params.status === undefined && params.snoozedUntil === undefined) {
|
|
178
299
|
return toolError('VALIDATION_ERROR', 'Say what became of the item (status) or until when to snooze it.');
|
|
179
300
|
}
|
|
180
|
-
const result = await client.updateInboxItem(params.
|
|
301
|
+
const result = await client.updateInboxItem(params.actionId, {
|
|
181
302
|
...(params.status !== undefined ? { status: params.status } : {}),
|
|
182
303
|
...(params.snoozedUntil !== undefined ? { snoozedUntil: params.snoozedUntil } : {}),
|
|
183
304
|
});
|
|
184
|
-
return toolResult(pickFields([result.data
|
|
305
|
+
return toolResult(pickFields([result.data], ITEM_FIELDS)[0]);
|
|
185
306
|
})
|
|
186
307
|
);
|
|
187
308
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -46,7 +46,11 @@ function withStrictInputs(server: McpServer): McpServer {
|
|
|
46
46
|
const handler = callback as (args: unknown, ctx: unknown) => unknown;
|
|
47
47
|
const withHint = (args: unknown, ctx: unknown) =>
|
|
48
48
|
notFoundHints.run(notFoundHint(name, args), () => handler(args, ctx));
|
|
49
|
-
const described = {
|
|
49
|
+
const described = {
|
|
50
|
+
...config,
|
|
51
|
+
description: `${writeNotice(name, config.annotations)}${config.description ?? ''}`,
|
|
52
|
+
...(ALWAYS_LOAD.has(name) ? { _meta: { 'anthropic/alwaysLoad': true } } : {}),
|
|
53
|
+
};
|
|
50
54
|
return register(name, schema ? { ...described, inputSchema: schema.strict() } : described, withHint);
|
|
51
55
|
}) as unknown as McpServer['registerTool'];
|
|
52
56
|
return server;
|
|
@@ -54,6 +58,16 @@ function withStrictInputs(server: McpServer): McpServer {
|
|
|
54
58
|
|
|
55
59
|
type ToolAnnotations = { readOnlyHint?: boolean; destructiveHint?: boolean };
|
|
56
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The entry points, loaded with their descriptions at session start
|
|
63
|
+
* (PRO-2800). Claude Code defers every other MCP tool behind tool search and
|
|
64
|
+
* shows the model only its name until it searches, so an agent meeting Taiga
|
|
65
|
+
* sees these three in full: where a product stands, which products there
|
|
66
|
+
* are, and what waits on a person. Three, because each one loaded upfront
|
|
67
|
+
* costs every session its description.
|
|
68
|
+
*/
|
|
69
|
+
export const ALWAYS_LOAD = new Set(['taiga_get_product_context', 'taiga_list_products', 'taiga_list_action_required']);
|
|
70
|
+
|
|
57
71
|
/**
|
|
58
72
|
* What a tool's annotations say, in its description's first words (PRO-2778).
|
|
59
73
|
* Annotations are for the client: Claude Code does not show them to the
|
|
@@ -1,158 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
OTHER_TENANT_DOCUMENT_TYPES,
|
|
3
|
-
ORIGIN_MEANING,
|
|
4
|
-
POLICY_TYPES,
|
|
5
|
-
PRODUCT_DOCUMENT_NAMES,
|
|
6
|
-
named,
|
|
7
|
-
} from './catalogue.js';
|
|
8
|
-
|
|
9
1
|
/**
|
|
10
|
-
* What the server tells a client at connect time
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* INSTRUCTIONS_HEAD_LIMIT, and the reference material after it.
|
|
2
|
+
* What the server tells a client at connect time. Claude Code loads only the
|
|
3
|
+
* tool NAMES and these instructions until the model searches for a tool, and
|
|
4
|
+
* cuts each server's instructions at 2,048 characters (code.claude.com, MCP
|
|
5
|
+
* tool search), so this is the whole introduction an agent gets: what Taiga
|
|
6
|
+
* holds, when to search these tools, where to start, the words, when to ask,
|
|
7
|
+
* and what only the web does. Everything else lives in the tool descriptions,
|
|
8
|
+
* which load with the tool. The earlier instructions ran to about 8,000
|
|
9
|
+
* characters, and everything past the first 2,000 never reached the model
|
|
10
|
+
* (PRO-2800).
|
|
20
11
|
*/
|
|
21
|
-
export const INSTRUCTIONS_HEAD = [
|
|
22
|
-
"You have access to Taiga, a policy-aware software development platform: the organization's",
|
|
23
|
-
'engineering knowledge base, its products, their backlog and what the coding agent has built.',
|
|
24
|
-
'',
|
|
25
|
-
'**Start here.** Call taiga_get_product_context first, naming the product by its git remote URL inside a',
|
|
26
|
-
'checkout (git remote get-url origin), else by slug or ID (taiga_list_products finds one by name with',
|
|
27
|
-
'query). It returns the product (and, while it is in setup, nextStep: what it waits for), its factory',
|
|
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.',
|
|
33
|
-
'',
|
|
34
|
-
'**Before you write.** Tell the person what you are about to do and wait for their yes before a tool that',
|
|
35
|
-
'starts paid agent work (queueing, approving, resuming, requesting, importing, generating), interrupts it',
|
|
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.',
|
|
38
|
-
'',
|
|
39
|
-
'**Answers.** A refusal carries the API reason: say it as it is. NOT_FOUND names the tool that lists the',
|
|
40
|
-
'right IDs; an ID is a UUID, never a name. An empty result says why when it has a reason.',
|
|
41
|
-
'',
|
|
42
|
-
'**Words.** A backlog item is an initiative; a build is a run, the record of one attempt at it; Action',
|
|
43
|
-
'Required is the inbox; the line is the queue; a factory is the group that owns a set of products.',
|
|
44
|
-
].join('\n');
|
|
45
|
-
|
|
46
|
-
/** The most the head may take: under what a client shows before it cuts. */
|
|
47
|
-
export const INSTRUCTIONS_HEAD_LIMIT = 1900;
|
|
48
|
-
|
|
49
12
|
export const SERVER_INSTRUCTIONS = [
|
|
50
|
-
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'',
|
|
58
|
-
'
|
|
59
|
-
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
'An imported (brownfield) product has no generated discovery chain: its derived documents are on the',
|
|
71
|
-
'reality side, while the documents a person writes (Look & Feel, Product Instructions) are on the plan',
|
|
72
|
-
'side of every product. Omit origin on a read and the server picks the right side (the list reports the',
|
|
73
|
-
"product's own as defaultOrigin). To understand",
|
|
74
|
-
'what to build, read the plan where there is one; to understand the code you are changing, read reality',
|
|
75
|
-
'when it exists, the ground truth for architecture, data flow and threats as built.',
|
|
76
|
-
'',
|
|
77
|
-
'**Backlog and runs** (taiga_list_backlog_items / taiga_get_backlog_item / taiga_list_builds / taiga_get_build):',
|
|
78
|
-
"The product's initiatives with their acceptance criteria, and what Taiga's coding agent already built",
|
|
79
|
-
'for them: runs, steps, commits and pull requests. taiga_list_builds without a product lists the runs of',
|
|
80
|
-
'every product the key can see, and active: true the ones executing now.',
|
|
81
|
-
'',
|
|
82
|
-
'**Operating the line** works the way Taiga is meant to be used. New work is described, not written:',
|
|
83
|
-
'taiga_request_initiatives asks an agent, which reads the product and its repository, expands the request',
|
|
84
|
-
'into initiatives in the Backlog (or says why it added none: already covered, not work, out of scope, or a',
|
|
85
|
-
'policy that does not allow it) -- follow it with taiga_wait_for_job. The queue is the only way to start',
|
|
86
|
-
'work (taiga_queue_backlog_item): Taiga plans and builds one initiative at a time, in queue order, and starts',
|
|
87
|
-
'the next when the pull request merges. Whether a landed plan waits for a review is the review gate',
|
|
88
|
-
'(taiga_set_auto_build, or autoBuild on the queue); read it with taiga_get_plan and answer with',
|
|
89
|
-
'taiga_approve_plan or taiga_reject_plan. Decisions about the work are made on the initiative: stop work by',
|
|
90
|
-
'moving it out of Build (taiga_move_backlog_item), carry it on with taiga_resume_backlog_item, ask for a new',
|
|
91
|
-
'plan with taiga_plan_again; a run is the record of an attempt. taiga_pause_line pauses the whole line.',
|
|
92
|
-
'taiga_stop_build is the emergency brake for a run whose initiative cannot be moved. Queueing, approving,',
|
|
93
|
-
'resuming and requesting start paid agent work; stopping and pausing interrupt it. Tell the person what you',
|
|
94
|
-
'are about to start or stop and wait for their yes; a client that prompts on destructive tools does that',
|
|
95
|
-
'for you. A refusal (CONFLICT) carries the API reason: say it as it is.',
|
|
96
|
-
'',
|
|
97
|
-
'**Watching the line** (taiga_get_queue / taiga_wait_for_backlog_item / taiga_wait_for_build /',
|
|
98
|
-
'taiga_get_job / taiga_wait_for_job): the queue read says what the line is waiting on (a paused line, a plan',
|
|
99
|
-
'to review, a pull request to merge, a halt and what lifts it). Planning takes minutes and a build up to an',
|
|
100
|
-
'hour: wait with the wait tools rather than reading the same item again and again; each returns when the',
|
|
101
|
-
'point is reached, when only a person can move it on, or at its timeout (call again to keep waiting).',
|
|
102
|
-
'',
|
|
103
|
-
'**Action Required** (taiga_list_action_required / taiga_list_setup_tasks / taiga_resolve_action): where work',
|
|
104
|
-
'comes back to a person, shared by the team. Check it first. Resolving an item clears it for everybody; a',
|
|
105
|
-
'setup task is marked done only by the person who did the setup.',
|
|
106
|
-
'',
|
|
107
|
-
'**Maintaining** (taiga_get_maintenance / taiga_list_findings / taiga_get_finding / taiga_ignore_finding /',
|
|
108
|
-
'taiga_fix_finding / taiga_get_policy_standing / taiga_fix_policy_violation): what the sweeps found and where',
|
|
109
|
-
'the product stands against its own policies. Sweeps run on their own; check when the last one ran before',
|
|
110
|
-
'trusting a quiet list. A finding is ignored with a reason or fixed; a fix raises an initiative at the end of',
|
|
111
|
-
'the queue. Nothing is marked fixed by hand: the next sweep or assessment decides.',
|
|
112
|
-
'',
|
|
113
|
-
'**Deployments and monitoring** (taiga_get_deployments / taiga_get_delivery_metrics / taiga_get_monitoring /',
|
|
114
|
-
'taiga_define_environment / taiga_update_environment): what runs where, how delivery is going, and how each',
|
|
115
|
-
'environment is doing. Taiga observes deployments and never performs them. An environment needs only its App',
|
|
116
|
-
'URL for monitoring to start.',
|
|
117
|
-
'',
|
|
118
|
-
'**Knowledge** (taiga_list_knowledge / taiga_get_knowledge / taiga_add_knowledge / taiga_retry_knowledge /',
|
|
119
|
-
'taiga_delete_knowledge): reference material agents search when work calls for it, at the organization, a',
|
|
120
|
-
'factory or a product. A rule that applies every time belongs in instructions instead.',
|
|
121
|
-
'',
|
|
122
|
-
'**Setting a product up** (taiga_create_factory / taiga_create_product / taiga_update_product /',
|
|
123
|
-
'taiga_list_linkable_repositories / taiga_link_repository / taiga_start_import / taiga_finish_discovery /',
|
|
124
|
-
'taiga_reopen_discovery / taiga_move_product): a product starts from scratch (Discovery is a conversation,',
|
|
125
|
-
'held in the web) or by importing a repository, chosen when it is created. An import links the repository,',
|
|
126
|
-
'starts the import and runs by itself; finishing Discovery locks the documents and opens the rest of the',
|
|
127
|
-
'product. Linking a repository and creating or moving factories are for owners and admins. GitHub is',
|
|
128
|
-
'connected once, in the web; members and deleting stay there too.',
|
|
129
|
-
'',
|
|
130
|
-
'**Product documents and autonomy** (taiga_update_product_document / taiga_publish_product_document /',
|
|
131
|
-
'taiga_regenerate_product_document / taiga_generate_product_documents / taiga_stop_generating_documents /',
|
|
132
|
-
'taiga_mark_product_document_reviewed /',
|
|
133
|
-
'taiga_set_product_autonomy / taiga_set_auto_merge): edit a product document in its draft and publish it;',
|
|
134
|
-
'documents built from it are marked outdated and regenerated, not rewritten. Set whether plans wait for a',
|
|
135
|
-
'review and pull requests for a person to merge. Organization-wide settings, members, roles and connections',
|
|
136
|
-
"are administrators' work in the web.",
|
|
137
|
-
'',
|
|
138
|
-
'**Sections.** Policies and instructions are maps of sections. Each section carries its text and two',
|
|
139
|
-
'provenance fields you can read past: `source` says where the text originally came from (template:',
|
|
140
|
-
"Taiga's standard text; overlay-<id>: a jurisdiction or industry overlay; cloud-preset:<providers>:",
|
|
141
|
-
"rendered for the organization's cloud providers; any other tag names the derivation that wrote the",
|
|
142
|
-
'text; a hand edit keeps the source it had) and',
|
|
143
|
-
'`priority: core` marks the sections every organization keeps. Neither changes what the section',
|
|
144
|
-
'requires of you.',
|
|
145
|
-
'',
|
|
146
|
-
'Without taiga_get_product_context, use taiga_list_factories and taiga_list_products, then read the',
|
|
147
|
-
"factory instructions, the product's specification and architecture, and the initiative you are working",
|
|
148
|
-
'on. A document read that finds nothing tells you what does exist and which tool reads or writes it.',
|
|
149
|
-
'',
|
|
150
|
-
'The same documents are resources (taiga://documents/{type}, taiga://products/{productId}/documents/{type},',
|
|
151
|
-
'taiga://factories/{factoryId}/instructions, taiga://backlog/{itemId}) for clients that attach them, and three',
|
|
152
|
-
'prompts open a session: taiga_start_session, taiga_implement_backlog_item, taiga_review_against_policies.',
|
|
153
|
-
'',
|
|
154
|
-
'When you notice a policy or factory instructions are outdated, fix the section with',
|
|
155
|
-
'taiga_update_document or taiga_update_factory_document. That saves a draft nobody follows yet;',
|
|
156
|
-
'publish it with taiga_publish_document or taiga_publish_factory_document only when the person asks.',
|
|
157
|
-
'This keeps the knowledge base accurate for everyone.',
|
|
13
|
+
"Taiga is a policy-aware software delivery platform: an organization's policies and knowledge, its factories",
|
|
14
|
+
"and products, each product's initiatives (its backlog), the line that plans and builds them one at a time, the",
|
|
15
|
+
"coding agent's runs and pull requests, maintenance findings, and Action Required, where work waits on a person.",
|
|
16
|
+
'Search these tools whenever the person asks about a Taiga product, an initiative, a run or pull request, policy',
|
|
17
|
+
'or compliance, findings, deployments or monitoring, or setting a product up.',
|
|
18
|
+
'',
|
|
19
|
+
'**Start here.** taiga_get_product_context (the product by git remote URL, slug or ID) gives its state in one',
|
|
20
|
+
'call: what it still needs while in setup, its documents as outlines, policies as section ids, open initiatives,',
|
|
21
|
+
'recent runs with their outcome, and what waits on a person. Then read only what you need. Across products,',
|
|
22
|
+
'taiga_list_products and taiga_list_action_required.',
|
|
23
|
+
'',
|
|
24
|
+
'**Words.** An initiative is a unit of work in the backlog; a run is one attempt by the coding agent to build it;',
|
|
25
|
+
"the line is the product's queue; a factory owns a set of products. IDs are UUIDs, never names; a NOT_FOUND names",
|
|
26
|
+
'the tool that lists the right ones. A refusal carries the reason: say it as it is.',
|
|
27
|
+
'',
|
|
28
|
+
'**Before you write.** A tool that writes says so first: "Writes." or "Acts on paid or shared work: ask the',
|
|
29
|
+
'person first." Ask before the second kind: it starts paid agent work, interrupts it, or changes what others see.',
|
|
30
|
+
'',
|
|
31
|
+
'**Only in the web:** connecting GitHub, members and roles, the Discovery conversation of a product started from',
|
|
32
|
+
'scratch, stamping checks, starting a run over, and deleting anything.',
|
|
158
33
|
].join('\n');
|
|
34
|
+
|
|
35
|
+
/** The most the instructions may take: Claude Code cuts each server's at 2,048 characters. */
|
|
36
|
+
export const SERVER_INSTRUCTIONS_LIMIT = 2048;
|
package/src/tools/jobs.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { createdInitiativeIdsOf, createdInitiativesOf, isBacklogGenerationEmptyResult } from '@taiga/types';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import type { AgentJob } from '@taiga/types';
|
|
4
5
|
import { idInput } from './inputs.js';
|
|
5
|
-
import type { TaigaClient } from '../client.js';
|
|
6
|
+
import type { TaigaClient, WithoutTenant } from '../client.js';
|
|
6
7
|
import { READ_ONLY } from './annotations.js';
|
|
7
8
|
import { runTool, toolResult } from './errors.js';
|
|
8
9
|
import { pickFields } from './summarize.js';
|
|
@@ -13,7 +14,7 @@ import { pickFields } from './summarize.js';
|
|
|
13
14
|
* it is, and what came of it.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
type
|
|
17
|
+
type Job = WithoutTenant<AgentJob>;
|
|
17
18
|
|
|
18
19
|
/** What a job says about itself: its kind, how far it got, and why it failed. */
|
|
19
20
|
const JOB_FIELDS = [
|
|
@@ -43,7 +44,7 @@ export const SETTLED_JOB_STATUSES: ReadonlySet<string> = new Set(['completed', '
|
|
|
43
44
|
* own words and the way to decide otherwise). Null while the job runs, and
|
|
44
45
|
* for a job that is not an initiative request.
|
|
45
46
|
*/
|
|
46
|
-
function requestOutcome(job:
|
|
47
|
+
function requestOutcome(job: Job) {
|
|
47
48
|
if (job.type !== 'generate_backlog' || job.status !== 'completed') return null;
|
|
48
49
|
const created = createdInitiativesOf(job.result);
|
|
49
50
|
if (created) return { outcome: 'created', initiatives: created };
|
|
@@ -63,7 +64,7 @@ function requestOutcome(job: Row): Row | null {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
/** A job as the tools hand it over: its own fields, and for an initiative request, what came of it. */
|
|
66
|
-
export function jobReport(job:
|
|
67
|
+
export function jobReport(job: Job) {
|
|
67
68
|
const outcome = requestOutcome(job);
|
|
68
69
|
return { ...pickFields([job], JOB_FIELDS)[0], ...(outcome ? { outcome } : {}) };
|
|
69
70
|
}
|
|
@@ -85,6 +86,6 @@ export function registerJobTools(server: McpServer, client: TaigaClient) {
|
|
|
85
86
|
inputSchema: z.object({ jobId: idInput('job', 'the tool that started the job returned it') }),
|
|
86
87
|
annotations: READ_ONLY,
|
|
87
88
|
},
|
|
88
|
-
(params) => runTool(async () => toolResult(jobReport((await client.getAgentJob(params.jobId)).data
|
|
89
|
+
(params) => runTool(async () => toolResult(jobReport((await client.getAgentJob(params.jobId)).data)))
|
|
89
90
|
);
|
|
90
91
|
}
|
package/src/tools/knowledge.ts
CHANGED
|
@@ -15,8 +15,6 @@ import { page, pickFields } from './summarize.js';
|
|
|
15
15
|
* admin, which the API enforces.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
type Row = Record<string, unknown>;
|
|
19
|
-
|
|
20
18
|
const KNOWLEDGE_FIELDS = [
|
|
21
19
|
'id',
|
|
22
20
|
'factoryId',
|
|
@@ -123,7 +121,7 @@ export function registerKnowledgeTools(server: McpServer, client: TaigaClient) {
|
|
|
123
121
|
? { factoryId: params.factoryId, productId: null }
|
|
124
122
|
: { factoryId: null, productId: null };
|
|
125
123
|
const result = await client.listKnowledge({ ...scope, limit: params.limit, cursor: params.cursor });
|
|
126
|
-
return toolResult(page(pickFields(result.data
|
|
124
|
+
return toolResult(page(pickFields(result.data, KNOWLEDGE_FIELDS), result.nextCursor));
|
|
127
125
|
})
|
|
128
126
|
);
|
|
129
127
|
|
|
@@ -143,7 +141,7 @@ export function registerKnowledgeTools(server: McpServer, client: TaigaClient) {
|
|
|
143
141
|
(params) =>
|
|
144
142
|
runTool(async () => {
|
|
145
143
|
const file = await client.getKnowledge(params.knowledgeId);
|
|
146
|
-
const row = pickFields([file.data
|
|
144
|
+
const row = pickFields([file.data], KNOWLEDGE_FIELDS)[0]!;
|
|
147
145
|
if (row.status !== 'ready') {
|
|
148
146
|
return toolResult({
|
|
149
147
|
...row,
|
|
@@ -213,7 +211,7 @@ export function registerKnowledgeTools(server: McpServer, client: TaigaClient) {
|
|
|
213
211
|
// there: the flag says so, so that file is not taken for a new one
|
|
214
212
|
// (and deleted as one in a clean-up).
|
|
215
213
|
return toolResult({
|
|
216
|
-
...pickFields([result.data
|
|
214
|
+
...pickFields([result.data], KNOWLEDGE_FIELDS)[0],
|
|
217
215
|
deduplicated: result.deduplicated === true,
|
|
218
216
|
});
|
|
219
217
|
})
|
|
@@ -231,7 +229,7 @@ export function registerKnowledgeTools(server: McpServer, client: TaigaClient) {
|
|
|
231
229
|
},
|
|
232
230
|
(params) =>
|
|
233
231
|
runTool(async () =>
|
|
234
|
-
toolResult(pickFields([(await client.retryKnowledge(params.knowledgeId)).data
|
|
232
|
+
toolResult(pickFields([(await client.retryKnowledge(params.knowledgeId)).data], KNOWLEDGE_FIELDS)[0])
|
|
235
233
|
)
|
|
236
234
|
);
|
|
237
235
|
|