Initial commit: Search MCP Server
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
SearXNGContainerName = "searxng-mcp-local"
|
||||
SearXNGImage = "searxng/searxng:latest"
|
||||
SearXNGInternalPort = "8080"
|
||||
)
|
||||
|
||||
type DockerManager struct {
|
||||
searxngPort string
|
||||
searxngImage string
|
||||
autostart bool
|
||||
stopOnExit bool
|
||||
}
|
||||
|
||||
func NewDockerManager(searxngPort, searxngImage string, autostart, stopOnExit bool) *DockerManager {
|
||||
return &DockerManager{
|
||||
searxngPort: searxngPort,
|
||||
searxngImage: searxngImage,
|
||||
autostart: autostart,
|
||||
stopOnExit: stopOnExit,
|
||||
}
|
||||
}
|
||||
|
||||
func (dm *DockerManager) Close() error {
|
||||
if dm.stopOnExit {
|
||||
return dm.StopSearXNG()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dm *DockerManager) runDocker(args ...string) (string, string, error) {
|
||||
cmd := exec.Command("docker", args...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
return stdout.String(), stderr.String(), err
|
||||
}
|
||||
|
||||
func (dm *DockerManager) IsDockerAvailable() bool {
|
||||
_, _, err := dm.runDocker("version")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (dm *DockerManager) IsSearXNGRunning() bool {
|
||||
stdout, _, err := dm.runDocker("ps", "-q", "-f", "name="+SearXNGContainerName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(stdout) != ""
|
||||
}
|
||||
|
||||
func (dm *DockerManager) GetSearXNGHealth() bool {
|
||||
url := fmt.Sprintf("http://127.0.0.1:%s/config", dm.searxngPort)
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.StatusCode == http.StatusOK
|
||||
}
|
||||
|
||||
func (dm *DockerManager) StartSearXNG() error {
|
||||
if !dm.autostart {
|
||||
return fmt.Errorf("auto-start disabled")
|
||||
}
|
||||
|
||||
// Validate port is within valid range
|
||||
var portNum int
|
||||
_, err := fmt.Sscanf(dm.searxngPort, "%d", &portNum)
|
||||
if err != nil || portNum < 1 || portNum > 65535 {
|
||||
return fmt.Errorf("invalid port: must be between 1 and 65535, got %s", dm.searxngPort)
|
||||
}
|
||||
|
||||
// Check if container exists but stopped
|
||||
stdout, _, _ := dm.runDocker("ps", "-a", "-q", "-f", "name="+SearXNGContainerName)
|
||||
if strings.TrimSpace(stdout) != "" {
|
||||
// Container exists, start it
|
||||
_, _, err := dm.runDocker("start", SearXNGContainerName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start existing SearXNG container: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pull image in background (don't wait)
|
||||
go dm.pullImage()
|
||||
|
||||
// Create and start new container with a random secret
|
||||
// Generate a random secret (SearXNG requires at least 16 chars)
|
||||
secret := fmt.Sprintf("mcp-search-%d-secret-key-local", time.Now().Unix())
|
||||
|
||||
// Create and start new container with JSON API enabled
|
||||
// Use environment variables to configure SearXNG
|
||||
_, stderr, err := dm.runDocker(
|
||||
"run",
|
||||
"-d",
|
||||
"--name", SearXNGContainerName,
|
||||
"-p", "127.0.0.1:"+dm.searxngPort+":"+SearXNGInternalPort,
|
||||
"--restart", "unless-stopped",
|
||||
"-e", "SEARXNG_SECRET="+secret,
|
||||
dm.searxngImage,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
// Check if it's a port conflict
|
||||
if strings.Contains(stderr, "Bind for 127.0.0.1") {
|
||||
return fmt.Errorf("port %s is already in use. Please stop the existing service or use a different port", dm.searxngPort)
|
||||
}
|
||||
return fmt.Errorf("failed to create SearXNG container: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dm *DockerManager) pullImage() {
|
||||
dm.runDocker("pull", dm.searxngImage)
|
||||
}
|
||||
|
||||
func (dm *DockerManager) StopSearXNG() error {
|
||||
_, _, _ = dm.runDocker("stop", SearXNGContainerName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dm *DockerManager) RemoveSearXNG() error {
|
||||
_, _, _ = dm.runDocker("rm", "-f", SearXNGContainerName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dm *DockerManager) EnsureSearXNG() error {
|
||||
if dm.IsSearXNGRunning() && dm.GetSearXNGHealth() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !dm.IsDockerAvailable() {
|
||||
return fmt.Errorf("Docker is not available. Please install Docker or start SearXNG manually at http://127.0.0.1:%s", dm.searxngPort)
|
||||
}
|
||||
|
||||
if err := dm.StartSearXNG(); err != nil {
|
||||
return fmt.Errorf("failed to start SearXNG: %w", err)
|
||||
}
|
||||
|
||||
// Configure SearXNG for JSON API access
|
||||
if err := dm.configureSearXNG(); err != nil {
|
||||
// Log but don't fail - container might already be configured
|
||||
fmt.Printf("Note: SearXNG configuration: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// configureSearXNG enables JSON format for API access
|
||||
func (dm *DockerManager) configureSearXNG() error {
|
||||
// Wait a bit for container to be fully ready
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Enable JSON format in settings
|
||||
// First uncomment the formats line, then add json to the list
|
||||
_, _, err := dm.runDocker("exec", SearXNGContainerName, "sh", "-c",
|
||||
`sed -i 's/# formats:/formats:/' /etc/searxng/settings.yml && sed -i '/formats:/,/^[a-z]/ { s/ - html/ - html\n - json/; }' /etc/searxng/settings.yml`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to enable JSON format: %w", err)
|
||||
}
|
||||
|
||||
// Restart to apply changes
|
||||
_, _, err = dm.runDocker("restart", SearXNGContainerName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restart SearXNG: %w", err)
|
||||
}
|
||||
|
||||
// Wait for restart
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dm *DockerManager) WaitForSearXNGReady(timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
if dm.GetSearXNGHealth() {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return fmt.Errorf("SearXNG failed to become ready within %v", timeout)
|
||||
}
|
||||
Reference in New Issue
Block a user