first commit
This commit is contained in:
303
app/mcp/server_mcpgo.go
Normal file
303
app/mcp/server_mcpgo.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user