feat(mcp): add anonymous public repository exploration #132

Merged
ClaudiuSchuster merged 8 commits from oss-oo/p1-public-mcp-catalog-20261014 into main 2026-10-15 09:40:43 +00:00
Owner

Agents can explore public Laboratory repositories without first obtaining credentials through the new POST /mcp/public endpoint. Its four tools support repository search, repository metadata, paginated directory browsing and lossless chunked file retrieval. Existing authenticated /mcp behavior remains available separately.

The public client never uses gateway/registrar credentials, follows no upstream redirects and presents private or missing repositories uniformly. Caddy restricts the public endpoint to POST, omits public CORS permissions and supplies the client address for its independent rate budget. Named tags resolve through the exact tag endpoint; raw file responses must honor Range and are read only up to the requested window plus one detection byte. Larger files remain retrievable in chunks.

Validation at 7b2e5d56576395d5ff6780fe697fe4983ce17a85 (tree cec0eab20c3e88c8f408674d001c4c3e47e5cfab):

  • 81 public-MCP unit/loopback tests; independent correction review additionally ran 46 targeted tests, eight optimized cases, parent regression reproductions and 13 negative probe-result mutations.
  • The full isolated end-to-end gate passes on the PVE test VM with the actual deployed Forgejo/Caddy image IDs. The new probe runs through the real Caddy snippet and verifies anonymous discovery/read/file SHA, method refusal, absent public CORS, health and authenticated compatibility.
  • The complete existing project/registration/event/console flows also pass. Test resources are removed; existing runner and resources remain unchanged. Only the documented test port substitutions affect the runtime copy.

This PR provides source and verified deployment instructions. Production activation is a separate Root-operated step after protected integration.

Agents can explore public Laboratory repositories without first obtaining credentials through the new POST `/mcp/public` endpoint. Its four tools support repository search, repository metadata, paginated directory browsing and lossless chunked file retrieval. Existing authenticated `/mcp` behavior remains available separately. The public client never uses gateway/registrar credentials, follows no upstream redirects and presents private or missing repositories uniformly. Caddy restricts the public endpoint to POST, omits public CORS permissions and supplies the client address for its independent rate budget. Named tags resolve through the exact tag endpoint; raw file responses must honor Range and are read only up to the requested window plus one detection byte. Larger files remain retrievable in chunks. Validation at `7b2e5d56576395d5ff6780fe697fe4983ce17a85` (tree `cec0eab20c3e88c8f408674d001c4c3e47e5cfab`): - 81 public-MCP unit/loopback tests; independent correction review additionally ran 46 targeted tests, eight optimized cases, parent regression reproductions and 13 negative probe-result mutations. - The full isolated end-to-end gate passes on the PVE test VM with the actual deployed Forgejo/Caddy image IDs. The new probe runs through the real Caddy snippet and verifies anonymous discovery/read/file SHA, method refusal, absent public CORS, health and authenticated compatibility. - The complete existing project/registration/event/console flows also pass. Test resources are removed; existing runner and resources remain unchanged. Only the documented test port substitutions affect the runtime copy. This PR provides source and verified deployment instructions. Production activation is a separate Root-operated step after protected integration.
mcp_public.py (agent platform P1): EXACTLY four exploration tools —
public_search_repositories, public_repo_info, public_list_tree,
public_get_file — plus its own anonymous-only upstream client, per-
client-IP sliding-window budget (fail-closed like the authenticated
tracker), HMAC-signed opaque cursors pinning the resolved commit, and
the raw+Range chunk transport verified per chunk (206, content-range
start/total, exact length, ETag == git blob sha) with a completeness-
checked /git/blobs fallback behind the upstream blob-size knob.

Structural isolation: no import of mcp_forgejo, no credential reads
(FORGEJO_TOKEN/USER/PASSWORD unreachable), no Authorization header on
any code path, redirects refused in-handler (never followed), strict
segment validation before any interpolation, and one identical
bounded refusal for private/nonexistent/missing-path outcomes.
Registrar and authenticated tool names are refused as unknown with
zero upstream calls.

Offline-testable by design; the e2e-local full-stack gate stays the
recorded pre-merge step (Root).
Additive routes only — the authenticated /mcp flow is byte-identical
in behavior. POST /mcp/public mirrors the authenticated handler MINUS
bearer/_authenticate/_record_activity: any inbound Authorization
header is stripped before anything else (never forwarded, never
logged), the per-client-IP budget runs before body read and any
upstream work (429 + Retry-After), and the outer catch answers a
sanitized bounded 500-class text. GET/DELETE /mcp/public answer 405
with Allow: POST; OPTIONS answers 204 with Allow: POST and
deliberately NO Access-Control-Allow-* headers — the server never
emits ACAO on the public route.

