From 0bdf8f5bb3b08b39edd4e61eb025c3eceaa2c4e7 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Mar 2026 01:49:40 +0000 Subject: [PATCH] wiki: accept plain text `content` instead of `content_base64` (#156) The `wiki_write` tool exposed the Gitea API's `content_base64` parameter directly, requiring callers to provide base64-encoded content. This caused LLM agents to incorrectly infer that other tools like `create_or_update_file` also require base64 encoding, corrupting files with literal base64 strings. Rename the parameter to `content` (plain text) and handle base64 encoding internally, matching the pattern already used by `create_or_update_file`. Also added test coverage for this. Closes #151 Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/156 Reviewed-by: Lunny Xiao Co-authored-by: silverwind Co-committed-by: silverwind --- operation/wiki/wiki.go | 11 +++--- operation/wiki/wiki_test.go | 75 +++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 operation/wiki/wiki_test.go diff --git a/operation/wiki/wiki.go b/operation/wiki/wiki.go index 1e4373b..b019724 100644 --- a/operation/wiki/wiki.go +++ b/operation/wiki/wiki.go @@ -2,6 +2,7 @@ package wiki import ( "context" + "encoding/base64" "fmt" "net/url" @@ -40,7 +41,7 @@ var ( mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("pageName", mcp.Description("wiki page name (required for 'update', 'delete')")), mcp.WithString("title", mcp.Description("wiki page title (required for 'create', optional for 'update')")), - mcp.WithString("content_base64", mcp.Description("page content, base64 encoded (required for 'create', 'update')")), + mcp.WithString("content", mcp.Description("page content (required for 'create', 'update')")), mcp.WithString("message", mcp.Description("commit message")), ) ) @@ -176,7 +177,7 @@ func createWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo if err != nil { return to.ErrorResult(err) } - contentBase64, err := params.GetString(args, "content_base64") + content, err := params.GetString(args, "content") if err != nil { return to.ErrorResult(err) } @@ -188,7 +189,7 @@ func createWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo requestBody := map[string]string{ "title": title, - "content_base64": contentBase64, + "content_base64": base64.StdEncoding.EncodeToString([]byte(content)), "message": message, } @@ -216,13 +217,13 @@ func updateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo if err != nil { return to.ErrorResult(err) } - contentBase64, err := params.GetString(args, "content_base64") + content, err := params.GetString(args, "content") if err != nil { return to.ErrorResult(err) } requestBody := map[string]string{ - "content_base64": contentBase64, + "content_base64": base64.StdEncoding.EncodeToString([]byte(content)), } // If title is given, use it. Otherwise, keep current page name diff --git a/operation/wiki/wiki_test.go b/operation/wiki/wiki_test.go new file mode 100644 index 0000000..e72137d --- /dev/null +++ b/operation/wiki/wiki_test.go @@ -0,0 +1,75 @@ +package wiki + +import ( + "context" + "encoding/base64" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + 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) { + tests := []struct { + name string + method string + content string + }{ + {"create ascii", "create", "Hello, World!"}, + {"create unicode", "create", "日本語テスト 🎉"}, + {"create multiline", "create", "line1\nline2\nline3"}, + {"update ascii", "update", "Updated content"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var gotBody map[string]string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + json.Unmarshal(body, &gotBody) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"title":"test"}`)) + })) + defer srv.Close() + + origHost := flag.Host + flag.Host = srv.URL + defer func() { flag.Host = origHost }() + + ctx := context.WithValue(context.Background(), mcpContext.TokenContextKey, "test-token") + + args := map[string]any{ + "method": tt.method, + "owner": "org", + "repo": "repo", + "content": tt.content, + "pageName": "TestPage", + "title": "TestPage", + } + + req := mcp.CallToolRequest{} + req.Params.Arguments = args + + result, err := wikiWriteFn(ctx, req) + if err != nil { + t.Fatalf("wikiWriteFn() error: %v", err) + } + if result.IsError { + t.Fatalf("wikiWriteFn() returned error result") + } + + got := gotBody["content_base64"] + want := base64.StdEncoding.EncodeToString([]byte(tt.content)) + if got != want { + t.Errorf("content_base64 = %q, want %q", got, want) + } + }) + } +}