Initial commit: Search MCP Server
This commit is contained in:
+187
@@ -0,0 +1,187 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user