@fruggr/zendesk-mcp-server 2.13.0 → 2.14.1

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 CHANGED
@@ -1478,6 +1478,7 @@ const markdownToHtml = (markdown) => {
1478
1478
  //#endregion
1479
1479
  //#region src/tools/help-center.ts
1480
1480
  const ARTICLE_ID_DESC = "Article ID — the numeric id of the Help Center article. Obtain it from list_articles or search_articles.";
1481
+ const listTranslations = (subdomain, token, articleId) => helpCenterGet(subdomain, token, `/articles/${articleId}/translations`).then((res) => res.translations);
1481
1482
  const largeArticleHint = (body, sectionCount) => {
1482
1483
  if (body.length < 3e3 && sectionCount < 4) return null;
1483
1484
  return [
@@ -1560,7 +1561,7 @@ const createHelpCenterTools = (ctx) => {
1560
1561
  const token = await getToken();
1561
1562
  const path = locale ? `/${locale}/articles/${article_id}` : `/articles/${article_id}`;
1562
1563
  const { article } = await helpCenterGet(subdomain, token, path);
1563
- const { translations } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations`);
1564
+ const translations = await listTranslations(subdomain, token, article_id);
1564
1565
  return { content: [{
1565
1566
  type: "text",
1566
1567
  text: truncateIfNeeded((largeArticleHint(article.body, parseSections(article.body).length) ?? "") + formatArticle(article) + `\n\n**Available translations**: ${translations.map((t) => t.locale).join(", ")}`)
@@ -1667,8 +1668,7 @@ const createHelpCenterTools = (ctx) => {
1667
1668
  text: formatList(articles, formatArticleSummary, extractPaginationMeta(response, articles.length))
1668
1669
  }] };
1669
1670
  const formatted = await Promise.all(articles.map(async (article) => {
1670
- const { translations } = await helpCenterGet(subdomain, token, `/articles/${article.id}/translations`);
1671
- const locales = translations.map((t) => t.locale).join(", ");
1671
+ const locales = (await listTranslations(subdomain, token, article.id)).map((t) => t.locale).join(", ");
1672
1672
  return `${formatArticleSummary(article)}\n- **Translations**: ${locales}`;
1673
1673
  }));
1674
1674
  const meta = extractPaginationMeta(response, articles.length);
@@ -1694,10 +1694,9 @@ const createHelpCenterTools = (ctx) => {
1694
1694
  handler: async (params) => {
1695
1695
  const { article_id } = params;
1696
1696
  const token = await getToken();
1697
- const { translations } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations`);
1698
1697
  return { content: [{
1699
1698
  type: "text",
1700
- text: formatList(translations, formatTranslationSummary)
1699
+ text: formatList(await listTranslations(subdomain, token, article_id), formatTranslationSummary)
1701
1700
  }] };
1702
1701
  }
1703
1702
  },
@@ -2120,7 +2119,7 @@ const createHelpCenterTools = (ctx) => {
2120
2119
  const { article } = await helpCenterGet(subdomain, token, `/articles/${article_id}`);
2121
2120
  const effectiveLocale = locale ?? article.source_locale;
2122
2121
  const { translation } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations/${effectiveLocale}`);
2123
- const { translations } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations`);
2122
+ const translations = await listTranslations(subdomain, token, article_id);
2124
2123
  const sections = parseSections(translation.body);
2125
2124
  const outlineLines = sections.length ? sections.map((s) => `- [${s.index}] ${s.headingTag ? `${s.headingTag}: ` : ""}${s.heading} (${s.wordCount} words)`).join("\n") : "_(no sections detected)_";
2126
2125
  const translationsList = translations.map((t) => `- ${t.locale}${t.outdated ? " (outdated)" : ""}`).join("\n");
@@ -2215,7 +2214,7 @@ const createHelpCenterTools = (ctx) => {
2215
2214
  namespace: "help_center",
2216
2215
  readOnly: true,
2217
2216
  title: "Compare Article Translations",
2218
- description: "Compare section structure between two locales of the same article, matched by index. Returns a compact table (one row per section) with status: \"ok\" (both present, source/target word count ratio within 25%), \"different\" (word count ratio diverges by more than 25% size signal only, NOT a semantic divergence: two locales may legitimately differ in verbosity) or \"missing\" (section absent in target). Useful to spot structurally stale or missing sections; do not interpret \"different\" as an edit regression on its own.",
2217
+ description: "Compare two locales of the same article to decide whether the target translation needs work, reporting independent signals instead of one ambiguous verdict. (1) Header a \"Freshness\" verdict for the target, derived from the two translations' updated_at timestamps: if the source was edited after the target it is \"likely behind, review recommended\" (with the day gap), otherwise \"up to date\". This is the primary staleness signal and is always available. (2) Zendesk's own per-translation \"outdated\" flag for the target (\"yes\"/\"no\"/\"unknown\"), shown as a secondary overlay: it is only set through Guide's native \"mark out of date\" workflow and NOT by API edits, so a \"no\" does not by itself mean current — prefer Freshness. (3) A global structure check (section count and heading-tag sequence); on mismatch the header warns the per-index rows may be misaligned. (4) A per-section table matched by index, status \"ok\" (present in both), \"missing\" (present in source, absent in target) or \"extra\" (present in target, absent in source). (5) Per-section source/target word counts, INFORMATIONAL ONLY: a length difference between languages is normal and is deliberately NOT flagged as a divergence — do not read a word-count gap as an edit regression or staleness. Read-only; performs three Help Center GET calls (both translations plus the translations list for the outdated flag).",
2219
2218
  inputSchema: z.object({
2220
2219
  article_id: z.number().int().describe(ARTICLE_ID_DESC),
2221
2220
  source_locale: z.string().describe("Reference locale to diff against, e.g. \"en-us\". Usually the article source_locale (from get_article)."),
@@ -2230,10 +2229,32 @@ const createHelpCenterTools = (ctx) => {
2230
2229
  handler: async (params) => {
2231
2230
  const { article_id, source_locale, target_locale } = params;
2232
2231
  const token = await getToken();
2233
- const [sourceRes, targetRes] = await Promise.all([helpCenterGet(subdomain, token, `/articles/${article_id}/translations/${source_locale}`), helpCenterGet(subdomain, token, `/articles/${article_id}/translations/${target_locale}`)]);
2232
+ const [sourceRes, targetRes, translations] = await Promise.all([
2233
+ helpCenterGet(subdomain, token, `/articles/${article_id}/translations/${source_locale}`),
2234
+ helpCenterGet(subdomain, token, `/articles/${article_id}/translations/${target_locale}`),
2235
+ listTranslations(subdomain, token, article_id)
2236
+ ]);
2234
2237
  const sourceSections = parseSections(sourceRes.translation.body);
2235
2238
  const targetSections = parseSections(targetRes.translation.body);
2236
2239
  const maxLen = Math.max(sourceSections.length, targetSections.length);
2240
+ const sourceUpdated = sourceRes.translation.updated_at;
2241
+ const targetUpdated = targetRes.translation.updated_at;
2242
+ const srcMs = Date.parse(sourceUpdated);
2243
+ const tgtMs = Date.parse(targetUpdated);
2244
+ const comparable = Number.isFinite(srcMs) && Number.isFinite(tgtMs);
2245
+ let freshnessLine;
2246
+ if (comparable && srcMs > tgtMs) {
2247
+ const days = Math.floor((srcMs - tgtMs) / 864e5);
2248
+ freshnessLine = `- **Freshness (target ${target_locale})**: source was edited ${days >= 1 ? `${days} day(s)` : "less than a day"} after this translation → likely behind, review recommended.`;
2249
+ } else if (comparable) freshnessLine = `- **Freshness (target ${target_locale})**: up to date (source has not been edited since this translation).`;
2250
+ else freshnessLine = `- **Freshness (target ${target_locale})**: unknown (could not compare edit timestamps).`;
2251
+ const targetLocaleKey = target_locale.toLowerCase();
2252
+ const targetListEntry = translations.find((t) => t.locale.toLowerCase() === targetLocaleKey);
2253
+ const outdated = targetListEntry?.outdated === void 0 ? "unknown" : targetListEntry.outdated ? "yes" : "no";
2254
+ const outdatedLine = outdated === "yes" ? `- **Zendesk outdated flag (target ${target_locale})**: yes — explicitly marked out of date in Guide.` : outdated === "no" ? `- **Zendesk outdated flag (target ${target_locale})**: no (only set via Guide's own edit workflow; "no" does not by itself mean current — rely on Freshness above).` : `- **Zendesk outdated flag (target ${target_locale})**: unknown.`;
2255
+ const sourceTags = sourceSections.map((s) => s.headingTag).join(",");
2256
+ const targetTags = targetSections.map((s) => s.headingTag).join(",");
2257
+ const structureLine = sourceSections.length === targetSections.length && sourceTags === targetTags ? `- **Structure**: ${sourceSections.length} sections in both locales — aligned.` : `- **Structure**: ${sourceSections.length} source vs ${targetSections.length} target sections — MISMATCH; the per-index rows below may be misaligned.`;
2237
2258
  const rows = [];
2238
2259
  rows.push(`| Idx | Heading | Status | Source words | Target words |`);
2239
2260
  rows.push(`| --- | --- | --- | --- | --- |`);
@@ -2245,11 +2266,8 @@ const createHelpCenterTools = (ctx) => {
2245
2266
  const targetWords = tgt?.wordCount ?? 0;
2246
2267
  let status;
2247
2268
  if (!tgt) status = "missing";
2248
- else if (!src) status = "different";
2249
- else {
2250
- const denom = Math.max(sourceWords, 1);
2251
- status = Math.abs(sourceWords - targetWords) / denom > .25 ? "different" : "ok";
2252
- }
2269
+ else if (!src) status = "extra";
2270
+ else status = "ok";
2253
2271
  rows.push(`| ${i} | ${heading} | ${status} | ${sourceWords} | ${targetWords} |`);
2254
2272
  }
2255
2273
  return { content: [{
@@ -2257,6 +2275,14 @@ const createHelpCenterTools = (ctx) => {
2257
2275
  text: [
2258
2276
  `# Translation diff — Article #${article_id} (${source_locale} → ${target_locale})`,
2259
2277
  "",
2278
+ freshnessLine,
2279
+ outdatedLine,
2280
+ structureLine,
2281
+ `- **Updated**: source ${sourceUpdated} | target ${targetUpdated}`,
2282
+ `- **Target draft**: ${targetRes.translation.draft ? "yes" : "no"}`,
2283
+ "",
2284
+ "_Word counts are informational: a length difference between languages is normal, not a divergence._",
2285
+ "",
2260
2286
  ...rows
2261
2287
  ].join("\n")
2262
2288
  }] };