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.
- functional-testingSkill
- functional-validation-planSkill
- probe-spikeSkill
- run-validation-planSkill
- tool-definition-reviewSkill
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
- Very specific: line 11
Keep each section short — durable rules and pointers, not reference material.
- Very specific: line 12
When a section grows into setup steps, command listings or exhaustive detail, it's user-facing (→ README.md) or a deep dive (→ docs/): link …
- Very specific: line 82
Every Zendesk API request goes through performFetch in client/zendesk-api.ts (the OAuth token exchange in auth/browser-oauth.ts …
- Distinctive: line 113
Never check an end-user tool with an agent token: several operations behave differently for the two roles, so that check passes where a …
- Distinctive: line 163
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 …
- Distinctive: line 181
Independence is about context, not identity: Every agent here acts under the same human's GitHub account, so the author shown on a PR …
- Very specific: line 200
Keep the !!/node_modules force-ignore in biome.json files.includes: it stops Biome 2's scanner from opening every dependency file for its …
- Very specific: line 215
Comments are optional; the code is the first explanation: Write one only for what the code cannot say — the *why*: a constraint, a …
- Very specific: line 236
Read every variable through readEnv (src/utils/env.ts).
- Very specific: line 248
Must work with every mainstream MCP agent.
- Distinctive: line 253
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 …
- Very specific: line 286
Before opening a PR, clear the Submission quality bar in CONTRIBUTING.md (same bar for human and AI authors).
≈ “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.
Files
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.jsonor 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-uon 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 inregisterCoreScenarios. Coverage thresholds invitest.config.tsare a ratchet. - Inter-LLM functional tests (proxy annotations,
[RO]prefix ontools/list) live intests/functional/; invoke via/functional-testing. Details intests/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 HEADreproduces 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_modulesforce-ignore inbiome.jsonfiles.includes: it stops Biome 2's scanner from opening every dependency file for its module graph (~40% ofpnpm checkwall 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.jsonrejects comments, hence this note. - Lint runs on every edit, formatting only at pre-commit (
lefthook.yml). Keep--skip=typeson thePostToolUsehook — 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 checkstays green in the same commit. Which rules are on, and a line on every rule that is off:docs/decisions/biome-rules.md.nurseryand thetypesdomain 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:httprejects non-ASCII bytes inWWW-Authenticateand 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).PORTstays 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 indocs/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.
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
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.

