feat(cmd): add -S/--scope to load only selected tool scopes (#219)

`-O` / `--tools` can already narrow the exposed tool set, but it needs every tool spelled out by name. `-S` / `--scope` (`GITEA_SCOPES`) complements it by selecting whole scopes, using the same names the `Scope` column of the tool tables documents.

```bash
gitea-mcp -S issue,pull_request                     # only those scopes
gitea-mcp --scope repository,branch --tools get_me   # those scopes plus one extra tool
```

## What changed

- Each scope is one `tool.Tool` registry carrying a canonical name, so `operation/repo` is split into the six scopes its files already imply: `repository` (`repo.go` + `tree.go`), `file`, `branch`, `tag`, `commit`, `release`. The other twelve packages map 1:1, giving 18 scopes.
- The two allowlists combine as a **union**: with neither set every tool loads; with only `--tools` set behaviour is unchanged; with both set, the selected scopes' tools plus the individually named tools load. `-r` / `GITEA_READONLY` still hides write tools on top.
- Scope names are normalized on input (case, spaces, hyphens), so `Pull Request`, `pull-request` and `PULL_REQUEST` all resolve to `pull_request`. Unknown names only warn and list the valid scopes, mirroring how `--tools` treats unknown tool names.
- The `Scope` column of all three READMEs now uses the canonical names verbatim, so it doubles as the reference for `--scope`, and `TestReadmeToolTables` compares that column against the registry in both directions — no translation table needed.
- Flag parsing moves from `cmd.init()` into `Execute()`. `main()` only ever calls `Execute()`, so this is behaviourally equivalent, and it makes the `cmd` package testable at all: previously the `init()` parse of `os.Args` hit `flag.CommandLine`'s `ExitOnError` on `go test`'s own `-test.*` flags.

## Verification

`make fmt`, `make lint-go` (0 issues) and `go test ./...` all pass. New tests cover the filter matrix (no filters / scope-only / tools-only regression / union / read-only interaction / unknown scope), scope-name uniqueness across `domainTools`, and the flag+env parsing and normalization.

Smoke-tested `tools/list` over stdio against the built binary:

| flags | tools exposed |
| :-- | :-- |
| _none_ | 54 (identical to `main`) |
| `-S branch` | `create_branch`, `delete_branch`, `list_branches` |
| `--scope 'Pull Request,TAG'` | the 4 `pull_request` + 4 `tag` tools |
| `-O get_me` | `get_me` (unchanged) |
| `-S commit -O get_me` | `get_commit`, `list_commits`, `get_me` |
| `-S file -r` | `get_dir_contents`, `get_file_contents` |
| `-S bogus,issue` | the 4 `issue` tools, plus a warning naming the valid scopes |

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/219
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: yp05327 <576951401@qq.com>
This commit is contained in:
yp05327
2026-07-27 18:12:29 +00:00
committed by bircni
parent c97c38996c
commit 21aa4684d9
29 changed files with 580 additions and 235 deletions
+62 -55
View File
@@ -131,61 +131,68 @@ Once configured, try `list all my repositories` in the chat box.
| Tool | Scope | Access | Description |
| :--------------------------- | :----------- | :----- | :----------------------------------------------------------------------------------------- |
| get_gitea_mcp_server_version | Version | Read | Get the Gitea MCP server version |
| get_me | User | Read | Get the current authenticated user |
| get_user_orgs | User | Read | List the current user's organizations |
| search_users | Search | Read | Search for users |
| search_org_teams | Search | Read | Search teams within an organization |
| search_repos | Search | Read | Search for repositories |
| search_issues | Search | Read | Search issues and pull requests across repositories |
| notification_read | Notification | Read | Read notifications: list (optionally scoped to a repo) or get a thread by ID |
| notification_write | Notification | Write | Mark a notification or all notifications as read |
| label_read | Label | Read | Read repository or organization labels |
| label_write | Label | Write | Write labels (repo or org): create, edit, delete |
| milestone_read | Milestone | Read | Read milestones: get one or list |
| milestone_write | Milestone | Write | Write milestones: create, update, delete |
| wiki_read | Wiki | Read | Read wiki: list pages, get content, revision history |
| wiki_write | Wiki | Write | Write wiki pages: create, update, delete |
| timetracking_read | Timetracking | Read | Read time tracking: issue/repo times, active stopwatches, your tracked times |
| timetracking_write | Timetracking | Write | Write time tracking: stopwatches and entries |
| package_read | Packages | Read | Read package registry: list packages, list versions, or get a version |
| package_write | Packages | Write | Delete a package version (irreversible) |
| list_issues | Issue | Read | List repository issues |
| attachment_read | Issue | Read | Read issue/comment attachments: list metadata, get metadata, or download content |
| issue_read | Issue | Read | Read issue: details, comments, or labels |
| issue_write | Issue | Write | Write issues: create, update, manage comments and labels |
| list_pull_requests | Pull Request | Read | List repository pull requests |
| pull_request_read | Pull Request | Read | Read pull request: details, diff, changed files, head commit status, reviews |
| pull_request_write | Pull Request | Write | Write pull requests: create, update, close, reopen, merge, update branch, manage reviewers |
| pull_request_review_write | Pull Request | Write | Write PR reviews: create, submit, delete, dismiss |
| actions_config_read | Actions | Read | Read Actions secrets and variables |
| actions_config_write | Actions | Write | Write Actions secrets and variables: upsert, create, update, delete |
| actions_run_read | Actions | Read | Read Actions workflows, runs, jobs, logs, and artifacts |
| actions_run_write | Actions | Write | Write Actions runs: dispatch, cancel, rerun |
| create_repo | Repository | Write | Create a new repository |
| fork_repo | Repository | Write | Fork a repository |
| list_my_repos | Repository | Read | List repositories owned by the current user |
| list_org_repos | Repository | Read | List repositories in an organization |
| get_repository_tree | Repository | Read | Get the repository file tree |
| get_file_contents | File | Read | Get file content and metadata |
| get_dir_contents | File | Read | Get the entries in a directory |
| create_or_update_file | File | Write | Create or update a file (provide sha to update an existing file) |
| delete_file | File | Write | Delete a file |
| create_branch | Branch | Write | Create a new branch |
| delete_branch | Branch | Write | Delete a branch |
| list_branches | Branch | Read | List repository branches |
| create_tag | Tag | Write | Create a tag |
| delete_tag | Tag | Write | Delete a tag |
| get_tag | Tag | Read | Get tag details |
| list_tags | Tag | Read | List repository tags |
| list_commits | Commit | Read | List repository commits |
| get_commit | Commit | Read | Get commit details |
| create_release | Release | Write | Create a release |
| delete_release | Release | Write | Delete a release |
| get_release | Release | Read | Get a release by ID |
| get_latest_release | Release | Read | Get the latest release |
| list_releases | Release | Read | List repository releases |
| get_gitea_mcp_server_version | version | Read | Get the Gitea MCP server version |
| get_me | user | Read | Get the current authenticated user |
| get_user_orgs | user | Read | List the current user's organizations |
| search_users | search | Read | Search for users |
| search_org_teams | search | Read | Search teams within an organization |
| search_repos | search | Read | Search for repositories |
| search_issues | search | Read | Search issues and pull requests across repositories |
| notification_read | notification | Read | Read notifications: list (optionally scoped to a repo) or get a thread by ID |
| notification_write | notification | Write | Mark a notification or all notifications as read |
| label_read | label | Read | Read repository or organization labels |
| label_write | label | Write | Write labels (repo or org): create, edit, delete |
| milestone_read | milestone | Read | Read milestones: get one or list |
| milestone_write | milestone | Write | Write milestones: create, update, delete |
| wiki_read | wiki | Read | Read wiki: list pages, get content, revision history |
| wiki_write | wiki | Write | Write wiki pages: create, update, delete |
| timetracking_read | timetracking | Read | Read time tracking: issue/repo times, active stopwatches, your tracked times |
| timetracking_write | timetracking | Write | Write time tracking: stopwatches and entries |
| package_read | packages | Read | Read package registry: list packages, list versions, or get a version |
| package_write | packages | Write | Delete a package version (irreversible) |
| list_issues | issue | Read | List repository issues |
| attachment_read | issue | Read | Read issue/comment attachments: list metadata, get metadata, or download content |
| issue_read | issue | Read | Read issue: details, comments, or labels |
| issue_write | issue | Write | Write issues: create, update, manage comments and labels |
| list_pull_requests | pull_request | Read | List repository pull requests |
| pull_request_read | pull_request | Read | Read pull request: details, diff, changed files, head commit status, reviews |
| pull_request_write | pull_request | Write | Write pull requests: create, update, close, reopen, merge, update branch, manage reviewers |
| pull_request_review_write | pull_request | Write | Write PR reviews: create, submit, delete, dismiss |
| actions_config_read | actions | Read | Read Actions secrets and variables |
| actions_config_write | actions | Write | Write Actions secrets and variables: upsert, create, update, delete |
| actions_run_read | actions | Read | Read Actions workflows, runs, jobs, logs, and artifacts |
| actions_run_write | actions | Write | Write Actions runs: dispatch, cancel, rerun |
| create_repo | repository | Write | Create a new repository |
| fork_repo | repository | Write | Fork a repository |
| list_my_repos | repository | Read | List repositories owned by the current user |
| list_org_repos | repository | Read | List repositories in an organization |
| get_repository_tree | repository | Read | Get the repository file tree |
| get_file_contents | file | Read | Get file content and metadata |
| get_dir_contents | file | Read | Get the entries in a directory |
| create_or_update_file | file | Write | Create or update a file (provide sha to update an existing file) |
| delete_file | file | Write | Delete a file |
| create_branch | branch | Write | Create a new branch |
| delete_branch | branch | Write | Delete a branch |
| list_branches | branch | Read | List repository branches |
| create_tag | tag | Write | Create a tag |
| delete_tag | tag | Write | Delete a tag |
| get_tag | tag | Read | Get tag details |
| list_tags | tag | Read | List repository tags |
| list_commits | commit | Read | List repository commits |
| get_commit | commit | Read | Get commit details |
| create_release | release | Write | Create a release |
| delete_release | release | Write | Delete a release |
| get_release | release | Read | Get a release by ID |
| get_latest_release | release | Read | Get the latest release |
| list_releases | release | Read | List repository releases |
> **Note:** Several tools are consolidated, action-based tools, a single tool exposes multiple operations through a `method` parameter. Tools with `Write` access are hidden when the server runs in read-only mode (`-r` / `GITEA_READONLY`), and the exposed tool set can be filtered with `-O` / `--tools` (`GITEA_TOOLS`).
> **Note:** Several tools are consolidated, action-based tools, a single tool exposes multiple operations through a `method` parameter. Tools with `Write` access are hidden when the server runs in read-only mode (`-r` / `GITEA_READONLY`), and the exposed tool set can be filtered by scope with `-S` / `--scope` (`GITEA_SCOPES`) and/or by individual tool name with `-O` / `--tools` (`GITEA_TOOLS`).
With neither flag set, every tool loads. `--scope` limits loading to tools whose Scope column value is in the given list; `--tools` limits loading to the named tools; setting both loads the union of the selected scopes and the individually named tools. Unknown scope names are ignored with a startup warning.
```bash
gitea-mcp -S issue,pull_request
gitea-mcp --scope repository,branch --tools get_me
```
Many tools accept `page` and `per_page` for pagination. The maximum effective page size is the Gitea server's `[api].MAX_RESPONSE_ITEMS` setting (default **50**), larger values are silently capped.
+63 -56
View File
@@ -130,62 +130,69 @@ Cursor 等客户端可使用 stdio 命令:
## 可用工具
| 工具 | 范围 | 访问 | 描述 |
| :--------------------------- | :------- | :--- | :------------------------------------------------------------------- |
| get_gitea_mcp_server_version | 版本 | 读取 | 获取 Gitea MCP 服务器版本 |
| get_me | 用户 | 读取 | 获取当前已认证用户 |
| get_user_orgs | 用户 | 读取 | 列出当前用户的组织 |
| search_users | 搜索 | 读取 | 搜索用户 |
| search_org_teams | 搜索 | 读取 | 搜索组织中的团队 |
| search_repos | 搜索 | 读取 | 搜索仓库 |
| search_issues | 搜索 | 读取 | 跨仓库搜索问题和拉取请求 |
| notification_read | 通知 | 读取 | 读取通知:列出(可限定仓库)或按 ID 获取会话 |
| notification_write | 通知 | 写入 | 将某条或全部通知标记为已读 |
| label_read | 标签 | 读取 | 读取仓库或组织标签 |
| label_write | 标签 | 写入 | 写入标签(仓库或组织):创建、编辑、删除 |
| milestone_read | 里程碑 | 读取 | 读取里程碑:获取单个或列出 |
| milestone_write | 里程碑 | 写入 | 写入里程碑:创建、更新、删除 |
| wiki_read | Wiki | 读取 | 读取 Wiki:列出页面、获取内容、修订历史 |
| wiki_write | Wiki | 写入 | 写入 Wiki 页面:创建、更新、删除 |
| timetracking_read | 时间跟踪 | 读取 | 读取时间跟踪:问题/仓库耗时、活动计时器、我的跟踪记录 |
| timetracking_write | 时间跟踪 | 写入 | 写入时间跟踪:计时器和记录条目 |
| package_read | 软件包 | 读取 | 读取软件包注册表:列出软件包、列出版本或获取某个版本 |
| package_write | 软件包 | 写入 | 删除软件包版本(不可恢复) |
| list_issues | 问题 | 读取 | 列出仓库问题 |
| attachment_read | 问题 | 读取 | 读取问题/评论附件:列出元数据、获取元数据或下载内容 |
| issue_read | 问题 | 读取 | 读取问题:详情、评论或标签 |
| issue_write | 问题 | 写入 | 写入问题:创建、更新、管理评论和标签 |
| list_pull_requests | 拉取请求 | 读取 | 列出仓库拉取请求 |
| pull_request_read | 拉取请求 | 读取 | 读取拉取请求:详情、差异、变更文件、头部提交状态、审查 |
| pull_request_write | 拉取请求 | 写入 | 写入拉取请求:创建、更新、关闭、重新打开、合并、更新分支、管理审查者 |
| pull_request_review_write | 拉取请求 | 写入 | 写入 PR 审查:创建、提交、删除、驳回 |
| actions_config_read | Actions | 读取 | 读取 Actions 密钥和变量 |
| actions_config_write | Actions | 写入 | 写入 Actions 密钥和变量:更新插入、创建、更新、删除 |
| actions_run_read | Actions | 读取 | 读取 Actions 工作流、运行、作业、日志和构件 |
| actions_run_write | Actions | 写入 | 写入 Actions 运行:触发、取消、重新运行 |
| create_repo | 仓库 | 写入 | 创建新仓库 |
| fork_repo | 仓库 | 写入 | 复刻仓库 |
| list_my_repos | 仓库 | 读取 | 列出当前用户拥有的仓库 |
| list_org_repos | 仓库 | 读取 | 列出组织中的仓库 |
| get_repository_tree | 仓库 | 读取 | 获取仓库文件树 |
| get_file_contents | 文件 | 读取 | 获取文件内容和元数据 |
| get_dir_contents | 文件 | 读取 | 获取目录中的条目 |
| create_or_update_file | 文件 | 写入 | 创建或更新文件(提供 sha 以更新现有文件) |
| delete_file | 文件 | 写入 | 删除文件 |
| create_branch | 分支 | 写入 | 创建新分支 |
| delete_branch | 分支 | 写入 | 删除分支 |
| list_branches | 分支 | 读取 | 列出仓库分支 |
| create_tag | Git 标签 | 写入 | 创建标签 |
| delete_tag | Git 标签 | 写入 | 删除标签 |
| get_tag | Git 标签 | 读取 | 获取标签详情 |
| list_tags | Git 标签 | 读取 | 列出仓库标签 |
| list_commits | 提交 | 读取 | 列出仓库提交 |
| get_commit | 提交 | 读取 | 获取提交详情 |
| create_release | 版本发布 | 写入 | 创建版本发布 |
| delete_release | 版本发布 | 写入 | 删除版本发布 |
| get_release | 版本发布 | 读取 | 按 ID 获取版本发布 |
| get_latest_release | 版本发布 | 读取 | 获取最新版本发布 |
| list_releases | 版本发布 | 读取 | 列出仓库版本发布 |
| :--------------------------- | :----------- | :- | :--------------------------------- |
| get_gitea_mcp_server_version | version | 读取 | 获取 Gitea MCP 服务器版本 |
| get_me | user | 读取 | 获取当前已认证用户 |
| get_user_orgs | user | 读取 | 列出当前用户的组织 |
| search_users | search | 读取 | 搜索用户 |
| search_org_teams | search | 读取 | 搜索组织中的团队 |
| search_repos | search | 读取 | 搜索仓库 |
| search_issues | search | 读取 | 跨仓库搜索问题和拉取请求 |
| notification_read | notification | 读取 | 读取通知:列出(可限定仓库)或按 ID 获取会话 |
| notification_write | notification | 写入 | 将某条或全部通知标记为已读 |
| label_read | label | 读取 | 读取仓库或组织标签 |
| label_write | label | 写入 | 写入标签(仓库或组织):创建、编辑、删除 |
| milestone_read | milestone | 读取 | 读取里程碑:获取单个或列出 |
| milestone_write | milestone | 写入 | 写入里程碑:创建、更新、删除 |
| wiki_read | wiki | 读取 | 读取 Wiki:列出页面、获取内容、修订历史 |
| wiki_write | wiki | 写入 | 写入 Wiki 页面:创建、更新、删除 |
| timetracking_read | timetracking | 读取 | 读取时间跟踪:问题/仓库耗时、活动计时器、我的跟踪记录 |
| timetracking_write | timetracking | 写入 | 写入时间跟踪:计时器和记录条目 |
| package_read | packages | 读取 | 读取软件包注册表:列出软件包、列出版本或获取某个版本 |
| package_write | packages | 写入 | 删除软件包版本(不可恢复) |
| list_issues | issue | 读取 | 列出仓库问题 |
| attachment_read | issue | 读取 | 读取问题/评论附件:列出元数据、获取元数据或下载内容 |
| issue_read | issue | 读取 | 读取问题:详情、评论或标签 |
| issue_write | issue | 写入 | 写入问题:创建、更新、管理评论和标签 |
| list_pull_requests | pull_request | 读取 | 列出仓库拉取请求 |
| pull_request_read | pull_request | 读取 | 读取拉取请求:详情、差异、变更文件、头部提交状态、审查 |
| pull_request_write | pull_request | 写入 | 写入拉取请求:创建、更新、关闭、重新打开、合并、更新分支、管理审查者 |
| pull_request_review_write | pull_request | 写入 | 写入 PR 审查:创建、提交、删除、驳回 |
| actions_config_read | actions | 读取 | 读取 Actions 密钥和变量 |
| actions_config_write | actions | 写入 | 写入 Actions 密钥和变量:更新插入、创建、更新、删除 |
| actions_run_read | actions | 读取 | 读取 Actions 工作流、运行、作业、日志和构件 |
| actions_run_write | actions | 写入 | 写入 Actions 运行:触发、取消、重新运行 |
| create_repo | repository | 写入 | 创建新仓库 |
| fork_repo | repository | 写入 | 复刻仓库 |
| list_my_repos | repository | 读取 | 列出当前用户拥有的仓库 |
| list_org_repos | repository | 读取 | 列出组织中的仓库 |
| get_repository_tree | repository | 读取 | 获取仓库文件树 |
| get_file_contents | file | 读取 | 获取文件内容和元数据 |
| get_dir_contents | file | 读取 | 获取目录中的条目 |
| create_or_update_file | file | 写入 | 创建或更新文件(提供 sha 以更新现有文件) |
| delete_file | file | 写入 | 删除文件 |
| create_branch | branch | 写入 | 创建新分支 |
| delete_branch | branch | 写入 | 删除分支 |
| list_branches | branch | 读取 | 列出仓库分支 |
| create_tag | tag | 写入 | 创建标签 |
| delete_tag | tag | 写入 | 删除标签 |
| get_tag | tag | 读取 | 获取标签详情 |
| list_tags | tag | 读取 | 列出仓库标签 |
| list_commits | commit | 读取 | 列出仓库提交 |
| get_commit | commit | 读取 | 获取提交详情 |
| create_release | release | 写入 | 创建版本发布 |
| delete_release | release | 写入 | 删除版本发布 |
| get_release | release | 读取 | 按 ID 获取版本发布 |
| get_latest_release | release | 读取 | 获取最新版本发布 |
| list_releases | release | 读取 | 列出仓库版本发布 |
> **说明:** 部分工具是聚合的、基于操作的工具,单个工具通过 `method` 参数暴露多个操作。当服务器以只读模式运行时(`-r` / `GITEA_READONLY`),访问为「写入」的工具会被隐藏;可通过 `-O` / `--tools``GITEA_TOOLS`)过滤对外暴露的工具集合。
> **说明:** 部分工具是聚合的、基于操作的工具,单个工具通过 `method` 参数暴露多个操作。当服务器以只读模式运行时(`-r` / `GITEA_READONLY`),访问为「写入」的工具会被隐藏;可通过 `-S` / `--scope``GITEA_SCOPES`)按范围过滤,或通过 `-O` / `--tools``GITEA_TOOLS`按工具名称过滤对外暴露的工具集合。
未设置任一参数时,会加载所有工具;仅设置 `--scope` 时,会加载这些范围内的所有工具;仅设置 `--tools` 时,只会加载指定名称的工具;两者都设置时,会加载所选范围的工具与指定工具名称的并集。范围名称即上表「范围」列中的值,未知的范围名称仅会在启动时产生警告并被忽略。
```bash
gitea-mcp -S issue,pull_request
gitea-mcp --scope repository,branch --tools get_me
```
许多工具支持 `page``per_page` 分页参数。最大有效页面大小由 Gitea 服务器的 `[api].MAX_RESPONSE_ITEMS` 设置决定(默认 **50**),超出的值会被静默截断。
+63 -56
View File
@@ -130,62 +130,69 @@ Cursor 等客戶端可使用 stdio 命令:
## 可用工具
| 工具 | 範圍 | 存取 | 描述 |
| :--------------------------- | :------- | :--- | :------------------------------------------------------------------- |
| get_gitea_mcp_server_version | 版本 | 讀取 | 取得 Gitea MCP 伺服器版本 |
| get_me | 用戶 | 讀取 | 取得目前已認證用戶 |
| get_user_orgs | 用戶 | 讀取 | 列出目前用戶的組織 |
| search_users | 搜尋 | 讀取 | 搜尋用戶 |
| search_org_teams | 搜尋 | 讀取 | 搜尋組織中的團隊 |
| search_repos | 搜尋 | 讀取 | 搜尋倉庫 |
| search_issues | 搜尋 | 讀取 | 跨倉庫搜尋問題和拉取請求 |
| notification_read | 通知 | 讀取 | 讀取通知:列出(可限定倉庫)或依 ID 取得會話 |
| notification_write | 通知 | 寫入 | 將某條或全部通知標記為已讀 |
| label_read | 標籤 | 讀取 | 讀取倉庫或組織標籤 |
| label_write | 標籤 | 寫入 | 寫入標籤(倉庫或組織):創建、編輯、刪除 |
| milestone_read | 里程碑 | 讀取 | 讀取里程碑:取得單個或列出 |
| milestone_write | 里程碑 | 寫入 | 寫入里程碑:創建、更新、刪除 |
| wiki_read | Wiki | 讀取 | 讀取 Wiki:列出頁面、取得內容、修訂歷史 |
| wiki_write | Wiki | 寫入 | 寫入 Wiki 頁面:創建、更新、刪除 |
| timetracking_read | 時間追蹤 | 讀取 | 讀取時間追蹤:問題/倉庫耗時、活動計時器、我的追蹤記錄 |
| timetracking_write | 時間追蹤 | 寫入 | 寫入時間追蹤:計時器和記錄項目 |
| package_read | 軟體套件 | 讀取 | 讀取軟體套件註冊表:列出套件、列出版本或取得某個版本 |
| package_write | 軟體套件 | 寫入 | 刪除軟體套件版本(不可復原) |
| list_issues | 問題 | 讀取 | 列出倉庫問題 |
| attachment_read | 問題 | 讀取 | 讀取問題/評論附件:列出中繼資料、取得中繼資料或下載內容 |
| issue_read | 問題 | 讀取 | 讀取問題:詳情、評論或標籤 |
| issue_write | 問題 | 寫入 | 寫入問題:創建、更新、管理評論和標籤 |
| list_pull_requests | 拉取請求 | 讀取 | 列出倉庫拉取請求 |
| pull_request_read | 拉取請求 | 讀取 | 讀取拉取請求:詳情、差異、變更檔案、頭部提交狀態、審查 |
| pull_request_write | 拉取請求 | 寫入 | 寫入拉取請求:創建、更新、關閉、重新開啟、合併、更新分支、管理審查者 |
| pull_request_review_write | 拉取請求 | 寫入 | 寫入 PR 審查:創建、提交、刪除、駁回 |
| actions_config_read | Actions | 讀取 | 讀取 Actions 密鑰和變數 |
| actions_config_write | Actions | 寫入 | 寫入 Actions 密鑰和變數:更新插入、創建、更新、刪除 |
| actions_run_read | Actions | 讀取 | 讀取 Actions 工作流程、執行、作業、日誌和產物 |
| actions_run_write | Actions | 寫入 | 寫入 Actions 執行:觸發、取消、重新執行 |
| create_repo | 倉庫 | 寫入 | 創建新倉庫 |
| fork_repo | 倉庫 | 寫入 | 復刻倉庫 |
| list_my_repos | 倉庫 | 讀取 | 列出目前用戶擁有的倉庫 |
| list_org_repos | 倉庫 | 讀取 | 列出組織中的倉庫 |
| get_repository_tree | 倉庫 | 讀取 | 取得倉庫檔案樹 |
| get_file_contents | 文件 | 讀取 | 取得檔案內容與中繼資料 |
| get_dir_contents | 文件 | 讀取 | 取得目錄中的項目 |
| create_or_update_file | 文件 | 寫入 | 創建或更新檔案(提供 sha 以更新現有檔案) |
| delete_file | 文件 | 寫入 | 刪除檔案 |
| create_branch | 分支 | 寫入 | 創建新分支 |
| delete_branch | 分支 | 寫入 | 刪除分支 |
| list_branches | 分支 | 讀取 | 列出倉庫分支 |
| create_tag | Git 標籤 | 寫入 | 創建標籤 |
| delete_tag | Git 標籤 | 寫入 | 刪除標籤 |
| get_tag | Git 標籤 | 讀取 | 取得標籤詳情 |
| list_tags | Git 標籤 | 讀取 | 列出倉庫標籤 |
| list_commits | 提交 | 讀取 | 列出倉庫提交 |
| get_commit | 提交 | 讀取 | 取得提交詳情 |
| create_release | 版本發布 | 寫入 | 創建版本發布 |
| delete_release | 版本發布 | 寫入 | 刪除版本發布 |
| get_release | 版本發布 | 讀取 | 依 ID 取得版本發布 |
| get_latest_release | 版本發布 | 讀取 | 取得最新版本發布 |
| list_releases | 版本發布 | 讀取 | 列出倉庫版本發布 |
| :--------------------------- | :----------- | :- | :--------------------------------- |
| get_gitea_mcp_server_version | version | 讀取 | 取得 Gitea MCP 伺服器版本 |
| get_me | user | 讀取 | 取得目前已認證用戶 |
| get_user_orgs | user | 讀取 | 列出目前用戶的組織 |
| search_users | search | 讀取 | 搜尋用戶 |
| search_org_teams | search | 讀取 | 搜尋組織中的團隊 |
| search_repos | search | 讀取 | 搜尋倉庫 |
| search_issues | search | 讀取 | 跨倉庫搜尋問題和拉取請求 |
| notification_read | notification | 讀取 | 讀取通知:列出(可限定倉庫)或依 ID 取得會話 |
| notification_write | notification | 寫入 | 將某條或全部通知標記為已讀 |
| label_read | label | 讀取 | 讀取倉庫或組織標籤 |
| label_write | label | 寫入 | 寫入標籤(倉庫或組織):創建、編輯、刪除 |
| milestone_read | milestone | 讀取 | 讀取里程碑:取得單個或列出 |
| milestone_write | milestone | 寫入 | 寫入里程碑:創建、更新、刪除 |
| wiki_read | wiki | 讀取 | 讀取 Wiki:列出頁面、取得內容、修訂歷史 |
| wiki_write | wiki | 寫入 | 寫入 Wiki 頁面:創建、更新、刪除 |
| timetracking_read | timetracking | 讀取 | 讀取時間追蹤:問題/倉庫耗時、活動計時器、我的追蹤記錄 |
| timetracking_write | timetracking | 寫入 | 寫入時間追蹤:計時器和記錄項目 |
| package_read | packages | 讀取 | 讀取軟體套件註冊表:列出套件、列出版本或取得某個版本 |
| package_write | packages | 寫入 | 刪除軟體套件版本(不可復原) |
| list_issues | issue | 讀取 | 列出倉庫問題 |
| attachment_read | issue | 讀取 | 讀取問題/評論附件:列出中繼資料、取得中繼資料或下載內容 |
| issue_read | issue | 讀取 | 讀取問題:詳情、評論或標籤 |
| issue_write | issue | 寫入 | 寫入問題:創建、更新、管理評論和標籤 |
| list_pull_requests | pull_request | 讀取 | 列出倉庫拉取請求 |
| pull_request_read | pull_request | 讀取 | 讀取拉取請求:詳情、差異、變更檔案、頭部提交狀態、審查 |
| pull_request_write | pull_request | 寫入 | 寫入拉取請求:創建、更新、關閉、重新開啟、合併、更新分支、管理審查者 |
| pull_request_review_write | pull_request | 寫入 | 寫入 PR 審查:創建、提交、刪除、駁回 |
| actions_config_read | actions | 讀取 | 讀取 Actions 密鑰和變數 |
| actions_config_write | actions | 寫入 | 寫入 Actions 密鑰和變數:更新插入、創建、更新、刪除 |
| actions_run_read | actions | 讀取 | 讀取 Actions 工作流程、執行、作業、日誌和產物 |
| actions_run_write | actions | 寫入 | 寫入 Actions 執行:觸發、取消、重新執行 |
| create_repo | repository | 寫入 | 創建新倉庫 |
| fork_repo | repository | 寫入 | 復刻倉庫 |
| list_my_repos | repository | 讀取 | 列出目前用戶擁有的倉庫 |
| list_org_repos | repository | 讀取 | 列出組織中的倉庫 |
| get_repository_tree | repository | 讀取 | 取得倉庫檔案樹 |
| get_file_contents | file | 讀取 | 取得檔案內容與中繼資料 |
| get_dir_contents | file | 讀取 | 取得目錄中的項目 |
| create_or_update_file | file | 寫入 | 創建或更新檔案(提供 sha 以更新現有檔案) |
| delete_file | file | 寫入 | 刪除檔案 |
| create_branch | branch | 寫入 | 創建新分支 |
| delete_branch | branch | 寫入 | 刪除分支 |
| list_branches | branch | 讀取 | 列出倉庫分支 |
| create_tag | tag | 寫入 | 創建標籤 |
| delete_tag | tag | 寫入 | 刪除標籤 |
| get_tag | tag | 讀取 | 取得標籤詳情 |
| list_tags | tag | 讀取 | 列出倉庫標籤 |
| list_commits | commit | 讀取 | 列出倉庫提交 |
| get_commit | commit | 讀取 | 取得提交詳情 |
| create_release | release | 寫入 | 創建版本發布 |
| delete_release | release | 寫入 | 刪除版本發布 |
| get_release | release | 讀取 | 依 ID 取得版本發布 |
| get_latest_release | release | 讀取 | 取得最新版本發布 |
| list_releases | release | 讀取 | 列出倉庫版本發布 |
> **說明:** 部分工具是聚合的、基於操作的工具,單個工具透過 `method` 參數暴露多個操作。當伺服器以唯讀模式執行時(`-r` / `GITEA_READONLY`),存取為「寫入」的工具會被隱藏;可透過 `-O` / `--tools``GITEA_TOOLS`)過濾對外暴露的工具集合。
> **說明:** 部分工具是聚合的、基於操作的工具,單個工具透過 `method` 參數暴露多個操作。當伺服器以唯讀模式執行時(`-r` / `GITEA_READONLY`),存取為「寫入」的工具會被隱藏;可透過 `-S` / `--scope``GITEA_SCOPES`)依範圍過濾,或透過 `-O` / `--tools``GITEA_TOOLS`依工具名稱過濾對外暴露的工具集合。
未設定任一參數時,會載入所有工具;僅設定 `--scope` 時,會載入這些範圍內的所有工具;僅設定 `--tools` 時,只會載入指定名稱的工具;兩者皆設定時,會載入所選範圍的工具與指定工具名稱的聯集。範圍名稱即上表「範圍」欄中的值,未知的範圍名稱僅會在啟動時發出警告並被忽略。
```bash
gitea-mcp -S issue,pull_request
gitea-mcp --scope repository,branch --tools get_me
```
許多工具支援 `page``per_page` 分頁參數。最大有效頁面大小由 Gitea 伺服器的 `[api].MAX_RESPONSE_ITEMS` 設定決定(預設 **50**),超出的值會被靜默截斷。
+28 -4
View File
@@ -20,16 +20,13 @@ var (
port int
token string
tools string
scopes string
version bool
maxInlineAttachmentBytes int
maxInlineAttachmentBytesFlagSet bool
osExit = os.Exit
)
func init() {
initFlagSet(flag.CommandLine, os.Args[1:], os.Getenv, os.ReadFile, os.Stderr)
}
func initFlagSet(fs *flag.FlagSet, args []string, getenv func(string) string, readFile func(string) ([]byte, error), stderr io.Writer) {
fs.StringVar(&flagPkg.Mode, "t", "stdio", "")
fs.StringVar(&flagPkg.Mode, "transport", "stdio", "")
@@ -44,6 +41,9 @@ func initFlagSet(fs *flag.FlagSet, args []string, getenv func(string) string, re
defaultTools := getenv("GITEA_TOOLS")
fs.StringVar(&tools, "O", defaultTools, "")
fs.StringVar(&tools, "tools", defaultTools, "")
defaultScopes := getenv("GITEA_SCOPES")
fs.StringVar(&scopes, "S", defaultScopes, "")
fs.StringVar(&scopes, "scope", defaultScopes, "")
fs.BoolVar(&flagPkg.Debug, "d", false, "")
fs.BoolVar(&flagPkg.Debug, "debug", false, "")
fs.BoolVar(&flagPkg.Insecure, "k", false, "")
@@ -72,6 +72,7 @@ func initFlagSet(fs *flag.FlagSet, args []string, getenv func(string) string, re
fmt.Fprintf(w, " -T, -token <token>\tPersonal access token\n")
fmt.Fprintf(w, " -r, -read-only\tExpose only read-only tools\n")
fmt.Fprintf(w, " -O, -tools <names>\tComma-separated list of tool names to expose\n")
fmt.Fprintf(w, " -S, -scope <names>\tComma-separated list of tool scopes to expose\n")
fmt.Fprintf(w, " -d, -debug\tEnable debug mode\n")
fmt.Fprintf(w, " -k, -insecure\tIgnore TLS certificate errors\n")
fmt.Fprintf(w, " -max-inline-attachment-bytes <bytes>\tInline image attachments up to this size (default: 5242880)\n")
@@ -85,6 +86,7 @@ func initFlagSet(fs *flag.FlagSet, args []string, getenv func(string) string, re
fmt.Fprintf(w, " GITEA_INSECURE\tSet to 'true' to ignore TLS errors\n")
fmt.Fprintf(w, " GITEA_MAX_INLINE_ATTACHMENT_BYTES\tOverride inline image attachment size limit in bytes\n")
fmt.Fprintf(w, " GITEA_READONLY\tSet to 'true' for read-only mode\n")
fmt.Fprintf(w, " GITEA_SCOPES\tComma-separated list of tool scopes to expose\n")
fmt.Fprintf(w, " GITEA_TOOLS\tComma-separated list of tool names to expose\n")
fmt.Fprintf(w, " MCP_MODE\tOverride transport mode\n")
_ = w.Flush()
@@ -131,6 +133,16 @@ func initFlagSet(fs *flag.FlagSet, args []string, getenv func(string) string, re
if len(allowed) > 0 {
flagPkg.AllowedTools = allowed
}
allowedScopes := map[string]struct{}{}
for s := range strings.SplitSeq(scopes, ",") {
if s = normalizeScope(s); s != "" {
allowedScopes[s] = struct{}{}
}
}
if len(allowedScopes) > 0 {
flagPkg.AllowedScopes = allowedScopes
}
if getenv("GITEA_DEBUG") == "true" {
flagPkg.Debug = true
}
@@ -149,7 +161,19 @@ func initFlagSet(fs *flag.FlagSet, args []string, getenv func(string) string, re
}
}
// normalizeScope trims whitespace, lowercases, and converts internal spaces
// and hyphens to underscores, so "Pull Request", "pull-request", and
// "PULL_REQUEST" all normalize to "pull_request".
func normalizeScope(s string) string {
s = strings.TrimSpace(s)
s = strings.ToLower(s)
s = strings.ReplaceAll(s, " ", "_")
s = strings.ReplaceAll(s, "-", "_")
return s
}
func Execute() {
initFlagSet(flag.CommandLine, os.Args[1:], os.Getenv, os.ReadFile, os.Stderr)
if version {
fmt.Fprintln(os.Stdout, flagPkg.Version)
return
+76
View File
@@ -0,0 +1,76 @@
package cmd
import (
"bytes"
"flag"
"maps"
"slices"
"testing"
flagPkg "gitea.com/gitea/gitea-mcp/pkg/flag"
)
func TestInitFlagSetScopes(t *testing.T) {
tests := []struct {
name string
args []string
env map[string]string
want []string
}{
{
name: "no scope flag or env leaves AllowedScopes unset",
args: []string{},
want: nil,
},
{
name: "-S sets a single scope",
args: []string{"-S", "repository"},
want: []string{"repository"},
},
{
name: "-scope sets a comma-separated list",
args: []string{"-scope", "repository,file"},
want: []string{"file", "repository"},
},
{
name: "GITEA_SCOPES env sets the default",
args: []string{},
env: map[string]string{"GITEA_SCOPES": "issue,pull_request"},
want: []string{"issue", "pull_request"},
},
{
name: "-S flag takes precedence over GITEA_SCOPES env",
args: []string{"-S", "file"},
env: map[string]string{"GITEA_SCOPES": "issue"},
want: []string{"file"},
},
{
name: "normalizes case, whitespace, and hyphens/spaces to underscores",
args: []string{"-S", " Pull Request , pull-request , PULL_REQUEST "},
want: []string{"pull_request"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
origScopes := flagPkg.AllowedScopes
t.Cleanup(func() {
flagPkg.AllowedScopes = origScopes
})
flagPkg.AllowedScopes = nil
getenv := func(key string) string { return tt.env[key] }
readFile := func(string) ([]byte, error) { return nil, nil }
fs := flag.NewFlagSet("test", flag.ContinueOnError)
var stderr bytes.Buffer
initFlagSet(fs, tt.args, getenv, readFile, &stderr)
got := slices.Sorted(maps.Keys(flagPkg.AllowedScopes))
want := slices.Clone(tt.want)
slices.Sort(want)
if !slices.Equal(got, want) {
t.Errorf("AllowedScopes = %v, want %v", got, want)
}
})
}
}
+1 -1
View File
@@ -5,4 +5,4 @@ import (
)
// Tool is the registry for all Actions-related MCP tools.
var Tool = tool.New()
var Tool = tool.New("actions")
+1 -1
View File
@@ -29,7 +29,7 @@ type commentWithAssets struct {
Assets []*gitea_sdk.Attachment `json:"assets"`
}
var Tool = tool.New()
var Tool = tool.New("issue")
const (
ListRepoIssuesToolName = "list_issues"
+1 -1
View File
@@ -16,7 +16,7 @@ import (
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
var Tool = tool.New("label")
const (
LabelReadToolName = "label_read"
+1 -1
View File
@@ -15,7 +15,7 @@ import (
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
var Tool = tool.New("milestone")
const (
MilestoneReadToolName = "milestone_read"
+1 -1
View File
@@ -16,7 +16,7 @@ import (
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
var Tool = tool.New("notification")
const (
NotificationReadToolName = "notification_read"
+2
View File
@@ -39,6 +39,7 @@ var (
user.Tool, actions.Tool, repo.Tool, notification.Tool, issue.Tool,
label.Tool, milestone.Tool, packages.Tool, pull.Tool, search.Tool,
version.Tool, wiki.Tool, timetracking.Tool,
repo.FileTool, repo.BranchTool, repo.TagTool, repo.CommitTool, repo.ReleaseTool,
}
)
@@ -47,6 +48,7 @@ func RegisterTool(s *server.MCPServer) {
s.AddTools(t.Tools()...)
}
tool.WarnUnmatchedAllowedTools(domainTools...)
tool.WarnUnmatchedAllowedScopes(domainTools...)
}
// parseAuthToken extracts the token from an Authorization header.
+20
View File
@@ -32,6 +32,26 @@ func TestAllToolsHaveDescriptions(t *testing.T) {
}
}
// TestDomainToolsScopesAreUniqueAndNonEmpty ensures every entry registered in
// domainTools has a canonical, non-empty scope name and that no two domains
// share the same scope (each domain.Tools() call is filtered by exactly one
// scope name via flag.AllowedScopes).
func TestDomainToolsScopesAreUniqueAndNonEmpty(t *testing.T) {
seen := map[string]struct{}{}
for _, d := range domainTools {
scope := d.Scope()
if scope == "" {
t.Errorf("domainTools contains a domain with an empty scope")
continue
}
if _, ok := seen[scope]; ok {
t.Errorf("domainTools contains a duplicate scope %q", scope)
continue
}
seen[scope] = struct{}{}
}
}
func TestParseAuthToken(t *testing.T) {
tests := []struct {
name string
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
var Tool = tool.New("packages")
const (
PackageReadToolName = "package_read"
+1 -1
View File
@@ -19,7 +19,7 @@ import (
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
var Tool = tool.New("pull_request")
const (
ListRepoPullRequestsToolName = "list_pull_requests"
+24 -11
View File
@@ -11,9 +11,9 @@ import (
)
// toolTableRow matches a row of the "Available Tools" table in the README
// files, capturing the tool name and the access cell, e.g.
// "| get_me | User | Read | Get the current authenticated user |".
var toolTableRow = regexp.MustCompile(`^\|\s*([a-z_]+)\s*\|[^|]*\|\s*(\S+)\s*\|`)
// files, capturing the tool name, the scope cell and the access cell, e.g.
// "| get_me | user | Read | Get the current authenticated user |".
var toolTableRow = regexp.MustCompile(`^\|\s*([a-z_]+)\s*\|\s*([a-z_]+)\s*\|\s*(\S+)\s*\|`)
// readmeAccessLabels maps each README to the access-column labels it uses.
var readmeAccessLabels = map[string]map[string]string{
@@ -22,18 +22,28 @@ var readmeAccessLabels = map[string]map[string]string{
"../README.zh-tw.md": {"讀取": "read", "寫入": "write"},
}
// toolInfo is what TestReadmeToolTables tracks per tool, both as registered
// in code and as documented in a README, so the two can be compared.
type toolInfo struct {
scope string
access string
}
// TestReadmeToolTables ensures the tool tables in the README files stay in sync
// with the registered tools, in both directions and for every translation.
// The tables listed tools that no longer existed for several releases before
// anyone noticed.
// The scope names in the README are the canonical, lowercase snake_case names
// returned by (*tool.Tool).Scope(), so no translation is needed to compare them.
func TestReadmeToolTables(t *testing.T) {
registered := map[string]string{}
registered := map[string]toolInfo{}
for _, d := range domainTools {
scope := d.Scope()
for _, st := range d.ReadTools() {
registered[st.Tool.Name] = "read"
registered[st.Tool.Name] = toolInfo{scope: scope, access: "read"}
}
for _, st := range d.WriteTools() {
registered[st.Tool.Name] = "write"
registered[st.Tool.Name] = toolInfo{scope: scope, access: "write"}
}
}
@@ -43,20 +53,23 @@ func TestReadmeToolTables(t *testing.T) {
if err != nil {
t.Fatal(err)
}
documented := map[string]string{}
documented := map[string]toolInfo{}
for line := range strings.SplitSeq(string(content), "\n") {
if match := toolTableRow.FindStringSubmatch(line); match != nil {
documented[match[1]] = labels[match[2]]
documented[match[1]] = toolInfo{scope: match[2], access: labels[match[3]]}
}
}
for _, name := range slices.Sorted(maps.Keys(registered)) {
access, ok := documented[name]
got, ok := documented[name]
want := registered[name]
switch {
case !ok:
t.Errorf("tool %q is registered but missing from the tool table", name)
case access != registered[name]:
t.Errorf("tool %q is documented with %q access, want %q", name, access, registered[name])
case got.access != want.access:
t.Errorf("tool %q is documented with %q access, want %q", name, got.access, want.access)
case got.scope != want.scope:
t.Errorf("tool %q is documented with scope %q, want %q", name, got.scope, want.scope)
}
}
for _, name := range slices.Sorted(maps.Keys(documented)) {
+7 -3
View File
@@ -8,12 +8,16 @@ import (
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool"
gitea_sdk "gitea.dev/sdk"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
// BranchTool holds the branch-related tools (scope "branch").
var BranchTool = tool.New("branch")
const (
CreateBranchToolName = "create_branch"
DeleteBranchToolName = "delete_branch"
@@ -52,15 +56,15 @@ var (
)
func init() {
Tool.RegisterWrite(server.ServerTool{
BranchTool.RegisterWrite(server.ServerTool{
Tool: CreateBranchTool,
Handler: CreateBranchFn,
})
Tool.RegisterWrite(server.ServerTool{
BranchTool.RegisterWrite(server.ServerTool{
Tool: DeleteBranchTool,
Handler: DeleteBranchFn,
})
Tool.RegisterRead(server.ServerTool{
BranchTool.RegisterRead(server.ServerTool{
Tool: ListBranchesTool,
Handler: ListBranchesFn,
})
+6 -2
View File
@@ -8,12 +8,16 @@ import (
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool"
gitea_sdk "gitea.dev/sdk"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
// CommitTool holds the commit-related tools (scope "commit").
var CommitTool = tool.New("commit")
const (
ListRepoCommitsToolName = "list_commits"
GetCommitToolName = "get_commit"
@@ -43,11 +47,11 @@ var (
)
func init() {
Tool.RegisterRead(server.ServerTool{
CommitTool.RegisterRead(server.ServerTool{
Tool: ListRepoCommitsTool,
Handler: ListRepoCommitsFn,
})
Tool.RegisterRead(server.ServerTool{
CommitTool.RegisterRead(server.ServerTool{
Tool: GetCommitTool,
Handler: GetCommitFn,
})
+8 -4
View File
@@ -12,12 +12,16 @@ import (
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool"
gitea_sdk "gitea.dev/sdk"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
// FileTool holds the file-related tools (scope "file").
var FileTool = tool.New("file")
const (
GetFileToolName = "get_file_contents"
GetDirToolName = "get_dir_contents"
@@ -75,19 +79,19 @@ var (
)
func init() {
Tool.RegisterRead(server.ServerTool{
FileTool.RegisterRead(server.ServerTool{
Tool: GetFileContentTool,
Handler: GetFileContentFn,
})
Tool.RegisterRead(server.ServerTool{
FileTool.RegisterRead(server.ServerTool{
Tool: GetDirContentTool,
Handler: GetDirContentFn,
})
Tool.RegisterWrite(server.ServerTool{
FileTool.RegisterWrite(server.ServerTool{
Tool: CreateOrUpdateFileTool,
Handler: CreateOrUpdateFileFn,
})
Tool.RegisterWrite(server.ServerTool{
FileTool.RegisterWrite(server.ServerTool{
Tool: DeleteFileTool,
Handler: DeleteFileFn,
})
+9 -5
View File
@@ -8,12 +8,16 @@ import (
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool"
gitea_sdk "gitea.dev/sdk"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
// ReleaseTool holds the release-related tools (scope "release").
var ReleaseTool = tool.New("release")
const (
CreateReleaseToolName = "create_release"
DeleteReleaseToolName = "delete_release"
@@ -77,23 +81,23 @@ var (
)
func init() {
Tool.RegisterWrite(server.ServerTool{
ReleaseTool.RegisterWrite(server.ServerTool{
Tool: CreateReleaseTool,
Handler: CreateReleaseFn,
})
Tool.RegisterWrite(server.ServerTool{
ReleaseTool.RegisterWrite(server.ServerTool{
Tool: DeleteReleaseTool,
Handler: DeleteReleaseFn,
})
Tool.RegisterRead(server.ServerTool{
ReleaseTool.RegisterRead(server.ServerTool{
Tool: GetReleaseTool,
Handler: GetReleaseFn,
})
Tool.RegisterRead(server.ServerTool{
ReleaseTool.RegisterRead(server.ServerTool{
Tool: GetLatestReleaseTool,
Handler: GetLatestReleaseFn,
})
Tool.RegisterRead(server.ServerTool{
ReleaseTool.RegisterRead(server.ServerTool{
Tool: ListReleasesTool,
Handler: ListReleasesFn,
})
+1 -1
View File
@@ -16,7 +16,7 @@ import (
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
var Tool = tool.New("repository")
const (
CreateRepoToolName = "create_repo"
+8 -4
View File
@@ -8,12 +8,16 @@ import (
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool"
gitea_sdk "gitea.dev/sdk"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
// TagTool holds the tag-related tools (scope "tag").
var TagTool = tool.New("tag")
const (
CreateTagToolName = "create_tag"
DeleteTagToolName = "delete_tag"
@@ -63,19 +67,19 @@ var (
)
func init() {
Tool.RegisterWrite(server.ServerTool{
TagTool.RegisterWrite(server.ServerTool{
Tool: CreateTagTool,
Handler: CreateTagFn,
})
Tool.RegisterWrite(server.ServerTool{
TagTool.RegisterWrite(server.ServerTool{
Tool: DeleteTagTool,
Handler: DeleteTagFn,
})
Tool.RegisterRead(server.ServerTool{
TagTool.RegisterRead(server.ServerTool{
Tool: GetTagTool,
Handler: GetTagFn,
})
Tool.RegisterRead(server.ServerTool{
TagTool.RegisterRead(server.ServerTool{
Tool: ListTagsTool,
Handler: ListTagsFn,
})
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
var Tool = tool.New("search")
const (
SearchUsersToolName = "search_users"
+1 -1
View File
@@ -16,7 +16,7 @@ import (
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
var Tool = tool.New("timetracking")
const (
TimetrackingReadToolName = "timetracking_read"
+1 -1
View File
@@ -21,7 +21,7 @@ const (
GetUserOrgsToolName = "get_user_orgs"
)
var Tool = tool.New()
var Tool = tool.New("user")
var (
GetMyUserInfoTool = mcp.NewTool(
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
var Tool = tool.New("version")
const (
GetGiteaMCPServerVersion = "get_gitea_mcp_server_version"
+1 -1
View File
@@ -16,7 +16,7 @@ import (
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
var Tool = tool.New("wiki")
const (
WikiReadToolName = "wiki_read"
+1
View File
@@ -13,4 +13,5 @@ var (
ReadOnly bool
Debug bool
AllowedTools map[string]struct{}
AllowedScopes map[string]struct{}
)
+57 -16
View File
@@ -11,17 +11,24 @@ import (
)
type Tool struct {
scope string
write []server.ServerTool
read []server.ServerTool
}
func New() *Tool {
func New(scope string) *Tool {
return &Tool{
scope: scope,
write: make([]server.ServerTool, 0, 100),
read: make([]server.ServerTool, 0, 100),
}
}
// Scope returns the canonical scope name this domain of tools was registered under.
func (t *Tool) Scope() string {
return t.scope
}
func (t *Tool) RegisterWrite(s server.ServerTool) {
t.write = append(t.write, s)
}
@@ -42,31 +49,55 @@ func (t *Tool) WriteTools() []server.ServerTool {
return t.write
}
// Tools returns the tools registered on this domain after applying the
// read-only filter and the scope/tool allowlists (union semantics: a tool is
// kept if its domain's scope is in AllowedScopes OR its name is in
// AllowedTools). With no allowlists set, all tools pass through unchanged.
func (t *Tool) Tools() []server.ServerTool {
all := make([]server.ServerTool, 0, len(t.write)+len(t.read))
if !flag.ReadOnly {
all = append(all, t.write...)
}
all = append(all, t.read...)
if len(flag.AllowedTools) == 0 {
if len(flag.AllowedScopes) == 0 && len(flag.AllowedTools) == 0 {
return all
}
_, scopeAllowed := flag.AllowedScopes[t.scope]
filtered := make([]server.ServerTool, 0, len(all))
for _, st := range all {
if _, ok := flag.AllowedTools[st.Tool.Name]; ok {
_, toolAllowed := flag.AllowedTools[st.Tool.Name]
if scopeAllowed || toolAllowed {
filtered = append(filtered, st)
}
}
return filtered
}
// warnUnmatched logs the names present in allowlist but absent from known,
// via logUnmatched, so WarnUnmatchedAllowedTools and WarnUnmatchedAllowedScopes
// share the same "collect, sort, no-op when empty" logic and can't drift.
// No-op if allowlist is empty or every name in it is known.
func warnUnmatched(allowlist, known map[string]struct{}, logUnmatched func(unmatched []string)) {
if len(allowlist) == 0 {
return
}
var unmatched []string
for name := range allowlist {
if _, ok := known[name]; !ok {
unmatched = append(unmatched, name)
}
}
if len(unmatched) == 0 {
return
}
slices.Sort(unmatched)
logUnmatched(unmatched)
}
// WarnUnmatchedAllowedTools logs any names in flag.AllowedTools that don't
// match a tool registered on any of the given domains. No-op if the allowlist
// is empty.
func WarnUnmatchedAllowedTools(domains ...*Tool) {
if len(flag.AllowedTools) == 0 {
return
}
known := map[string]struct{}{}
for _, d := range domains {
for _, st := range d.read {
@@ -76,15 +107,25 @@ func WarnUnmatchedAllowedTools(domains ...*Tool) {
known[st.Tool.Name] = struct{}{}
}
}
var unmatched []string
for name := range flag.AllowedTools {
if _, ok := known[name]; !ok {
unmatched = append(unmatched, name)
}
}
if len(unmatched) == 0 {
return
}
slices.Sort(unmatched)
warnUnmatched(flag.AllowedTools, known, func(unmatched []string) {
log.Warnf("Unknown tools in --tools allowlist (ignored): %s", strings.Join(unmatched, ", "))
})
}
// WarnUnmatchedAllowedScopes logs any names in flag.AllowedScopes that don't
// match the scope of any of the given domains. No-op if the allowlist is
// empty.
func WarnUnmatchedAllowedScopes(domains ...*Tool) {
knownSet := map[string]struct{}{}
known := make([]string, 0, len(domains))
for _, d := range domains {
if _, ok := knownSet[d.scope]; !ok {
knownSet[d.scope] = struct{}{}
known = append(known, d.scope)
}
}
warnUnmatched(flag.AllowedScopes, knownSet, func(unmatched []string) {
slices.Sort(known)
log.Warnf("Unknown scopes in --scope allowlist (ignored): %s. Valid scopes: %s", strings.Join(unmatched, ", "), strings.Join(known, ", "))
})
}
+128 -1
View File
@@ -83,7 +83,7 @@ func TestTools(t *testing.T) {
flag.ReadOnly = tt.readOnly
flag.AllowedTools = tt.allowed
tr := New()
tr := New("scope1")
for _, n := range tt.read {
tr.RegisterRead(makeTool(n))
}
@@ -98,3 +98,130 @@ func TestTools(t *testing.T) {
})
}
}
func TestToolsScopeFiltering(t *testing.T) {
tests := []struct {
name string
toolScope string
readOnly bool
allowedScopes map[string]struct{}
allowedTools map[string]struct{}
read []string
write []string
want []string
}{
{
name: "no scope or tool filters returns all",
toolScope: "repository",
read: []string{"r1"},
write: []string{"w1"},
want: []string{"w1", "r1"},
},
{
name: "matching scope keeps everything",
toolScope: "repository",
allowedScopes: map[string]struct{}{"repository": {}},
read: []string{"r1"},
write: []string{"w1"},
want: []string{"w1", "r1"},
},
{
name: "non-matching scope drops everything not in allowed tools",
toolScope: "repository",
allowedScopes: map[string]struct{}{"file": {}},
read: []string{"r1"},
write: []string{"w1"},
want: []string{},
},
{
name: "tools-only allowlist behaves as before scopes existed",
toolScope: "repository",
allowedTools: map[string]struct{}{"r1": {}},
read: []string{"r1", "r2"},
write: []string{"w1"},
want: []string{"r1"},
},
{
name: "scope and tools allowlists are unioned",
toolScope: "repository",
allowedScopes: map[string]struct{}{"file": {}},
allowedTools: map[string]struct{}{"r1": {}},
read: []string{"r1", "r2"},
write: []string{"w1"},
want: []string{"r1"},
},
{
name: "matching scope combined with read-only drops write entries",
toolScope: "repository",
readOnly: true,
allowedScopes: map[string]struct{}{"repository": {}},
read: []string{"r1"},
write: []string{"w1"},
want: []string{"r1"},
},
{
name: "unknown scope name matches nothing",
toolScope: "repository",
allowedScopes: map[string]struct{}{"unknown_scope": {}},
read: []string{"r1"},
write: []string{"w1"},
want: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
origRO, origAllowScopes, origAllowTools := flag.ReadOnly, flag.AllowedScopes, flag.AllowedTools
t.Cleanup(func() {
flag.ReadOnly, flag.AllowedScopes, flag.AllowedTools = origRO, origAllowScopes, origAllowTools
})
flag.ReadOnly = tt.readOnly
flag.AllowedScopes = tt.allowedScopes
flag.AllowedTools = tt.allowedTools
tr := New(tt.toolScope)
for _, n := range tt.read {
tr.RegisterRead(makeTool(n))
}
for _, n := range tt.write {
tr.RegisterWrite(makeTool(n))
}
got := names(tr.Tools())
if !slices.Equal(got, tt.want) {
t.Errorf("Tools() = %v, want %v", got, tt.want)
}
})
}
}
func TestScope(t *testing.T) {
tr := New("repository")
if got := tr.Scope(); got != "repository" {
t.Errorf("Scope() = %q, want %q", got, "repository")
}
}
func TestWarnUnmatchedAllowedScopes(t *testing.T) {
origAllowScopes := flag.AllowedScopes
t.Cleanup(func() {
flag.AllowedScopes = origAllowScopes
})
repoTool := New("repository")
fileTool := New("file")
t.Run("empty allowlist is a no-op", func(t *testing.T) {
flag.AllowedScopes = nil
WarnUnmatchedAllowedScopes(repoTool, fileTool)
})
t.Run("known scopes produce no warning", func(t *testing.T) {
flag.AllowedScopes = map[string]struct{}{"repository": {}, "file": {}}
WarnUnmatchedAllowedScopes(repoTool, fileTool)
})
t.Run("unknown scope is tolerated", func(t *testing.T) {
flag.AllowedScopes = map[string]struct{}{"not_a_real_scope": {}}
WarnUnmatchedAllowedScopes(repoTool, fileTool)
})
}