@amonstack/gitea-mcp 0.3.0 → 0.3.2

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
@@ -1,20 +1,50 @@
1
- # gitea-mcp
1
+ <p align="center">
2
+ <img alt="gitea-mcp" src="https://raw.githubusercontent.com/amonstack/gitea_mcp/master/docs/assets/gitea-mcp-banner.png" />
3
+ <h3 align="center">gitea-mcp</h3>
4
+ <p align="center">MCP server that exposes Gitea issues, labels, milestones, and comments as tools for AI assistants</p>
5
+ </p>
2
6
 
3
- [中文文档](https://github.com/amonstack/gitea_mcp/blob/master/README.zh-CN.md)
7
+ ---
4
8
 
5
- > MCP server that lets AI assistants manage Gitea repositories — issues, labels,
6
- > milestones, comments, and more via the Gitea API.
9
+ [![npm version](https://img.shields.io/npm/v/@amonstack/gitea-mcp?logo=npm)](https://www.npmjs.com/package/@amonstack/gitea-mcp)
10
+ [![codecov](https://codecov.io/gh/amonstack/gitea_mcp/branch/master/graph/badge.svg)](https://codecov.io/gh/amonstack/gitea_mcp)
11
+ [![license](https://img.shields.io/npm/l/@amonstack/gitea-mcp)](https://github.com/amonstack/gitea_mcp/blob/master/LICENSE)
12
+ [![Node](https://img.shields.io/node/v/@amonstack/gitea-mcp?logo=node.js)](https://www.npmjs.com/package/@amonstack/gitea-mcp)
13
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
14
+ [![MCP](https://img.shields.io/badge/MCP-server-6f42c1?logo=modelcontextprotocol&logoColor=white)](https://modelcontextprotocol.io)
7
15
 
8
- ## What is gitea-mcp?
16
+ **English** | [中文文档](https://github.com/amonstack/gitea_mcp/blob/master/README.zh-CN.md)
9
17
 
10
- `gitea-mcp` is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)
11
- server that exposes Gitea repository operations as tools. Once connected to an
12
- MCP client (Claude Desktop, opencode, Cursor, etc.), an AI assistant can list,
13
- create, update, and delete issues, labels, milestones, and comments on your
14
- Gitea instance — all through natural language.
18
+ `gitea-mcp` is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that exposes Gitea repository operations as tools. Once connected to an MCP client (Claude Desktop, opencode, Cursor, etc.), an AI assistant can list, create, update, and delete issues, labels, milestones, and comments on your Gitea instance — all through natural language.
15
19
 
16
- The server communicates over stdio and wraps the [Gitea REST API
17
- (`/api/v1`)](https://docs.gitea.com/api/1.22/).
20
+ The server communicates over stdio and wraps the [Gitea REST API (`/api/v1`)](https://docs.gitea.com/api/1.22/).
21
+
22
+ ## Features
23
+
24
+ - **Full Gitea project management** — issues, labels, milestones, and comments via natural language
25
+ - **Zero-config auto-discovery** — reads `baseUrl`, `owner`, `repo`, and token from the project's git config; one global install serves many repos
26
+ - **Multi-source auth with failover** — tries `[gitea]` config tokens, `GITEA_TOKEN`, then the git credential store, advancing automatically on `401`/`403`
27
+ - **Action-scoped skills** — ships one skill per workflow (find, create, label, comment, plan milestones, …) for opencode, Claude Code, Cursor, and more
28
+ - **Client-agnostic** — works with any stdio-based MCP client; ships guidance prompts and on-demand reference resources too
29
+
30
+ ## Table of Contents
31
+
32
+ - [Features](#features)
33
+ - [Requirements](#requirements)
34
+ - [Installation](#installation)
35
+ - [Configuration](#configuration)
36
+ - [MCP Client Setup](#mcp-client-setup)
37
+ - [Available Tools](#available-tools)
38
+ - [AI Guidance & Skills](#ai-guidance--skills)
39
+ - [Development](#development)
40
+ - [Contributing](#contributing)
41
+ - [License](#license)
42
+
43
+ ## Requirements
44
+
45
+ - **Node.js ≥ 18** — uses the global `fetch`
46
+ - A **Gitea instance** (self-hosted or Gitea Cloud) reachable over HTTP
47
+ - A **Gitea API token** (or a git credential-store entry) for anything beyond reading public repositories
18
48
 
19
49
  ## Installation
20
50
 
@@ -59,7 +89,7 @@ projects. Set them only to override the discovery.
59
89
  | Variable | Required | Description |
60
90
  |----------|:--------:|-------------|
61
91
  | `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. Resolved from `.git/config`, the git credential store, then this variable (see [Token discovery](#token-discovery)). |
92
+ | `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)). |
63
93
  | `GITEA_DEFAULT_OWNER` | No | Default repository owner — skip passing `owner` on every call |
64
94
  | `GITEA_DEFAULT_REPO` | No | Default repository name — skip passing `repo` on every call |
65
95
 
@@ -80,21 +110,48 @@ repository, or set `GITEA_BASE_URL` / `GITEA_TOKEN` explicitly.
80
110
 
81
111
  ### Token discovery
82
112
 
83
- The token is resolved in this order (first match wins):
113
+ `gitea-mcp` collects authentication **candidates** from three sources, in this
114
+ priority order:
84
115
 
85
- 1. A `[gitea "<baseUrl>"]` section in `.git/config`:
116
+ 1. A `[gitea "<baseUrl>"]` section in `.git/config` (a bare `[gitea]` section is
117
+ a host-wide fallback):
86
118
  ```ini
87
119
  [gitea "https://gitea.example.com"]
88
120
  token = <your-token>
89
121
  ```
90
- A bare `[gitea]` section with `token = ...` is a host-wide fallback.
91
- 2. The git credential store (`~/.git-credentials`, or `$XDG_CONFIG_HOME/git/credentials`)
92
- a line whose host matches the instance, e.g. `https://oauth2:<token>@gitea.example.com`.
93
- 3. The `GITEA_TOKEN` environment variable.
94
-
95
- If none resolves, the server still starts without a token (anonymous). Public repositories
96
- may be read; private repos and write operations return `401` — use the `gitea-configure`
97
- skill to guide setup, or set `GITEA_TOKEN`.
122
+ Always sent as `Authorization: token <token>`.
123
+ 2. The `GITEA_TOKEN` environment variable also sent as `Authorization: token`.
124
+ 3. The git credential store (`~/.git-credentials`, or
125
+ `$XDG_CONFIG_HOME/git/credentials`) every line whose host matches the
126
+ instance, e.g. `https://alice:s3cret@gitea.example.com`. When several lines
127
+ match, the one whose path best matches `owner/repo` is tried first.
128
+
129
+ A credential-store entry's `password` field may hold a real PAT, an account
130
+ password, or an OAuth token — git stores whatever was typed at the prompt, and
131
+ the server cannot tell them apart statically. So each credential-store entry is
132
+ tried under **two authentication schemes**:
133
+
134
+ - `Authorization: Basic <base64(user:pass)>` — works for account passwords and
135
+ PATs alike (Gitea checks that the username matches the secret's owner).
136
+ - `Authorization: token <secret>` — works only for real PATs.
137
+
138
+ The order is chosen by a username heuristic: a convention username
139
+ (`oauth2`, `x-oauth-basic`, or empty) tries `token` first; a real-looking
140
+ username (e.g. `alice`) tries `basic` first.
141
+
142
+ **Fault tolerance.** On `401`/`403` the server advances to the next
143
+ scheme/candidate and retries the same request; once a combination succeeds it is
144
+ locked for the rest of the session (no re-probing). Non-auth errors (`404`,
145
+ `500`, network) propagate immediately and do **not** trigger a retry.
146
+
147
+ **Diagnostics.** The `gitea_status` tool (see [Repository Helpers](#repository-helpers))
148
+ returns a redacted view of the current state — which candidate is active, which
149
+ are exhausted, the last status seen — without ever exposing the secret. Use it
150
+ to troubleshoot a `401` instead of guessing.
151
+
152
+ If no source resolves a credential, the server still starts anonymously. Public
153
+ repositories may be read; private repos and write operations return `401` — use
154
+ the `gitea-configure` skill to guide setup, or set `GITEA_TOKEN`.
98
155
 
99
156
  When `GITEA_DEFAULT_OWNER` and `GITEA_DEFAULT_REPO` are set, you can omit the
100
157
  `owner` and `repo` parameters in tool calls. The `resolve_repo` tool can also
@@ -234,6 +291,7 @@ gitea-mcp
234
291
  |------|-------------|
235
292
  | `list_my_repos` | List repositories accessible to the authenticated user |
236
293
  | `resolve_repo` | Detect `baseUrl`, `owner`, and `repo` from the project's git remotes (`upstream` preferred, then `origin`) |
294
+ | `gitea_status` | Inspect credential-handling state — active candidate, exhausted candidates, last error (redacted; secrets never exposed) |
237
295
 
238
296
  ## AI Guidance & Skills
239
297
 
@@ -290,11 +348,20 @@ npm ci
290
348
  | `make test` | Run unit tests |
291
349
  | `make test-watch` | Run tests in watch mode |
292
350
  | `make test-integration` | Run integration tests (needs live Gitea instance) |
351
+ | `make scan` | Scan for leaked secrets with gitleaks (part of `make verify`) |
293
352
  | `make dev` | Run directly with tsx |
294
353
 
295
354
  For the full architecture — module layout, dependency graph, core patterns, and
296
355
  the guide to adding a new tool — see [`docs/architecture.md`](docs/architecture.md).
297
356
 
357
+ ## Contributing
358
+
359
+ Contributions are welcome!
360
+
361
+ - Found a bug or have a feature idea? Please [open an issue](https://github.com/amonstack/gitea_mcp/issues).
362
+ - Pull requests are gladly accepted. This repo follows [Conventional Commits](https://www.conventionalcommits.org/) and the workflow described in [`AGENTS.md`](AGENTS.md) — please skim it before your first PR.
363
+ - For the module layout, dependency graph, and the guide to adding a new tool, see [`docs/architecture.md`](docs/architecture.md).
364
+
298
365
  ## License
299
366
 
300
- MIT
367
+ [MIT](LICENSE) — Copyright (c) 2026 [amonstack](https://github.com/amonstack).
package/README.zh-CN.md CHANGED
@@ -1,19 +1,50 @@
1
- # gitea-mcp
1
+ <p align="center">
2
+ <img alt="gitea-mcp" src="https://raw.githubusercontent.com/amonstack/gitea_mcp/master/docs/assets/gitea-mcp-banner.png" />
3
+ <h3 align="center">gitea-mcp</h3>
4
+ <p align="center">MCP 服务端,将 Gitea 的议题、标签、里程碑与评论封装为工具,供 AI 助手调用</p>
5
+ </p>
2
6
 
3
- [English](https://github.com/amonstack/gitea_mcp/blob/master/README.md)
7
+ ---
4
8
 
5
- > 让 AI 助手通过 MCP 协议管理 Gitea 仓库 —— 议题、标签、里程碑、评论等功能,
6
- > 全部通过 Gitea API 实现。
9
+ [![npm version](https://img.shields.io/npm/v/@amonstack/gitea-mcp?logo=npm)](https://www.npmjs.com/package/@amonstack/gitea-mcp)
10
+ [![codecov](https://codecov.io/gh/amonstack/gitea_mcp/branch/master/graph/badge.svg)](https://codecov.io/gh/amonstack/gitea_mcp)
11
+ [![license](https://img.shields.io/npm/l/@amonstack/gitea-mcp)](https://github.com/amonstack/gitea_mcp/blob/master/LICENSE)
12
+ [![Node](https://img.shields.io/node/v/@amonstack/gitea-mcp?logo=node.js)](https://www.npmjs.com/package/@amonstack/gitea-mcp)
13
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
14
+ [![MCP](https://img.shields.io/badge/MCP-server-6f42c1?logo=modelcontextprotocol&logoColor=white)](https://modelcontextprotocol.io)
7
15
 
8
- ## 这是什么?
16
+ [English](https://github.com/amonstack/gitea_mcp/blob/master/README.md) | **中文文档**
9
17
 
10
- `gitea-mcp` 是一个 [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)
11
- 服务端,将 Gitea 仓库操作暴露为 MCP 工具。连接到 MCP 客户端(Claude Desktop、
12
- opencode、Cursor 等)后,AI 助手即可通过自然语言在 Gitea 实例上创建、查询、
13
- 更新和删除议题、标签、里程碑和评论。
18
+ `gitea-mcp` 是一个 [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) 服务端,将 Gitea 仓库操作暴露为 MCP 工具。连接到 MCP 客户端(Claude Desktop、opencode、Cursor 等)后,AI 助手即可通过自然语言在 Gitea 实例上创建、查询、更新和删除议题、标签、里程碑和评论。
14
19
 
15
- 服务端通过 stdio 通信,封装了 [Gitea REST API
16
- (`/api/v1`)](https://docs.gitea.com/api/1.22/)。
20
+ 服务端通过 stdio 通信,封装了 [Gitea REST API (`/api/v1`)](https://docs.gitea.com/api/1.22/)。
21
+
22
+ ## 功能特性
23
+
24
+ - **完整的 Gitea 项目管理** —— 通过自然语言管理议题、标签、里程碑与评论
25
+ - **零配置自动发现** —— 从项目 git 配置读取 `baseUrl`、`owner`、`repo` 与令牌;一次全局安装即可服务多个仓库
26
+ - **多来源认证 + 自动容错** —— 依次尝试 `[gitea]` 配置令牌、`GITEA_TOKEN`、git 凭据存储,遇 `401`/`403` 自动切换
27
+ - **按动作划分的技能** —— 每个工作流一个技能(查找、创建、打标签、评论、规划里程碑……),适配 opencode、Claude Code、Cursor 等
28
+ - **客户端无关** —— 兼容任何基于 stdio 的 MCP 客户端;同时内置引导提示与按需参考资源
29
+
30
+ ## 目录
31
+
32
+ - [功能特性](#功能特性)
33
+ - [环境要求](#环境要求)
34
+ - [安装](#安装)
35
+ - [配置](#配置)
36
+ - [MCP 客户端配置](#mcp-客户端配置)
37
+ - [可用工具](#可用工具)
38
+ - [AI 引导与技能](#ai-引导与技能)
39
+ - [二次开发](#二次开发)
40
+ - [参与贡献](#参与贡献)
41
+ - [许可证](#许可证)
42
+
43
+ ## 环境要求
44
+
45
+ - **Node.js ≥ 18** —— 使用全局 `fetch`
46
+ - 一个可通过 HTTP 访问的 **Gitea 实例**(自托管或 Gitea Cloud)
47
+ - 一个 **Gitea API 令牌**(或 git 凭据存储条目),用于读取公开仓库以外的操作
17
48
 
18
49
  ## 安装
19
50
 
@@ -56,7 +87,7 @@ node dist/cli.js
56
87
  | 变量 | 必填 | 说明 |
57
88
  |------|:----:|------|
58
89
  | `GITEA_BASE_URL` | 否 | Gitea 实例地址(如 `https://gitea.example.com`)。未设置时从项目 git 远程地址自动推导。 |
59
- | `GITEA_TOKEN` | 否 | Gitea API 访问令牌。依次从 `.git/config`、git 凭据存储、本变量解析(见[令牌发现](#令牌发现))。 |
90
+ | `GITEA_TOKEN` | 否 | Gitea API 访问令牌。是多个认证候选之一;排在 `.git/config [gitea]` 令牌之后、git 凭据存储之前(见[令牌发现](#令牌发现))。 |
60
91
  | `GITEA_DEFAULT_OWNER` | 否 | 默认仓库所有者,免去每次传入 `owner` 参数 |
61
92
  | `GITEA_DEFAULT_REPO` | 否 | 默认仓库名称,免去每次传入 `repo` 参数 |
62
93
 
@@ -76,20 +107,40 @@ node dist/cli.js
76
107
 
77
108
  ### 令牌发现
78
109
 
79
- 令牌按以下顺序解析(先匹配者胜出):
110
+ `gitea-mcp` 从三个来源收集认证**候选**,按以下优先级排序:
80
111
 
81
- 1. `.git/config` 中的 `[gitea "<baseUrl>"]` 段:
112
+ 1. `.git/config` 中的 `[gitea "<baseUrl>"]` 段(不带地址的 `[gitea]` 段作为全局兜底):
82
113
  ```ini
83
114
  [gitea "https://gitea.example.com"]
84
115
  token = <your-token>
85
116
  ```
86
- 不带地址的 `[gitea]` 段中的 `token = ...` 作为全局兜底。
87
- 2. git 凭据存储(`~/.git-credentials`,或 `$XDG_CONFIG_HOME/git/credentials`)——host 匹配实例
88
- 的那一行,如 `https://oauth2:<token>@gitea.example.com`。
89
- 3. `GITEA_TOKEN` 环境变量。
117
+ 始终以 `Authorization: token <token>` 发送。
118
+ 2. `GITEA_TOKEN` 环境变量 —— 同样以 `Authorization: token` 发送。
119
+ 3. git 凭据存储(`~/.git-credentials`,或 `$XDG_CONFIG_HOME/git/credentials`)—— host 匹配实例
120
+ 的每一行,如 `https://alice:s3cret@gitea.example.com`。多行同时匹配时,path `owner/repo`
121
+ 最贴近的那一行优先尝试。
122
+
123
+ 凭据存储条目的 `password` 字段可能是真正的 PAT、账户登录密码,或 OAuth token —— git 存的是
124
+ 用户在密码提示里输入的任何内容,服务端无法静态区分。因此每个凭据存储条目会以**两种认证方案**
125
+ 依次尝试:
90
126
 
91
- 若都未解析到,服务器仍会以匿名方式启动。公开仓库可读;私有仓库和写操作返回 `401`——此时使用
92
- `gitea-configure` 技能引导配置,或设置 `GITEA_TOKEN`。
127
+ - `Authorization: Basic <base64(user:pass)>` —— 账户密码和 PAT 都能通过(Gitea 会校验用户名与
128
+ 密钥属主一致)。
129
+ - `Authorization: token <secret>` —— 仅对真正的 PAT 有效。
130
+
131
+ 尝试顺序由用户名启发式决定:约定用户名(`oauth2`、`x-oauth-basic`、空)先试 `token`;真实用户名
132
+ (如 `alice`)先试 `basic`。
133
+
134
+ **容错。** 遇到 `401`/`403`,服务端会切换到下一个方案/候选并重试同一请求;一旦某组凭据成功,
135
+ 它会被锁定到本会话结束(不再重复探测)。非认证类错误(`404`、`500`、网络错误)立即向上抛出,
136
+ **不会**触发重试。
137
+
138
+ **诊断。** `gitea_status` 工具(见[仓库辅助](#仓库辅助-repository-helpers))返回当前状态的脱敏
139
+ 视图 —— 哪个候选处于 active、哪些已耗尽、最近一次的状态码 —— 永不暴露密钥本身。排查 `401` 时
140
+ 用它代替盲猜。
141
+
142
+ 若所有来源都未解析到凭据,服务器仍会以匿名方式启动。公开仓库可读;私有仓库和写操作返回
143
+ `401` —— 此时使用 `gitea-configure` 技能引导配置,或设置 `GITEA_TOKEN`。
93
144
 
94
145
  设置 `GITEA_DEFAULT_OWNER` 和 `GITEA_DEFAULT_REPO` 后,调用工具时可以省略
95
146
  `owner` 和 `repo` 参数。也可以使用 `resolve_repo` 工具自动从本地 git 仓库
@@ -227,6 +278,7 @@ gitea-mcp
227
278
  |------|------|
228
279
  | `list_my_repos` | 列出当前用户可访问的仓库 |
229
280
  | `resolve_repo` | 从项目 git 远程地址检测 `baseUrl`、`owner`、`repo`(优先 `upstream`,回退 `origin`) |
281
+ | `gitea_status` | 查看认证处理状态 —— active 候选、已耗尽候选、最近一次错误(脱敏;永不暴露密钥) |
230
282
 
231
283
  ## AI 引导与技能
232
284
 
@@ -279,11 +331,20 @@ npm ci
279
331
  | `make test` | 运行单元测试 |
280
332
  | `make test-watch` | 监听模式运行测试 |
281
333
  | `make test-integration` | 运行集成测试(需要可用的 Gitea 实例) |
334
+ | `make scan` | 用 gitleaks 扫描泄露的密钥(属于 `make verify`) |
282
335
  | `make dev` | 通过 tsx 直接运行 |
283
336
 
284
337
  完整的架构说明(模块布局、依赖关系、核心模式,以及新增工具的指引)请参阅
285
338
  [`docs/architecture.md`](docs/architecture.md)。
286
339
 
340
+ ## 参与贡献
341
+
342
+ 欢迎贡献!
343
+
344
+ - 发现 bug 或有功能建议?请[提交一个 issue](https://github.com/amonstack/gitea_mcp/issues)。
345
+ - 欢迎提交 Pull Request。本仓库遵循 [Conventional Commits](https://www.conventionalcommits.org/),工作流见 [`AGENTS.md`](AGENTS.md) —— 首次 PR 前请先浏览一遍。
346
+ - 模块布局、依赖关系、核心模式以及新增工具的指引,请参阅 [`docs/architecture.md`](docs/architecture.md)。
347
+
287
348
  ## 许可证
288
349
 
289
- MIT
350
+ [MIT](LICENSE) —— 版权所有 (c) 2026 [amonstack](https://github.com/amonstack)。
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ export declare const TOP_LEVEL_USAGE = "Usage: gitea-mcp [command] [options]\n\nGitea MCP server \u2014 exposes Gitea issues, labels, milestones, and comments as\ntools for AI assistants. With no command, it starts the Model Context Protocol\nserver over stdio and auto-discovers baseUrl, owner, repo, and credentials from\nthe environment and the local git remote (.git/config).\n\nCommands:\n init Install bundled action skills into an AI tool's skills directory.\n\nOptions:\n -h, --help Show this help and exit.\n -V, --version Show version and exit.\n\nWith no command, gitea-mcp starts the MCP server. Run `gitea-mcp init --help`\nfor init-specific options.\n";
package/dist/cli.js CHANGED
@@ -1,8 +1,36 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
2
3
  import { runServer } from "./server.js";
3
4
  import { discoverConfig } from "./git-config.js";
5
+ const pkgVersion = createRequire(import.meta.url)("../package.json").version;
6
+ export const TOP_LEVEL_USAGE = `Usage: gitea-mcp [command] [options]
7
+
8
+ Gitea MCP server — exposes Gitea issues, labels, milestones, and comments as
9
+ tools for AI assistants. With no command, it starts the Model Context Protocol
10
+ server over stdio and auto-discovers baseUrl, owner, repo, and credentials from
11
+ the environment and the local git remote (.git/config).
12
+
13
+ Commands:
14
+ init Install bundled action skills into an AI tool's skills directory.
15
+
16
+ Options:
17
+ -h, --help Show this help and exit.
18
+ -V, --version Show version and exit.
19
+
20
+ With no command, gitea-mcp starts the MCP server. Run \`gitea-mcp init --help\`
21
+ for init-specific options.
22
+ `;
4
23
  const argv = process.argv.slice(2);
5
- if (argv[0] === "init") {
24
+ const head = argv[0];
25
+ if (head === "-h" || head === "--help" || head === "help") {
26
+ process.stdout.write(TOP_LEVEL_USAGE);
27
+ process.exit(0);
28
+ }
29
+ if (head === "-V" || head === "--version") {
30
+ process.stdout.write(`gitea-mcp ${pkgVersion}\n`);
31
+ process.exit(0);
32
+ }
33
+ if (head === "init") {
6
34
  // `gitea-mcp init [--tool <name>]` installs the bundled skills into a target
7
35
  // AI tool's skills directory. It needs no Gitea credentials, so it is
8
36
  // dispatched before the config-discovery logic below.
@@ -13,10 +41,14 @@ if (argv[0] === "init") {
13
41
  });
14
42
  }
15
43
  else {
16
- // Resolve baseUrl/owner/repo/token from env first, then the local git context
17
- // (`.git/config` remotes + credential store). When neither env nor any git
18
- // remote provides a baseUrl, the server is intentionally skipped: a single
19
- // global install should stay dormant outside of git projects.
44
+ // Resolve baseUrl/owner/repo/credentials from env first, then the local git
45
+ // context (`.git/config` remotes + credential store). Discovery collects ALL
46
+ // credential candidates (config token, env token, credential-store entries)
47
+ // rather than picking one, so the client can fall back across them when one
48
+ // scheme is rejected (e.g. an account password that is not a PAT). When
49
+ // neither env nor any git remote provides a baseUrl, the server is
50
+ // intentionally skipped: a single global install should stay dormant outside
51
+ // of git projects.
20
52
  const discovered = await discoverConfig().catch((err) => {
21
53
  console.error("Fatal error:", err);
22
54
  process.exit(1);
@@ -25,7 +57,7 @@ else {
25
57
  console.error(`gitea-mcp: no git remote found in ${process.cwd()} and GITEA_BASE_URL is not set; skipping server start.`);
26
58
  process.exit(0);
27
59
  }
28
- runServer(discovered.baseUrl, discovered.token, discovered.defaultOwner, discovered.defaultRepo).catch((err) => {
60
+ runServer(discovered.baseUrl, discovered.candidates, discovered.defaultOwner, discovered.defaultRepo).catch((err) => {
29
61
  console.error("Fatal error:", err);
30
62
  process.exit(1);
31
63
  });
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;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,8EAA8E;IAC9E,2EAA2E;IAC3E,2EAA2E;IAC3E,8DAA8D;IAC9D,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,KAAK,EAChB,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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,MAAM,UAAU,GAAI,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAAyB,CAAC,OAAO,CAAC;AAEtG,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;CAgB9B,CAAC;AAEF,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAErB,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;IAC1D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IACtC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;IAC1C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,UAAU,IAAI,CAAC,CAAC;IAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;IACpB,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;