gitea-mcp: 初次落地 Gitea MCP Server (.NET 10, V1 only-read)
把 Gitea (git.zhengchentao.win) 通过 MCP 暴露给 Claude.ai:列 repo、读代码、看 commits / issues / PR / orgs / packages / actions。 设计文档见 vault Coding/gitea-mcp/gitea-mcp 设计.md。 代码模板复用 obsidian-mcp(.NET 10 + ModelContextProtocol SDK + JwtBearer)。 19 个只读 Tool(全部 scope=read:gitea): Repo / 文件: - list_repos / read_repo - list_tree(max_entries=500 防爆) - read_file(max_bytes=1MB,超出 truncated=true) - search_code(走 /repos/search-code,indexer 未启用时返回结构化错误说明) 分支 / 提交: - list_branches / list_commits / read_commit(diff 文件数限 50) Issue / PR: - list_issues / read_issue(含评论) - list_pulls / read_pull(含评论 + 改动文件列表) Org / Package(用户额外授权 read:organization + read:package): - list_orgs / read_org - list_packages / read_package Gitea Actions(运维友好): - list_workflow_runs / read_run_log 技术栈: - .NET 10 + ModelContextProtocol SDK 1.0 - HttpClientFactory + Microsoft.Extensions.Http.Resilience(指数 backoff,5xx/429/网络错误重试) - JwtBearer (HS256, Current+Previous fallback, MapInboundClaims=false) - aud=gitea, scope=read:gitea, iss=https://auth.zhengchentao.win Gitea API client: - Authorization: token <PAT> (admin PAT,仅 read scope) - BaseUrl=https://git.zhengchentao.win - 错误映射:401/403 → UnauthorizedAccessException,404 → KeyNotFoundException,5xx → InvalidOperationException - RepoBlacklist 黑名单(owner/repo 精确匹配,默认空) 部署: - Dockerfile multi-stage,COPY --chown,non-root user - .gitea/workflows/build-image.yml:build + deploy 双 job,buildkit v0.13.2 - 容器内 :8080,宿主端口 9092 - 子域名 git-mcp.zhengchentao.win(区别于 Gitea 本体 git.zhengchentao.win) 测试:6/6 单测过(GiteaRepoFilter 黑名单匹配) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
# gitea-mcp
|
||||
|
||||
Read access to all Gitea repos (public + private, personal + org) via MCP.
|
||||
OAuth via nas-auth (DCR + PKCE + JWT HS256), admin PAT held internally — Claude never touches the PAT.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Claude.ai (MCP client)
|
||||
│
|
||||
│ ① GET /.well-known/oauth-authorization-server
|
||||
↓
|
||||
git-mcp.zhengchentao.win ←── this service
|
||||
│
|
||||
│ ② DCR + PKCE auth flow redirect
|
||||
↓
|
||||
auth.zhengchentao.win ←── nas-auth
|
||||
│
|
||||
│ ③ JWT (aud=gitea, scope=read:gitea)
|
||||
↓
|
||||
Claude.ai → Bearer JWT
|
||||
│
|
||||
│ ④ POST /mcp (MCP Streamable HTTP)
|
||||
↓
|
||||
git-mcp.zhengchentao.win
|
||||
│ Bearer <GITEA_ADMIN_PAT>
|
||||
↓
|
||||
git.zhengchentao.win ←── Gitea
|
||||
```
|
||||
|
||||
See [gitea-mcp 设计.md](../Coding/gitea-mcp/gitea-mcp%20设计.md) for full design rationale.
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_repos` | List all repos (personal + org, public + private) |
|
||||
| `read_repo` | Repo metadata: topics, stars, default branch, mirror flag |
|
||||
| `list_tree` | File tree at a ref (recursive optional, max 500 entries) |
|
||||
| `read_file` | Raw file content (UTF-8, truncated at 1MB) |
|
||||
| `search_code` | Code search via Gitea indexer (requires indexer enabled) |
|
||||
| `list_branches` | Branch list + last commit per branch |
|
||||
| `list_commits` | Recent commits with author + message |
|
||||
| `read_commit` | Full commit details + per-file diff (max 50 files) |
|
||||
| `list_issues` | Issues filtered by state (open/closed/all) |
|
||||
| `read_issue` | Issue body + all comments |
|
||||
| `list_pulls` | Pull requests filtered by state |
|
||||
| `read_pull` | PR body + review comments + changed files |
|
||||
| `list_orgs` | All organizations visible to admin token |
|
||||
| `read_org` | Org metadata |
|
||||
| `list_packages` | Packages in registry by owner (container/generic/npm/...) |
|
||||
| `read_package` | Package version metadata |
|
||||
| `list_workflow_runs` | Gitea Actions workflow run history |
|
||||
| `read_run_log` | Run details + job list + log (truncated at 1MB) |
|
||||
|
||||
All tools require a valid JWT with `scope=read:gitea` issued by nas-auth.
|
||||
|
||||
## Configuration (env vars)
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `Gitea__BaseUrl` | `https://git.zhengchentao.win` | Gitea backend URL |
|
||||
| `Gitea__AdminPat` | *(required)* | Gitea read-only PAT — see PAT Setup below |
|
||||
| `Gitea__RepoBlacklist` | *(empty)* | Comma-separated `owner/repo` pairs to hide from Claude |
|
||||
| `Gitea__DefaultLimit` | `50` | Default page size for list operations |
|
||||
| `Gitea__MaxFileBytes` | `1048576` | Max file read size in bytes (1MB) |
|
||||
| `Jwt__Issuer` | `https://auth.zhengchentao.win` | Expected JWT issuer |
|
||||
| `Jwt__Audience` | `gitea` | Expected JWT audience |
|
||||
| `Jwt__SigningKeyCurrent` | *(required)* | HS256 signing key (shared with nas-auth) |
|
||||
| `Jwt__SigningKeyPrevious` | *(empty)* | Previous key for rotation window |
|
||||
| `ASPNETCORE_ENVIRONMENT` | `Production` | Use `Development` locally |
|
||||
|
||||
All secrets come from `/volume1/docker/compose/.env.shared` on NAS — never hardcode them.
|
||||
|
||||
## Local Development
|
||||
|
||||
### 1. Restore and run
|
||||
|
||||
```bash
|
||||
dotnet restore
|
||||
dotnet run
|
||||
# Listens on http://localhost:5000
|
||||
```
|
||||
|
||||
### 2. Generate a dev JWT
|
||||
|
||||
Use `dotnet user-jwts` to sign a token without running nas-auth:
|
||||
|
||||
```bash
|
||||
dotnet user-jwts create \
|
||||
--issuer https://auth.zhengchentao.win \
|
||||
--audience gitea \
|
||||
--name tao \
|
||||
--claim sub=tao \
|
||||
--claim scope=read:gitea
|
||||
```
|
||||
|
||||
Or use [jwt.io](https://jwt.io) with alg=HS256 and the key from `Jwt:SigningKeyCurrent`.
|
||||
|
||||
### 3. Test with MCP Inspector
|
||||
|
||||
```bash
|
||||
npx @modelcontextprotocol/inspector
|
||||
# Transport: Streamable HTTP
|
||||
# URL: http://localhost:5000/mcp
|
||||
# Bearer Token: <token from step 2>
|
||||
```
|
||||
|
||||
### 4. Run unit tests
|
||||
|
||||
```bash
|
||||
dotnet test gitea-mcp.Tests/
|
||||
```
|
||||
|
||||
## PAT Setup (Gitea → Settings → Applications)
|
||||
|
||||
Generate a token with **only these scopes** (principle of least privilege):
|
||||
|
||||
- `read:repository`
|
||||
- `read:organization`
|
||||
- `read:package`
|
||||
- `read:issue`
|
||||
- `read:user`
|
||||
|
||||
Do NOT grant `write:*` or `admin:*` scopes.
|
||||
Store the generated token in `.env.shared` as `GITEA_MCP_PAT=<token>`.
|
||||
|
||||
If the PAT is compromised: revoke in Gitea → generate new → update `.env.shared` → `docker compose up -d gitea-mcp`.
|
||||
|
||||
## Docker Compose (NAS deployment)
|
||||
|
||||
```yaml
|
||||
# /volume1/docker/compose/gitea-mcp/docker-compose.yml
|
||||
services:
|
||||
gitea-mcp:
|
||||
image: git.zhengchentao.win/zhengchen.tao/gitea-mcp:latest
|
||||
container_name: gitea-mcp
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9092:8080"
|
||||
volumes:
|
||||
- /volume1/docker/gitea-mcp/logs:/app/logs
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Production
|
||||
- Gitea__BaseUrl=https://git.zhengchentao.win
|
||||
- Gitea__AdminPat=${GITEA_MCP_PAT}
|
||||
- Gitea__RepoBlacklist=
|
||||
- Jwt__Issuer=https://auth.zhengchentao.win
|
||||
- Jwt__Audience=gitea
|
||||
- Jwt__SigningKeyCurrent=${JWT_SIGNING_KEY_CURRENT}
|
||||
- TZ=Asia/Shanghai
|
||||
env_file:
|
||||
- ../.env.shared
|
||||
```
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [gitea-mcp 设计](../Obsidian%20Vault/Coding/gitea-mcp/gitea-mcp%20设计.md)
|
||||
- [MCP 实现指南](../Obsidian%20Vault/Coding/obsidian-mcp/MCP%20实现指南.md)
|
||||
- [nas-auth 设计](../Obsidian%20Vault/Coding/nas-auth/nas-auth%20设计.md)
|
||||
- [Gitea Actions Build-Deploy 模板](../Obsidian%20Vault/Coding/Gitea%20Actions%20Build-Deploy%20模板.md)
|
||||
Reference in New Issue
Block a user