@taiga-ai/mcp-server 0.26.0-dev.502 → 0.26.0-dev.505

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.
Files changed (2) hide show
  1. package/dist/index.js +91 -42
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2630,7 +2630,7 @@ function importStatusOf(product) {
2630
2630
  /** Why a side of the timeline is empty, for a list or a miss that shows one. */
2631
2631
  function emptyOriginNote(origin, product) {
2632
2632
  const imported = isImportedProduct(product);
2633
- if (origin === "plan") return imported ? `This product was imported, so no discovery chain was generated for it: its derived documents are on the reality side. Only the documents a person writes (${PLAN_ONLY_DOCUMENT_TYPES.map(productDocumentName).join(", ")}) would appear here.` : "No plan documents are published yet: they are written during discovery in Taiga.";
2633
+ if (origin === "plan") return imported ? `This product is set up from its code (an import), so no discovery chain is generated for it: its derived documents are on the reality side. Only the documents a person writes (${PLAN_ONLY_DOCUMENT_TYPES.map(productDocumentName).join(", ")}) would appear here.` : "No plan documents are published yet: they are written during discovery in Taiga.";
2634
2634
  if (!imported) return "No reality documents yet: they are derived from the code once the first initiative is done (a merged pull request marks it done) or the repository is pushed to. Until then read the plan side.";
2635
2635
  switch (importStatusOf(product)) {
2636
2636
  case "pending": return "No reality documents yet: the import has not started. taiga_start_import starts it once a repository is linked; taiga_get_product's readiness says whether one is.";
@@ -2700,7 +2700,7 @@ const BY_ARGUMENT = {
2700
2700
  /** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
2701
2701
  const BY_TOOL = {
2702
2702
  taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
2703
- taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_initiative's situation says whether one is being planned or failed, and taiga_plan_again asks for a new one." : ITEM_ID_HINT
2703
+ taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_initiative's situation says whether one is being planned or failed, and names the call that asks for one (taiga_queue_initiative off the line, taiga_plan_again for a failed plan)." : ITEM_ID_HINT
2704
2704
  };
2705
2705
  /**
2706
2706
  * What the API calls the thing each ID names, as its 404 says it ("Environment
@@ -3339,7 +3339,7 @@ const SEARCH_PAGE_LIMIT = 10;
3339
3339
  * the whole list to find its ID. `more` is true when it stopped before the end.
3340
3340
  */
3341
3341
  async function searchPages(fetchPage, query, text, limit, keep = () => true, nameOf) {
3342
- const needle = query.toLowerCase();
3342
+ const needle = searchable(query);
3343
3343
  const rows = [];
3344
3344
  const names = [];
3345
3345
  let cursor;
@@ -3349,7 +3349,7 @@ async function searchPages(fetchPage, query, text, limit, keep = () => true, nam
3349
3349
  for (const [at, row] of page.entries()) {
3350
3350
  if (!keep(row)) continue;
3351
3351
  if (nameOf) names.push(nameOf(row));
3352
- if (!text(row).some((value) => typeof value === "string" && value.toLowerCase().includes(needle))) continue;
3352
+ if (!text(row).some((value) => typeof value === "string" && searchable(value).includes(needle))) continue;
3353
3353
  rows.push(row);
3354
3354
  if (rows.length === limit) return {
3355
3355
  rows,
@@ -3372,12 +3372,18 @@ async function searchPages(fetchPage, query, text, limit, keep = () => true, nam
3372
3372
  };
3373
3373
  }
3374
3374
  /**
3375
+ * Text as a search compares it: lower case, punctuation as spaces, so "sign in" finds "Staff Sign-In" (PRO-2823).
3376
+ */
3377
+ function searchable(text) {
3378
+ return text.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim() || text.toLowerCase();
3379
+ }
3380
+ /**
3375
3381
  * Up to three names close to a query that matched none: a typo of the name or
3376
3382
  * of one of its words, within a third of the query's length ("tiaga", "rally"
3377
3383
  * for ref-rallly), or a shared word of three letters or more. Closest first.
3378
3384
  */
3379
3385
  function closestNames(query, names) {
3380
- const words = (text) => text.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length >= 3);
3386
+ const words = (text) => searchable(text).split(" ").filter((word) => word.length >= 3);
3381
3387
  const wanted = new Set(words(query));
3382
3388
  const ranked = names.map((name) => ({
3383
3389
  name,
@@ -4258,15 +4264,28 @@ function runOutcome(run) {
4258
4264
  }[disposition] ?? status
4259
4265
  };
4260
4266
  }
4267
+ /** Words a sentence does not end at: "e.g." and the like, and a lone initial. */
4268
+ const ABBREVIATION = /(?:\b(?:e\.g|i\.e|etc|vs|cf|incl|approx)|(?:^|\s)[A-Za-z])\.$/i;
4269
+ /** A finding's first sentence, capped: it ends at . ! or ? before white space, but not at an abbreviation. */
4270
+ function firstSentence$1(text) {
4271
+ let sentence = text;
4272
+ for (const match of text.matchAll(/[.!?](?=\s|$)/g)) {
4273
+ const end = match.index + 1;
4274
+ if (ABBREVIATION.test(text.slice(0, end))) continue;
4275
+ sentence = text.slice(0, end);
4276
+ break;
4277
+ }
4278
+ return sentence.length > 160 ? `${sentence.slice(0, 157)}...` : sentence;
4279
+ }
4261
4280
  /**
4262
4281
  * What hardening left blocking, in a sentence: the findings the run names (PRO-2817), or, for a run reported before
4263
4282
  * it recorded them, how many.
4264
4283
  */
4265
4284
  function hardeningSaid(blocking, named) {
4266
4285
  const count = `Hardening ended with ${blocking} blocking finding${blocking === 1 ? "" : "s"} it could not fix`;
4267
- if (named.length === 0) return `${count}; the run records how many, not which.`;
4286
+ if (named.length === 0) return `${count}; this run predates recording which.`;
4268
4287
  const shown = named.slice(0, 3).map((finding) => {
4269
- const message = String(finding["message"]);
4288
+ const message = firstSentence$1(String(finding["message"]));
4270
4289
  const where = finding["line"] ? `${String(finding["path"])}:${String(finding["line"])}` : String(finding["path"]);
4271
4290
  return `${message} (${String(finding["tool"])}, ${where})`;
4272
4291
  });
@@ -5101,7 +5120,7 @@ function registerBacklogReads(server, client) {
5101
5120
  }
5102
5121
  if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", "A title search answers one page; narrow the title, or leave query out to page through a column.");
5103
5122
  let closedSeen = 0;
5104
- const needle = query.toLowerCase();
5123
+ const needle = searchable(query);
5105
5124
  const found = await searchPages((cursor) => client.listBacklogItems(id, {
5106
5125
  ...apiFilters,
5107
5126
  ...column === "open" ? { open: void 0 } : {},
@@ -5109,14 +5128,15 @@ function registerBacklogReads(server, client) {
5109
5128
  cursor
5110
5129
  }), query, (row) => [row.title], params.limit ?? 25, (row) => {
5111
5130
  if (column !== "open" || !["done", "canceled"].includes(String(row.state))) return true;
5112
- if (String(row.title).toLowerCase().includes(needle)) closedSeen += 1;
5131
+ if (searchable(String(row.title)).includes(needle)) closedSeen += 1;
5113
5132
  return false;
5114
- });
5133
+ }, (row) => String(row.title));
5115
5134
  const hidden = column === "open" && !found.more ? closedSeen : 0;
5116
5135
  return toolResult({
5117
5136
  ...page(situatedRows(found.rows), null),
5118
5137
  ...hidden > 0 ? { closedMatches: `${hidden} done or canceled initiative${hidden === 1 ? " also matches" : "s also match"}; column: 'all' lists them.` } : {},
5119
- ...found.more ? { more: "There may be more than the limit carries: narrow the title, or pick one column." } : {}
5138
+ ...found.more ? { more: "There may be more than the limit carries: narrow the title, or pick one column." } : {},
5139
+ ...hidden === 0 ? nearNote(found.near) : {}
5120
5140
  });
5121
5141
  }));
5122
5142
  server.registerTool("taiga_get_initiative", {
@@ -5586,7 +5606,7 @@ function registerMaintenanceTools(server, client) {
5586
5606
  const summary = standingSummary(standing);
5587
5607
  const assessment = standing.assessment;
5588
5608
  const assessedAt = typeof assessment?.assessedAt === "string" ? assessment.assessedAt : null;
5589
- const note = assessment?.state === "never" ? "Never assessed: the documents have not been checked against the policies yet, so nothing here is an all-clear. An assessment runs when the reality documents are derived or refreshed (taiga_refresh_reality_documents, paid work: ask first)." : summary.controlsMet + summary.controlsNotApplicable === 0 && assessedAt ? `No control is recorded as met or not applicable: the last assessment (${assessedAt}) either ran before assessments recorded those verdicts, or found none. Until the next one, only violations are known; the rest are not assessed, which is no pass.` : null;
5609
+ const note = assessment?.state === "never" ? "Never assessed: the documents have not been checked against the policies yet, so nothing here is an all-clear. " + (standing.canAssess === false && standing.hasPolicies !== false ? "It has no reality documents to assess yet: an import derives them (taiga_start_import for a product set up from its code, paid work: ask first), and the assessment follows." : "An assessment runs when the reality documents are derived or refreshed (taiga_refresh_reality_documents, paid work: ask first).") : summary.controlsMet + summary.controlsNotApplicable === 0 && assessedAt ? `No control is recorded as met or not applicable: the last assessment (${assessedAt}) either ran before assessments recorded those verdicts, or found none. Until the next one, only violations are known; the rest are not assessed, which is no pass.` : null;
5590
5610
  const resolvedAny = Array.isArray(standing.resolvedDivergences) && standing.resolvedDivergences.length > 0;
5591
5611
  const wording = violations.length > 0 || resolvedAny ? { wordingNote: "A violation's evidence is from its assessedAt; its title and summary may be from when it was first recorded. Where they disagree, the evidence is current." } : {};
5592
5612
  if (params.controlId && params.violationId) return toolError("VALIDATION_ERROR", "Name either a control (controlId) or one violation (violationId), not both.");
@@ -5858,7 +5878,7 @@ const STALE_CHECKS = 10;
5858
5878
  async function staleReviews(client, rows) {
5859
5879
  const byId = new Map(rows.filter((row) => row["kind"] === "review_pr" && row["subjectType"] === "build_run").map((row) => [String(row["id"]), row]));
5860
5880
  const candidates = [...byId.values()].slice(0, STALE_CHECKS);
5861
- let unchecked = byId.size - candidates.length;
5881
+ const uncheckedIds = new Set([...byId.keys()].slice(STALE_CHECKS));
5862
5882
  const out = /* @__PURE__ */ new Map();
5863
5883
  await Promise.all(candidates.map(async (row) => {
5864
5884
  try {
@@ -5866,19 +5886,20 @@ async function staleReviews(client, rows) {
5866
5886
  if (run["prMerged"] === true) return void out.set(String(row["id"]), "its pull request has merged");
5867
5887
  if (run["prState"] === "closed") return void out.set(String(row["id"]), "its pull request was closed");
5868
5888
  if (typeof run["backlogItemId"] !== "string") return;
5869
- if (!client.hasCapability("backlog:read")) return void (unchecked += 1);
5889
+ if (!client.hasCapability("backlog:read")) return void uncheckedIds.add(String(row["id"]));
5870
5890
  const item = (await client.getBacklogItem(run["backlogItemId"])).data;
5871
5891
  if (item["state"] === "canceled" || item["status"] === "canceled") out.set(String(row["id"]), "its initiative was canceled; the pull request is still open on the forge");
5872
5892
  else if (item["state"] === "done" || item["status"] === "done") out.set(String(row["id"]), "its initiative is done; this run is not the one that landed it");
5873
5893
  } catch (error) {
5874
5894
  if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
5875
- unchecked += 1;
5895
+ uncheckedIds.add(String(row["id"]));
5876
5896
  }
5877
5897
  }));
5878
- unchecked += await staleBacklogReviews(client, rows, out);
5898
+ await staleBacklogReviews(client, rows, out, uncheckedIds);
5879
5899
  return {
5880
5900
  stale: out,
5881
- unchecked
5901
+ unchecked: uncheckedIds.size,
5902
+ uncheckedIds
5882
5903
  };
5883
5904
  }
5884
5905
  /**
@@ -5886,12 +5907,13 @@ async function staleReviews(client, rows) {
5886
5907
  * waiting 15 days on a product whose initiatives had all moved on, with nothing an agent could act on. Best effort,
5887
5908
  * a handful of products; returns how many could not be checked.
5888
5909
  */
5889
- async function staleBacklogReviews(client, rows, out) {
5910
+ async function staleBacklogReviews(client, rows, out, unchecked) {
5890
5911
  const asks = [...new Map(rows.filter((row) => row["kind"] === "review_backlog" && typeof row["productId"] === "string").map((row) => [String(row["id"]), row])).values()];
5891
- if (asks.length === 0) return 0;
5892
- if (!client.hasCapability("backlog:read")) return asks.length;
5912
+ const skip = (which) => which.forEach((row) => unchecked.add(String(row["id"])));
5913
+ if (asks.length === 0) return;
5914
+ if (!client.hasCapability("backlog:read")) return skip(asks);
5893
5915
  const checked = [...new Set(asks.map((row) => String(row["productId"])))].slice(0, STALE_CHECKS);
5894
- let unchecked = asks.filter((row) => !checked.includes(String(row["productId"]))).length;
5916
+ skip(asks.filter((row) => !checked.includes(String(row["productId"]))));
5895
5917
  const empty = /* @__PURE__ */ new Set();
5896
5918
  await Promise.all(checked.map(async (productId) => {
5897
5919
  try {
@@ -5902,11 +5924,10 @@ async function staleBacklogReviews(client, rows, out) {
5902
5924
  empty.add(productId);
5903
5925
  } catch (error) {
5904
5926
  if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
5905
- unchecked += asks.filter((row) => row["productId"] === productId).length;
5927
+ skip(asks.filter((row) => row["productId"] === productId));
5906
5928
  }
5907
5929
  }));
5908
5930
  for (const row of asks) if (empty.has(String(row["productId"]))) out.set(String(row["id"]), "no initiative is in the backlog or todo now: the ones it asked about have moved on");
5909
- return unchecked;
5910
5931
  }
5911
5932
  /**
5912
5933
  * One product's Action Required as counts, the way the list's summary counts them: open is what still stands and is
@@ -5932,6 +5953,35 @@ function isDue(row, now) {
5932
5953
  }
5933
5954
  /** Pages read for the summary; past this it says it counted only those. */
5934
5955
  const SUMMARY_PAGE = 100;
5956
+ /** Pages the summary reads before it says it stopped: 1,000 items. */
5957
+ const SUMMARY_PAGES = 10;
5958
+ /** Every item the summary counts, up to SUMMARY_PAGES pages; nextCursor set when it stopped short. Null on failure. */
5959
+ async function everyItem(client, query, orNull = true) {
5960
+ const data = [];
5961
+ let cursor;
5962
+ try {
5963
+ for (let index = 0; index < SUMMARY_PAGES; index += 1) {
5964
+ const page = await client.listInbox({
5965
+ ...query,
5966
+ limit: SUMMARY_PAGE,
5967
+ cursor
5968
+ });
5969
+ data.push(...page.data);
5970
+ if (!page.nextCursor) return {
5971
+ data,
5972
+ nextCursor: null
5973
+ };
5974
+ cursor = page.nextCursor;
5975
+ }
5976
+ return {
5977
+ data,
5978
+ nextCursor: cursor ?? null
5979
+ };
5980
+ } catch (error) {
5981
+ if (orNull && (error instanceof TaigaApiError || isNetworkFailure(error))) return null;
5982
+ throw error;
5983
+ }
5984
+ }
5935
5985
  const SETUP_TASK_FIELDS = [
5936
5986
  "id",
5937
5987
  "productId",
@@ -6026,14 +6076,7 @@ function registerInboxTools(server, client) {
6026
6076
  ...query,
6027
6077
  ...first ? { withTotal: true } : {}
6028
6078
  });
6029
- const overview = !first || resolved ? null : !result.nextCursor ? result : await client.listInbox({
6030
- ...query,
6031
- limit: SUMMARY_PAGE,
6032
- cursor: void 0
6033
- }).catch((error) => {
6034
- if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
6035
- throw error;
6036
- });
6079
+ const overview = !first || resolved ? null : !result.nextCursor ? result : await everyItem(client, query);
6037
6080
  const now = Date.now();
6038
6081
  const due_ = (row) => isDue(row, now);
6039
6082
  const rows = result.data;
@@ -6042,26 +6085,35 @@ function registerInboxTools(server, client) {
6042
6085
  const overviewDue = overview ? overview.data.filter(due_) : [];
6043
6086
  const checked = resolved ? {
6044
6087
  stale: /* @__PURE__ */ new Map(),
6045
- unchecked: 0
6088
+ unchecked: 0,
6089
+ uncheckedIds: /* @__PURE__ */ new Set()
6046
6090
  } : await staleReviews(client, [...due, ...overviewDue]);
6047
6091
  const stale = checked.stale;
6048
6092
  return toolResult({
6049
6093
  ...overview ? {
6050
6094
  summary: summaryOf(overviewDue, stale),
6051
- ...overview.nextCursor ? { summaryNote: `The summary counts the first ${SUMMARY_PAGE} items` + (params.productId ? "." : "; productId narrows it to one product.") } : {}
6095
+ ...overview.nextCursor ? { summaryNote: `The summary counts the first 1000 items` + (params.productId ? "." : "; productId narrows it to one product.") } : {}
6052
6096
  } : {},
6053
6097
  ...page(due.map((row) => {
6054
6098
  const item = itemRow(row, now, full === true, client.hasCapability("backlog:read"));
6055
6099
  const why = stale.get(String(row["id"]));
6056
- return why ? {
6100
+ if (why) return {
6057
6101
  ...item,
6058
6102
  stale: why
6103
+ };
6104
+ return checked.uncheckedIds.has(String(row["id"])) ? {
6105
+ ...item,
6106
+ staleChecked: false
6059
6107
  } : item;
6060
6108
  }), result.nextCursor),
6061
6109
  ...totalOf(result),
6062
- ...overview && typeof result.totalCount === "number" && (stale.size > 0 || overview.data.length > overviewDue.length || overview.nextCursor) ? { totalNote: "totalCount counts every item, stale and snoozed ones too; summary.open does not" + (overview.nextCursor ? `, and counts only the first ${SUMMARY_PAGE}.` : ".") } : {},
6110
+ ...overview && typeof result.totalCount === "number" && (stale.size > 0 || overview.data.length > overviewDue.length || overview.nextCursor) ? { totalNote: "totalCount counts every item, stale and snoozed ones too; summary.open does not" + (overview.nextCursor ? `, and counts only the first 1000.` : ".") } : {},
6063
6111
  ...hidden > 0 ? { snoozedHidden: hidden } : {},
6064
- ...checked.unchecked > 0 ? { staleNote: `${checked.unchecked} review${checked.unchecked === 1 ? " was" : "s were"} not checked for whether its subject moved on (past the cap, or the read was refused), so a row without stale may still be stale: taiga_get_initiative or taiga_get_run on its subject says.` } : {}
6112
+ ...checked.unchecked > 0 ? (() => {
6113
+ const onPage = due.filter((row) => checked.uncheckedIds.has(String(row["id"]))).length;
6114
+ const beyond = checked.unchecked - onPage;
6115
+ return { staleNote: `${[...onPage > 0 ? [`${onPage} review${onPage === 1 ? "" : "s"} on this page could not be checked for whether the subject moved on: those rows say staleChecked: false, and their readWith read tells`] : [], ...beyond > 0 ? [`${beyond} review${beyond === 1 ? "" : "s"} counted in the summary but not on this page could not be checked either, so the summary counts them open`] : []].join("; ")}.` };
6116
+ })() : {}
6065
6117
  });
6066
6118
  }));
6067
6119
  server.registerTool("taiga_list_setup_tasks", {
@@ -6426,13 +6478,10 @@ const CONTEXT_RUN_SAID = [
6426
6478
  * the list's disagree (18 open here, 16 there), because this one counted stale reviews and put-off items as open.
6427
6479
  */
6428
6480
  async function attentionPart(client, productId) {
6429
- const result = await client.listInbox({
6430
- productId,
6431
- limit: 100
6432
- });
6481
+ const result = await everyItem(client, { productId }, false);
6433
6482
  return {
6434
6483
  ...await attentionCounts(client, result.data),
6435
- ...result.nextCursor ? { note: "These count the first 100 items; taiga_list_action_required pages the rest." } : {}
6484
+ ...result.nextCursor ? { note: "These count the first 1000 items; taiga_list_action_required pages the rest." } : {}
6436
6485
  };
6437
6486
  }
6438
6487
  /** The latest sweep and what it left open, as counts; taiga_list_findings lists them. */
@@ -23007,7 +23056,7 @@ function registerAllTools(server, client, options = {}) {
23007
23056
  function registerWhoami(server, client, registered) {
23008
23057
  server.registerTool("taiga_whoami", {
23009
23058
  title: "Who am I",
23010
- description: "Read who this API key acts as: the API it talks to (api, which says the environment), the person's email and organization role, the key's scopes (null means every capability the person has), its capabilities, and its tools split into reads, writes, and ask-first writes (paid agent work, changes others see). A tool the key cannot use is not offered at all.",
23059
+ description: "Read who this API key acts as: the API host it talks to (api), the person's email and organization role, the key's scopes (null means every capability the person has), its capabilities, and its tools split into reads, writes, and ask-first writes (paid agent work, changes others see). A tool the key cannot use is not offered at all.",
23011
23060
  inputSchema: z.object({}),
23012
23061
  annotations: READ_ONLY
23013
23062
  }, () => runTool(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.26.0-dev.502",
3
+ "version": "0.26.0-dev.505",
4
4
  "description": "Taiga for AI coding assistants: read products, policies, initiatives and runs, and operate the line, over MCP",
5
5
  "mcpName": "ga.tai/mcp-server",
6
6
  "type": "module",