feat: slim tool responses (#141)

Reduce token usage by slimming tool responses. Instead of returning full Gitea SDK objects (with nested user/repo objects, avatars, permissions, etc.), each operation now has a colocated `slim.go` that extracts only the fields an LLM needs. List endpoints return even fewer fields than single-item endpoints.

Other changes:
- Add `params` helpers to DRY parameter extraction across 40+ handlers
- Remove `{"Result": ...}` wrapper for flatter responses
- Reduce default pageSize from 100 to 30

Fixes: https://gitea.com/gitea/gitea-mcp/issues/128

*Created by Claude on behalf of @silverwind*

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/141
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
This commit is contained in:
silverwind
2026-03-05 05:56:23 +00:00
committed by silverwind
parent 9ce5604e4c
commit c3db4fb65f
35 changed files with 2274 additions and 1156 deletions

View File

@@ -41,7 +41,7 @@ var (
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)),
)
GetRepoLabelTool = mcp.NewTool(
@@ -121,7 +121,7 @@ var (
mcp.WithDescription("Lists labels defined at organization level"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)),
)
CreateOrgLabelTool = mcp.NewTool(
@@ -210,21 +210,20 @@ func init() {
func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoLabelsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(req.GetArguments(), "repo")
if err != nil {
return to.ErrorResult(err)
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
page, pageSize := params.GetPagination(req.GetArguments(), 30)
opt := gitea_sdk.ListLabelsOptions{
ListOptions: gitea_sdk.ListOptions{
Page: int(page),
PageSize: int(pageSize),
Page: page,
PageSize: pageSize,
},
}
client, err := gitea.ClientFromContext(ctx)
@@ -235,18 +234,18 @@ func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
if err != nil {
return to.ErrorResult(fmt.Errorf("list %v/%v/labels err: %v", owner, repo, err))
}
return to.TextResult(labels)
return to.TextResult(slimLabels(labels))
}
func GetRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetRepoLabelFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(req.GetArguments(), "repo")
if err != nil {
return to.ErrorResult(err)
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
@@ -261,26 +260,26 @@ func GetRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/label/%v err: %v", owner, repo, id, err))
}
return to.TextResult(label)
return to.TextResult(slimLabel(label))
}
func CreateRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateRepoLabelFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(req.GetArguments(), "repo")
if err != nil {
return to.ErrorResult(err)
}
name, ok := req.GetArguments()["name"].(string)
if !ok {
return to.ErrorResult(errors.New("name is required"))
name, err := params.GetString(req.GetArguments(), "name")
if err != nil {
return to.ErrorResult(err)
}
color, ok := req.GetArguments()["color"].(string)
if !ok {
return to.ErrorResult(errors.New("color is required"))
color, err := params.GetString(req.GetArguments(), "color")
if err != nil {
return to.ErrorResult(err)
}
description, _ := req.GetArguments()["description"].(string) // Optional
@@ -298,18 +297,18 @@ func CreateRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
if err != nil {
return to.ErrorResult(fmt.Errorf("create %v/%v/label err: %v", owner, repo, err))
}
return to.TextResult(label)
return to.TextResult(slimLabel(label))
}
func EditRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditRepoLabelFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(req.GetArguments(), "repo")
if err != nil {
return to.ErrorResult(err)
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
@@ -335,18 +334,18 @@ func EditRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/%v/label/%v err: %v", owner, repo, id, err))
}
return to.TextResult(label)
return to.TextResult(slimLabel(label))
}
func DeleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteRepoLabelFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(req.GetArguments(), "repo")
if err != nil {
return to.ErrorResult(err)
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
@@ -366,13 +365,13 @@ func DeleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AddIssueLabelsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(req.GetArguments(), "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
@@ -403,18 +402,18 @@ func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
if err != nil {
return to.ErrorResult(fmt.Errorf("add labels to %v/%v/issue/%v err: %v", owner, repo, index, err))
}
return to.TextResult(issueLabels)
return to.TextResult(slimLabels(issueLabels))
}
func ReplaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ReplaceIssueLabelsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(req.GetArguments(), "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
@@ -445,18 +444,18 @@ func ReplaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
if err != nil {
return to.ErrorResult(fmt.Errorf("replace labels on %v/%v/issue/%v err: %v", owner, repo, index, err))
}
return to.TextResult(issueLabels)
return to.TextResult(slimLabels(issueLabels))
}
func ClearIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ClearIssueLabelsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(req.GetArguments(), "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
@@ -476,13 +475,13 @@ func ClearIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
func RemoveIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called RemoveIssueLabelFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
repo, err := params.GetString(req.GetArguments(), "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
@@ -506,17 +505,16 @@ func RemoveIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
func ListOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListOrgLabelsFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
org, err := params.GetString(req.GetArguments(), "org")
if err != nil {
return to.ErrorResult(err)
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
page, pageSize := params.GetPagination(req.GetArguments(), 30)
opt := gitea_sdk.ListOrgLabelsOptions{
ListOptions: gitea_sdk.ListOptions{
Page: int(page),
PageSize: int(pageSize),
Page: page,
PageSize: pageSize,
},
}
client, err := gitea.ClientFromContext(ctx)
@@ -527,22 +525,22 @@ func ListOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
if err != nil {
return to.ErrorResult(fmt.Errorf("list %v/labels err: %v", org, err))
}
return to.TextResult(labels)
return to.TextResult(slimLabels(labels))
}
func CreateOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateOrgLabelFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
org, err := params.GetString(req.GetArguments(), "org")
if err != nil {
return to.ErrorResult(err)
}
name, ok := req.GetArguments()["name"].(string)
if !ok {
return to.ErrorResult(errors.New("name is required"))
name, err := params.GetString(req.GetArguments(), "name")
if err != nil {
return to.ErrorResult(err)
}
color, ok := req.GetArguments()["color"].(string)
if !ok {
return to.ErrorResult(errors.New("color is required"))
color, err := params.GetString(req.GetArguments(), "color")
if err != nil {
return to.ErrorResult(err)
}
description, _ := req.GetArguments()["description"].(string)
exclusive, _ := req.GetArguments()["exclusive"].(bool)
@@ -562,14 +560,14 @@ func CreateOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
if err != nil {
return to.ErrorResult(fmt.Errorf("create %v/labels err: %v", org, err))
}
return to.TextResult(label)
return to.TextResult(slimLabel(label))
}
func EditOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditOrgLabelFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
org, err := params.GetString(req.GetArguments(), "org")
if err != nil {
return to.ErrorResult(err)
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
@@ -598,14 +596,14 @@ func EditOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/labels/%v err: %v", org, id, err))
}
return to.TextResult(label)
return to.TextResult(slimLabel(label))
}
func DeleteOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteOrgLabelFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
org, err := params.GetString(req.GetArguments(), "org")
if err != nil {
return to.ErrorResult(err)
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {

26
operation/label/slim.go Normal file
View File

@@ -0,0 +1,26 @@
package label
import (
gitea_sdk "code.gitea.io/sdk/gitea"
)
func slimLabel(l *gitea_sdk.Label) map[string]any {
if l == nil {
return nil
}
return map[string]any{
"id": l.ID,
"name": l.Name,
"color": l.Color,
"description": l.Description,
"exclusive": l.Exclusive,
}
}
func slimLabels(labels []*gitea_sdk.Label) []map[string]any {
out := make([]map[string]any, 0, len(labels))
for _, l := range labels {
out = append(out, slimLabel(l))
}
return out
}

View File

@@ -0,0 +1,25 @@
package label
import (
"testing"
gitea_sdk "code.gitea.io/sdk/gitea"
)
func TestSlimLabel(t *testing.T) {
l := &gitea_sdk.Label{
ID: 1,
Name: "bug",
Color: "#d73a4a",
Description: "Something isn't working",
Exclusive: false,
}
m := slimLabel(l)
if m["name"] != "bug" {
t.Errorf("expected name bug, got %v", m["name"])
}
if m["color"] != "#d73a4a" {
t.Errorf("expected color, got %v", m["color"])
}
}