mirror of
https://gitea.com/gitea/gitea-mcp.git
synced 2025-10-13 17:41:50 +00:00
feat: add wiki management tools (#95)
Fix #94 ## Summary This PR adds wiki management support to gitea-mcp adding new tools: creating, reading, updating, and deleting wiki pages. ## Changes - Added `operation/wiki/wiki.go` with wiki tools - Updated `operation/operation.go` to register it - Updated `README.md` ## New Tools - `list_wiki_pages` - List all wiki pages in a repository - `get_wiki_page` - Get wiki page content and metadata - `get_wiki_revisions` - Get revision history of a wiki page - `create_wiki_page` - Create a new wiki page - `update_wiki_page` - Update an existing wiki page - `delete_wiki_page` - Delete a wiki page ## Implementation Details - Uses direct HTTP calls to Gitea wiki API endpoints (v1.16.0+) - Follows existing MCP patterns and error handling - Includes fallback logic to prevent "unnamed" pages during updates - Proper base64 content encoding as per Gitea API spec ## Testing - All 6 tools tested and working correctly - Error handling validated - Integration with existing MCP server confirmed - Made a test repo & simulated a drone construction using Claude Code (in french sorry) at https://git.kernelpanik.fr/Test-Organization/test_wiki_tools/wiki Ready for review. Closes #[94] Co-authored-by: nox <nox@noxen.net> Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/95 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Reviewed-by: Bo-Yi Wu (吳柏毅) <appleboy.tw@gmail.com> Co-authored-by: Thierry PROST <3kynox@noreply.gitea.com> Co-committed-by: Thierry PROST <3kynox@noreply.gitea.com>
This commit is contained in:
committed by
Bo-Yi Wu (吳柏毅)
parent
de311344cd
commit
95ab3a4b73
@@ -14,6 +14,7 @@ import (
|
||||
"gitea.com/gitea/gitea-mcp/operation/search"
|
||||
"gitea.com/gitea/gitea-mcp/operation/user"
|
||||
"gitea.com/gitea/gitea-mcp/operation/version"
|
||||
"gitea.com/gitea/gitea-mcp/operation/wiki"
|
||||
mcpContext "gitea.com/gitea/gitea-mcp/pkg/context"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||
@@ -45,6 +46,9 @@ func RegisterTool(s *server.MCPServer) {
|
||||
// Version Tool
|
||||
s.AddTools(version.Tool.Tools()...)
|
||||
|
||||
// Wiki Tool
|
||||
s.AddTools(wiki.Tool.Tools()...)
|
||||
|
||||
s.DeleteTools("")
|
||||
}
|
||||
|
||||
|
363
operation/wiki/wiki.go
Normal file
363
operation/wiki/wiki.go
Normal file
@@ -0,0 +1,363 @@
|
||||
package wiki
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||
"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"
|
||||
)
|
||||
|
||||
var Tool = tool.New()
|
||||
|
||||
const (
|
||||
ListWikiPagesToolName = "list_wiki_pages"
|
||||
GetWikiPageToolName = "get_wiki_page"
|
||||
GetWikiRevisionsToolName = "get_wiki_revisions"
|
||||
CreateWikiPageToolName = "create_wiki_page"
|
||||
UpdateWikiPageToolName = "update_wiki_page"
|
||||
DeleteWikiPageToolName = "delete_wiki_page"
|
||||
)
|
||||
|
||||
var (
|
||||
ListWikiPagesTool = mcp.NewTool(
|
||||
ListWikiPagesToolName,
|
||||
mcp.WithDescription("List all wiki pages in a repository"),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
)
|
||||
|
||||
GetWikiPageTool = mcp.NewTool(
|
||||
GetWikiPageToolName,
|
||||
mcp.WithDescription("Get a wiki page content and metadata"),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithString("pageName", mcp.Required(), mcp.Description("wiki page name")),
|
||||
)
|
||||
|
||||
GetWikiRevisionsTool = mcp.NewTool(
|
||||
GetWikiRevisionsToolName,
|
||||
mcp.WithDescription("Get revisions history of a wiki page"),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithString("pageName", mcp.Required(), mcp.Description("wiki page name")),
|
||||
)
|
||||
|
||||
CreateWikiPageTool = mcp.NewTool(
|
||||
CreateWikiPageToolName,
|
||||
mcp.WithDescription("Create a new wiki page"),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithString("title", mcp.Required(), mcp.Description("wiki page title")),
|
||||
mcp.WithString("content_base64", mcp.Required(), mcp.Description("page content, base64 encoded")),
|
||||
mcp.WithString("message", mcp.Description("commit message (optional)")),
|
||||
)
|
||||
|
||||
UpdateWikiPageTool = mcp.NewTool(
|
||||
UpdateWikiPageToolName,
|
||||
mcp.WithDescription("Update an existing wiki page"),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithString("pageName", mcp.Required(), mcp.Description("current wiki page name")),
|
||||
mcp.WithString("title", mcp.Description("new page title (optional)")),
|
||||
mcp.WithString("content_base64", mcp.Required(), mcp.Description("page content, base64 encoded")),
|
||||
mcp.WithString("message", mcp.Description("commit message (optional)")),
|
||||
)
|
||||
|
||||
DeleteWikiPageTool = mcp.NewTool(
|
||||
DeleteWikiPageToolName,
|
||||
mcp.WithDescription("Delete a wiki page"),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithString("pageName", mcp.Required(), mcp.Description("wiki page name to delete")),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: ListWikiPagesTool,
|
||||
Handler: ListWikiPagesFn,
|
||||
})
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: GetWikiPageTool,
|
||||
Handler: GetWikiPageFn,
|
||||
})
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: GetWikiRevisionsTool,
|
||||
Handler: GetWikiRevisionsFn,
|
||||
})
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: CreateWikiPageTool,
|
||||
Handler: CreateWikiPageFn,
|
||||
})
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: UpdateWikiPageTool,
|
||||
Handler: UpdateWikiPageFn,
|
||||
})
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: DeleteWikiPageTool,
|
||||
Handler: DeleteWikiPageFn,
|
||||
})
|
||||
}
|
||||
|
||||
func ListWikiPagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called ListWikiPagesFn")
|
||||
owner, ok := req.GetArguments()["owner"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||
}
|
||||
repo, ok := req.GetArguments()["repo"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
// Use direct HTTP request because SDK does not support yet wikis
|
||||
result, err := makeWikiAPIRequest(ctx, client, "GET", fmt.Sprintf("repos/%s/%s/wiki/pages", url.QueryEscape(owner), url.QueryEscape(repo)), nil)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("list wiki pages err: %v", err))
|
||||
}
|
||||
|
||||
return to.TextResult(result)
|
||||
}
|
||||
|
||||
func GetWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called GetWikiPageFn")
|
||||
owner, ok := req.GetArguments()["owner"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||
}
|
||||
repo, ok := req.GetArguments()["repo"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||
}
|
||||
pageName, ok := req.GetArguments()["pageName"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("pageName is required"))
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
result, err := makeWikiAPIRequest(ctx, client, "GET", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get wiki page err: %v", err))
|
||||
}
|
||||
|
||||
return to.TextResult(result)
|
||||
}
|
||||
|
||||
func GetWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called GetWikiRevisionsFn")
|
||||
owner, ok := req.GetArguments()["owner"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||
}
|
||||
repo, ok := req.GetArguments()["repo"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||
}
|
||||
pageName, ok := req.GetArguments()["pageName"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("pageName is required"))
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
result, err := makeWikiAPIRequest(ctx, client, "GET", fmt.Sprintf("repos/%s/%s/wiki/revisions/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get wiki revisions err: %v", err))
|
||||
}
|
||||
|
||||
return to.TextResult(result)
|
||||
}
|
||||
|
||||
func CreateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called CreateWikiPageFn")
|
||||
owner, ok := req.GetArguments()["owner"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||
}
|
||||
repo, ok := req.GetArguments()["repo"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||
}
|
||||
title, ok := req.GetArguments()["title"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("title is required"))
|
||||
}
|
||||
contentBase64, ok := req.GetArguments()["content_base64"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("content_base64 is required"))
|
||||
}
|
||||
|
||||
message, _ := req.GetArguments()["message"].(string)
|
||||
if message == "" {
|
||||
message = fmt.Sprintf("Create wiki page '%s'", title)
|
||||
}
|
||||
|
||||
requestBody := map[string]string{
|
||||
"title": title,
|
||||
"content_base64": contentBase64,
|
||||
"message": message,
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
result, err := makeWikiAPIRequest(ctx, client, "POST", fmt.Sprintf("repos/%s/%s/wiki/new", url.QueryEscape(owner), url.QueryEscape(repo)), requestBody)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("create wiki page err: %v", err))
|
||||
}
|
||||
|
||||
return to.TextResult(result)
|
||||
}
|
||||
|
||||
func UpdateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called UpdateWikiPageFn")
|
||||
owner, ok := req.GetArguments()["owner"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||
}
|
||||
repo, ok := req.GetArguments()["repo"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||
}
|
||||
pageName, ok := req.GetArguments()["pageName"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("pageName is required"))
|
||||
}
|
||||
contentBase64, ok := req.GetArguments()["content_base64"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("content_base64 is required"))
|
||||
}
|
||||
|
||||
requestBody := map[string]string{
|
||||
"content_base64": contentBase64,
|
||||
}
|
||||
|
||||
// If title is given, use it. Otherwise, keep current page name
|
||||
if title, ok := req.GetArguments()["title"].(string); ok && title != "" {
|
||||
requestBody["title"] = title
|
||||
} else {
|
||||
// Utiliser pageName comme fallback pour éviter "unnamed"
|
||||
requestBody["title"] = pageName
|
||||
}
|
||||
|
||||
if message, ok := req.GetArguments()["message"].(string); ok && message != "" {
|
||||
requestBody["message"] = message
|
||||
} else {
|
||||
requestBody["message"] = fmt.Sprintf("Update wiki page '%s'", pageName)
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
result, err := makeWikiAPIRequest(ctx, client, "PATCH", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), requestBody)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("update wiki page err: %v", err))
|
||||
}
|
||||
|
||||
return to.TextResult(result)
|
||||
}
|
||||
|
||||
func DeleteWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called DeleteWikiPageFn")
|
||||
owner, ok := req.GetArguments()["owner"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||
}
|
||||
repo, ok := req.GetArguments()["repo"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||
}
|
||||
pageName, ok := req.GetArguments()["pageName"].(string)
|
||||
if !ok {
|
||||
return to.ErrorResult(fmt.Errorf("pageName is required"))
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
_, err = makeWikiAPIRequest(ctx, client, "DELETE", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("delete wiki page err: %v", err))
|
||||
}
|
||||
|
||||
return to.TextResult(map[string]string{"message": "Wiki page deleted successfully"})
|
||||
}
|
||||
|
||||
// Helper function to make HTTP requests to Gitea Wiki API
|
||||
func makeWikiAPIRequest(ctx context.Context, client interface{}, method, path string, body interface{}) (interface{}, error) {
|
||||
// Use flags to get base URL and token
|
||||
apiURL := fmt.Sprintf("%s/api/v1/%s", flag.Host, path)
|
||||
|
||||
httpClient := &http.Client{}
|
||||
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
reqBody = strings.NewReader(string(bodyBytes))
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, apiURL, reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", flag.Token))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("API request failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if method == "DELETE" {
|
||||
return map[string]string{"message": "success"}, nil
|
||||
}
|
||||
|
||||
var result interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
Reference in New Issue
Block a user