Skip to content

feat(agents): serve markdown to agents, add AGENTS.md, skills, and a docs MCP server - #4161

Closed
naveen-bandela-au2 wants to merge 8 commits into
developmentfrom
feat/agent-readiness
Closed

naveen-bandela-au2 wants to merge 8 commits into
developmentfrom
feat/agent-readiness

Conversation

@naveen-bandela-au2

Copy link
Copy Markdown
Contributor

Why

gofr.dev scores 53/100 (grade C) on orank's agent-readiness scan. The website side of the fix is gofr-dev/website#227; this is the half that lives here.

This PR must land before that one has any effect in production.

docs/Dockerfile: static-server v0.0.9 → v1.0.0

This one line is what makes the website PR visible. v1.0.0 adds:

  • Accept: text/markdown on an extensionless route resolves to the <route>.md sibling the site now generates for every page, with Content-Type: text/markdown and Vary: Accept
  • a Markdown 404 body pointing at /sitemap.xml and /llms.txt, so an agent that guesses a path can recover
  • _headers support, which is how the site declares its Link headers, nosniff, Referrer-Policy, and the media types for its extensionless /.well-known documents

On v0.0.9 all of the above ships in out/ but nothing serves it — .md files come back as HTML-or-nothing and _headers is an inert file.

Verified locally: built the site with this repo's docs overlay, served out/ with static-server:v1.0.0, and confirmed all of the above (16/16 assertions, listed in the website PR).

AGENTS.md at the repo root

The scan probed this repository for agent configuration and found none — the existing AGENTS.md lives only on the website. This one is for agents working on the framework itself: repo layout, the PRs-target-development rule, the separate datasource modules, the public-API contract, and what to run before opening a PR. Distinct from docs/AGENTS.md, which is published at gofr.dev/AGENTS.md and is about building applications with GoFr.

skills/

Three agent skills by capability area — gofr-rest-api, gofr-datasources, gofr-observability.

Every API in them was checked against the source rather than written from memory, which caught two errors before they shipped: it is app.Metrics().NewCounter(...), not app.NewCounter(...), and AddRESTHandlers returns an error that the example was discarding.

mcp/docs-server/

An MCP server exposing the documentation as search_docs, get_doc, and list_sections over stdio, so an agent looks GoFr APIs up instead of guessing them.

go run gofr.dev/mcp/docs-server@latest

Design notes:

  • Fetches rather than embeds. Content comes from /llms-full.txt, cached once per process, so the server stays current without being re-released alongside the docs.
  • Fetch is deferred to the first tool call. MCP clients start their servers eagerly when the editor opens; a fetch failure at startup would read as "the server crashed" rather than "that one call could not reach the network".
  • Standard library only. This lives in the root gofr.dev module, and a documentation tool is not a good reason to add a dependency that every go get gofr.dev would then carry. go.mod and go.sum are unchanged.
  • Tool-argument problems return MCP isError results, not JSON-RPC errors, so the model can read the message and correct its next call. Notifications correctly get no reply.

95.9% test coverage (go test -cover ./mcp/docs-server/).

.claude/plugin.json

agent-plugins.org manifest tying the skills and the MCP server together.

Verification

go build ./...                          ok, go.mod/go.sum unchanged
go test ./mcp/docs-server/...            ok, coverage 95.9%
gofmt -l mcp/docs-server/                clean

Known limitation

The MCP server is stdio-only. The scan's MCP checks want a Streamable HTTP endpoint and a serverUrl that resolves over HTTP, so those points stay partly out of reach until there is somewhere to host it. Flagged rather than papered over.

…docs MCP server

gofr.dev scores 53/100 on orank's agent-readiness scan. The website side
of the fix is in gofr-dev/website; this is the half that lives here.

docs/Dockerfile: bump zopdev/static-server v0.0.9 -> v1.0.0. This is the
change that makes the website's work visible. v1.0.0 resolves
`Accept: text/markdown` on an extensionless route to the `<route>.md`
sibling the site now generates for every page, serves a markdown 404 body
pointing at /sitemap.xml and /llms.txt, and reads the `_headers` file the
site uses to declare its Link headers and the media types for its
extensionless /.well-known documents. On v0.0.9 all of that ships but
nothing serves it.

AGENTS.md at the repo root: guidance for agents working on the framework
itself — repo layout, the development-not-main branch rule, the separate
datasource modules, and the public-API contract. Distinct from
docs/AGENTS.md, which is published at gofr.dev/AGENTS.md and is about
building applications with GoFr. The scan probed this repo and found
neither.

skills/: three agent skills by capability area — gofr-rest-api,
gofr-datasources, gofr-observability. Every API in them was checked
against the source rather than written from memory, which caught
app.Metrics().NewCounter (not app.NewCounter) and the error return from
AddRESTHandlers.

mcp/docs-server/: an MCP server exposing the documentation as
search_docs, get_doc, and list_sections over stdio. Content is fetched
once per process from /llms-full.txt, so it serves current docs without
being re-released alongside them, and the fetch is deferred to the first
tool call so a network failure reads as a failed call rather than a
crashed server. Standard library only — this is in the root gofr.dev
module and a docs tool is no reason to add a dependency that every
`go get gofr.dev` would carry. 95.9% test coverage.

.claude/plugin.json: agent-plugins.org manifest tying the skills and the
MCP server together.
Replaces map[string]any with structs. The compiler now checks the wire
shape the MCP client parses, and the repeated JSON keys that goconst
flagged disappear rather than being suppressed.
corpus.load used sync.Once, which caches the RESULT including a failure.
A flaky network at the first tool call left every later search in that
editor session returning the same stale error, curable only by
restarting the MCP client. That directly contradicts the design note in
the PR body, which claims a failure reads as a failed call rather than a
crashed server. Now only success is latched; a failure retries on the
next call. Added a regression test that fails the first fetch and
succeeds the second. Coverage 95.9% -> 96.5%.

AGENTS.md said Go 1.25+; go.mod declares go 1.26.0, so a contributor
following the file could not build the module.

A JSON-RPC parse error returned a nil id, which omitempty drops from the
response entirely. The spec requires an explicit null when the id cannot
be read.
Second time this landed, in a different place: the first ignore rule was
scoped to mcp/docs-server/, but `go build ./mcp/...` run from the repo
root drops the binary at the root instead. 8.1 MB of Mach-O in the tree.

Root cause is that the artifact lands wherever the build was invoked
from, not in the package directory, so a per-directory rule was always
going to miss a case. Matching the name repo-wide in .gitignore covers
every invocation point.
I wrote the manifest from memory instead of reading the specification.
Fetching the published JSON Schema showed three problems:

The $schema URL 404s. The real one is
agent-plugins.org/schemas/1.0.0/plugin.schema.json.

`skills` and `mcpServers` are not manifest fields. The schema declares
only $schema, name, version, description, author, homepage, repository,
license, keywords and extensions, with additional keys rejected. The spec
discovers components by fixed locations instead: skills/<name>/SKILL.md,
which this repo already has, and a separate mcp.json at the plugin root.
So the arrays were invalid and redundant at the same time.

The manifest was at .claude/plugin.json. The spec requires it at the
plugin root, which is very likely why the scan that motivated this change
reported no plugin.json found in the repo.

Both files now validate against the published schemas: no missing
required keys, no unknown keys, and the stdio server entry matches
$defs/stdioServer.

Also from the same review pass: sections() mapped a root route to an
empty section name, which would have rendered as a nameless row in
list_sections output, and the same output said "1 pages".
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