Files
ginimageApi/app/mcp/server_mcpgo.go
Beyhan Oğur e04ba85564 first commit
2026-04-26 21:40:14 +03:00

304 lines
8.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mcp
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"sync"
"time"
mcpModels "ginimageApi/app/mcp/models"
"ginimageApi/configs"
mcpgo "github.com/mark3labs/mcp-go/mcp"
mcpserver "github.com/mark3labs/mcp-go/server"
)
type mcpBaseURLKey struct{}
var (
mcpGoOnce sync.Once
mcpGoServer *mcpserver.MCPServer
mcpGoHTTPHandler http.Handler
mcpGoMu sync.RWMutex
)
func getMCPGoHTTPHandler() http.Handler {
mcpGoOnce.Do(func() {
mcpGoServer = newMCPGoServer()
mcpGoHTTPHandler = mcpserver.NewStreamableHTTPServer(
mcpGoServer,
mcpserver.WithStateLess(true),
mcpserver.WithHTTPContextFunc(func(ctx context.Context, r *http.Request) context.Context {
return context.WithValue(ctx, mcpBaseURLKey{}, resolveBaseURLFromRequest(r))
}),
)
})
mcpGoMu.RLock()
defer mcpGoMu.RUnlock()
return mcpGoHTTPHandler
}
// reloadMCPGoServer yeni bir MD rehber eklendikten sonra MCP server'i yeniden olusturur.
func reloadMCPGoServer() {
mcpGoMu.Lock()
defer mcpGoMu.Unlock()
mcpGoOnce = sync.Once{} // sıfırla
newServer := newMCPGoServer()
newHandler := mcpserver.NewStreamableHTTPServer(
newServer,
mcpserver.WithStateLess(true),
mcpserver.WithHTTPContextFunc(func(ctx context.Context, r *http.Request) context.Context {
return context.WithValue(ctx, mcpBaseURLKey{}, resolveBaseURLFromRequest(r))
}),
)
mcpGoServer = newServer
mcpGoHTTPHandler = newHandler
}
func newMCPGoServer() *mcpserver.MCPServer {
s := mcpserver.NewMCPServer("ginimage-api-mcp", "0.1.0")
s.AddTool(
mcpgo.NewTool(
"api_overview",
mcpgo.WithDescription("GinImage API endpoint ozeti ve kullanimi."),
),
withToolRunLog("api_overview", func(_ context.Context, _ mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
return mcpgo.NewToolResultText(apiOverviewText()), nil
}),
)
s.AddTool(
mcpgo.NewTool(
"health_check",
mcpgo.WithDescription("API health endpoint durumunu kontrol eder."),
mcpgo.WithString("path", mcpgo.Description("Kontrol edilecek path. Varsayilan: /swagger/index.html")),
),
withToolRunLog("health_check", func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
path := req.GetString("path", "/swagger/index.html")
baseURL := resolveBaseURLFromContext(ctx)
url := strings.TrimRight(baseURL, "/") + ensurePathPrefix(path)
resp, err := http.Get(url) //nolint:gosec
if err != nil {
return mcpgo.NewToolResultText(fmt.Sprintf("Health check failed: %v", err)), nil
}
defer resp.Body.Close()
return mcpgo.NewToolResultText(fmt.Sprintf("Health check: %s -> HTTP %d", url, resp.StatusCode)), nil
}),
)
s.AddTool(
mcpgo.NewTool(
"md_guide_list",
mcpgo.WithDescription("docs/mcp-tools altindaki markdown rehber dosyalarini listeler."),
),
withToolRunLog("md_guide_list", func(_ context.Context, _ mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
guides, err := listMDGuides()
if err != nil {
return mcpgo.NewToolResultError("failed to list guides"), nil
}
if len(guides) == 0 {
return mcpgo.NewToolResultText("No markdown guides found under docs/mcp-tools"), nil
}
return mcpgo.NewToolResultText("Available guides:\n- " + strings.Join(guides, "\n- ")), nil
}),
)
s.AddTool(
mcpgo.NewTool(
"md_guide_get",
mcpgo.WithDescription("Secilen markdown rehber dosyasinin icerigini dondurur."),
mcpgo.WithString(
"guide",
mcpgo.Description("Rehber dosya adi. Ornek: codebase_map.md"),
mcpgo.Required(),
),
),
withToolRunLog("md_guide_get", func(_ context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
guide, err := req.RequireString("guide")
if err != nil {
return mcpgo.NewToolResultError("invalid params"), nil
}
content, err := readMDGuide(guide)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
return mcpgo.NewToolResultText(content), nil
}),
)
s.AddTool(
mcpgo.NewTool(
"codebase_map",
mcpgo.WithDescription("Proje klasor ve kritik dosya yapisini ozetler."),
mcpgo.WithString("focus", mcpgo.Description("Opsiyonel odak klasoru. Ornek: app/images")),
mcpgo.WithNumber("depth", mcpgo.Description("Opsiyonel tarama derinligi. Varsayilan 2, maksimum 5")),
),
withToolRunLog("codebase_map", func(_ context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
focus := req.GetString("focus", "")
depth := req.GetInt("depth", 0)
text, err := buildCodebaseMap(focus, depth)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
return mcpgo.NewToolResultText(text), nil
}),
)
s.AddTool(
mcpgo.NewTool(
"tool_stats",
mcpgo.WithDescription("MCP tool kullanim istatistiklerini veritabanindan ozetler."),
mcpgo.WithNumber("limit", mcpgo.Description("Opsiyonel kayit limiti. Varsayilan 10, maksimum 50")),
),
withToolRunLog("tool_stats", func(_ context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
limit := req.GetInt("limit", 10)
statsText, err := getToolStats(limit)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
return mcpgo.NewToolResultText(statsText), nil
}),
)
registerMDGuideTools(s)
return s
}
// registerMDGuideTools docs/mcp-tools/ altindaki her .md dosyasini ayri bir tool olarak kaydeder.
func registerMDGuideTools(s *mcpserver.MCPServer) {
guides, err := listMDGuides()
if err != nil || len(guides) == 0 {
return
}
for _, guide := range guides {
guideName := guide // closure icin kopyala
toolName := mdGuideToolName(guideName)
description := fmt.Sprintf("Rehber: %s — MCP guide dokumani.", guideName)
s.AddTool(
mcpgo.NewTool(toolName, mcpgo.WithDescription(description)),
withToolRunLog(toolName, func(_ context.Context, _ mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
content, err := readMDGuide(guideName)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
return mcpgo.NewToolResultText(content), nil
}),
)
}
}
// mdGuideToolName dosya adini gecerli bir tool adina donusturur.
// Ornek: "codebase_map.md" -> "guide_codebase_map"
func mdGuideToolName(filename string) string {
name := strings.TrimSuffix(filename, ".md")
// Alfanumerik ve alt cizgi disindaki karakterleri _ ile degistir
var b strings.Builder
for _, r := range name {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
} else {
b.WriteRune('_')
}
}
return "guide_" + b.String()
}
func withToolRunLog(toolName string, next mcpserver.ToolHandlerFunc) mcpserver.ToolHandlerFunc {
return func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
started := time.Now()
result, err := next(ctx, req)
duration := time.Since(started)
status := "success"
errMessage := ""
if err != nil {
status = "error"
errMessage = err.Error()
} else if result != nil && result.IsError {
status = "error"
errMessage = extractToolResultText(result)
}
argsText := ""
if raw, marshalErr := json.Marshal(req.GetArguments()); marshalErr == nil {
argsText = string(raw)
}
if len(argsText) > 4096 {
argsText = argsText[:4096]
}
if configs.DB != nil && strings.TrimSpace(toolName) != "" {
run := mcpModels.ToolRun{
ToolName: toolName,
Status: status,
DurationMs: duration.Milliseconds(),
ErrorMessage: errMessage,
ArgumentsRaw: argsText,
}
_ = configs.DB.Create(&run).Error
}
return result, err
}
}
func extractToolResultText(result *mcpgo.CallToolResult) string {
if result == nil || len(result.Content) == 0 {
return "tool error"
}
for _, content := range result.Content {
if textContent, ok := content.(mcpgo.TextContent); ok {
if strings.TrimSpace(textContent.Text) != "" {
return textContent.Text
}
}
}
return "tool error"
}
func resolveBaseURLFromContext(ctx context.Context) string {
if envURL := strings.TrimSpace(os.Getenv("GINIMAGE_API_BASE_URL")); envURL != "" {
return envURL
}
if fromCtx, ok := ctx.Value(mcpBaseURLKey{}).(string); ok && strings.TrimSpace(fromCtx) != "" {
return fromCtx
}
port := strings.TrimSpace(os.Getenv("PORT"))
if port == "" {
port = "8080"
}
return "http://127.0.0.1:" + port
}
func resolveBaseURLFromRequest(r *http.Request) string {
if envURL := strings.TrimSpace(os.Getenv("GINIMAGE_API_BASE_URL")); envURL != "" {
return envURL
}
if r == nil {
return resolveBaseURLFromContext(context.Background())
}
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
return fmt.Sprintf("%s://%s", scheme, r.Host)
}
func runMCPGoFromEnv() error {
if mcpGoServer == nil {
_ = getMCPGoHTTPHandler()
}
return mcpserver.ServeStdio(mcpGoServer)
}