@taiga-ai/mcp-server 0.18.0-dev.447 → 0.18.0-dev.448

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/src/tools/wait.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/server';
2
2
  import { BUILD_RUN_TERMINAL_STATUSES, resolveAutoBuild } from '@taiga/types';
3
+ import type { BacklogItem, BacklogItemLifecycle, BuildRunWithContext } from '@taiga/types';
3
4
  import { z } from 'zod';
4
5
  import { idInput } from './inputs.js';
5
- import { TaigaApiError, type TaigaClient } from '../client.js';
6
+ import { TaigaApiError, type TaigaClient, type WithoutTenant } from '../client.js';
6
7
  import { READ_ONLY } from './annotations.js';
7
8
  import { isNetworkFailure, runTool, toolResult } from './errors.js';
8
9
  import { SETTLED_JOB_STATUSES, jobReport } from './jobs.js';
@@ -43,7 +44,11 @@ const timeoutSeconds = z
43
44
  'many stop a call at 60 seconds.'
44
45
  );
45
46
 
46
- type Row = Record<string, unknown>;
47
+ type Item = WithoutTenant<BacklogItem>;
48
+ type Run = WithoutTenant<BuildRunWithContext>;
49
+
50
+ /** The pull-request fields an initiative's latest run and a build run both carry. */
51
+ type PullRequestFields = Pick<Run, 'prUrl' | 'prMerged' | 'prState'>;
47
52
 
48
53
  /** Why a wait ended: the condition held, it can no longer hold on its own, or time ran out. */
49
54
  type Verdict = { reached: true } | { reached: false; stoppedBecause: string } | null;
