Skip to content

feat: write an AGENTS.md into generated projects - #33

Merged
IBJunior merged 1 commit into
mainfrom
feat/generated-agents-md
Sep 19, 2026
Merged

IBJunior merged 1 commit into
mainfrom
feat/generated-agents-md

Conversation

@IBJunior

@IBJunior IBJunior commented Sep 19, 2026 •

Copy link
Copy Markdown
Member

Every generated project now gets an AGENTS.md — working notes for whoever changes the project, as distinct from the README, which covers running it.

Fifth of the sequence. One PR left after this: installing the tool-design skill.

Why

Generated projects explained how to run themselves and nothing about how to change themselves. The traps that actually bite — state discarded between requests, stdout corrupting the protocol stream, a token rejection returning 500 instead of 401 — existed only as comments in the code, which is where you find them after the bug rather than before.

Example-agnostic on purpose

The shipped notes example exists so nothing starts empty, and it is meant to be deleted once real tools arrive. AGENTS.md stays. So any guidance naming notes-store or list_notes would rot the moment the example goes — the file would describe code that no longer exists.

It now says the example is meant to be replaced, then uses a neutral illustration (list_invoices, inv_4821) for every rule. A test asserts no example-specific name — notes-store, list_notes, get_note, create_note, summarize-notes — reaches any variant's output.

src/server.test.ts is still named, since that path survives the example being replaced.

No overlap with the README

The README keeps install, endpoints, Docker and OAuth provider tables. AGENTS.md has none of that; a test asserts it. The only deliberate overlap is a short command block, so an agent reading this file doesn't have to open another one to learn the test command.

It branches per variant, and the branching is the substance

Variant Gets
HTTP the per-request factory warning — createMcpHandler runs the factory once per request, so state on the server instance vanishes between calls: a write appears to succeed and the next read returns nothing
stdio the stdout rule instead, since the factory warning doesn't apply there and the protocol channel does
OAuth the OAuthError-not-Error rule, with the missing-JWKS guard called out as the deliberate exception
FastMCP a trimmed version stating plainly that it ships without tests, rather than leaving a gap the reader has to infer

Verified by generating all four and diffing their section lists:

a-http    Layout / State lives in the module / Writing a good tool /
          Organizing tools as they grow / Adding a tool means adding a test /
          Conventions worth keeping / Commands
a-stdio   Layout / stdout belongs to the protocol / ... (no per-request section)
a-oauth   ... + OAuth: throw OAuthError, never a plain Error
a-fast    Layout / Testing / Commands

Tool guidance, and organizing tools as they grow

SDK variants carry the tool-design rules — naming, parameter documentation with format and example, signalled truncation, actionable isError payloads, and not letting two different situations produce the same payload.

Then the section you asked for: how to organize tools as they grow.

  • A few tools — keep them in src/tools.ts, as now.
  • Enough that the file is hard to scan — split into src/tools/, one file per tool, with an index.ts re-exporting a single registerTools.
  • Several unrelated domains — group by feature: src/notes/{tools,resources,store}.ts.

The scaffold ships flat because that suits a handful of tools. The advice on outgrowing it belongs in prose, not in a directory structure the example hasn't earned.

One detail worth noting: the naming rule interpolates the real project name, so the warning reads my-server_invoices_list rather than an abstract example — the double-prefix problem is much more obvious when you see it with your own server's name.

Testing section

Repeats the framing the emitted suite already uses: one test per tool in that spirit is enough, and the nine shipped tests are a worked example, not a quota. Worth keeping consistent — the point of trimming the suite was to avoid implying a bar, and prose that contradicted that would undo it.

Verification

a-http   Tests  9 passed | build:ok | AGENTS.md:yes
a-stdio  Tests  9 passed | build:ok | AGENTS.md:yes
a-oauth  Tests  9 passed | build:ok | AGENTS.md:yes
a-fast   tests:none      | build:ok | AGENTS.md:yes
  • npm test — 456 tests across 18 files (up from 422); new tests cover the template, including that the OAuth and FastMCP sections appear and disappear correctly, and that no example-specific name leaks in
  • npm run lint clean; new files pass prettier --check
  • Command block alignment checked across npm/yarn and with/without the test line

One fix that came out of this

Adds vitest.config.ts. generated/ holds ~35 scratch projects from testing the CLI by hand — gitignored, but since PR #32 they ship their own test suites, so the repo's own npm test had started picking them up and failing on them. The config scopes the run to src/ and dist/.

Repo AGENTS.md and README updated to record the new template and the file it emits.

Generated projects explained how to run themselves and nothing about how to
change themselves. The traps that actually bite - state discarded between
requests, stdout corrupting the protocol stream, a token rejection returning
500 instead of 401 - were only comments in the code, where you find them after
the bug rather than before.

Adds src/templates/common/agents.md.ts, written for whoever edits the project
rather than whoever runs it. The README keeps the install and deployment story;
this has no overlap with it beyond a command block.

The file is deliberately example-agnostic. The shipped notes example exists so
nothing starts empty and is meant to be deleted once real tools arrive, but
AGENTS.md stays - so naming notes-store or list_notes in the guidance would rot
it the moment the example goes. It says the example is meant to be replaced,
then uses a neutral illustration (list_invoices) for every rule. A test asserts
no example-specific name reaches the output.

It branches per variant, and the branching is the substance:

- HTTP gets the per-request factory warning. createMcpHandler runs the factory
  once per request, so state on the server instance vanishes between calls - a
  write appears to succeed and the next read returns nothing.
- stdio gets the stdout rule instead, since the factory warning does not apply
  there and the protocol channel does.
- OAuth adds the OAuthError-not-Error rule, with the missing-JWKS guard called
  out as the deliberate exception.
- FastMCP gets a trimmed version that states plainly it ships without tests,
  rather than leaving a gap a reader has to infer.

SDK variants also carry the tool-design guidance - naming, parameter
documentation, signalled truncation, actionable errors, payloads that cannot
mean two things - and a section on organizing tools as they grow: flat files
while there are few, then src/tools/ one per tool, then feature folders once a
server spans unrelated domains. The scaffold ships flat because that suits a
handful of tools; the advice on outgrowing it belongs in prose rather than in a
directory structure the example has not earned.

The naming rule interpolates the real project name, so the double-prefix
warning reads "<project>_invoices_list" rather than an abstract example.

Also adds vitest.config.ts. generated/ holds scratch projects from testing the
CLI by hand; they are gitignored, but since they now ship their own test suites
the repo's own run was picking them up and failing on them.
@IBJunior
IBJunior force-pushed the feat/generated-agents-md branch from 789bffe to 4d968dd Compare September 19, 2026 16:49
@IBJunior
IBJunior merged commit ace478a into main Sep 19, 2026
2 checks passed
@IBJunior
IBJunior deleted the feat/generated-agents-md branch September 19, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant