agentleFS
fruggrfruggr/zendesk-mcp-server/AGENTS.md

zendesk-mcp-server

AGENTS.md15 starsChanged 20 days agoMITView on GitHub

Purpose & scope. This file orients an LLM working in this repo: where things live and the rules that aren't obvious from the code. It is not user documentation. - Don't document what the code already shows. File-by-file trees, command listings, env-var tables, setup steps — the agent reads those from the source, package.json or the docs faster than it trusts a prose copy here, and duplicating them only burns context and goes stale. - Keep each section short —…

  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 31,542 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

  • In this repository5 other files it gives its agents.

The file

We split this AGENTS.md into the rules it gives an agent

12 rules. About 0 are common, 4 are distinctive, and 8 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 8

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Keep each section short — durable rules and pointers, not reference material.

    line 11
  2. Very specific:

    When a section grows into setup steps, command listings or exhaustive detail, it's user-facing (→ README.md) or a deep dive (→ docs/): link …

    line 12
  3. Very specific:

    Every Zendesk API request goes through performFetch in client/zendesk-api.ts (the OAuth token exchange in auth/browser-oauth.ts …

    line 82
  4. Distinctive:

    Never check an end-user tool with an agent token: several operations behave differently for the two roles, so that check passes where a …

    line 113
  5. Distinctive:

    Measure before you plan: When a plan would rest on how Zendesk really answers (a sideload honoured or dropped, where a field sits, who may …

    line 163
  6. Distinctive:

    Independence is about context, not identity: Every agent here acts under the same human's GitHub account, so the author shown on a PR …

    line 181
  7. Very specific:

    Keep the !!/node_modules force-ignore in biome.json files.includes: it stops Biome 2's scanner from opening every dependency file for its …

    line 200
  8. Very specific:

    Comments are optional; the code is the first explanation: Write one only for what the code cannot say — the *why*: a constraint, a …

    line 215
  9. Very specific:

    Read every variable through readEnv (src/utils/env.ts).

    line 236
  10. Very specific:

    Must work with every mainstream MCP agent.

    line 248
  11. Distinctive:

    Every tool change must keep what agents depend on in the exposed JSON Schema (draft 2020-12): never drop a field or loosen a type, and …

    line 253
  12. Very specific:

    Before opening a PR, clear the Submission quality bar in CONTRIBUTING.md (same bar for human and AI authors).

    line 286

≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

AGENTS.md — Working guide for AI agents

Purpose & scope. This file orients an LLM working in this repo: where things live and the rules that aren't obvious from the code. It is not user documentation.

  • Don't document what the code already shows. File-by-file trees, command listings, env-var tables, setup steps — the agent reads those from the source, package.json or the docs faster than it trusts a prose copy here, and duplicating them only burns context and goes stale.
  • Keep each section short — durable rules and pointers, not reference material. When a section grows into setup steps, command listings or exhaustive detail, it's user-facing (→ README.md) or a deep dive (→ docs/): link it, don't inline it.
  • Write for the next agent, not for the record. A line earns its place here only by changing what someone does next. Don't log what was done, renamed or decided — git history already holds that. Rare exception: a migration still in flight, where both the old and the new state are live; drop the note once it lands.

Toolchain (Node 24 + pnpm 12) is the dev floor; the published package still runs on Node 22+ (a CI job exercises the packed tarball on Node 22).

On Claude Code on the web, the Node environment is set up automatically at session start (dependencies installed), so tests and linters work right away.

Iterating on tool code? Start the server with --dev (stdio) to expose a reload_tools tool. After editing a src/tools/*.ts file, call reload_tools to re-register the whole toolset over the live session — the edit takes effect with no restart and no client reconnect. Details in docs/configuration.md (Dev mode).

Claim before you build — no duplicate work

Parallel duplicate work has happened here. Before coding an issue: re-read it live (closed completed / released → it already shipped, stop); search open and merged PRs for its number (is:pr #<n>) — coordinate on an open one, don't fork; then self-assign, drop a "picking this up" comment, and open your PR as a draft early. Link the issue in the PR description with a closing keyword (Closes #<n> — not Implements/bare #<n>, which don't close it) so it auto-closes on squash-merge to main. Need a follow-up on already-shipped work? Open a new issue for the delta. Fuller checklist in CONTRIBUTING.md.

Branch name: <issue>-<kebab-slug>, the issue number first and no type prefix (278-ticket-followers-ccs, not feat/278-...). No issue number only when there is genuinely no issue, which the claim rule above makes rare. Keep the slug short, lowercase, hyphen-separated (never underscores) and about the change, not the file it touches. Branches named claude/* come from Claude Code on the web, which generates them: leave those alone, don't imitate the shape by hand.

Architecture

Transports: stdio and a node:http HTTP transport next to its own OAuth authorization server (auth/server/, on oidc-provider), both serving MCP 2025 and 2026-07-28 (docs/decisions/protocol-eras.md). A user's Zendesk token never lives in process-wide state: it reaches a tool only through the context the transport builds for that user, so a call cannot run with another user's token. A notification carries no sensitive data; requestState goes only through utils/request-state.ts; diagnostics go to stderr, never to an MCP logging channel. index.ts imports each transport lazily, and dist/ inlines every dependency (the package declares none): never statically import the HTTP stack, or a package only stdio needs (such as open), from a module both transports load — tests/unit/module-graph.test.ts pins it. Same for the article HTML <-> Markdown pipeline (utils/article-sections.ts, cheerio, unified): only Help Center article handlers need it, so they load it through utils/article-format.ts on first use.

Customizations (src/customization/) are HTTP-only and reach /mcp through the extraInstructions / decorate hooks of createMcpServer: server.ts must not import them. Their back-office is /mcp/admin, its own OAuth resource gated on the Zendesk admin role; its tools are not in createAllTools and never join a namespace. Why: docs/decisions/customizations.md.

Tool modes (chosen at startup by --mode): all (every tool individually), namespace (default — one proxy per namespace), single (one zendesk proxy, plus connector_preferences for this server's own settings). Proxies take { operation, params } and validate params through the original Zod schema. Each proxy dispatches only within its own scoped handler map, so a zendesk_tickets proxy cannot invoke a help_center operation. --namespace / --read-only / --tool are applied by filterTools before the mode switch; --tool also forces mode: all (config.ts).

Container base image: the Dockerfile tag names the full Node and Alpine versions (the README badge repeats it), and a PR that moves them carries a Docker Scout before/after comment (.github/workflows/base-image.yml) — read it before digging into a base bump.

Every Zendesk API request goes through performFetch in client/zendesk-api.ts (the OAuth token exchange in auth/browser-oauth.ts deliberately does not), so retry and network-error wrapping are decided once, in client/retry.ts: a new client method must pick its row in that policy table, where a write is never replayed once it may have reached Zendesk. Why the loop is ours rather than a library's — and what would change that: docs/decisions/client-retry.md.

Auth — per-user OAuth 2.1 PKCE only, no static API-token mode. stdio: lazy browser PKCE via token-store.ts. HTTP: the server is the authorization server (DCR, CIMD, consent), Zendesk the upstream login through the same public client, and /mcp accepts only its own audience-bound JWE — never a Zendesk token (docs/decisions/oauth-authorization-server.md). Dropping API-token auth is deliberate (static shared credential — insufficiently secure, doesn't scale to multi-user/remote); the rationale lives in README.md ("What this server does not do").

Local setup and auth flows live in README.md; the customer-facing walkthrough in docs/end-user-onboarding.md; CLI flags and env vars in docs/configuration.md; remote HTTP deployment in docs/http-deployment.md; troubleshooting in docs/troubleshooting.md; manual tool testing in docs/live-testing.md.

Two audiences

Agents and end users are different products sharing a codebase: the agent surface speaks /api/v2/tickets, the end-user one /api/v2/requests. The requests namespace is excluded from DEFAULT_NAMESPACES (config.ts), so it ships opt-in.

Changing the agent ticket surface? Ask whether the end-user side needs the equivalent, and when it doesn't, say why in the PR. They are not mirrors.

Never check an end-user tool with an agent token: several operations behave differently for the two roles, so that check passes where a customer's real call fails. Which ones, why opt-in, and the rejected --profile alternative: docs/decisions/end-user-namespace.md.

Opt-in is not a lower quality bar: Glama scores the flat surface, so a thin definition behind a flag drags the score down like any other.

Design principle — usage-first, not API-shaped

Design every tool for what an LLM needs to accomplish a real task, not for what the Zendesk API happens to expose. The API's endpoints, names, pagination and data shapes are an implementation detail, not a template — they may be awkward or built for a use case that isn't ours. Don't mirror them 1:1, and don't copy other MCP servers just because they exist; neither is evidence of what serves the LLM. A single tool may fan out to several Zendesk calls and join, reshape or drop the result to hand back exactly what the use case needs — that cross-calling is expected, not a smell. When API shape and usage pull apart, follow the usage. (This never overrides the multi-agent rule below: internal naming coherence and the quality bar still hold — the freedom is from Zendesk's shape, not from craft.)

Testing

  • New features: TDD — failing test first. Bug fixes: reproduce with a test first.
  • Existing tests are sacred: a failure is a potential regression — find the root cause before touching the test.
  • Mock Zendesk with MSW (tests/msw-handlers.ts), never the real API.
  • Exact-output assertions go through toMatchInlineSnapshot, committed filled — an empty one is auto-filled, passes, and kills no mutant. A -u on a formatter changes what an agent reads back: treat it as a behaviour change and justify it. Rule: CONTRIBUTING.md (Code standards).
  • Extend tests/integration/ when you add/change a tool, mode, filter or transport; shared behaviour goes in registerCoreScenarios. Coverage thresholds in vitest.config.ts are a ratchet.
  • Inter-LLM functional tests (proxy annotations, [RO] prefix on tools/list) live in tests/functional/; invoke via /functional-testing. Details in tests/functional/README.md.
  • Mutation testing (StrykerJS) runs over the scope in stryker.config.mjs, and CI fails a PR on a mutant that survived in a line it changed (pnpm test:mutation:diff origin/main HEAD reproduces it). That gate is the whole contract: the score is not a target and 100 % is not the goal. A survivor in a line you changed means an assertion is missing or too loose — tighten it, never weaken one to go green. The scope is kept at zero escaped mutants: a survivor that is not worth a test (internal log names without an observability contract, human-only debugging prose, equivalents) gets a // Stryker disable next-line <mutator>: <reason> whose reason is one sentence, two lines at most, so the dashboard shows only new, untriaged survivors. Rationale, scope and costs: docs/decisions/mutation-testing.md.

Planning

Measure before you plan. When a plan would rest on how Zendesk really answers (a sideload honoured or dropped, where a field sits, who may call an endpoint) and the tools' formatted output can't show it, scope with a probe spike first: a read-only scripts/probe-*.ts plus a protocol in a draft PR, run by an independent agent who posts the raw shapes; the plan is written from that report, and no feature code lands before the owner approves it. Use the probe-spike skill. Agents never handle the Zendesk token — no reading, no exporting, no curl: the scripts resolve it themselves (docs/live-testing.md).

An implementation plan that changes MCP-runtime-observable behaviour — the tool surface, transports, auth, resources, prompts, persistence or timing a running server exposes to a client — must also carry a functional validation plan for that behaviour, written for an independent validator (another agent or a human), not the implementer. It lives in the PR description; the validator posts their report as a PR comment (English). Author the plan with the functional-validation-plan skill; the validator executes it with the run-validation-plan skill — don't inline either here.

Independence is about context, not identity. Every agent here acts under the same human's GitHub account, so the author shown on a PR comment says nothing about which session wrote it. A validator is independent when it did not write the code: a separate session handed the branch and the plan, without the implementer's conversation. Sharing the account, the machine or the tenant does not make it the implementer, and a report is judged on its evidence, not on its login. State the role behind a run once, in one line; the repository owner decides whether it counts, and that decision is not reopened comment after comment.

Pure tooling / dev-tooling changes are exempt — build scripts, the typecheck/lint/format setup, dev-only scripts, CI and release automation change nothing a client observes at runtime, so the standard gates (pnpm typecheck / pnpm check / pnpm test + CI) are the validation; no third-party pass. The test: if a running server behaves no differently for a client, it's tooling.

Code style

  • TypeScript strict; Biome for lint/format (pnpm check).
  • Keep the !!**/node_modules force-ignore in biome.json files.includes: it stops Biome 2's scanner from opening every dependency file for its module graph (~40% of pnpm check wall time, worse on slow filesystems), and excludes them from the scan, not the lint — diagnostics here are unchanged. Revisit if a type-aware rule needs dependency types. biome.json rejects comments, hence this note.
  • Lint runs on every edit, formatting only at pre-commit (lefthook.yml). Keep --skip=types on the PostToolUse hook — those two rules dominate its cost and pre-commit still enforces them. Don't add formatting back to the per-edit hook. Why: docs/decisions/lint-tooling.md.
  • Lint and format through the pnpm scripts (pnpm check, pnpm check:fix).
  • Enable a lint rule only once the tree is already clean for it, so pnpm check stays green in the same commit. Which rules are on, and a line on every rule that is off: docs/decisions/biome-rules.md. nursery and the types domain are closed by policy — read that file before proposing either.
  • Comments are optional; the code is the first explanation. Write one only for what the code cannot say — the why: a constraint, a trade-off, a non-obvious API behaviour, a pointer to docs/decisions/. A comment that paraphrases the line below it is noise: delete it, or rename the identifier so it isn't needed. Cap an implementation comment at ~50 words; a /** */ block documenting an exported symbol's contract has its own, wider cap of ~100. Past either, it's a decision doc with a one-line pointer here.
  • Functional: pure functions, immutable data, no classes (except ZendeskApiError).
  • Tool handlers are standalone functions in ToolDefinition[] arrays.
  • ASCII-only error messages on auth paths — node:http rejects non-ASCII bytes in WWW-Authenticate and other headers (ERR_INVALID_CHAR), which surfaces as a 500 instead of the spec-required 401.

Environment variables

  • ZENDESK_* is reserved for what describes the Zendesk side: the tenant, or something registered in it (ZENDESK_SUBDOMAIN, ZENDESK_OAUTH_CLIENT_ID). A knob only this server reads to tune itself never takes the prefix.
  • Server knobs take no prefix, but a name so common that another program in the same shell or container reads it (HOST, SECRET) gets qualified (LISTEN_HOST). PORT stays bare on purpose: PaaS platforms inject it.
  • Read every variable through readEnv (src/utils/env.ts). A rename is never a big bang: map the old name there, so it keeps working with a deprecation warning, and delete the mapping in the next major. Docs show the new name only, plus the rename table in docs/configuration.md.
  • Dev-only variables (scripts, CI) rename outright: no alias.

Communication language

GitHub (PRs, commits, comments, code) in English. Chat follows the user's language.

Multi-agent compatibility — absolute rule

Must work with every mainstream MCP agent; no PR may degrade one. Every new tool must meet the Glama Tool Definition Quality bar (clear purpose, usage guidelines, stated side effects, per-parameter .describe()) and keep the tool set coherent (naming, disambiguation, no needless duplication) — the server score is 60% mean + 40% min, so one weak tool drags the whole surface down. Every tool change must keep what agents depend on in the exposed JSON Schema (draft 2020-12): never drop a field or loosen a type, and never drop what a description said — though saying it in fewer words is a win, and fixing a false one is a duty. Criteria, the checklist and how to diff the schema: docs/mcp-metadata.md. Reviewing a diff that touches src/tools/**, /code-review included: apply the tool-definition-review skill — a tool definition an agent now reads worse is a finding, not a style point.

Documentation maintenance

Any change to the tool surface syncs the tool tables in docs/mcp-tools-reference.md (and, if a whole namespace appears or disappears, the namespace list in the README.md "Tool surface" section) in the same PR. Prose and section headers are deliberately count-free (no per-section (N tools) or global tool totals) — don't reintroduce hardcoded counts, they only go stale. Exact counts still live where they're load-bearing: namespace counts in tests/unit/routing/registry.test.ts and the length assertions in tests/unit/tools/*.test.ts — update those. Proxies surface only a tool's first sentence and its parameter names, in the operation schema field, while the proxy description stays under the 2048-char client cap — keep the first sentence standalone. Limits and how to re-verify them: docs/decisions/proxy-schema-surface.md. Changes to the non-tool MCP surface (server instructions, resources, prompts) sync docs/help-center-context.md in the same PR — that's where the mechanics, per-flag toggles and request costs live; the README.md section is a short "why" summary and only changes when the pitch does.

The README.md is deliberately why/what, not how: keep setup mechanics, flags, costs and internals in docs/ and link them. Don't restate a point the README already makes elsewhere — the FAQ in particular is not a place to repeat the body.

Submission quality bar

Before opening a PR, clear the Submission quality bar in CONTRIBUTING.md (same bar for human and AI authors).

Bundle size

CI checks every dist/ chunk against bundle-budget.json (a reference per chunk, +10 %) and bundle-ceiling.json (fixed at 1.5 × a measured baseline), through scripts/bundle-size.mjs. A reference is a tripwire, not a limit: when the growth is justified, raise it in the same PR and say why; otherwise remove the cause. Never raise the ceiling: stop and ask the repository owner. Why, and the locks on the ceiling: docs/decisions/startup-footprint.md.

Release workflow

Fully automated via semantic-release on push to main; never hand-bump version in package.json or the version-controlled server.json — release syncs both. Reseed server.json with pnpm build:server-json when its metadata changes. Details: docs/release-automation.md.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.