Files
MCP/pkg/tool/tool.go
T
Renovate Bot e2052d903f chore(deps): update dependencies (#243)
This PR contains the following updates:

| Package | Type | Update | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|---|---|
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) ([changelog](https://github.com/docker/setup-buildx-action/compare/bb05f3f5519dd87d3ba754cc423b652a5edd6d2c..37fe631027851001ddb9b187196cc803df7f5f0e)) | action | digest | `bb05f3f` → `37fe631` |  |  |
| [go](https://go.dev/) ([source](https://github.com/golang/go)) | toolchain | minor | `1.26.6` → `1.27.0` | ![age](https://developer.mend.io/api/mc/badges/age/golang-version/go/1.27.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/golang-version/go/1.26.6/1.27.0?slim=true) |
| golang.org/x/vuln |  | minor | `v1.6.0` → `v1.7.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fvuln/v1.7.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fvuln/v1.6.0/v1.7.0?slim=true) |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTEuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE5MS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/243
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
2026-08-24 17:21:20 +00:00

186 lines
5.2 KiB
Go

package tool
import (
"context"
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
"gitea.com/gitea/gitea-mcp/pkg/flag"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type Handler func(context.Context, map[string]any) (*mcp.CallToolResult, error)
type ServerTool struct {
Tool *mcp.Tool
Handler Handler
}
type Tool struct {
scope string
write []ServerTool
read []ServerTool
}
func New(scope string) *Tool {
return &Tool{
scope: scope,
write: make([]ServerTool, 0, 100),
read: make([]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 ServerTool) {
t.write = append(t.write, s)
}
func (t *Tool) RegisterRead(s ServerTool) {
t.read = append(t.read, s)
}
// ReadTools returns the read-only tools registered on this domain, ignoring
// the read-only and allowlist flags that Tools applies.
func (t *Tool) ReadTools() []ServerTool {
return t.read
}
// WriteTools returns the write tools registered on this domain, ignoring the
// read-only and allowlist flags that Tools applies.
func (t *Tool) WriteTools() []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() []ServerTool {
all := make([]ServerTool, 0, len(t.write)+len(t.read))
if !flag.ReadOnly {
all = append(all, t.write...)
}
all = append(all, t.read...)
if len(flag.AllowedScopes) == 0 && len(flag.AllowedTools) == 0 {
return all
}
_, scopeAllowed := flag.AllowedScopes[t.scope]
filtered := make([]ServerTool, 0, len(all))
for _, st := range all {
_, toolAllowed := flag.AllowedTools[st.Tool.Name]
if scopeAllowed || toolAllowed {
filtered = append(filtered, st)
}
}
return filtered
}
// MCPHandler adapts a project handler to the official SDK's low-level handler.
func (s ServerTool) MCPHandler() mcp.ToolHandler {
return func(ctx context.Context, req *mcp.CallToolRequest) (result *mcp.CallToolResult, err error) {
defer func() {
if recovered := recover(); recovered != nil {
result, err = to.ErrorResult(fmt.Errorf("panic recovered in %s tool handler: %v", s.Tool.Name, recovered))
}
}()
arguments, err := decodeArguments(req.Params.Arguments)
if err != nil {
return nil, err
}
result, err = s.Handler(ctx, arguments)
if err != nil {
if _, ok := errors.AsType[*jsonrpc.Error](err); ok {
return nil, err
}
return to.ErrorResult(err)
}
return result, nil
}
}
func decodeArguments(raw json.RawMessage) (map[string]any, error) {
// An omitted and a null "arguments" both mean the tool was called without any.
if len(raw) == 0 || string(raw) == "null" {
return map[string]any{}, nil
}
var arguments map[string]any
if err := json.Unmarshal(raw, &arguments); err != nil {
return nil, &jsonrpc.Error{
Code: jsonrpc.CodeInvalidParams,
Message: fmt.Sprintf("invalid tool arguments: %v", err),
}
}
return arguments, nil
}
// 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) {
known := map[string]struct{}{}
for _, d := range domains {
for _, st := range d.read {
known[st.Tool.Name] = struct{}{}
}
for _, st := range d.write {
known[st.Tool.Name] = struct{}{}
}
}
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, ", "))
})
}