e2e-local full-stack gate remains the recorded pre-merge step (Root).
test_public.py: loopback fake forge (recording every upstream request,
headers included; fixture blobs carry REAL git blob shas) covering the
P1 spec cases — four-tool catalog shape and zero-upstream refusal of
registrar/authenticated names; zero Authorization on every upstream
request incl. nested resolution; same- and cross-origin redirect
refusal with the target never contacted; private-vs-nonexistent
byte-identical refusals; search projection/pagination; repo_info
allowlist + private:true refusal; list_tree byte-sort, windowing,
cursor-pinned continuation across a moved branch tip, mismatch/
tamper/out-of-range/snapshot-expired refusals, dotfiles; get_file
zero-byte, binary and unicode exact roundtrips, multi-chunk contiguity
and eof math, forged offset, 200-ignoring-range, ETag/content-range/
length verification failures, directory and submodule refusals,
cross-tool cursor confusion; blob-fallback integrity (corruption,
wrong length), knob-gated refusal and the no-cap raw path; route-level
budget independence (X-Real-IP), no-ACAO on every /mcp/public
response, and the authenticated /mcp bearer door still intact — driven
against the REAL RemoteMCPServer over loopback HTTP.

CI runs it beside test_listing.py in both -I and -I -O forms.
Nested FIRST inside the @mcp handle (the proven healthz placement
pattern — a matching handle preempts the anonymous-401 guard and the
CORS headers): POST-only proxy for /mcp/public with the same
X-Real-IP/X-Forwarded-For/-Forwarded hygiene, non-POST answered 405
at the edge, and no Access-Control-Allow-* on the public route. The
guard comment now names the public catalog as the second deliberate
anonymous exception beside liveness.

Live edge-order verification is the recorded runtime gate — this file
claims no deployed behavior; the e2e-local caddy leg validates the
snippet before any deploy (Root).
Document the four anonymous tools, the anonymous-only upstream client
(no credentials, redirects refused, one not-found shape), signed
commit-pinned cursors and their continuation rules, the Range chunk
transport with per-chunk verification and the completeness-checked
fallback behind the blob-size knob (with the honest capability
statement), the per-client-IP budget knobs, and the deliberate no-CORS
decision on /mcp/public.
Fold the continuation and first-call branches of public_list_tree into
a single walk/type-check/entries sequence; the snapshot-expired
mapping now covers a GC'd target TREE object too (not just a vanished
commit or path) — every 404 on a cursor-pinned snapshot reads as
'snapshot expired; restart the listing'.
fix(gateway): validation findings — deployed shapes, pagination, edge CORS scoping
All checks were successful
access-validation-contract / contract (pull_request) Successful in 1m30s
ci / ci (pull_request) Successful in 2m18s
efd8a1435e
Independent validation (ACCEPT-WITH-FINDINGS) found three must-fix
defects against the deployed forge, all fixed here with regressions:

- F1/F4: deployed /git/trees serves entries under 'tree' with
  page/total_count/truncated pagination — read the deployed key with an
  'entries' fallback and gather pages up to GATEWAY_PUBLIC_TREE_MAX_PAGES
  (default 20); an upstream truncated flag or a directory past the cap is
  an honest bounded refusal, never a silently short listing.
- F2: header names are case-insensitive and the deployed forge sends
  Go-canonical 'Etag' — responses now use a case-insensitive header view
  (raw chunk ETag, content-range, X-Total-Count lookups).
- F3: a bare sibling 'header Access-Control-Allow-Origin *' applies to
  /mcp/public edge responses too — scope it with a not-path matcher so
  only the authenticated MCP surface stays browser-readable.
- F5: a raw 404 during a get_file CONTINUATION maps to snapshot-expired
  (restart), never a fallback that could serve a different revision.
- F6: non-object JSON-RPC params answer -32602, not a 500.
- F7: path depth capped (GATEWAY_PUBLIC_PATH_DEPTH, default 64).
- F10: repo_info treats anything but an explicit false/absent private
  flag as not found (truthy strings from a misbehaving upstream never
  pass).

Suite: 73 tests (9 new regressions), green under -I and -I -O.
fix(mcp): resolve public tags and bound raw range responses
All checks were successful
access-validation-contract / contract (pull_request) Successful in 1m31s
ci / ci (pull_request) Successful in 2m27s
7b2e5d5657
Use the exact named tag endpoint, reject ignored Range responses before reading, and exercise the public catalog through the existing Caddy end-to-end gate. Independent source review and the complete isolated PVE gate pass on the committed tree.
ClaudiuSchuster changed title from P1: anonymous public-read-only MCP catalog (POST /mcp/public) to feat(mcp): add anonymous public repository exploration 2026-10-15 09:33:45 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
OSS-OO/laboratory!132
No description provided.