@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 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` | Yes | Gitea instance URL (e.g. `https://gitea.example.com`) |
58
- | `GITEA_TOKEN` | Yes | Gitea API access 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, set the required environment variables and run:
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` | Auto-detect `owner` and `repo` from a local git remote URL |
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` | | Gitea 实例地址(如 `https://gitea.example.com`) |
56
- | `GITEA_TOKEN` | | Gitea API 访问令牌 |
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` | 从本地 git 仓库的远程地址自动检测 `owner` `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 (set at server start).
14
- 3. The `resolve_repo` tool (detects owner/repo from a local git remote).
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 neither argument nor env var is set, the call errors. Before a batch of work on a
17
- repo you have not used this session, call `resolve_repo` (or confirm the env defaults)
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 scope/expiry; 404 wrong owner/repo or no permission; 409 conflict;
63
- 422validation. Do not retry blindly on 4xx.
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 the `origin` remote of the git repo at `path` (default: current directory) and parses owner/repo from SSH (`git@gitea.example:owner/repo.git`) or HTTPS (`https://gitea.example/owner/repo.git`) URLs.
12
- - Throws if there is no `origin` remote or the URL is unparseable. Do NOT guess owner/repo from context — call this, or ask the user.
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` → `resolve_repo`. If none resolve, the tool throws `owner and repo are required`. Wrong values 404 or silently target the wrong repo — never guess.
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 env-var guard below.
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
- const baseUrl = process.env.GITEA_BASE_URL;
16
- const token = process.env.GITEA_TOKEN;
17
- const defaultOwner = process.env.GITEA_DEFAULT_OWNER;
18
- const defaultRepo = process.env.GITEA_DEFAULT_REPO;
19
- if (!baseUrl || !token) {
20
- console.error("GITEA_BASE_URL and GITEA_TOKEN environment variables are required");
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, token, defaultOwner, defaultRepo).catch((err) => {
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;AAExC,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,6CAA6C;IAC7C,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,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAC3C,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IACtC,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;IACrD,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAEnD,IAAI,CAAC,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC;QACvB,OAAO,CAAC,KAAK,CAAC,mEAAmE,CAAC,CAAC;QACnF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QAC1E,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;QACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,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;