fix: routing pod cannot authenticate against gitea-mcp (401 on project_create) #13

Closed
opened 2026-05-18 14:02:46 +00:00 by mathias · 2 comments
Owner

Problem

project_create fails at step create_repo with mcp http 401: unauthorized when called via the routing pod. The routing pod cannot authenticate against the gitea-mcp server.

Symptoms

{"code": -32000, "message": "project_create step \"create_repo\" failed: mcp http 401: unauthorized"}

Investigation so far (2026-05-18)

What we know

  • routing pod is running and healthy
  • GITEA_MCP_TOKEN was patched into routing-secrets but appears to be empty
  • GITEA_MCP_STATIC_TOKEN in gitea-mcp-secrets appears to be a placeholder value (---...)
  • gitea-mcp logs show no auth errors — only startup message visible
  • gitea-mcp has been running since 2026-05-18T06:06:42Z with no restarts

What needs investigation

Question 1: What is GITEA_MCP_STATIC_TOKEN actually used for?

gitea-mcp has two token env vars:

  • GITEA_MCP_DEFAULT_TOKEN — likely the Gitea API token (for API calls to Gitea)
  • GITEA_MCP_STATIC_TOKEN — likely the MCP server auth token (for authenticating MCP clients)

Verify by reading gitea-mcp source:

grep -r "STATIC_TOKEN\|static_token\|StaticToken" ~/dev/gitea-mcp/
grep -r "GITEA_MCP_AUTH\|MCP_AUTH\|bearer\|Bearer" ~/dev/gitea-mcp/internal/auth/

Question 2: Is the GITEA_MCP_STATIC_TOKEN a real value or placeholder?

kubectl get secret -n gitea-mcp gitea-mcp-secrets \
  -o jsonpath='{.data}' | jq 'to_entries[] | {key: .key, value: (.value | @base64d)}'

If it's a placeholder, the real token lives elsewhere (SOPS encrypted file, .env on disk, etc).

Question 3: How does claude.ai authenticate to gitea-mcp today?

claude.ai calls gitea-mcp successfully (we use it in every session). Check what auth mechanism it uses — likely Dex JWT, not static token. If so, routing pod needs to use Dex JWT too, not a static token.

Check gitea-mcp auth middleware:

cat ~/dev/gitea-mcp/internal/auth/

Question 4: What does the mcpclient in hyperguild send as auth?

cat ~/dev/hyperguild/internal/mcpclient/client.go | grep -i "auth\|token\|bearer\|header"

The mcpclient.New(cfg.GiteaMCPURL, cfg.GiteaMCPToken) call — what does it set as the auth header? Does it match what gitea-mcp expects?

Expected fix

Once the correct token/auth mechanism is identified:

  1. If static token: extract real value from SOPS/secrets, patch into routing-secrets correctly
  2. If Dex JWT: routing pod needs to obtain a JWT token to call gitea-mcp — may need a service account or client credentials flow
  3. If no auth required for internal cluster calls: check if gitea-mcp has an internal (no-auth) endpoint on a different port

After fix

Re-run e2e test:

# From claude.ai session or direct curl:
routing:project_create(
  name="test-throwaway-003",
  description="E2E test",
  hypothesis="...",
  stack="go-web",
  folder="AGENTS",
  private=true
)

Expected: all five artifacts created — Gitea repo, GitHub repo, mirror sync, infra namespace, issue.

Cleanup needed

test-throwaway-001 and test-throwaway-002 Gitea repos still exist and should be deleted after this is resolved. GitHub repos too if they were created.

  • hyperguild #11 (e2e findings)
  • hyperguild #12 (GITHUB_PAT config — resolved)
  • infra commits: 92cdeccf, b6d8244, fd3d4d1 (deployment.yaml patches)
