Commit Graph
10 Commits
Author SHA1 Message Date
claudeandClaude Opus 5 83af1ce3f0 deps: migrate to mcp 2.x (lift the <2 pin)
mcp 2.0.0 removed `mcp.server.fastmcp`; the `<2` ceiling in 23c7a3d
stopped the bleeding but left this server on the previous major.
Ports to the 2.x API, following the same shape seed-mcp shipped.

- requirements.txt: `mcp[fastmcp]>=1.0.0,<2` -> `mcp>=2,<3` (2.x has
  no [fastmcp] extra)
- server.py: FastMCP -> mcp.server.mcpserver.MCPServer; `mcp.settings`
  is gone, so host/port/stateless_http/transport_security are now
  run() kwargs
- stateless_http is gated to streamable-http. Under 1.x it was a
  constructor arg and so applied to sse too; sse is a dev-only path
  here, and this matches seed-mcp.
- CI: both workflows now run `python -c "import docs_mcp.server"`
  against the built image before pushing it. This is the durable
  half — a green build can no longer ship a non-importing image.
- CLAUDE.md / README.md: correct the FastMCP references. PLAN.md is
  left alone; it tracks the upstream template, not this repo.

Verification (mcp 1.27.1 -> 2.2.0):
- tools/list dumped in wire format is byte-for-byte identical,
  md5 cea0326f59b55abbbf47c9679252d38f both sides. All 5 tools
  (search_docs, get_page, list_versions, corpus_status,
  crop_chem_api_lessons) unchanged, so routing is unaffected.
- streamable-http: `initialize` -> 200, no mcp-session-id header
  (stateless_http confirmed active); tools/list and a real
  tools/call over the wire both succeed.
- stdio: initialize + tools/list both fine.
- Image built; the new CI smoke command passes against it.
  corpus_status reads the baked indexes (4,164 labels / 216,467
  chunks) and a live `search_docs` returns hits with mode=hybrid-rrf
  against Ollama on .0.125.

No eval numbers: this touches transport and packaging only. The
chunker, embedder, Chroma/BM25 stores, RRF and the reranker are all
untouched, and the live search above confirms retrieval still runs.

Note: `serverInfo.version` now reports "" instead of the SDK version
(2.x behaviour for an unversioned server). Cosmetic, but visible to
clients. httpx2 + opentelemetry-api come in as 2.x deps and coexist
with the existing httpx 0.28.1 pin, as expected.

