feat(params): add structured argument binding helper

Add params.Bind, which unmarshals a tool call's map[string]any args into a
typed struct via JSON round-trip so JSON numbers land in the correct Go
numeric field types, and enforces `required:"true"` struct tags with clear
errors. Migrate the branch, tree, and file repo handlers to use it instead
of repeated args["x"].(string)/!ok extraction, preserving existing
validation behavior for each field.

Co-Authored-By: Codet <codet@commitgo.dev> (GPT-5-Codex)
This commit is contained in:
Lunny Xiao
2026-08-24 00:14:34 -07:00
parent 815a0e26aa
commit b22ee74148
5 changed files with 288 additions and 117 deletions
+60 -71
View File
@@ -102,30 +102,28 @@ type ContentLine struct {
Content string `json:"content"`
}
type getFileContentArgs struct {
Owner string `json:"owner" required:"true"`
Repo string `json:"repo" required:"true"`
Ref string `json:"ref"`
Path string `json:"path" required:"true"`
WithLines bool `json:"withLines"`
}
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)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
ref, _ := args["ref"].(string)
filePath, err := params.GetString(args, "path")
if err != nil {
var in getFileContentArgs
if err := params.Bind(args, &in); err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
content, _, err := client.Repositories.GetContents(ctx, owner, repo, ref, filePath)
content, _, err := client.Repositories.GetContents(ctx, in.Owner, in.Repo, in.Ref, in.Path)
if err != nil {
return to.ErrorResult(fmt.Errorf("get file err: %v", err))
}
withLines, _ := args["withLines"].(bool)
if withLines {
if in.WithLines {
rawContent, err := base64.StdEncoding.DecodeString(*content.Content)
if err != nil {
return to.ErrorResult(fmt.Errorf("decode base64 content err: %v", err))
@@ -164,49 +162,45 @@ func GetFileContentFn(ctx context.Context, args map[string]any) (*mcp.CallToolRe
return to.TextResult(slimContents(content))
}
type getDirContentArgs struct {
Owner string `json:"owner" required:"true"`
Repo string `json:"repo" required:"true"`
Ref string `json:"ref"`
Path string `json:"path" required:"true"`
}
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)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
ref, _ := args["ref"].(string)
filePath, err := params.GetString(args, "path")
if err != nil {
var in getDirContentArgs
if err := params.Bind(args, &in); err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
content, _, err := client.Repositories.ListContents(ctx, owner, repo, ref, filePath)
content, _, err := client.Repositories.ListContents(ctx, in.Owner, in.Repo, in.Ref, in.Path)
if err != nil {
return to.ErrorResult(fmt.Errorf("get dir content err: %v", err))
}
return to.TextResult(slimDirEntries(content))
}
type createOrUpdateFileArgs struct {
Owner string `json:"owner" required:"true"`
Repo string `json:"repo" required:"true"`
Path string `json:"path" required:"true"`
Content string `json:"content"`
Message string `json:"message"`
BranchName string `json:"branch_name"`
NewBranchName string `json:"new_branch_name"`
SHA string `json:"sha"`
}
func CreateOrUpdateFileFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
owner, err := params.GetString(args, "owner")
if err != nil {
var in createOrUpdateFileArgs
if err := params.Bind(args, &in); err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
filePath, err := params.GetString(args, "path")
if err != nil {
return to.ErrorResult(err)
}
content, _ := args["content"].(string)
message, _ := args["message"].(string)
branchName, _ := args["branch_name"].(string)
newBranchName, _ := args["new_branch_name"].(string)
sha, _ := args["sha"].(string)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
@@ -214,20 +208,20 @@ func CreateOrUpdateFileFn(ctx context.Context, args map[string]any) (*mcp.CallTo
}
fileOpt := gitea_sdk.FileOptions{
Message: message,
BranchName: branchName,
NewBranchName: newBranchName,
Message: in.Message,
BranchName: in.BranchName,
NewBranchName: in.NewBranchName,
}
targetBranch := cmp.Or(newBranchName, branchName)
targetBranch := cmp.Or(in.NewBranchName, in.BranchName)
if sha != "" {
if in.SHA != "" {
// Update existing file
opt := gitea_sdk.UpdateFileOptions{
SHA: sha,
Content: base64.StdEncoding.EncodeToString([]byte(content)),
SHA: in.SHA,
Content: base64.StdEncoding.EncodeToString([]byte(in.Content)),
FileOptions: fileOpt,
}
_, _, err = client.Repositories.UpdateFile(ctx, owner, repo, filePath, opt)
_, _, err = client.Repositories.UpdateFile(ctx, in.Owner, in.Repo, in.Path, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("update file err: %v", err))
}
@@ -236,47 +230,42 @@ func CreateOrUpdateFileFn(ctx context.Context, args map[string]any) (*mcp.CallTo
// Create new file
opt := gitea_sdk.CreateFileOptions{
Content: base64.StdEncoding.EncodeToString([]byte(content)),
Content: base64.StdEncoding.EncodeToString([]byte(in.Content)),
FileOptions: fileOpt,
}
_, _, err = client.Repositories.CreateFile(ctx, owner, repo, filePath, opt)
_, _, err = client.Repositories.CreateFile(ctx, in.Owner, in.Repo, in.Path, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create file err: %v", err))
}
return to.TextResult("Create file success on branch " + targetBranch)
}
type deleteFileArgs struct {
Owner string `json:"owner" required:"true"`
Repo string `json:"repo" required:"true"`
Path string `json:"path" required:"true"`
Message string `json:"message"`
BranchName string `json:"branch_name"`
SHA string `json:"sha" required:"true"`
}
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)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
filePath, err := params.GetString(args, "path")
if err != nil {
return to.ErrorResult(err)
}
message, _ := args["message"].(string)
branchName, _ := args["branch_name"].(string)
sha, err := params.GetString(args, "sha")
if err != nil {
var in deleteFileArgs
if err := params.Bind(args, &in); err != nil {
return to.ErrorResult(err)
}
opt := gitea_sdk.DeleteFileOptions{
FileOptions: gitea_sdk.FileOptions{
Message: message,
BranchName: branchName,
Message: in.Message,
BranchName: in.BranchName,
},
SHA: sha,
SHA: in.SHA,
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.Repositories.DeleteFile(ctx, owner, repo, filePath, opt)
_, err = client.Repositories.DeleteFile(ctx, in.Owner, in.Repo, in.Path, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete file err: %v", err))
}