mirror of
https://github.com/alibaba/higress.git
synced 2026-05-27 22:27:29 +08:00
Add remote mcp server sdk (#1946)
This commit is contained in:
25
plugins/wasm-go/mcp-servers/quark-search/go.mod
Normal file
25
plugins/wasm-go/mcp-servers/quark-search/go.mod
Normal file
@@ -0,0 +1,25 @@
|
||||
module quark-search
|
||||
|
||||
go 1.24
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250324133957-dab499f6ade6
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250323151219-d75620c61711 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
38
plugins/wasm-go/mcp-servers/quark-search/go.sum
Normal file
38
plugins/wasm-go/mcp-servers/quark-search/go.sum
Normal file
@@ -0,0 +1,38 @@
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250324133957-dab499f6ade6 h1:/iHNur+B0lHmcy97XYwHb6QrnHJichzKs37gnTyGP3k=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250324133957-dab499f6ade6/go.mod h1:csP9Mpkc+gVgbZsizCdcYSy0LJrQA+//RcnZBInyknc=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
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/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250323151219-d75620c61711 h1:n5sZwSZWQ5uKS69hu50/0gliTFrIJ1w+g/FSdIIiZIs=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250323151219-d75620c61711/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
32
plugins/wasm-go/mcp-servers/quark-search/main.go
Normal file
32
plugins/wasm-go/mcp-servers/quark-search/main.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"quark-search/server"
|
||||
"quark-search/tools"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"quark-mcp-server",
|
||||
wrapper.ParseRawConfig(server.ParseFromConfig),
|
||||
wrapper.AddMCPTool("web_search", tools.WebSearch{}),
|
||||
)
|
||||
}
|
||||
41
plugins/wasm-go/mcp-servers/quark-search/server/server.go
Normal file
41
plugins/wasm-go/mcp-servers/quark-search/server/server.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
type QuarkMCPServer struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
func (s QuarkMCPServer) ConfigHasError() error {
|
||||
if s.ApiKey == "" {
|
||||
return errors.New("missing api key")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseFromConfig(configBytes []byte, server *QuarkMCPServer) error {
|
||||
return json.Unmarshal(configBytes, server)
|
||||
}
|
||||
|
||||
func ParseFromRequest(ctx wrapper.HttpContext, server *QuarkMCPServer) error {
|
||||
return ctx.ParseMCPServerConfig(server)
|
||||
}
|
||||
130
plugins/wasm-go/mcp-servers/quark-search/tools/web_search.go
Normal file
130
plugins/wasm-go/mcp-servers/quark-search/tools/web_search.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"quark-search/server"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type SearchResult struct {
|
||||
Title string
|
||||
Link string
|
||||
Content string
|
||||
}
|
||||
|
||||
func (result SearchResult) Valid() bool {
|
||||
return result.Title != "" && result.Link != "" && result.Content != ""
|
||||
}
|
||||
|
||||
func (result SearchResult) Format() string {
|
||||
return fmt.Sprintf(`
|
||||
## Title: %s
|
||||
|
||||
### Reference URL
|
||||
%s
|
||||
|
||||
### Content
|
||||
%s
|
||||
`, result.Title, result.Link, result.Content)
|
||||
}
|
||||
|
||||
type WebSearch struct {
|
||||
Query string `json:"query" jsonschema_description:"Search query, please use Chinese" jsonschema:"example=黄金价格走势"`
|
||||
ContentMode string `json:"contentMode,omitempty" jsonschema_description:"Return the level of content detail, choose to use summary or full text" jsonschema:"enum=full,enum=summary,default=summary"`
|
||||
Number uint32 `json:"number,omitempty" jsonschema_description:"Number of results" jsonschema:"default=5"`
|
||||
}
|
||||
|
||||
// Description returns the description field for the MCP tool definition.
|
||||
// This corresponds to the "description" field in the MCP tool JSON response,
|
||||
// which provides a human-readable explanation of the tool's purpose and usage.
|
||||
func (t WebSearch) Description() string {
|
||||
return `Performs a web search using the Quark Search API, ideal for general queries, news, articles, and online content.
|
||||
Use this for broad information gathering, recent events, or when you need diverse web sources.
|
||||
Because Quark search performs poorly for English searches, please use Chinese for the query parameters.`
|
||||
}
|
||||
|
||||
// InputSchema returns the inputSchema field for the MCP tool definition.
|
||||
// This corresponds to the "inputSchema" field in the MCP tool JSON response,
|
||||
// which defines the JSON Schema for the tool's input parameters, including
|
||||
// property types, descriptions, and required fields.
|
||||
func (t WebSearch) InputSchema() map[string]any {
|
||||
return wrapper.ToInputSchema(&WebSearch{})
|
||||
}
|
||||
|
||||
// Create instantiates a new WebSearch tool instance based on the input parameters
|
||||
// from an MCP tool call.
|
||||
func (t WebSearch) Create(params []byte) wrapper.MCPTool[server.QuarkMCPServer] {
|
||||
webSearch := &WebSearch{
|
||||
ContentMode: "summary",
|
||||
Number: 5,
|
||||
}
|
||||
json.Unmarshal(params, &webSearch)
|
||||
return webSearch
|
||||
}
|
||||
|
||||
// Call implements the core logic for handling an MCP tool call. This method is executed
|
||||
// when the tool is invoked through the MCP framework. It processes the configured parameters,
|
||||
// makes the actual API request to the service, parses the response,
|
||||
// and formats the results to be returned to the caller.
|
||||
func (t WebSearch) Call(ctx wrapper.HttpContext, config server.QuarkMCPServer) error {
|
||||
err := server.ParseFromRequest(ctx, &config)
|
||||
if err != nil {
|
||||
log.Errorf("parse config from request failed, err:%s", err)
|
||||
}
|
||||
err = config.ConfigHasError()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.RouteCall(http.MethodGet, fmt.Sprintf("https://cloud-iqs.aliyuncs.com/search/genericSearch?query=%s", url.QueryEscape(t.Query)),
|
||||
[][2]string{{"Accept", "application/json"},
|
||||
{"X-API-Key", config.ApiKey}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
ctx.OnMCPToolCallError(fmt.Errorf("quark search call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
jsonObj := gjson.ParseBytes(responseBody)
|
||||
var results []string
|
||||
for index, item := range jsonObj.Get("pageItems").Array() {
|
||||
var content string
|
||||
if t.ContentMode == "full" {
|
||||
content = item.Get("markdownText").String()
|
||||
if content == "" {
|
||||
content = item.Get("mainText").String()
|
||||
}
|
||||
} else if t.ContentMode == "summary" {
|
||||
content = item.Get("snippet").String()
|
||||
}
|
||||
result := SearchResult{
|
||||
Title: item.Get("title").String(),
|
||||
Link: item.Get("link").String(),
|
||||
Content: content,
|
||||
}
|
||||
if result.Valid() && index < int(t.Number) {
|
||||
results = append(results, result.Format())
|
||||
}
|
||||
}
|
||||
ctx.SendMCPToolTextResult(fmt.Sprintf("# Search Results\n\n%s", strings.Join(results, "\n\n")))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestWebSearchInputSchema tests the InputSchema method of WebSearch
|
||||
// to verify that the JSON schema configuration is correct.
|
||||
func TestWebSearchInputSchema(t *testing.T) {
|
||||
// Create a WebSearch instance
|
||||
webSearch := WebSearch{}
|
||||
|
||||
// Get the input schema
|
||||
schema := webSearch.InputSchema()
|
||||
|
||||
// Marshal the schema to JSON for better readability
|
||||
schemaJSON, err := json.MarshalIndent(schema, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal schema to JSON: %v", err)
|
||||
}
|
||||
|
||||
// Print the schema
|
||||
fmt.Printf("WebSearch InputSchema:\n%s\n", string(schemaJSON))
|
||||
|
||||
// Basic validation to ensure the schema is not empty
|
||||
if len(schema) == 0 {
|
||||
t.Error("InputSchema returned an empty schema")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user