Browse Source

feat: web search mcp server (#209)

* feat: web search mcp server

* fix: ci lint

* fix: ci lint
zijiren 7 months ago
parent
commit
7ab9715ae1

+ 3 - 3
mcp-servers/README.md

@@ -334,7 +334,7 @@ func NewServer(config map[string]string, reusingConfig map[string]string) (*serv
         Description: "Retrieve data from the API",
         InputSchema: mcp.ToolInputSchema{
             Type: "object",
-            Properties: map[string]interface{}{
+            Properties: map[string]any{
                 "query": {
                     "type":        "string",
                     "description": "Search query",
@@ -342,14 +342,14 @@ func NewServer(config map[string]string, reusingConfig map[string]string) (*serv
             },
             Required: []string{"query"},
         },
-    }, func(arguments map[string]interface{}) (*server.ToolResult, error) {
+    }, func(arguments map[string]any) (*server.ToolResult, error) {
         query := arguments["query"].(string)
         
         // Implement your tool logic here
         // Use endpoint and apiKey for API calls
         
         return &server.ToolResult{
-            Content: []interface{}{
+            Content: []any{
                 server.TextContent{
                     Type: "text",
                     Text: fmt.Sprintf("Retrieved data for query: %s", query),

+ 7 - 0
mcp-servers/go.mod

@@ -3,6 +3,7 @@ module github.com/labring/aiproxy/mcp-servers
 go 1.24
 
 require (
+	github.com/bytedance/sonic v1.13.2
 	github.com/labring/aiproxy/core v0.0.0-20250527101240-aac8e89068ad
 	github.com/labring/aiproxy/openapi-mcp v0.0.0-20250527101240-aac8e89068ad
 	github.com/mark3labs/mcp-go v0.30.0
@@ -10,6 +11,8 @@ require (
 
 require (
 	github.com/KyleBanks/depth v1.2.1 // indirect
+	github.com/bytedance/sonic/loader v0.2.4 // indirect
+	github.com/cloudwego/base64x v0.1.5 // indirect
 	github.com/getkin/kin-openapi v0.132.0 // indirect
 	github.com/go-openapi/jsonpointer v0.21.1 // indirect
 	github.com/go-openapi/jsonreference v0.21.0 // indirect
@@ -17,6 +20,7 @@ require (
 	github.com/go-openapi/swag v0.23.1 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.10 // indirect
 	github.com/mailru/easyjson v0.9.0 // indirect
 	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
 	github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
@@ -24,8 +28,11 @@ require (
 	github.com/perimeterx/marshmallow v1.1.5 // indirect
 	github.com/spf13/cast v1.8.0 // indirect
 	github.com/swaggo/swag v1.16.4 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.13 // indirect
 	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
+	golang.org/x/arch v0.17.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/tools v0.33.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )

+ 28 - 0
mcp-servers/go.sum

@@ -1,5 +1,14 @@
 github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
 github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
+github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
+github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
+github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
+github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
+github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -22,6 +31,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -48,22 +61,37 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
 github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
 github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 github.com/ugorji/go/codec v1.2.13 h1:6nvAfJXxwEVFG0UdQwvobVN44a+xQAFiQajSG1Z6bU8=
 github.com/ugorji/go/codec v1.2.13/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
 github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
+golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
+golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
 golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
 golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
 golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
 golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
 golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

+ 1 - 0
mcp-servers/mcpregister/init.go

@@ -3,4 +3,5 @@ package mcpregister
 import (
 	// register embed mcp
 	_ "github.com/labring/aiproxy/mcp-servers/aiproxy-openapi"
+	_ "github.com/labring/aiproxy/mcp-servers/web-search"
 )

+ 43 - 0
mcp-servers/web-search/README.md

@@ -0,0 +1,43 @@
+# Web Search MCP Server
+
+A comprehensive web search MCP server that provides access to multiple search engines including Google, Bing, and Arxiv.
+
+## Features
+
+- **Multiple Search Engines**: Integrated support for Google, Bing, and Arxiv
+- **Flexible Configuration**: Configure only the search engines you need
+- **Multi-Engine Search**: Search across multiple engines simultaneously
+- **Smart Search**: Intelligent query optimization and result aggregation
+- **Academic Search**: Specialized support for academic papers through Arxiv
+- **Language Support**: Search in different languages
+- **Result Control**: Configure the maximum number of results
+
+## Configuration
+
+### Required Configuration
+
+At least one search engine must be configured with valid API credentials:
+
+#### Google Search
+
+- `google_api_key`: Your Google Custom Search API key
+- `google_cx`: Your Google Custom Search Engine ID
+
+#### Bing Search
+
+- `bing_api_key`: Your Bing Search API key
+
+#### Arxiv Search
+
+No configuration required - Arxiv is free to use.
+
+### Optional Configuration
+
+- `default_engine`: Default search engine to use (google, bing, arxiv)
+- `max_results`: Maximum number of search results to return (1-50, default: 10)
+
+## How to test in aiproxy
+
+```http
+http://localhost:3000/api/test-embedmcp/web-search/streamable?key=sealos&config[google_api_key]=google-api-key&config[google_cx]=google-cx
+```

+ 98 - 0
mcp-servers/web-search/engine/arxiv.go

@@ -0,0 +1,98 @@
+package engine
+
+import (
+	"context"
+	"encoding/xml"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+)
+
+type ArxivEngine struct {
+	client *http.Client
+}
+
+func NewArxivEngine() *ArxivEngine {
+	return &ArxivEngine{
+		client: &http.Client{
+			Timeout: 10 * time.Second,
+		},
+	}
+}
+
+type arxivFeed struct {
+	Entries []arxivEntry `xml:"entry"`
+}
+
+type arxivEntry struct {
+	Title   string `xml:"title"`
+	ID      string `xml:"id"`
+	Summary string `xml:"summary"`
+	Authors []struct {
+		Name string `xml:"name"`
+	} `xml:"author"`
+	Published string `xml:"published"`
+}
+
+func (a *ArxivEngine) Search(ctx context.Context, query SearchQuery) ([]SearchResult, error) {
+	searchQuery := "all:" + url.QueryEscape(query.Query)
+	if query.ArxivCategory != "" {
+		searchQuery = fmt.Sprintf("%s+AND+cat:%s", searchQuery, query.ArxivCategory)
+	}
+
+	searchURL := fmt.Sprintf(
+		"https://export.arxiv.org/api/query?search_query=%s&max_results=%d",
+		searchQuery,
+		query.MaxResults,
+	)
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := a.client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		body, _ := io.ReadAll(resp.Body)
+		return nil, fmt.Errorf("arxiv search failed: %d - %s", resp.StatusCode, string(body))
+	}
+
+	var feed arxivFeed
+	if err := xml.NewDecoder(resp.Body).Decode(&feed); err != nil {
+		return nil, err
+	}
+
+	results := make([]SearchResult, 0, len(feed.Entries))
+	for _, entry := range feed.Entries {
+		authors := make([]string, 0, len(entry.Authors))
+		for _, author := range entry.Authors {
+			authors = append(authors, author.Name)
+		}
+
+		// Convert arxiv ID to URL
+		arxivID := strings.TrimPrefix(entry.ID, "http://arxiv.org/abs/")
+		link := "https://arxiv.org/abs/" + arxivID
+
+		content := fmt.Sprintf("%s\nAuthors: %s\nPublished: %s",
+			entry.Summary,
+			strings.Join(authors, ", "),
+			entry.Published,
+		)
+
+		results = append(results, SearchResult{
+			Title:   entry.Title,
+			Link:    link,
+			Content: content,
+		})
+	}
+
+	return results, nil
+}

+ 83 - 0
mcp-servers/web-search/engine/bing.go

@@ -0,0 +1,83 @@
+package engine
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strconv"
+	"time"
+
+	"github.com/bytedance/sonic"
+)
+
+type BingEngine struct {
+	apiKey string
+	client *http.Client
+}
+
+func NewBingEngine(apiKey string) *BingEngine {
+	return &BingEngine{
+		apiKey: apiKey,
+		client: &http.Client{
+			Timeout: 10 * time.Second,
+		},
+	}
+}
+
+type bingResponse struct {
+	WebPages bingResponseWebPages `json:"webPages"`
+}
+
+type bingResponseWebPages struct {
+	Value []bingResponseWebPagesValue `json:"value"`
+}
+
+type bingResponseWebPagesValue struct {
+	Name    string `json:"name"`
+	URL     string `json:"url"`
+	Snippet string `json:"snippet"`
+}
+
+func (b *BingEngine) Search(ctx context.Context, query SearchQuery) ([]SearchResult, error) {
+	querys := url.Values{}
+	querys.Set("q", query.Query)
+	querys.Set("count", strconv.Itoa(query.MaxResults))
+	if query.Language != "" {
+		querys.Set("mkt", query.Language)
+	}
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.bing.microsoft.com/v7.0/search?"+querys.Encode(), nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Ocp-Apim-Subscription-Key", b.apiKey)
+
+	resp, err := b.client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		body, _ := io.ReadAll(resp.Body)
+		return nil, fmt.Errorf("bing search failed: %d - %s", resp.StatusCode, string(body))
+	}
+
+	var response bingResponse
+	if err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&response); err != nil {
+		return nil, err
+	}
+
+	results := make([]SearchResult, 0, len(response.WebPages.Value))
+	for _, item := range response.WebPages.Value {
+		results = append(results, SearchResult{
+			Title:   item.Name,
+			Link:    item.URL,
+			Content: item.Snippet,
+		})
+	}
+
+	return results, nil
+}

+ 90 - 0
mcp-servers/web-search/engine/google.go

@@ -0,0 +1,90 @@
+package engine
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strconv"
+	"time"
+
+	"github.com/bytedance/sonic"
+)
+
+// https://developers.google.com/custom-search/v1/overview?hl=zh-cn
+// https://programmablesearchengine.google.com/controlpanel/create
+// https://zhuanlan.zhihu.com/p/174666017
+type GoogleEngine struct {
+	apiKey string
+	cx     string
+	client *http.Client
+}
+
+func NewGoogleEngine(apiKey, cx string) *GoogleEngine {
+	return &GoogleEngine{
+		apiKey: apiKey,
+		cx:     cx,
+		client: &http.Client{
+			Timeout: 10 * time.Second,
+		},
+	}
+}
+
+type googleResponse struct {
+	Items []googleResponseItem `json:"items"`
+}
+
+type googleResponseItem struct {
+	Title   string `json:"title"`
+	Link    string `json:"link"`
+	Snippet string `json:"snippet"`
+}
+
+func (g *GoogleEngine) Search(ctx context.Context, query SearchQuery) ([]SearchResult, error) {
+	if query.MaxResults > 10 {
+		query.MaxResults = 10 // Google API limit
+	}
+
+	querys := url.Values{}
+	querys.Set("cx", g.cx)
+	querys.Set("q", query.Query)
+	querys.Set("num", strconv.Itoa(query.MaxResults))
+	querys.Set("key", g.apiKey)
+
+	if query.Language != "" {
+		querys.Set("lr", "lang_"+query.Language)
+	}
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.googleapis.com/customsearch/v1?"+querys.Encode(), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := g.client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		body, _ := io.ReadAll(resp.Body)
+		return nil, fmt.Errorf("google search failed: %d - %s", resp.StatusCode, string(body))
+	}
+
+	var response googleResponse
+	if err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&response); err != nil {
+		return nil, err
+	}
+
+	results := make([]SearchResult, 0, len(response.Items))
+	for _, item := range response.Items {
+		results = append(results, SearchResult{
+			Title:   item.Title,
+			Link:    item.Link,
+			Content: item.Snippet,
+		})
+	}
+
+	return results, nil
+}

+ 25 - 0
mcp-servers/web-search/engine/types.go

@@ -0,0 +1,25 @@
+package engine
+
+import (
+	"context"
+)
+
+// SearchResult represents a single search result
+type SearchResult struct {
+	Title   string
+	Link    string
+	Content string
+}
+
+// SearchQuery represents search parameters
+type SearchQuery struct {
+	Query         string
+	MaxResults    int
+	Language      string
+	ArxivCategory string
+}
+
+// Engine interface for search engines
+type Engine interface {
+	Search(ctx context.Context, query SearchQuery) ([]SearchResult, error)
+}

+ 51 - 0
mcp-servers/web-search/features.md

@@ -0,0 +1,51 @@
+# Web Search MCP Server
+
+This MCP server provides web search capabilities through multiple search engines including Google, Bing, and Arxiv.
+
+## Features
+
+- **Multiple Search Engines**: Support for Google, Bing, and Arxiv
+- **Multi-Engine Search**: Search across multiple engines simultaneously
+- **Smart Search**: Intelligent query optimization and result aggregation
+- **Academic Search**: Specialized support for academic papers via Arxiv
+- **Language Support**: Search in different languages
+- **Configurable Results**: Control the number of results returned
+
+## Configuration
+
+Configure the search engines you want to use by providing their API keys:
+
+- **Google**: Requires both API key and Custom Search Engine ID
+- **Bing**: Requires Bing Search API key
+- **Arxiv**: No API key required (free to use)
+
+## Available Tools
+
+### web_search
+
+Basic web search using a single search engine.
+
+### multi_search
+
+Search across multiple engines simultaneously for comprehensive results.
+
+### smart_search
+
+Intelligently optimize queries and aggregate results for better answers.
+
+## Usage Examples
+
+1. Basic search:
+   - Query: "latest AI developments"
+   - Engine: "google"
+   - Max results: 10
+
+2. Academic search:
+   - Query: "transformer architecture"
+   - Engine: "arxiv"
+   - Category: "cs.AI"
+
+3. Multi-engine search:
+   - Query: "climate change impacts"
+   - Engines: ["google", "bing", "arxiv"]
+   - Max results per engine: 5

+ 578 - 0
mcp-servers/web-search/server.go

@@ -0,0 +1,578 @@
+package websearch
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"slices"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/bytedance/sonic"
+	mcpservers "github.com/labring/aiproxy/mcp-servers"
+	"github.com/labring/aiproxy/mcp-servers/web-search/engine"
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+
+	// embed static files
+	_ "embed"
+)
+
+// Configuration templates for the web search server
+var configTemplates = map[string]mcpservers.ConfigTemplate{
+	// Google Search Configuration
+	"google_api_key": {
+		Name:        "Google API Key",
+		Required:    mcpservers.ConfigRequiredTypeInitOptional,
+		Example:     "AIzaSyC...",
+		Description: "Google Custom Search API key",
+	},
+	"google_cx": {
+		Name:        "Google Search Engine ID",
+		Required:    mcpservers.ConfigRequiredTypeInitOptional,
+		Example:     "017576662512468239146:omuauf_lfve",
+		Description: "Google Custom Search Engine ID",
+	},
+
+	// Bing Search Configuration
+	"bing_api_key": {
+		Name:        "Bing API Key",
+		Required:    mcpservers.ConfigRequiredTypeInitOptional,
+		Example:     "1234567890abcdef",
+		Description: "Bing Search API key",
+	},
+
+	// Common Configuration
+	"default_engine": {
+		Name:        "Default Search Engine",
+		Required:    mcpservers.ConfigRequiredTypeInitOptional,
+		Example:     "google",
+		Description: "Default search engine to use (google, bing, arxiv)",
+		Validator: func(value string) error {
+			validEngines := []string{"google", "bing", "arxiv"}
+			for _, e := range validEngines {
+				if value == e {
+					return nil
+				}
+			}
+			return fmt.Errorf("invalid engine: %s, must be one of: %s", value, strings.Join(validEngines, ", "))
+		},
+	},
+	"max_results": {
+		Name:        "Max Results",
+		Required:    mcpservers.ConfigRequiredTypeInitOptional,
+		Example:     "10",
+		Description: "Maximum number of search results to return (default: 10)",
+		Validator: func(value string) error {
+			// Validate it's a number between 1 and 50
+			var num int
+			if _, err := fmt.Sscanf(value, "%d", &num); err != nil {
+				return errors.New("must be a number")
+			}
+			if num < 1 || num > 50 {
+				return errors.New("must be between 1 and 50")
+			}
+			return nil
+		},
+	},
+}
+
+// searchQuery represents a search query with parameters
+type searchQuery struct {
+	Query      string
+	MaxResults int
+	Type       string // "general", "academic", "news"
+}
+
+// NewServer creates a new MCP server for web search
+func NewServer(config map[string]string, _ map[string]string) (*server.MCPServer, error) {
+	// Create MCP server
+	mcpServer := server.NewMCPServer(
+		"web-search",
+		"1.0.0",
+	)
+
+	// Initialize search engines and settings
+	engines, defaultEngine, maxResults := initializeEngines(config)
+
+	// Add tools to the server
+	addWebSearchTool(mcpServer, engines, defaultEngine, maxResults)
+	addMultiSearchTool(mcpServer, engines, maxResults)
+
+	// Add smart search tool if engines are available
+	if len(engines) > 0 {
+		addSmartSearchTool(mcpServer, engines)
+	}
+
+	return mcpServer, nil
+}
+
+// initializeEngines sets up search engines based on configuration
+func initializeEngines(config map[string]string) (map[string]engine.Engine, string, int) {
+	engines := make(map[string]engine.Engine)
+
+	// Google Search
+	if apiKey := config["google_api_key"]; apiKey != "" {
+		if cx := config["google_cx"]; cx != "" {
+			engines["google"] = engine.NewGoogleEngine(apiKey, cx)
+		}
+	}
+
+	// Bing Search
+	if apiKey := config["bing_api_key"]; apiKey != "" {
+		engines["bing"] = engine.NewBingEngine(apiKey)
+	}
+
+	// Arxiv is always available (no API key required)
+	engines["arxiv"] = engine.NewArxivEngine()
+
+	// Get default settings
+	defaultEngine := config["default_engine"]
+	if defaultEngine == "" && len(engines) > 0 {
+		// Pick first available engine as default
+		for name := range engines {
+			defaultEngine = name
+			break
+		}
+	}
+
+	maxResults := 10
+	if maxStr := config["max_results"]; maxStr != "" {
+		maxResults, _ = strconv.Atoi(maxStr)
+	}
+
+	return engines, defaultEngine, maxResults
+}
+
+// addWebSearchTool adds the basic web search tool to the server
+func addWebSearchTool(mcpServer *server.MCPServer, engines map[string]engine.Engine, defaultEngine string, maxResults int) {
+	mcpServer.AddTool(
+		mcp.Tool{
+			Name:        "web_search",
+			Description: "Search the web using various search engines (Google, Bing, Arxiv)",
+			InputSchema: mcp.ToolInputSchema{
+				Type: "object",
+				Properties: map[string]any{
+					"query": map[string]any{
+						"type":        "string",
+						"description": "The search query",
+					},
+					"engine": map[string]any{
+						"type":        "string",
+						"description": "Search engine to use",
+						"enum":        getAvailableEngines(engines),
+						"default":     defaultEngine,
+					},
+					"max_results": map[string]any{
+						"type":        "integer",
+						"description": "Maximum number of results to return",
+						"default":     maxResults,
+						"minimum":     1,
+						"maximum":     50,
+					},
+					"language": map[string]any{
+						"type":        "string",
+						"description": "Language code for search results (e.g., 'en', 'zh')",
+					},
+					"arxiv_category": map[string]any{
+						"type":        "string",
+						"description": "Arxiv category for academic paper search (e.g., 'cs.AI', 'math.CO')",
+					},
+				},
+				Required: []string{"query"},
+			},
+		},
+		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+			args := request.GetArguments()
+
+			query, ok := args["query"].(string)
+			if !ok || query == "" {
+				return nil, errors.New("query is required")
+			}
+
+			engineName := defaultEngine
+			if e, ok := args["engine"].(string); ok && e != "" {
+				engineName = e
+			}
+
+			searchEngine, exists := engines[engineName]
+			if !exists {
+				return nil, fmt.Errorf("search engine '%s' is not available", engineName)
+			}
+
+			maxRes := maxResults
+			if m, ok := args["max_results"].(float64); ok {
+				maxRes = int(m)
+			}
+
+			language := ""
+			if l, ok := args["language"].(string); ok {
+				language = l
+			}
+
+			arxivCategory := ""
+			if ac, ok := args["arxiv_category"].(string); ok {
+				arxivCategory = ac
+			}
+
+			// Perform search
+			results, err := searchEngine.Search(ctx, engine.SearchQuery{
+				Query:         query,
+				MaxResults:    maxRes,
+				Language:      language,
+				ArxivCategory: arxivCategory,
+			})
+			if err != nil {
+				return nil, fmt.Errorf("search failed: %w", err)
+			}
+
+			// Format results
+			var formattedResults []map[string]any
+			for i, result := range results {
+				formattedResults = append(formattedResults, map[string]any{
+					"index":   i + 1,
+					"title":   result.Title,
+					"link":    result.Link,
+					"snippet": result.Content,
+				})
+			}
+
+			response := map[string]any{
+				"engine":  engineName,
+				"query":   query,
+				"count":   len(results),
+				"results": formattedResults,
+			}
+
+			responseJSON, err := sonic.Marshal(response)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshal response: %w", err)
+			}
+
+			return mcp.NewToolResultText(string(responseJSON)), nil
+		},
+	)
+}
+
+// addMultiSearchTool adds the multi-engine search tool to the server
+func addMultiSearchTool(mcpServer *server.MCPServer, engines map[string]engine.Engine, maxResults int) {
+	mcpServer.AddTool(
+		mcp.Tool{
+			Name:        "multi_search",
+			Description: "Search across multiple search engines simultaneously",
+			InputSchema: mcp.ToolInputSchema{
+				Type: "object",
+				Properties: map[string]any{
+					"query": map[string]any{
+						"type":        "string",
+						"description": "The search query",
+					},
+					"engines": map[string]any{
+						"type":        "array",
+						"description": "List of search engines to use",
+						"items": map[string]any{
+							"type": "string",
+							"enum": getAvailableEngines(engines),
+						},
+						"default": getAvailableEngines(engines),
+					},
+					"max_results_per_engine": map[string]any{
+						"type":        "integer",
+						"description": "Maximum number of results per engine",
+						"default":     5,
+						"minimum":     1,
+						"maximum":     20,
+					},
+				},
+				Required: []string{"query"},
+			},
+		},
+		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+			args := request.GetArguments()
+
+			query, ok := args["query"].(string)
+			if !ok || query == "" {
+				return nil, errors.New("query is required")
+			}
+
+			engineNames := getAvailableEngines(engines)
+			if e, ok := args["engines"].([]any); ok {
+				engineNames = []string{}
+				for _, eng := range e {
+					if engStr, ok := eng.(string); ok {
+						engineNames = append(engineNames, engStr)
+					}
+				}
+			}
+
+			if m, ok := args["max_results_per_engine"].(float64); ok {
+				maxResults = int(m)
+			}
+
+			allResults := make(map[string][]map[string]any)
+
+			for _, engineName := range engineNames {
+				searchEngine, exists := engines[engineName]
+				if !exists {
+					continue
+				}
+
+				results, err := searchEngine.Search(ctx, engine.SearchQuery{
+					Query:      query,
+					MaxResults: maxResults,
+				})
+				if err != nil {
+					// Log error but continue with other engines
+					allResults[engineName] = []map[string]any{
+						{"error": err.Error()},
+					}
+					continue
+				}
+
+				var engineResults []map[string]any
+				for i, result := range results {
+					engineResults = append(engineResults, map[string]any{
+						"index":   i + 1,
+						"title":   result.Title,
+						"link":    result.Link,
+						"snippet": result.Content,
+					})
+				}
+				allResults[engineName] = engineResults
+			}
+
+			response := map[string]any{
+				"query":   query,
+				"engines": engineNames,
+				"results": allResults,
+			}
+
+			responseJSON, err := sonic.Marshal(response)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshal response: %w", err)
+			}
+
+			return mcp.NewToolResultText(string(responseJSON)), nil
+		},
+	)
+}
+
+// addSmartSearchTool adds the smart search tool to the server
+func addSmartSearchTool(mcpServer *server.MCPServer, engines map[string]engine.Engine) {
+	mcpServer.AddTool(
+		mcp.Tool{
+			Name:        "smart_search",
+			Description: "Intelligently search the web with query optimization and result summarization",
+			InputSchema: mcp.ToolInputSchema{
+				Type: "object",
+				Properties: map[string]any{
+					"question": map[string]any{
+						"type":        "string",
+						"description": "The user's question or search intent",
+					},
+					"search_depth": map[string]any{
+						"type":        "string",
+						"description": "Search depth: 'quick' (1-2 queries), 'normal' (3-5 queries), 'deep' (5-10 queries)",
+						"enum":        []string{"quick", "normal", "deep"},
+						"default":     "normal",
+					},
+					"include_academic": map[string]any{
+						"type":        "boolean",
+						"description": "Whether to include academic papers from Arxiv",
+						"default":     false,
+					},
+				},
+				Required: []string{"question"},
+			},
+		},
+		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+			args := request.GetArguments()
+
+			question, ok := args["question"].(string)
+			if !ok || question == "" {
+				return nil, errors.New("question is required")
+			}
+
+			searchDepth := "normal"
+			if d, ok := args["search_depth"].(string); ok {
+				searchDepth = d
+			}
+
+			includeAcademic := false
+			if ia, ok := args["include_academic"].(bool); ok {
+				includeAcademic = ia
+			}
+
+			// Generate optimized search queries based on the question
+			queries := generateSearchQueries(question, searchDepth)
+
+			allResults := []engine.SearchResult{}
+			searchSummary := map[string]any{
+				"original_question": question,
+				"search_queries":    queries,
+				"engines_used":      []string{},
+			}
+
+			// Execute searches
+			for _, q := range queries {
+				// Determine which engine to use based on query type
+				engineName := determineEngine(q, engines, includeAcademic)
+				if engineName == "" {
+					continue
+				}
+
+				searchEngine := engines[engineName]
+				results, err := searchEngine.Search(ctx, engine.SearchQuery{
+					Query:      q.Query,
+					MaxResults: q.MaxResults,
+				})
+				if err == nil {
+					allResults = append(allResults, results...)
+					if !slices.Contains(searchSummary["engines_used"].([]string), engineName) {
+						searchSummary["engines_used"] = append(searchSummary["engines_used"].([]string), engineName)
+					}
+				}
+			}
+
+			// Remove duplicates
+			uniqueResults := removeDuplicates(allResults)
+
+			// Format final response
+			var formattedResults []map[string]any
+			for i, result := range uniqueResults {
+				formattedResults = append(formattedResults, map[string]any{
+					"index":   i + 1,
+					"title":   result.Title,
+					"link":    result.Link,
+					"snippet": result.Content,
+				})
+			}
+
+			response := map[string]any{
+				"summary":       searchSummary,
+				"total_results": len(uniqueResults),
+				"results":       formattedResults,
+				"search_time":   time.Now().Format(time.RFC3339),
+			}
+
+			responseJSON, err := sonic.Marshal(response)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshal response: %w", err)
+			}
+
+			return mcp.NewToolResultText(string(responseJSON)), nil
+		},
+	)
+}
+
+// getAvailableEngines returns a list of available search engine names
+func getAvailableEngines(engines map[string]engine.Engine) []string {
+	names := make([]string, 0, len(engines))
+	for name := range engines {
+		names = append(names, name)
+	}
+	return names
+}
+
+// generateSearchQueries creates search queries based on the user's question and depth
+func generateSearchQueries(question string, depth string) []searchQuery {
+	// Simple query generation logic - in production, this could use AI
+	queries := []searchQuery{}
+
+	baseQueries := 1
+	switch depth {
+	case "quick":
+		baseQueries = 1
+	case "normal":
+		baseQueries = 2
+	case "deep":
+		baseQueries = 3
+	}
+
+	// Generate variations of the question
+	queries = append(queries, searchQuery{
+		Query:      question,
+		MaxResults: 10,
+		Type:       "general",
+	})
+
+	if baseQueries >= 2 {
+		// Add a more specific query
+		queries = append(queries, searchQuery{
+			Query:      question + " latest news",
+			MaxResults: 5,
+			Type:       "news",
+		})
+	}
+
+	if baseQueries >= 3 {
+		// Add an academic query
+		queries = append(queries, searchQuery{
+			Query:      question + " research papers",
+			MaxResults: 5,
+			Type:       "academic",
+		})
+	}
+
+	return queries
+}
+
+// determineEngine selects the appropriate search engine for a query
+func determineEngine(q searchQuery, engines map[string]engine.Engine, includeAcademic bool) string {
+	// Simple engine selection logic
+	if q.Type == "academic" && includeAcademic {
+		if _, ok := engines["arxiv"]; ok {
+			return "arxiv"
+		}
+	}
+
+	// Prefer Google if available
+	if _, ok := engines["google"]; ok {
+		return "google"
+	}
+
+	// Then Bing
+	if _, ok := engines["bing"]; ok {
+		return "bing"
+	}
+
+	// Return first available engine
+	for name := range engines {
+		return name
+	}
+
+	return ""
+}
+
+// removeDuplicates removes duplicate search results based on URL
+func removeDuplicates(results []engine.SearchResult) []engine.SearchResult {
+	seen := make(map[string]bool, len(results))
+	unique := []engine.SearchResult{}
+
+	for _, result := range results {
+		if !seen[result.Link] {
+			seen[result.Link] = true
+			unique = append(unique, result)
+		}
+	}
+
+	return unique
+}
+
+//go:embed features.md
+var readme string
+
+// Register the server
+func init() {
+	mcpservers.Register(
+		mcpservers.NewEmbedMcp(
+			"web-search",
+			"Web Search",
+			NewServer,
+			mcpservers.WithConfigTemplates(configTemplates),
+			mcpservers.WithTags([]string{"search", "web", "google", "bing", "arxiv"}),
+			mcpservers.WithReadme(readme),
+		),
+	)
+}

