c8b9955f35
- Comment out grep_search, github_file, github_batch_files tools - Grep.app is hitting rate limits / Vercel security checkpoint - Keep SearXNG web search as primary working feature - Downgrade mcp-go to v0.18.0 for Go 1.23 compatibility
680 lines
20 KiB
Go
680 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/mark3labs/mcp-go/server"
|
|
)
|
|
|
|
var searxngClient *SearXNGClient
|
|
var grepAppClient *GrepAppClient
|
|
var dockerManager *DockerManager
|
|
|
|
func main() {
|
|
var transport string
|
|
var host string
|
|
var port string
|
|
var searxngURL string
|
|
var searxngPort string
|
|
var searxngImage string
|
|
var autostart bool
|
|
var stopOnExit bool
|
|
var searxngTimeout int
|
|
|
|
flag.StringVar(&transport, "t", "stdio", "Transport type (stdio or sse)")
|
|
flag.StringVar(&host, "h", "0.0.0.0", "Host of sse server")
|
|
flag.StringVar(&port, "p", "8892", "Port of sse server")
|
|
flag.StringVar(&searxngURL, "searxng", "http://127.0.0.1:8080", "SearXNG instance URL (overrides -searxng-port)")
|
|
flag.StringVar(&searxngPort, "searxng-port", "8080", "Port for SearXNG container")
|
|
flag.StringVar(&searxngImage, "searxng-image", SearXNGImage, "Docker image for SearXNG")
|
|
flag.BoolVar(&autostart, "searxng-autostart", true, "Auto-start SearXNG Docker container if not running")
|
|
flag.BoolVar(&stopOnExit, "searxng-stop-on-exit", false, "Stop SearXNG container when MCP server exits")
|
|
flag.IntVar(&searxngTimeout, "searxng-timeout", 60, "Timeout in seconds to wait for SearXNG to become ready")
|
|
flag.Parse()
|
|
|
|
// Initialize clients
|
|
searxngClient = NewSearXNGClient(searxngURL)
|
|
grepAppClient = NewGrepAppClient()
|
|
dockerManager = NewDockerManager(searxngPort, searxngImage, autostart, stopOnExit)
|
|
|
|
// Clean up on exit
|
|
defer dockerManager.Close()
|
|
|
|
// Version 2.0.0 - with auto-managed SearXNG
|
|
mcpServer := server.NewMCPServer(
|
|
"search-mcp",
|
|
"2.0.0",
|
|
)
|
|
|
|
// SearXNG Search Tools
|
|
searchTool := mcp.NewTool("searxng_search",
|
|
mcp.WithDescription("Search information through SearXNG. Supports various categories and search engines. Auto-starts SearXNG if not running."),
|
|
mcp.WithString("query",
|
|
mcp.Required(),
|
|
mcp.Description("Search query"),
|
|
),
|
|
mcp.WithString("categories",
|
|
mcp.Description("Search categories (general, images, videos, news, music, files, science, it). Multiple values separated by comma"),
|
|
),
|
|
mcp.WithString("engines",
|
|
mcp.Description("Search engines (google, bing, duckduckgo, yandex, etc.). Multiple values separated by comma"),
|
|
),
|
|
mcp.WithString("language",
|
|
mcp.Description("Search language (ru, en, de, fr, etc.)"),
|
|
),
|
|
mcp.WithNumber("page",
|
|
mcp.Description("Page number of results (default 1)"),
|
|
),
|
|
mcp.WithString("time_range",
|
|
mcp.Description("Time range (day, week, month, year)"),
|
|
),
|
|
mcp.WithNumber("safe_search",
|
|
mcp.Description("Safe search (0 - disabled, 1 - moderate, 2 - strict)"),
|
|
),
|
|
)
|
|
mcpServer.AddTool(searchTool, searxngSearchHandler)
|
|
|
|
enginesInfoTool := mcp.NewTool("searxng_engines_info",
|
|
mcp.WithDescription("Get information about available SearXNG search engines and categories"),
|
|
)
|
|
mcpServer.AddTool(enginesInfoTool, searxngEnginesInfoHandler)
|
|
|
|
imageSearchTool := mcp.NewTool("searxng_image_search",
|
|
mcp.WithDescription("Specialized image search through SearXNG. Auto-starts SearXNG if not running."),
|
|
mcp.WithString("query",
|
|
mcp.Required(),
|
|
mcp.Description("Search query for images"),
|
|
),
|
|
mcp.WithString("engines",
|
|
mcp.Description("Image search engines (google images, bing images, flickr, etc.)"),
|
|
),
|
|
mcp.WithNumber("page",
|
|
mcp.Description("Page number of results"),
|
|
),
|
|
)
|
|
mcpServer.AddTool(imageSearchTool, searxngImageSearchHandler)
|
|
|
|
newsSearchTool := mcp.NewTool("searxng_news_search",
|
|
mcp.WithDescription("Specialized news search through SearXNG. Auto-starts SearXNG if not running."),
|
|
mcp.WithString("query",
|
|
mcp.Required(),
|
|
mcp.Description("Search query for news"),
|
|
),
|
|
mcp.WithString("time_range",
|
|
mcp.Description("Time range for news (day, week, month, year)"),
|
|
),
|
|
mcp.WithString("language",
|
|
mcp.Description("News language"),
|
|
),
|
|
mcp.WithNumber("page",
|
|
mcp.Description("Page number of results"),
|
|
),
|
|
)
|
|
mcpServer.AddTool(newsSearchTool, searxngNewsSearchHandler)
|
|
|
|
// Code Search Tools (via Grep.app) - DISABLED due to rate limiting
|
|
// grepSearchTool := mcp.NewTool("grep_search",
|
|
// mcp.WithDescription("Search code across public GitHub repositories using grep.app. Find real-world code examples, patterns, and implementations."),
|
|
// mcp.WithString("query",
|
|
// mcp.Required(),
|
|
// mcp.Description("Search query string - code pattern to search for"),
|
|
// ),
|
|
// mcp.WithString("language",
|
|
// mcp.Description("Programming language filter (e.g., Go, Python, JavaScript, TypeScript)"),
|
|
// ),
|
|
// mcp.WithString("repo",
|
|
// mcp.Description("Repository filter in format 'owner/repo' (e.g., 'vercel/next.js')"),
|
|
// ),
|
|
// mcp.WithString("path",
|
|
// mcp.Description("Path filter for specific directories (e.g., 'src/', 'lib/')"),
|
|
// ),
|
|
// mcp.WithBoolean("regex",
|
|
// mcp.Description("Treat query as regex pattern"),
|
|
// ),
|
|
// mcp.WithBoolean("caseSensitive",
|
|
// mcp.Description("Case-sensitive search"),
|
|
// ),
|
|
// mcp.WithBoolean("wholeWords",
|
|
// mcp.Description("Search for whole words only"),
|
|
// ),
|
|
// )
|
|
// mcpServer.AddTool(grepSearchTool, grepSearchHandler)
|
|
|
|
// githubFileTool := mcp.NewTool("github_file",
|
|
// mcp.WithDescription("Fetch a single file from a GitHub repository"),
|
|
// mcp.WithString("owner",
|
|
// mcp.Required(),
|
|
// mcp.Description("Repository owner (e.g., 'vercel', 'facebook')"),
|
|
// ),
|
|
// mcp.WithString("repo",
|
|
// mcp.Required(),
|
|
// mcp.Description("Repository name (e.g., 'next.js', 'react')"),
|
|
// ),
|
|
// mcp.WithString("path",
|
|
// mcp.Required(),
|
|
// mcp.Description("File path (e.g., 'package.json', 'src/index.ts')"),
|
|
// ),
|
|
// mcp.WithString("ref",
|
|
// mcp.Description("Branch, tag, or commit SHA (default: default branch)"),
|
|
// ),
|
|
// )
|
|
// mcpServer.AddTool(githubFileTool, githubFileHandler)
|
|
|
|
// githubBatchFilesTool := mcp.NewTool("github_batch_files",
|
|
// mcp.WithDescription("Fetch multiple files from GitHub repositories in parallel"),
|
|
// mcp.WithArray("files",
|
|
// mcp.Required(),
|
|
// mcp.Description("Array of file objects with owner, repo, path, and optional ref"),
|
|
// ),
|
|
// )
|
|
// mcpServer.AddTool(githubBatchFilesTool, githubBatchFilesHandler)
|
|
|
|
// New v2.0.0 Management Tools
|
|
searchStatusTool := mcp.NewTool("search_status",
|
|
mcp.WithDescription("Check the status of search services: SearXNG, Docker, and code search availability"),
|
|
)
|
|
mcpServer.AddTool(searchStatusTool, searchStatusHandler)
|
|
|
|
searxngStopTool := mcp.NewTool("searxng_stop",
|
|
mcp.WithDescription("Stop the SearXNG Docker container to free resources"),
|
|
)
|
|
mcpServer.AddTool(searxngStopTool, searxngStopHandler)
|
|
|
|
searxngStartTool := mcp.NewTool("searxng_start",
|
|
mcp.WithDescription("Start the SearXNG Docker container manually"),
|
|
)
|
|
mcpServer.AddTool(searxngStartTool, searxngStartHandler)
|
|
|
|
searxngConfigTool := mcp.NewTool("searxng_config",
|
|
mcp.WithDescription("Get the default SearXNG configuration settings used by this MCP server"),
|
|
)
|
|
mcpServer.AddTool(searxngConfigTool, searxngConfigHandler)
|
|
|
|
// Start server
|
|
if transport == "sse" {
|
|
sseServer := server.NewSSEServer(mcpServer, server.WithBaseURL(fmt.Sprintf("http://localhost:%s", port)))
|
|
log.Printf("SSE server listening on %s:%s URL: http://127.0.0.1:%s/sse", host, port, port)
|
|
log.Printf("SearXNG: auto-start=%v, stop-on-exit=%v", autostart, stopOnExit)
|
|
if err := sseServer.Start(fmt.Sprintf("%s:%s", host, port)); err != nil {
|
|
log.Fatalf("Server error: %v", err)
|
|
}
|
|
} else {
|
|
log.Printf("Stdio server started. SearXNG: auto-start=%v, stop-on-exit=%v", autostart, stopOnExit)
|
|
if err := server.ServeStdio(mcpServer); err != nil {
|
|
log.Fatalf("Server error: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ensureSearXNG ensures SearXNG is running before performing a search
|
|
func ensureSearXNG() error {
|
|
if dockerManager == nil {
|
|
return fmt.Errorf("docker manager not initialized")
|
|
}
|
|
|
|
// Fast path: already running and healthy
|
|
if dockerManager.IsSearXNGRunning() && dockerManager.GetSearXNGHealth() {
|
|
return nil
|
|
}
|
|
|
|
// Try to ensure it's running
|
|
if err := dockerManager.EnsureSearXNG(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Wait for it to be ready
|
|
return dockerManager.WaitForSearXNGReady(30 * time.Second)
|
|
}
|
|
|
|
func searxngSearchHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
// Ensure SearXNG is running
|
|
if err := ensureSearXNG(); err != nil {
|
|
return nil, fmt.Errorf("SearXNG not available: %w", err)
|
|
}
|
|
|
|
query, ok := request.Params.Arguments["query"].(string)
|
|
if !ok {
|
|
return nil, errors.New("query must be a string")
|
|
}
|
|
|
|
params := SearchParams{
|
|
Query: query,
|
|
Categories: []string{"general"},
|
|
Engines: []string{"duckduckgo", "brave"},
|
|
Language: "en",
|
|
}
|
|
|
|
if categories, ok := request.Params.Arguments["categories"].(string); ok && categories != "" {
|
|
params.Categories = strings.Split(categories, ",")
|
|
for i := range params.Categories {
|
|
params.Categories[i] = strings.TrimSpace(params.Categories[i])
|
|
}
|
|
}
|
|
|
|
if engines, ok := request.Params.Arguments["engines"].(string); ok && engines != "" {
|
|
params.Engines = strings.Split(engines, ",")
|
|
for i := range params.Engines {
|
|
params.Engines[i] = strings.TrimSpace(params.Engines[i])
|
|
}
|
|
}
|
|
|
|
if language, ok := request.Params.Arguments["language"].(string); ok && language != "" {
|
|
params.Language = language
|
|
}
|
|
|
|
if pageFloat, ok := request.Params.Arguments["page"].(float64); ok {
|
|
params.PageNo = int(pageFloat)
|
|
}
|
|
|
|
if timeRange, ok := request.Params.Arguments["time_range"].(string); ok {
|
|
params.TimeRange = timeRange
|
|
}
|
|
|
|
if safeSearchFloat, ok := request.Params.Arguments["safe_search"].(float64); ok {
|
|
params.SafeSearch = int(safeSearchFloat)
|
|
}
|
|
|
|
result, err := searxngClient.Search(params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("search error: %w", err)
|
|
}
|
|
|
|
response := map[string]interface{}{
|
|
"query": result.Query,
|
|
"number_of_results": result.NumberOfResults,
|
|
"results": result.Results,
|
|
}
|
|
|
|
if len(result.Answers) > 0 {
|
|
response["answers"] = result.Answers
|
|
}
|
|
if len(result.Suggestions) > 0 {
|
|
response["suggestions"] = result.Suggestions
|
|
}
|
|
if len(result.Corrections) > 0 {
|
|
response["corrections"] = result.Corrections
|
|
}
|
|
|
|
jsonResult, err := json.MarshalIndent(response, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("result serialization error: %w", err)
|
|
}
|
|
|
|
return mcp.NewToolResultText(string(jsonResult)), nil
|
|
}
|
|
|
|
func searxngEnginesInfoHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
// Ensure SearXNG is running
|
|
if err := ensureSearXNG(); err != nil {
|
|
return nil, fmt.Errorf("SearXNG not available: %w", err)
|
|
}
|
|
|
|
config, err := searxngClient.GetEngines()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting engines information: %w", err)
|
|
}
|
|
|
|
jsonResult, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("result serialization error: %w", err)
|
|
}
|
|
|
|
return mcp.NewToolResultText(string(jsonResult)), nil
|
|
}
|
|
|
|
func searxngImageSearchHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
// Ensure SearXNG is running
|
|
if err := ensureSearXNG(); err != nil {
|
|
return nil, fmt.Errorf("SearXNG not available: %w", err)
|
|
}
|
|
|
|
query, ok := request.Params.Arguments["query"].(string)
|
|
if !ok {
|
|
return nil, errors.New("query must be a string")
|
|
}
|
|
|
|
params := SearchParams{
|
|
Query: query,
|
|
Categories: []string{"images"},
|
|
Engines: []string{"bing images"},
|
|
Language: "en",
|
|
}
|
|
|
|
if engines, ok := request.Params.Arguments["engines"].(string); ok && engines != "" {
|
|
params.Engines = strings.Split(engines, ",")
|
|
for i := range params.Engines {
|
|
params.Engines[i] = strings.TrimSpace(params.Engines[i])
|
|
}
|
|
}
|
|
|
|
if pageFloat, ok := request.Params.Arguments["page"].(float64); ok {
|
|
params.PageNo = int(pageFloat)
|
|
}
|
|
|
|
result, err := searxngClient.Search(params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("image search error: %w", err)
|
|
}
|
|
|
|
jsonResult, err := json.MarshalIndent(result, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("result serialization error: %w", err)
|
|
}
|
|
|
|
return mcp.NewToolResultText(string(jsonResult)), nil
|
|
}
|
|
|
|
func searxngNewsSearchHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
// Ensure SearXNG is running
|
|
if err := ensureSearXNG(); err != nil {
|
|
return nil, fmt.Errorf("SearXNG not available: %w", err)
|
|
}
|
|
|
|
query, ok := request.Params.Arguments["query"].(string)
|
|
if !ok {
|
|
return nil, errors.New("query must be a string")
|
|
}
|
|
|
|
params := SearchParams{
|
|
Query: query,
|
|
Categories: []string{"news"},
|
|
Engines: []string{"bing news"},
|
|
Language: "en",
|
|
}
|
|
|
|
if timeRange, ok := request.Params.Arguments["time_range"].(string); ok {
|
|
params.TimeRange = timeRange
|
|
}
|
|
|
|
if language, ok := request.Params.Arguments["language"].(string); ok && language != "" {
|
|
params.Language = language
|
|
}
|
|
|
|
if pageFloat, ok := request.Params.Arguments["page"].(float64); ok {
|
|
params.PageNo = int(pageFloat)
|
|
}
|
|
|
|
result, err := searxngClient.Search(params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("news search error: %w", err)
|
|
}
|
|
|
|
jsonResult, err := json.MarshalIndent(result, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("result serialization error: %w", err)
|
|
}
|
|
|
|
return mcp.NewToolResultText(string(jsonResult)), nil
|
|
}
|
|
|
|
func grepSearchHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
query, ok := request.Params.Arguments["query"].(string)
|
|
if !ok {
|
|
return nil, errors.New("query must be a string")
|
|
}
|
|
|
|
params := GrepSearchParams{
|
|
Query: query,
|
|
}
|
|
|
|
if language, ok := request.Params.Arguments["language"].(string); ok && language != "" {
|
|
params.Language = language
|
|
}
|
|
|
|
if repo, ok := request.Params.Arguments["repo"].(string); ok && repo != "" {
|
|
params.Repo = repo
|
|
}
|
|
|
|
if path, ok := request.Params.Arguments["path"].(string); ok && path != "" {
|
|
params.Path = path
|
|
}
|
|
|
|
if regex, ok := request.Params.Arguments["regex"].(bool); ok {
|
|
params.Regex = regex
|
|
}
|
|
|
|
if caseSensitive, ok := request.Params.Arguments["caseSensitive"].(bool); ok {
|
|
params.CaseSensitive = caseSensitive
|
|
}
|
|
|
|
if wholeWords, ok := request.Params.Arguments["wholeWords"].(bool); ok {
|
|
params.WholeWords = wholeWords
|
|
}
|
|
|
|
result, err := grepAppClient.Search(params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("grep search error: %w", err)
|
|
}
|
|
|
|
response := map[string]interface{}{
|
|
"query": params.Query,
|
|
"total_hits": result.Hits.Total,
|
|
}
|
|
|
|
var results []map[string]interface{}
|
|
for _, hit := range result.Hits.Hits {
|
|
r := map[string]interface{}{
|
|
"repo": hit.Source.Repo,
|
|
"path": hit.Source.Path,
|
|
"filename": hit.Source.Filename,
|
|
"language": hit.Source.Language,
|
|
"url": hit.Source.URL,
|
|
}
|
|
|
|
if len(hit.Source.Lines) > 0 {
|
|
var lines []map[string]interface{}
|
|
for _, line := range hit.Source.Lines {
|
|
lines = append(lines, map[string]interface{}{
|
|
"content": line.Content,
|
|
"line_number": line.LineNumber,
|
|
})
|
|
}
|
|
r["lines"] = lines
|
|
}
|
|
|
|
results = append(results, r)
|
|
}
|
|
|
|
response["results"] = results
|
|
|
|
jsonResult, err := json.MarshalIndent(response, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("result serialization error: %w", err)
|
|
}
|
|
|
|
return mcp.NewToolResultText(string(jsonResult)), nil
|
|
}
|
|
|
|
func githubFileHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
owner, ok := request.Params.Arguments["owner"].(string)
|
|
if !ok {
|
|
return nil, errors.New("owner is required")
|
|
}
|
|
|
|
repo, ok := request.Params.Arguments["repo"].(string)
|
|
if !ok {
|
|
return nil, errors.New("repo is required")
|
|
}
|
|
|
|
path, ok := request.Params.Arguments["path"].(string)
|
|
if !ok {
|
|
return nil, errors.New("path is required")
|
|
}
|
|
|
|
ref := ""
|
|
if refVal, ok := request.Params.Arguments["ref"].(string); ok {
|
|
ref = refVal
|
|
}
|
|
|
|
content, err := grepAppClient.GetFile(owner, repo, path, ref)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error fetching file: %w", err)
|
|
}
|
|
|
|
response := map[string]interface{}{
|
|
"owner": owner,
|
|
"repo": repo,
|
|
"path": path,
|
|
"ref": ref,
|
|
"content": content,
|
|
}
|
|
|
|
jsonResult, err := json.MarshalIndent(response, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("result serialization error: %w", err)
|
|
}
|
|
|
|
return mcp.NewToolResultText(string(jsonResult)), nil
|
|
}
|
|
|
|
func githubBatchFilesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
filesArg, ok := request.Params.Arguments["files"].([]interface{})
|
|
if !ok {
|
|
return nil, errors.New("files must be an array")
|
|
}
|
|
|
|
var files []GitHubFileParams
|
|
for _, f := range filesArg {
|
|
fileMap, ok := f.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
file := GitHubFileParams{}
|
|
if owner, ok := fileMap["owner"].(string); ok {
|
|
file.Owner = owner
|
|
}
|
|
if repo, ok := fileMap["repo"].(string); ok {
|
|
file.Repo = repo
|
|
}
|
|
if path, ok := fileMap["path"].(string); ok {
|
|
file.Path = path
|
|
}
|
|
if ref, ok := fileMap["ref"].(string); ok {
|
|
file.Ref = ref
|
|
}
|
|
|
|
if file.Owner != "" && file.Repo != "" && file.Path != "" {
|
|
files = append(files, file)
|
|
}
|
|
}
|
|
|
|
if len(files) == 0 {
|
|
return nil, errors.New("no valid files provided")
|
|
}
|
|
|
|
results, err := grepAppClient.GetFilesBatch(files)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error fetching files: %w", err)
|
|
}
|
|
|
|
jsonResult, err := json.MarshalIndent(results, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("result serialization error: %w", err)
|
|
}
|
|
|
|
return mcp.NewToolResultText(string(jsonResult)), nil
|
|
}
|
|
|
|
// v2.0.0 Management Handlers
|
|
|
|
func searchStatusHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
status := map[string]interface{}{
|
|
"version": "2.0.0",
|
|
"timestamp": time.Now().Format(time.RFC3339),
|
|
}
|
|
|
|
// Docker status
|
|
if dockerManager != nil {
|
|
dockerAvailable := dockerManager.IsDockerAvailable()
|
|
status["docker_available"] = dockerAvailable
|
|
|
|
if dockerAvailable {
|
|
status["searxng_running"] = dockerManager.IsSearXNGRunning()
|
|
status["searxng_healthy"] = dockerManager.GetSearXNGHealth()
|
|
status["searxng_port"] = dockerManager.searxngPort
|
|
status["searxng_image"] = dockerManager.searxngImage
|
|
}
|
|
}
|
|
|
|
// Test code search (lightweight check)
|
|
status["code_search_available"] = grepAppClient != nil
|
|
|
|
jsonResult, err := json.MarshalIndent(status, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("result serialization error: %w", err)
|
|
}
|
|
|
|
return mcp.NewToolResultText(string(jsonResult)), nil
|
|
}
|
|
|
|
func searxngStopHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
if dockerManager == nil {
|
|
return nil, errors.New("docker manager not initialized")
|
|
}
|
|
|
|
if !dockerManager.IsDockerAvailable() {
|
|
return nil, errors.New("Docker is not available")
|
|
}
|
|
|
|
if !dockerManager.IsSearXNGRunning() {
|
|
return mcp.NewToolResultText("SearXNG is not currently running"), nil
|
|
}
|
|
|
|
if err := dockerManager.StopSearXNG(); err != nil {
|
|
return nil, fmt.Errorf("failed to stop SearXNG: %w", err)
|
|
}
|
|
|
|
return mcp.NewToolResultText("SearXNG stopped successfully"), nil
|
|
}
|
|
|
|
func searxngStartHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
if dockerManager == nil {
|
|
return nil, errors.New("docker manager not initialized")
|
|
}
|
|
|
|
if !dockerManager.IsDockerAvailable() {
|
|
return nil, errors.New("Docker is not available. Please install Docker to use search.")
|
|
}
|
|
|
|
if dockerManager.IsSearXNGRunning() && dockerManager.GetSearXNGHealth() {
|
|
return mcp.NewToolResultText("SearXNG is already running and healthy"), nil
|
|
}
|
|
|
|
if err := dockerManager.EnsureSearXNG(); err != nil {
|
|
return nil, fmt.Errorf("failed to start SearXNG: %w", err)
|
|
}
|
|
|
|
if err := dockerManager.WaitForSearXNGReady(30 * time.Second); err != nil {
|
|
return nil, fmt.Errorf("SearXNG started but failed to become ready: %w", err)
|
|
}
|
|
|
|
return mcp.NewToolResultText("SearXNG started successfully and is ready for searches"), nil
|
|
}
|
|
|
|
func searxngConfigHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
config := map[string]interface{}{
|
|
"default_settings": GetSearXNGSettingsYAML(),
|
|
"container_name": SearXNGContainerName,
|
|
"default_image": SearXNGImage,
|
|
"default_port": "8080",
|
|
"privacy_note": "SearXNG is a privacy-respecting metasearch engine that queries multiple search engines without tracking",
|
|
"engine_documentation": "https://docs.searxng.org/",
|
|
}
|
|
|
|
jsonResult, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("result serialization error: %w", err)
|
|
}
|
|
|
|
return mcp.NewToolResultText(string(jsonResult)), nil
|
|
}
|