@fruggr/zendesk-mcp-server 2.0.1 → 2.1.0

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 CHANGED
@@ -151,6 +151,27 @@ zendesk-mcp-server acme --namespace tickets
151
151
 
152
152
  </details>
153
153
 
154
+ ## Help Center context (instructions + resources)
155
+
156
+ Beyond tools, the server hands an LLM the structural context it needs to work
157
+ against *your* Help Center — so it stops guessing locales or fuzzy-matching
158
+ section names and uses real IDs instead. This is delivered through two
159
+ MCP-native channels (both active only when the `help_center` namespace is, and
160
+ disabled together with `--no-topology`):
161
+
162
+ - **`instructions`** (sent on `initialize`): a short, static blob auto-loaded by
163
+ compliant clients. It names the subdomain and points at the topology resource.
164
+ - **`zendesk-hc://topology`** (a pull-only [MCP resource](https://modelcontextprotocol.io/docs/concepts/resources)):
165
+ read on demand, it returns Markdown describing the active locales (and the
166
+ default), the category → section tree with IDs, the visibility user segments,
167
+ the permission groups, and the calling user's role. It is fetched **with the
168
+ caller's own token**, so it respects that user's read permissions. On a very
169
+ large Help Center the section tree is summarized (per-category, with a pointer
170
+ to `list_sections`) to stay concise.
171
+
172
+ Clients that don't consume `instructions` or `resources` simply ignore them —
173
+ the feature degrades silently. Use `--no-topology` to turn both off server-wide.
174
+
154
175
  ## Prerequisites
155
176
 
156
177
  - **Node.js** >= 20 (runtime — declared in `package.json#engines.node`)
@@ -418,6 +439,8 @@ Options:
418
439
  --namespace <ns> Filter by namespace (repeatable): tickets, help_center, users
419
440
  --tool <name> Filter by tool name (repeatable, forces --mode all)
420
441
  --read-only Only expose read operations
442
+ --no-topology Disable the Help Center structural context
443
+ (instructions + zendesk-hc://topology resource)
421
444
  --log-level <level> debug | info (default) | warn | error
422
445
  --transport <t> stdio (default) | http
423
446
  --host <host> HTTP bind host (default: 0.0.0.0)
package/dist/index.js CHANGED
@@ -566,6 +566,14 @@ const ConfigSchema = z.object({
566
566
  readOnly: z.boolean(),
567
567
  namespaces: z.array(Namespace).optional(),
568
568
  tools: z.array(z.string()).optional(),
569
+ /**
570
+ * Whether to expose the Help Center structural context (the `instructions`
571
+ * blob + the `zendesk-hc://topology` resource). On by default; an operator
572
+ * disables it server-wide with `--no-topology` (e.g. on a very large Help
573
+ * Center, or when the context is unwanted). Only ever active when the
574
+ * `help_center` namespace itself is active.
575
+ */
576
+ topology: z.boolean().default(true),
569
577
  transport: Transport,
570
578
  host: z.string().min(1),
571
579
  port: z.number().int().min(0).max(65535),
@@ -596,6 +604,7 @@ const parseCliArgs = (args) => {
596
604
  result.mode = next;
597
605
  i++;
598
606
  } else if (arg === "--read-only") result.readOnly = true;
607
+ else if (arg === "--no-topology") result.topology = false;
599
608
  else if (arg === "--namespace" && next) {
600
609
  result.namespaces = result.namespaces ?? [];
601
610
  result.namespaces.push(next);
@@ -653,6 +662,7 @@ const loadConfig = (argv = process.argv.slice(2)) => {
653
662
  readOnly: cli.readOnly ?? false,
654
663
  namespaces: cli.namespaces,
655
664
  tools: cli.tools,
665
+ topology: cli.topology ?? true,
656
666
  transport,
657
667
  host,
658
668
  port,
@@ -771,6 +781,246 @@ const helpCenterUpload = async (subdomain, token, path, formData) => {
771
781
  return response.json();
772
782
  };
773
783
  //#endregion
784
+ //#region src/guidance/instructions.ts
785
+ /** Stable URI of the dynamic Help Center topology resource. */
786
+ const TOPOLOGY_RESOURCE_URI = "zendesk-hc://topology";
787
+ /**
788
+ * Whether the Help Center structural context (init instructions + the
789
+ * `zendesk-hc://topology` resource) should be exposed. True only when the
790
+ * feature is enabled (`--no-topology` not set) AND the `help_center` namespace
791
+ * is active (no `--namespace` filter, or one that includes it). Shared by the
792
+ * instructions builder and the resource registration in `server.ts` so both
793
+ * gates stay in sync.
794
+ */
795
+ const helpCenterContextEnabled = (config) => config.topology && (!config.namespaces?.length || config.namespaces.includes("help_center"));
796
+ /**
797
+ * The static `instructions` blob sent on `initialize`. Deliberately short and
798
+ * I/O-free: it must not trigger the lazy OAuth/PKCE flow just to connect, and
799
+ * it stays within a tight token budget. The rich, dynamic topology lives in the
800
+ * pull-only `zendesk-hc://topology` resource referenced here.
801
+ */
802
+ const buildInstructions = (config) => {
803
+ if (!helpCenterContextEnabled(config)) return void 0;
804
+ return [
805
+ `This MCP server is connected to the Zendesk Help Center of "${config.subdomain}".`,
806
+ "",
807
+ `Before creating or editing Help Center content, read the resource ${TOPOLOGY_RESOURCE_URI}.`,
808
+ "It describes the active locales (and the default one), the category → section tree with IDs,",
809
+ "the visibility user segments, the permission groups, and your current role.",
810
+ "Prefer the IDs from that resource (section_id, permission_group_id, user_segment_id, locale)",
811
+ "over guessing from names."
812
+ ].join("\n");
813
+ };
814
+ //#endregion
815
+ //#region src/utils/formatting.ts
816
+ const truncateIfNeeded = (text) => {
817
+ if (text.length <= 25e3) return text;
818
+ return `${text.slice(0, CHARACTER_LIMIT)}\n\n--- Response truncated (${text.length} chars, limit ${CHARACTER_LIMIT}). Use pagination or filters to reduce results. ---`;
819
+ };
820
+ const formatPagination = (meta) => {
821
+ const parts = [`Results: ${meta.count}`];
822
+ if (meta.has_more) parts.push(`More available (cursor: ${meta.after_cursor})`);
823
+ return parts.join(" | ");
824
+ };
825
+ const formatTicket = (ticket) => [
826
+ `## Ticket #${ticket.id}: ${ticket.subject}`,
827
+ `- **Status**: ${ticket.status} | **Priority**: ${ticket.priority ?? "none"} | **Type**: ${ticket.type ?? "none"}`,
828
+ `- **Requester**: ${ticket.requester_id} | **Assignee**: ${ticket.assignee_id ?? "unassigned"}`,
829
+ `- **Tags**: ${ticket.tags.length > 0 ? ticket.tags.join(", ") : "none"}`,
830
+ `- **Created**: ${ticket.created_at} | **Updated**: ${ticket.updated_at}`,
831
+ ticket.description ? `\n${ticket.description}` : ""
832
+ ].filter(Boolean).join("\n");
833
+ const formatComment = (comment) => {
834
+ const lines = [`### ${comment.public ? "Public comment" : "Internal note"} by ${comment.author_id}`, `*${comment.created_at}*`];
835
+ if (comment.attachments?.length) {
836
+ const summary = comment.attachments.map((a) => `#${a.id} (${a.content_type})`).join(", ");
837
+ lines.push(`Attachments: ${summary}`);
838
+ }
839
+ lines.push("", comment.body);
840
+ return lines.join("\n");
841
+ };
842
+ const formatUser = (user) => [
843
+ `## ${user.name} (${user.id})`,
844
+ `- **Email**: ${user.email}`,
845
+ `- **Role**: ${user.role}`,
846
+ user.role_type != null ? `- **Role type**: ${user.role_type}` : "",
847
+ `- **Active**: ${user.active}`,
848
+ user.organization_id ? `- **Organization**: ${user.organization_id}` : ""
849
+ ].filter(Boolean).join("\n");
850
+ const formatOrganization = (org) => [
851
+ `## ${org.name} (${org.id})`,
852
+ org.details ? `- **Details**: ${org.details}` : "",
853
+ org.domain_names.length > 0 ? `- **Domains**: ${org.domain_names.join(", ")}` : "",
854
+ org.tags.length > 0 ? `- **Tags**: ${org.tags.join(", ")}` : ""
855
+ ].filter(Boolean).join("\n");
856
+ const formatArticleSummary = (article) => [
857
+ `## ${article.title} (${article.id})`,
858
+ `- **Locale**: ${article.locale} | **Source locale**: ${article.source_locale}`,
859
+ `- **Section**: ${article.section_id} | **Draft**: ${article.draft}`,
860
+ typeof article.position === "number" ? `- **Position**: ${article.position}` : "",
861
+ article.label_names.length > 0 ? `- **Labels**: ${article.label_names.join(", ")}` : "",
862
+ `- **Created**: ${article.created_at} | **Updated**: ${article.updated_at}`
863
+ ].filter(Boolean).join("\n");
864
+ const formatArticle = (article) => [
865
+ formatArticleSummary(article),
866
+ "",
867
+ article.body
868
+ ].join("\n");
869
+ const formatTranslationSummary = (translation) => [
870
+ `## Translation: ${translation.locale} (${translation.id})`,
871
+ `- **Title**: ${translation.title}`,
872
+ `- **Draft**: ${translation.draft}`,
873
+ `- **Updated**: ${translation.updated_at}`
874
+ ].join("\n");
875
+ const formatTranslation = (translation) => [
876
+ formatTranslationSummary(translation),
877
+ "",
878
+ translation.body
879
+ ].join("\n");
880
+ const formatCategory = (category) => `- **${category.name}** (${category.id}) — ${category.description || "No description"}`;
881
+ const formatSection = (section) => `- **${section.name}** (${section.id}) — Category: ${section.category_id} — ${section.description || "No description"}`;
882
+ const formatPermissionGroup = (group) => `- **${group.name}** (${group.id})${group.built_in ? " — Built-in" : ""}`;
883
+ const formatContentTag = (tag) => `- **${tag.name}** (${tag.id})`;
884
+ const formatLabel = (label) => `- **${label.name}** (${label.id})`;
885
+ const formatUserSegment = (segment) => `- **${segment.name}** (${segment.id}) — ${segment.user_type}${segment.built_in ? " — Built-in" : ""}`;
886
+ const formatAttachment = (attachment) => `- **${attachment.file_name}** (${attachment.id}) — ${attachment.content_type} — ${attachment.size} bytes`;
887
+ const formatList = (items, formatter, meta) => {
888
+ return truncateIfNeeded([meta ? formatPagination(meta) : "", items.map(formatter).join("\n\n")].filter(Boolean).join("\n\n"));
889
+ };
890
+ //#endregion
891
+ //#region src/utils/pagination.ts
892
+ const buildCursorParams = (pageSize, cursor) => {
893
+ const params = { "page[size]": String(pageSize) };
894
+ if (cursor) params["page[after]"] = cursor;
895
+ return params;
896
+ };
897
+ const buildOffsetParams = (perPage, page) => {
898
+ const params = { per_page: String(perPage) };
899
+ if (page && page > 1) params["page"] = String(page);
900
+ return params;
901
+ };
902
+ const extractPaginationMeta = (response) => ({
903
+ has_more: response.meta?.has_more ?? response.next_page != null,
904
+ after_cursor: response.meta?.after_cursor ?? null,
905
+ count: response.count ?? 0
906
+ });
907
+ const extractSearchPaginationMeta = (response, perPage, page) => {
908
+ const count = response.count ?? 0;
909
+ const has_more = count > page * perPage;
910
+ return {
911
+ has_more,
912
+ after_cursor: has_more ? String(page + 1) : null,
913
+ count
914
+ };
915
+ };
916
+ //#endregion
917
+ //#region src/guidance/topology.ts
918
+ /**
919
+ * Fetch the structural topology with the CALLER'S token, so the result respects
920
+ * that user's read permissions (no privileged shared credential). Categories
921
+ * and sections are each capped at one max-size page; `sectionsHasMore` /
922
+ * `categoriesHasMore` signal a Help Center too large to enumerate inline.
923
+ */
924
+ const fetchTopology = async (subdomain, token) => {
925
+ const pageParams = { "page[size]": String(100) };
926
+ const [locales, categoriesRes, sectionsRes, segmentsRes, permsRes, meRes] = await Promise.all([
927
+ helpCenterGet(subdomain, token, "/locales"),
928
+ helpCenterGet(subdomain, token, "/categories", pageParams),
929
+ helpCenterGet(subdomain, token, "/sections", pageParams),
930
+ helpCenterGet(subdomain, token, "/user_segments"),
931
+ zendeskGet(subdomain, token, "/guide/permission_groups"),
932
+ zendeskGet(subdomain, token, "/users/me")
933
+ ]);
934
+ return {
935
+ subdomain,
936
+ locales,
937
+ categories: categoriesRes.categories ?? [],
938
+ sections: sectionsRes.sections ?? [],
939
+ sectionsHasMore: extractPaginationMeta(sectionsRes).has_more,
940
+ categoriesHasMore: extractPaginationMeta(categoriesRes).has_more,
941
+ userSegments: segmentsRes.user_segments ?? [],
942
+ permissionGroups: permsRes.permission_groups ?? [],
943
+ currentUser: meRes.user
944
+ };
945
+ };
946
+ const renderTree = (data) => {
947
+ if (data.categoriesHasMore || data.sectionsHasMore) {
948
+ const reasons = [];
949
+ if (data.categoriesHasMore) reasons.push(`more than 100 categories`);
950
+ if (data.sectionsHasMore) reasons.push(`more than 100 sections`);
951
+ return [
952
+ `Large Help Center (${reasons.join(" and ")}) — the full tree is omitted to stay concise.`,
953
+ data.categoriesHasMore ? "Categories (partial list):" : "Categories:",
954
+ ...data.categories.map(formatCategory),
955
+ "",
956
+ ...data.categoriesHasMore ? ["Use the `list_categories` tool to enumerate all categories."] : [],
957
+ "Use the `list_sections` tool (filtered by `category_id`) to enumerate sections under a category."
958
+ ];
959
+ }
960
+ const byCategory = /* @__PURE__ */ new Map();
961
+ for (const section of data.sections) {
962
+ const list = byCategory.get(section.category_id) ?? [];
963
+ list.push(section);
964
+ byCategory.set(section.category_id, list);
965
+ }
966
+ const lines = [];
967
+ for (const category of data.categories) {
968
+ lines.push(formatCategory(category));
969
+ for (const section of byCategory.get(category.id) ?? []) lines.push(` ${formatSection(section)}`);
970
+ }
971
+ return lines.length ? lines : ["_(no categories)_"];
972
+ };
973
+ /** Render the topology as a compact Markdown document for the LLM context. */
974
+ const formatTopology = (data) => {
975
+ return truncateIfNeeded([
976
+ `# Zendesk Help Center topology — ${data.subdomain}`,
977
+ "",
978
+ `**Your access**: ${data.currentUser.name} (id ${data.currentUser.id}), role "${data.currentUser.role}".`,
979
+ "",
980
+ "## Locales",
981
+ `- Default: ${data.locales.default_locale}`,
982
+ `- Active: ${data.locales.locales.join(", ")}`,
983
+ "",
984
+ "## Categories → sections",
985
+ ...renderTree(data),
986
+ "",
987
+ "## Visibility (user segments)",
988
+ ...data.userSegments.length ? data.userSegments.map(formatUserSegment) : ["_(none)_"],
989
+ "",
990
+ "## Permission groups",
991
+ ...data.permissionGroups.length ? data.permissionGroups.map(formatPermissionGroup) : ["_(none)_"]
992
+ ].join("\n"));
993
+ };
994
+ /**
995
+ * Build a topology provider holding a memoized-promise cache (TTL
996
+ * `TOPOLOGY_TTL_MS`). In HTTP mode `createMcpServer` — and therefore this
997
+ * provider — is instantiated PER SESSION, so this cache is per-session /
998
+ * per-caller: it must NOT be hoisted to module scope, or one tenant's structure
999
+ * would leak to another. The promise (not the value) is cached to coalesce
1000
+ * concurrent reads; failures are evicted so the next read retries with a fresh
1001
+ * token. A 401 notifies `onUnauthorized` (stdio OAuth only) to invalidate the
1002
+ * stale token, mirroring the tool dispatch path in `server.ts`.
1003
+ */
1004
+ const createTopologyProvider = (getToken, subdomain, onUnauthorized) => {
1005
+ let cached;
1006
+ return { read() {
1007
+ const now = Date.now();
1008
+ if (cached && now - cached.at < 3e5) return cached.promise;
1009
+ const promise = (async () => {
1010
+ return formatTopology(await fetchTopology(subdomain, await getToken()));
1011
+ })().catch((err) => {
1012
+ cached = void 0;
1013
+ if (onUnauthorized && err instanceof ZendeskApiError && err.status === 401) onUnauthorized();
1014
+ throw err;
1015
+ });
1016
+ cached = {
1017
+ at: now,
1018
+ promise
1019
+ };
1020
+ return promise;
1021
+ } };
1022
+ };
1023
+ //#endregion
774
1024
  //#region src/routing/registry.ts
775
1025
  const filterTools = (allTools, options) => allTools.filter((tool) => {
776
1026
  if (options.readOnly && !tool.readOnly) return false;
@@ -883,108 +1133,6 @@ const markdownToHtml = (markdown) => {
883
1133
  return String(mdToHtmlProcessor.processSync(markdown));
884
1134
  };
885
1135
  //#endregion
886
- //#region src/utils/formatting.ts
887
- const truncateIfNeeded = (text) => {
888
- if (text.length <= 25e3) return text;
889
- return `${text.slice(0, CHARACTER_LIMIT)}\n\n--- Response truncated (${text.length} chars, limit ${CHARACTER_LIMIT}). Use pagination or filters to reduce results. ---`;
890
- };
891
- const formatPagination = (meta) => {
892
- const parts = [`Results: ${meta.count}`];
893
- if (meta.has_more) parts.push(`More available (cursor: ${meta.after_cursor})`);
894
- return parts.join(" | ");
895
- };
896
- const formatTicket = (ticket) => [
897
- `## Ticket #${ticket.id}: ${ticket.subject}`,
898
- `- **Status**: ${ticket.status} | **Priority**: ${ticket.priority ?? "none"} | **Type**: ${ticket.type ?? "none"}`,
899
- `- **Requester**: ${ticket.requester_id} | **Assignee**: ${ticket.assignee_id ?? "unassigned"}`,
900
- `- **Tags**: ${ticket.tags.length > 0 ? ticket.tags.join(", ") : "none"}`,
901
- `- **Created**: ${ticket.created_at} | **Updated**: ${ticket.updated_at}`,
902
- ticket.description ? `\n${ticket.description}` : ""
903
- ].filter(Boolean).join("\n");
904
- const formatComment = (comment) => {
905
- const lines = [`### ${comment.public ? "Public comment" : "Internal note"} by ${comment.author_id}`, `*${comment.created_at}*`];
906
- if (comment.attachments?.length) {
907
- const summary = comment.attachments.map((a) => `#${a.id} (${a.content_type})`).join(", ");
908
- lines.push(`Attachments: ${summary}`);
909
- }
910
- lines.push("", comment.body);
911
- return lines.join("\n");
912
- };
913
- const formatUser = (user) => [
914
- `## ${user.name} (${user.id})`,
915
- `- **Email**: ${user.email}`,
916
- `- **Role**: ${user.role}`,
917
- user.role_type != null ? `- **Role type**: ${user.role_type}` : "",
918
- `- **Active**: ${user.active}`,
919
- user.organization_id ? `- **Organization**: ${user.organization_id}` : ""
920
- ].filter(Boolean).join("\n");
921
- const formatOrganization = (org) => [
922
- `## ${org.name} (${org.id})`,
923
- org.details ? `- **Details**: ${org.details}` : "",
924
- org.domain_names.length > 0 ? `- **Domains**: ${org.domain_names.join(", ")}` : "",
925
- org.tags.length > 0 ? `- **Tags**: ${org.tags.join(", ")}` : ""
926
- ].filter(Boolean).join("\n");
927
- const formatArticleSummary = (article) => [
928
- `## ${article.title} (${article.id})`,
929
- `- **Locale**: ${article.locale} | **Source locale**: ${article.source_locale}`,
930
- `- **Section**: ${article.section_id} | **Draft**: ${article.draft}`,
931
- typeof article.position === "number" ? `- **Position**: ${article.position}` : "",
932
- article.label_names.length > 0 ? `- **Labels**: ${article.label_names.join(", ")}` : "",
933
- `- **Created**: ${article.created_at} | **Updated**: ${article.updated_at}`
934
- ].filter(Boolean).join("\n");
935
- const formatArticle = (article) => [
936
- formatArticleSummary(article),
937
- "",
938
- article.body
939
- ].join("\n");
940
- const formatTranslationSummary = (translation) => [
941
- `## Translation: ${translation.locale} (${translation.id})`,
942
- `- **Title**: ${translation.title}`,
943
- `- **Draft**: ${translation.draft}`,
944
- `- **Updated**: ${translation.updated_at}`
945
- ].join("\n");
946
- const formatTranslation = (translation) => [
947
- formatTranslationSummary(translation),
948
- "",
949
- translation.body
950
- ].join("\n");
951
- const formatCategory = (category) => `- **${category.name}** (${category.id}) — ${category.description || "No description"}`;
952
- const formatSection = (section) => `- **${section.name}** (${section.id}) — Category: ${section.category_id} — ${section.description || "No description"}`;
953
- const formatPermissionGroup = (group) => `- **${group.name}** (${group.id})${group.built_in ? " — Built-in" : ""}`;
954
- const formatContentTag = (tag) => `- **${tag.name}** (${tag.id})`;
955
- const formatLabel = (label) => `- **${label.name}** (${label.id})`;
956
- const formatUserSegment = (segment) => `- **${segment.name}** (${segment.id}) — ${segment.user_type}${segment.built_in ? " — Built-in" : ""}`;
957
- const formatAttachment = (attachment) => `- **${attachment.file_name}** (${attachment.id}) — ${attachment.content_type} — ${attachment.size} bytes`;
958
- const formatList = (items, formatter, meta) => {
959
- return truncateIfNeeded([meta ? formatPagination(meta) : "", items.map(formatter).join("\n\n")].filter(Boolean).join("\n\n"));
960
- };
961
- //#endregion
962
- //#region src/utils/pagination.ts
963
- const buildCursorParams = (pageSize, cursor) => {
964
- const params = { "page[size]": String(pageSize) };
965
- if (cursor) params["page[after]"] = cursor;
966
- return params;
967
- };
968
- const buildOffsetParams = (perPage, page) => {
969
- const params = { per_page: String(perPage) };
970
- if (page && page > 1) params["page"] = String(page);
971
- return params;
972
- };
973
- const extractPaginationMeta = (response) => ({
974
- has_more: response.meta?.has_more ?? response.next_page != null,
975
- after_cursor: response.meta?.after_cursor ?? null,
976
- count: response.count ?? 0
977
- });
978
- const extractSearchPaginationMeta = (response, perPage, page) => {
979
- const count = response.count ?? 0;
980
- const has_more = count > page * perPage;
981
- return {
982
- has_more,
983
- after_cursor: has_more ? String(page + 1) : null,
984
- count
985
- };
986
- };
987
- //#endregion
988
1136
  //#region src/tools/help-center.ts
989
1137
  const largeArticleHint = (body, sectionCount) => {
990
1138
  if (body.length < 3e3 && sectionCount < 4) return null;
@@ -2294,10 +2442,14 @@ const registerProxyTool = (server, toolName, title, tools, readOnlyMode, onUnaut
2294
2442
  };
2295
2443
  const createMcpServer = (config, getToken, logger = silentLogger, onUnauthorized) => {
2296
2444
  const pkg = readPackageInfo();
2445
+ const instructions = buildInstructions(config);
2297
2446
  const server = new McpServer({
2298
2447
  name: pkg.name,
2299
2448
  version: pkg.version
2300
- }, { capabilities: { logging: {} } });
2449
+ }, {
2450
+ capabilities: { logging: {} },
2451
+ ...instructions ? { instructions } : {}
2452
+ });
2301
2453
  logger.attachServer(server);
2302
2454
  const filteredTools = filterTools(createAllTools({
2303
2455
  subdomain: config.subdomain,
@@ -2328,6 +2480,18 @@ const createMcpServer = (config, getToken, logger = silentLogger, onUnauthorized
2328
2480
  registerProxyTool(server, "zendesk", "Zendesk", filteredTools, config.readOnly, onUnauthorized);
2329
2481
  break;
2330
2482
  }
2483
+ if (helpCenterContextEnabled(config)) {
2484
+ const topology = createTopologyProvider(getToken, config.subdomain, onUnauthorized);
2485
+ server.registerResource("help-center-topology", TOPOLOGY_RESOURCE_URI, {
2486
+ title: "Zendesk Help Center topology",
2487
+ description: "Active locales, category → section tree, visibility segments, permission groups, and your role. Read before creating or editing content.",
2488
+ mimeType: "text/markdown"
2489
+ }, async (uri) => ({ contents: [{
2490
+ uri: uri.toString(),
2491
+ mimeType: "text/markdown",
2492
+ text: await topology.read()
2493
+ }] }));
2494
+ }
2331
2495
  logger.info("tools_registered", {
2332
2496
  count: filteredTools.length,
2333
2497
  mode: config.mode