mirror of
https://gitea.com/gitea/gitea-mcp.git
synced 2026-08-03 15:49:23 +02:00
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:
@@ -5,4 +5,4 @@ import (
|
||||
)
|
||||
|
||||
// Tool is the registry for all Actions-related MCP tools.
|
||||
var Tool = tool.New()
|
||||
var Tool = tool.New("actions")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
var Tool = tool.New()
|
||||
var Tool = tool.New("label")
|
||||
|
||||
const (
|
||||
LabelReadToolName = "label_read"
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
var Tool = tool.New()
|
||||
var Tool = tool.New("milestone")
|
||||
|
||||
const (
|
||||
MilestoneReadToolName = "milestone_read"
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
var Tool = tool.New()
|
||||
var Tool = tool.New("notification")
|
||||
|
||||
const (
|
||||
NotificationReadToolName = "notification_read"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
var Tool = tool.New()
|
||||
var Tool = tool.New("packages")
|
||||
|
||||
const (
|
||||
PackageReadToolName = "package_read"
|
||||
|
||||
@@ -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
@@ -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)) {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,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,
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
var Tool = tool.New()
|
||||
var Tool = tool.New("search")
|
||||
|
||||
const (
|
||||
SearchUsersToolName = "search_users"
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
var Tool = tool.New()
|
||||
var Tool = tool.New("timetracking")
|
||||
|
||||
const (
|
||||
TimetrackingReadToolName = "timetracking_read"
|
||||
|
||||
@@ -21,7 +21,7 @@ const (
|
||||
GetUserOrgsToolName = "get_user_orgs"
|
||||
)
|
||||
|
||||
var Tool = tool.New()
|
||||
var Tool = tool.New("user")
|
||||
|
||||
var (
|
||||
GetMyUserInfoTool = mcp.NewTool(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
var Tool = tool.New()
|
||||
var Tool = tool.New("wiki")
|
||||
|
||||
const (
|
||||
WikiReadToolName = "wiki_read"
|
||||
|
||||
Reference in New Issue
Block a user