@amonstack/gitea-mcp 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -4
- package/README.zh-CN.md +69 -5
- package/dist/assets/instructions.md +19 -7
- package/dist/assets/skills/gitea-configure/SKILL.md +60 -0
- package/dist/assets/skills/gitea-resolve-repo/SKILL.md +17 -6
- package/dist/cli.js +17 -8
- package/dist/cli.js.map +1 -1
- package/dist/credentials.d.ts +145 -0
- package/dist/credentials.js +141 -0
- package/dist/credentials.js.map +1 -0
- package/dist/git-config.d.ts +97 -0
- package/dist/git-config.js +295 -0
- package/dist/git-config.js.map +1 -0
- package/dist/gitea-client.d.ts +55 -2
- package/dist/gitea-client.js +110 -8
- package/dist/gitea-client.js.map +1 -1
- package/dist/server.d.ts +3 -2
- package/dist/server.js +36 -25
- package/dist/server.js.map +1 -1
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +1 -0
- package/dist/tools.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -52,13 +52,77 @@ node dist/cli.js
|
|
|
52
52
|
|
|
53
53
|
## Configuration
|
|
54
54
|
|
|
55
|
+
All variables are optional — `gitea-mcp` auto-discovers the Gitea instance, repository,
|
|
56
|
+
and token from the project's local git config so a single global install can serve many
|
|
57
|
+
projects. Set them only to override the discovery.
|
|
58
|
+
|
|
55
59
|
| Variable | Required | Description |
|
|
56
60
|
|----------|:--------:|-------------|
|
|
57
|
-
| `GITEA_BASE_URL` |
|
|
58
|
-
| `GITEA_TOKEN` |
|
|
61
|
+
| `GITEA_BASE_URL` | No | Gitea instance URL (e.g. `https://gitea.example.com`). Auto-detected from the project's git remote when omitted. |
|
|
62
|
+
| `GITEA_TOKEN` | No | Gitea API access token. One of several auth candidates; tried after a `.git/config [gitea]` token and before the git credential store (see [Token discovery](#token-discovery)). |
|
|
59
63
|
| `GITEA_DEFAULT_OWNER` | No | Default repository owner — skip passing `owner` on every call |
|
|
60
64
|
| `GITEA_DEFAULT_REPO` | No | Default repository name — skip passing `repo` on every call |
|
|
61
65
|
|
|
66
|
+
### How auto-discovery works
|
|
67
|
+
|
|
68
|
+
On start, `gitea-mcp` reads `<cwd>/.git/config` and derives:
|
|
69
|
+
|
|
70
|
+
- **Instance URL** — from the selected remote's host. SSH remotes (`git@host:owner/repo`)
|
|
71
|
+
resolve to `https://host`. Override with `GITEA_BASE_URL`.
|
|
72
|
+
- **owner / repo** — from the selected remote's URL. Override with `GITEA_DEFAULT_OWNER` /
|
|
73
|
+
`GITEA_DEFAULT_REPO`, or detect ad hoc with the `resolve_repo` tool.
|
|
74
|
+
- **Remote selection** — the `upstream` remote is preferred, falling back to `origin`, then
|
|
75
|
+
any other remote. Both are reported by `resolve_repo` when they differ.
|
|
76
|
+
|
|
77
|
+
If the current directory has no git remote and `GITEA_BASE_URL` is not set, the server does
|
|
78
|
+
**not** start — it prints a skip reason and exits 0. Run it from inside a cloned Gitea
|
|
79
|
+
repository, or set `GITEA_BASE_URL` / `GITEA_TOKEN` explicitly.
|
|
80
|
+
|
|
81
|
+
### Token discovery
|
|
82
|
+
|
|
83
|
+
`gitea-mcp` collects authentication **candidates** from three sources, in this
|
|
84
|
+
priority order:
|
|
85
|
+
|
|
86
|
+
1. A `[gitea "<baseUrl>"]` section in `.git/config` (a bare `[gitea]` section is
|
|
87
|
+
a host-wide fallback):
|
|
88
|
+
```ini
|
|
89
|
+
[gitea "https://gitea.example.com"]
|
|
90
|
+
token = <your-token>
|
|
91
|
+
```
|
|
92
|
+
Always sent as `Authorization: token <token>`.
|
|
93
|
+
2. The `GITEA_TOKEN` environment variable — also sent as `Authorization: token`.
|
|
94
|
+
3. The git credential store (`~/.git-credentials`, or
|
|
95
|
+
`$XDG_CONFIG_HOME/git/credentials`) — every line whose host matches the
|
|
96
|
+
instance, e.g. `https://alice:s3cret@gitea.example.com`. When several lines
|
|
97
|
+
match, the one whose path best matches `owner/repo` is tried first.
|
|
98
|
+
|
|
99
|
+
A credential-store entry's `password` field may hold a real PAT, an account
|
|
100
|
+
password, or an OAuth token — git stores whatever was typed at the prompt, and
|
|
101
|
+
the server cannot tell them apart statically. So each credential-store entry is
|
|
102
|
+
tried under **two authentication schemes**:
|
|
103
|
+
|
|
104
|
+
- `Authorization: Basic <base64(user:pass)>` — works for account passwords and
|
|
105
|
+
PATs alike (Gitea checks that the username matches the secret's owner).
|
|
106
|
+
- `Authorization: token <secret>` — works only for real PATs.
|
|
107
|
+
|
|
108
|
+
The order is chosen by a username heuristic: a convention username
|
|
109
|
+
(`oauth2`, `x-oauth-basic`, or empty) tries `token` first; a real-looking
|
|
110
|
+
username (e.g. `alice`) tries `basic` first.
|
|
111
|
+
|
|
112
|
+
**Fault tolerance.** On `401`/`403` the server advances to the next
|
|
113
|
+
scheme/candidate and retries the same request; once a combination succeeds it is
|
|
114
|
+
locked for the rest of the session (no re-probing). Non-auth errors (`404`,
|
|
115
|
+
`500`, network) propagate immediately and do **not** trigger a retry.
|
|
116
|
+
|
|
117
|
+
**Diagnostics.** The `gitea_status` tool (see [Repository Helpers](#repository-helpers))
|
|
118
|
+
returns a redacted view of the current state — which candidate is active, which
|
|
119
|
+
are exhausted, the last status seen — without ever exposing the secret. Use it
|
|
120
|
+
to troubleshoot a `401` instead of guessing.
|
|
121
|
+
|
|
122
|
+
If no source resolves a credential, the server still starts anonymously. Public
|
|
123
|
+
repositories may be read; private repos and write operations return `401` — use
|
|
124
|
+
the `gitea-configure` skill to guide setup, or set `GITEA_TOKEN`.
|
|
125
|
+
|
|
62
126
|
When `GITEA_DEFAULT_OWNER` and `GITEA_DEFAULT_REPO` are set, you can omit the
|
|
63
127
|
`owner` and `repo` parameters in tool calls. The `resolve_repo` tool can also
|
|
64
128
|
auto-detect them from a local git repository.
|
|
@@ -131,7 +195,14 @@ directory; use `--dir` for an exact location. Then restart your tool. See
|
|
|
131
195
|
### Other MCP Clients
|
|
132
196
|
|
|
133
197
|
Any client that supports stdio-based MCP servers can use `gitea-mcp`. After
|
|
134
|
-
installation,
|
|
198
|
+
installation, run it from inside a cloned Gitea repository (config is auto-discovered):
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
cd /path/to/your/gitea-repo
|
|
202
|
+
gitea-mcp
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Or set the variables explicitly if you prefer:
|
|
135
206
|
|
|
136
207
|
```bash
|
|
137
208
|
export GITEA_BASE_URL="https://gitea.example.com"
|
|
@@ -189,7 +260,8 @@ gitea-mcp
|
|
|
189
260
|
| Tool | Description |
|
|
190
261
|
|------|-------------|
|
|
191
262
|
| `list_my_repos` | List repositories accessible to the authenticated user |
|
|
192
|
-
| `resolve_repo` |
|
|
263
|
+
| `resolve_repo` | Detect `baseUrl`, `owner`, and `repo` from the project's git remotes (`upstream` preferred, then `origin`) |
|
|
264
|
+
| `gitea_status` | Inspect credential-handling state — active candidate, exhausted candidates, last error (redacted; secrets never exposed) |
|
|
193
265
|
|
|
194
266
|
## AI Guidance & Skills
|
|
195
267
|
|
|
@@ -222,6 +294,7 @@ say, delete instructions while creating). Install them with the
|
|
|
222
294
|
| `gitea-summarize-issue` | reading and summarizing an issue's discussion |
|
|
223
295
|
| `gitea-plan-milestones` | creating / editing / closing milestones |
|
|
224
296
|
| `gitea-resolve-repo` | resolving owner/repo or listing repositories |
|
|
297
|
+
| `gitea-configure` | fixing the connection — instance URL, token, or 401/403 errors |
|
|
225
298
|
|
|
226
299
|
Each skill is a short, AI-facing action flow (purpose, when to use, when not to,
|
|
227
300
|
rules, and what to check first). The create, comment, and milestone skills also
|
|
@@ -245,6 +318,7 @@ npm ci
|
|
|
245
318
|
| `make test` | Run unit tests |
|
|
246
319
|
| `make test-watch` | Run tests in watch mode |
|
|
247
320
|
| `make test-integration` | Run integration tests (needs live Gitea instance) |
|
|
321
|
+
| `make scan` | Scan for leaked secrets with gitleaks (part of `make verify`) |
|
|
248
322
|
| `make dev` | Run directly with tsx |
|
|
249
323
|
|
|
250
324
|
For the full architecture — module layout, dependency graph, core patterns, and
|
package/README.zh-CN.md
CHANGED
|
@@ -50,13 +50,67 @@ node dist/cli.js
|
|
|
50
50
|
|
|
51
51
|
## 配置
|
|
52
52
|
|
|
53
|
+
所有变量都是可选的——`gitea-mcp` 会从项目本地 git 配置自动发现 Gitea 实例、仓库和令牌,
|
|
54
|
+
因此一次全局安装即可服务多个项目。仅在需要覆盖自动发现结果时才设置它们。
|
|
55
|
+
|
|
53
56
|
| 变量 | 必填 | 说明 |
|
|
54
57
|
|------|:----:|------|
|
|
55
|
-
| `GITEA_BASE_URL` |
|
|
56
|
-
| `GITEA_TOKEN` |
|
|
58
|
+
| `GITEA_BASE_URL` | 否 | Gitea 实例地址(如 `https://gitea.example.com`)。未设置时从项目 git 远程地址自动推导。 |
|
|
59
|
+
| `GITEA_TOKEN` | 否 | Gitea API 访问令牌。是多个认证候选之一;排在 `.git/config [gitea]` 令牌之后、git 凭据存储之前(见[令牌发现](#令牌发现))。 |
|
|
57
60
|
| `GITEA_DEFAULT_OWNER` | 否 | 默认仓库所有者,免去每次传入 `owner` 参数 |
|
|
58
61
|
| `GITEA_DEFAULT_REPO` | 否 | 默认仓库名称,免去每次传入 `repo` 参数 |
|
|
59
62
|
|
|
63
|
+
### 自动发现的工作方式
|
|
64
|
+
|
|
65
|
+
启动时,`gitea-mcp` 读取 `<cwd>/.git/config` 并推导:
|
|
66
|
+
|
|
67
|
+
- **实例地址** —— 取自选中远程地址的 host。SSH 远程(`git@host:owner/repo`)会被推导为
|
|
68
|
+
`https://host`。可用 `GITEA_BASE_URL` 覆盖。
|
|
69
|
+
- **owner / repo** —— 取自选中远程地址。可用 `GITEA_DEFAULT_OWNER` / `GITEA_DEFAULT_REPO`
|
|
70
|
+
覆盖,或随时用 `resolve_repo` 工具检测。
|
|
71
|
+
- **远程选择** —— 优先 `upstream`,回退 `origin`,再回退其它远程。两者不同时 `resolve_repo`
|
|
72
|
+
会同时返回。
|
|
73
|
+
|
|
74
|
+
若当前目录没有 git 远程、且未设置 `GITEA_BASE_URL`,服务器**不会启动**——它会打印跳过原因并
|
|
75
|
+
以 exit 0 退出。请在克隆的 Gitea 仓库内运行,或显式设置 `GITEA_BASE_URL` / `GITEA_TOKEN`。
|
|
76
|
+
|
|
77
|
+
### 令牌发现
|
|
78
|
+
|
|
79
|
+
`gitea-mcp` 从三个来源收集认证**候选**,按以下优先级排序:
|
|
80
|
+
|
|
81
|
+
1. `.git/config` 中的 `[gitea "<baseUrl>"]` 段(不带地址的 `[gitea]` 段作为全局兜底):
|
|
82
|
+
```ini
|
|
83
|
+
[gitea "https://gitea.example.com"]
|
|
84
|
+
token = <your-token>
|
|
85
|
+
```
|
|
86
|
+
始终以 `Authorization: token <token>` 发送。
|
|
87
|
+
2. `GITEA_TOKEN` 环境变量 —— 同样以 `Authorization: token` 发送。
|
|
88
|
+
3. git 凭据存储(`~/.git-credentials`,或 `$XDG_CONFIG_HOME/git/credentials`)—— host 匹配实例
|
|
89
|
+
的每一行,如 `https://alice:s3cret@gitea.example.com`。多行同时匹配时,path 与 `owner/repo`
|
|
90
|
+
最贴近的那一行优先尝试。
|
|
91
|
+
|
|
92
|
+
凭据存储条目的 `password` 字段可能是真正的 PAT、账户登录密码,或 OAuth token —— git 存的是
|
|
93
|
+
用户在密码提示里输入的任何内容,服务端无法静态区分。因此每个凭据存储条目会以**两种认证方案**
|
|
94
|
+
依次尝试:
|
|
95
|
+
|
|
96
|
+
- `Authorization: Basic <base64(user:pass)>` —— 账户密码和 PAT 都能通过(Gitea 会校验用户名与
|
|
97
|
+
密钥属主一致)。
|
|
98
|
+
- `Authorization: token <secret>` —— 仅对真正的 PAT 有效。
|
|
99
|
+
|
|
100
|
+
尝试顺序由用户名启发式决定:约定用户名(`oauth2`、`x-oauth-basic`、空)先试 `token`;真实用户名
|
|
101
|
+
(如 `alice`)先试 `basic`。
|
|
102
|
+
|
|
103
|
+
**容错。** 遇到 `401`/`403`,服务端会切换到下一个方案/候选并重试同一请求;一旦某组凭据成功,
|
|
104
|
+
它会被锁定到本会话结束(不再重复探测)。非认证类错误(`404`、`500`、网络错误)立即向上抛出,
|
|
105
|
+
**不会**触发重试。
|
|
106
|
+
|
|
107
|
+
**诊断。** `gitea_status` 工具(见[仓库辅助](#仓库辅助-repository-helpers))返回当前状态的脱敏
|
|
108
|
+
视图 —— 哪个候选处于 active、哪些已耗尽、最近一次的状态码 —— 永不暴露密钥本身。排查 `401` 时
|
|
109
|
+
用它代替盲猜。
|
|
110
|
+
|
|
111
|
+
若所有来源都未解析到凭据,服务器仍会以匿名方式启动。公开仓库可读;私有仓库和写操作返回
|
|
112
|
+
`401` —— 此时使用 `gitea-configure` 技能引导配置,或设置 `GITEA_TOKEN`。
|
|
113
|
+
|
|
60
114
|
设置 `GITEA_DEFAULT_OWNER` 和 `GITEA_DEFAULT_REPO` 后,调用工具时可以省略
|
|
61
115
|
`owner` 和 `repo` 参数。也可以使用 `resolve_repo` 工具自动从本地 git 仓库
|
|
62
116
|
检测这两个值。
|
|
@@ -126,8 +180,15 @@ gitea-mcp init --dir /exact/path # 自定义路径
|
|
|
126
180
|
|
|
127
181
|
### 其他 MCP 客户端
|
|
128
182
|
|
|
129
|
-
任何支持 stdio 方式运行 MCP
|
|
130
|
-
|
|
183
|
+
任何支持 stdio 方式运行 MCP 服务端的客户端都可以使用。安装完成后,在克隆的 Gitea 仓库内
|
|
184
|
+
吏动即可(配置会自动发现):
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
cd /path/to/your/gitea-repo
|
|
188
|
+
gitea-mcp
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
也可显式设置环境变量:
|
|
131
192
|
|
|
132
193
|
```bash
|
|
133
194
|
export GITEA_BASE_URL="https://gitea.example.com"
|
|
@@ -185,7 +246,8 @@ gitea-mcp
|
|
|
185
246
|
| 工具 | 说明 |
|
|
186
247
|
|------|------|
|
|
187
248
|
| `list_my_repos` | 列出当前用户可访问的仓库 |
|
|
188
|
-
| `resolve_repo` |
|
|
249
|
+
| `resolve_repo` | 从项目 git 远程地址检测 `baseUrl`、`owner`、`repo`(优先 `upstream`,回退 `origin`) |
|
|
250
|
+
| `gitea_status` | 查看认证处理状态 —— active 候选、已耗尽候选、最近一次错误(脱敏;永不暴露密钥) |
|
|
189
251
|
|
|
190
252
|
## AI 引导与技能
|
|
191
253
|
|
|
@@ -216,6 +278,7 @@ gitea-mcp
|
|
|
216
278
|
| `gitea-summarize-issue` | 读取并总结某 issue 的讨论 |
|
|
217
279
|
| `gitea-plan-milestones` | 创建 / 编辑 / 关闭里程碑 |
|
|
218
280
|
| `gitea-resolve-repo` | 解析 owner/repo 或列出仓库 |
|
|
281
|
+
| `gitea-configure` | 修复连接——实例地址、令牌或 401/403 报错 |
|
|
219
282
|
|
|
220
283
|
每个技能都是面向 AI 的简短动作流程(目的、何时用、何时不用、规则、先检查什么)。
|
|
221
284
|
创建、评论、里程碑三类技能还内嵌**正文模板**(bug / 新功能 / 性能 issue、评论、
|
|
@@ -237,6 +300,7 @@ npm ci
|
|
|
237
300
|
| `make test` | 运行单元测试 |
|
|
238
301
|
| `make test-watch` | 监听模式运行测试 |
|
|
239
302
|
| `make test-integration` | 运行集成测试(需要可用的 Gitea 实例) |
|
|
303
|
+
| `make scan` | 用 gitleaks 扫描泄露的密钥(属于 `make verify`) |
|
|
240
304
|
| `make dev` | 通过 tsx 直接运行 |
|
|
241
305
|
|
|
242
306
|
完整的架构说明(模块布局、依赖关系、核心模式,以及新增工具的指引)请参阅
|
|
@@ -4,18 +4,29 @@ You manage Gitea issues, comments, labels, and milestones through this MCP serve
|
|
|
4
4
|
Every tool returns Gitea's JSON verbatim as text. Follow these rules to use them
|
|
5
5
|
correctly.
|
|
6
6
|
|
|
7
|
+
## Config is auto-discovered from git (env vars optional)
|
|
8
|
+
|
|
9
|
+
On start the server reads `<cwd>/.git/config` and derives `baseUrl`, `owner`, `repo`,
|
|
10
|
+
and `token` so one global install can serve many projects. The `GITEA_BASE_URL` /
|
|
11
|
+
`GITEA_TOKEN` / `GITEA_DEFAULT_OWNER` / `GITEA_DEFAULT_REPO` env vars are OPTIONAL
|
|
12
|
+
overrides; when set they take precedence over git discovery. The remote is chosen
|
|
13
|
+
`upstream` first, then `origin`. If the cwd has no git remote and `GITEA_BASE_URL` is
|
|
14
|
+
unset, the server does not start (it prints a skip reason and exits 0). If you hit a
|
|
15
|
+
401/403 or the connection looks unset, use the **gitea-configure** skill.
|
|
16
|
+
|
|
7
17
|
## Resolve owner/repo FIRST (most common failure)
|
|
8
18
|
|
|
9
19
|
Most tools target one repository and need both `owner` and `repo`. They resolve in
|
|
10
20
|
this order:
|
|
11
21
|
|
|
12
22
|
1. The `owner` / `repo` arguments you pass to the call.
|
|
13
|
-
2. `GITEA_DEFAULT_OWNER` / `GITEA_DEFAULT_REPO` env vars (
|
|
14
|
-
|
|
23
|
+
2. `GITEA_DEFAULT_OWNER` / `GITEA_DEFAULT_REPO` env vars (or the git-discovered defaults
|
|
24
|
+
captured at server start).
|
|
25
|
+
3. The `resolve_repo` tool (re-reads `.git/config` and returns `baseUrl`, `owner`, `repo`,
|
|
26
|
+
the selected `remote`, and every parsed remote under `remotes`).
|
|
15
27
|
|
|
16
|
-
If
|
|
17
|
-
|
|
18
|
-
once and reuse the result — do not guess.
|
|
28
|
+
If none resolve, the call errors. Before a batch of work on a repo you have not used this
|
|
29
|
+
session, call `resolve_repo` once and reuse the result — do not guess.
|
|
19
30
|
|
|
20
31
|
## Labels: IDs vs names (critical gotcha)
|
|
21
32
|
|
|
@@ -59,5 +70,6 @@ Confirm the target id/index and scope with the user before calling. For labels,
|
|
|
59
70
|
## Error format
|
|
60
71
|
|
|
61
72
|
Failed calls throw `Gitea API error (<status>): <body>`. Read the status: 401/403 →
|
|
62
|
-
token
|
|
63
|
-
|
|
73
|
+
token missing, wrong, or lacks scope — run the **gitea-configure** skill to fix the
|
|
74
|
+
connection; 404 → wrong owner/repo or no permission; 409 → conflict; 422 → validation.
|
|
75
|
+
Do not retry blindly on 4xx.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gitea-configure
|
|
3
|
+
description: Invoke to CONFIGURE / SET UP / FIX the Gitea connection — instance URL, token, or owner/repo discovery. Use when a tool fails with 401/403 (bad or missing token), when the user asks how to wire up gitea-mcp, or when baseUrl/owner/repo could not be auto-detected. Do NOT invoke for normal issue/label/milestone work once the connection works.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# gitea-configure
|
|
7
|
+
|
|
8
|
+
Diagnose and fix the Gitea connection. The server auto-discovers its config from the
|
|
9
|
+
project's git remotes; this skill is the fallback when that fails or the token is wrong.
|
|
10
|
+
|
|
11
|
+
## How gitea-mcp discovers config (no env vars required)
|
|
12
|
+
|
|
13
|
+
On start, the server reads `<cwd>/.git/config` and resolves in this order:
|
|
14
|
+
|
|
15
|
+
1. **baseUrl** — `GITEA_BASE_URL` env var, else derived from the selected remote's host.
|
|
16
|
+
SSH remotes (`git@host:owner/repo`) resolve to `https://host`.
|
|
17
|
+
2. **owner / repo** — `GITEA_DEFAULT_OWNER` / `GITEA_DEFAULT_REPO` env vars, else from the
|
|
18
|
+
selected remote's URL.
|
|
19
|
+
3. **Remote selection** — `upstream` remote first, falling back to `origin`, then any
|
|
20
|
+
other remote. Both are surfaced in `resolve_repo` output.
|
|
21
|
+
|
|
22
|
+
If the working directory has NO git remote and `GITEA_BASE_URL` is unset, the server does
|
|
23
|
+
NOT start (it prints a skip reason and exits 0). That is intentional, not a crash.
|
|
24
|
+
|
|
25
|
+
## Token discovery chain (tried in order)
|
|
26
|
+
|
|
27
|
+
1. `.git/config` — a `[gitea "<baseUrl>"]` section, e.g.
|
|
28
|
+
```ini
|
|
29
|
+
[gitea "https://gitea.example.com"]
|
|
30
|
+
token = <your-token>
|
|
31
|
+
```
|
|
32
|
+
A bare `[gitea]` section with `token = ...` is a host-wide fallback.
|
|
33
|
+
2. The git credential store (`~/.git-credentials`, or `$XDG_CONFIG_HOME/git/credentials`)
|
|
34
|
+
— a line whose host matches the instance, e.g.
|
|
35
|
+
`https://oauth2:<token>@gitea.example.com`.
|
|
36
|
+
3. `GITEA_TOKEN` env var.
|
|
37
|
+
4. If none of the above yield a token, the server starts WITHOUT a token (anonymous). Public
|
|
38
|
+
repos may be read; writes and private repos return 401 — that is the signal to help the
|
|
39
|
+
user add a token via one of the sources above.
|
|
40
|
+
|
|
41
|
+
## Fix flow — when a tool returns 401 / 403
|
|
42
|
+
|
|
43
|
+
1. Confirm the instance: run `resolve_repo` (no args) and read its `baseUrl` and `remote`.
|
|
44
|
+
If it throws, the cwd has no usable git remote — tell the user to run gitea-mcp from a
|
|
45
|
+
cloned repo, or set `GITEA_BASE_URL` + `GITEA_TOKEN`.
|
|
46
|
+
2. Ask the user to create a token at `<baseUrl>/user/settings/applications` (Gitea → Settings
|
|
47
|
+
→ Applications → Access Tokens). Capture the scopes they need: `issue`, `comment`,
|
|
48
|
+
`label`, `milestone` (read+write). NEVER have the user paste a token into chat unless they
|
|
49
|
+
explicitly choose to — prefer having them run a git command themselves.
|
|
50
|
+
3. Have the user store it so discovery finds it. Recommend, in priority order:
|
|
51
|
+
- `git config --file=.git/config gitea."<baseUrl>".token "<token>"` (project-scoped), or
|
|
52
|
+
- add to the credential store, or
|
|
53
|
+
- export `GITEA_TOKEN` (and `GITEA_BASE_URL`) in their MCP client config.
|
|
54
|
+
`<baseUrl>` is the EXACT value `resolve_repo` reported (scheme + host, with port if any).
|
|
55
|
+
4. The server must be restarted for new config to take effect — discovery runs once at start.
|
|
56
|
+
|
|
57
|
+
## Never log the token
|
|
58
|
+
|
|
59
|
+
Tokens are secret. Do not echo, paste into notes, or include in issue/comment bodies. Pass
|
|
60
|
+
configuration values to the user as commands to run themselves; never print a token back.
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gitea-resolve-repo
|
|
3
|
-
description: Invoke to RESOLVE which owner/repo the other gitea tools should target (auto-detect from a git remote) or to LIST repositories the token can access. Run FIRST when the target repository is unknown. Do NOT invoke for issue/comment/label/milestone work once owner/repo are known.
|
|
3
|
+
description: Invoke to RESOLVE which owner/repo (and baseUrl) the other gitea tools should target (auto-detect from a git remote) or to LIST repositories the token can access. Run FIRST when the target repository is unknown. Do NOT invoke for issue/comment/label/milestone work once owner/repo are known.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# gitea-resolve-repo
|
|
7
7
|
|
|
8
|
-
Discover the target repository. Tools: `resolve_repo`, `list_my_repos`.
|
|
8
|
+
Discover the target repository and instance. Tools: `resolve_repo`, `list_my_repos`.
|
|
9
9
|
|
|
10
10
|
## resolve_repo
|
|
11
|
-
- Reads
|
|
12
|
-
|
|
11
|
+
- Reads `.git/config` of the git repo at `path` (default: current directory) and parses
|
|
12
|
+
EVERY remote from SSH (`git@gitea.example:owner/repo.git`) or HTTPS
|
|
13
|
+
(`https://gitea.example/owner/repo.git`) URLs.
|
|
14
|
+
- Selects the remote to target in this priority: `upstream` → `origin` → any other.
|
|
15
|
+
SSH remotes resolve to `baseUrl = https://<host>` (the API is assumed HTTPS).
|
|
16
|
+
- Returns `{ baseUrl, owner, repo, remote, remote_url, remotes: { <name>: {baseUrl, owner, repo, url} } }`.
|
|
17
|
+
Read `baseUrl` to know which instance a token must be valid for; read `remotes` to see
|
|
18
|
+
upstream vs origin when they differ.
|
|
19
|
+
- Throws `No parseable git remotes found` if the cwd has no git remote or none parse. Do
|
|
20
|
+
NOT guess owner/repo from context — call this, or ask the user.
|
|
13
21
|
|
|
14
22
|
## list_my_repos
|
|
15
|
-
- Lists repositories the token can access. GLOBAL. Returns large objects — paginate (`limit` ≤ 100) and read only the fields you need (name, full_name, owner.login).
|
|
23
|
+
- Lists repositories the token can access. GLOBAL. Returns large objects — paginate (`limit` ≤ 100) and read only the fields you need (name, full_name, owner.login). If this returns
|
|
24
|
+
401/403 there is no usable token — switch to the **gitea-configure** skill.
|
|
16
25
|
|
|
17
26
|
## Resolution order (every repo-scoped tool)
|
|
18
|
-
explicit `owner`/`repo` args → `GITEA_DEFAULT_OWNER`/`GITEA_DEFAULT_REPO`
|
|
27
|
+
explicit `owner`/`repo` args → `GITEA_DEFAULT_OWNER`/`GITEA_DEFAULT_REPO` (or git-discovered
|
|
28
|
+
defaults) → `resolve_repo`. If none resolve, the tool throws `owner and repo are required`.
|
|
29
|
+
Wrong values 404 or silently target the wrong repo — never guess.
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runServer } from "./server.js";
|
|
3
|
+
import { discoverConfig } from "./git-config.js";
|
|
3
4
|
const argv = process.argv.slice(2);
|
|
4
5
|
if (argv[0] === "init") {
|
|
5
6
|
// `gitea-mcp init [--tool <name>]` installs the bundled skills into a target
|
|
6
7
|
// AI tool's skills directory. It needs no Gitea credentials, so it is
|
|
7
|
-
// dispatched before the
|
|
8
|
+
// dispatched before the config-discovery logic below.
|
|
8
9
|
const { runInitCommand } = await import("./skills.js");
|
|
9
10
|
runInitCommand(argv.slice(1)).catch((err) => {
|
|
10
11
|
console.error("Fatal error:", err);
|
|
@@ -12,15 +13,23 @@ if (argv[0] === "init") {
|
|
|
12
13
|
});
|
|
13
14
|
}
|
|
14
15
|
else {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
// Resolve baseUrl/owner/repo/credentials from env first, then the local git
|
|
17
|
+
// context (`.git/config` remotes + credential store). Discovery collects ALL
|
|
18
|
+
// credential candidates (config token, env token, credential-store entries)
|
|
19
|
+
// rather than picking one, so the client can fall back across them when one
|
|
20
|
+
// scheme is rejected (e.g. an account password that is not a PAT). When
|
|
21
|
+
// neither env nor any git remote provides a baseUrl, the server is
|
|
22
|
+
// intentionally skipped: a single global install should stay dormant outside
|
|
23
|
+
// of git projects.
|
|
24
|
+
const discovered = await discoverConfig().catch((err) => {
|
|
25
|
+
console.error("Fatal error:", err);
|
|
21
26
|
process.exit(1);
|
|
27
|
+
});
|
|
28
|
+
if (!discovered) {
|
|
29
|
+
console.error(`gitea-mcp: no git remote found in ${process.cwd()} and GITEA_BASE_URL is not set; skipping server start.`);
|
|
30
|
+
process.exit(0);
|
|
22
31
|
}
|
|
23
|
-
runServer(baseUrl,
|
|
32
|
+
runServer(discovered.baseUrl, discovered.candidates, discovered.defaultOwner, discovered.defaultRepo).catch((err) => {
|
|
24
33
|
console.error("Fatal error:", err);
|
|
25
34
|
process.exit(1);
|
|
26
35
|
});
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;IACvB,6EAA6E;IAC7E,sEAAsE;IACtE,sDAAsD;IACtD,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;IACvD,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACnD,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;QACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;KAAM,CAAC;IACN,4EAA4E;IAC5E,6EAA6E;IAC7E,4EAA4E;IAC5E,4EAA4E;IAC5E,wEAAwE;IACxE,mEAAmE;IACnE,6EAA6E;IAC7E,mBAAmB;IACnB,MAAM,UAAU,GAAG,MAAM,cAAc,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QAC/D,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;QACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,CAAC,KAAK,CACX,qCAAqC,OAAO,CAAC,GAAG,EAAE,wDAAwD,CAC3G,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,SAAS,CACP,UAAU,CAAC,OAAO,EAClB,UAAU,CAAC,UAAU,EACrB,UAAU,CAAC,YAAY,EACvB,UAAU,CAAC,WAAW,CACvB,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACvB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;QACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential candidate model and state machine for fault-tolerant Gitea auth.
|
|
3
|
+
*
|
|
4
|
+
* A discovered credential is treated as an opaque candidate: the `secret`
|
|
5
|
+
* field might be a Gitea Personal Access Token, an account login password,
|
|
6
|
+
* or an OAuth token — the type cannot be determined statically from any
|
|
7
|
+
* source (`~/.git-credentials` stores whatever the user typed into git's
|
|
8
|
+
* password prompt, and `[gitea] token` / `GITEA_TOKEN` are by convention but
|
|
9
|
+
* not guarantee a PAT). The runtime therefore tries each candidate under
|
|
10
|
+
* one or more HTTP auth schemes, advancing on 401/403 until something works
|
|
11
|
+
* or every candidate × scheme is exhausted.
|
|
12
|
+
*
|
|
13
|
+
* This module is pure (no I/O, no fetch) so the state machine is unit-tested
|
|
14
|
+
* directly. Mutation is contained to per-candidate state fields; callers
|
|
15
|
+
* never replace the candidates array.
|
|
16
|
+
*
|
|
17
|
+
* SECURITY: `secret` is never logged, never interpolated into error
|
|
18
|
+
* messages, and never surfaced by `summarizeCandidates`. Diagnostic output
|
|
19
|
+
* only ever carries `secretPresent: boolean` and a masked `username`.
|
|
20
|
+
*/
|
|
21
|
+
/** Where a candidate came from. Determines scheme ordering and priority. */
|
|
22
|
+
export type CredentialSource = "gitea-config" | "env" | "credential-store";
|
|
23
|
+
/**
|
|
24
|
+
* HTTP auth scheme. Both are sent via the `Authorization` header.
|
|
25
|
+
* - `token` → `Authorization: token <PAT>` (Gitea's PAT/OAuth-token scheme)
|
|
26
|
+
* - `basic` → `Authorization: Basic base64(<username>:<secret>)` (works for
|
|
27
|
+
* both account passwords and PATs when the username matches the owner)
|
|
28
|
+
*/
|
|
29
|
+
export type AuthScheme = "token" | "basic";
|
|
30
|
+
/**
|
|
31
|
+
* A single candidate credential with its runtime state. The state fields
|
|
32
|
+
* (`status`, `nextSchemeIndex`, `lastTriedScheme`, `lastError`,
|
|
33
|
+
* `activeScheme`) are mutated by the GiteaClient as requests succeed or fail.
|
|
34
|
+
*/
|
|
35
|
+
export interface CandidateCredential {
|
|
36
|
+
source: CredentialSource;
|
|
37
|
+
/** Username from a credential-store URL. Undefined for config/env sources. */
|
|
38
|
+
username?: string;
|
|
39
|
+
/** The secret value (PAT, password, or OAuth token). Never logged. */
|
|
40
|
+
secret: string;
|
|
41
|
+
/**
|
|
42
|
+
* Ordered list of auth schemes to try for this candidate. The first scheme
|
|
43
|
+
* that succeeds is locked in via `activeScheme` and reused for subsequent
|
|
44
|
+
* requests without re-iterating.
|
|
45
|
+
*/
|
|
46
|
+
schemes: AuthScheme[];
|
|
47
|
+
/** Runtime state: pending (untried), active (verified working), exhausted. */
|
|
48
|
+
status: "pending" | "active" | "exhausted";
|
|
49
|
+
/** Index into `schemes` for the next untried scheme. */
|
|
50
|
+
nextSchemeIndex: number;
|
|
51
|
+
/** The scheme that last produced an HTTP error (for diagnostics). */
|
|
52
|
+
lastTriedScheme?: AuthScheme;
|
|
53
|
+
/**
|
|
54
|
+
* The scheme that succeeded and is in active use. Set when status becomes
|
|
55
|
+
* "active"; subsequent requests reuse it without re-iterating.
|
|
56
|
+
*/
|
|
57
|
+
activeScheme?: AuthScheme;
|
|
58
|
+
/**
|
|
59
|
+
* Short error reason from the last failed attempt (e.g. "401"). Never
|
|
60
|
+
* contains the secret or the response body.
|
|
61
|
+
*/
|
|
62
|
+
lastError?: string;
|
|
63
|
+
}
|
|
64
|
+
/** Result of credential discovery — feeds straight into GiteaClient. */
|
|
65
|
+
export interface CredentialDiscoveryResult {
|
|
66
|
+
baseUrl: string;
|
|
67
|
+
defaultOwner?: string;
|
|
68
|
+
defaultRepo?: string;
|
|
69
|
+
/** Name of the remote the values were derived from. */
|
|
70
|
+
remote?: string;
|
|
71
|
+
/** Candidates in priority order (highest priority first). */
|
|
72
|
+
candidates: CandidateCredential[];
|
|
73
|
+
}
|
|
74
|
+
/** A picked next attempt — the candidate index plus the scheme to apply. */
|
|
75
|
+
export interface Attempt {
|
|
76
|
+
candidateIndex: number;
|
|
77
|
+
scheme: AuthScheme;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Decide scheme ordering for a credential-store entry based on username
|
|
81
|
+
* heuristic. Config/env candidates always use `["token"]` (per project
|
|
82
|
+
* decision to preserve their "simple token" semantics).
|
|
83
|
+
*
|
|
84
|
+
* - Username `oauth2`, `x-oauth-basic`, or empty → `["token", "basic"]`:
|
|
85
|
+
* these are git OAuth conventions; basic auth with such a username fails
|
|
86
|
+
* for real passwords, so try `token` first.
|
|
87
|
+
* - Any other (real-looking) username → `["basic", "token"]`: basic auth
|
|
88
|
+
* with the correct username works for both PATs and passwords, so it has
|
|
89
|
+
* the widest coverage and goes first.
|
|
90
|
+
*/
|
|
91
|
+
export declare function orderSchemesForCredentialStore(username?: string): AuthScheme[];
|
|
92
|
+
/**
|
|
93
|
+
* Build the `Authorization` header value for one attempt. Mutates nothing.
|
|
94
|
+
*
|
|
95
|
+
* For `basic`, the username falls back to `oauth2` when absent (a missing
|
|
96
|
+
* username only happens for malformed credential-store entries; Gitea
|
|
97
|
+
* rejects basic auth without a username, so the attempt will fail and the
|
|
98
|
+
* state machine will advance).
|
|
99
|
+
*/
|
|
100
|
+
export declare function buildAuthHeader(candidate: CandidateCredential, scheme: AuthScheme): string;
|
|
101
|
+
/**
|
|
102
|
+
* Pick the next (candidate, scheme) to try. Skips exhausted candidates and
|
|
103
|
+
* candidates whose scheme list is fully tried. Does NOT mutate state — the
|
|
104
|
+
* caller records the attempt via `markAttemptFailed` / `markAttemptSucceeded`.
|
|
105
|
+
*
|
|
106
|
+
* Returns null when every candidate × scheme has been tried.
|
|
107
|
+
*/
|
|
108
|
+
export declare function pickNextAttempt(candidates: CandidateCredential[]): Attempt | null;
|
|
109
|
+
/**
|
|
110
|
+
* Record that an attempt failed with `error` (a short reason like "401").
|
|
111
|
+
* Advances the candidate's scheme index; when no schemes remain, marks the
|
|
112
|
+
* candidate as exhausted.
|
|
113
|
+
*/
|
|
114
|
+
export declare function markAttemptFailed(candidates: CandidateCredential[], candidateIndex: number, error: string): void;
|
|
115
|
+
/**
|
|
116
|
+
* Record that an attempt succeeded. Marks the candidate as active, locks in
|
|
117
|
+
* `activeScheme`, and marks all PRIOR candidates as exhausted (they were
|
|
118
|
+
* tried and failed before this one succeeded).
|
|
119
|
+
*/
|
|
120
|
+
export declare function markAttemptSucceeded(candidates: CandidateCredential[], candidateIndex: number, scheme: AuthScheme): void;
|
|
121
|
+
/**
|
|
122
|
+
* Mask a username for diagnostic output: first character plus `***`. Returns
|
|
123
|
+
* null when the candidate has no username (config/env sources, or malformed
|
|
124
|
+
* credential-store entries).
|
|
125
|
+
*/
|
|
126
|
+
export declare function maskUsername(username?: string): string | null;
|
|
127
|
+
/**
|
|
128
|
+
* Build a redacted summary of all candidates for the `gitea_status` tool.
|
|
129
|
+
* The `secret` value is never included — only `secretPresent: true`. The
|
|
130
|
+
* caller passes the active candidate index (or null) so the summary can flag
|
|
131
|
+
* which candidate is currently in use.
|
|
132
|
+
*/
|
|
133
|
+
export interface CandidateSummary {
|
|
134
|
+
source: CredentialSource;
|
|
135
|
+
schemes: AuthScheme[];
|
|
136
|
+
username: string | null;
|
|
137
|
+
secretPresent: boolean;
|
|
138
|
+
status: "pending" | "active" | "exhausted";
|
|
139
|
+
lastTriedScheme: AuthScheme | null;
|
|
140
|
+
activeScheme: AuthScheme | null;
|
|
141
|
+
lastError: string | null;
|
|
142
|
+
}
|
|
143
|
+
export declare function summarizeCandidates(candidates: CandidateCredential[]): CandidateSummary[];
|
|
144
|
+
/** Find the index of the active candidate, or null when none is active. */
|
|
145
|
+
export declare function findActiveCandidateIndex(candidates: CandidateCredential[]): number | null;
|