From 80c8b25d6e22088f1c2e6aa7818e50bb4f0e055e Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Sun, 2 Aug 2026 21:30:19 +0800 Subject: [PATCH] refactor!: replace mcp-go with the official MCP Go SDK - Swap the mark3labs MCP dependency for modelcontextprotocol/go-sdk v1.7.0 - Add a declarative tool definition and JSON Schema builder to the tool package - Adapt registered tools to the official low-level handler, recovering panics and mapping failures to JSON-RPC errors - Narrow tool handlers to take a plain argument map instead of an SDK request type - Rewire stdio and HTTP transports onto the official server, moving Authorization parsing into receiving middleware - Add a golden contract test that locks the exposed tool schemas, plus SDK integration and helper tests - Add a test target and run it in the pull request workflow BREAKING CHANGE: The exported helpers change signature. Tool.RegisterRead and Tool.RegisterWrite now take tool.ServerTool instead of server.ServerTool, and the annotation constructors return *mcp.ToolAnnotations from the official SDK. Callers must build tool definitions with tool.NewDefinition and handlers with the func(context.Context, map[string]any) signature. The 30-second SSE heartbeat is removed because the official SDK has no equivalent. ServerOptions.KeepAlive is deliberately not used as a substitute, since it sends MCP ping requests and ping is removed in protocol 2026. HTTP stays stateless=false, so new clients negotiate at most 2025-11-25. Co-Authored-By: Claude Opus 5 (1M context) --- .gitea/workflows/test-pr.yml | 2 + Makefile | 4 + go.mod | 12 +- go.sum | 36 +- operation/actions/artifacts.go | 42 +- operation/actions/config.go | 222 +- operation/actions/runs.go | 214 +- operation/issue/attachment.go | 80 +- operation/issue/attachment_test.go | 73 +- operation/issue/issue.go | 244 +- operation/issue/issue_test.go | 66 +- operation/label/label.go | 155 +- operation/milestone/milestone.go | 120 +- operation/milestone/milestone_test.go | 6 +- operation/notification/notification.go | 77 +- operation/operation.go | 71 +- operation/operation_test.go | 5 +- operation/packages/packages.go | 71 +- operation/packages/packages_test.go | 48 +- operation/pull/pull.go | 282 ++- operation/pull/pull_test.go | 218 +- operation/repo/branch.go | 58 +- operation/repo/commit.go | 43 +- operation/repo/file.go | 93 +- operation/repo/release.go | 102 +- operation/repo/repo.go | 95 +- operation/repo/tag.go | 77 +- operation/repo/tree.go | 27 +- operation/repo/tree_test.go | 4 +- operation/sdk_integration_test.go | 393 ++++ operation/search/search.go | 109 +- operation/search/search_test.go | 10 +- operation/search/slim_test.go | 4 +- operation/testdata/tools.golden.json | 2817 ++++++++++++++++++++++++ operation/timetracking/timetracking.go | 135 +- operation/tool_contract_test.go | 179 ++ operation/user/user.go | 29 +- operation/version/version.go | 13 +- operation/wiki/wiki.go | 79 +- operation/wiki/wiki_test.go | 7 +- pkg/annotation/annotation.go | 24 +- pkg/annotation/annotation_test.go | 20 + pkg/to/to.go | 6 +- pkg/to/to_test.go | 33 + pkg/tool/definition.go | 110 + pkg/tool/definition_test.go | 80 + pkg/tool/handler_test.go | 100 + pkg/tool/tool.go | 101 +- pkg/tool/tool_test.go | 9 +- 49 files changed, 5277 insertions(+), 1528 deletions(-) create mode 100644 operation/sdk_integration_test.go create mode 100644 operation/testdata/tools.golden.json create mode 100644 operation/tool_contract_test.go create mode 100644 pkg/annotation/annotation_test.go create mode 100644 pkg/to/to_test.go create mode 100644 pkg/tool/definition.go create mode 100644 pkg/tool/definition_test.go create mode 100644 pkg/tool/handler_test.go diff --git a/.gitea/workflows/test-pr.yml b/.gitea/workflows/test-pr.yml index d7a3d02..97bbb6d 100644 --- a/.gitea/workflows/test-pr.yml +++ b/.gitea/workflows/test-pr.yml @@ -13,6 +13,8 @@ jobs: go-version-file: 'go.mod' - name: lint run: make lint + - name: test + run: make test - name: build run: make build - name: security-check diff --git a/Makefile b/Makefile index d9e25d0..6a22af5 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,10 @@ clean: ## delete build artifacts build: ## build the application $(GO) build -v -ldflags '-s -w $(LDFLAGS)' -o $(EXECUTABLE) +.PHONY: test +test: ## run Go tests + $(GO) test ./... + .PHONY: air air: ## install air for hot reload @hash air > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ diff --git a/go.mod b/go.mod index 4d08737..163dfbf 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.0 require ( gitea.dev/sdk v1.2.0 - github.com/mark3labs/mcp-go v0.56.0 + github.com/modelcontextprotocol/go-sdk v1.7.0 go.uber.org/zap v1.28.0 go.uber.org/zap/exp v0.3.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 @@ -14,13 +14,15 @@ require ( github.com/42wim/httpsig v1.2.4 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect github.com/google/jsonschema-go v0.4.3 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-version v1.9.0 // indirect - github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect - github.com/spf13/cast v1.10.0 // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/segmentio/encoding v0.5.4 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.54.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.40.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.47.0 // indirect ) diff --git a/go.sum b/go.sum index a49e54b..648c0a0 100644 --- a/go.sum +++ b/go.sum @@ -6,32 +6,22 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mark3labs/mcp-go v0.56.0 h1:7aCj2wODCskMi08f923ADG+EfELZBdiKILny415cIS8= -github.com/mark3labs/mcp-go v0.56.0/go.mod h1:+8WclSK1ZUweCP3hvktSji8n8ABG/95QaEkeVE/Uwas= +github.com/modelcontextprotocol/go-sdk v1.7.0 h1:yqjY2dsbKAC0LSuWZVBMrHgiG8ukXv6NRo0JiALay44= +github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= @@ -50,6 +40,10 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5 golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= @@ -57,9 +51,11 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/operation/actions/artifacts.go b/operation/actions/artifacts.go index 3fb0d86..e5711e4 100644 --- a/operation/actions/artifacts.go +++ b/operation/actions/artifacts.go @@ -15,7 +15,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/params" "gitea.com/gitea/gitea-mcp/pkg/to" - "github.com/mark3labs/mcp-go/mcp" + "github.com/modelcontextprotocol/go-sdk/mcp" ) // Artifact endpoints require Gitea 1.25+. Older servers answer 404/405, which is @@ -28,21 +28,21 @@ func artifactNotSupportedErr(err error) error { return err } -func listRepoActionArtifactsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func listRepoActionArtifactsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) + page, pageSize := params.GetPagination(args, 30) query := url.Values{} query.Set("page", strconv.Itoa(page)) query.Set("limit", strconv.Itoa(pageSize)) - if name := params.GetOptionalString(req.GetArguments(), "artifact_name", ""); name != "" { + if name := params.GetOptionalString(args, "artifact_name", ""); name != "" { query.Set("name", name) } @@ -59,25 +59,25 @@ func listRepoActionArtifactsFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(slimActionArtifacts(result)) } -func listRepoActionRunArtifactsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func listRepoActionRunArtifactsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - runID, err := params.GetIndex(req.GetArguments(), "run_id") + runID, err := params.GetIndex(args, "run_id") if err != nil || runID <= 0 { return to.ErrorResult(errors.New("run_id is required")) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) + page, pageSize := params.GetPagination(args, 30) query := url.Values{} query.Set("page", strconv.Itoa(page)) query.Set("limit", strconv.Itoa(pageSize)) - if name := params.GetOptionalString(req.GetArguments(), "artifact_name", ""); name != "" { + if name := params.GetOptionalString(args, "artifact_name", ""); name != "" { query.Set("name", name) } @@ -94,16 +94,16 @@ func listRepoActionRunArtifactsFn(ctx context.Context, req mcp.CallToolRequest) return to.TextResult(slimActionArtifacts(result)) } -func getRepoActionArtifactFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func getRepoActionArtifactFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - artifactID, err := params.GetIndex(req.GetArguments(), "artifact_id") + artifactID, err := params.GetIndex(args, "artifact_id") if err != nil || artifactID <= 0 { return to.ErrorResult(errors.New("artifact_id is required")) } @@ -121,20 +121,20 @@ func getRepoActionArtifactFn(ctx context.Context, req mcp.CallToolRequest) (*mcp return to.TextResult(slimActionArtifact(result)) } -func downloadRepoActionArtifactFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func downloadRepoActionArtifactFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - artifactID, err := params.GetIndex(req.GetArguments(), "artifact_id") + artifactID, err := params.GetIndex(args, "artifact_id") if err != nil || artifactID <= 0 { return to.ErrorResult(errors.New("artifact_id is required")) } - outputPath, _ := req.GetArguments()["output_path"].(string) + outputPath, _ := args["output_path"].(string) // Best-effort metadata lookup: gives a friendly filename and lets us fail // early with a clear message when the artifact has expired. diff --git a/operation/actions/config.go b/operation/actions/config.go index 32a00c0..41f70f4 100644 --- a/operation/actions/config.go +++ b/operation/actions/config.go @@ -11,10 +11,10 @@ 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" + "github.com/modelcontextprotocol/go-sdk/mcp" ) const ( @@ -44,103 +44,103 @@ func toSecretMetas(secrets []*gitea_sdk.Secret) []secretMeta { } var ( - ActionsConfigReadTool = mcp.NewTool( + ActionsConfigReadTool = tool.NewDefinition( ActionsConfigReadToolName, - mcp.WithDescription("Read Actions secrets and variables."), - mcp.WithToolAnnotation(annotation.ReadOnly("Read Actions secrets and variables")), - mcp.WithString("method", mcp.Required(), mcp.Enum("list_repo_secrets", "list_org_secrets", "list_repo_variables", "get_repo_variable", "list_org_variables", "get_org_variable")), - mcp.WithString("owner", mcp.Description("for repo methods")), - mcp.WithString("repo", mcp.Description("for repo methods")), - mcp.WithString("org", mcp.Description("for org methods")), - mcp.WithString("name", mcp.Description("for get methods")), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30), mcp.Min(1)), + "Read Actions secrets and variables.", + annotation.ReadOnly("Read Actions secrets and variables"), + tool.String("method", tool.Required(), tool.Enum("list_repo_secrets", "list_org_secrets", "list_repo_variables", "get_repo_variable", "list_org_variables", "get_org_variable")), + tool.String("owner", tool.Description("for repo methods")), + tool.String("repo", tool.Description("for repo methods")), + tool.String("org", tool.Description("for org methods")), + tool.String("name", tool.Description("for get methods")), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1), tool.Minimum(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30), tool.Minimum(1)), ) - ActionsConfigWriteTool = mcp.NewTool( + ActionsConfigWriteTool = tool.NewDefinition( ActionsConfigWriteToolName, - mcp.WithDescription("Write Actions secrets and variables: upsert, create, update, delete."), - mcp.WithToolAnnotation(annotation.Destructive("Manage Actions secrets and variables")), - mcp.WithString("method", mcp.Required(), mcp.Enum("upsert_repo_secret", "delete_repo_secret", "upsert_org_secret", "delete_org_secret", "create_repo_variable", "update_repo_variable", "delete_repo_variable", "create_org_variable", "update_org_variable", "delete_org_variable")), - mcp.WithString("owner", mcp.Description("for repo methods")), - mcp.WithString("repo", mcp.Description("for repo methods")), - mcp.WithString("org", mcp.Description("for org methods")), - mcp.WithString("name", mcp.Description("secret or variable name")), - mcp.WithString("data", mcp.Description("secret value (upsert)")), - mcp.WithString("value", mcp.Description("variable value")), - mcp.WithString("description"), + "Write Actions secrets and variables: upsert, create, update, delete.", + annotation.Destructive("Manage Actions secrets and variables"), + tool.String("method", tool.Required(), tool.Enum("upsert_repo_secret", "delete_repo_secret", "upsert_org_secret", "delete_org_secret", "create_repo_variable", "update_repo_variable", "delete_repo_variable", "create_org_variable", "update_org_variable", "delete_org_variable")), + tool.String("owner", tool.Description("for repo methods")), + tool.String("repo", tool.Description("for repo methods")), + tool.String("org", tool.Description("for org methods")), + tool.String("name", tool.Description("secret or variable name")), + tool.String("data", tool.Description("secret value (upsert)")), + tool.String("value", tool.Description("variable value")), + tool.String("description"), ) ) func init() { - Tool.RegisterRead(server.ServerTool{Tool: ActionsConfigReadTool, Handler: configReadFn}) - Tool.RegisterWrite(server.ServerTool{Tool: ActionsConfigWriteTool, Handler: configWriteFn}) + Tool.RegisterRead(tool.ServerTool{Tool: ActionsConfigReadTool, Handler: configReadFn}) + Tool.RegisterWrite(tool.ServerTool{Tool: ActionsConfigWriteTool, Handler: configWriteFn}) } -func configReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := params.GetString(req.GetArguments(), "method") +func configReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "list_repo_secrets": - return listRepoActionSecretsFn(ctx, req) + return listRepoActionSecretsFn(ctx, args) case "list_org_secrets": - return listOrgActionSecretsFn(ctx, req) + return listOrgActionSecretsFn(ctx, args) case "list_repo_variables": - return listRepoActionVariablesFn(ctx, req) + return listRepoActionVariablesFn(ctx, args) case "get_repo_variable": - return getRepoActionVariableFn(ctx, req) + return getRepoActionVariableFn(ctx, args) case "list_org_variables": - return listOrgActionVariablesFn(ctx, req) + return listOrgActionVariablesFn(ctx, args) case "get_org_variable": - return getOrgActionVariableFn(ctx, req) + return getOrgActionVariableFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func configWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := params.GetString(req.GetArguments(), "method") +func configWriteFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "upsert_repo_secret": - return upsertRepoActionSecretFn(ctx, req) + return upsertRepoActionSecretFn(ctx, args) case "delete_repo_secret": - return deleteRepoActionSecretFn(ctx, req) + return deleteRepoActionSecretFn(ctx, args) case "upsert_org_secret": - return upsertOrgActionSecretFn(ctx, req) + return upsertOrgActionSecretFn(ctx, args) case "delete_org_secret": - return deleteOrgActionSecretFn(ctx, req) + return deleteOrgActionSecretFn(ctx, args) case "create_repo_variable": - return createRepoActionVariableFn(ctx, req) + return createRepoActionVariableFn(ctx, args) case "update_repo_variable": - return updateRepoActionVariableFn(ctx, req) + return updateRepoActionVariableFn(ctx, args) case "delete_repo_variable": - return deleteRepoActionVariableFn(ctx, req) + return deleteRepoActionVariableFn(ctx, args) case "create_org_variable": - return createOrgActionVariableFn(ctx, req) + return createOrgActionVariableFn(ctx, args) case "update_org_variable": - return updateOrgActionVariableFn(ctx, req) + return updateOrgActionVariableFn(ctx, args) case "delete_org_variable": - return deleteOrgActionVariableFn(ctx, req) + return deleteOrgActionVariableFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func listRepoActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func listRepoActionSecretsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) + page, pageSize := params.GetPagination(args, 30) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -157,24 +157,24 @@ func listRepoActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp return to.TextResult(toSecretMetas(secrets)) } -func upsertRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func upsertRepoActionSecretFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - name, err := params.GetString(req.GetArguments(), "name") + name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) } - data, err := params.GetString(req.GetArguments(), "data") + data, err := params.GetString(args, "data") if err != nil { return to.ErrorResult(err) } - description, _ := req.GetArguments()["description"].(string) + description, _ := args["description"].(string) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -190,16 +190,16 @@ func upsertRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mc return to.TextResult(map[string]any{"message": "secret upserted", "status": resp.StatusCode}) } -func deleteRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func deleteRepoActionSecretFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - name, err := params.GetString(req.GetArguments(), "name") + name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) } @@ -215,12 +215,12 @@ func deleteRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mc return to.TextResult(map[string]any{"message": "secret deleted", "status": resp.StatusCode}) } -func listOrgActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := params.GetString(req.GetArguments(), "org") +func listOrgActionSecretsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + org, err := params.GetString(args, "org") if err != nil { return to.ErrorResult(err) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) + page, pageSize := params.GetPagination(args, 30) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -237,20 +237,20 @@ func listOrgActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp. return to.TextResult(toSecretMetas(secrets)) } -func upsertOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := params.GetString(req.GetArguments(), "org") +func upsertOrgActionSecretFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + org, err := params.GetString(args, "org") if err != nil { return to.ErrorResult(err) } - name, err := params.GetString(req.GetArguments(), "name") + name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) } - data, err := params.GetString(req.GetArguments(), "data") + data, err := params.GetString(args, "data") if err != nil { return to.ErrorResult(err) } - description, _ := req.GetArguments()["description"].(string) + description, _ := args["description"].(string) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -266,12 +266,12 @@ func upsertOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp return to.TextResult(map[string]any{"message": "secret upserted", "status": resp.StatusCode}) } -func deleteOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := params.GetString(req.GetArguments(), "org") +func deleteOrgActionSecretFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + org, err := params.GetString(args, "org") if err != nil { return to.ErrorResult(err) } - name, err := params.GetString(req.GetArguments(), "name") + name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) } @@ -285,16 +285,16 @@ func deleteOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp return to.TextResult(map[string]any{"message": "secret deleted"}) } -func listRepoActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func listRepoActionVariablesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) + page, pageSize := params.GetPagination(args, 30) query := url.Values{} query.Set("page", strconv.Itoa(page)) @@ -308,16 +308,16 @@ func listRepoActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(result) } -func getRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func getRepoActionVariableFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - name, err := params.GetString(req.GetArguments(), "name") + name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) } @@ -333,20 +333,20 @@ func getRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp return to.TextResult(variable) } -func createRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func createRepoActionVariableFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - name, err := params.GetString(req.GetArguments(), "name") + name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) } - value, err := params.GetString(req.GetArguments(), "value") + value, err := params.GetString(args, "value") if err != nil { return to.ErrorResult(err) } @@ -362,20 +362,20 @@ func createRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (* return to.TextResult(map[string]any{"message": "variable created", "status": resp.StatusCode}) } -func updateRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func updateRepoActionVariableFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - name, err := params.GetString(req.GetArguments(), "name") + name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) } - value, err := params.GetString(req.GetArguments(), "value") + value, err := params.GetString(args, "value") if err != nil { return to.ErrorResult(err) } @@ -391,16 +391,16 @@ func updateRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (* return to.TextResult(map[string]any{"message": "variable updated", "status": resp.StatusCode}) } -func deleteRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func deleteRepoActionVariableFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - name, err := params.GetString(req.GetArguments(), "name") + name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) } @@ -416,12 +416,12 @@ func deleteRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (* return to.TextResult(map[string]any{"message": "variable deleted", "status": resp.StatusCode}) } -func listOrgActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := params.GetString(req.GetArguments(), "org") +func listOrgActionVariablesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + org, err := params.GetString(args, "org") if err != nil { return to.ErrorResult(err) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) + page, pageSize := params.GetPagination(args, 30) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -436,12 +436,12 @@ func listOrgActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*mc return to.TextResult(variables) } -func getOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := params.GetString(req.GetArguments(), "org") +func getOrgActionVariableFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + org, err := params.GetString(args, "org") if err != nil { return to.ErrorResult(err) } - name, err := params.GetString(req.GetArguments(), "name") + name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) } @@ -457,20 +457,20 @@ func getOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp. return to.TextResult(variable) } -func createOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := params.GetString(req.GetArguments(), "org") +func createOrgActionVariableFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + org, err := params.GetString(args, "org") if err != nil { return to.ErrorResult(err) } - name, err := params.GetString(req.GetArguments(), "name") + name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) } - value, err := params.GetString(req.GetArguments(), "value") + value, err := params.GetString(args, "value") if err != nil { return to.ErrorResult(err) } - description, _ := req.GetArguments()["description"].(string) + description, _ := args["description"].(string) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -486,20 +486,20 @@ func createOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(map[string]any{"message": "variable created", "status": resp.StatusCode}) } -func updateOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := params.GetString(req.GetArguments(), "org") +func updateOrgActionVariableFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + org, err := params.GetString(args, "org") if err != nil { return to.ErrorResult(err) } - name, err := params.GetString(req.GetArguments(), "name") + name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) } - value, err := params.GetString(req.GetArguments(), "value") + value, err := params.GetString(args, "value") if err != nil { return to.ErrorResult(err) } - description, _ := req.GetArguments()["description"].(string) + description, _ := args["description"].(string) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -516,12 +516,12 @@ func updateOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(map[string]any{"message": "variable updated", "status": resp.StatusCode}) } -func deleteOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := params.GetString(req.GetArguments(), "org") +func deleteOrgActionVariableFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + org, err := params.GetString(args, "org") if err != nil { return to.ErrorResult(err) } - name, err := params.GetString(req.GetArguments(), "name") + name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) } diff --git a/operation/actions/runs.go b/operation/actions/runs.go index 98332ae..539f782 100644 --- a/operation/actions/runs.go +++ b/operation/actions/runs.go @@ -14,9 +14,9 @@ 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" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) const ( @@ -25,94 +25,94 @@ const ( ) var ( - ActionsRunReadTool = mcp.NewTool( + ActionsRunReadTool = tool.NewDefinition( ActionsRunReadToolName, - mcp.WithDescription("Read Actions workflows, runs, jobs, logs, and artifacts."), - mcp.WithToolAnnotation(annotation.ReadOnly("Read Actions workflow, run, job, and artifact data")), - mcp.WithString("method", mcp.Required(), mcp.Enum("list_workflows", "get_workflow", "list_runs", "get_run", "list_jobs", "list_run_jobs", "get_job", "get_job_log_preview", "download_job_log", "list_artifacts", "list_run_artifacts", "get_artifact", "download_artifact")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("workflow_id", mcp.Description("ID or filename (for 'get_workflow')")), - mcp.WithNumber("run_id", mcp.Description("for 'get_run'/'list_run_jobs'/'list_run_artifacts'")), - mcp.WithNumber("job_id", mcp.Description("for 'get_job'/log methods")), - mcp.WithNumber("artifact_id", mcp.Description("for 'get_artifact'/'download_artifact'")), - mcp.WithString("artifact_name", mcp.Description("name filter for 'list_artifacts'/'list_run_artifacts'")), - mcp.WithString("status", mcp.Description("filter for 'list_runs'/'list_jobs'")), - mcp.WithNumber("tail_lines", mcp.Description("log tail lines"), mcp.DefaultNumber(200), mcp.Min(1)), - mcp.WithNumber("max_bytes", mcp.Description("max log bytes"), mcp.DefaultNumber(65536), mcp.Min(1024)), - mcp.WithString("output_path", mcp.Description("for 'download_job_log'/'download_artifact'")), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30), mcp.Min(1)), + "Read Actions workflows, runs, jobs, logs, and artifacts.", + annotation.ReadOnly("Read Actions workflow, run, job, and artifact data"), + tool.String("method", tool.Required(), tool.Enum("list_workflows", "get_workflow", "list_runs", "get_run", "list_jobs", "list_run_jobs", "get_job", "get_job_log_preview", "download_job_log", "list_artifacts", "list_run_artifacts", "get_artifact", "download_artifact")), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("workflow_id", tool.Description("ID or filename (for 'get_workflow')")), + tool.Number("run_id", tool.Description("for 'get_run'/'list_run_jobs'/'list_run_artifacts'")), + tool.Number("job_id", tool.Description("for 'get_job'/log methods")), + tool.Number("artifact_id", tool.Description("for 'get_artifact'/'download_artifact'")), + tool.String("artifact_name", tool.Description("name filter for 'list_artifacts'/'list_run_artifacts'")), + tool.String("status", tool.Description("filter for 'list_runs'/'list_jobs'")), + tool.Number("tail_lines", tool.Description("log tail lines"), tool.Default(200), tool.Minimum(1)), + tool.Number("max_bytes", tool.Description("max log bytes"), tool.Default(65536), tool.Minimum(1024)), + tool.String("output_path", tool.Description("for 'download_job_log'/'download_artifact'")), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1), tool.Minimum(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30), tool.Minimum(1)), ) - ActionsRunWriteTool = mcp.NewTool( + ActionsRunWriteTool = tool.NewDefinition( ActionsRunWriteToolName, - mcp.WithDescription("Write Actions runs: dispatch, cancel, rerun."), - mcp.WithToolAnnotation(annotation.Write("Trigger, cancel, or rerun Actions workflows")), - mcp.WithString("method", mcp.Required(), mcp.Enum("dispatch_workflow", "cancel_run", "rerun_run")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("workflow_id", mcp.Description("ID or filename (for 'dispatch_workflow')")), - mcp.WithString("ref", mcp.Description("branch or tag (for 'dispatch_workflow')")), - mcp.WithObject("inputs", mcp.Description("for 'dispatch_workflow'")), - mcp.WithNumber("run_id", mcp.Description("for 'cancel_run'/'rerun_run'")), + "Write Actions runs: dispatch, cancel, rerun.", + annotation.Write("Trigger, cancel, or rerun Actions workflows"), + tool.String("method", tool.Required(), tool.Enum("dispatch_workflow", "cancel_run", "rerun_run")), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("workflow_id", tool.Description("ID or filename (for 'dispatch_workflow')")), + tool.String("ref", tool.Description("branch or tag (for 'dispatch_workflow')")), + tool.Object("inputs", tool.Description("for 'dispatch_workflow'")), + tool.Number("run_id", tool.Description("for 'cancel_run'/'rerun_run'")), ) ) func init() { - Tool.RegisterRead(server.ServerTool{Tool: ActionsRunReadTool, Handler: runReadFn}) - Tool.RegisterWrite(server.ServerTool{Tool: ActionsRunWriteTool, Handler: runWriteFn}) + Tool.RegisterRead(tool.ServerTool{Tool: ActionsRunReadTool, Handler: runReadFn}) + Tool.RegisterWrite(tool.ServerTool{Tool: ActionsRunWriteTool, Handler: runWriteFn}) } -func runReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := params.GetString(req.GetArguments(), "method") +func runReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "list_workflows": - return listRepoActionWorkflowsFn(ctx, req) + return listRepoActionWorkflowsFn(ctx, args) case "get_workflow": - return getRepoActionWorkflowFn(ctx, req) + return getRepoActionWorkflowFn(ctx, args) case "list_runs": - return listRepoActionRunsFn(ctx, req) + return listRepoActionRunsFn(ctx, args) case "get_run": - return getRepoActionRunFn(ctx, req) + return getRepoActionRunFn(ctx, args) case "list_jobs": - return listRepoActionJobsFn(ctx, req) + return listRepoActionJobsFn(ctx, args) case "list_run_jobs": - return listRepoActionRunJobsFn(ctx, req) + return listRepoActionRunJobsFn(ctx, args) case "get_job": - return getRepoActionJobFn(ctx, req) + return getRepoActionJobFn(ctx, args) case "get_job_log_preview": - return getRepoActionJobLogPreviewFn(ctx, req) + return getRepoActionJobLogPreviewFn(ctx, args) case "download_job_log": - return downloadRepoActionJobLogFn(ctx, req) + return downloadRepoActionJobLogFn(ctx, args) case "list_artifacts": - return listRepoActionArtifactsFn(ctx, req) + return listRepoActionArtifactsFn(ctx, args) case "list_run_artifacts": - return listRepoActionRunArtifactsFn(ctx, req) + return listRepoActionRunArtifactsFn(ctx, args) case "get_artifact": - return getRepoActionArtifactFn(ctx, req) + return getRepoActionArtifactFn(ctx, args) case "download_artifact": - return downloadRepoActionArtifactFn(ctx, req) + return downloadRepoActionArtifactFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func runWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := params.GetString(req.GetArguments(), "method") +func runWriteFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "dispatch_workflow": - return dispatchRepoActionWorkflowFn(ctx, req) + return dispatchRepoActionWorkflowFn(ctx, args) case "cancel_run": - return cancelRepoActionRunFn(ctx, req) + return cancelRepoActionRunFn(ctx, args) case "rerun_run": - return rerunRepoActionRunFn(ctx, req) + return rerunRepoActionRunFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } @@ -135,16 +135,16 @@ func doJSONWithFallback(ctx context.Context, method string, paths []string, quer return lastErr } -func listRepoActionWorkflowsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func listRepoActionWorkflowsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) + page, pageSize := params.GetPagination(args, 30) query := url.Values{} query.Set("page", strconv.Itoa(page)) query.Set("limit", strconv.Itoa(pageSize)) @@ -162,16 +162,16 @@ func listRepoActionWorkflowsFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(slimActionWorkflows(result)) } -func getRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func getRepoActionWorkflowFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - workflowID, err := params.GetString(req.GetArguments(), "workflow_id") + workflowID, err := params.GetString(args, "workflow_id") if err != nil { return to.ErrorResult(err) } @@ -189,26 +189,26 @@ func getRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp return to.TextResult(slimActionWorkflow(result)) } -func dispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func dispatchRepoActionWorkflowFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - workflowID, err := params.GetString(req.GetArguments(), "workflow_id") + workflowID, err := params.GetString(args, "workflow_id") if err != nil { return to.ErrorResult(err) } - ref, err := params.GetString(req.GetArguments(), "ref") + ref, err := params.GetString(args, "ref") if err != nil { return to.ErrorResult(err) } var inputs map[string]any - if raw, exists := req.GetArguments()["inputs"]; exists { + if raw, exists := args["inputs"]; exists { if m, ok := raw.(map[string]any); ok { inputs = m } @@ -238,17 +238,17 @@ func dispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) return to.TextResult(map[string]any{"message": "workflow dispatched"}) } -func listRepoActionRunsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func listRepoActionRunsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) - statusFilter, _ := req.GetArguments()["status"].(string) + page, pageSize := params.GetPagination(args, 30) + statusFilter, _ := args["status"].(string) query := url.Values{} query.Set("page", strconv.Itoa(page)) @@ -270,16 +270,16 @@ func listRepoActionRunsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca return to.TextResult(slimActionRuns(result)) } -func getRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func getRepoActionRunFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - runID, err := params.GetIndex(req.GetArguments(), "run_id") + runID, err := params.GetIndex(args, "run_id") if err != nil || runID <= 0 { return to.ErrorResult(errors.New("run_id is required")) } @@ -297,16 +297,16 @@ func getRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.TextResult(slimActionRun(result)) } -func cancelRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func cancelRepoActionRunFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - runID, err := params.GetIndex(req.GetArguments(), "run_id") + runID, err := params.GetIndex(args, "run_id") if err != nil || runID <= 0 { return to.ErrorResult(errors.New("run_id is required")) } @@ -323,16 +323,16 @@ func cancelRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.C return to.TextResult(map[string]any{"message": "run cancellation requested"}) } -func rerunRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func rerunRepoActionRunFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - runID, err := params.GetIndex(req.GetArguments(), "run_id") + runID, err := params.GetIndex(args, "run_id") if err != nil || runID <= 0 { return to.ErrorResult(errors.New("run_id is required")) } @@ -354,17 +354,17 @@ func rerunRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca return to.TextResult(map[string]any{"message": "run rerun requested"}) } -func listRepoActionJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func listRepoActionJobsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) - statusFilter, _ := req.GetArguments()["status"].(string) + page, pageSize := params.GetPagination(args, 30) + statusFilter, _ := args["status"].(string) query := url.Values{} query.Set("page", strconv.Itoa(page)) @@ -386,20 +386,20 @@ func listRepoActionJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca return to.TextResult(slimActionJobs(result)) } -func listRepoActionRunJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func listRepoActionRunJobsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - runID, err := params.GetIndex(req.GetArguments(), "run_id") + runID, err := params.GetIndex(args, "run_id") if err != nil || runID <= 0 { return to.ErrorResult(errors.New("run_id is required")) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) + page, pageSize := params.GetPagination(args, 30) query := url.Values{} query.Set("page", strconv.Itoa(page)) @@ -418,16 +418,16 @@ func listRepoActionRunJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp return to.TextResult(slimActionJobs(result)) } -func getRepoActionJobFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func getRepoActionJobFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - jobID, err := params.GetIndex(req.GetArguments(), "job_id") + jobID, err := params.GetIndex(args, "job_id") if err != nil || jobID <= 0 { return to.ErrorResult(errors.New("job_id is required")) } @@ -503,21 +503,21 @@ func limitBytes(data []byte, maxBytes int) ([]byte, bool) { return data[len(data)-maxBytes:], true } -func getRepoActionJobLogPreviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func getRepoActionJobLogPreviewFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - jobID, err := params.GetIndex(req.GetArguments(), "job_id") + jobID, err := params.GetIndex(args, "job_id") if err != nil { return to.ErrorResult(err) } - tailLines := int(params.GetOptionalInt(req.GetArguments(), "tail_lines", 200)) - maxBytes := int(params.GetOptionalInt(req.GetArguments(), "max_bytes", 65536)) + tailLines := int(params.GetOptionalInt(args, "tail_lines", 200)) + maxBytes := int(params.GetOptionalInt(args, "max_bytes", 65536)) raw, usedPath, err := fetchJobLogBytes(ctx, owner, repo, jobID) if err != nil { return to.ErrorResult(fmt.Errorf("get job log err: %v", err)) @@ -537,20 +537,20 @@ func getRepoActionJobLogPreviewFn(ctx context.Context, req mcp.CallToolRequest) }) } -func downloadRepoActionJobLogFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func downloadRepoActionJobLogFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - jobID, err := params.GetIndex(req.GetArguments(), "job_id") + jobID, err := params.GetIndex(args, "job_id") if err != nil { return to.ErrorResult(err) } - outputPath, _ := req.GetArguments()["output_path"].(string) + outputPath, _ := args["output_path"].(string) raw, usedPath, err := fetchJobLogBytes(ctx, owner, repo, jobID) if err != nil { diff --git a/operation/issue/attachment.go b/operation/issue/attachment.go index b610158..1275389 100644 --- a/operation/issue/attachment.go +++ b/operation/issue/attachment.go @@ -3,7 +3,6 @@ package issue import ( "bytes" "context" - "encoding/base64" "errors" "fmt" "io" @@ -17,51 +16,51 @@ 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" + "github.com/modelcontextprotocol/go-sdk/mcp" ) const AttachmentReadToolName = "attachment_read" -var AttachmentReadTool = mcp.NewTool( +var AttachmentReadTool = tool.NewDefinition( AttachmentReadToolName, - mcp.WithDescription("Read issue/comment attachments: list metadata, get metadata, or download content."), - mcp.WithToolAnnotation(annotation.ReadOnly("Read issue or comment attachments")), - mcp.WithString("method", mcp.Required(), mcp.Enum("list", "get", "download")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithNumber("issue_number", mcp.Description("required for issue attachment list/get or issue-scoped metadata lookup")), - mcp.WithNumber("comment_id", mcp.Description("required for comment attachment list/get or comment-scoped metadata lookup")), - mcp.WithNumber("attachment_id", mcp.Description("required for get and for download when attachment_uuid is not provided")), - mcp.WithString("attachment_uuid", mcp.Description("attachment UUID for direct download path lookup")), - mcp.WithString("output_path", mcp.Description("write the attachment to this exact path")), + "Read issue/comment attachments: list metadata, get metadata, or download content.", + annotation.ReadOnly("Read issue or comment attachments"), + tool.String("method", tool.Required(), tool.Enum("list", "get", "download")), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.Number("issue_number", tool.Description("required for issue attachment list/get or issue-scoped metadata lookup")), + tool.Number("comment_id", tool.Description("required for comment attachment list/get or comment-scoped metadata lookup")), + tool.Number("attachment_id", tool.Description("required for get and for download when attachment_uuid is not provided")), + tool.String("attachment_uuid", tool.Description("attachment UUID for direct download path lookup")), + tool.String("output_path", tool.Description("write the attachment to this exact path")), ) func init() { - Tool.RegisterRead(server.ServerTool{Tool: AttachmentReadTool, Handler: attachmentReadFn}) + Tool.RegisterRead(tool.ServerTool{Tool: AttachmentReadTool, Handler: attachmentReadFn}) } -func attachmentReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := params.GetString(req.GetArguments(), "method") +func attachmentReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "list": - return listAttachmentsFn(ctx, req) + return listAttachmentsFn(ctx, args) case "get": - return getAttachmentFn(ctx, req) + return getAttachmentFn(ctx, args) case "download": - return downloadAttachmentFn(ctx, req) + return downloadAttachmentFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func listAttachmentsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, repo, issueNumber, commentID, err := attachmentScopeArgs(req) +func listAttachmentsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, repo, issueNumber, commentID, err := attachmentScopeArgs(args) if err != nil { return to.ErrorResult(err) } @@ -81,29 +80,29 @@ func listAttachmentsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.TextResult(slimAttachments(attachments)) } -func getAttachmentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - att, err := lookupAttachment(ctx, req) +func getAttachmentFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + att, err := lookupAttachment(ctx, args) if err != nil { return to.ErrorResult(err) } return to.TextResult(slimAttachment(att)) } -func downloadAttachmentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func downloadAttachmentFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - explicitOutputPath := params.GetOptionalString(req.GetArguments(), "output_path", "") - attachmentUUID := strings.TrimSpace(params.GetOptionalString(req.GetArguments(), "attachment_uuid", "")) + explicitOutputPath := params.GetOptionalString(args, "output_path", "") + attachmentUUID := strings.TrimSpace(params.GetOptionalString(args, "attachment_uuid", "")) var att *gitea_sdk.Attachment if attachmentUUID == "" { - att, err = lookupAttachment(ctx, req) + att, err = lookupAttachment(ctx, args) if err != nil { return to.ErrorResult(err) } @@ -132,7 +131,10 @@ func downloadAttachmentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca } if len(limited) <= flag.MaxInlineAttachmentBytes { text := fmt.Sprintf("attachment %s (%s, %d bytes, %s)", name, attachmentUUID, len(limited), mimeType) - return mcp.NewToolResultImage(text, base64.StdEncoding.EncodeToString(limited), mimeType), nil + return &mcp.CallToolResult{Content: []mcp.Content{ + &mcp.TextContent{Text: text}, + &mcp.ImageContent{Data: limited, MIMEType: mimeType}, + }}, nil } outputPath := defaultAttachmentPath(owner, repo, name, attachmentUUID) if err := os.MkdirAll(filepath.Dir(outputPath), 0o700); err != nil { @@ -185,29 +187,29 @@ func attachmentFileResult(att *gitea_sdk.Attachment, outputPath string, written return to.TextResult(res) } -func attachmentScopeArgs(req mcp.CallToolRequest) (owner, repo string, issueNumber, commentID int64, err error) { - owner, err = params.GetString(req.GetArguments(), "owner") +func attachmentScopeArgs(args map[string]any) (owner, repo string, issueNumber, commentID int64, err error) { + owner, err = params.GetString(args, "owner") if err != nil { return "", "", 0, 0, err } - repo, err = params.GetString(req.GetArguments(), "repo") + repo, err = params.GetString(args, "repo") if err != nil { return "", "", 0, 0, err } - issueNumber = params.GetOptionalInt(req.GetArguments(), "issue_number", 0) - commentID = params.GetOptionalInt(req.GetArguments(), "comment_id", 0) + issueNumber = params.GetOptionalInt(args, "issue_number", 0) + commentID = params.GetOptionalInt(args, "comment_id", 0) if (issueNumber > 0) == (commentID > 0) { return "", "", 0, 0, errors.New("exactly one of issue_number or comment_id is required") } return owner, repo, issueNumber, commentID, nil } -func lookupAttachment(ctx context.Context, req mcp.CallToolRequest) (*gitea_sdk.Attachment, error) { - owner, repo, issueNumber, commentID, err := attachmentScopeArgs(req) +func lookupAttachment(ctx context.Context, args map[string]any) (*gitea_sdk.Attachment, error) { + owner, repo, issueNumber, commentID, err := attachmentScopeArgs(args) if err != nil { return nil, err } - attachmentID := params.GetOptionalInt(req.GetArguments(), "attachment_id", 0) + attachmentID := params.GetOptionalInt(args, "attachment_id", 0) if attachmentID <= 0 { return nil, errors.New("attachment_id is required") } diff --git a/operation/issue/attachment_test.go b/operation/issue/attachment_test.go index 5c143ce..8f924e7 100644 --- a/operation/issue/attachment_test.go +++ b/operation/issue/attachment_test.go @@ -1,7 +1,9 @@ package issue import ( + "bytes" "context" + "encoding/base64" "encoding/json" "fmt" "net/http" @@ -14,7 +16,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/flag" "gitea.com/gitea/gitea-mcp/pkg/gitea" - "github.com/mark3labs/mcp-go/mcp" + "github.com/modelcontextprotocol/go-sdk/mcp" ) func TestAttachmentFilename(t *testing.T) { @@ -79,13 +81,13 @@ func TestAttachmentReadListIssueAttachments(t *testing.T) { flag.Host, flag.Token, flag.Version = server.URL, "", "test" defer func() { flag.Host, flag.Token, flag.Version = origHost, origToken, origVersion }() - res, err := attachmentReadFn(context.Background(), mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: map[string]any{ + res, err := attachmentReadFn(context.Background(), map[string]any{ "method": "list", "owner": owner, "repo": repo, "issue_number": float64(42), - }}}) + }) if err != nil { t.Fatalf("attachmentReadFn() error = %v", err) } - body := res.Content[0].(mcp.TextContent).Text + body := res.Content[0].(*mcp.TextContent).Text if !strings.Contains(body, `"mime_type":"image/png"`) || !strings.Contains(body, `"uuid":"uuid-1"`) { t.Fatalf("unexpected body: %s", body) } @@ -158,13 +160,13 @@ func TestAttachmentReadDownloadSavesLargeAttachmentToDefaultFile(t *testing.T) { flag.Host, flag.Token, flag.Version, flag.MaxInlineAttachmentBytes = origHost, origToken, origVersion, origInline }() - res, err := attachmentReadFn(context.Background(), mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: map[string]any{ + res, err := attachmentReadFn(context.Background(), map[string]any{ "method": "download", "owner": owner, "repo": repo, "issue_number": float64(42), "attachment_id": float64(1), - }}}) + }) if err != nil { t.Fatalf("attachmentReadFn() error = %v", err) } - body := res.Content[0].(mcp.TextContent).Text + body := res.Content[0].(*mcp.TextContent).Text wantPath := filepath.Join(home, ".gitea-mcp", "attachments", owner, repo, "large-uuid-1.bin") if !strings.Contains(body, wantPath) { t.Fatalf("result missing path %q: %s", wantPath, body) @@ -180,3 +182,60 @@ func TestAttachmentReadDownloadSavesLargeAttachmentToDefaultFile(t *testing.T) { t.Fatalf("result missing bytes: %s", body) } } + +func TestAttachmentReadDownloadReturnsRawImageContent(t *testing.T) { + const uuid = "uuid-1" + payload := []byte{0, 1, 2, 250} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/attachments/"+uuid { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "image/png") + _, _ = w.Write(payload) + })) + defer server.Close() + + originalHost := flag.Host + originalLimit := flag.MaxInlineAttachmentBytes + flag.Host = server.URL + flag.MaxInlineAttachmentBytes = len(payload) + defer func() { + flag.Host = originalHost + flag.MaxInlineAttachmentBytes = originalLimit + }() + + result, err := attachmentReadFn(context.Background(), map[string]any{ + "method": "download", + "owner": "octo", + "repo": "demo", + "attachment_uuid": uuid, + }) + if err != nil { + t.Fatalf("attachmentReadFn() error = %v", err) + } + if len(result.Content) != 2 { + t.Fatalf("content count = %d, want 2", len(result.Content)) + } + if _, ok := result.Content[0].(*mcp.TextContent); !ok { + t.Fatalf("first content type = %T, want *mcp.TextContent", result.Content[0]) + } + image, ok := result.Content[1].(*mcp.ImageContent) + if !ok { + t.Fatalf("second content type = %T, want *mcp.ImageContent", result.Content[1]) + } + if image.MIMEType != "image/png" { + t.Errorf("image MIME type = %q, want image/png", image.MIMEType) + } + if !bytes.Equal(image.Data, payload) { + t.Errorf("image data = %v, want raw payload %v", image.Data, payload) + } + wire, err := json.Marshal(image) + if err != nil { + t.Fatalf("json.Marshal() error = %v", err) + } + wantBase64 := base64.StdEncoding.EncodeToString(payload) + if !strings.Contains(string(wire), `"data":"`+wantBase64+`"`) { + t.Errorf("wire image = %s, want base64 data %q", wire, wantBase64) + } +} diff --git a/operation/issue/issue.go b/operation/issue/issue.go index ef4a731..e3703bc 100644 --- a/operation/issue/issue.go +++ b/operation/issue/issue.go @@ -13,8 +13,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) // issueWithAssets / commentWithAssets wrap the SDK types to capture the @@ -38,125 +37,123 @@ const ( ) var ( - ListRepoIssuesTool = mcp.NewTool( + ListRepoIssuesTool = tool.NewDefinition( ListRepoIssuesToolName, - mcp.WithDescription("List issues in a repository (or pull requests, via the 'type' filter), filterable by state, labels, milestones, and update time range."), - mcp.WithToolAnnotation(annotation.ReadOnly("List repository issues")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("state", mcp.DefaultString("all")), - mcp.WithString("type", mcp.Description("issues or pulls"), mcp.Enum("issues", "pulls")), - mcp.WithArray("labels", mcp.Description("label name filter"), mcp.Items(map[string]any{"type": "string"})), - mcp.WithArray("milestones", mcp.Description("milestone name or ID filter"), mcp.Items(map[string]any{"type": "string"})), - mcp.WithString("since", mcp.Description("updated after ISO 8601")), - mcp.WithString("before", mcp.Description("updated before ISO 8601")), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), + "List issues in a repository (or pull requests, via the 'type' filter), filterable by state, labels, milestones, and update time range.", + annotation.ReadOnly("List repository issues"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("state", tool.Default("all")), + tool.String("type", tool.Description("issues or pulls"), tool.Enum("issues", "pulls")), + tool.Array("labels", tool.Description("label name filter"), tool.Items(map[string]any{"type": "string"})), + tool.Array("milestones", tool.Description("milestone name or ID filter"), tool.Items(map[string]any{"type": "string"})), + tool.String("since", tool.Description("updated after ISO 8601")), + tool.String("before", tool.Description("updated before ISO 8601")), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) - IssueReadTool = mcp.NewTool( + IssueReadTool = tool.NewDefinition( IssueReadToolName, - mcp.WithDescription("Read issue: details, comments, or labels."), - mcp.WithToolAnnotation(annotation.ReadOnly("Read issue details")), - mcp.WithString("method", mcp.Required(), mcp.Enum("get", "get_comments", "get_labels")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithNumber("issue_number", mcp.Required()), + "Read issue: details, comments, or labels.", + annotation.ReadOnly("Read issue details"), + tool.String("method", tool.Required(), tool.Enum("get", "get_comments", "get_labels")), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.Number("issue_number", tool.Required()), ) - IssueWriteTool = mcp.NewTool( + IssueWriteTool = tool.NewDefinition( IssueWriteToolName, - mcp.WithDescription("Write issues: create, update, manage comments and labels."), - mcp.WithToolAnnotation(annotation.Write("Create or update issues, comments, and labels")), - mcp.WithString("method", mcp.Required(), mcp.Enum("create", "update", "add_comment", "edit_comment", "add_labels", "remove_label", "replace_labels", "clear_labels")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithNumber("issue_number", mcp.Description("required except for 'create'")), - mcp.WithString("title", mcp.Description("required for 'create'")), - mcp.WithString("body", mcp.Description("required for 'create'/'add_comment'/'edit_comment'")), - mcp.WithArray("assignees", mcp.Items(map[string]any{"type": "string"})), - mcp.WithNumber("milestone"), - mcp.WithString("state", mcp.Enum("open", "closed", "all")), - mcp.WithNumber("commentID", mcp.Description("for 'edit_comment'")), - mcp.WithArray("labels", mcp.Description("label IDs"), mcp.Items(map[string]any{"type": "number"})), - mcp.WithNumber("label_id", mcp.Description("for 'remove_label'")), - mcp.WithString("ref", mcp.Description("branch to associate")), - mcp.WithString("deadline", mcp.Description("ISO 8601")), - mcp.WithBoolean("remove_deadline"), + "Write issues: create, update, manage comments and labels.", + annotation.Write("Create or update issues, comments, and labels"), + tool.String("method", tool.Required(), tool.Enum("create", "update", "add_comment", "edit_comment", "add_labels", "remove_label", "replace_labels", "clear_labels")), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.Number("issue_number", tool.Description("required except for 'create'")), + tool.String("title", tool.Description("required for 'create'")), + tool.String("body", tool.Description("required for 'create'/'add_comment'/'edit_comment'")), + tool.Array("assignees", tool.Items(map[string]any{"type": "string"})), + tool.Number("milestone"), + tool.String("state", tool.Enum("open", "closed", "all")), + tool.Number("commentID", tool.Description("for 'edit_comment'")), + tool.Array("labels", tool.Description("label IDs"), tool.Items(map[string]any{"type": "number"})), + tool.Number("label_id", tool.Description("for 'remove_label'")), + tool.String("ref", tool.Description("branch to associate")), + tool.String("deadline", tool.Description("ISO 8601")), + tool.Boolean("remove_deadline"), ) ) func init() { - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: ListRepoIssuesTool, Handler: listRepoIssuesFn, }) - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: IssueReadTool, Handler: issueReadFn, }) - Tool.RegisterWrite(server.ServerTool{ + Tool.RegisterWrite(tool.ServerTool{ Tool: IssueWriteTool, Handler: issueWriteFn, }) } -func issueReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func issueReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "get": - return getIssueByIndexFn(ctx, req) + return getIssueByIndexFn(ctx, args) case "get_comments": - return getIssueCommentsByIndexFn(ctx, req) + return getIssueCommentsByIndexFn(ctx, args) case "get_labels": - return getIssueLabelsFn(ctx, req) + return getIssueLabelsFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func issueWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func issueWriteFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "create": - return createIssueFn(ctx, req) + return createIssueFn(ctx, args) case "update": - return editIssueFn(ctx, req) + return editIssueFn(ctx, args) case "add_comment": - return createIssueCommentFn(ctx, req) + return createIssueCommentFn(ctx, args) case "edit_comment": - return editIssueCommentFn(ctx, req) + return editIssueCommentFn(ctx, args) case "add_labels": - return addIssueLabelsFn(ctx, req) + return addIssueLabelsFn(ctx, args) case "remove_label": - return removeIssueLabelFn(ctx, req) + return removeIssueLabelFn(ctx, args) case "replace_labels": - return replaceIssueLabelsFn(ctx, req) + return replaceIssueLabelsFn(ctx, args) case "clear_labels": - return clearIssueLabelsFn(ctx, req) + return clearIssueLabelsFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func getIssueByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func getIssueByIndexFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } @@ -170,22 +167,22 @@ func getIssueByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.TextResult(m) } -func listRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func listRepoIssuesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - state, ok := req.GetArguments()["state"].(string) + state, ok := args["state"].(string) if !ok { state = "all" } - labels := params.GetStringSlice(req.GetArguments(), "labels") - milestones := params.GetStringSlice(req.GetArguments(), "milestones") - page, pageSize := params.GetPagination(req.GetArguments(), 30) + labels := params.GetStringSlice(args, "labels") + milestones := params.GetStringSlice(args, "milestones") + page, pageSize := params.GetPagination(args, 30) opt := gitea_sdk.ListIssueOption{ State: gitea_sdk.StateType(state), Labels: labels, @@ -195,16 +192,16 @@ func listRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo PageSize: pageSize, }, } - switch req.GetArguments()["type"] { + switch args["type"] { case "issues": opt.Type = gitea_sdk.IssueTypeIssue case "pulls": opt.Type = gitea_sdk.IssueTypePull } - if t := params.GetOptionalTime(req.GetArguments(), "since"); t != nil { + if t := params.GetOptionalTime(args, "since"); t != nil { opt.Since = *t } - if t := params.GetOptionalTime(req.GetArguments(), "before"); t != nil { + if t := params.GetOptionalTime(args, "before"); t != nil { opt.Before = *t } client, err := gitea.ClientFromContext(ctx) @@ -218,20 +215,20 @@ func listRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(slimIssues(issues)) } -func createIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func createIssueFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - title, err := params.GetString(req.GetArguments(), "title") + title, err := params.GetString(args, "title") if err != nil { return to.ErrorResult(err) } - body, err := params.GetString(req.GetArguments(), "body") + body, err := params.GetString(args, "body") if err != nil { return to.ErrorResult(err) } @@ -243,19 +240,19 @@ func createIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR Title: title, Body: body, } - opt.Assignees = params.GetStringSlice(req.GetArguments(), "assignees") - if val, exists := req.GetArguments()["milestone"]; exists { + opt.Assignees = params.GetStringSlice(args, "assignees") + if val, exists := args["milestone"]; exists { if milestone, ok := params.ToInt64(val); ok { opt.Milestone = milestone } } - if labelIDs, err := params.GetInt64Slice(req.GetArguments(), "labels"); err == nil { + if labelIDs, err := params.GetInt64Slice(args, "labels"); err == nil { opt.Labels = labelIDs } - if ref, ok := req.GetArguments()["ref"].(string); ok { + if ref, ok := args["ref"].(string); ok { opt.Ref = ref } - opt.Deadline = params.GetOptionalTime(req.GetArguments(), "deadline") + opt.Deadline = params.GetOptionalTime(args, "deadline") issue, _, err := client.Issues.CreateIssue(ctx, owner, repo, opt) if err != nil { return to.ErrorResult(fmt.Errorf("create %v/%v/issue err: %v", owner, repo, err)) @@ -264,20 +261,20 @@ func createIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR return to.TextResult(slimIssue(issue)) } -func createIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func createIssueCommentFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } - body, err := params.GetString(req.GetArguments(), "body") + body, err := params.GetString(args, "body") if err != nil { return to.ErrorResult(err) } @@ -296,21 +293,20 @@ func createIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca return to.TextResult(slimComment(issueComment)) } -func editIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func editIssueFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } - args := req.GetArguments() opt := gitea_sdk.EditIssueOption{ Body: params.GetPresentStringPtr(args, "body"), Ref: params.GetPresentStringPtr(args, "ref"), @@ -343,20 +339,20 @@ func editIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes return to.TextResult(slimIssue(issue)) } -func editIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func editIssueCommentFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - commentID, err := params.GetIndex(req.GetArguments(), "commentID") + commentID, err := params.GetIndex(args, "commentID") if err != nil { return to.ErrorResult(err) } - body, err := params.GetString(req.GetArguments(), "body") + body, err := params.GetString(args, "body") if err != nil { return to.ErrorResult(err) } @@ -375,16 +371,16 @@ func editIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.TextResult(slimComment(issueComment)) } -func getIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func getIssueCommentsByIndexFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } @@ -402,16 +398,16 @@ func getIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(out) } -func getIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func getIssueLabelsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } @@ -427,20 +423,20 @@ func getIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(slim.Labels(labels)) } -func addIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func addIssueLabelsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } - labels, err := params.GetInt64Slice(req.GetArguments(), "labels") + labels, err := params.GetInt64Slice(args, "labels") if err != nil { return to.ErrorResult(err) } @@ -456,20 +452,20 @@ func addIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(slim.Labels(issueLabels)) } -func replaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func replaceIssueLabelsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } - labels, err := params.GetInt64Slice(req.GetArguments(), "labels") + labels, err := params.GetInt64Slice(args, "labels") if err != nil { return to.ErrorResult(err) } @@ -485,16 +481,16 @@ func replaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca return to.TextResult(slim.Labels(issueLabels)) } -func clearIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func clearIssueLabelsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } @@ -510,20 +506,20 @@ func clearIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.TextResult("Labels cleared successfully") } -func removeIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func removeIssueLabelFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } - labelID, err := params.GetIndex(req.GetArguments(), "label_id") + labelID, err := params.GetIndex(args, "label_id") if err != nil { return to.ErrorResult(err) } diff --git a/operation/issue/issue_test.go b/operation/issue/issue_test.go index 767235d..f65f0a9 100644 --- a/operation/issue/issue_test.go +++ b/operation/issue/issue_test.go @@ -12,7 +12,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/flag" - "github.com/mark3labs/mcp-go/mcp" + "github.com/modelcontextprotocol/go-sdk/mcp" ) func Test_listRepoIssuesFn_filters(t *testing.T) { @@ -60,20 +60,16 @@ func Test_listRepoIssuesFn_filters(t *testing.T) { flag.Version = origVersion }() - req := mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Arguments: map[string]any{ - "owner": owner, - "repo": repo, - "type": "issues", - "labels": []any{"bug", "enhancement"}, - "milestones": []any{"v1.0", "2"}, - "since": "2026-01-01T00:00:00Z", - }, - }, + args := map[string]any{ + "owner": owner, + "repo": repo, + "type": "issues", + "labels": []any{"bug", "enhancement"}, + "milestones": []any{"v1.0", "2"}, + "since": "2026-01-01T00:00:00Z", } - _, err := listRepoIssuesFn(context.Background(), req) + _, err := listRepoIssuesFn(context.Background(), args) if err != nil { t.Fatalf("listRepoIssuesFn() error = %v", err) } @@ -126,17 +122,17 @@ func Test_listRepoIssuesFn_includesMilestone(t *testing.T) { flag.Host, flag.Token, flag.Version = server.URL, "", "test" defer func() { flag.Host, flag.Token, flag.Version = origHost, origToken, origVersion }() - req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: map[string]any{ + args := map[string]any{ "owner": owner, "repo": repo, - }}} - res, err := listRepoIssuesFn(context.Background(), req) + } + res, err := listRepoIssuesFn(context.Background(), args) if err != nil { t.Fatalf("listRepoIssuesFn() error = %v", err) } if res.IsError { t.Fatalf("unexpected error result: %v", res.Content) } - body := res.Content[0].(mcp.TextContent).Text + body := res.Content[0].(*mcp.TextContent).Text if !strings.Contains(body, `"milestone"`) || !strings.Contains(body, `"v1.0"`) { t.Fatalf("expected milestone in list output, got: %s", body) } @@ -189,20 +185,16 @@ func Test_createIssueFn_labels(t *testing.T) { flag.Version = origVersion }() - req := mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Arguments: map[string]any{ - "owner": owner, - "repo": repo, - "title": "test issue", - "body": "body", - "labels": []any{float64(10), float64(20)}, - "deadline": "2026-06-01T00:00:00Z", - }, - }, + args := map[string]any{ + "owner": owner, + "repo": repo, + "title": "test issue", + "body": "body", + "labels": []any{float64(10), float64(20)}, + "deadline": "2026-06-01T00:00:00Z", } - _, err := createIssueFn(context.Background(), req) + _, err := createIssueFn(context.Background(), args) if err != nil { t.Fatalf("createIssueFn() error = %v", err) } @@ -255,17 +247,17 @@ func Test_getIssueByIndexFn_includesAttachments(t *testing.T) { flag.Host, flag.Token, flag.Version = server.URL, "", "test" defer func() { flag.Host, flag.Token, flag.Version = origHost, origToken, origVersion }() - req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: map[string]any{ + args := map[string]any{ "owner": owner, "repo": repo, "issue_number": float64(42), - }}} - res, err := getIssueByIndexFn(context.Background(), req) + } + res, err := getIssueByIndexFn(context.Background(), args) if err != nil { t.Fatalf("getIssueByIndexFn() error = %v", err) } if res.IsError { t.Fatalf("unexpected error result: %v", res.Content) } - body := res.Content[0].(mcp.TextContent).Text + body := res.Content[0].(*mcp.TextContent).Text if !strings.Contains(body, `[shot.png](https://example/shot.png)`) { t.Fatalf("expected attachment markdown inlined in body, got: %s", body) } @@ -304,17 +296,17 @@ func Test_getIssueCommentsByIndexFn_includesAttachments(t *testing.T) { flag.Host, flag.Token, flag.Version = server.URL, "", "test" defer func() { flag.Host, flag.Token, flag.Version = origHost, origToken, origVersion }() - req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: map[string]any{ + args := map[string]any{ "owner": owner, "repo": repo, "issue_number": float64(7), - }}} - res, err := getIssueCommentsByIndexFn(context.Background(), req) + } + res, err := getIssueCommentsByIndexFn(context.Background(), args) if err != nil { t.Fatalf("getIssueCommentsByIndexFn() error = %v", err) } if res.IsError { t.Fatalf("unexpected error result: %v", res.Content) } - body := res.Content[0].(mcp.TextContent).Text + body := res.Content[0].(*mcp.TextContent).Text if !strings.Contains(body, `[log.txt](https://example/log.txt)`) { t.Fatalf("expected attachment markdown inlined in body, got: %s", body) } diff --git a/operation/label/label.go b/operation/label/label.go index 8123a1f..4a94a9f 100644 --- a/operation/label/label.go +++ b/operation/label/label.go @@ -12,8 +12,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) var Tool = tool.New("label") @@ -24,99 +23,97 @@ const ( ) var ( - LabelReadTool = mcp.NewTool( + LabelReadTool = tool.NewDefinition( LabelReadToolName, - mcp.WithDescription("Read repo or org labels."), - mcp.WithToolAnnotation(annotation.ReadOnly("Read labels")), - mcp.WithString("method", mcp.Required(), mcp.Enum("list_repo_labels", "get_repo_label", "list_org_labels")), - mcp.WithString("owner", mcp.Description("for repo methods")), - mcp.WithString("repo", mcp.Description("for repo methods")), - mcp.WithString("org", mcp.Description("for org methods")), - mcp.WithNumber("id", mcp.Description("label ID (for 'get_repo_label')")), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), + "Read repo or org labels.", + annotation.ReadOnly("Read labels"), + tool.String("method", tool.Required(), tool.Enum("list_repo_labels", "get_repo_label", "list_org_labels")), + tool.String("owner", tool.Description("for repo methods")), + tool.String("repo", tool.Description("for repo methods")), + tool.String("org", tool.Description("for org methods")), + tool.Number("id", tool.Description("label ID (for 'get_repo_label')")), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) - LabelWriteTool = mcp.NewTool( + LabelWriteTool = tool.NewDefinition( LabelWriteToolName, - mcp.WithDescription("Write labels (repo or org): create, edit, delete."), - mcp.WithToolAnnotation(annotation.Destructive("Create, update, or delete labels")), - mcp.WithString("method", mcp.Required(), mcp.Enum("create_repo_label", "edit_repo_label", "delete_repo_label", "create_org_label", "edit_org_label", "delete_org_label")), - mcp.WithString("owner", mcp.Description("for repo methods")), - mcp.WithString("repo", mcp.Description("for repo methods")), - mcp.WithString("org", mcp.Description("for org methods")), - mcp.WithNumber("id", mcp.Description("for edit/delete")), - mcp.WithString("name", mcp.Description("required for create")), - mcp.WithString("color", mcp.Description("hex (#RRGGBB); required for create")), - mcp.WithString("description"), - mcp.WithBoolean("exclusive", mcp.Description("exclusive (org only)")), - mcp.WithBoolean("is_archived", mcp.Description("archived (repo only)")), + "Write labels (repo or org): create, edit, delete.", + annotation.Destructive("Create, update, or delete labels"), + tool.String("method", tool.Required(), tool.Enum("create_repo_label", "edit_repo_label", "delete_repo_label", "create_org_label", "edit_org_label", "delete_org_label")), + tool.String("owner", tool.Description("for repo methods")), + tool.String("repo", tool.Description("for repo methods")), + tool.String("org", tool.Description("for org methods")), + tool.Number("id", tool.Description("for edit/delete")), + tool.String("name", tool.Description("required for create")), + tool.String("color", tool.Description("hex (#RRGGBB); required for create")), + tool.String("description"), + tool.Boolean("exclusive", tool.Description("exclusive (org only)")), + tool.Boolean("is_archived", tool.Description("archived (repo only)")), ) ) func init() { - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: LabelReadTool, Handler: labelReadFn, }) - Tool.RegisterWrite(server.ServerTool{ + Tool.RegisterWrite(tool.ServerTool{ Tool: LabelWriteTool, Handler: labelWriteFn, }) } -func labelReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func labelReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "list_repo_labels": - return listRepoLabelsFn(ctx, req) + return listRepoLabelsFn(ctx, args) case "get_repo_label": - return getRepoLabelFn(ctx, req) + return getRepoLabelFn(ctx, args) case "list_org_labels": - return listOrgLabelsFn(ctx, req) + return listOrgLabelsFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func labelWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func labelWriteFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "create_repo_label": - return createRepoLabelFn(ctx, req) + return createRepoLabelFn(ctx, args) case "edit_repo_label": - return editRepoLabelFn(ctx, req) + return editRepoLabelFn(ctx, args) case "delete_repo_label": - return deleteRepoLabelFn(ctx, req) + return deleteRepoLabelFn(ctx, args) case "create_org_label": - return createOrgLabelFn(ctx, req) + return createOrgLabelFn(ctx, args) case "edit_org_label": - return editOrgLabelFn(ctx, req) + return editOrgLabelFn(ctx, args) case "delete_org_label": - return deleteOrgLabelFn(ctx, req) + return deleteOrgLabelFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func listRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func listRepoLabelsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) + page, pageSize := params.GetPagination(args, 30) opt := gitea_sdk.ListLabelsOptions{ ListOptions: gitea_sdk.ListOptions{ @@ -135,16 +132,16 @@ func listRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(slim.Labels(labels)) } -func getRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func getRepoLabelFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - id, err := params.GetIndex(req.GetArguments(), "id") + id, err := params.GetIndex(args, "id") if err != nil { return to.ErrorResult(err) } @@ -160,26 +157,26 @@ func getRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool return to.TextResult(slim.Label(label)) } -func createRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func createRepoLabelFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - name, err := params.GetString(req.GetArguments(), "name") + name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) } - color, err := params.GetString(req.GetArguments(), "color") + color, err := params.GetString(args, "color") if err != nil { return to.ErrorResult(err) } - description, _ := req.GetArguments()["description"].(string) // Optional + description, _ := args["description"].(string) // Optional - isArchived, _ := req.GetArguments()["is_archived"].(bool) + isArchived, _ := args["is_archived"].(bool) opt := gitea_sdk.CreateLabelOption{ Name: name, @@ -199,21 +196,20 @@ func createRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.TextResult(slim.Label(label)) } -func editRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func editRepoLabelFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - id, err := params.GetIndex(req.GetArguments(), "id") + id, err := params.GetIndex(args, "id") if err != nil { return to.ErrorResult(err) } - args := req.GetArguments() opt := gitea_sdk.EditLabelOption{ Name: params.GetOptionalStringPtr(args, "name"), Color: params.GetOptionalStringPtr(args, "color"), @@ -232,16 +228,16 @@ func editRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult(slim.Label(label)) } -func deleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func deleteRepoLabelFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - id, err := params.GetIndex(req.GetArguments(), "id") + id, err := params.GetIndex(args, "id") if err != nil { return to.ErrorResult(err) } @@ -257,12 +253,12 @@ func deleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.TextResult("Label deleted successfully") } -func listOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := params.GetString(req.GetArguments(), "org") +func listOrgLabelsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + org, err := params.GetString(args, "org") if err != nil { return to.ErrorResult(err) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) + page, pageSize := params.GetPagination(args, 30) opt := gitea_sdk.ListOrgLabelsOptions{ ListOptions: gitea_sdk.ListOptions{ @@ -281,21 +277,21 @@ func listOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult(slim.Labels(labels)) } -func createOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := params.GetString(req.GetArguments(), "org") +func createOrgLabelFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + org, err := params.GetString(args, "org") if err != nil { return to.ErrorResult(err) } - name, err := params.GetString(req.GetArguments(), "name") + name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) } - color, err := params.GetString(req.GetArguments(), "color") + color, err := params.GetString(args, "color") if err != nil { return to.ErrorResult(err) } - description, _ := req.GetArguments()["description"].(string) - exclusive, _ := req.GetArguments()["exclusive"].(bool) + description, _ := args["description"].(string) + exclusive, _ := args["exclusive"].(bool) opt := gitea_sdk.CreateOrgLabelOption{ Name: name, @@ -315,17 +311,16 @@ func createOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(slim.Label(label)) } -func editOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := params.GetString(req.GetArguments(), "org") +func editOrgLabelFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + org, err := params.GetString(args, "org") if err != nil { return to.ErrorResult(err) } - id, err := params.GetIndex(req.GetArguments(), "id") + id, err := params.GetIndex(args, "id") if err != nil { return to.ErrorResult(err) } - args := req.GetArguments() opt := gitea_sdk.EditOrgLabelOption{ Name: params.GetOptionalStringPtr(args, "name"), Color: params.GetOptionalStringPtr(args, "color"), @@ -344,12 +339,12 @@ func editOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool return to.TextResult(slim.Label(label)) } -func deleteOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := params.GetString(req.GetArguments(), "org") +func deleteOrgLabelFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + org, err := params.GetString(args, "org") if err != nil { return to.ErrorResult(err) } - id, err := params.GetIndex(req.GetArguments(), "id") + id, err := params.GetIndex(args, "id") if err != nil { return to.ErrorResult(err) } diff --git a/operation/milestone/milestone.go b/operation/milestone/milestone.go index c1345c1..10947b4 100644 --- a/operation/milestone/milestone.go +++ b/operation/milestone/milestone.go @@ -11,8 +11,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) var Tool = tool.New("milestone") @@ -23,90 +22,90 @@ const ( ) var ( - MilestoneReadTool = mcp.NewTool( + MilestoneReadTool = tool.NewDefinition( MilestoneReadToolName, - mcp.WithDescription("Read milestones: get one or list."), - mcp.WithToolAnnotation(annotation.ReadOnly("Read milestones")), - mcp.WithString("method", mcp.Required(), mcp.Enum("get", "list")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithNumber("id", mcp.Description("for 'get'")), - mcp.WithString("state", mcp.DefaultString("all")), - mcp.WithString("name", mcp.Description("name filter (for 'list')")), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), + "Read milestones: get one or list.", + annotation.ReadOnly("Read milestones"), + tool.String("method", tool.Required(), tool.Enum("get", "list")), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.Number("id", tool.Description("for 'get'")), + tool.String("state", tool.Default("all")), + tool.String("name", tool.Description("name filter (for 'list')")), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) - MilestoneWriteTool = mcp.NewTool( + MilestoneWriteTool = tool.NewDefinition( MilestoneWriteToolName, - mcp.WithDescription("Write milestones: create, update, delete."), - mcp.WithToolAnnotation(annotation.Destructive("Create, update, or delete milestones")), - mcp.WithString("method", mcp.Required(), mcp.Enum("create", "update", "edit", "delete")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithNumber("id", mcp.Description("for 'update'/'delete'")), - mcp.WithString("title", mcp.Description("for 'create'")), - mcp.WithString("description"), - mcp.WithString("due_on", mcp.Description("due date")), - mcp.WithString("state", mcp.Enum("open", "closed")), + "Write milestones: create, update, delete.", + annotation.Destructive("Create, update, or delete milestones"), + tool.String("method", tool.Required(), tool.Enum("create", "update", "edit", "delete")), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.Number("id", tool.Description("for 'update'/'delete'")), + tool.String("title", tool.Description("for 'create'")), + tool.String("description"), + tool.String("due_on", tool.Description("due date")), + tool.String("state", tool.Enum("open", "closed")), ) ) func init() { - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: MilestoneReadTool, Handler: milestoneReadFn, }) - Tool.RegisterWrite(server.ServerTool{ + Tool.RegisterWrite(tool.ServerTool{ Tool: MilestoneWriteTool, Handler: milestoneWriteFn, }) } -func milestoneReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := params.GetString(req.GetArguments(), "method") +func milestoneReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "get": - return getMilestoneFn(ctx, req) + return getMilestoneFn(ctx, args) case "list": - return listMilestonesFn(ctx, req) + return listMilestonesFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func milestoneWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := params.GetString(req.GetArguments(), "method") +func milestoneWriteFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "create": - return createMilestoneFn(ctx, req) + return createMilestoneFn(ctx, args) case "update": - return editMilestoneFn(ctx, req) + return editMilestoneFn(ctx, args) case "edit": - return editMilestoneFn(ctx, req) + return editMilestoneFn(ctx, args) case "delete": - return deleteMilestoneFn(ctx, req) + return deleteMilestoneFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func getMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func getMilestoneFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - id, err := params.GetIndex(req.GetArguments(), "id") + id, err := params.GetIndex(args, "id") if err != nil { return to.ErrorResult(err) } @@ -122,18 +121,18 @@ func getMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool return to.TextResult(slimMilestone(milestone)) } -func listMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func listMilestonesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - state := params.GetOptionalString(req.GetArguments(), "state", "all") - name := params.GetOptionalString(req.GetArguments(), "name", "") - page, pageSize := params.GetPagination(req.GetArguments(), 30) + state := params.GetOptionalString(args, "state", "all") + name := params.GetOptionalString(args, "name", "") + page, pageSize := params.GetPagination(args, 30) opt := gitea_sdk.ListMilestoneOption{ State: gitea_sdk.StateType(state), Name: name, @@ -153,16 +152,16 @@ func listMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(slimMilestones(milestones)) } -func createMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func createMilestoneFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - title, err := params.GetString(req.GetArguments(), "title") + title, err := params.GetString(args, "title") if err != nil { return to.ErrorResult(err) } @@ -171,11 +170,11 @@ func createMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT Title: title, } - description, ok := req.GetArguments()["description"].(string) + description, ok := args["description"].(string) if ok { opt.Description = description } - opt.Deadline = params.GetOptionalTime(req.GetArguments(), "due_on") + opt.Deadline = params.GetOptionalTime(args, "due_on") client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -189,21 +188,20 @@ func createMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.TextResult(slimMilestone(milestone)) } -func editMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func editMilestoneFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - id, err := params.GetIndex(req.GetArguments(), "id") + id, err := params.GetIndex(args, "id") if err != nil { return to.ErrorResult(err) } - args := req.GetArguments() opt := gitea_sdk.EditMilestoneOption{ Description: params.GetPresentStringPtr(args, "description"), Deadline: params.GetOptionalTime(args, "due_on"), @@ -228,16 +226,16 @@ func editMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult(slimMilestone(milestone)) } -func deleteMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func deleteMilestoneFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - id, err := params.GetIndex(req.GetArguments(), "id") + id, err := params.GetIndex(args, "id") if err != nil { return to.ErrorResult(err) } diff --git a/operation/milestone/milestone_test.go b/operation/milestone/milestone_test.go index 7a2a505..3a5a222 100644 --- a/operation/milestone/milestone_test.go +++ b/operation/milestone/milestone_test.go @@ -12,7 +12,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/flag" - "github.com/mark3labs/mcp-go/mcp" + "github.com/modelcontextprotocol/go-sdk/mcp" ) func Test_milestoneWriteFn_dueOn(t *testing.T) { @@ -56,7 +56,7 @@ func Test_milestoneWriteFn_dueOn(t *testing.T) { cases := []struct { name string - fn func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) + fn func(context.Context, map[string]any) (*mcp.CallToolResult, error) method string extra map[string]any }{ @@ -69,7 +69,7 @@ func Test_milestoneWriteFn_dueOn(t *testing.T) { a := map[string]any{} maps.Copy(a, args) maps.Copy(a, tc.extra) - res, err := tc.fn(context.Background(), mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: a}}) + res, err := tc.fn(context.Background(), a) if err != nil || res.IsError { t.Fatalf("%s err=%v result=%v", tc.name, err, res) } diff --git a/operation/notification/notification.go b/operation/notification/notification.go index f219d70..d2c8dc9 100644 --- a/operation/notification/notification.go +++ b/operation/notification/notification.go @@ -12,8 +12,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) var Tool = tool.New("notification") @@ -24,79 +23,76 @@ const ( ) var ( - NotificationReadTool = mcp.NewTool( + NotificationReadTool = tool.NewDefinition( NotificationReadToolName, - mcp.WithDescription("Read notifications: list (optionally scoped to a repo) or get a thread by ID."), - mcp.WithToolAnnotation(annotation.ReadOnly("Read notifications")), - mcp.WithString("method", mcp.Required(), mcp.Enum("list", "get")), - mcp.WithString("owner", mcp.Description("scope 'list' to a repo")), - mcp.WithString("repo", mcp.Description("scope 'list' to a repo")), - mcp.WithNumber("id", mcp.Description("thread ID (for 'get')")), - mcp.WithString("status", mcp.Enum("unread", "read", "pinned")), - mcp.WithString("subject_type", mcp.Enum("Issue", "Pull", "Commit", "Repository")), - mcp.WithString("since", mcp.Description("updated after ISO 8601")), - mcp.WithString("before", mcp.Description("updated before ISO 8601")), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), + "Read notifications: list (optionally scoped to a repo) or get a thread by ID.", + annotation.ReadOnly("Read notifications"), + tool.String("method", tool.Required(), tool.Enum("list", "get")), + tool.String("owner", tool.Description("scope 'list' to a repo")), + tool.String("repo", tool.Description("scope 'list' to a repo")), + tool.Number("id", tool.Description("thread ID (for 'get')")), + tool.String("status", tool.Enum("unread", "read", "pinned")), + tool.String("subject_type", tool.Enum("Issue", "Pull", "Commit", "Repository")), + tool.String("since", tool.Description("updated after ISO 8601")), + tool.String("before", tool.Description("updated before ISO 8601")), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) - NotificationWriteTool = mcp.NewTool( + NotificationWriteTool = tool.NewDefinition( NotificationWriteToolName, - mcp.WithDescription("Mark a notification or all notifications as read."), - mcp.WithToolAnnotation(annotation.Write("Manage notifications")), - mcp.WithString("method", mcp.Required(), mcp.Enum("mark_read", "mark_all_read")), - mcp.WithNumber("id", mcp.Description("thread ID (for 'mark_read')")), - mcp.WithString("owner", mcp.Description("scope 'mark_all_read' to a repo")), - mcp.WithString("repo", mcp.Description("scope 'mark_all_read' to a repo")), - mcp.WithString("last_read_at", mcp.Description("ISO 8601; defaults to now")), + "Mark a notification or all notifications as read.", + annotation.Write("Manage notifications"), + tool.String("method", tool.Required(), tool.Enum("mark_read", "mark_all_read")), + tool.Number("id", tool.Description("thread ID (for 'mark_read')")), + tool.String("owner", tool.Description("scope 'mark_all_read' to a repo")), + tool.String("repo", tool.Description("scope 'mark_all_read' to a repo")), + tool.String("last_read_at", tool.Description("ISO 8601; defaults to now")), ) ) func init() { - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: NotificationReadTool, Handler: notificationReadFn, }) - Tool.RegisterWrite(server.ServerTool{ + Tool.RegisterWrite(tool.ServerTool{ Tool: NotificationWriteTool, Handler: notificationWriteFn, }) } -func notificationReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func notificationReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "list": - return listNotificationsFn(ctx, req) + return listNotificationsFn(ctx, args) case "get": - return getNotificationFn(ctx, req) + return getNotificationFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func notificationWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func notificationWriteFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "mark_read": - return markNotificationReadFn(ctx, req) + return markNotificationReadFn(ctx, args) case "mark_all_read": - return markAllNotificationsReadFn(ctx, req) + return markAllNotificationsReadFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func listNotificationsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func listNotificationsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { page, pageSize := params.GetPagination(args, 30) opt := gitea_sdk.ListNotificationOptions{ ListOptions: gitea_sdk.ListOptions{ @@ -139,8 +135,8 @@ func listNotificationsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal return to.TextResult(slimThreads(threads)) } -func getNotificationFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - id, err := params.GetIndex(req.GetArguments(), "id") +func getNotificationFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + id, err := params.GetIndex(args, "id") if err != nil { return to.ErrorResult(err) } @@ -155,8 +151,8 @@ func getNotificationFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.TextResult(slimThread(thread)) } -func markNotificationReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - id, err := params.GetIndex(req.GetArguments(), "id") +func markNotificationReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + id, err := params.GetIndex(args, "id") if err != nil { return to.ErrorResult(err) } @@ -174,8 +170,7 @@ func markNotificationReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp. return to.TextResult("Notification marked as read") } -func markAllNotificationsReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func markAllNotificationsReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { lastReadAt := time.Now() if t := params.GetOptionalTime(args, "last_read_at"); t != nil { lastReadAt = *t diff --git a/operation/operation.go b/operation/operation.go index 0732e1a..dc4b2c9 100644 --- a/operation/operation.go +++ b/operation/operation.go @@ -29,11 +29,11 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/tool" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) var ( - mcpServer *server.MCPServer + mcpServer *mcp.Server domainTools = []*tool.Tool{ user.Tool, actions.Tool, repo.Tool, notification.Tool, issue.Tool, @@ -43,9 +43,11 @@ var ( } ) -func RegisterTool(s *server.MCPServer) { +func RegisterTool(s *mcp.Server) { for _, t := range domainTools { - s.AddTools(t.Tools()...) + for _, registeredTool := range t.Tools() { + s.AddTool(registeredTool.Tool, registeredTool.MCPHandler()) + } } tool.WarnUnmatchedAllowedTools(domainTools...) tool.WarnUnmatchedAllowedScopes(domainTools...) @@ -71,8 +73,7 @@ func parseAuthToken(authHeader string) (string, bool) { return "", false } -func getContextWithToken(ctx context.Context, r *http.Request) context.Context { - authHeader := r.Header.Get("Authorization") +func getContextWithToken(ctx context.Context, authHeader string) context.Context { if authHeader == "" { return ctx } @@ -85,23 +86,45 @@ func getContextWithToken(ctx context.Context, r *http.Request) context.Context { return context.WithValue(ctx, mcpContext.TokenContextKey, token) } +func authTokenMiddleware(next mcp.MethodHandler) mcp.MethodHandler { + return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) { + if extra := req.GetExtra(); extra != nil { + ctx = getContextWithToken(ctx, extra.Header.Get("Authorization")) + } + return next(ctx, method, req) + } +} + +func newStreamableHTTPHandler(s *mcp.Server) http.Handler { + return mcp.NewStreamableHTTPHandler( + func(*http.Request) *mcp.Server { return s }, + &mcp.StreamableHTTPOptions{ + Logger: log.Slog(), + MaxRequestBodyBytes: -1, + Stateless: false, + }, + ) +} + +func newHTTPServer(addr string, s *mcp.Server) *http.Server { + mux := http.NewServeMux() + mux.Handle("/mcp", newStreamableHTTPHandler(s)) + return &http.Server{ + Addr: addr, + Handler: mux, + } +} + func Run() error { mcpServer = newMCPServer(flag.Version) RegisterTool(mcpServer) switch flag.Mode { case "stdio": - if err := server.ServeStdio( - mcpServer, - ); err != nil { + if err := mcpServer.Run(context.Background(), &mcp.StdioTransport{}); err != nil { return err } case "http": - httpServer := server.NewStreamableHTTPServer( - mcpServer, - server.WithStreamableHTTPLogger(log.Slog()), - server.WithHeartbeatInterval(30*time.Second), - server.WithHTTPContextFunc(getContextWithToken), - ) + httpServer := newHTTPServer(fmt.Sprintf(":%d", flag.Port), mcpServer) log.Infof("Gitea MCP HTTP server listening on :%d", flag.Port) // Graceful shutdown setup @@ -120,7 +143,7 @@ func Run() error { close(shutdownDone) }() - if err := httpServer.Start(fmt.Sprintf(":%d", flag.Port)); err != nil && !errors.Is(err, http.ErrServerClosed) { + if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { return err } <-shutdownDone // Wait for shutdown to finish @@ -130,12 +153,14 @@ func Run() error { return nil } -func newMCPServer(version string) *server.MCPServer { - return server.NewMCPServer( - "Gitea MCP Server", - version, - server.WithToolCapabilities(true), - server.WithLogging(), - server.WithRecovery(), +func newMCPServer(version string) *mcp.Server { + s := mcp.NewServer( + &mcp.Implementation{ + Name: "Gitea MCP Server", + Version: version, + }, + &mcp.ServerOptions{Logger: log.Slog()}, ) + s.AddReceivingMiddleware(authTokenMiddleware) + return s } diff --git a/operation/operation_test.go b/operation/operation_test.go index 49e7d70..8e0b3cf 100644 --- a/operation/operation_test.go +++ b/operation/operation_test.go @@ -7,10 +7,7 @@ import ( ) // TestAllToolsHaveDescriptions ensures every registered tool sets a non-empty -// Tool.Description. mcp-go only serializes the "description" field of a tool -// when it is non-empty, so an omitted description makes strict MCP clients -// (e.g. mcp-probe) reject the tools/list response with "missing field -// `description`". +// Tool.Description, as strict MCP clients reject tools without one. func TestAllToolsHaveDescriptions(t *testing.T) { origRO, origAllow := flag.ReadOnly, flag.AllowedTools t.Cleanup(func() { diff --git a/operation/packages/packages.go b/operation/packages/packages.go index cba0aac..818bae5 100644 --- a/operation/packages/packages.go +++ b/operation/packages/packages.go @@ -13,8 +13,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/tool" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) var Tool = tool.New("packages") @@ -25,70 +24,68 @@ const ( ) var ( - PackageReadTool = mcp.NewTool( + PackageReadTool = tool.NewDefinition( PackageReadToolName, - mcp.WithToolAnnotation(annotation.ReadOnly("Read package registry")), - mcp.WithDescription("Read package registry: list packages (one entry per version, filter via 'q'/'type'), list versions, or get a version."), - mcp.WithString("method", mcp.Required(), mcp.Enum("list", "list_versions", "get")), - mcp.WithString("owner", mcp.Required(), mcp.Description("user or org")), - mcp.WithString("type", mcp.Description("container/npm/maven/pypi/cargo/generic; required except 'list'")), - mcp.WithString("name", mcp.Description("slashes auto-encoded; required except 'list'")), - mcp.WithString("version", mcp.Description("for 'get'")), - mcp.WithString("q", mcp.Description("search query")), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30), mcp.Min(1)), + "Read package registry: list packages (one entry per version, filter via 'q'/'type'), list versions, or get a version.", + annotation.ReadOnly("Read package registry"), + tool.String("method", tool.Required(), tool.Enum("list", "list_versions", "get")), + tool.String("owner", tool.Required(), tool.Description("user or org")), + tool.String("type", tool.Description("container/npm/maven/pypi/cargo/generic; required except 'list'")), + tool.String("name", tool.Description("slashes auto-encoded; required except 'list'")), + tool.String("version", tool.Description("for 'get'")), + tool.String("q", tool.Description("search query")), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1), tool.Minimum(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30), tool.Minimum(1)), ) - PackageWriteTool = mcp.NewTool( + PackageWriteTool = tool.NewDefinition( PackageWriteToolName, - mcp.WithToolAnnotation(annotation.Destructive("Delete a package version")), - mcp.WithDescription("Delete a package version (irreversible)."), - mcp.WithString("method", mcp.Required(), mcp.Enum("delete")), - mcp.WithString("owner", mcp.Required(), mcp.Description("user or org")), - mcp.WithString("type", mcp.Required(), mcp.Description("container/npm/maven/pypi/cargo/generic")), - mcp.WithString("name", mcp.Required(), mcp.Description("slashes auto-encoded")), - mcp.WithString("version", mcp.Required()), + "Delete a package version (irreversible).", + annotation.Destructive("Delete a package version"), + tool.String("method", tool.Required(), tool.Enum("delete")), + tool.String("owner", tool.Required(), tool.Description("user or org")), + tool.String("type", tool.Required(), tool.Description("container/npm/maven/pypi/cargo/generic")), + tool.String("name", tool.Required(), tool.Description("slashes auto-encoded")), + tool.String("version", tool.Required()), ) ) func init() { - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: PackageReadTool, Handler: packageReadFn, }) - Tool.RegisterWrite(server.ServerTool{ + Tool.RegisterWrite(tool.ServerTool{ Tool: PackageWriteTool, Handler: packageWriteFn, }) } -func packageReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func packageReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "list": - return listPackagesFn(ctx, req) + return listPackagesFn(ctx, args) case "list_versions": - return listPackageVersionsFn(ctx, req) + return listPackageVersionsFn(ctx, args) case "get": - return getPackageFn(ctx, req) + return getPackageFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func packageWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func packageWriteFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "delete": - return deletePackageVersionFn(ctx, req) + return deletePackageVersionFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } @@ -108,8 +105,7 @@ func escapePackageName(name string) string { return url.PathEscape(name) } -func listPackagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func listPackagesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -135,8 +131,7 @@ func listPackagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool return to.TextResult(slimPackages(result)) } -func listPackageVersionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func listPackageVersionsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -164,8 +159,7 @@ func listPackageVersionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.C return to.TextResult(slimPackages(result)) } -func getPackageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func getPackageFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -192,8 +186,7 @@ func getPackageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe return to.TextResult(slimPackage(result)) } -func deletePackageVersionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func deletePackageVersionFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) diff --git a/operation/packages/packages_test.go b/operation/packages/packages_test.go index 0027db8..424381f 100644 --- a/operation/packages/packages_test.go +++ b/operation/packages/packages_test.go @@ -11,7 +11,7 @@ import ( mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" "gitea.com/gitea/gitea-mcp/pkg/flag" - "github.com/mark3labs/mcp-go/mcp" + "github.com/modelcontextprotocol/go-sdk/mcp" ) func TestPackageReadList(t *testing.T) { @@ -37,13 +37,12 @@ func TestPackageReadList(t *testing.T) { ctx := context.WithValue(context.Background(), mcpContext.TokenContextKey, "test-token") t.Run("basic list", func(t *testing.T) { - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]any{ + args := map[string]any{ "method": "list", "owner": "test-org", } - result, err := packageReadFn(ctx, req) + result, err := packageReadFn(ctx, args) if err != nil { t.Fatalf("packageReadFn() error: %v", err) } @@ -51,7 +50,7 @@ func TestPackageReadList(t *testing.T) { t.Fatal("packageReadFn() returned error result") } - text := result.Content[0].(mcp.TextContent).Text + text := result.Content[0].(*mcp.TextContent).Text var packages []map[string]any if err := json.Unmarshal([]byte(text), &packages); err != nil { t.Fatalf("failed to unmarshal result: %v", err) @@ -68,15 +67,14 @@ func TestPackageReadList(t *testing.T) { }) t.Run("with type and query filters", func(t *testing.T) { - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]any{ + args := map[string]any{ "method": "list", "owner": "test-org", "type": "container", "q": "myimage", } - _, err := packageReadFn(ctx, req) + _, err := packageReadFn(ctx, args) if err != nil { t.Fatalf("packageReadFn() error: %v", err) } @@ -92,15 +90,14 @@ func TestPackageReadList(t *testing.T) { }) t.Run("with pagination", func(t *testing.T) { - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]any{ + args := map[string]any{ "method": "list", "owner": "test-org", "page": float64(2), "per_page": float64(10), } - _, err := packageReadFn(ctx, req) + _, err := packageReadFn(ctx, args) if err != nil { t.Fatalf("packageReadFn() error: %v", err) } @@ -148,15 +145,14 @@ func TestPackageReadListVersions(t *testing.T) { for _, tt := range tests { t.Run(tt.testName, func(t *testing.T) { - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]any{ + args := map[string]any{ "method": "list_versions", "owner": "test-org", "type": "container", "name": tt.name, } - result, err := packageReadFn(ctx, req) + result, err := packageReadFn(ctx, args) if err != nil { t.Fatalf("packageReadFn() error: %v", err) } @@ -171,7 +167,7 @@ func TestPackageReadListVersions(t *testing.T) { } mu.Unlock() - text := result.Content[0].(mcp.TextContent).Text + text := result.Content[0].(*mcp.TextContent).Text var versions []map[string]any if err := json.Unmarshal([]byte(text), &versions); err != nil { t.Fatalf("failed to unmarshal result: %v", err) @@ -215,8 +211,7 @@ func TestPackageReadGet(t *testing.T) { for _, tt := range tests { t.Run(tt.testName, func(t *testing.T) { - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]any{ + args := map[string]any{ "method": "get", "owner": "test-org", "type": "container", @@ -224,7 +219,7 @@ func TestPackageReadGet(t *testing.T) { "version": "v1.0.0", } - result, err := packageReadFn(ctx, req) + result, err := packageReadFn(ctx, args) if err != nil { t.Fatalf("packageReadFn() error: %v", err) } @@ -239,7 +234,7 @@ func TestPackageReadGet(t *testing.T) { } mu.Unlock() - text := result.Content[0].(mcp.TextContent).Text + text := result.Content[0].(*mcp.TextContent).Text var pkg map[string]any if err := json.Unmarshal([]byte(text), &pkg); err != nil { t.Fatalf("failed to unmarshal result: %v", err) @@ -277,8 +272,7 @@ func TestPackageWriteDelete(t *testing.T) { ctx := context.WithValue(context.Background(), mcpContext.TokenContextKey, "test-token") - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]any{ + args := map[string]any{ "method": "delete", "owner": "test-org", "type": "container", @@ -286,7 +280,7 @@ func TestPackageWriteDelete(t *testing.T) { "version": "v1.0.0", } - result, err := packageWriteFn(ctx, req) + result, err := packageWriteFn(ctx, args) if err != nil { t.Fatalf("packageWriteFn() error: %v", err) } @@ -307,24 +301,22 @@ func TestPackageWriteDelete(t *testing.T) { func TestPackageReadUnknownMethod(t *testing.T) { ctx := context.Background() - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]any{ + args := map[string]any{ "method": "bogus", "owner": "test-org", } - if _, err := packageReadFn(ctx, req); err == nil { + if _, err := packageReadFn(ctx, args); err == nil { t.Fatal("expected error for unknown method") } } func TestPackageWriteUnknownMethod(t *testing.T) { ctx := context.Background() - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]any{ + args := map[string]any{ "method": "bogus", "owner": "test-org", } - if _, err := packageWriteFn(ctx, req); err == nil { + if _, err := packageWriteFn(ctx, args); err == nil { t.Fatal("expected error for unknown method") } } diff --git a/operation/pull/pull.go b/operation/pull/pull.go index a354bef..7fb36b8 100644 --- a/operation/pull/pull.go +++ b/operation/pull/pull.go @@ -15,8 +15,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) var Tool = tool.New("pull_request") @@ -29,79 +28,79 @@ const ( ) var ( - ListRepoPullRequestsTool = mcp.NewTool( + ListRepoPullRequestsTool = tool.NewDefinition( ListRepoPullRequestsToolName, - mcp.WithDescription("List pull requests in a repository, filterable by state and milestone, with configurable sort order (e.g. recently updated, most commented)."), - mcp.WithToolAnnotation(annotation.ReadOnly("List pull requests")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("state", mcp.Enum("open", "closed", "all"), mcp.DefaultString("all")), - mcp.WithString("sort", mcp.Enum("oldest", "recentupdate", "leastupdate", "mostcomment", "leastcomment", "priority"), mcp.DefaultString("recentupdate")), - mcp.WithNumber("milestone"), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), + "List pull requests in a repository, filterable by state and milestone, with configurable sort order (e.g. recently updated, most commented).", + annotation.ReadOnly("List pull requests"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("state", tool.Enum("open", "closed", "all"), tool.Default("all")), + tool.String("sort", tool.Enum("oldest", "recentupdate", "leastupdate", "mostcomment", "leastcomment", "priority"), tool.Default("recentupdate")), + tool.Number("milestone"), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) - PullRequestReadTool = mcp.NewTool( + PullRequestReadTool = tool.NewDefinition( PullRequestReadToolName, - mcp.WithDescription("Read pull request: details, diff, changed files, head commit status, reviews, review comments."), - mcp.WithToolAnnotation(annotation.ReadOnly("Read pull request details")), - mcp.WithString("method", mcp.Required(), mcp.Enum("get", "get_diff", "get_files", "get_status", "get_reviews", "get_review", "get_review_comments")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithNumber("pull_number", mcp.Required()), - mcp.WithNumber("review_id", mcp.Description("for 'get_review'; optional for 'get_review_comments', omit to list all")), - mcp.WithBoolean("binary", mcp.Description("include binary diff")), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), + "Read pull request: details, diff, changed files, head commit status, reviews, review comments.", + annotation.ReadOnly("Read pull request details"), + tool.String("method", tool.Required(), tool.Enum("get", "get_diff", "get_files", "get_status", "get_reviews", "get_review", "get_review_comments")), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.Number("pull_number", tool.Required()), + tool.Number("review_id", tool.Description("for 'get_review'; optional for 'get_review_comments', omit to list all")), + tool.Boolean("binary", tool.Description("include binary diff")), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) - PullRequestWriteTool = mcp.NewTool( + PullRequestWriteTool = tool.NewDefinition( PullRequestWriteToolName, - mcp.WithDescription("Write pull requests: create, update, close, reopen, merge, update branch from base, manage reviewers."), - mcp.WithToolAnnotation(annotation.Write("Create, update, close, reopen, or merge pull requests")), - mcp.WithString("method", mcp.Required(), mcp.Enum("create", "update", "close", "reopen", "merge", "update_branch", "add_reviewers", "remove_reviewers")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithNumber("pull_number", mcp.Description("required except for 'create'")), - mcp.WithString("title", mcp.Description("required for 'create'; optional for 'update'/'merge'")), - mcp.WithString("body", mcp.Description("required for 'create'; optional for 'update'")), - mcp.WithString("head", mcp.Description("head branch (required for 'create')")), - mcp.WithString("base", mcp.Description("base branch (required for 'create')")), - mcp.WithString("assignee", mcp.Description("for 'update'")), - mcp.WithArray("assignees", mcp.Description("for 'update'"), mcp.Items(map[string]any{"type": "string"})), - mcp.WithNumber("milestone", mcp.Description("for 'update'")), - mcp.WithString("state", mcp.Description("for 'update'"), mcp.Enum("open", "closed")), - mcp.WithBoolean("allow_maintainer_edit", mcp.Description("for 'update'")), - mcp.WithArray("labels", mcp.Description("label IDs"), mcp.Items(map[string]any{"type": "number"})), - mcp.WithString("deadline", mcp.Description("ISO 8601")), - mcp.WithBoolean("remove_deadline", mcp.Description("for 'update'")), - mcp.WithString("merge_style", mcp.Description("for 'merge'"), mcp.Enum("merge", "rebase", "rebase-merge", "squash", "fast-forward-only"), mcp.DefaultString("merge")), - mcp.WithString("message", mcp.Description("merge commit message or dismissal reason")), - mcp.WithBoolean("delete_branch", mcp.Description("for 'merge'")), - mcp.WithBoolean("force_merge", mcp.Description("merge even if checks fail")), - mcp.WithBoolean("merge_when_checks_succeed", mcp.Description("for 'merge'")), - mcp.WithString("head_commit_id", mcp.Description("expected head SHA for conflict detection")), - mcp.WithArray("reviewers", mcp.Description("for 'add_reviewers'/'remove_reviewers'"), mcp.Items(map[string]any{"type": "string"})), - mcp.WithArray("team_reviewers", mcp.Description("for 'add_reviewers'/'remove_reviewers'"), mcp.Items(map[string]any{"type": "string"})), - mcp.WithBoolean("draft", mcp.Description("uses 'WIP: ' title prefix")), + "Write pull requests: create, update, close, reopen, merge, update branch from base, manage reviewers.", + annotation.Write("Create, update, close, reopen, or merge pull requests"), + tool.String("method", tool.Required(), tool.Enum("create", "update", "close", "reopen", "merge", "update_branch", "add_reviewers", "remove_reviewers")), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.Number("pull_number", tool.Description("required except for 'create'")), + tool.String("title", tool.Description("required for 'create'; optional for 'update'/'merge'")), + tool.String("body", tool.Description("required for 'create'; optional for 'update'")), + tool.String("head", tool.Description("head branch (required for 'create')")), + tool.String("base", tool.Description("base branch (required for 'create')")), + tool.String("assignee", tool.Description("for 'update'")), + tool.Array("assignees", tool.Description("for 'update'"), tool.Items(map[string]any{"type": "string"})), + tool.Number("milestone", tool.Description("for 'update'")), + tool.String("state", tool.Description("for 'update'"), tool.Enum("open", "closed")), + tool.Boolean("allow_maintainer_edit", tool.Description("for 'update'")), + tool.Array("labels", tool.Description("label IDs"), tool.Items(map[string]any{"type": "number"})), + tool.String("deadline", tool.Description("ISO 8601")), + tool.Boolean("remove_deadline", tool.Description("for 'update'")), + tool.String("merge_style", tool.Description("for 'merge'"), tool.Enum("merge", "rebase", "rebase-merge", "squash", "fast-forward-only"), tool.Default("merge")), + tool.String("message", tool.Description("merge commit message or dismissal reason")), + tool.Boolean("delete_branch", tool.Description("for 'merge'")), + tool.Boolean("force_merge", tool.Description("merge even if checks fail")), + tool.Boolean("merge_when_checks_succeed", tool.Description("for 'merge'")), + tool.String("head_commit_id", tool.Description("expected head SHA for conflict detection")), + tool.Array("reviewers", tool.Description("for 'add_reviewers'/'remove_reviewers'"), tool.Items(map[string]any{"type": "string"})), + tool.Array("team_reviewers", tool.Description("for 'add_reviewers'/'remove_reviewers'"), tool.Items(map[string]any{"type": "string"})), + tool.Boolean("draft", tool.Description("uses 'WIP: ' title prefix")), ) - PullRequestReviewWriteTool = mcp.NewTool( + PullRequestReviewWriteTool = tool.NewDefinition( PullRequestReviewWriteToolName, - mcp.WithDescription("Write PR reviews: create, submit, delete, dismiss, reply to and resolve review comments."), - mcp.WithToolAnnotation(annotation.Write("Write pull request reviews")), - mcp.WithString("method", mcp.Required(), mcp.Enum("create", "submit", "delete", "dismiss", "reply_comment", "resolve_thread", "unresolve_thread")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithNumber("pull_number", mcp.Description("required except for 'resolve_thread'/'unresolve_thread'")), - mcp.WithNumber("review_id", mcp.Description("for 'submit'/'delete'/'dismiss'")), - mcp.WithNumber("comment_id", mcp.Description("comment ID from 'get_review_comments'; resolve takes the thread's first")), - mcp.WithString("state", mcp.Enum("APPROVED", "REQUEST_CHANGES", "COMMENT", "PENDING")), - mcp.WithString("body", mcp.Description("review body, or reply text for 'reply_comment'")), - mcp.WithString("commit_id", mcp.Description("for 'create'")), - mcp.WithString("message", mcp.Description("dismissal reason")), - mcp.WithArray("comments", mcp.Description("inline comments (for 'create')"), mcp.Items(map[string]any{ + "Write PR reviews: create, submit, delete, dismiss, reply to and resolve review comments.", + annotation.Write("Write pull request reviews"), + tool.String("method", tool.Required(), tool.Enum("create", "submit", "delete", "dismiss", "reply_comment", "resolve_thread", "unresolve_thread")), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.Number("pull_number", tool.Description("required except for 'resolve_thread'/'unresolve_thread'")), + tool.Number("review_id", tool.Description("for 'submit'/'delete'/'dismiss'")), + tool.Number("comment_id", tool.Description("comment ID from 'get_review_comments'; resolve takes the thread's first")), + tool.String("state", tool.Enum("APPROVED", "REQUEST_CHANGES", "COMMENT", "PENDING")), + tool.String("body", tool.Description("review body, or reply text for 'reply_comment'")), + tool.String("commit_id", tool.Description("for 'create'")), + tool.String("message", tool.Description("dismissal reason")), + tool.Array("comments", tool.Description("inline comments (for 'create')"), tool.Items(map[string]any{ "type": "object", "properties": map[string]any{ "path": map[string]any{"type": "string"}, @@ -114,86 +113,86 @@ var ( ) func init() { - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: ListRepoPullRequestsTool, Handler: listRepoPullRequestsFn, }) - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: PullRequestReadTool, Handler: pullRequestReadFn, }) - Tool.RegisterWrite(server.ServerTool{ + Tool.RegisterWrite(tool.ServerTool{ Tool: PullRequestWriteTool, Handler: pullRequestWriteFn, }) - Tool.RegisterWrite(server.ServerTool{ + Tool.RegisterWrite(tool.ServerTool{ Tool: PullRequestReviewWriteTool, Handler: pullRequestReviewWriteFn, }) } -func pullRequestReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := params.GetString(req.GetArguments(), "method") +func pullRequestReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "get": - return getPullRequestByIndexFn(ctx, req) + return getPullRequestByIndexFn(ctx, args) case "get_diff": - return getPullRequestDiffFn(ctx, req) + return getPullRequestDiffFn(ctx, args) case "get_files": - return getPullRequestFilesFn(ctx, req) + return getPullRequestFilesFn(ctx, args) case "get_status": - return getPullRequestStatusFn(ctx, req) + return getPullRequestStatusFn(ctx, args) case "get_reviews": - return listPullRequestReviewsFn(ctx, req) + return listPullRequestReviewsFn(ctx, args) case "get_review": - return getPullRequestReviewFn(ctx, req) + return getPullRequestReviewFn(ctx, args) case "get_review_comments": - return listPullRequestReviewCommentsFn(ctx, req) + return listPullRequestReviewCommentsFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func pullRequestWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := params.GetString(req.GetArguments(), "method") +func pullRequestWriteFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "create": - return createPullRequestFn(ctx, req) + return createPullRequestFn(ctx, args) case "update": - return editPullRequestFn(ctx, req) + return editPullRequestFn(ctx, args) case "close": - return closePullRequestFn(ctx, req) + return closePullRequestFn(ctx, args) case "reopen": - return reopenPullRequestFn(ctx, req) + return reopenPullRequestFn(ctx, args) case "merge": - return mergePullRequestFn(ctx, req) + return mergePullRequestFn(ctx, args) case "update_branch": - return updatePullRequestBranchFn(ctx, req) + return updatePullRequestBranchFn(ctx, args) case "add_reviewers": - return createPullRequestReviewerFn(ctx, req) + return createPullRequestReviewerFn(ctx, args) case "remove_reviewers": - return deletePullRequestReviewerFn(ctx, req) + return deletePullRequestReviewerFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func closePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func closePullRequestFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "pull_number") + index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } @@ -214,16 +213,16 @@ func closePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.TextResult(slimPullRequest(pr)) } -func reopenPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func reopenPullRequestFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "pull_number") + index, err := params.GetIndex(args, "pull_number") if err != nil { return to.ErrorResult(err) } @@ -244,33 +243,32 @@ func reopenPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal return to.TextResult(slimPullRequest(pr)) } -func pullRequestReviewWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := params.GetString(req.GetArguments(), "method") +func pullRequestReviewWriteFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "create": - return createPullRequestReviewFn(ctx, req) + return createPullRequestReviewFn(ctx, args) case "submit": - return submitPullRequestReviewFn(ctx, req) + return submitPullRequestReviewFn(ctx, args) case "delete": - return deletePullRequestReviewFn(ctx, req) + return deletePullRequestReviewFn(ctx, args) case "dismiss": - return dismissPullRequestReviewFn(ctx, req) + return dismissPullRequestReviewFn(ctx, args) case "reply_comment": - return replyPullRequestReviewCommentFn(ctx, req) + return replyPullRequestReviewCommentFn(ctx, args) case "resolve_thread": - return resolveReviewThreadFn(ctx, req) + return resolveReviewThreadFn(ctx, args) case "unresolve_thread": - return unresolveReviewThreadFn(ctx, req) + return unresolveReviewThreadFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func getPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func getPullRequestByIndexFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -305,8 +303,7 @@ func getPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp return to.TextResult(m) } -func getPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func getPullRequestDiffFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -335,8 +332,7 @@ func getPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca return to.TextResult(string(diffBytes)) } -func listRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func listRepoPullRequestsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -392,8 +388,7 @@ func applyDraftPrefix(title string, isDraft bool) string { return title } -func createPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func createPullRequestFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -447,8 +442,7 @@ func createPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal type reviewerOp func(client *gitea_sdk.PullRequestsService, ctx context.Context, owner, repo string, index int64, opt gitea_sdk.PullReviewRequestOptions) (*gitea_sdk.Response, error) -func pullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest, verb string, op reviewerOp) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func pullRequestReviewerFn(ctx context.Context, args map[string]any, verb string, op reviewerOp) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -486,16 +480,15 @@ func pullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest, verb st }) } -func createPullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return pullRequestReviewerFn(ctx, req, "create", (*gitea_sdk.PullRequestsService).CreateReviewRequests) +func createPullRequestReviewerFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + return pullRequestReviewerFn(ctx, args, "create", (*gitea_sdk.PullRequestsService).CreateReviewRequests) } -func deletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return pullRequestReviewerFn(ctx, req, "delete", (*gitea_sdk.PullRequestsService).DeleteReviewRequests) +func deletePullRequestReviewerFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + return pullRequestReviewerFn(ctx, args, "delete", (*gitea_sdk.PullRequestsService).DeleteReviewRequests) } -func listPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func listPullRequestReviewsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -528,8 +521,7 @@ func listPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mc return to.TextResult(slimReviews(reviews)) } -func getPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func getPullRequestReviewFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -560,8 +552,7 @@ func getPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp. return to.TextResult(slimReview(review)) } -func listPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func listPullRequestReviewCommentsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -612,8 +603,7 @@ func listPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolReques return to.TextResult(slimReviewComments(comments)) } -func createPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func createPullRequestReviewFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -676,8 +666,7 @@ func createPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(slimReview(review)) } -func submitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func submitPullRequestReviewFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -719,8 +708,7 @@ func submitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(slimReview(review)) } -func deletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func deletePullRequestReviewFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -758,8 +746,7 @@ func deletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(successMsg) } -func dismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func dismissPullRequestReviewFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -802,8 +789,7 @@ func dismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (* return to.TextResult(successMsg) } -func replyPullRequestReviewCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func replyPullRequestReviewCommentFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -840,16 +826,15 @@ func replyPullRequestReviewCommentFn(ctx context.Context, req mcp.CallToolReques return to.TextResult(slimReviewComment(comment)) } -func resolveReviewThreadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return setReviewThreadResolvedFn(ctx, req, true) +func resolveReviewThreadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + return setReviewThreadResolvedFn(ctx, args, true) } -func unresolveReviewThreadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return setReviewThreadResolvedFn(ctx, req, false) +func unresolveReviewThreadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + return setReviewThreadResolvedFn(ctx, args, false) } -func setReviewThreadResolvedFn(ctx context.Context, req mcp.CallToolRequest, resolved bool) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func setReviewThreadResolvedFn(ctx context.Context, args map[string]any, resolved bool) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -887,8 +872,7 @@ func setReviewThreadResolvedFn(ctx context.Context, req mcp.CallToolRequest, res return to.TextResult(successMsg) } -func mergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func mergePullRequestFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -951,8 +935,7 @@ func mergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.TextResult(successMsg) } -func editPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func editPullRequestFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -1026,8 +1009,7 @@ func editPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.TextResult(slimPullRequest(pr)) } -func updatePullRequestBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func updatePullRequestBranchFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -1048,8 +1030,7 @@ func updatePullRequestBranchFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(map[string]any{"message": "branch updated from base"}) } -func getPullRequestFilesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func getPullRequestFilesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -1076,8 +1057,7 @@ func getPullRequestFilesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.C return to.TextResult(files) } -func getPullRequestStatusFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func getPullRequestStatusFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) diff --git a/operation/pull/pull_test.go b/operation/pull/pull_test.go index 769fc82..e3d3398 100644 --- a/operation/pull/pull_test.go +++ b/operation/pull/pull_test.go @@ -12,7 +12,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/flag" - "github.com/mark3labs/mcp-go/mcp" + "github.com/modelcontextprotocol/go-sdk/mcp" ) func Test_editPullRequestFn(t *testing.T) { @@ -77,19 +77,15 @@ func Test_editPullRequestFn(t *testing.T) { flag.Version = origVersion }() - req := mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Arguments: map[string]any{ - "owner": owner, - "repo": repo, - "pull_number": ii.val, - "title": "WIP: my feature", - "state": "open", - }, - }, + args := map[string]any{ + "owner": owner, + "repo": repo, + "pull_number": ii.val, + "title": "WIP: my feature", + "state": "open", } - result, err := editPullRequestFn(context.Background(), req) + result, err := editPullRequestFn(context.Background(), args) if err != nil { t.Fatalf("editPullRequestFn() error = %v", err) } @@ -113,7 +109,7 @@ func Test_editPullRequestFn(t *testing.T) { if len(result.Content) == 0 { t.Fatalf("expected content in result") } - textContent, ok := mcp.AsTextContent(result.Content[0]) + textContent, ok := result.Content[0].(*mcp.TextContent) if !ok { t.Fatalf("expected text content, got %T", result.Content[0]) } @@ -193,21 +189,17 @@ func Test_mergePullRequestFn(t *testing.T) { flag.Version = origVersion }() - req := mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Arguments: map[string]any{ - "owner": owner, - "repo": repo, - "pull_number": ii.val, - "merge_style": "squash", - "title": "feat: my squashed commit", - "message": "Squash merge of PR #5", - "delete_branch": true, - }, - }, + args := map[string]any{ + "owner": owner, + "repo": repo, + "pull_number": ii.val, + "merge_style": "squash", + "title": "feat: my squashed commit", + "message": "Squash merge of PR #5", + "delete_branch": true, } - result, err := mergePullRequestFn(context.Background(), req) + result, err := mergePullRequestFn(context.Background(), args) if err != nil { t.Fatalf("mergePullRequestFn() error = %v", err) } @@ -237,7 +229,7 @@ func Test_mergePullRequestFn(t *testing.T) { if len(result.Content) == 0 { t.Fatalf("expected content in result") } - textContent, ok := mcp.AsTextContent(result.Content[0]) + textContent, ok := result.Content[0].(*mcp.TextContent) if !ok { t.Fatalf("expected text content, got %T", result.Content[0]) } @@ -306,21 +298,17 @@ func Test_mergePullRequestFn_newParams(t *testing.T) { flag.Version = origVersion }() - req := mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Arguments: map[string]any{ - "owner": owner, - "repo": repo, - "pull_number": float64(index), - "merge_style": "merge", - "force_merge": true, - "merge_when_checks_succeed": true, - "head_commit_id": "abc123", - }, - }, + args := map[string]any{ + "owner": owner, + "repo": repo, + "pull_number": float64(index), + "merge_style": "merge", + "force_merge": true, + "merge_when_checks_succeed": true, + "head_commit_id": "abc123", } - _, err := mergePullRequestFn(context.Background(), req) + _, err := mergePullRequestFn(context.Background(), args) if err != nil { t.Fatalf("mergePullRequestFn() error = %v", err) } @@ -386,22 +374,18 @@ func Test_createPullRequestFn_labels(t *testing.T) { flag.Version = origVersion }() - req := mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Arguments: map[string]any{ - "owner": owner, - "repo": repo, - "title": "test", - "body": "body", - "head": "feature", - "base": "main", - "labels": []any{float64(1), float64(2)}, - "deadline": "2026-06-01T00:00:00Z", - }, - }, + args := map[string]any{ + "owner": owner, + "repo": repo, + "title": "test", + "body": "body", + "head": "feature", + "base": "main", + "labels": []any{float64(1), float64(2)}, + "deadline": "2026-06-01T00:00:00Z", } - _, err := createPullRequestFn(context.Background(), req) + _, err := createPullRequestFn(context.Background(), args) if err != nil { t.Fatalf("createPullRequestFn() error = %v", err) } @@ -525,13 +509,7 @@ func Test_createPullRequestFn_draft(t *testing.T) { args["draft"] = tc.draft } - req := mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Arguments: args, - }, - } - - _, err := createPullRequestFn(context.Background(), req) + _, err := createPullRequestFn(context.Background(), args) if err != nil { t.Fatalf("createPullRequestFn() error = %v", err) } @@ -630,13 +608,7 @@ func Test_editPullRequestFn_draft(t *testing.T) { args["draft"] = tc.draft } - req := mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Arguments: args, - }, - } - - _, err := editPullRequestFn(context.Background(), req) + _, err := editPullRequestFn(context.Background(), args) if err != nil { t.Fatalf("editPullRequestFn() error = %v", err) } @@ -720,18 +692,14 @@ func Test_getPullRequestDiffFn(t *testing.T) { flag.Version = origVersion }() - req := mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Arguments: map[string]any{ - "owner": owner, - "repo": repo, - "pull_number": ii.val, - "binary": true, - }, - }, + args := map[string]any{ + "owner": owner, + "repo": repo, + "pull_number": ii.val, + "binary": true, } - result, err := getPullRequestDiffFn(context.Background(), req) + result, err := getPullRequestDiffFn(context.Background(), args) if err != nil { t.Fatalf("getPullRequestDiffFn() error = %v", err) } @@ -758,7 +726,7 @@ func Test_getPullRequestDiffFn(t *testing.T) { t.Fatalf("expected content in result") } - textContent, ok := mcp.AsTextContent(result.Content[0]) + textContent, ok := result.Content[0].(*mcp.TextContent) if !ok { t.Fatalf("expected text content, got %T", result.Content[0]) } @@ -807,17 +775,17 @@ func Test_getPullRequestByIndexFn_includesAttachments(t *testing.T) { flag.Host, flag.Token, flag.Version = server.URL, "", "test" defer func() { flag.Host, flag.Token, flag.Version = origHost, origToken, origVersion }() - req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: map[string]any{ + args := map[string]any{ "owner": owner, "repo": repo, "pull_number": float64(index), - }}} - res, err := getPullRequestByIndexFn(context.Background(), req) + } + res, err := getPullRequestByIndexFn(context.Background(), args) if err != nil { t.Fatalf("getPullRequestByIndexFn() error = %v", err) } if res.IsError { t.Fatalf("unexpected error result: %v", res.Content) } - body := res.Content[0].(mcp.TextContent).Text + body := res.Content[0].(*mcp.TextContent).Text if !strings.Contains(body, `[shot.png](https://example/shot.png)`) { t.Fatalf("expected attachment markdown inlined in body, got: %s", body) } @@ -855,14 +823,14 @@ func Test_getPullRequestByIndexFn_emptyAssetsLeavesBody(t *testing.T) { flag.Host, flag.Token, flag.Version = server.URL, "", "test" defer func() { flag.Host, flag.Token, flag.Version = origHost, origToken, origVersion }() - req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: map[string]any{ + args := map[string]any{ "owner": owner, "repo": repo, "pull_number": float64(index), - }}} - res, err := getPullRequestByIndexFn(context.Background(), req) + } + res, err := getPullRequestByIndexFn(context.Background(), args) if err != nil { t.Fatalf("getPullRequestByIndexFn() error = %v", err) } - body := res.Content[0].(mcp.TextContent).Text + body := res.Content[0].(*mcp.TextContent).Text if !strings.Contains(body, `"body":"plain body"`) { t.Fatalf("expected body unchanged when assets are empty, got: %s", body) } @@ -899,17 +867,17 @@ func Test_getPullRequestByIndexFn_assetsFailureNonFatal(t *testing.T) { flag.Host, flag.Token, flag.Version = server.URL, "", "test" defer func() { flag.Host, flag.Token, flag.Version = origHost, origToken, origVersion }() - req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: map[string]any{ + args := map[string]any{ "owner": owner, "repo": repo, "pull_number": float64(index), - }}} - res, err := getPullRequestByIndexFn(context.Background(), req) + } + res, err := getPullRequestByIndexFn(context.Background(), args) if err != nil { t.Fatalf("getPullRequestByIndexFn() error = %v", err) } if res.IsError { t.Fatalf("assets fetch failure should not fail the PR fetch: %v", res.Content) } - body := res.Content[0].(mcp.TextContent).Text + body := res.Content[0].(*mcp.TextContent).Text if !strings.Contains(body, `"plain body"`) { t.Fatalf("expected PR body preserved when assets fail, got: %s", body) } @@ -954,18 +922,14 @@ func Test_closePullRequestFn(t *testing.T) { flag.Token = "test-token" t.Cleanup(func() { flag.Host = origHost; flag.Token = origToken }) - req := mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Arguments: map[string]any{ - "method": "close", - "owner": owner, - "repo": repo, - "pull_number": float64(index), - }, - }, + args := map[string]any{ + "method": "close", + "owner": owner, + "repo": repo, + "pull_number": float64(index), } - result, err := closePullRequestFn(context.Background(), req) + result, err := closePullRequestFn(context.Background(), args) if err != nil { t.Fatalf("closePullRequestFn() error = %v", err) } @@ -1018,18 +982,14 @@ func Test_reopenPullRequestFn(t *testing.T) { flag.Token = "test-token" t.Cleanup(func() { flag.Host = origHost; flag.Token = origToken }) - req := mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Arguments: map[string]any{ - "method": "reopen", - "owner": owner, - "repo": repo, - "pull_number": float64(index), - }, - }, + args := map[string]any{ + "method": "reopen", + "owner": owner, + "repo": repo, + "pull_number": float64(index), } - result, err := reopenPullRequestFn(context.Background(), req) + result, err := reopenPullRequestFn(context.Background(), args) if err != nil { t.Fatalf("reopenPullRequestFn() error = %v", err) } @@ -1103,20 +1063,16 @@ func Test_pullRequestReviewWriteFn_comments(t *testing.T) { _, _ = w.Write([]byte(`{"id":43,"body":"sure","path":"main.go","position":3}`)) }) - req := mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Arguments: map[string]any{ - "method": tc.method, - "owner": owner, - "repo": repo, - "pull_number": float64(index), - "comment_id": float64(commentID), - "body": "sure", - }, - }, + args := map[string]any{ + "method": tc.method, + "owner": owner, + "repo": repo, + "pull_number": float64(index), + "comment_id": float64(commentID), + "body": "sure", } - result, err := pullRequestReviewWriteFn(context.Background(), req) + result, err := pullRequestReviewWriteFn(context.Background(), args) if err != nil { t.Fatalf("pullRequestReviewWriteFn() error = %v", err) } @@ -1162,18 +1118,14 @@ func Test_listPullRequestReviewCommentsFn_allReviews(t *testing.T) { } }) - req := mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Arguments: map[string]any{ - "method": "get_review_comments", - "owner": owner, - "repo": repo, - "pull_number": float64(index), - }, - }, + args := map[string]any{ + "method": "get_review_comments", + "owner": owner, + "repo": repo, + "pull_number": float64(index), } - result, err := pullRequestReadFn(context.Background(), req) + result, err := pullRequestReadFn(context.Background(), args) if err != nil { t.Fatalf("pullRequestReadFn() error = %v", err) } diff --git a/operation/repo/branch.go b/operation/repo/branch.go index d40254c..461752f 100644 --- a/operation/repo/branch.go +++ b/operation/repo/branch.go @@ -11,8 +11,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) // BranchTool holds the branch-related tools (scope "branch"). @@ -25,53 +24,52 @@ const ( ) var ( - CreateBranchTool = mcp.NewTool( + CreateBranchTool = tool.NewDefinition( CreateBranchToolName, - mcp.WithDescription("Create a new branch in a repository, optionally from a specific source branch (defaults to the repository's default branch)."), - mcp.WithToolAnnotation(annotation.Write("Create a new branch")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("branch", mcp.Required()), - mcp.WithString("old_branch", mcp.Description("source branch (default: repo default)")), + "Create a new branch in a repository, optionally from a specific source branch (defaults to the repository's default branch).", + annotation.Write("Create a new branch"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("branch", tool.Required()), + tool.String("old_branch", tool.Description("source branch (default: repo default)")), ) - DeleteBranchTool = mcp.NewTool( + DeleteBranchTool = tool.NewDefinition( DeleteBranchToolName, - mcp.WithDescription("Permanently delete a branch from a repository. This action is destructive and cannot be undone."), - mcp.WithToolAnnotation(annotation.Destructive("Delete a branch")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("branch", mcp.Required()), + "Permanently delete a branch from a repository. This action is destructive and cannot be undone.", + annotation.Destructive("Delete a branch"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("branch", tool.Required()), ) - ListBranchesTool = mcp.NewTool( + ListBranchesTool = tool.NewDefinition( ListBranchesToolName, - mcp.WithDescription("List all branches in a repository, paginated."), - mcp.WithToolAnnotation(annotation.ReadOnly("List repository branches")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), + "List all branches in a repository, paginated.", + annotation.ReadOnly("List repository branches"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) ) func init() { - BranchTool.RegisterWrite(server.ServerTool{ + BranchTool.RegisterWrite(tool.ServerTool{ Tool: CreateBranchTool, Handler: CreateBranchFn, }) - BranchTool.RegisterWrite(server.ServerTool{ + BranchTool.RegisterWrite(tool.ServerTool{ Tool: DeleteBranchTool, Handler: DeleteBranchFn, }) - BranchTool.RegisterRead(server.ServerTool{ + BranchTool.RegisterRead(tool.ServerTool{ Tool: ListBranchesTool, Handler: ListBranchesFn, }) } -func CreateBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func CreateBranchFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -101,8 +99,7 @@ func CreateBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool return to.TextResult("Branch Created") } -func DeleteBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func DeleteBranchFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -127,8 +124,7 @@ func DeleteBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool return to.TextResult("Branch Deleted") } -func ListBranchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func ListBranchesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) diff --git a/operation/repo/commit.go b/operation/repo/commit.go index 6abc895..8907ea7 100644 --- a/operation/repo/commit.go +++ b/operation/repo/commit.go @@ -11,8 +11,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) // CommitTool holds the commit-related tools (scope "commit"). @@ -24,41 +23,40 @@ const ( ) var ( - ListRepoCommitsTool = mcp.NewTool( + ListRepoCommitsTool = tool.NewDefinition( ListRepoCommitsToolName, - mcp.WithDescription("List commits in a repository, optionally starting from a specific branch or SHA and filtered to commits touching a given file path."), - mcp.WithToolAnnotation(annotation.ReadOnly("List repository commits")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("sha", mcp.Description("starting SHA or branch")), - mcp.WithString("path", mcp.Description("only commits touching this path")), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30), mcp.Min(1)), + "List commits in a repository, optionally starting from a specific branch or SHA and filtered to commits touching a given file path.", + annotation.ReadOnly("List repository commits"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("sha", tool.Description("starting SHA or branch")), + tool.String("path", tool.Description("only commits touching this path")), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1), tool.Minimum(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30), tool.Minimum(1)), ) - GetCommitTool = mcp.NewTool( + GetCommitTool = tool.NewDefinition( GetCommitToolName, - mcp.WithDescription("Get details for a single commit in a repository by its SHA."), - mcp.WithToolAnnotation(annotation.ReadOnly("Get commit details")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("sha", mcp.Required()), + "Get details for a single commit in a repository by its SHA.", + annotation.ReadOnly("Get commit details"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("sha", tool.Required()), ) ) func init() { - CommitTool.RegisterRead(server.ServerTool{ + CommitTool.RegisterRead(tool.ServerTool{ Tool: ListRepoCommitsTool, Handler: ListRepoCommitsFn, }) - CommitTool.RegisterRead(server.ServerTool{ + CommitTool.RegisterRead(tool.ServerTool{ Tool: GetCommitTool, Handler: GetCommitFn, }) } -func ListRepoCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func ListRepoCommitsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -89,8 +87,7 @@ func ListRepoCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.TextResult(slimCommits(commits)) } -func GetCommitFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func GetCommitFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) diff --git a/operation/repo/file.go b/operation/repo/file.go index a04af73..9a9d7a2 100644 --- a/operation/repo/file.go +++ b/operation/repo/file.go @@ -15,8 +15,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) // FileTool holds the file-related tools (scope "file"). @@ -30,68 +29,68 @@ const ( ) var ( - GetFileContentTool = mcp.NewTool( + GetFileContentTool = tool.NewDefinition( GetFileToolName, - mcp.WithDescription("Get file content and metadata"), - mcp.WithToolAnnotation(annotation.ReadOnly("Get file content")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("ref", mcp.Required(), mcp.Description("branch, tag, or commit SHA")), - mcp.WithString("path", mcp.Required()), - mcp.WithBoolean("withLines", mcp.Description("return numbered lines")), + "Get file content and metadata", + annotation.ReadOnly("Get file content"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("ref", tool.Required(), tool.Description("branch, tag, or commit SHA")), + tool.String("path", tool.Required()), + tool.Boolean("withLines", tool.Description("return numbered lines")), ) - GetDirContentTool = mcp.NewTool( + GetDirContentTool = tool.NewDefinition( GetDirToolName, - mcp.WithDescription("List the entries (files and subdirectories) in a repository directory at a given ref (branch, tag, or commit SHA)."), - mcp.WithToolAnnotation(annotation.ReadOnly("Get directory contents")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("ref", mcp.Required(), mcp.Description("branch, tag, or commit SHA")), - mcp.WithString("path", mcp.Required()), + "List the entries (files and subdirectories) in a repository directory at a given ref (branch, tag, or commit SHA).", + annotation.ReadOnly("Get directory contents"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("ref", tool.Required(), tool.Description("branch, tag, or commit SHA")), + tool.String("path", tool.Required()), ) - CreateOrUpdateFileTool = mcp.NewTool( + CreateOrUpdateFileTool = tool.NewDefinition( CreateOrUpdateFileToolName, - mcp.WithDescription("Create or update a file (provide sha to update an existing file)."), - mcp.WithToolAnnotation(annotation.Write("Create or update a file")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("path", mcp.Required()), - mcp.WithString("content", mcp.Required()), - mcp.WithString("message", mcp.Required(), mcp.Description("commit message")), - mcp.WithString("branch_name", mcp.Required()), - mcp.WithString("sha", mcp.Description("existing file SHA (omit to create)")), - mcp.WithString("new_branch_name", mcp.Description("new branch (create only)")), + "Create or update a file (provide sha to update an existing file).", + annotation.Write("Create or update a file"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("path", tool.Required()), + tool.String("content", tool.Required()), + tool.String("message", tool.Required(), tool.Description("commit message")), + tool.String("branch_name", tool.Required()), + tool.String("sha", tool.Description("existing file SHA (omit to create)")), + tool.String("new_branch_name", tool.Description("new branch (create only)")), ) - DeleteFileTool = mcp.NewTool( + DeleteFileTool = tool.NewDefinition( DeleteFileToolName, - mcp.WithDescription("Delete a file from a repository by committing the removal to a branch. Requires the file's current SHA and a commit message."), - mcp.WithToolAnnotation(annotation.Destructive("Delete a file")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("path", mcp.Required()), - mcp.WithString("message", mcp.Required(), mcp.Description("commit message")), - mcp.WithString("branch_name", mcp.Required()), - mcp.WithString("sha", mcp.Required()), + "Delete a file from a repository by committing the removal to a branch. Requires the file's current SHA and a commit message.", + annotation.Destructive("Delete a file"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("path", tool.Required()), + tool.String("message", tool.Required(), tool.Description("commit message")), + tool.String("branch_name", tool.Required()), + tool.String("sha", tool.Required()), ) ) func init() { - FileTool.RegisterRead(server.ServerTool{ + FileTool.RegisterRead(tool.ServerTool{ Tool: GetFileContentTool, Handler: GetFileContentFn, }) - FileTool.RegisterRead(server.ServerTool{ + FileTool.RegisterRead(tool.ServerTool{ Tool: GetDirContentTool, Handler: GetDirContentFn, }) - FileTool.RegisterWrite(server.ServerTool{ + FileTool.RegisterWrite(tool.ServerTool{ Tool: CreateOrUpdateFileTool, Handler: CreateOrUpdateFileFn, }) - FileTool.RegisterWrite(server.ServerTool{ + FileTool.RegisterWrite(tool.ServerTool{ Tool: DeleteFileTool, Handler: DeleteFileFn, }) @@ -102,8 +101,7 @@ type ContentLine struct { Content string `json:"content"` } -func GetFileContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func GetFileContentFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -165,8 +163,7 @@ func GetFileContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(slimContents(content)) } -func GetDirContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func GetDirContentFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -191,8 +188,7 @@ func GetDirContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult(slimDirEntries(content)) } -func CreateOrUpdateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func CreateOrUpdateFileFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -250,8 +246,7 @@ func CreateOrUpdateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca return to.TextResult("Create file success") } -func DeleteFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func DeleteFileFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) diff --git a/operation/repo/release.go b/operation/repo/release.go index b632025..59b5882 100644 --- a/operation/repo/release.go +++ b/operation/repo/release.go @@ -11,8 +11,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) // ReleaseTool holds the release-related tools (scope "release"). @@ -27,84 +26,83 @@ const ( ) var ( - CreateReleaseTool = mcp.NewTool( + CreateReleaseTool = tool.NewDefinition( CreateReleaseToolName, - mcp.WithDescription("Create a new release in a repository from a tag, optionally marking it as a draft or pre-release."), - mcp.WithToolAnnotation(annotation.Write("Create a release")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("tag_name", mcp.Required()), - mcp.WithString("target", mcp.Required(), mcp.Description("commitish")), - mcp.WithString("title", mcp.Required()), - mcp.WithBoolean("is_draft"), - mcp.WithBoolean("is_pre_release"), - mcp.WithString("body"), + "Create a new release in a repository from a tag, optionally marking it as a draft or pre-release.", + annotation.Write("Create a release"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("tag_name", tool.Required()), + tool.String("target", tool.Required(), tool.Description("commitish")), + tool.String("title", tool.Required()), + tool.Boolean("is_draft"), + tool.Boolean("is_pre_release"), + tool.String("body"), ) - DeleteReleaseTool = mcp.NewTool( + DeleteReleaseTool = tool.NewDefinition( DeleteReleaseToolName, - mcp.WithDescription("Delete a release from a repository by its numeric ID. This action is destructive and cannot be undone."), - mcp.WithToolAnnotation(annotation.Destructive("Delete a release")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithNumber("id", mcp.Required()), + "Delete a release from a repository by its numeric ID. This action is destructive and cannot be undone.", + annotation.Destructive("Delete a release"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.Number("id", tool.Required()), ) - GetReleaseTool = mcp.NewTool( + GetReleaseTool = tool.NewDefinition( GetReleaseToolName, - mcp.WithDescription("Get a release by ID"), - mcp.WithToolAnnotation(annotation.ReadOnly("Get release details")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithNumber("id", mcp.Required()), + "Get a release by ID", + annotation.ReadOnly("Get release details"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.Number("id", tool.Required()), ) - GetLatestReleaseTool = mcp.NewTool( + GetLatestReleaseTool = tool.NewDefinition( GetLatestReleaseToolName, - mcp.WithDescription("Get the most recent published (non-draft) release in a repository."), - mcp.WithToolAnnotation(annotation.ReadOnly("Get latest release")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), + "Get the most recent published (non-draft) release in a repository.", + annotation.ReadOnly("Get latest release"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), ) - ListReleasesTool = mcp.NewTool( + ListReleasesTool = tool.NewDefinition( ListReleasesToolName, - mcp.WithDescription("List releases in a repository, optionally filtered to drafts or pre-releases."), - mcp.WithToolAnnotation(annotation.ReadOnly("List releases")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithBoolean("is_draft"), - mcp.WithBoolean("is_pre_release"), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(20), mcp.Min(1)), + "List releases in a repository, optionally filtered to drafts or pre-releases.", + annotation.ReadOnly("List releases"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.Boolean("is_draft"), + tool.Boolean("is_pre_release"), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1), tool.Minimum(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(20), tool.Minimum(1)), ) ) func init() { - ReleaseTool.RegisterWrite(server.ServerTool{ + ReleaseTool.RegisterWrite(tool.ServerTool{ Tool: CreateReleaseTool, Handler: CreateReleaseFn, }) - ReleaseTool.RegisterWrite(server.ServerTool{ + ReleaseTool.RegisterWrite(tool.ServerTool{ Tool: DeleteReleaseTool, Handler: DeleteReleaseFn, }) - ReleaseTool.RegisterRead(server.ServerTool{ + ReleaseTool.RegisterRead(tool.ServerTool{ Tool: GetReleaseTool, Handler: GetReleaseFn, }) - ReleaseTool.RegisterRead(server.ServerTool{ + ReleaseTool.RegisterRead(tool.ServerTool{ Tool: GetLatestReleaseTool, Handler: GetLatestReleaseFn, }) - ReleaseTool.RegisterRead(server.ServerTool{ + ReleaseTool.RegisterRead(tool.ServerTool{ Tool: ListReleasesTool, Handler: ListReleasesFn, }) } -func CreateReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func CreateReleaseFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -148,8 +146,7 @@ func CreateReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult("Release Created") } -func DeleteReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func DeleteReleaseFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -175,8 +172,7 @@ func DeleteReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult("Release deleted successfully") } -func GetReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func GetReleaseFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -202,8 +198,7 @@ func GetReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe return to.TextResult(slimRelease(release)) } -func GetLatestReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func GetLatestReleaseFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -225,8 +220,7 @@ func GetLatestReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.TextResult(slimRelease(release)) } -func ListReleasesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func ListReleasesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) diff --git a/operation/repo/repo.go b/operation/repo/repo.go index 1183859..5338924 100644 --- a/operation/repo/repo.go +++ b/operation/repo/repo.go @@ -12,8 +12,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) var Tool = tool.New("repository") @@ -26,74 +25,73 @@ const ( ) var ( - CreateRepoTool = mcp.NewTool( + CreateRepoTool = tool.NewDefinition( CreateRepoToolName, - mcp.WithDescription("Create a new Git repository, optionally under an organization (defaults to the authenticated user's account), with options for visibility, template, license, .gitignore, and initial README."), - mcp.WithToolAnnotation(annotation.Write("Create a new repository")), - mcp.WithString("name", mcp.Required()), - mcp.WithString("description"), - mcp.WithBoolean("private"), - mcp.WithString("issue_labels"), - mcp.WithBoolean("auto_init"), - mcp.WithBoolean("template"), - mcp.WithString("gitignores"), - mcp.WithString("license"), - mcp.WithString("readme"), - mcp.WithString("default_branch"), - mcp.WithString("trust_model", mcp.Enum("default", "collaborator", "committer", "collaboratorcommitter")), - mcp.WithString("object_format_name", mcp.Enum("sha1", "sha256")), - mcp.WithString("organization", mcp.Description("defaults to personal account")), + "Create a new Git repository, optionally under an organization (defaults to the authenticated user's account), with options for visibility, template, license, .gitignore, and initial README.", + annotation.Write("Create a new repository"), + tool.String("name", tool.Required()), + tool.String("description"), + tool.Boolean("private"), + tool.String("issue_labels"), + tool.Boolean("auto_init"), + tool.Boolean("template"), + tool.String("gitignores"), + tool.String("license"), + tool.String("readme"), + tool.String("default_branch"), + tool.String("trust_model", tool.Enum("default", "collaborator", "committer", "collaboratorcommitter")), + tool.String("object_format_name", tool.Enum("sha1", "sha256")), + tool.String("organization", tool.Description("defaults to personal account")), ) - ForkRepoTool = mcp.NewTool( + ForkRepoTool = tool.NewDefinition( ForkRepoToolName, - mcp.WithDescription("Fork an existing repository into the authenticated user's account or a target organization, optionally under a new name."), - mcp.WithToolAnnotation(annotation.Write("Fork a repository")), - mcp.WithString("user", mcp.Required(), mcp.Description("owner of source repo")), - mcp.WithString("repo", mcp.Required()), - mcp.WithString("organization", mcp.Description("target org")), - mcp.WithString("name", mcp.Description("fork name")), + "Fork an existing repository into the authenticated user's account or a target organization, optionally under a new name.", + annotation.Write("Fork a repository"), + tool.String("user", tool.Required(), tool.Description("owner of source repo")), + tool.String("repo", tool.Required()), + tool.String("organization", tool.Description("target org")), + tool.String("name", tool.Description("fork name")), ) - ListMyReposTool = mcp.NewTool( + ListMyReposTool = tool.NewDefinition( ListMyReposToolName, - mcp.WithDescription("List repositories owned by the authenticated user."), - mcp.WithToolAnnotation(annotation.ReadOnly("List my repositories")), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30), mcp.Min(1)), + "List repositories owned by the authenticated user.", + annotation.ReadOnly("List my repositories"), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1), tool.Minimum(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30), tool.Minimum(1)), ) - ListOrgReposTool = mcp.NewTool( + ListOrgReposTool = tool.NewDefinition( ListOrgReposToolName, - mcp.WithDescription("List repositories belonging to an organization."), - mcp.WithToolAnnotation(annotation.ReadOnly("List organization repositories")), - mcp.WithString("org", mcp.Required()), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(100), mcp.Min(1)), + "List repositories belonging to an organization.", + annotation.ReadOnly("List organization repositories"), + tool.String("org", tool.Required()), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1), tool.Minimum(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(100), tool.Minimum(1)), ) ) func init() { - Tool.RegisterWrite(server.ServerTool{ + Tool.RegisterWrite(tool.ServerTool{ Tool: CreateRepoTool, Handler: CreateRepoFn, }) - Tool.RegisterWrite(server.ServerTool{ + Tool.RegisterWrite(tool.ServerTool{ Tool: ForkRepoTool, Handler: ForkRepoFn, }) - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: ListMyReposTool, Handler: ListMyReposFn, }) - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: ListOrgReposTool, Handler: ListOrgReposFn, }) } -func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func CreateRepoFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { name, err := params.GetString(args, "name") if err != nil { return to.ErrorResult(err) @@ -145,8 +143,7 @@ func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe return to.TextResult(slim.Repo(repo)) } -func ForkRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func ForkRepoFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { user, err := params.GetString(args, "user") if err != nil { return to.ErrorResult(err) @@ -170,8 +167,8 @@ func ForkRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResu return to.TextResult("Fork success") } -func ListMyReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - page, pageSize := params.GetPagination(req.GetArguments(), 30) +func ListMyReposFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + page, pageSize := params.GetPagination(args, 30) opt := gitea_sdk.ListReposOptions{ ListOptions: gitea_sdk.ListOptions{ Page: page, @@ -190,12 +187,12 @@ func ListMyReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR return to.TextResult(slim.Repos(repos)) } -func ListOrgReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := params.GetString(req.GetArguments(), "org") +func ListOrgReposFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + org, err := params.GetString(args, "org") if err != nil { return to.ErrorResult(err) } - page, pageSize := params.GetPagination(req.GetArguments(), 100) + page, pageSize := params.GetPagination(args, 100) opt := gitea_sdk.ListOrgReposOptions{ ListOptions: gitea_sdk.ListOptions{ Page: page, diff --git a/operation/repo/tag.go b/operation/repo/tag.go index ba6f176..43d9686 100644 --- a/operation/repo/tag.go +++ b/operation/repo/tag.go @@ -11,8 +11,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) // TagTool holds the tag-related tools (scope "tag"). @@ -26,67 +25,66 @@ const ( ) var ( - CreateTagTool = mcp.NewTool( + CreateTagTool = tool.NewDefinition( CreateTagToolName, - mcp.WithDescription("Create a new Git tag in a repository at a target commit, branch, or existing tag, with an optional annotation message."), - mcp.WithToolAnnotation(annotation.Write("Create a tag")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("tag_name", mcp.Required()), - mcp.WithString("target", mcp.Description("commitish")), - mcp.WithString("message", mcp.Description("tag message")), + "Create a new Git tag in a repository at a target commit, branch, or existing tag, with an optional annotation message.", + annotation.Write("Create a tag"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("tag_name", tool.Required()), + tool.String("target", tool.Description("commitish")), + tool.String("message", tool.Description("tag message")), ) - DeleteTagTool = mcp.NewTool( + DeleteTagTool = tool.NewDefinition( DeleteTagToolName, - mcp.WithDescription("Permanently delete a tag from a repository. This action is destructive and cannot be undone."), - mcp.WithToolAnnotation(annotation.Destructive("Delete a tag")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("tag_name", mcp.Required()), + "Permanently delete a tag from a repository. This action is destructive and cannot be undone.", + annotation.Destructive("Delete a tag"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("tag_name", tool.Required()), ) - GetTagTool = mcp.NewTool( + GetTagTool = tool.NewDefinition( GetTagToolName, - mcp.WithDescription("Get details for a single tag in a repository by name."), - mcp.WithToolAnnotation(annotation.ReadOnly("Get tag details")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("tag_name", mcp.Required()), + "Get details for a single tag in a repository by name.", + annotation.ReadOnly("Get tag details"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("tag_name", tool.Required()), ) - ListTagsTool = mcp.NewTool( + ListTagsTool = tool.NewDefinition( ListTagsToolName, - mcp.WithDescription("List all tags in a repository, paginated."), - mcp.WithToolAnnotation(annotation.ReadOnly("List tags")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(20), mcp.Min(1)), + "List all tags in a repository, paginated.", + annotation.ReadOnly("List tags"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1), tool.Minimum(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(20), tool.Minimum(1)), ) ) func init() { - TagTool.RegisterWrite(server.ServerTool{ + TagTool.RegisterWrite(tool.ServerTool{ Tool: CreateTagTool, Handler: CreateTagFn, }) - TagTool.RegisterWrite(server.ServerTool{ + TagTool.RegisterWrite(tool.ServerTool{ Tool: DeleteTagTool, Handler: DeleteTagFn, }) - TagTool.RegisterRead(server.ServerTool{ + TagTool.RegisterRead(tool.ServerTool{ Tool: GetTagTool, Handler: GetTagFn, }) - TagTool.RegisterRead(server.ServerTool{ + TagTool.RegisterRead(tool.ServerTool{ Tool: ListTagsTool, Handler: ListTagsFn, }) } -func CreateTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func CreateTagFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -118,8 +116,7 @@ func CreateTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes return to.TextResult("Tag Created") } -func DeleteTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func DeleteTagFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -145,8 +142,7 @@ func DeleteTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes return to.TextResult("Tag deleted") } -func GetTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func GetTagFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -172,8 +168,7 @@ func GetTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult return to.TextResult(slimTag(tag)) } -func ListTagsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func ListTagsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) diff --git a/operation/repo/tree.go b/operation/repo/tree.go index 732f492..c10bcbd 100644 --- a/operation/repo/tree.go +++ b/operation/repo/tree.go @@ -8,37 +8,36 @@ 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" + "github.com/modelcontextprotocol/go-sdk/mcp" ) const ( GetRepoTreeToolName = "get_repository_tree" ) -var GetRepoTreeTool = mcp.NewTool( +var GetRepoTreeTool = tool.NewDefinition( GetRepoTreeToolName, - mcp.WithDescription("Get the file tree of a repository at a given ref (SHA, branch, or tag), optionally recursively."), - mcp.WithToolAnnotation(annotation.ReadOnly("Get repository file tree")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("tree_sha", mcp.Required(), mcp.Description("SHA, branch, or tag")), - mcp.WithBoolean("recursive"), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), + "Get the file tree of a repository at a given ref (SHA, branch, or tag), optionally recursively.", + annotation.ReadOnly("Get repository file tree"), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("tree_sha", tool.Required(), tool.Description("SHA, branch, or tag")), + tool.Boolean("recursive"), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) func init() { - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: GetRepoTreeTool, Handler: GetRepoTreeFn, }) } -func GetRepoTreeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func GetRepoTreeFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) diff --git a/operation/repo/tree_test.go b/operation/repo/tree_test.go index 0cb7a15..681858a 100644 --- a/operation/repo/tree_test.go +++ b/operation/repo/tree_test.go @@ -44,8 +44,10 @@ func TestSlimTreeNil(t *testing.T) { } func TestGetRepoTreeToolRequired(t *testing.T) { + inputSchema := GetRepoTreeTool.InputSchema.(map[string]any) + required, _ := inputSchema["required"].([]string) for _, field := range []string{"owner", "repo", "tree_sha"} { - if !slices.Contains(GetRepoTreeTool.InputSchema.Required, field) { + if !slices.Contains(required, field) { t.Errorf("expected %q to be required", field) } } diff --git a/operation/sdk_integration_test.go b/operation/sdk_integration_test.go new file mode 100644 index 0000000..daac001 --- /dev/null +++ b/operation/sdk_integration_test.go @@ -0,0 +1,393 @@ +package operation + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" + "gitea.com/gitea/gitea-mcp/pkg/flag" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +const testServerVersion = "test-version" + +func exposeAllTools(t *testing.T) { + t.Helper() + originalReadOnly := flag.ReadOnly + originalAllowedTools := flag.AllowedTools + originalAllowedScopes := flag.AllowedScopes + originalVersion := flag.Version + t.Cleanup(func() { + flag.ReadOnly = originalReadOnly + flag.AllowedTools = originalAllowedTools + flag.AllowedScopes = originalAllowedScopes + flag.Version = originalVersion + }) + flag.ReadOnly = false + flag.AllowedTools = nil + flag.AllowedScopes = nil + flag.Version = testServerVersion +} + +func assertVersionToolResult(t *testing.T, result *mcp.CallToolResult) { + t.Helper() + if len(result.Content) != 1 { + t.Fatalf("version tool content count = %d, want 1", len(result.Content)) + } + content, ok := result.Content[0].(*mcp.TextContent) + if !ok { + t.Fatalf("version tool content type = %T, want *mcp.TextContent", result.Content[0]) + } + if !strings.Contains(content.Text, testServerVersion) { + t.Errorf("version tool result = %q, want it to contain %q", content.Text, testServerVersion) + } +} + +func listAndCallVersion(ctx context.Context, t *testing.T, session *mcp.ClientSession) { + t.Helper() + result, err := session.ListTools(ctx, nil) + if err != nil { + t.Fatalf("ListTools() error = %v", err) + } + if len(result.Tools) != 54 { + t.Fatalf("ListTools() count = %d, want 54", len(result.Tools)) + } + callResult, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "get_gitea_mcp_server_version", + }) + if err != nil { + t.Fatalf("CallTool() error = %v", err) + } + assertVersionToolResult(t, callResult) +} + +func TestOfficialSDKInMemory(t *testing.T) { + exposeAllTools(t) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + serverTransport, clientTransport := mcp.NewInMemoryTransports() + server := newMCPServer(testServerVersion) + RegisterTool(server) + serverDone := make(chan error, 1) + go func() { + serverDone <- server.Run(ctx, serverTransport) + }() + + client := mcp.NewClient(&mcp.Implementation{Name: "gitea-mcp-test", Version: "1"}, nil) + session, err := client.Connect(ctx, clientTransport, nil) + if err != nil { + t.Fatalf("Connect() error = %v", err) + } + if got := session.InitializeResult().ProtocolVersion; got != "2026-07-28" { + t.Errorf("protocol version = %q, want %q", got, "2026-07-28") + } + listAndCallVersion(ctx, t, session) + if err := session.Close(); err != nil { + t.Fatalf("Close() error = %v", err) + } + select { + case err := <-serverDone: + if err != nil && !errors.Is(err, context.Canceled) { + t.Fatalf("server Run() error = %v", err) + } + case <-ctx.Done(): + t.Fatal("server did not stop after the client session closed") + } +} + +func TestStreamableHTTPStateful(t *testing.T) { + exposeAllTools(t) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + server := newMCPServer(testServerVersion) + RegisterTool(server) + httpTestServer := httptest.NewServer(newHTTPServer("", server).Handler) + defer httpTestServer.Close() + + client := mcp.NewClient(&mcp.Implementation{Name: "gitea-mcp-http-test", Version: "1"}, nil) + session, err := client.Connect(ctx, &mcp.StreamableClientTransport{ + Endpoint: httpTestServer.URL + "/mcp", + HTTPClient: httpTestServer.Client(), + DisableStandaloneSSE: true, + MaxRetries: -1, + }, nil) + if err != nil { + t.Fatalf("Connect() error = %v", err) + } + defer session.Close() + if got := session.InitializeResult().ProtocolVersion; got != "2025-11-25" { + t.Errorf("protocol version = %q, want %q", got, "2025-11-25") + } + listAndCallVersion(ctx, t, session) + + response, err := httpTestServer.Client().Get(httpTestServer.URL + "/not-mcp") + if err != nil { + t.Fatalf("GET outside /mcp error = %v", err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusNotFound { + t.Errorf("GET outside /mcp status = %d, want %d", response.StatusCode, http.StatusNotFound) + } +} + +func TestStreamableHTTPAllowsLegacyLargeBodies(t *testing.T) { + server := newMCPServer(testServerVersion) + httpTestServer := httptest.NewServer(newHTTPServer("", server).Handler) + defer httpTestServer.Close() + + body := strings.NewReader(strings.Repeat(" ", mcp.DefaultMaxRequestBodyBytes+1)) + request, err := http.NewRequest(http.MethodPost, httpTestServer.URL+"/mcp", body) + if err != nil { + t.Fatalf("NewRequest() error = %v", err) + } + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Accept", "application/json, text/event-stream") + response, err := httpTestServer.Client().Do(request) + if err != nil { + t.Fatalf("POST large body error = %v", err) + } + defer response.Body.Close() + if response.StatusCode == http.StatusRequestEntityTooLarge { + t.Errorf("POST large body status = %d; PR 1 must preserve the previous unlimited body behavior", response.StatusCode) + } +} + +type authorizationTransport struct { + base http.RoundTripper + mu sync.RWMutex + value string +} + +func (t *authorizationTransport) set(value string) { + t.mu.Lock() + defer t.mu.Unlock() + t.value = value +} + +func (t *authorizationTransport) RoundTrip(request *http.Request) (*http.Response, error) { + clone := request.Clone(request.Context()) + clone.Header = request.Header.Clone() + t.mu.RLock() + value := t.value + t.mu.RUnlock() + if value != "" { + clone.Header.Set("Authorization", value) + } + return t.base.RoundTrip(clone) +} + +func authContextValue(ctx context.Context, session *mcp.ClientSession) (string, error) { + result, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "test_auth_context"}) + if err != nil { + return "", err + } + if len(result.Content) != 1 { + return "", fmt.Errorf("content count = %d, want 1", len(result.Content)) + } + content, ok := result.Content[0].(*mcp.TextContent) + if !ok { + return "", fmt.Errorf("content type = %T, want *mcp.TextContent", result.Content[0]) + } + return content.Text, nil +} + +func TestHTTPAuthPerRequest(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + server := newMCPServer(testServerVersion) + server.AddTool( + &mcp.Tool{ + Name: "test_auth_context", + Description: "Return the request-scoped authentication token.", + InputSchema: map[string]any{"type": "object", "properties": map[string]any{}}, + }, + func(ctx context.Context, _ *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + token, _ := ctx.Value(mcpContext.TokenContextKey).(string) + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: token}}, + }, nil + }, + ) + httpTestServer := httptest.NewServer(newHTTPServer("", server).Handler) + defer httpTestServer.Close() + + baseTransport := httpTestServer.Client().Transport + auth := &authorizationTransport{base: baseTransport} + auth.set("Bearer first-token") + baseClient := &http.Client{Transport: auth} + client := mcp.NewClient(&mcp.Implementation{Name: "gitea-mcp-auth-test", Version: "1"}, nil) + session, err := client.Connect(ctx, &mcp.StreamableClientTransport{ + Endpoint: httpTestServer.URL + "/mcp", + HTTPClient: baseClient, + DisableStandaloneSSE: true, + MaxRetries: -1, + }, nil) + if err != nil { + t.Fatalf("Connect() error = %v", err) + } + defer session.Close() + + for _, test := range []struct { + header string + want string + }{ + {header: "Bearer first-token", want: "first-token"}, + {header: "token second-token", want: "second-token"}, + {header: "Basic ignored", want: ""}, + } { + auth.set(test.header) + token, err := authContextValue(ctx, session) + if err != nil { + t.Fatalf("CallTool() with %q error = %v", test.header, err) + } + if token != test.want { + t.Errorf("CallTool() token = %q, want %q", token, test.want) + } + } + + type authenticatedSession struct { + session *mcp.ClientSession + want string + } + concurrentSessions := make([]authenticatedSession, 0, 2) + for index, token := range []string{"parallel-one", "parallel-two"} { + transport := &authorizationTransport{base: baseTransport} + transport.set("Bearer " + token) + httpClient := &http.Client{Transport: transport} + parallelClient := mcp.NewClient(&mcp.Implementation{ + Name: fmt.Sprintf("gitea-mcp-auth-parallel-%d", index), + Version: "1", + }, nil) + parallelSession, err := parallelClient.Connect(ctx, &mcp.StreamableClientTransport{ + Endpoint: httpTestServer.URL + "/mcp", + HTTPClient: httpClient, + DisableStandaloneSSE: true, + MaxRetries: -1, + }, nil) + if err != nil { + t.Fatalf("parallel Connect() error = %v", err) + } + defer parallelSession.Close() + concurrentSessions = append(concurrentSessions, authenticatedSession{session: parallelSession, want: token}) + } + + var waitGroup sync.WaitGroup + errorsCh := make(chan error, 20) + for _, authenticated := range concurrentSessions { + for range 10 { + waitGroup.Go(func() { + got, err := authContextValue(ctx, authenticated.session) + if err != nil { + errorsCh <- err + return + } + if got != authenticated.want { + errorsCh <- fmt.Errorf("parallel token = %q, want %q", got, authenticated.want) + } + }) + } + } + waitGroup.Wait() + close(errorsCh) + for err := range errorsCh { + t.Error(err) + } +} + +func TestStdioCommandTransport(t *testing.T) { + if testing.Short() { + t.Skip("skipping subprocess build in short mode") + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + binary := filepath.Join(t.TempDir(), "gitea-mcp") + build := exec.CommandContext(ctx, "go", "build", "-o", binary, "..") + build.Env = os.Environ() + if output, err := build.CombinedOutput(); err != nil { + t.Fatalf("build stdio test binary: %v\n%s", err, output) + } + + client := mcp.NewClient(&mcp.Implementation{Name: "gitea-mcp-stdio-test", Version: "1"}, nil) + command := exec.CommandContext(ctx, binary, "--transport", "stdio") + session, err := client.Connect(ctx, &mcp.CommandTransport{ + Command: command, + TerminateDuration: 2 * time.Second, + }, nil) + if err != nil { + t.Fatalf("Connect() error = %v", err) + } + defer session.Close() + if got := session.InitializeResult().ProtocolVersion; got != "2026-07-28" { + t.Errorf("protocol version = %q, want %q", got, "2026-07-28") + } + result, err := session.ListTools(ctx, nil) + if err != nil { + t.Fatalf("ListTools() error = %v", err) + } + if len(result.Tools) != 54 { + t.Errorf("ListTools() count = %d, want 54", len(result.Tools)) + } + callResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "get_gitea_mcp_server_version"}) + if err != nil { + t.Fatalf("CallTool() error = %v", err) + } + content, ok := callResult.Content[0].(*mcp.TextContent) + if !ok { + t.Fatalf("CallTool() content type = %T, want *mcp.TextContent", callResult.Content[0]) + } + if !strings.Contains(content.Text, "Gitea MCP Server version:") { + t.Errorf("CallTool() result = %q, want server version", content.Text) + } +} + +func TestNewHTTPServerAddress(t *testing.T) { + server := newHTTPServer(":12345", newMCPServer(testServerVersion)) + if server.Addr != ":12345" { + t.Errorf("server address = %q, want %q", server.Addr, ":12345") + } + if server.Handler == nil { + t.Error("server handler is nil") + } +} + +func TestHTTPServerGracefulShutdown(t *testing.T) { + server := newHTTPServer("127.0.0.1:0", newMCPServer(testServerVersion)) + listener, err := net.Listen("tcp", server.Addr) + if err != nil { + t.Fatalf("Listen() error = %v", err) + } + serveDone := make(chan error, 1) + go func() { + serveDone <- server.Serve(listener) + }() + + shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + if err := server.Shutdown(shutdownCtx); err != nil { + t.Fatalf("Shutdown() error = %v", err) + } + select { + case err := <-serveDone: + if !errors.Is(err, http.ErrServerClosed) { + t.Errorf("Serve() error = %v, want http.ErrServerClosed", err) + } + case <-shutdownCtx.Done(): + t.Fatal("server did not stop after Shutdown()") + } +} diff --git a/operation/search/search.go b/operation/search/search.go index 3696f4c..53faeb3 100644 --- a/operation/search/search.go +++ b/operation/search/search.go @@ -13,8 +13,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) var Tool = tool.New("search") @@ -27,81 +26,81 @@ const ( ) var ( - SearchUsersTool = mcp.NewTool( + SearchUsersTool = tool.NewDefinition( SearchUsersToolName, - mcp.WithDescription("Search for Gitea users by username or full name."), - mcp.WithToolAnnotation(annotation.ReadOnly("Search users")), - mcp.WithString("query", mcp.Required()), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), + "Search for Gitea users by username or full name.", + annotation.ReadOnly("Search users"), + tool.String("query", tool.Required()), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) - SearOrgTeamsTool = mcp.NewTool( + SearOrgTeamsTool = tool.NewDefinition( SearchOrgTeamsToolName, - mcp.WithDescription("Search for teams within an organization by name, optionally including each team's description in the results."), - mcp.WithToolAnnotation(annotation.ReadOnly("Search organization teams")), - mcp.WithString("org", mcp.Required()), - mcp.WithString("query", mcp.Required()), - mcp.WithBoolean("includeDescription"), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), + "Search for teams within an organization by name, optionally including each team's description in the results.", + annotation.ReadOnly("Search organization teams"), + tool.String("org", tool.Required()), + tool.String("query", tool.Required()), + tool.Boolean("includeDescription"), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) - SearchReposTool = mcp.NewTool( + SearchReposTool = tool.NewDefinition( SearchReposToolName, - mcp.WithDescription("Search for repositories by keyword, with filters for topic/description matching, owner, visibility, archived status, and sort order."), - mcp.WithToolAnnotation(annotation.ReadOnly("Search repositories")), - mcp.WithString("query", mcp.Required()), - mcp.WithBoolean("keywordIsTopic"), - mcp.WithBoolean("keywordInDescription"), - mcp.WithNumber("ownerID"), - mcp.WithBoolean("isPrivate"), - mcp.WithBoolean("isArchived"), - mcp.WithString("sort"), - mcp.WithString("order"), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), + "Search for repositories by keyword, with filters for topic/description matching, owner, visibility, archived status, and sort order.", + annotation.ReadOnly("Search repositories"), + tool.String("query", tool.Required()), + tool.Boolean("keywordIsTopic"), + tool.Boolean("keywordInDescription"), + tool.Number("ownerID"), + tool.Boolean("isPrivate"), + tool.Boolean("isArchived"), + tool.String("sort"), + tool.String("order"), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) - SearchIssuesTool = mcp.NewTool( + SearchIssuesTool = tool.NewDefinition( SearchIssuesToolName, - mcp.WithDescription("Search issues and PRs across repositories"), - mcp.WithToolAnnotation(annotation.ReadOnly("Search issues")), - mcp.WithString("query", mcp.Required()), - mcp.WithString("state", mcp.Enum("open", "closed", "all")), - mcp.WithString("type", mcp.Enum("issues", "pulls")), - mcp.WithString("labels", mcp.Description("comma-separated")), - mcp.WithString("owner", mcp.Description("filter by owner")), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), + "Search issues and PRs across repositories", + annotation.ReadOnly("Search issues"), + tool.String("query", tool.Required()), + tool.String("state", tool.Enum("open", "closed", "all")), + tool.String("type", tool.Enum("issues", "pulls")), + tool.String("labels", tool.Description("comma-separated")), + tool.String("owner", tool.Description("filter by owner")), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) ) func init() { - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: SearchUsersTool, Handler: UsersFn, }) - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: SearOrgTeamsTool, Handler: OrgTeamsFn, }) - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: SearchReposTool, Handler: ReposFn, }) - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: SearchIssuesTool, Handler: IssuesFn, }) } -func UsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - keyword, err := params.GetString(req.GetArguments(), "query") +func UsersFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + keyword, err := params.GetString(args, "query") if err != nil { return to.ErrorResult(err) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) + page, pageSize := params.GetPagination(args, 30) opt := gitea_sdk.SearchUsersOption{ KeyWord: keyword, ListOptions: gitea_sdk.ListOptions{ @@ -120,17 +119,17 @@ func UsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, return to.TextResult(slimUserDetails(users)) } -func OrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - org, err := params.GetString(req.GetArguments(), "org") +func OrgTeamsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + org, err := params.GetString(args, "org") if err != nil { return to.ErrorResult(err) } - query, err := params.GetString(req.GetArguments(), "query") + query, err := params.GetString(args, "query") if err != nil { return to.ErrorResult(err) } - includeDescription, _ := req.GetArguments()["includeDescription"].(bool) - page, pageSize := params.GetPagination(req.GetArguments(), 30) + includeDescription, _ := args["includeDescription"].(bool) + page, pageSize := params.GetPagination(args, 30) opt := gitea_sdk.SearchTeamsOptions{ Query: query, IncludeDescription: includeDescription, @@ -150,12 +149,11 @@ func OrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResu return to.TextResult(slimTeams(teams)) } -func ReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - keyword, err := params.GetString(req.GetArguments(), "query") +func ReposFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + keyword, err := params.GetString(args, "query") if err != nil { return to.ErrorResult(err) } - args := req.GetArguments() keywordIsTopic, _ := args["keywordIsTopic"].(bool) keywordInDescription, _ := args["keywordInDescription"].(bool) sort, _ := args["sort"].(string) @@ -186,8 +184,7 @@ func ReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, return to.TextResult(slim.Repos(repos)) } -func IssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func IssuesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { query, err := params.GetString(args, "query") if err != nil { return to.ErrorResult(err) diff --git a/operation/search/search_test.go b/operation/search/search_test.go index 1dce5d9..6ae8026 100644 --- a/operation/search/search_test.go +++ b/operation/search/search_test.go @@ -4,13 +4,13 @@ import ( "slices" "testing" - "github.com/mark3labs/mcp-go/mcp" + "github.com/modelcontextprotocol/go-sdk/mcp" ) func TestSearchToolsRequiredFields(t *testing.T) { tests := []struct { name string - tool mcp.Tool + tool *mcp.Tool required []string }{ { @@ -32,9 +32,11 @@ func TestSearchToolsRequiredFields(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + inputSchema := tt.tool.InputSchema.(map[string]any) + required, _ := inputSchema["required"].([]string) for _, field := range tt.required { - if !slices.Contains(tt.tool.InputSchema.Required, field) { - t.Errorf("tool %s: expected %q to be required, got required=%v", tt.name, field, tt.tool.InputSchema.Required) + if !slices.Contains(required, field) { + t.Errorf("tool %s: expected %q to be required, got required=%v", tt.name, field, required) } } }) diff --git a/operation/search/slim_test.go b/operation/search/slim_test.go index e0914ed..8c5e2a8 100644 --- a/operation/search/slim_test.go +++ b/operation/search/slim_test.go @@ -48,7 +48,9 @@ func TestSlimIssues(t *testing.T) { } func TestSearchIssuesToolRequired(t *testing.T) { - if !slices.Contains(SearchIssuesTool.InputSchema.Required, "query") { + inputSchema := SearchIssuesTool.InputSchema.(map[string]any) + required, _ := inputSchema["required"].([]string) + if !slices.Contains(required, "query") { t.Error("search_issues should require query") } } diff --git a/operation/testdata/tools.golden.json b/operation/testdata/tools.golden.json new file mode 100644 index 0000000..00e2fc8 --- /dev/null +++ b/operation/testdata/tools.golden.json @@ -0,0 +1,2817 @@ +[ + { + "scope": "actions", + "access": "read", + "name": "actions_config_read", + "description": "Read Actions secrets and variables.", + "inputSchema": { + "properties": { + "method": { + "enum": [ + "list_repo_secrets", + "list_org_secrets", + "list_repo_variables", + "get_repo_variable", + "list_org_variables", + "get_org_variable" + ], + "type": "string" + }, + "name": { + "description": "for get methods", + "type": "string" + }, + "org": { + "description": "for org methods", + "type": "string" + }, + "owner": { + "description": "for repo methods", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "minimum": 1, + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "for repo methods", + "type": "string" + } + }, + "required": [ + "method" + ], + "type": "object" + }, + "annotations": { + "title": "Read Actions secrets and variables", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "actions", + "access": "read", + "name": "actions_run_read", + "description": "Read Actions workflows, runs, jobs, logs, and artifacts.", + "inputSchema": { + "properties": { + "artifact_id": { + "description": "for 'get_artifact'/'download_artifact'", + "type": "number" + }, + "artifact_name": { + "description": "name filter for 'list_artifacts'/'list_run_artifacts'", + "type": "string" + }, + "job_id": { + "description": "for 'get_job'/log methods", + "type": "number" + }, + "max_bytes": { + "default": 65536, + "description": "max log bytes", + "minimum": 1024, + "type": "number" + }, + "method": { + "enum": [ + "list_workflows", + "get_workflow", + "list_runs", + "get_run", + "list_jobs", + "list_run_jobs", + "get_job", + "get_job_log_preview", + "download_job_log", + "list_artifacts", + "list_run_artifacts", + "get_artifact", + "download_artifact" + ], + "type": "string" + }, + "output_path": { + "description": "for 'download_job_log'/'download_artifact'", + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "minimum": 1, + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "run_id": { + "description": "for 'get_run'/'list_run_jobs'/'list_run_artifacts'", + "type": "number" + }, + "status": { + "description": "filter for 'list_runs'/'list_jobs'", + "type": "string" + }, + "tail_lines": { + "default": 200, + "description": "log tail lines", + "minimum": 1, + "type": "number" + }, + "workflow_id": { + "description": "ID or filename (for 'get_workflow')", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "Read Actions workflow, run, job, and artifact data", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "actions", + "access": "write", + "name": "actions_config_write", + "description": "Write Actions secrets and variables: upsert, create, update, delete.", + "inputSchema": { + "properties": { + "data": { + "description": "secret value (upsert)", + "type": "string" + }, + "description": { + "type": "string" + }, + "method": { + "enum": [ + "upsert_repo_secret", + "delete_repo_secret", + "upsert_org_secret", + "delete_org_secret", + "create_repo_variable", + "update_repo_variable", + "delete_repo_variable", + "create_org_variable", + "update_org_variable", + "delete_org_variable" + ], + "type": "string" + }, + "name": { + "description": "secret or variable name", + "type": "string" + }, + "org": { + "description": "for org methods", + "type": "string" + }, + "owner": { + "description": "for repo methods", + "type": "string" + }, + "repo": { + "description": "for repo methods", + "type": "string" + }, + "value": { + "description": "variable value", + "type": "string" + } + }, + "required": [ + "method" + ], + "type": "object" + }, + "annotations": { + "title": "Manage Actions secrets and variables", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "actions", + "access": "write", + "name": "actions_run_write", + "description": "Write Actions runs: dispatch, cancel, rerun.", + "inputSchema": { + "properties": { + "inputs": { + "description": "for 'dispatch_workflow'", + "properties": {}, + "type": "object" + }, + "method": { + "enum": [ + "dispatch_workflow", + "cancel_run", + "rerun_run" + ], + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "ref": { + "description": "branch or tag (for 'dispatch_workflow')", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "run_id": { + "description": "for 'cancel_run'/'rerun_run'", + "type": "number" + }, + "workflow_id": { + "description": "ID or filename (for 'dispatch_workflow')", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "Trigger, cancel, or rerun Actions workflows", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "branch", + "access": "read", + "name": "list_branches", + "description": "List all branches in a repository, paginated.", + "inputSchema": { + "properties": { + "owner": { + "description": "repo owner", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "type": "number" + }, + "repo": { + "description": "repo name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "List repository branches", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "branch", + "access": "write", + "name": "create_branch", + "description": "Create a new branch in a repository, optionally from a specific source branch (defaults to the repository's default branch).", + "inputSchema": { + "properties": { + "branch": { + "type": "string" + }, + "old_branch": { + "description": "source branch (default: repo default)", + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "branch" + ], + "type": "object" + }, + "annotations": { + "title": "Create a new branch", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "branch", + "access": "write", + "name": "delete_branch", + "description": "Permanently delete a branch from a repository. This action is destructive and cannot be undone.", + "inputSchema": { + "properties": { + "branch": { + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "branch" + ], + "type": "object" + }, + "annotations": { + "title": "Delete a branch", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "commit", + "access": "read", + "name": "get_commit", + "description": "Get details for a single commit in a repository by its SHA.", + "inputSchema": { + "properties": { + "owner": { + "description": "repo owner", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "sha": { + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "sha" + ], + "type": "object" + }, + "annotations": { + "title": "Get commit details", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "commit", + "access": "read", + "name": "list_commits", + "description": "List commits in a repository, optionally starting from a specific branch or SHA and filtered to commits touching a given file path.", + "inputSchema": { + "properties": { + "owner": { + "description": "repo owner", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "minimum": 1, + "type": "number" + }, + "path": { + "description": "only commits touching this path", + "type": "string" + }, + "per_page": { + "default": 30, + "description": "results per page", + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "sha": { + "description": "starting SHA or branch", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "List repository commits", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "file", + "access": "read", + "name": "get_dir_contents", + "description": "List the entries (files and subdirectories) in a repository directory at a given ref (branch, tag, or commit SHA).", + "inputSchema": { + "properties": { + "owner": { + "description": "repo owner", + "type": "string" + }, + "path": { + "type": "string" + }, + "ref": { + "description": "branch, tag, or commit SHA", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "ref", + "path" + ], + "type": "object" + }, + "annotations": { + "title": "Get directory contents", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "file", + "access": "read", + "name": "get_file_contents", + "description": "Get file content and metadata", + "inputSchema": { + "properties": { + "owner": { + "description": "repo owner", + "type": "string" + }, + "path": { + "type": "string" + }, + "ref": { + "description": "branch, tag, or commit SHA", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "withLines": { + "description": "return numbered lines", + "type": "boolean" + } + }, + "required": [ + "owner", + "repo", + "ref", + "path" + ], + "type": "object" + }, + "annotations": { + "title": "Get file content", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "file", + "access": "write", + "name": "create_or_update_file", + "description": "Create or update a file (provide sha to update an existing file).", + "inputSchema": { + "properties": { + "branch_name": { + "type": "string" + }, + "content": { + "type": "string" + }, + "message": { + "description": "commit message", + "type": "string" + }, + "new_branch_name": { + "description": "new branch (create only)", + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "path": { + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "sha": { + "description": "existing file SHA (omit to create)", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path", + "content", + "message", + "branch_name" + ], + "type": "object" + }, + "annotations": { + "title": "Create or update a file", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "file", + "access": "write", + "name": "delete_file", + "description": "Delete a file from a repository by committing the removal to a branch. Requires the file's current SHA and a commit message.", + "inputSchema": { + "properties": { + "branch_name": { + "type": "string" + }, + "message": { + "description": "commit message", + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "path": { + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "sha": { + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path", + "message", + "branch_name", + "sha" + ], + "type": "object" + }, + "annotations": { + "title": "Delete a file", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "issue", + "access": "read", + "name": "attachment_read", + "description": "Read issue/comment attachments: list metadata, get metadata, or download content.", + "inputSchema": { + "properties": { + "attachment_id": { + "description": "required for get and for download when attachment_uuid is not provided", + "type": "number" + }, + "attachment_uuid": { + "description": "attachment UUID for direct download path lookup", + "type": "string" + }, + "comment_id": { + "description": "required for comment attachment list/get or comment-scoped metadata lookup", + "type": "number" + }, + "issue_number": { + "description": "required for issue attachment list/get or issue-scoped metadata lookup", + "type": "number" + }, + "method": { + "enum": [ + "list", + "get", + "download" + ], + "type": "string" + }, + "output_path": { + "description": "write the attachment to this exact path", + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "Read issue or comment attachments", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "issue", + "access": "read", + "name": "issue_read", + "description": "Read issue: details, comments, or labels.", + "inputSchema": { + "properties": { + "issue_number": { + "type": "number" + }, + "method": { + "enum": [ + "get", + "get_comments", + "get_labels" + ], + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "issue_number" + ], + "type": "object" + }, + "annotations": { + "title": "Read issue details", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "issue", + "access": "read", + "name": "list_issues", + "description": "List issues in a repository (or pull requests, via the 'type' filter), filterable by state, labels, milestones, and update time range.", + "inputSchema": { + "properties": { + "before": { + "description": "updated before ISO 8601", + "type": "string" + }, + "labels": { + "description": "label name filter", + "items": { + "type": "string" + }, + "type": "array" + }, + "milestones": { + "description": "milestone name or ID filter", + "items": { + "type": "string" + }, + "type": "array" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "type": "number" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "since": { + "description": "updated after ISO 8601", + "type": "string" + }, + "state": { + "default": "all", + "type": "string" + }, + "type": { + "description": "issues or pulls", + "enum": [ + "issues", + "pulls" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "List repository issues", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "issue", + "access": "write", + "name": "issue_write", + "description": "Write issues: create, update, manage comments and labels.", + "inputSchema": { + "properties": { + "assignees": { + "items": { + "type": "string" + }, + "type": "array" + }, + "body": { + "description": "required for 'create'/'add_comment'/'edit_comment'", + "type": "string" + }, + "commentID": { + "description": "for 'edit_comment'", + "type": "number" + }, + "deadline": { + "description": "ISO 8601", + "type": "string" + }, + "issue_number": { + "description": "required except for 'create'", + "type": "number" + }, + "label_id": { + "description": "for 'remove_label'", + "type": "number" + }, + "labels": { + "description": "label IDs", + "items": { + "type": "number" + }, + "type": "array" + }, + "method": { + "enum": [ + "create", + "update", + "add_comment", + "edit_comment", + "add_labels", + "remove_label", + "replace_labels", + "clear_labels" + ], + "type": "string" + }, + "milestone": { + "type": "number" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "ref": { + "description": "branch to associate", + "type": "string" + }, + "remove_deadline": { + "type": "boolean" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "state": { + "enum": [ + "open", + "closed", + "all" + ], + "type": "string" + }, + "title": { + "description": "required for 'create'", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "Create or update issues, comments, and labels", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "label", + "access": "read", + "name": "label_read", + "description": "Read repo or org labels.", + "inputSchema": { + "properties": { + "id": { + "description": "label ID (for 'get_repo_label')", + "type": "number" + }, + "method": { + "enum": [ + "list_repo_labels", + "get_repo_label", + "list_org_labels" + ], + "type": "string" + }, + "org": { + "description": "for org methods", + "type": "string" + }, + "owner": { + "description": "for repo methods", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "type": "number" + }, + "repo": { + "description": "for repo methods", + "type": "string" + } + }, + "required": [ + "method" + ], + "type": "object" + }, + "annotations": { + "title": "Read labels", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "label", + "access": "write", + "name": "label_write", + "description": "Write labels (repo or org): create, edit, delete.", + "inputSchema": { + "properties": { + "color": { + "description": "hex (#RRGGBB); required for create", + "type": "string" + }, + "description": { + "type": "string" + }, + "exclusive": { + "description": "exclusive (org only)", + "type": "boolean" + }, + "id": { + "description": "for edit/delete", + "type": "number" + }, + "is_archived": { + "description": "archived (repo only)", + "type": "boolean" + }, + "method": { + "enum": [ + "create_repo_label", + "edit_repo_label", + "delete_repo_label", + "create_org_label", + "edit_org_label", + "delete_org_label" + ], + "type": "string" + }, + "name": { + "description": "required for create", + "type": "string" + }, + "org": { + "description": "for org methods", + "type": "string" + }, + "owner": { + "description": "for repo methods", + "type": "string" + }, + "repo": { + "description": "for repo methods", + "type": "string" + } + }, + "required": [ + "method" + ], + "type": "object" + }, + "annotations": { + "title": "Create, update, or delete labels", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "milestone", + "access": "read", + "name": "milestone_read", + "description": "Read milestones: get one or list.", + "inputSchema": { + "properties": { + "id": { + "description": "for 'get'", + "type": "number" + }, + "method": { + "enum": [ + "get", + "list" + ], + "type": "string" + }, + "name": { + "description": "name filter (for 'list')", + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "type": "number" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "state": { + "default": "all", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "Read milestones", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "milestone", + "access": "write", + "name": "milestone_write", + "description": "Write milestones: create, update, delete.", + "inputSchema": { + "properties": { + "description": { + "type": "string" + }, + "due_on": { + "description": "due date", + "type": "string" + }, + "id": { + "description": "for 'update'/'delete'", + "type": "number" + }, + "method": { + "enum": [ + "create", + "update", + "edit", + "delete" + ], + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "state": { + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "title": { + "description": "for 'create'", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "Create, update, or delete milestones", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "notification", + "access": "read", + "name": "notification_read", + "description": "Read notifications: list (optionally scoped to a repo) or get a thread by ID.", + "inputSchema": { + "properties": { + "before": { + "description": "updated before ISO 8601", + "type": "string" + }, + "id": { + "description": "thread ID (for 'get')", + "type": "number" + }, + "method": { + "enum": [ + "list", + "get" + ], + "type": "string" + }, + "owner": { + "description": "scope 'list' to a repo", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "type": "number" + }, + "repo": { + "description": "scope 'list' to a repo", + "type": "string" + }, + "since": { + "description": "updated after ISO 8601", + "type": "string" + }, + "status": { + "enum": [ + "unread", + "read", + "pinned" + ], + "type": "string" + }, + "subject_type": { + "enum": [ + "Issue", + "Pull", + "Commit", + "Repository" + ], + "type": "string" + } + }, + "required": [ + "method" + ], + "type": "object" + }, + "annotations": { + "title": "Read notifications", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "notification", + "access": "write", + "name": "notification_write", + "description": "Mark a notification or all notifications as read.", + "inputSchema": { + "properties": { + "id": { + "description": "thread ID (for 'mark_read')", + "type": "number" + }, + "last_read_at": { + "description": "ISO 8601; defaults to now", + "type": "string" + }, + "method": { + "enum": [ + "mark_read", + "mark_all_read" + ], + "type": "string" + }, + "owner": { + "description": "scope 'mark_all_read' to a repo", + "type": "string" + }, + "repo": { + "description": "scope 'mark_all_read' to a repo", + "type": "string" + } + }, + "required": [ + "method" + ], + "type": "object" + }, + "annotations": { + "title": "Manage notifications", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "packages", + "access": "read", + "name": "package_read", + "description": "Read package registry: list packages (one entry per version, filter via 'q'/'type'), list versions, or get a version.", + "inputSchema": { + "properties": { + "method": { + "enum": [ + "list", + "list_versions", + "get" + ], + "type": "string" + }, + "name": { + "description": "slashes auto-encoded; required except 'list'", + "type": "string" + }, + "owner": { + "description": "user or org", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "minimum": 1, + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "minimum": 1, + "type": "number" + }, + "q": { + "description": "search query", + "type": "string" + }, + "type": { + "description": "container/npm/maven/pypi/cargo/generic; required except 'list'", + "type": "string" + }, + "version": { + "description": "for 'get'", + "type": "string" + } + }, + "required": [ + "method", + "owner" + ], + "type": "object" + }, + "annotations": { + "title": "Read package registry", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "packages", + "access": "write", + "name": "package_write", + "description": "Delete a package version (irreversible).", + "inputSchema": { + "properties": { + "method": { + "enum": [ + "delete" + ], + "type": "string" + }, + "name": { + "description": "slashes auto-encoded", + "type": "string" + }, + "owner": { + "description": "user or org", + "type": "string" + }, + "type": { + "description": "container/npm/maven/pypi/cargo/generic", + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "method", + "owner", + "type", + "name", + "version" + ], + "type": "object" + }, + "annotations": { + "title": "Delete a package version", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "pull_request", + "access": "read", + "name": "list_pull_requests", + "description": "List pull requests in a repository, filterable by state and milestone, with configurable sort order (e.g. recently updated, most commented).", + "inputSchema": { + "properties": { + "milestone": { + "type": "number" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "type": "number" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "sort": { + "default": "recentupdate", + "enum": [ + "oldest", + "recentupdate", + "leastupdate", + "mostcomment", + "leastcomment", + "priority" + ], + "type": "string" + }, + "state": { + "default": "all", + "enum": [ + "open", + "closed", + "all" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "List pull requests", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "pull_request", + "access": "read", + "name": "pull_request_read", + "description": "Read pull request: details, diff, changed files, head commit status, reviews, review comments.", + "inputSchema": { + "properties": { + "binary": { + "description": "include binary diff", + "type": "boolean" + }, + "method": { + "enum": [ + "get", + "get_diff", + "get_files", + "get_status", + "get_reviews", + "get_review", + "get_review_comments" + ], + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "type": "number" + }, + "pull_number": { + "type": "number" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "review_id": { + "description": "for 'get_review'; optional for 'get_review_comments', omit to list all", + "type": "number" + } + }, + "required": [ + "method", + "owner", + "repo", + "pull_number" + ], + "type": "object" + }, + "annotations": { + "title": "Read pull request details", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "pull_request", + "access": "write", + "name": "pull_request_review_write", + "description": "Write PR reviews: create, submit, delete, dismiss, reply to and resolve review comments.", + "inputSchema": { + "properties": { + "body": { + "description": "review body, or reply text for 'reply_comment'", + "type": "string" + }, + "comment_id": { + "description": "comment ID from 'get_review_comments'; resolve takes the thread's first", + "type": "number" + }, + "comments": { + "description": "inline comments (for 'create')", + "items": { + "properties": { + "body": { + "type": "string" + }, + "new_line_num": { + "description": "new-file line (additions)", + "type": "number" + }, + "old_line_num": { + "description": "old-file line (deletions)", + "type": "number" + }, + "path": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "commit_id": { + "description": "for 'create'", + "type": "string" + }, + "message": { + "description": "dismissal reason", + "type": "string" + }, + "method": { + "enum": [ + "create", + "submit", + "delete", + "dismiss", + "reply_comment", + "resolve_thread", + "unresolve_thread" + ], + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "pull_number": { + "description": "required except for 'resolve_thread'/'unresolve_thread'", + "type": "number" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "review_id": { + "description": "for 'submit'/'delete'/'dismiss'", + "type": "number" + }, + "state": { + "enum": [ + "APPROVED", + "REQUEST_CHANGES", + "COMMENT", + "PENDING" + ], + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "Write pull request reviews", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "pull_request", + "access": "write", + "name": "pull_request_write", + "description": "Write pull requests: create, update, close, reopen, merge, update branch from base, manage reviewers.", + "inputSchema": { + "properties": { + "allow_maintainer_edit": { + "description": "for 'update'", + "type": "boolean" + }, + "assignee": { + "description": "for 'update'", + "type": "string" + }, + "assignees": { + "description": "for 'update'", + "items": { + "type": "string" + }, + "type": "array" + }, + "base": { + "description": "base branch (required for 'create')", + "type": "string" + }, + "body": { + "description": "required for 'create'; optional for 'update'", + "type": "string" + }, + "deadline": { + "description": "ISO 8601", + "type": "string" + }, + "delete_branch": { + "description": "for 'merge'", + "type": "boolean" + }, + "draft": { + "description": "uses 'WIP: ' title prefix", + "type": "boolean" + }, + "force_merge": { + "description": "merge even if checks fail", + "type": "boolean" + }, + "head": { + "description": "head branch (required for 'create')", + "type": "string" + }, + "head_commit_id": { + "description": "expected head SHA for conflict detection", + "type": "string" + }, + "labels": { + "description": "label IDs", + "items": { + "type": "number" + }, + "type": "array" + }, + "merge_style": { + "default": "merge", + "description": "for 'merge'", + "enum": [ + "merge", + "rebase", + "rebase-merge", + "squash", + "fast-forward-only" + ], + "type": "string" + }, + "merge_when_checks_succeed": { + "description": "for 'merge'", + "type": "boolean" + }, + "message": { + "description": "merge commit message or dismissal reason", + "type": "string" + }, + "method": { + "enum": [ + "create", + "update", + "close", + "reopen", + "merge", + "update_branch", + "add_reviewers", + "remove_reviewers" + ], + "type": "string" + }, + "milestone": { + "description": "for 'update'", + "type": "number" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "pull_number": { + "description": "required except for 'create'", + "type": "number" + }, + "remove_deadline": { + "description": "for 'update'", + "type": "boolean" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "reviewers": { + "description": "for 'add_reviewers'/'remove_reviewers'", + "items": { + "type": "string" + }, + "type": "array" + }, + "state": { + "description": "for 'update'", + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "team_reviewers": { + "description": "for 'add_reviewers'/'remove_reviewers'", + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "description": "required for 'create'; optional for 'update'/'merge'", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "Create, update, close, reopen, or merge pull requests", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "release", + "access": "read", + "name": "get_latest_release", + "description": "Get the most recent published (non-draft) release in a repository.", + "inputSchema": { + "properties": { + "owner": { + "description": "repo owner", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "Get latest release", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "release", + "access": "read", + "name": "get_release", + "description": "Get a release by ID", + "inputSchema": { + "properties": { + "id": { + "type": "number" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "id" + ], + "type": "object" + }, + "annotations": { + "title": "Get release details", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "release", + "access": "read", + "name": "list_releases", + "description": "List releases in a repository, optionally filtered to drafts or pre-releases.", + "inputSchema": { + "properties": { + "is_draft": { + "type": "boolean" + }, + "is_pre_release": { + "type": "boolean" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "minimum": 1, + "type": "number" + }, + "per_page": { + "default": 20, + "description": "results per page", + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "repo name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "List releases", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "release", + "access": "write", + "name": "create_release", + "description": "Create a new release in a repository from a tag, optionally marking it as a draft or pre-release.", + "inputSchema": { + "properties": { + "body": { + "type": "string" + }, + "is_draft": { + "type": "boolean" + }, + "is_pre_release": { + "type": "boolean" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "tag_name": { + "type": "string" + }, + "target": { + "description": "commitish", + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag_name", + "target", + "title" + ], + "type": "object" + }, + "annotations": { + "title": "Create a release", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "release", + "access": "write", + "name": "delete_release", + "description": "Delete a release from a repository by its numeric ID. This action is destructive and cannot be undone.", + "inputSchema": { + "properties": { + "id": { + "type": "number" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "id" + ], + "type": "object" + }, + "annotations": { + "title": "Delete a release", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "repository", + "access": "read", + "name": "get_repository_tree", + "description": "Get the file tree of a repository at a given ref (SHA, branch, or tag), optionally recursively.", + "inputSchema": { + "properties": { + "owner": { + "description": "repo owner", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "type": "number" + }, + "recursive": { + "type": "boolean" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "tree_sha": { + "description": "SHA, branch, or tag", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tree_sha" + ], + "type": "object" + }, + "annotations": { + "title": "Get repository file tree", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "repository", + "access": "read", + "name": "list_my_repos", + "description": "List repositories owned by the authenticated user.", + "inputSchema": { + "properties": { + "page": { + "default": 1, + "description": "page", + "minimum": 1, + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "minimum": 1, + "type": "number" + } + }, + "required": [], + "type": "object" + }, + "annotations": { + "title": "List my repositories", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "repository", + "access": "read", + "name": "list_org_repos", + "description": "List repositories belonging to an organization.", + "inputSchema": { + "properties": { + "org": { + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "minimum": 1, + "type": "number" + }, + "per_page": { + "default": 100, + "description": "results per page", + "minimum": 1, + "type": "number" + } + }, + "required": [ + "org" + ], + "type": "object" + }, + "annotations": { + "title": "List organization repositories", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "repository", + "access": "write", + "name": "create_repo", + "description": "Create a new Git repository, optionally under an organization (defaults to the authenticated user's account), with options for visibility, template, license, .gitignore, and initial README.", + "inputSchema": { + "properties": { + "auto_init": { + "type": "boolean" + }, + "default_branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "gitignores": { + "type": "string" + }, + "issue_labels": { + "type": "string" + }, + "license": { + "type": "string" + }, + "name": { + "type": "string" + }, + "object_format_name": { + "enum": [ + "sha1", + "sha256" + ], + "type": "string" + }, + "organization": { + "description": "defaults to personal account", + "type": "string" + }, + "private": { + "type": "boolean" + }, + "readme": { + "type": "string" + }, + "template": { + "type": "boolean" + }, + "trust_model": { + "enum": [ + "default", + "collaborator", + "committer", + "collaboratorcommitter" + ], + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "annotations": { + "title": "Create a new repository", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "repository", + "access": "write", + "name": "fork_repo", + "description": "Fork an existing repository into the authenticated user's account or a target organization, optionally under a new name.", + "inputSchema": { + "properties": { + "name": { + "description": "fork name", + "type": "string" + }, + "organization": { + "description": "target org", + "type": "string" + }, + "repo": { + "type": "string" + }, + "user": { + "description": "owner of source repo", + "type": "string" + } + }, + "required": [ + "user", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "Fork a repository", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "search", + "access": "read", + "name": "search_issues", + "description": "Search issues and PRs across repositories", + "inputSchema": { + "properties": { + "labels": { + "description": "comma-separated", + "type": "string" + }, + "owner": { + "description": "filter by owner", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "type": "number" + }, + "query": { + "type": "string" + }, + "state": { + "enum": [ + "open", + "closed", + "all" + ], + "type": "string" + }, + "type": { + "enum": [ + "issues", + "pulls" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "annotations": { + "title": "Search issues", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "search", + "access": "read", + "name": "search_org_teams", + "description": "Search for teams within an organization by name, optionally including each team's description in the results.", + "inputSchema": { + "properties": { + "includeDescription": { + "type": "boolean" + }, + "org": { + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "type": "number" + }, + "query": { + "type": "string" + } + }, + "required": [ + "org", + "query" + ], + "type": "object" + }, + "annotations": { + "title": "Search organization teams", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "search", + "access": "read", + "name": "search_repos", + "description": "Search for repositories by keyword, with filters for topic/description matching, owner, visibility, archived status, and sort order.", + "inputSchema": { + "properties": { + "isArchived": { + "type": "boolean" + }, + "isPrivate": { + "type": "boolean" + }, + "keywordInDescription": { + "type": "boolean" + }, + "keywordIsTopic": { + "type": "boolean" + }, + "order": { + "type": "string" + }, + "ownerID": { + "type": "number" + }, + "page": { + "default": 1, + "description": "page", + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "type": "number" + }, + "query": { + "type": "string" + }, + "sort": { + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "annotations": { + "title": "Search repositories", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "search", + "access": "read", + "name": "search_users", + "description": "Search for Gitea users by username or full name.", + "inputSchema": { + "properties": { + "page": { + "default": 1, + "description": "page", + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "type": "number" + }, + "query": { + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "annotations": { + "title": "Search users", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "tag", + "access": "read", + "name": "get_tag", + "description": "Get details for a single tag in a repository by name.", + "inputSchema": { + "properties": { + "owner": { + "description": "repo owner", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "tag_name": { + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag_name" + ], + "type": "object" + }, + "annotations": { + "title": "Get tag details", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "tag", + "access": "read", + "name": "list_tags", + "description": "List all tags in a repository, paginated.", + "inputSchema": { + "properties": { + "owner": { + "description": "repo owner", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "minimum": 1, + "type": "number" + }, + "per_page": { + "default": 20, + "description": "results per page", + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "repo name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "List tags", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "tag", + "access": "write", + "name": "create_tag", + "description": "Create a new Git tag in a repository at a target commit, branch, or existing tag, with an optional annotation message.", + "inputSchema": { + "properties": { + "message": { + "description": "tag message", + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "tag_name": { + "type": "string" + }, + "target": { + "description": "commitish", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag_name" + ], + "type": "object" + }, + "annotations": { + "title": "Create a tag", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "tag", + "access": "write", + "name": "delete_tag", + "description": "Permanently delete a tag from a repository. This action is destructive and cannot be undone.", + "inputSchema": { + "properties": { + "owner": { + "description": "repo owner", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "tag_name": { + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag_name" + ], + "type": "object" + }, + "annotations": { + "title": "Delete a tag", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "timetracking", + "access": "read", + "name": "timetracking_read", + "description": "Read time tracking: issue times, repo times, active stopwatches, your tracked times.", + "inputSchema": { + "properties": { + "issue_number": { + "description": "for 'list_issue_times'", + "type": "number" + }, + "method": { + "enum": [ + "list_issue_times", + "list_repo_times", + "get_my_stopwatches", + "get_my_times" + ], + "type": "string" + }, + "owner": { + "description": "for list_* methods", + "type": "string" + }, + "page": { + "default": 1, + "description": "page", + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "type": "number" + }, + "repo": { + "description": "for list_* methods", + "type": "string" + } + }, + "required": [ + "method" + ], + "type": "object" + }, + "annotations": { + "title": "Read tracked time", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "timetracking", + "access": "write", + "name": "timetracking_write", + "description": "Write time tracking: stopwatches and entries.", + "inputSchema": { + "properties": { + "id": { + "description": "entry ID (for 'delete_time')", + "type": "number" + }, + "issue_number": { + "type": "number" + }, + "method": { + "enum": [ + "start_stopwatch", + "stop_stopwatch", + "delete_stopwatch", + "add_time", + "delete_time" + ], + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "time": { + "description": "seconds (for 'add_time')", + "type": "number" + } + }, + "required": [ + "method" + ], + "type": "object" + }, + "annotations": { + "title": "Add or manage tracked time", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "user", + "access": "read", + "name": "get_me", + "description": "Get current user", + "inputSchema": { + "properties": {}, + "required": [], + "type": "object" + }, + "annotations": { + "title": "Get current user information", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "user", + "access": "read", + "name": "get_user_orgs", + "description": "List current user's organizations", + "inputSchema": { + "properties": { + "page": { + "default": 1, + "description": "page", + "type": "number" + }, + "per_page": { + "default": 30, + "description": "results per page", + "type": "number" + } + }, + "required": [], + "type": "object" + }, + "annotations": { + "title": "Get user organizations", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "version", + "access": "read", + "name": "get_gitea_mcp_server_version", + "description": "Get the running version of the Gitea MCP Server itself (not the Gitea instance it connects to).", + "inputSchema": { + "properties": {}, + "required": [], + "type": "object" + }, + "annotations": { + "title": "Get server version", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "wiki", + "access": "read", + "name": "wiki_read", + "description": "Read wiki: list pages, get content, revision history.", + "inputSchema": { + "properties": { + "method": { + "enum": [ + "list", + "get", + "get_revisions" + ], + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "pageName": { + "description": "for 'get'/'get_revisions'", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "Read wiki pages", + "readOnlyHint": true, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "scope": "wiki", + "access": "write", + "name": "wiki_write", + "description": "Write wiki pages: create, update, delete.", + "inputSchema": { + "properties": { + "content": { + "description": "for 'create'/'update'", + "type": "string" + }, + "message": { + "description": "commit message", + "type": "string" + }, + "method": { + "enum": [ + "create", + "update", + "delete" + ], + "type": "string" + }, + "owner": { + "description": "repo owner", + "type": "string" + }, + "pageName": { + "description": "for 'update'/'delete'", + "type": "string" + }, + "repo": { + "description": "repo name", + "type": "string" + }, + "title": { + "description": "for 'create'", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "annotations": { + "title": "Create, update, or delete wiki pages", + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + } +] diff --git a/operation/timetracking/timetracking.go b/operation/timetracking/timetracking.go index 3e7354e..819f877 100644 --- a/operation/timetracking/timetracking.go +++ b/operation/timetracking/timetracking.go @@ -12,8 +12,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) var Tool = tool.New("timetracking") @@ -24,86 +23,86 @@ const ( ) var ( - TimetrackingReadTool = mcp.NewTool( + TimetrackingReadTool = tool.NewDefinition( TimetrackingReadToolName, - mcp.WithDescription("Read time tracking: issue times, repo times, active stopwatches, your tracked times."), - mcp.WithToolAnnotation(annotation.ReadOnly("Read tracked time")), - mcp.WithString("method", mcp.Required(), mcp.Enum("list_issue_times", "list_repo_times", "get_my_stopwatches", "get_my_times")), - mcp.WithString("owner", mcp.Description("for list_* methods")), - mcp.WithString("repo", mcp.Description("for list_* methods")), - mcp.WithNumber("issue_number", mcp.Description("for 'list_issue_times'")), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), + "Read time tracking: issue times, repo times, active stopwatches, your tracked times.", + annotation.ReadOnly("Read tracked time"), + tool.String("method", tool.Required(), tool.Enum("list_issue_times", "list_repo_times", "get_my_stopwatches", "get_my_times")), + tool.String("owner", tool.Description("for list_* methods")), + tool.String("repo", tool.Description("for list_* methods")), + tool.Number("issue_number", tool.Description("for 'list_issue_times'")), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) - TimetrackingWriteTool = mcp.NewTool( + TimetrackingWriteTool = tool.NewDefinition( TimetrackingWriteToolName, - mcp.WithDescription("Write time tracking: stopwatches and entries."), - mcp.WithToolAnnotation(annotation.Write("Add or manage tracked time")), - mcp.WithString("method", mcp.Required(), mcp.Enum("start_stopwatch", "stop_stopwatch", "delete_stopwatch", "add_time", "delete_time")), - mcp.WithString("owner", mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Description(params.RepoDesc)), - mcp.WithNumber("issue_number"), - mcp.WithNumber("time", mcp.Description("seconds (for 'add_time')")), - mcp.WithNumber("id", mcp.Description("entry ID (for 'delete_time')")), + "Write time tracking: stopwatches and entries.", + annotation.Write("Add or manage tracked time"), + tool.String("method", tool.Required(), tool.Enum("start_stopwatch", "stop_stopwatch", "delete_stopwatch", "add_time", "delete_time")), + tool.String("owner", tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Description(params.RepoDesc)), + tool.Number("issue_number"), + tool.Number("time", tool.Description("seconds (for 'add_time')")), + tool.Number("id", tool.Description("entry ID (for 'delete_time')")), ) ) func init() { - Tool.RegisterRead(server.ServerTool{Tool: TimetrackingReadTool, Handler: readFn}) - Tool.RegisterWrite(server.ServerTool{Tool: TimetrackingWriteTool, Handler: writeFn}) + Tool.RegisterRead(tool.ServerTool{Tool: TimetrackingReadTool, Handler: readFn}) + Tool.RegisterWrite(tool.ServerTool{Tool: TimetrackingWriteTool, Handler: writeFn}) } -func readFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := params.GetString(req.GetArguments(), "method") +func readFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "list_issue_times": - return listTrackedTimesFn(ctx, req) + return listTrackedTimesFn(ctx, args) case "list_repo_times": - return listRepoTimesFn(ctx, req) + return listRepoTimesFn(ctx, args) case "get_my_stopwatches": - return getMyStopwatchesFn(ctx, req) + return getMyStopwatchesFn(ctx, args) case "get_my_times": - return getMyTimesFn(ctx, req) + return getMyTimesFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func writeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := params.GetString(req.GetArguments(), "method") +func writeFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "start_stopwatch": - return startStopwatchFn(ctx, req) + return startStopwatchFn(ctx, args) case "stop_stopwatch": - return stopStopwatchFn(ctx, req) + return stopStopwatchFn(ctx, args) case "delete_stopwatch": - return deleteStopwatchFn(ctx, req) + return deleteStopwatchFn(ctx, args) case "add_time": - return addTrackedTimeFn(ctx, req) + return addTrackedTimeFn(ctx, args) case "delete_time": - return deleteTrackedTimeFn(ctx, req) + return deleteTrackedTimeFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func startStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func startStopwatchFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } @@ -118,16 +117,16 @@ func startStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(fmt.Sprintf("Stopwatch started on issue %s/%s#%d", owner, repo, index)) } -func stopStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func stopStopwatchFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } @@ -142,16 +141,16 @@ func stopStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult(fmt.Sprintf("Stopwatch stopped on issue %s/%s#%d - time recorded", owner, repo, index)) } -func deleteStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func deleteStopwatchFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } @@ -166,7 +165,7 @@ func deleteStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.TextResult(fmt.Sprintf("Stopwatch deleted/cancelled on issue %s/%s#%d", owner, repo, index)) } -func getMyStopwatchesFn(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func getMyStopwatchesFn(ctx context.Context, _ map[string]any) (*mcp.CallToolResult, error) { client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) @@ -181,20 +180,20 @@ func getMyStopwatchesFn(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(slimStopWatches(stopwatches)) } -func listTrackedTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func listTrackedTimesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) + page, pageSize := params.GetPagination(args, 30) client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) @@ -215,21 +214,21 @@ func listTrackedTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.TextResult(slimTrackedTimes(times)) } -func addTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func addTrackedTimeFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } - timeSeconds, err := params.GetIndex(req.GetArguments(), "time") + timeSeconds, err := params.GetIndex(args, "time") if err != nil { return to.ErrorResult(err) } @@ -246,21 +245,21 @@ func addTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(slimTrackedTime(trackedTime)) } -func deleteTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func deleteTrackedTimeFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "issue_number") + index, err := params.GetIndex(args, "issue_number") if err != nil { return to.ErrorResult(err) } - id, err := params.GetIndex(req.GetArguments(), "id") + id, err := params.GetIndex(args, "id") if err != nil { return to.ErrorResult(err) } @@ -275,17 +274,17 @@ func deleteTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal return to.TextResult(fmt.Sprintf("Tracked time entry %d deleted from issue %s/%s#%d", id, owner, repo, index)) } -func listRepoTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := params.GetString(req.GetArguments(), "owner") +func listRepoTimesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - repo, err := params.GetString(req.GetArguments(), "repo") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - page, pageSize := params.GetPagination(req.GetArguments(), 30) + page, pageSize := params.GetPagination(args, 30) client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) @@ -305,7 +304,7 @@ func listRepoTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult(slimTrackedTimes(times)) } -func getMyTimesFn(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func getMyTimesFn(ctx context.Context, _ map[string]any) (*mcp.CallToolResult, error) { client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) diff --git a/operation/tool_contract_test.go b/operation/tool_contract_test.go new file mode 100644 index 0000000..8af878d --- /dev/null +++ b/operation/tool_contract_test.go @@ -0,0 +1,179 @@ +package operation + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "sort" + "testing" +) + +const updateToolContractEnv = "UPDATE_TOOL_CONTRACT" + +type toolContract struct { + Scope string `json:"scope"` + Access string `json:"access"` + Name string `json:"name"` + Description string `json:"description"` + InputSchema any `json:"inputSchema"` + Annotations contractAnnotations `json:"annotations"` +} + +type contractAnnotations struct { + Title string `json:"title"` + ReadOnlyHint bool `json:"readOnlyHint"` + DestructiveHint bool `json:"destructiveHint"` + IdempotentHint bool `json:"idempotentHint"` + OpenWorldHint bool `json:"openWorldHint"` +} + +func TestToolContract(t *testing.T) { + const ( + wantDomains = 18 + wantTools = 54 + wantRead = 33 + wantWrite = 21 + ) + + contracts := make([]toolContract, 0, wantTools) + seenScopes := make(map[string]struct{}, wantDomains) + seenNames := make(map[string]struct{}, wantTools) + readCount, writeCount := 0, 0 + + for _, domain := range domainTools { + scope := domain.Scope() + if scope == "" { + t.Fatal("registered tool domain has an empty scope") + } + if _, duplicate := seenScopes[scope]; duplicate { + t.Fatalf("duplicate tool domain scope %q", scope) + } + seenScopes[scope] = struct{}{} + + for _, registered := range domain.ReadTools() { + contracts = append(contracts, decodeToolContract(t, scope, "read", registered.Tool)) + readCount++ + } + for _, registered := range domain.WriteTools() { + contracts = append(contracts, decodeToolContract(t, scope, "write", registered.Tool)) + writeCount++ + } + } + + if len(seenScopes) != wantDomains { + t.Errorf("domain count = %d, want %d", len(seenScopes), wantDomains) + } + if len(contracts) != wantTools { + t.Errorf("tool count = %d, want %d", len(contracts), wantTools) + } + if readCount != wantRead { + t.Errorf("read tool count = %d, want %d", readCount, wantRead) + } + if writeCount != wantWrite { + t.Errorf("write tool count = %d, want %d", writeCount, wantWrite) + } + + for _, contract := range contracts { + if _, duplicate := seenNames[contract.Name]; duplicate { + t.Errorf("duplicate tool name %q", contract.Name) + } + seenNames[contract.Name] = struct{}{} + } + + sort.Slice(contracts, func(i, j int) bool { + if contracts[i].Scope != contracts[j].Scope { + return contracts[i].Scope < contracts[j].Scope + } + if contracts[i].Access != contracts[j].Access { + return contracts[i].Access < contracts[j].Access + } + return contracts[i].Name < contracts[j].Name + }) + + got, err := json.MarshalIndent(contracts, "", " ") + if err != nil { + t.Fatalf("marshal tool contract: %v", err) + } + got = append(got, '\n') + + goldenPath := filepath.Join("testdata", "tools.golden.json") + if os.Getenv(updateToolContractEnv) == "1" { + if err := os.WriteFile(goldenPath, got, 0o644); err != nil { + t.Fatalf("update tool contract: %v", err) + } + } + + want, err := os.ReadFile(goldenPath) + if err != nil { + t.Fatalf("read tool contract: %v", err) + } + if !bytes.Equal(got, want) { + t.Errorf("tool contract changed; inspect the semantic diff before running %s=1 go test -run '^TestToolContract$' ./operation/", updateToolContractEnv) + } +} + +func decodeToolContract(t *testing.T, scope, access string, toolDefinition any) toolContract { + t.Helper() + + data, err := json.Marshal(toolDefinition) + if err != nil { + t.Fatalf("marshal %s tool in scope %q: %v", access, scope, err) + } + var definition map[string]any + if err := json.Unmarshal(data, &definition); err != nil { + t.Fatalf("decode %s tool in scope %q: %v", access, scope, err) + } + + name := requiredString(t, definition, "name", scope) + description := requiredString(t, definition, "description", name) + inputSchema, ok := definition["inputSchema"].(map[string]any) + if !ok { + t.Fatalf("tool %q has inputSchema of type %T, want JSON object", name, definition["inputSchema"]) + } + // An omitted required keyword and an empty array have the same JSON Schema meaning. + if _, ok := inputSchema["required"]; !ok { + inputSchema["required"] = []any{} + } + annotations, _ := definition["annotations"].(map[string]any) + + // Normalize protocol defaults independently of SDK omitempty behavior. + return toolContract{ + Scope: scope, + Access: access, + Name: name, + Description: description, + InputSchema: inputSchema, + Annotations: contractAnnotations{ + Title: stringField(annotations, "title", ""), + ReadOnlyHint: boolField(annotations, "readOnlyHint", false), + DestructiveHint: boolField(annotations, "destructiveHint", true), + IdempotentHint: boolField(annotations, "idempotentHint", false), + OpenWorldHint: boolField(annotations, "openWorldHint", true), + }, + } +} + +func requiredString(t *testing.T, object map[string]any, key, owner string) string { + t.Helper() + + value, ok := object[key].(string) + if !ok || value == "" { + t.Fatalf("%s has missing or empty %q", owner, key) + } + return value +} + +func stringField(object map[string]any, key, fallback string) string { + if value, ok := object[key].(string); ok { + return value + } + return fallback +} + +func boolField(object map[string]any, key string, fallback bool) bool { + if value, ok := object[key].(bool); ok { + return value + } + return fallback +} diff --git a/operation/user/user.go b/operation/user/user.go index 78c9b15..03416b1 100644 --- a/operation/user/user.go +++ b/operation/user/user.go @@ -12,8 +12,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) const ( @@ -24,27 +23,27 @@ const ( var Tool = tool.New("user") var ( - GetMyUserInfoTool = mcp.NewTool( + GetMyUserInfoTool = tool.NewDefinition( GetMyUserInfoToolName, - mcp.WithDescription("Get current user"), - mcp.WithToolAnnotation(annotation.ReadOnly("Get current user information")), + "Get current user", + annotation.ReadOnly("Get current user information"), ) - GetUserOrgsTool = mcp.NewTool( + GetUserOrgsTool = tool.NewDefinition( GetUserOrgsToolName, - mcp.WithDescription("List current user's organizations"), - mcp.WithToolAnnotation(annotation.ReadOnly("Get user organizations")), - mcp.WithNumber("page", mcp.Description(params.PageDesc), mcp.DefaultNumber(1)), - mcp.WithNumber("per_page", mcp.Description(params.PaginationDesc), mcp.DefaultNumber(30)), + "List current user's organizations", + annotation.ReadOnly("Get user organizations"), + tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), + tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) ) func init() { - Tool.RegisterRead(server.ServerTool{Tool: GetMyUserInfoTool, Handler: GetUserInfoFn}) - Tool.RegisterRead(server.ServerTool{Tool: GetUserOrgsTool, Handler: GetUserOrgsFn}) + Tool.RegisterRead(tool.ServerTool{Tool: GetMyUserInfoTool, Handler: GetUserInfoFn}) + Tool.RegisterRead(tool.ServerTool{Tool: GetUserOrgsTool, Handler: GetUserOrgsFn}) } -func GetUserInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func GetUserInfoFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) @@ -56,8 +55,8 @@ func GetUserInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR return to.TextResult(slim.UserDetail(user)) } -func GetUserOrgsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - page, pageSize := params.GetPagination(req.GetArguments(), 30) +func GetUserOrgsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + page, pageSize := params.GetPagination(args, 30) opt := gitea_sdk.ListOrgsOptions{ ListOptions: gitea_sdk.ListOptions{ diff --git a/operation/version/version.go b/operation/version/version.go index 9a929b4..7d07f07 100644 --- a/operation/version/version.go +++ b/operation/version/version.go @@ -9,8 +9,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/tool" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) var Tool = tool.New("version") @@ -19,20 +18,20 @@ const ( GetGiteaMCPServerVersion = "get_gitea_mcp_server_version" ) -var GetGiteaMCPServerVersionTool = mcp.NewTool( +var GetGiteaMCPServerVersionTool = tool.NewDefinition( GetGiteaMCPServerVersion, - mcp.WithDescription("Get the running version of the Gitea MCP Server itself (not the Gitea instance it connects to)."), - mcp.WithToolAnnotation(annotation.ReadOnly("Get server version")), + "Get the running version of the Gitea MCP Server itself (not the Gitea instance it connects to).", + annotation.ReadOnly("Get server version"), ) func init() { - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: GetGiteaMCPServerVersionTool, Handler: GetGiteaMCPServerVersionFn, }) } -func GetGiteaMCPServerVersionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func GetGiteaMCPServerVersionFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { version := flag.Version if version == "" { version = "dev" diff --git a/operation/wiki/wiki.go b/operation/wiki/wiki.go index dde4892..7d50086 100644 --- a/operation/wiki/wiki.go +++ b/operation/wiki/wiki.go @@ -12,8 +12,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/tool" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) var Tool = tool.New("wiki") @@ -24,77 +23,76 @@ const ( ) var ( - WikiReadTool = mcp.NewTool( + WikiReadTool = tool.NewDefinition( WikiReadToolName, - mcp.WithDescription("Read wiki: list pages, get content, revision history."), - mcp.WithToolAnnotation(annotation.ReadOnly("Read wiki pages")), - mcp.WithString("method", mcp.Required(), mcp.Enum("list", "get", "get_revisions")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("pageName", mcp.Description("for 'get'/'get_revisions'")), + "Read wiki: list pages, get content, revision history.", + annotation.ReadOnly("Read wiki pages"), + tool.String("method", tool.Required(), tool.Enum("list", "get", "get_revisions")), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("pageName", tool.Description("for 'get'/'get_revisions'")), ) - WikiWriteTool = mcp.NewTool( + WikiWriteTool = tool.NewDefinition( WikiWriteToolName, - mcp.WithDescription("Write wiki pages: create, update, delete."), - mcp.WithToolAnnotation(annotation.Destructive("Create, update, or delete wiki pages")), - mcp.WithString("method", mcp.Required(), mcp.Enum("create", "update", "delete")), - mcp.WithString("owner", mcp.Required(), mcp.Description(params.OwnerDesc)), - mcp.WithString("repo", mcp.Required(), mcp.Description(params.RepoDesc)), - mcp.WithString("pageName", mcp.Description("for 'update'/'delete'")), - mcp.WithString("title", mcp.Description("for 'create'")), - mcp.WithString("content", mcp.Description("for 'create'/'update'")), - mcp.WithString("message", mcp.Description("commit message")), + "Write wiki pages: create, update, delete.", + annotation.Destructive("Create, update, or delete wiki pages"), + tool.String("method", tool.Required(), tool.Enum("create", "update", "delete")), + tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), + tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), + tool.String("pageName", tool.Description("for 'update'/'delete'")), + tool.String("title", tool.Description("for 'create'")), + tool.String("content", tool.Description("for 'create'/'update'")), + tool.String("message", tool.Description("commit message")), ) ) func init() { - Tool.RegisterRead(server.ServerTool{ + Tool.RegisterRead(tool.ServerTool{ Tool: WikiReadTool, Handler: wikiReadFn, }) - Tool.RegisterWrite(server.ServerTool{ + Tool.RegisterWrite(tool.ServerTool{ Tool: WikiWriteTool, Handler: wikiWriteFn, }) } -func wikiReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := params.GetString(req.GetArguments(), "method") +func wikiReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "list": - return listWikiPagesFn(ctx, req) + return listWikiPagesFn(ctx, args) case "get": - return getWikiPageFn(ctx, req) + return getWikiPageFn(ctx, args) case "get_revisions": - return getWikiRevisionsFn(ctx, req) + return getWikiRevisionsFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func wikiWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := params.GetString(req.GetArguments(), "method") +func wikiWriteFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { + method, err := params.GetString(args, "method") if err != nil { return to.ErrorResult(err) } switch method { case "create": - return createWikiPageFn(ctx, req) + return createWikiPageFn(ctx, args) case "update": - return updateWikiPageFn(ctx, req) + return updateWikiPageFn(ctx, args) case "delete": - return deleteWikiPageFn(ctx, req) + return deleteWikiPageFn(ctx, args) default: return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) } } -func listWikiPagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func listWikiPagesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -113,8 +111,7 @@ func listWikiPagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult(result) } -func getWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func getWikiPageFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -137,8 +134,7 @@ func getWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR return to.TextResult(result) } -func getWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func getWikiRevisionsFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -161,8 +157,7 @@ func getWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.TextResult(result) } -func createWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func createWikiPageFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -200,8 +195,7 @@ func createWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(result) } -func updateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func updateWikiPageFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) @@ -245,8 +239,7 @@ func updateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(result) } -func deleteWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - args := req.GetArguments() +func deleteWikiPageFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) diff --git a/operation/wiki/wiki_test.go b/operation/wiki/wiki_test.go index 2c9d5d9..701e5e8 100644 --- a/operation/wiki/wiki_test.go +++ b/operation/wiki/wiki_test.go @@ -11,8 +11,6 @@ import ( mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" "gitea.com/gitea/gitea-mcp/pkg/flag" - - "github.com/mark3labs/mcp-go/mcp" ) func TestWikiWriteBase64Encoding(t *testing.T) { @@ -54,10 +52,7 @@ func TestWikiWriteBase64Encoding(t *testing.T) { "title": "TestPage", } - req := mcp.CallToolRequest{} - req.Params.Arguments = args - - result, err := wikiWriteFn(ctx, req) + result, err := wikiWriteFn(ctx, args) if err != nil { t.Fatalf("wikiWriteFn() error: %v", err) } diff --git a/pkg/annotation/annotation.go b/pkg/annotation/annotation.go index c078419..e0418d0 100644 --- a/pkg/annotation/annotation.go +++ b/pkg/annotation/annotation.go @@ -1,18 +1,16 @@ package annotation -import "github.com/mark3labs/mcp-go/mcp" +import "github.com/modelcontextprotocol/go-sdk/mcp" -func ReadOnly(title string) mcp.ToolAnnotation { +func ReadOnly(title string) *mcp.ToolAnnotations { + return &mcp.ToolAnnotations{Title: title, ReadOnlyHint: true} +} + +func Write(title string) *mcp.ToolAnnotations { + return &mcp.ToolAnnotations{Title: title} +} + +func Destructive(title string) *mcp.ToolAnnotations { t := true - return mcp.ToolAnnotation{Title: title, ReadOnlyHint: &t} -} - -func Write(title string) mcp.ToolAnnotation { - f := false - return mcp.ToolAnnotation{Title: title, ReadOnlyHint: &f} -} - -func Destructive(title string) mcp.ToolAnnotation { - f, t := false, true - return mcp.ToolAnnotation{Title: title, ReadOnlyHint: &f, DestructiveHint: &t} + return &mcp.ToolAnnotations{Title: title, DestructiveHint: &t} } diff --git a/pkg/annotation/annotation_test.go b/pkg/annotation/annotation_test.go new file mode 100644 index 0000000..7364062 --- /dev/null +++ b/pkg/annotation/annotation_test.go @@ -0,0 +1,20 @@ +package annotation + +import "testing" + +func TestAnnotations(t *testing.T) { + readOnly := ReadOnly("Read") + if readOnly.Title != "Read" || !readOnly.ReadOnlyHint || readOnly.DestructiveHint != nil { + t.Errorf("ReadOnly() = %#v", readOnly) + } + + write := Write("Write") + if write.Title != "Write" || write.ReadOnlyHint || write.DestructiveHint != nil { + t.Errorf("Write() = %#v", write) + } + + destructive := Destructive("Delete") + if destructive.Title != "Delete" || destructive.ReadOnlyHint || destructive.DestructiveHint == nil || !*destructive.DestructiveHint { + t.Errorf("Destructive() = %#v", destructive) + } +} diff --git a/pkg/to/to.go b/pkg/to/to.go index b56dd23..72c617a 100644 --- a/pkg/to/to.go +++ b/pkg/to/to.go @@ -7,7 +7,7 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/flag" "gitea.com/gitea/gitea-mcp/pkg/log" - "github.com/mark3labs/mcp-go/mcp" + "github.com/modelcontextprotocol/go-sdk/mcp" ) func TextResult(v any) (*mcp.CallToolResult, error) { @@ -18,7 +18,9 @@ func TextResult(v any) (*mcp.CallToolResult, error) { if flag.Debug { log.Debugf("Text Result: %s", string(resultBytes)) } - return mcp.NewToolResultText(string(resultBytes)), nil + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: string(resultBytes)}}, + }, nil } func ErrorResult(err error) (*mcp.CallToolResult, error) { diff --git a/pkg/to/to_test.go b/pkg/to/to_test.go new file mode 100644 index 0000000..27add52 --- /dev/null +++ b/pkg/to/to_test.go @@ -0,0 +1,33 @@ +package to + +import ( + "errors" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func TestTextResult(t *testing.T) { + result, err := TextResult(map[string]any{"name": "gitea"}) + if err != nil { + t.Fatalf("TextResult() error = %v", err) + } + if len(result.Content) != 1 { + t.Fatalf("len(Content) = %d, want 1", len(result.Content)) + } + content, ok := result.Content[0].(*mcp.TextContent) + if !ok { + t.Fatalf("Content[0] type = %T, want *mcp.TextContent", result.Content[0]) + } + if content.Text != `{"name":"gitea"}` { + t.Errorf("Text = %q, want JSON object", content.Text) + } +} + +func TestErrorResult(t *testing.T) { + want := errors.New("failed") + result, err := ErrorResult(want) + if result != nil || !errors.Is(err, want) { + t.Errorf("ErrorResult() = (%#v, %v), want (nil, %v)", result, err, want) + } +} diff --git a/pkg/tool/definition.go b/pkg/tool/definition.go new file mode 100644 index 0000000..6a85deb --- /dev/null +++ b/pkg/tool/definition.go @@ -0,0 +1,110 @@ +package tool + +import "github.com/modelcontextprotocol/go-sdk/mcp" + +// Property describes one property in a tool's input schema. +type Property struct { + name string + schema map[string]any + required bool +} + +// PropertyOption configures one property in a tool's input schema. +type PropertyOption func(map[string]any) + +// NewDefinition builds a tool definition without enabling SDK-side validation. +func NewDefinition(name, description string, annotations *mcp.ToolAnnotations, properties ...Property) *mcp.Tool { + inputProperties := make(map[string]any, len(properties)) + required := make([]string, 0, len(properties)) + for _, property := range properties { + inputProperties[property.name] = property.schema + if property.required { + required = append(required, property.name) + } + } + + inputSchema := map[string]any{ + "type": "object", + "properties": inputProperties, + } + if len(required) > 0 { + inputSchema["required"] = required + } + + return &mcp.Tool{ + Name: name, + Description: description, + Annotations: annotations, + InputSchema: inputSchema, + } +} + +func String(name string, options ...PropertyOption) Property { + return newProperty(name, "string", false, options...) +} + +func Number(name string, options ...PropertyOption) Property { + return newProperty(name, "number", false, options...) +} + +func Boolean(name string, options ...PropertyOption) Property { + return newProperty(name, "boolean", false, options...) +} + +func Array(name string, options ...PropertyOption) Property { + return newProperty(name, "array", false, options...) +} + +func Object(name string, options ...PropertyOption) Property { + return newProperty(name, "object", true, options...) +} + +func newProperty(name, propertyType string, object bool, options ...PropertyOption) Property { + schema := map[string]any{"type": propertyType} + if object { + schema["properties"] = map[string]any{} + } + for _, option := range options { + option(schema) + } + + required, _ := schema["required"].(bool) + delete(schema, "required") + return Property{name: name, schema: schema, required: required} +} + +func Required() PropertyOption { + return func(schema map[string]any) { + schema["required"] = true + } +} + +func Description(description string) PropertyOption { + return func(schema map[string]any) { + schema["description"] = description + } +} + +func Enum(values ...string) PropertyOption { + return func(schema map[string]any) { + schema["enum"] = values + } +} + +func Default(value any) PropertyOption { + return func(schema map[string]any) { + schema["default"] = value + } +} + +func Minimum(value float64) PropertyOption { + return func(schema map[string]any) { + schema["minimum"] = value + } +} + +func Items(schema any) PropertyOption { + return func(propertySchema map[string]any) { + propertySchema["items"] = schema + } +} diff --git a/pkg/tool/definition_test.go b/pkg/tool/definition_test.go new file mode 100644 index 0000000..8e9f013 --- /dev/null +++ b/pkg/tool/definition_test.go @@ -0,0 +1,80 @@ +package tool + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func TestNewDefinition(t *testing.T) { + annotations := &mcp.ToolAnnotations{Title: "Example", ReadOnlyHint: true} + definition := NewDefinition( + "example", + "Example tool", + annotations, + String("owner", Required(), Description("repository owner"), Enum("one", "two"), Default("one")), + Number("page", Required(), Default(1), Minimum(1)), + Boolean("draft"), + Array("labels", Items(map[string]any{"type": "string"})), + Object("inputs", Description("workflow inputs")), + ) + + if definition.Name != "example" || definition.Description != "Example tool" { + t.Fatalf("definition = %#v", definition) + } + if definition.Annotations != annotations { + t.Fatal("NewDefinition did not preserve annotations") + } + + want := map[string]any{ + "type": "object", + "properties": map[string]any{ + "owner": map[string]any{ + "type": "string", + "description": "repository owner", + "enum": []string{"one", "two"}, + "default": "one", + }, + "page": map[string]any{ + "type": "number", + "default": 1, + "minimum": float64(1), + }, + "draft": map[string]any{"type": "boolean"}, + "labels": map[string]any{ + "type": "array", + "items": map[string]any{"type": "string"}, + }, + "inputs": map[string]any{ + "type": "object", + "properties": map[string]any{}, + "description": "workflow inputs", + }, + }, + "required": []string{"owner", "page"}, + } + if !reflect.DeepEqual(definition.InputSchema, want) { + t.Errorf("InputSchema = %#v, want %#v", definition.InputSchema, want) + } + + data, err := json.Marshal(definition) + if err != nil { + t.Fatalf("json.Marshal() error = %v", err) + } + if !json.Valid(data) { + t.Fatalf("json.Marshal() returned invalid JSON: %s", data) + } +} + +func TestNewDefinitionWithoutRequiredProperties(t *testing.T) { + definition := NewDefinition("empty", "", nil) + schema := definition.InputSchema.(map[string]any) + if _, ok := schema["required"]; ok { + t.Errorf("InputSchema unexpectedly contains required: %#v", schema) + } + if got := schema["properties"]; !reflect.DeepEqual(got, map[string]any{}) { + t.Errorf("properties = %#v, want empty map", got) + } +} diff --git a/pkg/tool/handler_test.go b/pkg/tool/handler_test.go new file mode 100644 index 0000000..f0f8fe5 --- /dev/null +++ b/pkg/tool/handler_test.go @@ -0,0 +1,100 @@ +package tool + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "github.com/modelcontextprotocol/go-sdk/jsonrpc" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func TestMCPHandler(t *testing.T) { + var got map[string]any + serverTool := ServerTool{ + Tool: &mcp.Tool{Name: "example"}, + Handler: func(_ context.Context, arguments map[string]any) (*mcp.CallToolResult, error) { + got = arguments + return &mcp.CallToolResult{}, nil + }, + } + + result, err := serverTool.MCPHandler()(context.Background(), &mcp.CallToolRequest{ + Params: &mcp.CallToolParamsRaw{Arguments: json.RawMessage(`{"count":2,"nested":{"enabled":true}}`)}, + }) + if err != nil { + t.Fatalf("MCPHandler() error = %v", err) + } + if result == nil { + t.Fatal("MCPHandler() result is nil") + } + if got["count"] != float64(2) { + t.Errorf("count type/value = %T(%v), want float64(2)", got["count"], got["count"]) + } +} + +func TestMCPHandlerRejectsInvalidArguments(t *testing.T) { + called := false + serverTool := ServerTool{ + Tool: &mcp.Tool{Name: "example"}, + Handler: func(context.Context, map[string]any) (*mcp.CallToolResult, error) { + called = true + return &mcp.CallToolResult{}, nil + }, + } + + for _, arguments := range []json.RawMessage{json.RawMessage(`[]`), json.RawMessage(`null`), json.RawMessage(`{"broken"`)} { + _, err := serverTool.MCPHandler()(context.Background(), &mcp.CallToolRequest{ + Params: &mcp.CallToolParamsRaw{Arguments: arguments}, + }) + assertProtocolErrorCode(t, err, jsonrpc.CodeInvalidParams) + } + if called { + t.Fatal("handler was called with invalid arguments") + } +} + +func TestMCPHandlerConvertsErrorsAndRecoversPanics(t *testing.T) { + t.Run("handler error", func(t *testing.T) { + serverTool := ServerTool{ + Tool: &mcp.Tool{Name: "example"}, + Handler: func(context.Context, map[string]any) (*mcp.CallToolResult, error) { + return nil, errors.New("failed") + }, + } + _, err := serverTool.MCPHandler()(context.Background(), &mcp.CallToolRequest{Params: &mcp.CallToolParamsRaw{}}) + assertProtocolErrorCode(t, err, jsonrpc.CodeInternalError) + }) + + t.Run("panic", func(t *testing.T) { + calls := 0 + serverTool := ServerTool{ + Tool: &mcp.Tool{Name: "example"}, + Handler: func(context.Context, map[string]any) (*mcp.CallToolResult, error) { + calls++ + if calls == 1 { + panic("failed") + } + return &mcp.CallToolResult{}, nil + }, + } + handler := serverTool.MCPHandler() + _, err := handler(context.Background(), &mcp.CallToolRequest{Params: &mcp.CallToolParamsRaw{}}) + assertProtocolErrorCode(t, err, jsonrpc.CodeInternalError) + if _, err := handler(context.Background(), &mcp.CallToolRequest{Params: &mcp.CallToolParamsRaw{}}); err != nil { + t.Fatalf("second handler call after panic error = %v", err) + } + }) +} + +func assertProtocolErrorCode(t *testing.T, err error, want int64) { + t.Helper() + var protocolErr *jsonrpc.Error + if !errors.As(err, &protocolErr) { + t.Fatalf("error = %v, want *jsonrpc.Error", err) + } + if protocolErr.Code != want { + t.Errorf("error code = %d, want %d", protocolErr.Code, want) + } +} diff --git a/pkg/tool/tool.go b/pkg/tool/tool.go index 4eb0329..ef3d094 100644 --- a/pkg/tool/tool.go +++ b/pkg/tool/tool.go @@ -1,26 +1,39 @@ package tool import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" "slices" "strings" "gitea.com/gitea/gitea-mcp/pkg/flag" "gitea.com/gitea/gitea-mcp/pkg/log" - "github.com/mark3labs/mcp-go/server" + "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 []server.ServerTool - read []server.ServerTool + write []ServerTool + read []ServerTool } func New(scope string) *Tool { return &Tool{ scope: scope, - write: make([]server.ServerTool, 0, 100), - read: make([]server.ServerTool, 0, 100), + write: make([]ServerTool, 0, 100), + read: make([]ServerTool, 0, 100), } } @@ -29,23 +42,23 @@ func (t *Tool) Scope() string { return t.scope } -func (t *Tool) RegisterWrite(s server.ServerTool) { +func (t *Tool) RegisterWrite(s ServerTool) { t.write = append(t.write, s) } -func (t *Tool) RegisterRead(s server.ServerTool) { +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() []server.ServerTool { +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() []server.ServerTool { +func (t *Tool) WriteTools() []ServerTool { return t.write } @@ -53,8 +66,8 @@ func (t *Tool) WriteTools() []server.ServerTool { // 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)) +func (t *Tool) Tools() []ServerTool { + all := make([]ServerTool, 0, len(t.write)+len(t.read)) if !flag.ReadOnly { all = append(all, t.write...) } @@ -63,7 +76,7 @@ func (t *Tool) Tools() []server.ServerTool { return all } _, scopeAllowed := flag.AllowedScopes[t.scope] - filtered := make([]server.ServerTool, 0, len(all)) + filtered := make([]ServerTool, 0, len(all)) for _, st := range all { _, toolAllowed := flag.AllowedTools[st.Tool.Name] if scopeAllowed || toolAllowed { @@ -73,6 +86,70 @@ func (t *Tool) Tools() []server.ServerTool { 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) { + name := "" + if s.Tool != nil { + name = s.Tool.Name + } + defer func() { + if recovered := recover(); recovered != nil { + panicErr := fmt.Errorf("panic recovered in %s tool handler: %v", name, recovered) + log.Errorf("%s", panicErr) + result = nil + err = &jsonrpc.Error{Code: jsonrpc.CodeInternalError, Message: panicErr.Error()} + } + }() + + if req == nil || req.Params == nil { + return nil, invalidParamsError("missing tool call parameters") + } + + arguments, err := decodeArguments(req.Params.Arguments) + if err != nil { + return nil, err + } + if s.Handler == nil { + return nil, internalError(fmt.Errorf("tool %q has no handler", name)) + } + + result, err = s.Handler(ctx, arguments) + if err != nil { + var protocolErr *jsonrpc.Error + if errors.As(err, &protocolErr) { + return nil, err + } + return nil, internalError(err) + } + return result, nil + } +} + +func decodeArguments(raw json.RawMessage) (map[string]any, error) { + trimmed := bytes.TrimSpace(raw) + if len(trimmed) == 0 { + return map[string]any{}, nil + } + if bytes.Equal(trimmed, []byte("null")) { + return nil, invalidParamsError("tool arguments must be an object") + } + + var arguments map[string]any + if err := json.Unmarshal(trimmed, &arguments); err != nil { + return nil, invalidParamsError(fmt.Sprintf("invalid tool arguments: %v", err)) + } + return arguments, nil +} + +func invalidParamsError(message string) error { + return &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: message} +} + +func internalError(err error) error { + return &jsonrpc.Error{Code: jsonrpc.CodeInternalError, Message: err.Error()} +} + // 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. diff --git a/pkg/tool/tool_test.go b/pkg/tool/tool_test.go index 1fcb761..4faca2d 100644 --- a/pkg/tool/tool_test.go +++ b/pkg/tool/tool_test.go @@ -6,15 +6,14 @@ import ( "gitea.com/gitea/gitea-mcp/pkg/flag" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" ) -func makeTool(name string) server.ServerTool { - return server.ServerTool{Tool: mcp.NewTool(name)} +func makeTool(name string) ServerTool { + return ServerTool{Tool: &mcp.Tool{Name: name}} } -func names(sts []server.ServerTool) []string { +func names(sts []ServerTool) []string { out := make([]string, len(sts)) for i, st := range sts { out[i] = st.Tool.Name