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) }