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) <noreply@anthropic.com>
This commit is contained in:
Bo-Yi Wu
2026-08-02 21:30:19 +08:00
parent 290d06b40b
commit 80c8b25d6e
49 changed files with 5277 additions and 1528 deletions
+21 -21
View File
@@ -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.
+111 -111
View File
@@ -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)
}
+107 -107
View File
@@ -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 {
+41 -39
View File
@@ -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")
}
+66 -7
View File
@@ -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)
}
}
+120 -124
View File
@@ -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)
}
+29 -37
View File
@@ -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)
}
+75 -80
View File
@@ -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)
}
+59 -61
View File
@@ -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)
}
+3 -3
View File
@@ -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)
}
+36 -41
View File
@@ -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
+48 -23
View File
@@ -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
}
+1 -4
View File
@@ -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() {
+32 -39
View File
@@ -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)
+20 -28
View File
@@ -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")
}
}
+131 -151
View File
@@ -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)
+85 -133
View File
@@ -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)
}
+27 -31
View File
@@ -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)
+20 -23
View File
@@ -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)
+44 -49
View File
@@ -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)
+48 -54
View File
@@ -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)
+46 -49
View File
@@ -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,
+36 -41
View File
@@ -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)
+13 -14
View File
@@ -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)
+3 -1
View File
@@ -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)
}
}
+393
View File
@@ -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()")
}
}
+53 -56
View File
@@ -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)
+6 -4
View File
@@ -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)
}
}
})
+3 -1
View File
@@ -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")
}
}
File diff suppressed because it is too large Load Diff
+67 -68
View File
@@ -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))
+179
View File
@@ -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
}
+14 -15
View File
@@ -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{
+6 -7
View File
@@ -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"
+36 -43
View File
@@ -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)
+1 -6
View File
@@ -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)
}