Per-repo MCP tools 404 — routing falls back to /api/swagger instead of /api/v1/repos/{owner}/{repo} #36

Closed
opened 2026-06-10 05:37:57 +00:00 by mathias · 1 comment
Owner

Summary

Every per-repo MCP tool 404s while owner-only tools work. The leaked failure URL points at https://gitea.d-ma.be/api/swagger, so the per-repo route resolver falls back to a wrong base path instead of /api/v1/repos/{owner}/{repo}/....

Affected (per-repo path)

  • repo_get, repo_status -> 404
  • dir_list -> 404
  • workflow_run_list, workflow_run_status -> 404
  • issue_create -> 405 (route present but method misrouted)

Works (owner-only param)

  • repo_list (owner=mathias) -> returns repos incl. mathias/tapir

Evidence

  • repo_get(owner=mathias, repo=tapir) -> -32000 not found: 404 page not found
  • dir_list(owner=mathias, repo=hyperguild, path="") -> leaks the cause: -32000 not found: {"errors":null,"message":"not found","url":"https://gitea.d-ma.be/api/swagger"}
  • issue_create(owner=mathias, repo=gitea-mcp) -> -32000 unexpected status 405
  • Same failures on tapir (private) and hyperguild (public) -> not repo- or visibility-specific.

Diagnosis

Not auth (owner-scoped calls authenticate fine) and not a missing repo (repo_list enumerates it). Per-repo handlers construct the wrong upstream API URL — /api/swagger rather than /api/v1/repos/{owner}/{repo}/contents (and /actions/... for the workflow tools). Likely a base-URL / route-template bug in the per-repo resolver.

Impact

Cannot read CI runs, file contents, or repo metadata through the MCP front door. Owner-scoped operations unaffected.

Repro

  1. repo_get(owner=mathias, repo=tapir) -> 404
  2. dir_list(owner=mathias, repo=hyperguild, path="") -> 404 with url:.../api/swagger

Filed via gitea API after the MCP per-repo tools failed during a tapir push/CI check (var: DMABE_GITEA_API_TOKEN via op run).

## Summary Every **per-repo** MCP tool 404s while owner-only tools work. The leaked failure URL points at `https://gitea.d-ma.be/api/swagger`, so the per-repo route resolver falls back to a wrong base path instead of `/api/v1/repos/{owner}/{repo}/...`. ## Affected (per-repo path) - `repo_get`, `repo_status` -> 404 - `dir_list` -> 404 - `workflow_run_list`, `workflow_run_status` -> 404 - `issue_create` -> 405 (route present but method misrouted) ## Works (owner-only param) - `repo_list` (owner=mathias) -> returns repos incl. `mathias/tapir` ## Evidence - `repo_get(owner=mathias, repo=tapir)` -> `-32000 not found: 404 page not found` - `dir_list(owner=mathias, repo=hyperguild, path="")` -> leaks the cause: `-32000 not found: {"errors":null,"message":"not found","url":"https://gitea.d-ma.be/api/swagger"}` - `issue_create(owner=mathias, repo=gitea-mcp)` -> `-32000 unexpected status 405` - Same failures on `tapir` (private) and `hyperguild` (public) -> not repo- or visibility-specific. ## Diagnosis Not auth (owner-scoped calls authenticate fine) and not a missing repo (`repo_list` enumerates it). Per-repo handlers construct the wrong upstream API URL — `/api/swagger` rather than `/api/v1/repos/{owner}/{repo}/contents` (and `/actions/...` for the workflow tools). Likely a base-URL / route-template bug in the per-repo resolver. ## Impact Cannot read CI runs, file contents, or repo metadata through the MCP front door. Owner-scoped operations unaffected. ## Repro 1. `repo_get(owner=mathias, repo=tapir)` -> 404 2. `dir_list(owner=mathias, repo=hyperguild, path="")` -> 404 with `url:.../api/swagger` Filed via gitea API after the MCP per-repo tools failed during a tapir push/CI check (var: DMABE_GITEA_API_TOKEN via op run).
Author
Owner

Root cause — parameter-name contract mismatch, not a routing fault

Not a route resolver / base-URL bug. MCP dispatch was correct the whole time.

Every per-repo tool declares the repo identifier as name (and issue/PR index as number), but every caller — the claude.ai connector and LLMs primed on gitea's own owner/repo API — sends repo and index. The unmatched fields zero-valued the path segment, so the client built /api/v1/repos/{owner}//... and forwarded it upstream. Gitea answers an unmatched /api/v1 path with its generic 404 whose body always carries "url":".../api/swagger" — that swagger pointer is gitea boilerplate, not a misroute. repo_list "worked" only because it's the one tool requiring owner alone.

Confirmed by raw curl to the live pod (claude.ai bypassed): same tool, name arg → 200; repo arg → 404.

Fix (v0.2.8, deployed 184d5a9)

  1. parseArgs aliases reponame and indexnumber (explicit canonical wins; alias key preserved so pr_merge's real index is unaffected) — accepts the idiomatic argument names so the recurrence is gone.
  2. The gitea client rejects any path with a mid-path empty segment (//) before the HTTP call, returning ErrValidation instead of leaking gitea's opaque swagger-404.
  3. Round-trip dispatch test asserts every registered tool resolves and ships a parseable schema (shared RegisterAll), so a future tool can't silently fail to route — caught by CI, not by a 404 in a planning session.

Verified live

repo_get/dir_list/issue_get/issue_list with the old repo/index args → all . Canonical name/number. Owner-level repo_list regression → .

## Root cause — parameter-name contract mismatch, not a routing fault Not a route resolver / base-URL bug. MCP dispatch was correct the whole time. Every per-repo tool declares the repo identifier as **`name`** (and issue/PR index as **`number`**), but every caller — the claude.ai connector and LLMs primed on gitea's own `owner`/`repo` API — sends **`repo`** and **`index`**. The unmatched fields zero-valued the path segment, so the client built `/api/v1/repos/{owner}//...` and forwarded it upstream. Gitea answers an unmatched `/api/v1` path with its generic 404 whose body always carries `"url":".../api/swagger"` — that swagger pointer is gitea boilerplate, **not** a misroute. `repo_list` "worked" only because it's the one tool requiring `owner` alone. Confirmed by raw curl to the live pod (claude.ai bypassed): same tool, `name` arg → 200; `repo` arg → 404. ## Fix (v0.2.8, deployed `184d5a9`) 1. `parseArgs` aliases `repo`→`name` and `index`→`number` (explicit canonical wins; alias key preserved so `pr_merge`'s real `index` is unaffected) — accepts the idiomatic argument names so the recurrence is gone. 2. The gitea client rejects any path with a mid-path empty segment (`//`) before the HTTP call, returning `ErrValidation` instead of leaking gitea's opaque swagger-404. 3. Round-trip dispatch test asserts every registered tool resolves and ships a parseable schema (shared `RegisterAll`), so a future tool can't silently fail to route — caught by CI, not by a 404 in a planning session. ## Verified live `repo_get`/`dir_list`/`issue_get`/`issue_list` with the old `repo`/`index` args → all ✅. Canonical `name`/`number` → ✅. Owner-level `repo_list` regression → ✅.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/gitea-mcp#36