Closes #4

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01FFBDnRWHispovmJVK9rXc9
2026-09-10 19:47:32 -04:00
claude 98842d1ed6 perf(epa_ppls): make the monthly refresh fit the runner's 3 h budget (#3)
Image rebuild (skip scrape) / build (push) Successful in 1h46m17s
Co-authored-by: claude <[email protected]>
2026-09-01 16:40:48 -04:00
claude 0f296a0ec4 fix(ci): image tag was empty on scheduled runs (#2)
Co-authored-by: claude <[email protected]>
2026-09-01 11:19:09 -04:00
justinandClaude Opus 4.7 3a279212ef ci: continue-on-error for the GC step (403 with current PAT scope)
Run #122 finished green-on-everything-that-matters: indexing,
docker login (REGISTRY_TOKEN fix worked), build + push, and the
package-link API call all succeeded. The image is published with
all four expected tags: latest, c5ed5560fc, corpus-2026.05.24,
a97107de46 (manual earlier push).

Only the final GC step failed with HTTP 403 enumerating
/packages/.../versions — the PAT we use as REGISTRY_TOKEN has
push/pull scope but not the broader package-admin scope needed
to list + delete old versions.

GC is housekeeping, not part of the publish path. Marking it
continue-on-error: true keeps the whole run green so monitoring
can rely on "red = real problem." Both workflows get the same
treatment.

Followup TODO baked into the workflow comments: mint a separate
PAT with admin:package scope and add it as a second secret
(PACKAGES_ADMIN_TOKEN) — then point the GC step at it. Then
remove continue-on-error.

Workflow-only commit, doesn't trigger image-only.yml (path filter
excludes .gitea/**).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-24 16:29:54 -04:00
justinandClaude Opus 4.7 719cfee2ca ci: add concurrency control to image-only.yml
Two rapid commits today (2acba0a → c5ed556) queued back-to-back
runs of the same workflow. Each run is ~90 min (the reindex is the
long pole), and the older commit's image just gets overwritten by
the newer one anyway — so the older run is pure waste.

  concurrency:
    group: image-only
    cancel-in-progress: true

cancels any in-flight run on the same workflow when a new push
lands. Future-me will thank present-me.

(The workflow-file change doesn't itself trigger image-only.yml
since .gitea/workflows/** isn't in the paths: filter — so this
commit doesn't kick off a third run.)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-24 14:27:31 -04:00
justinandClaude Opus 4.7 420b4fa2d8 workflows: use LAN registry endpoint for push (CF 100MB cap)
Cloudflare in front of git.jpaul.io caps HTTP request bodies at
100 MB, which kills container blob pushes for our 6 GB image
(Chroma layer alone is ~2 GB). Per the template gotcha doc:

  Push via LAN endpoint (192.168.0.2:1234, plain HTTP, in the
  Gitea host's insecure-registries list).
  Pull via public hostname (git.jpaul.io) — pull response bodies
  aren't capped.

REGISTRY_PUSH:  192.168.0.2:1234
REGISTRY_PULL:  git.jpaul.io   (unchanged; used for the package-link API)

This matches how hvm-docs / morpheus-docs / opsramp-docs / zerto-docs
CI workflows push successfully on the same Gitea host.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-24 12:47:41 -04:00
justinandClaude Opus 4.7 a97107de46 docker: production image + Gitea Actions for monthly refresh
Image rebuild (skip scrape) / build (push) Failing after 1h37m12s
Dockerfile: self-contained image with corpus + Chroma + BM25 baked
in. Drawbar's compose pulls + runs without volume mounts. Built from
sources.json (labels schema), PRODUCT_NAME=crop_chem by default,
HYBRID_SEARCH=true (always-on for production quality). RERANK_URL +
OLLAMA_URL get set at compose time.

.gitea/workflows/refresh.yml: monthly cron (1st @ 06:00 UTC) does
full scrape → reindex → image push. Scrapes Bayer (~30 min) +
EPA PPLS row-crop filtered (~7h). Skips reindex+push if no corpus
diff. Tags pushed: :latest, :<sha12>, :corpus-<YYYY.MM.DD>.

.gitea/workflows/image-only.yml: on-demand or auto on code-only
pushes to main (paths: docs_mcp/, rag/, scrape/, requirements.txt,
Dockerfile, sources.json). Reindexes from committed corpus, builds
image, pushes. ~10 min vs ~9h full refresh.

.gitignore: corpus/ now COMMITTED (4,159 labels, 265 MB of .md +
sidecars). Lets image-only.yml rebuild indexes without re-scraping.
chroma/ + bm25/ still gitignored (regenerable binary indexes).

.dockerignore: drops venv, eval results, PLAN/README/CLAUDE.md,
deploy/, .git/ — keeps the image lean. corpus + chroma + bm25
explicitly NOT in dockerignore (those go INTO the image).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-24 12:32:41 -04:00
justinandClaude Opus 4.7 43728320bf ci: default PRODUCT_NAME to repo name (caught by template dispatch test)
First dispatch on the empty template failed at Chroma collection
creation because PRODUCT_NAME was the literal string "<product>"
(YAML doesn't expand placeholders), and Chroma rejects collection
names containing characters outside [a-zA-Z0-9._-]:

  chromadb.errors.InvalidArgumentError: Validation error: name:
  Expected a name containing 3-512 characters from [a-zA-Z0-9._-],
  starting and ending with a character in [a-zA-Z0-9]. Got:
  <product>_docs

Same fix as the IMAGE env: derive from the repo name dynamically
via ${{ github.event.repository.name }}. Cloners can still override
explicitly, but a fresh clone now runs the index-rebuild step
cleanly out of the box.

Verified by re-dispatch — should fail next at docker login (placeholder
REGISTRY_PUSH hostname), which is the next-expected fail point and a
real per-deployment config the cloner has to fill in.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-22 09:37:07 -04:00
justinandClaude Opus 4.7 33b0fd652e ci: derive image name + package linking from repo, add link step
Both workflows had a static IMAGE env (<owner>/<product>-docs-mcp)
and a static --package arg in the GC step. Switch both to Gitea
Actions context variables so a clone of the template into any repo
name works on the first CI run without find/replace:

  IMAGE: ${{ github.repository_owner }}/${{ github.event.repository.name }}
  --owner ${{ github.repository_owner }}
  --package ${{ github.event.repository.name }}

Also add the "Link container package to this repo" step that was
missing from the template (and which, naively copy-pasted from the
reference build, would have linked everything back to docs-mcp-
template). The new step derives owner + package + link-target all
from the running repo's context.

The github.* namespace is Gitea Actions' inherited GitHub-Actions
context — values come from the Gitea server, not github.com. Same
mechanism the reference build's $GITHUB_SHA tag-builder uses.

CLAUDE.md updated to note that image and package naming are
repo-derived; only registry endpoints and the Ollama URL need
per-clone editing.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-22 09:34:26 -04:00
justinandClaude Opus 4.7 9ba615c8ee initial: docs-mcp-template — build guide + scaffolded server
Template for building hosted MCP servers over a product's public
documentation. Distilled from one production build; everything
product-specific has been factored out.

Contents:

- PLAN.md — comprehensive build guide. 13 phases from project
  skeleton through weekly_digest. Includes the gotchas
  ("fetch-depth: 0 always", reranker per-pair token limit,
  Cloudflare body cap, dash-not-bash on Gitea runners), the
  decisions worth carrying forward, and a per-product
  customization checklist.

- CLAUDE.md — guidance for Claude Code working in a clone of this
  template. Phase identification table, conventions (env-gating +
  operator confirmation for side-effecting tools, defensive
  fallback for retrieval components), common commands.

- README.md — quick-start summary.

Scaffolded code (all signature-stable, with NotImplementedError
stubs where phase-specific work is required):

  docs_mcp/server.py    FastMCP server, stateless_http=True, with
                        search_docs / get_page / list_versions
                        baseline tools and commented stubs for the
                        rest of the phase set.
  docs_mcp/usage.py     TimedCall telemetry, JSONL, daily rotation,
                        90-day retention. Reusable as-is.
  rag/embeddings.py     Ollama embedder (nomic-embed-text default),
                        load-balanced across N URLs. Reusable.
  rag/chunk.py          Paragraph-aware chunker with synthetic
                        chunk 0. Per-product tunable.
  rag/index.py          Chroma + BM25 builder. --rebuild and
                        --bm25-only flags.
  rag/bm25.py           SQLite FTS5 lexical index. Reusable.
  scrape/changelog.py   --cached / --ref / --json / --history-out.
                        Reusable.
  scrape/README.md      What you write per-product.
  eval/queries.jsonl.example
                        Curate ~25 hand-labeled queries here.
  eval/retrievers.py    Retriever protocol + stub classes.
  eval/run_eval.py      MRR / Recall@K / nDCG@K harness skeleton.
  scripts/usage_report.py
                        Standalone log analyzer; the
                        FOLLOW-UP CHECKS pattern noted in the
                        module docstring.
  scripts/registry_gc.py
                        Gitea container registry cleanup. Reusable.

Deployment + CI:

  Dockerfile               Python 3.12-slim; COPY corpus + chroma
                           + bm25 last for cache efficiency.
  deploy/docker-compose.yml MCP + reranker sidecar + Watchtower.
                           Templated with <placeholders>.
  .gitea/workflows/refresh.yml    Weekly cron + manual dispatch.
                                  fetch-depth: 0, retry-on-race,
                                  three-tag image scheme.
  .gitea/workflows/image-only.yml Code-only ship cycle, ~18min.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-22 09:18:17 -04:00