@taiga-ai/mcp-server 0.24.0-rc.462 → 0.25.0-dev.465

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.
@@ -279,7 +279,7 @@ export async function resolveProductRef(
279
279
  return refusal(
280
280
  apiFailureCode(error),
281
281
  error instanceof TaigaApiError && error.status === 404
282
- ? 'No product with that ID the key can see; taiga_list_products shows the ones it can.'
282
+ ? 'No product with that ID the key can see. taiga_list_products (query finds one by name) shows the ones it can; a slug or git remote URL names one too.'
283
283
  : `No product with that ID the key can see (${apiFailureText(error)}); taiga_list_products shows the ones it can.`
284
284
  );
285
285
  }
@@ -470,41 +470,23 @@ export function registerProductTools(server: McpServer, client: TaigaClient) {
470
470
  })
471
471
  );
472
472
 
473
- server.registerTool(
474
- 'taiga_list_product_documents',
475
- {
476
- title: 'List product documents',
477
- description:
478
- "List a product's documents on both timelines (type, name, version, draft state, review and update " +
479
- `times); defaultOrigin is the product's own. ${ORIGIN_MEANING.plan} ${ORIGIN_MEANING.reality} ` +
480
- 'An empty side says why; an unlisted side is null with a note. ' +
481
- 'taiga_get_product_document reads one.',
482
- inputSchema: z.object({ productId }),
483
- annotations: READ_ONLY,
484
- },
485
- (params) =>
486
- runTool(async () => {
487
- // `unlistable` is how a miss message is built; the list says the
488
- // same in its notes and a null side.
489
- const { unlistable: _unlistable, ...timelines } = await documentTimelines(client, params.productId);
490
- return toolResult(timelines);
491
- })
492
- );
493
-
494
473
  server.registerTool(
495
474
  'taiga_get_product_document',
496
475
  {
497
476
  title: 'Get product document',
498
477
  description:
499
- 'Read one product document from one timeline: plan (specification, architecture, threat_model, ' +
500
- 'technology_decisions) says what to build; reality says what the code does, so read it before changing ' +
501
- "code. Omit origin for the product's own timeline. sections: [] returns the outline (ids, sizes); then " +
502
- 'name the sections you need (unknownSections names missing ids). A miss says which documents exist.',
478
+ 'Read one product document from one timeline, or without type list both timelines (type, version, ' +
479
+ "draft and review state; defaultOrigin is the product's own; an empty side says why). plan " +
480
+ '(specification, architecture, threat_model, technology_decisions) says what to build; reality says ' +
481
+ "what the code does, so read it before changing code. Omit origin for the product's own timeline. " +
482
+ 'sections: [] returns the outline (ids, sizes); then name the sections you need (unknownSections names ' +
483
+ 'missing ids). A miss says which documents exist.',
503
484
  inputSchema: z.object({
504
485
  productId,
505
486
  type: z
506
487
  .enum(PRODUCT_DOCUMENT_TYPES)
507
- .describe('Document type -- taiga_list_product_documents shows which ones the product has'),
488
+ .optional()
489
+ .describe('Document type; omit to list what the product has on both timelines'),
508
490
  origin: originInput,
509
491
  sections: sectionsInput,
510
492
  }),
@@ -512,16 +494,25 @@ export function registerProductTools(server: McpServer, client: TaigaClient) {
512
494
  },
513
495
  (params) =>
514
496
  runTool(async () => {
497
+ // Without a type, the list of both timelines (PRO-2800): one tool where a list tool stood beside it.
498
+ if (params.type === undefined) {
499
+ if (params.sections || params.origin) {
500
+ return toolError('VALIDATION_ERROR', 'Name the document (type) to read one timeline or its sections.');
501
+ }
502
+ const { unlistable: _unlistable, ...timelines } = await documentTimelines(client, params.productId);
503
+ return toolResult(timelines);
504
+ }
505
+ const type = params.type;
515
506
  // The product's own timeline when none is asked for; the API's
516
507
  // default is plan, which is the wrong side of an imported product.
517
508
  let product: ProductSettingsView | undefined;
518
509
  if (!params.origin) product = (await client.getProduct(params.productId)).data;
519
- const origin = params.origin ?? defaultOriginFor(product, params.type);
510
+ const origin = params.origin ?? defaultOriginFor(product, type);
520
511
  try {
521
- const result = await client.getProductDocument(params.productId, params.type, origin);
512
+ const result = await client.getProductDocument(params.productId, type, origin);
522
513
  const doc = stripProvenance(result.data);
523
514
  if (!params.sections) return toolResult(doc);
524
- return sectionReadResult(doc, params.sections, `${origin} ${params.type}`);
515
+ return sectionReadResult(doc, params.sections, `${origin} ${type}`);
525
516
  } catch (error) {
526
517
  if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
527
518
  // A product the key cannot see is a product miss, not a document
@@ -530,7 +521,7 @@ export function registerProductTools(server: McpServer, client: TaigaClient) {
530
521
  // propagates, as the API failing.
531
522
  product ??= (await client.getProduct(params.productId)).data;
532
523
  const timelines = await documentTimelines(client, params.productId, product);
533
- return toolError('NOT_FOUND', describeMiss(params.type, origin, timelines, product));
524
+ return toolError('NOT_FOUND', describeMiss(type, origin, timelines, product));
534
525
  }
535
526
  })
536
527
  );
@@ -148,7 +148,7 @@ export function registerPrompts(server: McpServer, client: TaigaClient) {
148
148
  architecture.status === 'fulfilled'
149
149
  ? fenced('markdown', renderDocument(stripProvenance(architecture.value.data)).text)
150
150
  : isNotFound(architecture.reason)
151
- ? `(No published ${origin} architecture; taiga_list_product_documents shows what exists.)`
151
+ ? `(No published ${origin} architecture; taiga_get_product_document without type shows what exists.)`
152
152
  : unreadable('The architecture', architecture.reason);
153
153
  const instructionsText_ =
154
154
  instructions.status === 'fulfilled'
@@ -144,10 +144,10 @@ function nextStepFor(state: {
144
144
  if (state.missing && state.missing.length > 0) {
145
145
  return (
146
146
  `The import finished but ${named(state.missing)} ${state.missing.length === 1 ? 'is' : 'are'} not ` +
147
- 'published: taiga_list_product_documents shows them, taiga_publish_product_document publishes a draft.'
147
+ 'published: taiga_get_product_document without type shows them, taiga_publish_product_document publishes a draft.'
148
148
  );
149
149
  }
150
- return `The import is done: review the reality documents (taiga_list_product_documents), then ${finish}`;
150
+ return `The import is done: review the reality documents (taiga_get_product_document without type), then ${finish}`;
151
151
  default:
152
152
  return 'taiga_start_import derives the documents from the code and runs by itself (paid agent work: ask first).';
153
153
  }
@@ -49,6 +49,8 @@ const GITHUB_REPOSITORY_FIELDS = [
49
49
  'language',
50
50
  // A product already links it (PRO-2800); absent from an API that predates the field.
51
51
  'linked',
52
+ // Which product holds it, to a key that sees every product (PRO-2822).
53
+ 'linkedProduct',
52
54
  ] as const;
53
55
 
54
56
  const repositoryId = idInput(
@@ -72,7 +74,10 @@ export function interleave<T>(lists: T[][]): T[] {
72
74
  */
73
75
  async function holderOf(client: TaigaClient, error: unknown, productId: string) {
74
76
  if (!(error instanceof TaigaApiError) || error.status !== 409) return null;
75
- const name = /^(\S+\/\S+) is already linked to another product\.?$/.exec(error.apiMessage ?? '')?.[1];
77
+ // An API that names the holder (PRO-2807) says it itself; an older one, or a key that cannot see every product,
78
+ // says "another product", and this lists the ones the key can see.
79
+ if (error.details?.['holder']) return null;
80
+ const name = /^(\S+\/\S+) is already linked to another product\./.exec(error.apiMessage ?? '')?.[1];
76
81
  if (!name) return null;
77
82
  try {
78
83
  // The refusal names the repository only, not its host, and the same name can live on github.com and on an
@@ -162,7 +167,7 @@ export function registerRepositoryTools(server: McpServer, client: TaigaClient)
162
167
  "List the repositories the organization's GitHub connection can reach, to link one with " +
163
168
  "taiga_link_repository (it takes the row's id as linkableRepositoryId). query searches by name; account (login, owner, or " +
164
169
  'installationId) reads one account, else rows come from every account in turn. linked: true means a ' +
165
- 'product already links it (taiga_get_product with its URL says which). No connection means GitHub is not ' +
170
+ 'product already links it, and linkedProduct names it to a key that sees every product. No connection means GitHub is not ' +
166
171
  'connected yet; someone who can install the app does that once, in the web.',
167
172
  inputSchema: z.object({
168
173
  query: z.string().trim().min(1).max(256).optional().describe('Part of the repository name'),
@@ -142,7 +142,7 @@ export function registerResources(server: McpServer, client: TaigaClient) {
142
142
  const read = async (uri: URL, productId: string, type: ProductDocumentType, origin: ProductDocumentOrigin) => {
143
143
  const result = await served(
144
144
  client.getProductDocument(productId, type, origin),
145
- `No published ${origin} ${type} for this product, or the key cannot see it; taiga_list_product_documents shows what exists`
145
+ `No published ${origin} ${type} for this product, or the key cannot see it; taiga_get_product_document without type shows what exists`
146
146
  );
147
147
  return contents(uri.href, { name: productDocumentName(type), ...result.data });
148
148
  };
@@ -173,7 +173,7 @@ export function registerResources(server: McpServer, client: TaigaClient) {
173
173
  title: 'Product document',
174
174
  description:
175
175
  "A product document from the product's own timeline (reality for an imported product, plan otherwise; " +
176
- 'the documents a person writes are always on the plan side). taiga_list_product_documents shows what ' +
176
+ 'the documents a person writes are always on the plan side). taiga_get_product_document without type shows what ' +
177
177
  'exists; add ?origin=plan or ?origin=reality to read the other side. Markdown for a section map, JSON ' +
178
178
  'otherwise (the read says which).',
179
179
  },
@@ -272,7 +272,7 @@ export function sectionReadResult(
272
272
  ? {
273
273
  note: policy
274
274
  ? `${asControl} is a control of ${policy}: read it with taiga_get_document (type: ${policy}, sections: ["${asControl}"]).`
275
- : `${asControl} looks like a policy control id, which only a policy resolves: taiga_list_documents lists them.`,
275
+ : `${asControl} looks like a policy control id, which only a policy resolves: taiga_get_document without type lists them.`,
276
276
  }
277
277
  : {};
278
278
  if (known.length === 0) {