+ 24 - 24
openapi-mcp/convert/convert.go

@@ -235,7 +235,7 @@ type Args struct {
 	Forms           map[string]any
 }
 
-func getArgs(args map[string]interface{}) Args {
+func getArgs(args map[string]any) Args {
 	arg := Args{
 		Headers: make(map[string]any),
 		Query:   make(map[string]any),
@@ -414,7 +414,7 @@ func (c *Converter) generateResponseDescription(responses openapi3.Responses) st
 		response := responseRef.Value
 		desc := fmt.Sprintf("- status: %s, description: %s", code, *response.Description)
 
-		rawSchema, ok := response.Extensions["schema"].(map[string]interface{})
+		rawSchema, ok := response.Extensions["schema"].(map[string]any)
 		if ok && len(rawSchema) > 0 {
 			jsonStr, err := json.Marshal(rawSchema)
 			if err != nil {
@@ -654,8 +654,8 @@ func (c *Converter) convertParameters(parameters openapi3.Parameters) []mcp.Tool
 }
 
 // processSchemaItems processes schema items for array types
-func (c *Converter) processSchemaItems(schema *openapi3.Schema, visited map[string]bool) map[string]interface{} {
-	item := make(map[string]interface{})
+func (c *Converter) processSchemaItems(schema *openapi3.Schema, visited map[string]bool) map[string]any {
+	item := make(map[string]any)
 
 	if schema.Type != nil {
 		item["type"] = schema.Type
@@ -667,7 +667,7 @@ func (c *Converter) processSchemaItems(schema *openapi3.Schema, visited map[stri
 
 	// Process nested properties if this is an object
 	if len(schema.Properties) > 0 {
-		properties := make(map[string]interface{})
+		properties := make(map[string]any)
 		for propName, propRef := range schema.Properties {
 			if propRef.Value != nil {
 				properties[propName] = c.processSchemaProperty(propRef.Value, visited)
@@ -685,8 +685,8 @@ func (c *Converter) processSchemaItems(schema *openapi3.Schema, visited map[stri
 }
 
 // processSchemaProperties processes schema properties for object types
-func (c *Converter) processSchemaProperties(schema *openapi3.Schema, visited map[string]bool) map[string]interface{} {
-	obj := make(map[string]interface{})
+func (c *Converter) processSchemaProperties(schema *openapi3.Schema, visited map[string]bool) map[string]any {
+	obj := make(map[string]any)
 
 	for propName, propRef := range schema.Properties {
 		if propRef.Value != nil {
@@ -698,13 +698,13 @@ func (c *Converter) processSchemaProperties(schema *openapi3.Schema, visited map
 }
 
 // processSchemaProperty processes a single schema property
-func (c *Converter) processSchemaProperty(schema *openapi3.Schema, visited map[string]bool) map[string]interface{} {
+func (c *Converter) processSchemaProperty(schema *openapi3.Schema, visited map[string]bool) map[string]any {
 	// Check for circular references
 	if schema.Title != "" {
 		refKey := schema.Title
 		if visited[refKey] {
 			// We've seen this schema before, return a simplified reference to avoid circular references
-			return map[string]interface{}{
+			return map[string]any{
 				"type":        "reference",
 				"description": "Circular reference to " + refKey,
 				"title":       refKey,
@@ -724,8 +724,8 @@ func (c *Converter) processSchemaProperty(schema *openapi3.Schema, visited map[s
 
 // buildPropertyMap builds the property map for a schema
 // This function was extracted to reduce cyclomatic complexity
-func (c *Converter) buildPropertyMap(schema *openapi3.Schema, visited map[string]bool) map[string]interface{} {
-	property := make(map[string]interface{})
+func (c *Converter) buildPropertyMap(schema *openapi3.Schema, visited map[string]bool) map[string]any {
+	property := make(map[string]any)
 
 	// Add basic schema information
 	c.addBasicSchemaInfo(schema, property)
@@ -749,7 +749,7 @@ func (c *Converter) buildPropertyMap(schema *openapi3.Schema, visited map[string
 }
 
 // addBasicSchemaInfo adds basic schema information to the property map
-func (c *Converter) addBasicSchemaInfo(schema *openapi3.Schema, property map[string]interface{}) {
+func (c *Converter) addBasicSchemaInfo(schema *openapi3.Schema, property map[string]any) {
 	if schema.Type != nil {
 		property["type"] = schema.Type
 	}
@@ -776,7 +776,7 @@ func (c *Converter) addBasicSchemaInfo(schema *openapi3.Schema, property map[str
 }
 
 // addSchemaValidations adds schema validations to the property map
-func (c *Converter) addSchemaValidations(schema *openapi3.Schema, property map[string]interface{}) {
+func (c *Converter) addSchemaValidations(schema *openapi3.Schema, property map[string]any) {
 	// Boolean flags
 	if schema.Nullable {
 		property["nullable"] = schema.Nullable
@@ -846,10 +846,10 @@ func (c *Converter) addSchemaValidations(schema *openapi3.Schema, property map[s
 }
 
 // addSchemaComposition adds schema composition to the property map
-func (c *Converter) addSchemaComposition(schema *openapi3.Schema, property map[string]interface{}, visited map[string]bool) {
+func (c *Converter) addSchemaComposition(schema *openapi3.Schema, property map[string]any, visited map[string]bool) {
 	// Schema composition
 	if len(schema.OneOf) > 0 {
-		oneOf := make([]interface{}, 0, len(schema.OneOf))
+		oneOf := make([]any, 0, len(schema.OneOf))
 		for _, schemaRef := range schema.OneOf {
 			if schemaRef.Value != nil {
 				oneOf = append(oneOf, c.processSchemaProperty(schemaRef.Value, visited))
@@ -861,7 +861,7 @@ func (c *Converter) addSchemaComposition(schema *openapi3.Schema, property map[s
 	}
 
 	if len(schema.AnyOf) > 0 {
-		anyOf := make([]interface{}, 0, len(schema.AnyOf))
+		anyOf := make([]any, 0, len(schema.AnyOf))
 		for _, schemaRef := range schema.AnyOf {
 			if schemaRef.Value != nil {
 				anyOf = append(anyOf, c.processSchemaProperty(schemaRef.Value, visited))
@@ -873,7 +873,7 @@ func (c *Converter) addSchemaComposition(schema *openapi3.Schema, property map[s
 	}
 
 	if len(schema.AllOf) > 0 {
-		allOf := make([]interface{}, 0, len(schema.AllOf))
+		allOf := make([]any, 0, len(schema.AllOf))
 		for _, schemaRef := range schema.AllOf {
 			if schemaRef.Value != nil {
 				allOf = append(allOf, c.processSchemaProperty(schemaRef.Value, visited))
@@ -890,7 +890,7 @@ func (c *Converter) addSchemaComposition(schema *openapi3.Schema, property map[s
 }
 
 // addObjectProperties adds object properties to the property map
-func (c *Converter) addObjectProperties(schema *openapi3.Schema, property map[string]interface{}, visited map[string]bool) {
+func (c *Converter) addObjectProperties(schema *openapi3.Schema, property map[string]any, visited map[string]bool) {
 	// Handle AdditionalProperties
 	if schema.AdditionalProperties.Has != nil {
 		property["additionalProperties"] = *schema.AdditionalProperties.Has
@@ -900,7 +900,7 @@ func (c *Converter) addObjectProperties(schema *openapi3.Schema, property map[st
 
 	// Handle discriminator
 	if schema.Discriminator != nil {
-		discriminator := make(map[string]interface{})
+		discriminator := make(map[string]any)
 		discriminator["propertyName"] = schema.Discriminator.PropertyName
 		if len(schema.Discriminator.Mapping) > 0 {
 			discriminator["mapping"] = schema.Discriminator.Mapping
@@ -910,7 +910,7 @@ func (c *Converter) addObjectProperties(schema *openapi3.Schema, property map[st
 
 	// Recursively process nested objects
 	if schema.Type != nil && schema.Type.Is("object") && len(schema.Properties) > 0 {
-		nestedProps := make(map[string]interface{})
+		nestedProps := make(map[string]any)
 		for propName, propRef := range schema.Properties {
 			if propRef.Value != nil {
 				nestedProps[propName] = c.processSchemaProperty(propRef.Value, visited)
@@ -920,17 +920,17 @@ func (c *Converter) addObjectProperties(schema *openapi3.Schema, property map[st
 	}
 }
 
-func (c *Converter) addArrayItems(schema *openapi3.Schema, property map[string]interface{}, visited map[string]bool) {
+func (c *Converter) addArrayItems(schema *openapi3.Schema, property map[string]any, visited map[string]bool) {
 	// Recursively process array items
 	if schema.Type != nil && schema.Type.Is("array") && schema.Items != nil && schema.Items.Value != nil {
 		property["items"] = c.processSchemaItems(schema.Items.Value, visited)
 	}
 }
 
-func (c *Converter) addAdditionalMetadata(schema *openapi3.Schema, property map[string]interface{}) {
+func (c *Converter) addAdditionalMetadata(schema *openapi3.Schema, property map[string]any) {
 	// Handle external docs if present
 	if schema.ExternalDocs != nil {
-		externalDocs := make(map[string]interface{})
+		externalDocs := make(map[string]any)
 		if schema.ExternalDocs.Description != "" {
 			externalDocs["description"] = schema.ExternalDocs.Description
 		}
@@ -942,7 +942,7 @@ func (c *Converter) addAdditionalMetadata(schema *openapi3.Schema, property map[
 
 	// Handle XML object if present
 	if schema.XML != nil {
-		xml := make(map[string]interface{})
+		xml := make(map[string]any)
 		if schema.XML.Name != "" {
 			xml["name"] = schema.XML.Name
 		}