Gitea MCP connector: MCP OAuth mode on bikoret.ch
The claude.ai connector ran the OAuth flow against a server in static-token mode, which has no OAuth endpoints: the login ended on a 404. The proper form is gitea-mcp's -oauth mode, where each user authorizes with their own Gitea account; it needs both origins public over HTTPS and Gitea's ROOT_URL to be that origin. - gitea-ingress.yaml: git.bikoret.ch → temple-council/gitea, TLS - gitea-public-patch.yaml: ROOT_URL/DOMAIN/SSH_DOMAIN = git.bikoret.ch (were a name that never resolved and a released NLB IP) - gitea-mcp-deployment.yaml: -oauth -oauth-public-url https://mcp.bikoret.ch -H https://git.bikoret.ch, no static token, probes on /healthz - gitea-mcp-ingress.yaml: host mcp.bikoret.ch (mcp.shorashim.ch retired) - gitea-public-apply.sh / gitea-public-verify.sh: the operator's DNS-gated apply and read-only verification (also in ~/.local/bin) - KOHEN.md, deploy/ARCHITECTURE.md: the new doors and the Gitea OAuth2 app ("Claude", PKCE, claude.ai + Claude Code redirect URIs) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
861cd4e64e
commit
53778f372e
9 files changed
+186
-31
No files matched your search
@@ -36,9 +36,12 @@ Kept in `deploy/k8s/`:
|
||||
- `manna-volume.yaml` — PVC for provision file
|
||||
- `deployment.yaml` — the pod with 3 containers + 2 init containers (`shoresh-engine-install`, `migrate`). Four images are still built and pushed; shoresh-engine ships a binary, not a running container.
|
||||
- `service.yaml` — ClusterIP and external LoadBalancer
|
||||
- `gitea-mcp-deployment.yaml` — separate MCP server for Gitea connector (optional)
|
||||
- `gitea-mcp-ingress.yaml` — TLS ingress for the MCP server
|
||||
- `gitea-mcp-secret.yaml` — template for Gitea PAT
|
||||
- `gitea-mcp-deployment.yaml` — Gitea MCP server for the claude.ai / Claude Code connector, MCP OAuth mode (users act as themselves in Gitea)
|
||||
- `gitea-mcp-ingress.yaml` — TLS ingress `mcp.bikoret.ch` for the MCP server
|
||||
- `gitea-mcp-secret.yaml` — template for a Gitea PAT (static-token mode only; kept for rollback)
|
||||
- `gitea-ingress.yaml` — TLS ingress `git.bikoret.ch` for Gitea itself (namespace temple-council), required by OAuth mode
|
||||
- `gitea-public-patch.yaml` — patch that sets Gitea's ROOT_URL/DOMAIN/SSH_DOMAIN to `git.bikoret.ch`
|
||||
- `gitea-public-apply.sh` / `gitea-public-verify.sh` — the operator's apply and read-only check for the four above
|
||||
- `cert-manager-issuer.yaml` — Let's Encrypt ClusterIssuer
|
||||
|
||||
## Containers
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Ingress for Gitea itself — public HTTPS at git.bikoret.ch
|
||||
#
|
||||
# Added 2026-10-19 so the Gitea MCP server can run in MCP OAuth mode:
|
||||
# claude.ai and Claude Code send the user to Gitea's own OAuth2
|
||||
# authorize page, which must be reachable over HTTPS at Gitea's ROOT_URL
|
||||
# (gitea-public-patch.yaml sets ROOT_URL = https://git.bikoret.ch/).
|
||||
#
|
||||
# Gitea lives in namespace temple-council (Service gitea, 80 → 3000).
|
||||
# The plain-HTTP TCP passthrough on :3000 and SSH on :22
|
||||
# (ingress-tcp-services.yaml) stay as they are; this adds a third door.
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. DNS: git.bikoret.ch → 185.19.28.170 (Infomaniak zone)
|
||||
# 2. ClusterIssuer letsencrypt-prod
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: gitea
|
||||
namespace: temple-council
|
||||
labels:
|
||||
app.kubernetes.io/name: gitea
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
# git push of large packs; Gitea enforces its own limits
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- git.bikoret.ch
|
||||
secretName: gitea-bikoret-tls
|
||||
rules:
|
||||
- host: git.bikoret.ch
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: gitea
|
||||
port:
|
||||
number: 80
|
||||
@@ -1,13 +1,28 @@
|
||||
# Gitea MCP Server — public connector for claude.ai
|
||||
# Gitea MCP Server — public connector for claude.ai and Claude Code
|
||||
#
|
||||
# Deploys the official Gitea MCP server (gitea.com/gitea/gitea-mcp)
|
||||
# in HTTP mode, exposing /mcp endpoint for Streamable HTTP transport.
|
||||
# in HTTP mode with MCP OAuth (-oauth): every client authorizes against
|
||||
# Gitea with its OWN account (PKCE, no client secret), and the server
|
||||
# forwards that bearer token to Gitea per request. No shared static
|
||||
# token is used any more (gitea-mcp-secrets is kept only for a rollback
|
||||
# to the pre-2026-10-19 static-token mode).
|
||||
#
|
||||
# The MCP server connects to the Gitea backend over the cluster network
|
||||
# (gitea.temple-council.svc.cluster.local) using a Personal Access Token
|
||||
# stored in gitea-mcp-secrets. Until 2026-10-06 it went out to Gitea's
|
||||
# own NLB (185.19.29.94) and back in; that NLB is gone — Gitea is behind
|
||||
# the shared ingress now (ingress-tcp-services.yaml).
|
||||
# The server serves /.well-known/oauth-protected-resource naming Gitea
|
||||
# as the authorization server, so BOTH origins must be public HTTPS:
|
||||
# -oauth-public-url https://mcp.bikoret.ch (gitea-mcp-ingress.yaml)
|
||||
# -H https://git.bikoret.ch (gitea-ingress.yaml;
|
||||
# Gitea ROOT_URL must be the same, gitea-public-patch.yaml)
|
||||
# /mcp accepts only the Host from -oauth-public-url, and reverse proxies
|
||||
# must pass Mcp-Protocol-Version, Mcp-Method and Mcp-Name through
|
||||
# (ingress-nginx does).
|
||||
#
|
||||
# One-time in Gitea (Settings → Applications → Manage OAuth2 Applications):
|
||||
# name Claude
|
||||
# redirect URIs https://claude.ai/api/mcp/auth_callback
|
||||
# http://127.0.0.1/callback (Claude Code; any port)
|
||||
# confidential UNCHECKED (PKCE)
|
||||
# The client ID goes into the claude.ai connector's Advanced settings,
|
||||
# with the connector URL https://mcp.bikoret.ch/mcp.
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
@@ -33,18 +48,13 @@ spec:
|
||||
args:
|
||||
- "-t"
|
||||
- "http"
|
||||
- "-oauth"
|
||||
- "-oauth-public-url"
|
||||
- "https://mcp.bikoret.ch"
|
||||
- "--host"
|
||||
- "http://gitea.temple-council.svc.cluster.local"
|
||||
- "https://git.bikoret.ch"
|
||||
- "--port"
|
||||
- "8080"
|
||||
env:
|
||||
- name: GITEA_ACCESS_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: gitea-mcp-secrets
|
||||
key: GITEA_TOKEN
|
||||
- name: GITEA_HOST
|
||||
value: "http://gitea.temple-council.svc.cluster.local"
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
@@ -56,12 +66,14 @@ spec:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# Ingress for Gitea MCP Server — public HTTPS endpoint
|
||||
# Routes mcp.shorashim.ch to the gitea-mcp service
|
||||
# Routes mcp.bikoret.ch to the gitea-mcp service (moved from
|
||||
# mcp.bikoret.ch on 2026-10-19 with the switch to MCP OAuth; the
|
||||
# server only answers /mcp for the host in -oauth-public-url).
|
||||
# TLS via cert-manager (Let's Encrypt)
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. nginx-ingress controller installed
|
||||
# 2. cert-manager installed
|
||||
# 3. ClusterIssuer letsencrypt-prod applied
|
||||
# 4. DNS: mcp.shorashim.ch → Ingress external IP
|
||||
# 4. DNS: mcp.bikoret.ch → 185.19.28.170 (the one NLB; Infomaniak zone)
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
@@ -22,10 +24,10 @@ spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- mcp.shorashim.ch
|
||||
secretName: gitea-mcp-tls
|
||||
- mcp.bikoret.ch
|
||||
secretName: gitea-mcp-bikoret-tls
|
||||
rules:
|
||||
- host: mcp.shorashim.ch
|
||||
- host: mcp.bikoret.ch
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
# Gitea MCP Server — secrets template
|
||||
# NEVER commit actual values. Fill in and apply manually.
|
||||
#
|
||||
# Two secrets needed:
|
||||
# Since 2026-10-19 the MCP server runs in OAuth mode and uses NO static
|
||||
# token: users authorize with their own Gitea account. This secret is
|
||||
# kept only for a rollback to static-token mode (see the deployment).
|
||||
#
|
||||
# Two secrets needed (static-token mode):
|
||||
# 1. GITEA_TOKEN: Personal Access Token from Gitea
|
||||
# Create at: http://185.19.28.170:3000/user/settings/applications
|
||||
# Permissions: repo (read/write)
|
||||
#
|
||||
# 2. OAuth Client ID/Secret: Registered OAuth app in Gitea
|
||||
# Create at: http://185.19.28.170:3000/user/settings/applications
|
||||
# Redirect URI: https://claude.ai/api/mcp/auth_callback
|
||||
# (These go into claude.ai connector settings, not this secret)
|
||||
# Redirect URIs: https://claude.ai/api/mcp/auth_callback
|
||||
# http://127.0.0.1/callback (Claude Code)
|
||||
# Confidential Client: UNCHECKED (PKCE). Only the Client ID is needed;
|
||||
# it goes into the claude.ai connector's Advanced settings, not here.
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# Move Gitea and its MCP server to public HTTPS on bikoret.ch and switch
|
||||
# the MCP server to MCP OAuth (2026-10-19). Run by the transmitter:
|
||||
# ! bash ~/.local/bin/gitea-public-apply
|
||||
# Idempotent. Needs DNS first: git.bikoret.ch and mcp.bikoret.ch → 185.19.28.170.
|
||||
#
|
||||
# Rollback (static-token mode on mcp.shorashim.ch, as before 2026-10-19):
|
||||
# kubectl -n temple-council set env deploy/gitea \
|
||||
# GITEA__server__ROOT_URL=http://gitea.temple-council.ch/ \
|
||||
# GITEA__server__DOMAIN=gitea.temple-council.ch GITEA__server__SSH_DOMAIN-
|
||||
# then restore deploy/k8s/gitea-mcp-*.yaml from commit 861cd4e6 and apply them.
|
||||
set -euo pipefail
|
||||
REPO="${REPO:-$HOME/bikoret}"
|
||||
cd "$REPO"
|
||||
NLB=185.19.28.170
|
||||
for h in git.bikoret.ch mcp.bikoret.ch; do
|
||||
ip=$(dig +short "$h" | tail -1 || true)
|
||||
if [ "$ip" != "$NLB" ]; then
|
||||
echo "DNS: $h → '${ip:-nothing}' (need $NLB). Add the A record at Infomaniak first." >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
echo "== 1/4 ingresses (certificates issue once DNS resolves)"
|
||||
kubectl apply -f deploy/k8s/gitea-ingress.yaml
|
||||
kubectl apply -f deploy/k8s/gitea-mcp-ingress.yaml
|
||||
echo "== 2/4 Gitea ROOT_URL → https://git.bikoret.ch/ (one restart)"
|
||||
kubectl -n temple-council patch deploy gitea --patch-file deploy/k8s/gitea-public-patch.yaml
|
||||
kubectl -n temple-council rollout status deploy/gitea --timeout=180s
|
||||
echo "== 3/4 gitea-mcp in OAuth mode at https://mcp.bikoret.ch"
|
||||
kubectl apply -f deploy/k8s/gitea-mcp-deployment.yaml
|
||||
kubectl -n shorashim rollout status deploy/gitea-mcp --timeout=120s
|
||||
echo "== 4/4 certificates"
|
||||
for i in $(seq 1 24); do
|
||||
ready=$(kubectl get certificate -A -o jsonpath='{range .items[?(@.metadata.name=="gitea-bikoret-tls")]}{.status.conditions[?(@.type=="Ready")].status}{end}{range .items[?(@.metadata.name=="gitea-mcp-bikoret-tls")]}{.status.conditions[?(@.type=="Ready")].status}{end}' 2>/dev/null || true)
|
||||
[ "$ready" = "TrueTrue" ] && break
|
||||
sleep 5
|
||||
done
|
||||
kubectl get certificate -A | grep -E "NAME|bikoret"
|
||||
echo "== verify"
|
||||
bash "$REPO/deploy/k8s/gitea-public-verify.sh"
|
||||
@@ -0,0 +1,28 @@
|
||||
# Strategic-merge patch for the Gitea Deployment (namespace temple-council,
|
||||
# which is NOT otherwise managed from this repo): make Gitea's own URL the
|
||||
# public HTTPS host, so its OAuth2 authorize/token endpoints and the
|
||||
# issuer in /.well-known/openid-configuration are reachable by claude.ai.
|
||||
#
|
||||
# kubectl -n temple-council patch deploy gitea --patch-file deploy/k8s/gitea-public-patch.yaml
|
||||
#
|
||||
# The Gitea image rewrites app.ini from GITEA__section__KEY at start, so
|
||||
# this replaces ROOT_URL/DOMAIN (were http://gitea.temple-council.ch, a
|
||||
# name that never resolved) and SSH_DOMAIN (was 185.19.29.94, an NLB IP
|
||||
# released on 2026-10-06). Clone URLs shown by Gitea become
|
||||
# https://git.bikoret.ch/... and git@git.bikoret.ch:...; the old
|
||||
# http://185.19.28.170:3000 remote keeps working for push/pull.
|
||||
# Side effect: with an https ROOT_URL Gitea marks its session cookie
|
||||
# Secure, so the WEB UI must be used at https://git.bikoret.ch, not on
|
||||
# the plain-HTTP :3000 door (git over :3000 is unaffected).
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: gitea
|
||||
env:
|
||||
- name: GITEA__server__ROOT_URL
|
||||
value: "https://git.bikoret.ch/"
|
||||
- name: GITEA__server__DOMAIN
|
||||
value: "git.bikoret.ch"
|
||||
- name: GITEA__server__SSH_DOMAIN
|
||||
value: "git.bikoret.ch"
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Read-only checks for the public Gitea + OAuth MCP setup. Exit 0 = all pass.
|
||||
# bash ~/.local/bin/gitea-public-verify
|
||||
set -u
|
||||
fail=0
|
||||
chk() { # label expected actual
|
||||
if [ "$2" = "$3" ]; then echo "ok $1: $3"; else echo "FAIL $1: got '$3', want '$2'"; fail=1; fi
|
||||
}
|
||||
INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
|
||||
chk "https://git.bikoret.ch/ status" 200 "$(curl -sS -m 15 -o /dev/null -w '%{http_code}' https://git.bikoret.ch/ || echo err)"
|
||||
chk "Gitea OIDC issuer" "https://git.bikoret.ch" "$(curl -sS -m 15 https://git.bikoret.ch/.well-known/openid-configuration | python3 -c 'import sys,json;print(json.load(sys.stdin)["issuer"].rstrip("/"))' 2>/dev/null || echo err)"
|
||||
chk "mcp /healthz" 200 "$(curl -sS -m 15 -o /dev/null -w '%{http_code}' https://mcp.bikoret.ch/healthz || echo err)"
|
||||
prm=$(curl -sS -m 15 https://mcp.bikoret.ch/.well-known/oauth-protected-resource || echo err)
|
||||
chk "protected-resource names Gitea" yes "$(printf '%s' "$prm" | grep -q 'git.bikoret.ch' && echo yes || echo no)"
|
||||
chk "POST /mcp without token → 401" 401 "$(curl -sS -m 15 -o /dev/null -w '%{http_code}' -X POST https://mcp.bikoret.ch/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d "$INIT" || echo err)"
|
||||
chk "git over :3000 still answers" 200 "$(curl -sS -m 15 -o /dev/null -w '%{http_code}' http://185.19.28.170:3000/api/v1/version || echo err)"
|
||||
echo "protected-resource: $prm"
|
||||
exit $fail
|
||||
Reference in new issue
Block a user