@@ -91,17 +96,17 @@ function isTransient(error: unknown): boolean {
91
96
  * it, which also keeps a client that resets its timeout on progress from
92
97
  * cutting the wait short.
93
98
  */
94
- async function waitFor(
95
- read: (signal: AbortSignal) => Promise<Row>,
96
- judge: (now: Row, first: Row) => Verdict,
99
+ async function waitFor<T>(
100
+ read: (signal: AbortSignal) => Promise<T>,
101
+ judge: (now: T, first: T) => Verdict,
97
102
  timeoutMs: number,
98
103
  options: WaitOptions,
99
104
  progress: Progress
100
105
  ) {
101
106
  const started = Date.now();
102
107
  const deadline = started + timeoutMs;
103
- let first: Row | null = null;
104
- let now: Row | null = null;
108
+ let first: T | null = null;
109
+ let now: T | null = null;
105
110
  let verdict: Verdict = null;
106
111
  let failures = 0;
107
112
  for (let attempt = 0; !progress.signal.aborted; attempt += 1) {
@@ -156,7 +161,7 @@ const SETTLED_RUN: ReadonlySet<string> = new Set(BUILD_RUN_TERMINAL_STATUSES);
156
161
  * is open; the mirrored state and the merged latch say which. An unknown
157
162
  * state (null, just opened) counts as open.
158
163
  */
159
- function prIsOpen(run: Row): boolean {
164
+ function prIsOpen(run: PullRequestFields): boolean {
160
165
  return typeof run.prUrl === 'string' && run.prUrl !== '' && run.prMerged !== true && run.prState !== 'closed';
161
166
  }
162
167
 
@@ -167,11 +172,13 @@ function prIsOpen(run: Row): boolean {
167
172
  const ITEM_CONDITIONS = ['changed', 'planned', 'your_turn', 'pr_open', 'run_settled', 'done'] as const;
168
173
  type ItemCondition = (typeof ITEM_CONDITIONS)[number];
169
174
 
170
- const latestBuildOf = (item: Row) => ((item.lifecycle as Row | undefined)?.latestBuild ?? null) as Row | null;
171
- const lifecycleOf = (item: Row) => (item.lifecycle ?? {}) as Row;
175
+ // The API sends a lifecycle on every backlog item (backlogItemSchema); a
176
+ // response without one reads as no lifecycle rather than failing the wait.
177
+ const latestBuildOf = (item: Item) => item.lifecycle?.latestBuild ?? null;
178
+ const lifecycleOf = (item: Item): Partial<BacklogItemLifecycle> => item.lifecycle ?? {};
172
179
 
173
180
  /** What "changed" compares: where the item is, whose turn it is and how its latest run stands. */
174
- function itemSignature(item: Row): string {
181
+ function itemSignature(item: Item): string {
175
182
  const lifecycle = lifecycleOf(item);
176
183
  const run = latestBuildOf(item);
177
184
  return JSON.stringify([
@@ -209,7 +216,7 @@ interface GateDefault {
209
216
  * the gate cannot be resolved (no gate of its own, and the product's default
210
217
  * unreadable) the answer is unknown, and the wait says so rather than guess.
211
218
  */
212
- function personTurn(item: Row, gate: GateDefault): 'yes' | 'no' | 'unknown' {
219
+ function personTurn(item: Item, gate: GateDefault): 'yes' | 'no' | 'unknown' {
213
220
  const lifecycle = lifecycleOf(item);
214
221
  if (lifecycle.turn !== 'you') return 'no';
215
222
  if (lifecycle.cta !== 'start_build') return 'yes';
@@ -219,7 +226,7 @@ function personTurn(item: Row, gate: GateDefault): 'yes' | 'no' | 'unknown' {
219
226
  return resolveAutoBuild({ initiative: own, product: product ?? true }) ? 'no' : 'yes';
220
227
  }
221
228
 
222
- function itemHolds(until: ItemCondition, item: Row, first: Row, gate: GateDefault): boolean {
229
+ function itemHolds(until: ItemCondition, item: Item, first: Item, gate: GateDefault): boolean {
223
230
  const lifecycle = lifecycleOf(item);
224
231
  const run = latestBuildOf(item);
225
232
  switch (until) {
@@ -239,7 +246,7 @@ function itemHolds(until: ItemCondition, item: Row, first: Row, gate: GateDefaul
239
246
  }
240
247
 
241
248
  function judgeItem(until: ItemCondition, gate: GateDefault) {
242
- return (item: Row, first: Row): Verdict => {
249
+ return (item: Item, first: Item): Verdict => {
243
250
  if (itemHolds(until, item, first, gate)) return { reached: true };
244
251
  // "changed" is how a model waits for a person to act on a flagged or
245
252
  // finished initiative, so none of the stops below apply to it.
@@ -276,7 +283,7 @@ function judgeItem(until: ItemCondition, gate: GateDefault) {
276
283
  }
277
284
 
278
285
  /** The initiative as a wait reports it: where it is, whose turn, its latest run and its criteria. */
279
- function itemReport(item: Row) {
286
+ function itemReport(item: Item) {
280
287
  const run = latestBuildOf(item);
281
288
  return {
282
289
  ...pickFields(
@@ -316,11 +323,11 @@ const RUN_REPORT_FIELDS = [
316
323
  'actions',
317
324
  ] as const;
318
325
 
319
- const runSignature = (run: Row) =>
326
+ const runSignature = (run: Run) =>
320
327
  JSON.stringify([run.status, run.completedSteps, run.prUrl, run.prState, run.prMerged]);
321
328
 
322
329
  function judgeBuild(until: BuildCondition) {
323
- return (run: Row, first: Row): Verdict => {
330
+ return (run: Run, first: Run): Verdict => {
324
331
  const holds =
325
332
  until === 'changed'
326
333
  ? runSignature(run) !== runSignature(first)
@@ -367,14 +374,14 @@ export function registerWaitTools(server: McpServer, client: TaigaClient, option
367
374
  runTool(async () => {
368
375
  const gate: GateDefault = { product: undefined };
369
376
  const read = async (signal: AbortSignal) => {
370
- const item = (await client.getBacklogItem(params.itemId, signal)).data as Row;
377
+ const item = (await client.getBacklogItem(params.itemId, signal)).data;
371
378
  // The product's default, once, when this initiative has no gate of its own to answer with.
372
379
  if (gate.product === undefined && typeof item.autoBuild !== 'boolean') {
373
380
  gate.product = client.hasCapability('products:read')
374
381
  ? await client
375
382
  .getProduct(String(item.productId), signal)
376
383
  .then((p) => {
377
- const value = (p.data as { autoBuild?: unknown }).autoBuild;
384
+ const value = p.data.autoBuild;
378
385
  return typeof value === 'boolean' ? value : true;
379
386
  })
380
387
  // Not allowed, or gone: unknown for good. Anything transient
@@ -427,7 +434,7 @@ export function registerWaitTools(server: McpServer, client: TaigaClient, option
427
434
  (params, ctx) =>
428
435
  runTool(async () => {
429
436
  const result = await waitFor(
430
- async (signal) => (await client.getBuildRun(params.buildId, signal)).data as Row,
437
+ async (signal) => (await client.getBuildRun(params.buildId, signal)).data,
431
438
  judgeBuild(params.until),
432
439
  (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1000,
433
440
  options,
@@ -466,7 +473,7 @@ export function registerWaitTools(server: McpServer, client: TaigaClient, option
466
473
  (params, ctx) =>
467
474
  runTool(async () => {
468
475
  const result = await waitFor(
469
- async (signal) => (await client.getAgentJob(params.jobId, signal)).data as Row,
476
+ async (signal) => (await client.getAgentJob(params.jobId, signal)).data,
470
477
  (job) => (SETTLED_JOB_STATUSES.has(String(job.status)) ? { reached: true } : null),
471
478
  (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1000,
472
479
  options,
package/tsconfig.json CHANGED
@@ -13,11 +13,7 @@
13
13
 
14
14
  // @tsconfig/strictest leaves these two out, so every package sets them.
15
15
  "verbatimModuleSyntax": true,
16
- "erasableSyntaxOnly": true,
17
-
18
- // Preset flags this package still fails (PRO-2509). Each line is deleted
19
- // by the issue named beside it; no new line is added here.
20
- "noPropertyAccessFromIndexSignature": false // PRO-2519
16
+ "erasableSyntaxOnly": true
21
17
  },
22
18
  "include": ["src"],
23
19
  "exclude": ["node_modules", "dist"],