## Problem `project_create` fails at step `create_repo` with `mcp http 401: unauthorized` when called via the routing pod. The routing pod cannot authenticate against the gitea-mcp server. ## Symptoms ``` {"code": -32000, "message": "project_create step \"create_repo\" failed: mcp http 401: unauthorized"} ``` ## Investigation so far (2026-05-18) ### What we know - routing pod is running and healthy - `GITEA_MCP_TOKEN` was patched into `routing-secrets` but appears to be **empty** - `GITEA_MCP_STATIC_TOKEN` in `gitea-mcp-secrets` appears to be a placeholder value (`---...`) - gitea-mcp logs show no auth errors — only startup message visible - gitea-mcp has been running since `2026-05-18T06:06:42Z` with no restarts ### What needs investigation **Question 1: What is GITEA_MCP_STATIC_TOKEN actually used for?** gitea-mcp has two token env vars: - `GITEA_MCP_DEFAULT_TOKEN` — likely the Gitea API token (for API calls to Gitea) - `GITEA_MCP_STATIC_TOKEN` — likely the MCP server auth token (for authenticating MCP clients) Verify by reading gitea-mcp source: ```bash grep -r "STATIC_TOKEN\|static_token\|StaticToken" ~/dev/gitea-mcp/ grep -r "GITEA_MCP_AUTH\|MCP_AUTH\|bearer\|Bearer" ~/dev/gitea-mcp/internal/auth/ ``` **Question 2: Is the GITEA_MCP_STATIC_TOKEN a real value or placeholder?** ```bash kubectl get secret -n gitea-mcp gitea-mcp-secrets \ -o jsonpath='{.data}' | jq 'to_entries[] | {key: .key, value: (.value | @base64d)}' ``` If it's a placeholder, the real token lives elsewhere (SOPS encrypted file, .env on disk, etc). **Question 3: How does claude.ai authenticate to gitea-mcp today?** claude.ai calls gitea-mcp successfully (we use it in every session). Check what auth mechanism it uses — likely Dex JWT, not static token. If so, routing pod needs to use Dex JWT too, not a static token. Check gitea-mcp auth middleware: ```bash cat ~/dev/gitea-mcp/internal/auth/ ``` **Question 4: What does the mcpclient in hyperguild send as auth?** ```bash cat ~/dev/hyperguild/internal/mcpclient/client.go | grep -i "auth\|token\|bearer\|header" ``` The `mcpclient.New(cfg.GiteaMCPURL, cfg.GiteaMCPToken)` call — what does it set as the auth header? Does it match what gitea-mcp expects? ## Expected fix Once the correct token/auth mechanism is identified: 1. If static token: extract real value from SOPS/secrets, patch into `routing-secrets` correctly 2. If Dex JWT: routing pod needs to obtain a JWT token to call gitea-mcp — may need a service account or client credentials flow 3. If no auth required for internal cluster calls: check if gitea-mcp has an internal (no-auth) endpoint on a different port ## After fix Re-run e2e test: ```bash # From claude.ai session or direct curl: routing:project_create( name="test-throwaway-003", description="E2E test", hypothesis="...", stack="go-web", folder="AGENTS", private=true ) ``` Expected: all five artifacts created — Gitea repo, GitHub repo, mirror sync, infra namespace, issue. ## Cleanup needed `test-throwaway-001` and `test-throwaway-002` Gitea repos still exist and should be deleted after this is resolved. GitHub repos too if they were created. ## Related - hyperguild #11 (e2e findings) - hyperguild #12 (GITHUB_PAT config — resolved) - infra commits: `92cdeccf`, `b6d8244`, `fd3d4d1` (deployment.yaml patches)
Author
Owner

Resolved

Root cause

routing-secrets did not contain GITEA_MCP_TOKEN. The mcpclient.New(url, token) constructor accepts an empty token without complaint, and at request time it only sets Authorization: Bearer … when c.token != "". So the routing pod was sending requests with no Authorization header at all — gitea-mcp's BearerMiddleware rejects that outright as 401.

Answers to the investigation questions

  1. GITEA_MCP_STATIC_TOKEN is the MCP server auth token, compared constant-time against the incoming Bearer in internal/auth/bearer.go. GITEA_MCP_DEFAULT_TOKEN is the upstream Gitea PAT used by the gitea client. They're intentionally decoupled (see BearerMiddleware doc comment).
  2. Real value, not placeholderd7423fc3eabcb9d2f1ee2b0d88b5d15a2c51b54f in gitea-mcp/secrets.enc.yaml.
  3. claude.ai uses Dex JWT (the else branch of the middleware). Routing pod doesn't have a JWT-issuing identity inside the cluster, so static token is the correct choice for service-to-service.
  4. mcpclient sends Authorization: Bearer <c.token> only when token is non-empty. Empty c.token ⇒ no header ⇒ 401. Matches what gitea-mcp expects when the value is the static token.

Fix

