mcp-tavily-search

by y7ut
package tavily import ( "context" "encoding/json" "fmt" "io" "net/http" "os" "strings" "github.com/y7ut/mcp-tavily-search/pkg/param" ) // TravilySearch is a singleton var TravilySearch *TavilySearch const ( TopicGeneral = "general" TopicNews = "news" DepthBasic = "basic" DepthAdvanced = "advanced" DefaultDays = 7 TavilySearchEndpoint = "https://api.tavily.com/search" ) type TavilySearchResquest struct { MaxResults int `json:"max_results"` IncludeImages bool `json:"include_images"` IncludeImageDesc bool `json:"include_image_descriptions"` IncludeAnswer bool `json:"include_answer"` IncludeRawContent bool `json:"include_raw_content"` Query string `json:"query"` ApiKey string `json:"api_key"` Topic string `json:"topic"` SearchDepth string `json:"search_depth"` Days int `json:"days"` IncludeDomains []string `json:"include_domains"` ExcludeDomains []string `json:"exclude_domains"` } type TavilySearch struct { ApiKey string IncludeDomains []string ExcludeDomains []string Debug bool } type TavilySearchImage struct { URL string `json:"url"` Description string `json:"description"` } type TavilySearchResult struct { Title string `json:"title"` URL string `json:"url"` Content string `json:"content"` Score float64 `json:"score"` RawContent *string `json:"raw_content"` PublishedDate *string `json:"published_date"` } type TavilySearchResponse struct { Query string `json:"query"` FollowUpQuestions *string `json:"follow_up_questions"` Answer *string `json:"answer"` Images []TavilySearchImage `json:"images"` Results []TavilySearchResult `json:"results"` ResponseTime float64 `json:"response_time"` } // Init initialize func Init(apiKey string, debug bool, includeDomain []string, excludeDomain []string) { if TravilySearch == nil { TravilySearch = NewTavilySearch(apiKey, debug, includeDomain, excludeDomain) } } // NewTavilySearch func NewTavilySearch(apiKey string, debug bool, includeDomain []string, excludeDomain []string) *TavilySearch { return &TavilySearch{ ApiKey: apiKey, Debug: debug, IncludeDomains: includeDomain, ExcludeDomains: excludeDomain, } } // Search search from tavily with keyword and options func Search(ctx context.Context, query string, h ...WithOptionHelper) ([]TavilySearchResult, error) { if TravilySearch == nil { return nil, fmt.Errorf("tavily search is not initialized") } return TravilySearch.Search(ctx, query, h...) } // Search func (t *TavilySearch) Search(ctx context.Context, query string, h ...WithOptionHelper) ([]TavilySearchResult, error) { tavilyParams := NewOptionManager() for _, helper := range h { helper(tavilyParams) } tavilyReq, err := t.applyParams(*tavilyParams) if err != nil { return nil, err } tavilyReq.Query = query tavilyReq.ApiKey = t.ApiKey tavilyReq.IncludeDomains = t.IncludeDomains tavilyReq.ExcludeDomains = t.ExcludeDomains var body io.Reader reqbody, err := json.Marshal(tavilyReq) if err != nil { return nil, fmt.Errorf("tavily params marshal error: %v", err) } body = strings.NewReader(string(reqbody)) if t.Debug { fmt.Fprintf(os.Stderr, "Tavily api input: %s\n", string(reqbody)) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, TavilySearchEndpoint, body) if err != nil { return nil, fmt.Errorf("tavily api request error: %v", err) } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("tavily API request error: %v", err) } defer resp.Body.Close() // 读取响应体 respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read Tavily API response: %v", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("tavily API error: status %d, body: %s", resp.StatusCode, string(respBody)) } // 解析响应 if t.Debug { fmt.Fprintf(os.Stderr, "Tavily API output: %s\n", string(respBody)) } var tsResponse TavilySearchResponse if err := json.Unmarshal(respBody, &tsResponse); err != nil { return nil, fmt.Errorf("failed to unmarshal Tavily API response: %v", err) } // 整理返回结果 return tsResponse.Results, nil } // applyParams // Available params: // - debug: bool // - limit: int // - topic: string // - search_depth: string // - days: int func (t *TavilySearch) applyParams(options OptionManager) (*TavilySearchResquest, error) { tavilyParams := TavilySearchResquest{} if err := param.Assign(&tavilyParams.MaxResults, options.GetOptionWithDefault("limit", 5)); err != nil { return nil, err } if err := param.Assign(&tavilyParams.Topic, options.GetOptionWithDefault("topic", TopicGeneral)); err != nil { return nil, err } if tavilyParams.Topic != TopicGeneral && tavilyParams.Topic != TopicNews { return nil, fmt.Errorf("tavily topic error: %s is not a valid topic", tavilyParams.Topic) } if err := param.Assign(&tavilyParams.SearchDepth, options.GetOptionWithDefault("search_depth", DepthBasic)); err != nil { return nil, err } if tavilyParams.SearchDepth != DepthBasic && tavilyParams.SearchDepth != DepthAdvanced { return nil, fmt.Errorf("tavily search depth error: %s is not a valid search depth", tavilyParams.SearchDepth) } if err := param.Assign(&tavilyParams.Days, options.GetOptionWithDefault("days", DefaultDays)); err != nil { return nil, err } if tavilyParams.Days < 1 || tavilyParams.Days > 30 { return nil, fmt.Errorf("tavily days error: %d is not a valid days, days must between 1 and 30", tavilyParams.Days) } return &tavilyParams, nil }