@taiga-ai/mcp-server 0.25.0-rc.472 → 0.26.0-dev.479
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/README.md +111 -0
- package/dist/index.js +322 -122
- package/package.json +15 -2
- package/scripts/build.mjs +0 -37
- package/scripts/prepare-publish.cjs +0 -49
- package/src/client.test.ts +0 -424
- package/src/client.ts +0 -976
- package/src/config.test.ts +0 -24
- package/src/config.ts +0 -28
- package/src/index.ts +0 -45
- package/src/modern.integration.test.ts +0 -153
- package/src/server.integration.test.ts +0 -9075
- package/src/server.ts +0 -18
- package/src/tools/annotations.ts +0 -38
- package/src/tools/autonomy.ts +0 -86
- package/src/tools/backlogs.ts +0 -766
- package/src/tools/builds.ts +0 -668
- package/src/tools/catalogue.test.ts +0 -170
- package/src/tools/catalogue.ts +0 -223
- package/src/tools/context.ts +0 -513
- package/src/tools/documents.ts +0 -375
- package/src/tools/errors.ts +0 -172
- package/src/tools/hints.ts +0 -99
- package/src/tools/inbox.ts +0 -546
- package/src/tools/index.ts +0 -239
- package/src/tools/inputs.ts +0 -47
- package/src/tools/instructions.ts +0 -41
- package/src/tools/jobs.ts +0 -90
- package/src/tools/knowledge.ts +0 -249
- package/src/tools/maintenance.ts +0 -540
- package/src/tools/markdown.test.ts +0 -249
- package/src/tools/markdown.ts +0 -151
- package/src/tools/observe.ts +0 -311
- package/src/tools/product-documents.ts +0 -184
- package/src/tools/products.ts +0 -529
- package/src/tools/prompts.ts +0 -265
- package/src/tools/queue.ts +0 -250
- package/src/tools/readiness.ts +0 -177
- package/src/tools/remote.test.ts +0 -44
- package/src/tools/remote.ts +0 -107
- package/src/tools/repositories.ts +0 -360
- package/src/tools/request-prompt.test.ts +0 -32
- package/src/tools/request-prompt.ts +0 -28
- package/src/tools/resources.ts +0 -217
- package/src/tools/sections.test.ts +0 -143
- package/src/tools/sections.ts +0 -287
- package/src/tools/setup.ts +0 -455
- package/src/tools/summarize.test.ts +0 -20
- package/src/tools/summarize.ts +0 -96
- package/src/tools/wait.ts +0 -490
- package/src/version.test.ts +0 -10
- package/src/version.ts +0 -14
- package/taiga-ai-mcp-server-0.25.0-rc.472.tgz +0 -0
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -9
package/src/tools/wait.ts
DELETED
|
@@ -1,490 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
-
import { BUILD_RUN_TERMINAL_STATUSES, resolveAutoBuild } from '@taiga/types';
|
|
3
|
-
import type { BacklogItem, BacklogItemLifecycle, BuildRunWithContext } from '@taiga/types';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
import { idInput } from './inputs.js';
|
|
6
|
-
import { TaigaApiError, type TaigaClient, type WithoutTenant } from '../client.js';
|
|
7
|
-
import { READ_ONLY } from './annotations.js';
|
|
8
|
-
import { isNetworkFailure, runTool, toolResult } from './errors.js';
|
|
9
|
-
import { SETTLED_JOB_STATUSES, jobReport } from './jobs.js';
|
|
10
|
-
import { criteriaCounts, pickFields } from './summarize.js';
|
|
11
|
-
import { situationOf } from './backlogs.js';
|
|
12
|
-
import { readableRow } from './builds.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Waiting without a timer. A model in an editor has no clock: to learn that
|
|
16
|
-
* a plan landed or a run opened its pull request it either reads the item
|
|
17
|
-
* again and again or hands the wait to a shell loop outside the server. These
|
|
18
|
-
* tools poll on the server instead and return as soon as the condition holds,
|
|
19
|
-
* when it can no longer hold without a person, or at the timeout, whichever
|
|
20
|
-
* is first; a timeout is an answer too (reached: false), and the model calls
|
|
21
|
-
* again to keep waiting.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
export interface WaitOptions {
|
|
25
|
-
/** How often the API is read while waiting. */
|
|
26
|
-
pollIntervalMs: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const DEFAULT_WAIT_OPTIONS: WaitOptions = { pollIntervalMs: 5_000 };
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Under the 60 seconds many clients allow a call by default (the MCP SDK's
|
|
33
|
-
* own request timeout), with room for the last read.
|
|
34
|
-
*/
|
|
35
|
-
const DEFAULT_TIMEOUT_SECONDS = 50;
|
|
36
|
-
const MAX_TIMEOUT_SECONDS = 600;
|
|
37
|
-
|
|
38
|
-
const timeoutSeconds = z
|
|
39
|
-
.number()
|
|
40
|
-
.int()
|
|
41
|
-
.min(1)
|
|
42
|
-
.max(MAX_TIMEOUT_SECONDS)
|
|
43
|
-
.optional()
|
|
44
|
-
.describe(
|
|
45
|
-
`How long to wait at most (default ${DEFAULT_TIMEOUT_SECONDS}). Keep it under your client's tool-call limit; ` +
|
|
46
|
-
'many stop a call at 60 seconds.'
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
type Item = WithoutTenant<BacklogItem>;
|
|
50
|
-
type Run = WithoutTenant<BuildRunWithContext>;
|
|
51
|
-
|
|
52
|
-
/** The pull-request fields an initiative's latest run and a build run both carry. */
|
|
53
|
-
type PullRequestFields = Pick<Run, 'prUrl' | 'prMerged' | 'prState'>;
|
|
54
|
-
|
|
55
|
-
/** Why a wait ended: the condition held, it can no longer hold on its own, or time ran out. */
|
|
56
|
-
type Verdict = { reached: true } | { reached: false; stoppedBecause: string } | null;
|
|
57
|
-
|
|
58
|
-
interface Progress {
|
|
59
|
-
progressToken: string | number | undefined;
|
|
60
|
-
notify: (notification: {
|
|
61
|
-
method: 'notifications/progress';
|
|
62
|
-
params: { progressToken: string | number; progress: number; total: number; message: string };
|
|
63
|
-
}) => Promise<void>;
|
|
64
|
-
signal: AbortSignal;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function pause(ms: number, signal: AbortSignal): Promise<void> {
|
|
68
|
-
return new Promise((resolve) => {
|
|
69
|
-
if (signal.aborted) return resolve();
|
|
70
|
-
const timer = setTimeout(done, ms);
|
|
71
|
-
function done() {
|
|
72
|
-
clearTimeout(timer);
|
|
73
|
-
signal.removeEventListener('abort', done);
|
|
74
|
-
resolve();
|
|
75
|
-
}
|
|
76
|
-
signal.addEventListener('abort', done, { once: true });
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Failed reads in a row a wait rides out before it gives up and says why. */
|
|
81
|
-
const TRANSIENT_FAILURES_TOLERATED = 3;
|
|
82
|
-
|
|
83
|
-
/** A failure the next read may not hit: rate limited, the API erroring or unreachable. Not a refusal or a miss. */
|
|
84
|
-
function isTransient(error: unknown): boolean {
|
|
85
|
-
if (isNetworkFailure(error)) return true;
|
|
86
|
-
return error instanceof TaigaApiError && (error.status === 429 || error.status >= 500);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Read until `judge` gives a verdict or the time is up. Every read is bound
|
|
91
|
-
* by what is left of the timeout and by the call's cancellation, so a slow
|
|
92
|
-
* API cannot hold the wait past either: a read cut short ends the wait with
|
|
93
|
-
* the last state seen (none, when not even the first read came back). A
|
|
94
|
-
* read that fails transiently is ridden out, a few in a row and the first
|
|
95
|
-
* read included, so one 502 does not throw away minutes of waiting; a
|
|
96
|
-
* refusal fails as it comes. Each
|
|
97
|
-
* read that leaves the wait open reports progress when the client asked for
|
|
98
|
-
* it, which also keeps a client that resets its timeout on progress from
|
|
99
|
-
* cutting the wait short.
|
|
100
|
-
*/
|
|
101
|
-
async function waitFor<T>(
|
|
102
|
-
read: (signal: AbortSignal) => Promise<T>,
|
|
103
|
-
judge: (now: T, first: T) => Verdict,
|
|
104
|
-
timeoutMs: number,
|
|
105
|
-
options: WaitOptions,
|
|
106
|
-
progress: Progress
|
|
107
|
-
) {
|
|
108
|
-
const started = Date.now();
|
|
109
|
-
const deadline = started + timeoutMs;
|
|
110
|
-
let first: T | null = null;
|
|
111
|
-
let now: T | null = null;
|
|
112
|
-
let verdict: Verdict = null;
|
|
113
|
-
let failures = 0;
|
|
114
|
-
for (let attempt = 0; !progress.signal.aborted; attempt += 1) {
|
|
115
|
-
if (attempt > 0) {
|
|
116
|
-
const left = deadline - Date.now();
|
|
117
|
-
if (left <= 0) break;
|
|
118
|
-
await pause(Math.min(options.pollIntervalMs, left), progress.signal);
|
|
119
|
-
}
|
|
120
|
-
const remaining = deadline - Date.now();
|
|
121
|
-
if (progress.signal.aborted || remaining <= 0) break;
|
|
122
|
-
const bound = AbortSignal.timeout(remaining);
|
|
123
|
-
try {
|
|
124
|
-
now = await read(AbortSignal.any([progress.signal, bound]));
|
|
125
|
-
first ??= now;
|
|
126
|
-
failures = 0;
|
|
127
|
-
} catch (error) {
|
|
128
|
-
// Cut short by the deadline or the cancellation: the wait is over, not failed.
|
|
129
|
-
if (bound.aborted || progress.signal.aborted) break;
|
|
130
|
-
failures += 1;
|
|
131
|
-
if (!isTransient(error) || failures >= TRANSIENT_FAILURES_TOLERATED) throw error;
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
verdict = judge(now, first);
|
|
135
|
-
if (verdict) break;
|
|
136
|
-
if (attempt > 0 && progress.progressToken !== undefined) {
|
|
137
|
-
// Best effort: a progress line the client cannot take does not end the wait.
|
|
138
|
-
await progress
|
|
139
|
-
.notify({
|
|
140
|
-
method: 'notifications/progress',
|
|
141
|
-
params: {
|
|
142
|
-
progressToken: progress.progressToken,
|
|
143
|
-
// Milliseconds, so every notification is larger than the last, as the protocol asks.
|
|
144
|
-
progress: Date.now() - started,
|
|
145
|
-
total: timeoutMs,
|
|
146
|
-
message: 'Still waiting',
|
|
147
|
-
},
|
|
148
|
-
})
|
|
149
|
-
.catch(() => undefined);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const waitedSeconds = Math.round((Date.now() - started) / 1000);
|
|
153
|
-
if (verdict) return { ...verdict, waitedSeconds, now };
|
|
154
|
-
const stoppedBecause = progress.signal.aborted ? 'cancelled' : 'timeout';
|
|
155
|
-
return { reached: false, stoppedBecause, waitedSeconds, now };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const SETTLED_RUN: ReadonlySet<string> = new Set(BUILD_RUN_TERMINAL_STATUSES);
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* A pull request that is open now. A run keeps its prUrl after the pull
|
|
162
|
-
* request merges or closes, so the URL alone says one existed, not that it
|
|
163
|
-
* is open; the mirrored state and the merged latch say which. An unknown
|
|
164
|
-
* state (null, just opened) counts as open.
|
|
165
|
-
*/
|
|
166
|
-
function prIsOpen(run: PullRequestFields): boolean {
|
|
167
|
-
return typeof run.prUrl === 'string' && run.prUrl !== '' && run.prMerged !== true && run.prState !== 'closed';
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ---------------------------------------------------------------------------
|
|
171
|
-
// Initiatives
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
|
|
174
|
-
const ITEM_CONDITIONS = ['changed', 'planned', 'your_turn', 'pr_open', 'run_settled', 'done'] as const;
|
|
175
|
-
type ItemCondition = (typeof ITEM_CONDITIONS)[number];
|
|
176
|
-
|
|
177
|
-
// The API sends a lifecycle on every backlog item (backlogItemSchema); a
|
|
178
|
-
// response without one reads as no lifecycle rather than failing the wait.
|
|
179
|
-
const latestBuildOf = (item: Item) => item.lifecycle?.latestBuild ?? null;
|
|
180
|
-
const lifecycleOf = (item: Item): Partial<BacklogItemLifecycle> => item.lifecycle ?? {};
|
|
181
|
-
|
|
182
|
-
/** What "changed" compares: where the item is, whose turn it is and how its latest run stands. */
|
|
183
|
-
function itemSignature(item: Item): string {
|
|
184
|
-
const lifecycle = lifecycleOf(item);
|
|
185
|
-
const run = latestBuildOf(item);
|
|
186
|
-
return JSON.stringify([
|
|
187
|
-
item.state,
|
|
188
|
-
item.planningStatus,
|
|
189
|
-
item.needsHumanCause,
|
|
190
|
-
// The review gate decides whose turn a landed plan is, while the lifecycle reads the same either way.
|
|
191
|
-
item.autoBuild,
|
|
192
|
-
lifecycle.stage,
|
|
193
|
-
lifecycle.turn,
|
|
194
|
-
lifecycle.cta,
|
|
195
|
-
run?.id,
|
|
196
|
-
run?.status,
|
|
197
|
-
run?.completedSteps,
|
|
198
|
-
run?.prUrl,
|
|
199
|
-
run?.prState,
|
|
200
|
-
run?.prMerged,
|
|
201
|
-
]);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* The product's auto-build default, as far as the wait knows it: read once,
|
|
206
|
-
* and only when an initiative with no gate of its own needs it. Null when the
|
|
207
|
-
* key may not read it (or the product is gone); undefined until read.
|
|
208
|
-
*/
|
|
209
|
-
interface GateDefault {
|
|
210
|
-
product: boolean | null | undefined;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Whether the initiative waits on a person now: yes, no, or unknown. The
|
|
215
|
-
* lifecycle says `turn: 'you'` for every landed plan (`start_build`), but a
|
|
216
|
-
* plan whose review gate is open is waiting for the Build slot, not for
|
|
217
|
-
* anyone: the queue keeps such an entry active and starts it on its own. When
|
|
218
|
-
* the gate cannot be resolved (no gate of its own, and the product's default
|
|
219
|
-
* unreadable) the answer is unknown, and the wait says so rather than guess.
|
|
220
|
-
*/
|
|
221
|
-
function personTurn(item: Item, gate: GateDefault): 'yes' | 'no' | 'unknown' {
|
|
222
|
-
const lifecycle = lifecycleOf(item);
|
|
223
|
-
if (lifecycle.turn !== 'you') return 'no';
|
|
224
|
-
if (lifecycle.cta !== 'start_build') return 'yes';
|
|
225
|
-
const own = typeof item.autoBuild === 'boolean' ? item.autoBuild : null;
|
|
226
|
-
const product = typeof gate.product === 'boolean' ? gate.product : null;
|
|
227
|
-
if (own === null && product === null) return 'unknown';
|
|
228
|
-
return resolveAutoBuild({ initiative: own, product: product ?? true }) ? 'no' : 'yes';
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function itemHolds(until: ItemCondition, item: Item, first: Item, gate: GateDefault): boolean {
|
|
232
|
-
const lifecycle = lifecycleOf(item);
|
|
233
|
-
const run = latestBuildOf(item);
|
|
234
|
-
switch (until) {
|
|
235
|
-
case 'changed':
|
|
236
|
-
return itemSignature(item) !== itemSignature(first);
|
|
237
|
-
case 'planned':
|
|
238
|
-
return item.planningStatus === 'planned';
|
|
239
|
-
case 'your_turn':
|
|
240
|
-
return personTurn(item, gate) === 'yes';
|
|
241
|
-
case 'pr_open':
|
|
242
|
-
return run !== null && prIsOpen(run);
|
|
243
|
-
case 'run_settled':
|
|
244
|
-
return run !== null && SETTLED_RUN.has(String(run.status));
|
|
245
|
-
case 'done':
|
|
246
|
-
return lifecycle.stage === 'done';
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function judgeItem(until: ItemCondition, gate: GateDefault) {
|
|
251
|
-
return (item: Item, first: Item): Verdict => {
|
|
252
|
-
if (itemHolds(until, item, first, gate)) return { reached: true };
|
|
253
|
-
// "changed" is how a model waits for a person to act on a flagged or
|
|
254
|
-
// finished initiative, so none of the stops below apply to it.
|
|
255
|
-
if (until === 'changed') return null;
|
|
256
|
-
// Past these the condition cannot come true without a person acting.
|
|
257
|
-
if (typeof item.needsHumanCause === 'string') {
|
|
258
|
-
return { reached: false, stoppedBecause: `needs a person: ${item.needsHumanCause}` };
|
|
259
|
-
}
|
|
260
|
-
// The ordinary gates set no flag: a plan waiting for its verdict, a pull
|
|
261
|
-
// request waiting for a review. The lifecycle says whose turn it is.
|
|
262
|
-
const lifecycle = lifecycleOf(item);
|
|
263
|
-
const turn = personTurn(item, gate);
|
|
264
|
-
if (until !== 'your_turn' && turn === 'yes') {
|
|
265
|
-
return { reached: false, stoppedBecause: `waits on a person: ${String(lifecycle.cta ?? lifecycle.stage)}` };
|
|
266
|
-
}
|
|
267
|
-
if (turn === 'unknown') {
|
|
268
|
-
return {
|
|
269
|
-
reached: false,
|
|
270
|
-
stoppedBecause:
|
|
271
|
-
"the plan has landed, and whether it waits for a review is unknown: the product's default could not be read " +
|
|
272
|
-
'(it needs products:read)',
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
if (until === 'planned' && item.planningStatus === 'failed') {
|
|
276
|
-
return { reached: false, stoppedBecause: 'planning failed' };
|
|
277
|
-
}
|
|
278
|
-
const stage = lifecycleOf(item).stage;
|
|
279
|
-
// Canceled ends every wait, done included: it is not the done asked for.
|
|
280
|
-
if (stage === 'canceled' || (until !== 'done' && stage === 'done')) {
|
|
281
|
-
return { reached: false, stoppedBecause: `the initiative is ${String(stage)}` };
|
|
282
|
-
}
|
|
283
|
-
return null;
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/** The initiative as a wait reports it: where it is, whose turn, its latest run and its criteria. */
|
|
288
|
-
function itemReport(item: Item) {
|
|
289
|
-
const run = latestBuildOf(item);
|
|
290
|
-
const situation = situationOf(item);
|
|
291
|
-
return {
|
|
292
|
-
...(situation ? { situation } : {}),
|
|
293
|
-
...pickFields(
|
|
294
|
-
[item],
|
|
295
|
-
['id', 'title', 'state', 'planningStatus', 'needsHumanCause', 'needsHumanDetail', 'autoBuild']
|
|
296
|
-
)[0],
|
|
297
|
-
lifecycle: pickFields([lifecycleOf(item)], ['turn', 'stage', 'cta', 'planFailureCause'])[0],
|
|
298
|
-
latestBuild: run
|
|
299
|
-
? pickFields(
|
|
300
|
-
[run],
|
|
301
|
-
['id', 'status', 'completedSteps', 'totalSteps', 'prUrl', 'prState', 'prMerged', 'actions']
|
|
302
|
-
)[0]
|
|
303
|
-
: null,
|
|
304
|
-
criteria: criteriaCounts(item),
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// ---------------------------------------------------------------------------
|
|
309
|
-
// Runs
|
|
310
|
-
// ---------------------------------------------------------------------------
|
|
311
|
-
|
|
312
|
-
const BUILD_CONDITIONS = ['changed', 'step_done', 'pr_open', 'settled'] as const;
|
|
313
|
-
type BuildCondition = (typeof BUILD_CONDITIONS)[number];
|
|
314
|
-
|
|
315
|
-
const RUN_REPORT_FIELDS = [
|
|
316
|
-
'id',
|
|
317
|
-
'backlogItemId',
|
|
318
|
-
'targetTitle',
|
|
319
|
-
'status',
|
|
320
|
-
'failedReason',
|
|
321
|
-
'completedSteps',
|
|
322
|
-
'totalSteps',
|
|
323
|
-
'prUrl',
|
|
324
|
-
'prState',
|
|
325
|
-
'prDraft',
|
|
326
|
-
'prMerged',
|
|
327
|
-
'reviewEscalation',
|
|
328
|
-
'error',
|
|
329
|
-
'prNumber',
|
|
330
|
-
'baseBranch',
|
|
331
|
-
'actions',
|
|
332
|
-
] as const;
|
|
333
|
-
|
|
334
|
-
const runSignature = (run: Run) =>
|
|
335
|
-
JSON.stringify([run.status, run.completedSteps, run.prUrl, run.prState, run.prMerged]);
|
|
336
|
-
|
|
337
|
-
function judgeBuild(until: BuildCondition) {
|
|
338
|
-
return (run: Run, first: Run): Verdict => {
|
|
339
|
-
const holds =
|
|
340
|
-
until === 'changed'
|
|
341
|
-
? runSignature(run) !== runSignature(first)
|
|
342
|
-
: until === 'step_done'
|
|
343
|
-
? Number(run.completedSteps ?? 0) > Number(first.completedSteps ?? 0)
|
|
344
|
-
: until === 'pr_open'
|
|
345
|
-
? prIsOpen(run)
|
|
346
|
-
: SETTLED_RUN.has(String(run.status));
|
|
347
|
-
if (holds) return { reached: true };
|
|
348
|
-
// "changed" waits out a settled run too: a person resuming it is the change.
|
|
349
|
-
if (until !== 'changed' && SETTLED_RUN.has(String(run.status))) {
|
|
350
|
-
return { reached: false, stoppedBecause: `the run is ${String(run.status)}` };
|
|
351
|
-
}
|
|
352
|
-
return null;
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
export function registerWaitTools(server: McpServer, client: TaigaClient, options: WaitOptions) {
|
|
357
|
-
if (client.hasCapability('backlog:read')) {
|
|
358
|
-
server.registerTool(
|
|
359
|
-
'taiga_wait_for_initiative',
|
|
360
|
-
{
|
|
361
|
-
title: 'Wait for an initiative',
|
|
362
|
-
description:
|
|
363
|
-
'Wait until an initiative reaches a condition instead of polling: changed, planned, your_turn (waits on ' +
|
|
364
|
-
'a person), pr_open (open now; merged or closed does not count), run_settled (latest run stopped moving) ' +
|
|
365
|
-
'or done. Returns reached: true when it ' +
|
|
366
|
-
'holds; early with stoppedBecause when only a person can move it (a flag, failed plan, plan or pull ' +
|
|
367
|
-
'request awaiting review, a cancel); or reached: false at the timeout: call again. A condition already ' +
|
|
368
|
-
'true answers at once. Planning takes minutes, a run up to an hour.',
|
|
369
|
-
inputSchema: z.object({
|
|
370
|
-
initiativeId: idInput('initiative', 'taiga_list_initiatives (query finds one by title) gives it'),
|
|
371
|
-
until: z.enum(ITEM_CONDITIONS).describe('What to wait for'),
|
|
372
|
-
timeoutSeconds,
|
|
373
|
-
}),
|
|
374
|
-
annotations: READ_ONLY,
|
|
375
|
-
},
|
|
376
|
-
(params, ctx) =>
|
|
377
|
-
runTool(async () => {
|
|
378
|
-
const gate: GateDefault = { product: undefined };
|
|
379
|
-
const read = async (signal: AbortSignal) => {
|
|
380
|
-
const item = (await client.getBacklogItem(params.initiativeId, signal)).data;
|
|
381
|
-
// The product's default, once, when this initiative has no gate of its own to answer with.
|
|
382
|
-
if (gate.product === undefined && typeof item.autoBuild !== 'boolean') {
|
|
383
|
-
gate.product = client.hasCapability('products:read')
|
|
384
|
-
? await client
|
|
385
|
-
.getProduct(String(item.productId), signal)
|
|
386
|
-
.then((p) => {
|
|
387
|
-
const value = p.data.autoBuild;
|
|
388
|
-
return typeof value === 'boolean' ? value : true;
|
|
389
|
-
})
|
|
390
|
-
// Not allowed, or gone: unknown for good. Anything transient
|
|
391
|
-
// propagates, so the wait rides it out and reads again.
|
|
392
|
-
.catch((error: unknown) => {
|
|
393
|
-
if (error instanceof TaigaApiError && (error.status === 403 || error.status === 404)) return null;
|
|
394
|
-
throw error;
|
|
395
|
-
})
|
|
396
|
-
: null;
|
|
397
|
-
}
|
|
398
|
-
return item;
|
|
399
|
-
};
|
|
400
|
-
const result = await waitFor(
|
|
401
|
-
read,
|
|
402
|
-
judgeItem(params.until, gate),
|
|
403
|
-
(params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1000,
|
|
404
|
-
options,
|
|
405
|
-
{
|
|
406
|
-
progressToken: ctx.mcpReq._meta?.progressToken,
|
|
407
|
-
notify: ctx.mcpReq.notify,
|
|
408
|
-
signal: ctx.mcpReq.signal,
|
|
409
|
-
}
|
|
410
|
-
);
|
|
411
|
-
const { now, ...outcome } = result;
|
|
412
|
-
return toolResult({ until: params.until, ...outcome, item: now ? itemReport(now) : null });
|
|
413
|
-
})
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (client.hasCapability('products:read')) {
|
|
418
|
-
server.registerTool(
|
|
419
|
-
'taiga_wait_for_run',
|
|
420
|
-
{
|
|
421
|
-
title: 'Wait for a run',
|
|
422
|
-
description:
|
|
423
|
-
'Wait until a run reaches a condition: changed, step_done (one more step finished), pr_open (open now) ' +
|
|
424
|
-
'or settled (in review, completed, failed, stopped). Returns reached: true when it holds; early with ' +
|
|
425
|
-
'stoppedBecause if the run settled without it; or reached: false at the timeout: call again. A settled ' +
|
|
426
|
-
'run answers settled at once; run is null if the first read timed out.',
|
|
427
|
-
inputSchema: z.object({
|
|
428
|
-
runId: idInput('run', 'taiga_list_runs gives it'),
|
|
429
|
-
until: z.enum(BUILD_CONDITIONS).describe('What to wait for'),
|
|
430
|
-
timeoutSeconds,
|
|
431
|
-
}),
|
|
432
|
-
annotations: READ_ONLY,
|
|
433
|
-
},
|
|
434
|
-
(params, ctx) =>
|
|
435
|
-
runTool(async () => {
|
|
436
|
-
const result = await waitFor(
|
|
437
|
-
async (signal) => (await client.getBuildRun(params.runId, signal)).data,
|
|
438
|
-
judgeBuild(params.until),
|
|
439
|
-
(params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1000,
|
|
440
|
-
options,
|
|
441
|
-
{
|
|
442
|
-
progressToken: ctx.mcpReq._meta?.progressToken,
|
|
443
|
-
notify: ctx.mcpReq.notify,
|
|
444
|
-
signal: ctx.mcpReq.signal,
|
|
445
|
-
}
|
|
446
|
-
);
|
|
447
|
-
const { now, ...outcome } = result;
|
|
448
|
-
return toolResult({
|
|
449
|
-
until: params.until,
|
|
450
|
-
...outcome,
|
|
451
|
-
run: now ? readableRow(pickFields([now], RUN_REPORT_FIELDS)[0]!) : null,
|
|
452
|
-
});
|
|
453
|
-
})
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (client.hasCapability('products:read')) {
|
|
458
|
-
server.registerTool(
|
|
459
|
-
'taiga_wait_for_job',
|
|
460
|
-
{
|
|
461
|
-
title: 'Wait for an agent job',
|
|
462
|
-
description:
|
|
463
|
-
'Wait until an agent job finishes (completed, failed or cancelled). Returns the job as taiga_get_job ' +
|
|
464
|
-
"reads it, with an initiative request's outcome, and reached: true; or reached: false at the timeout: " +
|
|
465
|
-
'call again to keep waiting. Most jobs take a few minutes.',
|
|
466
|
-
inputSchema: z.object({
|
|
467
|
-
jobId: idInput('job', 'the tool that started the job returned it'),
|
|
468
|
-
timeoutSeconds,
|
|
469
|
-
}),
|
|
470
|
-
annotations: READ_ONLY,
|
|
471
|
-
},
|
|
472
|
-
(params, ctx) =>
|
|
473
|
-
runTool(async () => {
|
|
474
|
-
const result = await waitFor(
|
|
475
|
-
async (signal) => (await client.getAgentJob(params.jobId, signal)).data,
|
|
476
|
-
(job) => (SETTLED_JOB_STATUSES.has(String(job.status)) ? { reached: true } : null),
|
|
477
|
-
(params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1000,
|
|
478
|
-
options,
|
|
479
|
-
{
|
|
480
|
-
progressToken: ctx.mcpReq._meta?.progressToken,
|
|
481
|
-
notify: ctx.mcpReq.notify,
|
|
482
|
-
signal: ctx.mcpReq.signal,
|
|
483
|
-
}
|
|
484
|
-
);
|
|
485
|
-
const { now, ...outcome } = result;
|
|
486
|
-
return toolResult({ ...outcome, job: now ? jobReport(now) : null });
|
|
487
|
-
})
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
}
|
package/src/version.test.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { SERVER_VERSION } from './version.js';
|
|
4
|
-
|
|
5
|
-
describe('SERVER_VERSION', () => {
|
|
6
|
-
it('is the version in package.json, not a hand-kept copy', () => {
|
|
7
|
-
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string };
|
|
8
|
-
expect(SERVER_VERSION).toBe(pkg.version);
|
|
9
|
-
});
|
|
10
|
-
});
|
package/src/version.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { createRequire } from 'node:module';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* This package's own version, read from package.json at runtime.
|
|
5
|
-
*
|
|
6
|
-
* Read at runtime rather than inlined at build: the publish workflow sets the
|
|
7
|
-
* final version (`0.3.0-dev.<run>`, `0.3.0-rc.<run>`) after the bundle is
|
|
8
|
-
* built, so only a runtime read reports what was actually published. Both
|
|
9
|
-
* `src/` and the bundled `dist/index.js` sit one level under the package root,
|
|
10
|
-
* and npm always ships package.json.
|
|
11
|
-
*/
|
|
12
|
-
const pkg = createRequire(import.meta.url)('../package.json') as { version: string };
|
|
13
|
-
|
|
14
|
-
export const SERVER_VERSION: string = pkg.version;
|
|
Binary file
|
package/tsconfig.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "@tsconfig/strictest/tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"target": "ES2022",
|
|
5
|
-
"module": "ESNext",
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"declaration": true,
|
|
8
|
-
"declarationMap": true,
|
|
9
|
-
"outDir": "./dist",
|
|
10
|
-
"rootDir": "./src",
|
|
11
|
-
"resolveJsonModule": true,
|
|
12
|
-
"types": ["node"],
|
|
13
|
-
|
|
14
|
-
// @tsconfig/strictest leaves these two out, so every package sets them.
|
|
15
|
-
"verbatimModuleSyntax": true,
|
|
16
|
-
"erasableSyntaxOnly": true
|
|
17
|
-
},
|
|
18
|
-
"include": ["src"],
|
|
19
|
-
"exclude": ["node_modules", "dist"],
|
|
20
|
-
"references": [{ "path": "../../packages/types" }]
|
|
21
|
-
}
|