infra commit 2b7d792:

  • sops set GITEA_MCP_TOKEN: d7423fc3… into routing-secrets.
  • Bumped pod-template secrets-revision annotation so the rollout picked up the new envFrom value (envFrom doesn't auto-reload on Secret changes — known gotcha).

Verification

E2E project_create(name=test-throwaway-003, …) via port-forward to routing/3210:

"reached":["create_repo","create_github_repo","mirror","infra_commit","issue"]

All five steps green.

Cleanup

  • mathias/test-throwaway-002, mathias/test-throwaway-003 deleted
  • staging/test-throwaway-003 branch on infra deleted
  • GitHub side: mathiasb/test-throwaway-003 still orphaned — gh token lacks delete_repo scope. Run gh auth refresh -h github.com -s delete_repo then gh repo delete mathiasb/test-throwaway-003 --yes. (-001 and -002 were 404 — never created on GitHub because GITHUB_PAT was missing during those runs.)

Closing.

## Resolved ### Root cause `routing-secrets` did not contain `GITEA_MCP_TOKEN`. The `mcpclient.New(url, token)` constructor accepts an empty token without complaint, and at request time it only sets `Authorization: Bearer …` when `c.token != ""`. So the routing pod was sending requests **with no Authorization header at all** — gitea-mcp's `BearerMiddleware` rejects that outright as 401. ### Answers to the investigation questions 1. **`GITEA_MCP_STATIC_TOKEN`** is the MCP server auth token, compared constant-time against the incoming Bearer in `internal/auth/bearer.go`. `GITEA_MCP_DEFAULT_TOKEN` is the upstream Gitea PAT used by the gitea client. They're intentionally decoupled (see `BearerMiddleware` doc comment). 2. **Real value, not placeholder** — `d7423fc3eabcb9d2f1ee2b0d88b5d15a2c51b54f` in `gitea-mcp/secrets.enc.yaml`. 3. **claude.ai uses Dex JWT** (the `else` branch of the middleware). Routing pod doesn't have a JWT-issuing identity inside the cluster, so static token is the correct choice for service-to-service. 4. **mcpclient sends `Authorization: Bearer <c.token>`** only when token is non-empty. Empty `c.token` ⇒ no header ⇒ 401. Matches what gitea-mcp expects when the value is the static token. ### Fix infra commit `2b7d792`: - `sops set` `GITEA_MCP_TOKEN: d7423fc3…` into `routing-secrets`. - Bumped pod-template `secrets-revision` annotation so the rollout picked up the new `envFrom` value (envFrom doesn't auto-reload on Secret changes — known gotcha). ### Verification E2E `project_create(name=test-throwaway-003, …)` via port-forward to `routing/3210`: ``` "reached":["create_repo","create_github_repo","mirror","infra_commit","issue"] ``` All five steps green. ### Cleanup - `mathias/test-throwaway-002`, `mathias/test-throwaway-003` deleted ✅ - `staging/test-throwaway-003` branch on infra deleted ✅ - GitHub side: `mathiasb/test-throwaway-003` still orphaned — `gh` token lacks `delete_repo` scope. Run `gh auth refresh -h github.com -s delete_repo` then `gh repo delete mathiasb/test-throwaway-003 --yes`. (`-001` and `-002` were 404 — never created on GitHub because GITHUB_PAT was missing during those runs.) Closing.
Author
Owner

RESOLVED — 2026-05-18

Root cause: GITEA_MCP_TOKEN in routing-secrets was empty. Fixed by CC on koala — correct static token patched into routing-secrets and routing pod restarted.

project_create e2e test passed after fix. All five steps reached including create_repo which was previously 401.

Remaining action

GITEA_MCP_TOKEN should be added to the SOPS-encrypted secrets.enc.yaml for the routing pod so it survives secret rotation and cluster rebuilds — currently only patched imperatively into routing-secrets.

## ✅ RESOLVED — 2026-05-18 Root cause: `GITEA_MCP_TOKEN` in routing-secrets was empty. Fixed by CC on koala — correct static token patched into routing-secrets and routing pod restarted. project_create e2e test passed after fix. All five steps reached including `create_repo` which was previously 401. ## Remaining action GITEA_MCP_TOKEN should be added to the SOPS-encrypted `secrets.enc.yaml` for the routing pod so it survives secret rotation and cluster rebuilds — currently only patched imperatively into routing-secrets.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/hyperguild#13