188 lines
4.7 KiB
Go
188 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type SearXNGClient struct {
|
|
BaseURL string
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
func NewSearXNGClient(baseURL string) *SearXNGClient {
|
|
return &SearXNGClient{
|
|
BaseURL: strings.TrimSuffix(baseURL, "/"),
|
|
HTTPClient: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
type SearchResult struct {
|
|
Title string `json:"title"`
|
|
URL string `json:"url"`
|
|
Content string `json:"content"`
|
|
Engine string `json:"engine"`
|
|
Category string `json:"category"`
|
|
Score float64 `json:"score,omitempty"`
|
|
PublishedDate string `json:"publishedDate,omitempty"`
|
|
}
|
|
|
|
type SearchResponse struct {
|
|
Query string `json:"query"`
|
|
NumberOfResults int `json:"number_of_results"`
|
|
Results []SearchResult `json:"results"`
|
|
Answers []string `json:"answers,omitempty"`
|
|
Corrections []string `json:"corrections,omitempty"`
|
|
Infoboxes []interface{} `json:"infoboxes,omitempty"`
|
|
Suggestions []string `json:"suggestions,omitempty"`
|
|
}
|
|
|
|
type SearchParams struct {
|
|
Query string
|
|
Categories []string
|
|
Engines []string
|
|
Language string
|
|
PageNo int
|
|
TimeRange string
|
|
SafeSearch int
|
|
}
|
|
|
|
func (c *SearXNGClient) Search(params SearchParams) (*SearchResponse, error) {
|
|
searchURL := fmt.Sprintf("%s/search", c.BaseURL)
|
|
|
|
values := url.Values{}
|
|
values.Set("q", params.Query)
|
|
values.Set("format", "json")
|
|
|
|
if len(params.Categories) > 0 {
|
|
values.Set("categories", strings.Join(params.Categories, ","))
|
|
}
|
|
|
|
if len(params.Engines) > 0 {
|
|
values.Set("engines", strings.Join(params.Engines, ","))
|
|
}
|
|
|
|
if params.Language != "" {
|
|
values.Set("language", params.Language)
|
|
}
|
|
|
|
if params.PageNo > 0 {
|
|
values.Set("pageno", strconv.Itoa(params.PageNo))
|
|
}
|
|
|
|
if params.TimeRange != "" {
|
|
values.Set("time_range", params.TimeRange)
|
|
}
|
|
|
|
if params.SafeSearch >= 0 && params.SafeSearch <= 2 {
|
|
values.Set("safesearch", strconv.Itoa(params.SafeSearch))
|
|
}
|
|
|
|
// Use POST request with form data (required by SearXNG)
|
|
req, err := http.NewRequest("POST", searchURL, strings.NewReader(values.Encode()))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("User-Agent", "MCP-SearXNG-Client/1.0")
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("Origin", c.BaseURL)
|
|
req.Header.Set("Referer", c.BaseURL+"/")
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error executing request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading response: %w", err)
|
|
}
|
|
|
|
var searchResponse SearchResponse
|
|
if err := json.Unmarshal(body, &searchResponse); err != nil {
|
|
return nil, fmt.Errorf("error parsing JSON: %w", err)
|
|
}
|
|
|
|
return &searchResponse, nil
|
|
}
|
|
|
|
func (c *SearXNGClient) GetEngines() (map[string]interface{}, error) {
|
|
enginesURL := fmt.Sprintf("%s/config", c.BaseURL)
|
|
|
|
req, err := http.NewRequest("GET", enginesURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("User-Agent", "MCP-SearXNG-Client/1.0")
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error executing request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("HTTP error %d", resp.StatusCode)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading response: %w", err)
|
|
}
|
|
|
|
var config map[string]interface{}
|
|
if err := json.Unmarshal(body, &config); err != nil {
|
|
return nil, fmt.Errorf("error parsing JSON: %w", err)
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// Ping checks if SearXNG is reachable and responding
|
|
func (c *SearXNGClient) Ping() error {
|
|
enginesURL := fmt.Sprintf("%s/config", c.BaseURL)
|
|
client := &http.Client{Timeout: 2 * time.Second}
|
|
resp, err := client.Get(enginesURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("HTTP %d", resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WaitForReady polls SearXNG until it's ready or timeout is reached
|
|
func (c *SearXNGClient) WaitForReady(timeout time.Duration) error {
|
|
deadline := time.Now().Add(timeout)
|
|
checkInterval := 500 * time.Millisecond
|
|
|
|
for time.Now().Before(deadline) {
|
|
if err := c.Ping(); err == nil {
|
|
return nil
|
|
}
|
|
time.Sleep(checkInterval)
|
|
}
|
|
|
|
return fmt.Errorf("SearXNG failed to become ready within %v", timeout)
|
|
}
|