first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:40:14 +03:00
commit e04ba85564
129 changed files with 17541 additions and 0 deletions

303
app/mcp/server_mcpgo.go Normal file
View File

@@ -0,0 +1,303 @@
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)
}