From 037c71a320dc06f831edc410ff490125f36bb383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?= Date: Sat, 29 Mar 2025 20:28:10 +0800 Subject: [PATCH] refactor mcp sdk (#1977) --- plugins/wasm-go/mcp-servers/README.md | 95 ++++++++---------- plugins/wasm-go/mcp-servers/README_zh.md | 96 ++++++++----------- .../server.go => amap-tools/config/config.go} | 28 +----- plugins/wasm-go/mcp-servers/amap-tools/go.mod | 6 +- plugins/wasm-go/mcp-servers/amap-tools/go.sum | 4 +- .../wasm-go/mcp-servers/amap-tools/main.go | 56 ++++++----- .../amap-tools/tools/maps_around_search.go | 62 ++++++------ .../amap-tools/tools/maps_bicycling.go | 74 +++++++------- .../tools/maps_direction_driving.go | 78 ++++++++------- .../maps_direction_transit_integrated.go | 91 ++++++++++-------- .../tools/maps_direction_walking.go | 75 ++++++++------- .../amap-tools/tools/maps_distance.go | 66 +++++++------ .../mcp-servers/amap-tools/tools/maps_geo.go | 61 +++++++----- .../amap-tools/tools/maps_ip_location.go | 68 +++++++------ .../amap-tools/tools/maps_regeocode.go | 62 ++++++------ .../amap-tools/tools/maps_search_detail.go | 80 +++++++++------- .../amap-tools/tools/maps_text_search.go | 71 ++++++++------ .../amap-tools/tools/maps_weather.go | 86 +++++++++-------- .../mcp-servers/quark-search/config/config.go | 19 ++++ .../wasm-go/mcp-servers/quark-search/go.mod | 2 +- .../wasm-go/mcp-servers/quark-search/go.sum | 4 +- .../wasm-go/mcp-servers/quark-search/main.go | 12 +-- .../quark-search/tools/web_search.go | 32 +++---- 23 files changed, 652 insertions(+), 576 deletions(-) rename plugins/wasm-go/mcp-servers/{quark-search/server/server.go => amap-tools/config/config.go} (55%) create mode 100644 plugins/wasm-go/mcp-servers/quark-search/config/config.go diff --git a/plugins/wasm-go/mcp-servers/README.md b/plugins/wasm-go/mcp-servers/README.md index 5b09e797f..155e3d3f9 100644 --- a/plugins/wasm-go/mcp-servers/README.md +++ b/plugins/wasm-go/mcp-servers/README.md @@ -33,49 +33,20 @@ my-mcp-server/ ├── go.mod # Go module definition ├── go.sum # Go module checksums ├── main.go # Entry point that registers tools and resources -├── server/ -│ └── server.go # Server configuration and parsing └── tools/ └── my_tool.go # Tool implementation ``` ## Server Configuration -The server configuration defines the parameters needed for the server to function. For example: +Define a configuration structure for your MCP server to store settings like API keys: ```go -// server/server.go -package server +// config/config.go +package config -import ( - "encoding/json" - "errors" - - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" -) - -// Define your server configuration structure -type MyMCPServer struct { +type MyServerConfig struct { ApiKey string `json:"apiKey"` - // Add other configuration fields as needed -} - -// Validate the configuration -func (s MyMCPServer) ConfigHasError() error { - if s.ApiKey == "" { - return errors.New("missing api key") - } - return nil -} - -// Parse configuration from JSON -func ParseFromConfig(configBytes []byte, server *MyMCPServer) error { - return json.Unmarshal(configBytes, server) -} - -// Parse configuration from HTTP request -func ParseFromRequest(ctx wrapper.HttpContext, server *MyMCPServer) error { - return ctx.ParseMCPServerConfig(server) } ``` @@ -96,12 +67,13 @@ package tools import ( "encoding/json" + "errors" "fmt" "net/http" - "my-mcp-server/server" - - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "my-mcp-server/config" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" ) // Define your tool structure with input parameters @@ -122,13 +94,13 @@ func (t MyTool) Description() string { // which defines the JSON Schema for the tool's input parameters, including // property types, descriptions, and required fields. func (t MyTool) InputSchema() map[string]any { - return wrapper.ToInputSchema(&MyTool{}) + return server.ToInputSchema(&MyTool{}) } // Create instantiates a new tool instance based on the input parameters // from an MCP tool call. It deserializes the JSON parameters into a struct, // applying default values for optional fields, and returns the configured tool instance. -func (t MyTool) Create(params []byte) wrapper.MCPTool[server.MyMCPServer] { +func (t MyTool) Create(params []byte) server.Tool { myTool := &MyTool{ Param2: 5, // Default value } @@ -139,22 +111,19 @@ func (t MyTool) Create(params []byte) wrapper.MCPTool[server.MyMCPServer] { // 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 any necessary API requests, and formats the results to be returned to the caller. -func (t MyTool) Call(ctx wrapper.HttpContext, config server.MyMCPServer) error { - // Validate configuration - err := server.ParseFromRequest(ctx, &config) - if err != nil { - return err - } - err = config.ConfigHasError() - if err != nil { - return err +func (t MyTool) Call(ctx server.HttpContext, s server.Server) error { + // Get server configuration + serverConfig := &config.MyServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("missing api key in server configuration") } // Implement your tool's logic here // ... // Return results - ctx.SendMCPToolTextResult(fmt.Sprintf("Result: %s, %d", t.Param1, t.Param2)) + utils.SendMCPToolTextResult(ctx, fmt.Sprintf("Result: %s, %d", t.Param1, t.Param2)) return nil } ``` @@ -168,21 +137,20 @@ The main.go file is the entry point for your MCP server. It registers your tools package main import ( - "my-mcp-server/server" "my-mcp-server/tools" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" ) func main() {} func init() { - wrapper.SetCtx( + myMCPServer := &server.MCPServer{} + server.Load(server.AddMCPServer( "my-mcp-server", // Server name - wrapper.ParseRawConfig(server.ParseFromConfig), - wrapper.AddMCPTool("my_tool", tools.MyTool{}), // Register tools + myMCPServer.AddMCPTool("my_tool", &tools.MyTool{}), // Register tools // Add more tools as needed - ) + )) } ``` @@ -191,8 +159,21 @@ func init() { Your MCP server must use a specific version of the wasm-go SDK that supports Go 1.24's WebAssembly compilation features: ```bash -# Add the required dependency with the specific version tag -go get github.com/alibaba/higress/plugins/wasm-go@wasm-go-1.24 +# Add the required dependency +go get github.com/alibaba/higress/plugins/wasm-go +``` + +Make sure your go.mod file specifies Go 1.24: + +``` +module my-mcp-server + +go 1.24 + +require ( + github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250324133957-dab499f6ade6 + // other dependencies +) ``` ## Building the WASM Binary @@ -242,6 +223,8 @@ import ( "testing" ) +// TestMyToolInputSchema tests the InputSchema method of MyTool +// to verify that the JSON schema configuration is correct. func TestMyToolInputSchema(t *testing.T) { myTool := MyTool{} schema := myTool.InputSchema() diff --git a/plugins/wasm-go/mcp-servers/README_zh.md b/plugins/wasm-go/mcp-servers/README_zh.md index d317b1d91..9c1143b5e 100644 --- a/plugins/wasm-go/mcp-servers/README_zh.md +++ b/plugins/wasm-go/mcp-servers/README_zh.md @@ -33,49 +33,20 @@ my-mcp-server/ ├── go.mod # Go 模块定义 ├── go.sum # Go 模块校验和 ├── main.go # 注册工具和资源的入口点 -├── server/ -│ └── server.go # 服务器配置和解析 └── tools/ └── my_tool.go # 工具实现 ``` ## 服务器配置 -服务器配置定义了服务器运行所需的参数。例如: +为您的 MCP 服务器定义一个配置结构,用于存储 API 密钥等设置: ```go -// server/server.go -package server +// config/config.go +package config -import ( - "encoding/json" - "errors" - - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" -) - -// 定义服务器配置结构 -type MyMCPServer struct { +type MyServerConfig struct { ApiKey string `json:"apiKey"` - // 根据需要添加其他配置字段 -} - -// 验证配置 -func (s MyMCPServer) ConfigHasError() error { - if s.ApiKey == "" { - return errors.New("missing api key") - } - return nil -} - -// 从 JSON 解析配置 -func ParseFromConfig(configBytes []byte, server *MyMCPServer) error { - return json.Unmarshal(configBytes, server) -} - -// 从 HTTP 请求解析配置 -func ParseFromRequest(ctx wrapper.HttpContext, server *MyMCPServer) error { - return ctx.ParseMCPServerConfig(server) } ``` @@ -96,12 +67,13 @@ package tools import ( "encoding/json" + "errors" "fmt" "net/http" - "my-mcp-server/server" - - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "my-mcp-server/config" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" ) // 定义带有输入参数的工具结构 @@ -121,12 +93,12 @@ func (t MyTool) Description() string { // 这对应于 MCP 工具 JSON 响应中的 "inputSchema" 字段, // 定义了工具输入参数的 JSON Schema,包括属性类型、描述和必填字段。 func (t MyTool) InputSchema() map[string]any { - return wrapper.ToInputSchema(&MyTool{}) + return server.ToInputSchema(&MyTool{}) } // Create 基于 MCP 工具调用的输入参数实例化一个新的工具实例。 // 它将 JSON 参数反序列化为结构体,为可选字段应用默认值,并返回配置好的工具实例。 -func (t MyTool) Create(params []byte) wrapper.MCPTool[server.MyMCPServer] { +func (t MyTool) Create(params []byte) server.Tool { myTool := &MyTool{ Param2: 5, // 默认值 } @@ -136,22 +108,19 @@ func (t MyTool) Create(params []byte) wrapper.MCPTool[server.MyMCPServer] { // Call 实现处理 MCP 工具调用的核心逻辑。当通过 MCP 框架调用工具时,执行此方法。 // 它处理配置的参数,进行必要的 API 请求,并格式化返回给调用者的结果。 -func (t MyTool) Call(ctx wrapper.HttpContext, config server.MyMCPServer) error { - // 验证配置 - err := server.ParseFromRequest(ctx, &config) - if err != nil { - return err - } - err = config.ConfigHasError() - if err != nil { - return err +func (t MyTool) Call(ctx server.HttpContext, s server.Server) error { + // 获取服务器配置 + serverConfig := &config.MyServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("服务器配置中缺少 API 密钥") } // 在这里实现工具的逻辑 // ... // 返回结果 - ctx.SendMCPToolTextResult(fmt.Sprintf("结果: %s, %d", t.Param1, t.Param2)) + utils.SendMCPToolTextResult(ctx, fmt.Sprintf("结果: %s, %d", t.Param1, t.Param2)) return nil } ``` @@ -165,21 +134,20 @@ main.go 文件是 MCP 服务器的入口点。它注册工具和资源: package main import ( - "my-mcp-server/server" "my-mcp-server/tools" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" ) func main() {} func init() { - wrapper.SetCtx( + myMCPServer := &server.MCPServer{} + server.Load(server.AddMCPServer( "my-mcp-server", // 服务器名称 - wrapper.ParseRawConfig(server.ParseFromConfig), - wrapper.AddMCPTool("my_tool", tools.MyTool{}), // 注册工具 + myMCPServer.AddMCPTool("my_tool", &tools.MyTool{}), // 注册工具 // 根据需要添加更多工具 - ) + )) } ``` @@ -188,8 +156,21 @@ func init() { 您的 MCP 服务器必须使用支持 Go 1.24 WebAssembly 编译功能的特定版本的 wasm-go SDK: ```bash -# 添加具有特定版本标签的必需依赖项 -go get github.com/alibaba/higress/plugins/wasm-go@wasm-go-1.24 +# 添加必需的依赖项 +go get github.com/alibaba/higress/plugins/wasm-go +``` + +确保您的 go.mod 文件指定 Go 1.24: + +``` +module my-mcp-server + +go 1.24 + +require ( + github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250324133957-dab499f6ade6 + // 其他依赖项 +) ``` ## 构建 WASM 二进制文件 @@ -239,6 +220,8 @@ import ( "testing" ) +// TestMyToolInputSchema 测试 MyTool 的 InputSchema 方法 +// 以验证 JSON schema 配置是否正确。 func TestMyToolInputSchema(t *testing.T) { myTool := MyTool{} schema := myTool.InputSchema() @@ -254,3 +237,4 @@ func TestMyToolInputSchema(t *testing.T) { t.Error("InputSchema 返回了空 schema") } } +``` diff --git a/plugins/wasm-go/mcp-servers/quark-search/server/server.go b/plugins/wasm-go/mcp-servers/amap-tools/config/config.go similarity index 55% rename from plugins/wasm-go/mcp-servers/quark-search/server/server.go rename to plugins/wasm-go/mcp-servers/amap-tools/config/config.go index d115b7725..f4ab858df 100644 --- a/plugins/wasm-go/mcp-servers/quark-search/server/server.go +++ b/plugins/wasm-go/mcp-servers/amap-tools/config/config.go @@ -12,30 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package server +package config -import ( - "encoding/json" - "errors" - - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" -) - -type QuarkMCPServer struct { +// AmapServerConfig defines the configuration structure for the Amap MCP server +type AmapServerConfig 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) + // Add other configuration fields as needed } diff --git a/plugins/wasm-go/mcp-servers/amap-tools/go.mod b/plugins/wasm-go/mcp-servers/amap-tools/go.mod index 15a243f0c..269c2a2fa 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/go.mod +++ b/plugins/wasm-go/mcp-servers/amap-tools/go.mod @@ -4,7 +4,10 @@ go 1.24 toolchain go1.24.1 -require github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250324133957-dab499f6ade6 +require ( + github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250329122411-e07be585ff55 + github.com/tidwall/gjson v1.17.3 +) require ( github.com/bahlo/generic-list-go v0.2.0 // indirect @@ -13,7 +16,6 @@ require ( 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/gjson v1.17.3 // 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 diff --git a/plugins/wasm-go/mcp-servers/amap-tools/go.sum b/plugins/wasm-go/mcp-servers/amap-tools/go.sum index b983448fa..feaa28469 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/go.sum +++ b/plugins/wasm-go/mcp-servers/amap-tools/go.sum @@ -1,5 +1,5 @@ -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/alibaba/higress/plugins/wasm-go v1.4.4-0.20250329122411-e07be585ff55 h1:yGPhs3VhC4Mj4SmbaLcKSZ1sV8wRW/eGf/13P0c2+S8= +github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250329122411-e07be585ff55/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= diff --git a/plugins/wasm-go/mcp-servers/amap-tools/main.go b/plugins/wasm-go/mcp-servers/amap-tools/main.go index e76acfc6c..10eec8b6c 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/main.go +++ b/plugins/wasm-go/mcp-servers/amap-tools/main.go @@ -1,32 +1,42 @@ -// main.go +// 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 ( - "amap-test/server" - "amap-test/tools" + "amap-tools/tools" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" ) func main() {} func init() { - wrapper.SetCtx( - "amap-test", // Server name - wrapper.ParseRawConfig(server.ParseFromConfig), - // wrapper.AddMCPTool("my_tool", tools.MyTool{}), // Register tools - // Add more tools as needed - wrapper.AddMCPTool("maps_bicycling", tools.BicyclingRequest{}), - wrapper.AddMCPTool("maps_geo", tools.GeoRequest{}), - wrapper.AddMCPTool("maps_direction_transit_integrated", tools.TransitIntegratedRequest{}), - wrapper.AddMCPTool("maps_ip_location", tools.IPLocationRequest{}), - wrapper.AddMCPTool("maps_weather", tools.WeatherRequest{}), - wrapper.AddMCPTool("maps_direction_driving", tools.DrivingRequest{}), - wrapper.AddMCPTool("maps_around_search", tools.AroundSearchRequest{}), - wrapper.AddMCPTool("maps_search_detail", tools.SearchDetailRequest{}), - wrapper.AddMCPTool("maps_regeocode", tools.ReGeocodeRequest{}), - wrapper.AddMCPTool("maps_text_search", tools.TextSearchRequest{}), - wrapper.AddMCPTool("maps_distance", tools.DistanceRequest{}), - wrapper.AddMCPTool("maps_direction_walking", tools.WalkingRequest{}), - ) -} \ No newline at end of file + amapServer := &server.MCPServer{} + server.Load(server.AddMCPServer( + "amap-tools", + amapServer.AddMCPTool("maps_geo", &tools.GeoRequest{}). + AddMCPTool("maps_bicycling", &tools.BicyclingRequest{}). + AddMCPTool("maps_direction_transit_integrated", &tools.TransitIntegratedRequest{}). + AddMCPTool("maps_ip_location", &tools.IPLocationRequest{}). + AddMCPTool("maps_weather", &tools.WeatherRequest{}). + AddMCPTool("maps_direction_driving", &tools.DrivingRequest{}). + AddMCPTool("maps_around_search", &tools.AroundSearchRequest{}). + AddMCPTool("maps_search_detail", &tools.SearchDetailRequest{}). + AddMCPTool("maps_regeocode", &tools.ReGeocodeRequest{}). + AddMCPTool("maps_text_search", &tools.TextSearchRequest{}). + AddMCPTool("maps_distance", &tools.DistanceRequest{}). + AddMCPTool("maps_direction_walking", &tools.WalkingRequest{}), + )) +} diff --git a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_around_search.go b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_around_search.go index 161140a81..69b756189 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_around_search.go +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_around_search.go @@ -1,17 +1,34 @@ +// 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" + "errors" "fmt" "net/http" "net/url" - "amap-tools/server" + "amap-tools/config" - "github.com/alibaba/higress/plugins/wasm-go/pkg/log" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" ) +var _ server.Tool = AroundSearchRequest{} + type AroundSearchRequest struct { Location string `json:"location" jsonschema_description:"中心点经度纬度"` Radius string `json:"radius" jsonschema_description:"搜索半径"` @@ -23,42 +40,33 @@ func (t AroundSearchRequest) Description() string { } func (t AroundSearchRequest) InputSchema() map[string]any { - return wrapper.ToInputSchema(&AroundSearchRequest{}) + return server.ToInputSchema(&AroundSearchRequest{}) } -func (t AroundSearchRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { +func (t AroundSearchRequest) Create(params []byte) server.Tool { request := &AroundSearchRequest{} json.Unmarshal(params, &request) return request } -func (t AroundSearchRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServer) error { - err := server.ParseFromRequest(ctx, &config) - if err != nil { - log.Errorf("parse config from request failed, err:%s", err) - return err - } - err = config.ConfigHasError() - if err != nil { - return err +func (t AroundSearchRequest) Call(ctx server.HttpContext, s server.Server) error { + serverConfig := &config.AmapServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("amap API-KEY is not configured") } - apiKey := config.ApiKey - if apiKey == "" { - return fmt.Errorf("amap API-KEY is not set") - } - - url := fmt.Sprintf("http://restapi.amap.com/v3/place/around?key=%s&location=%s&radius=%s&keywords=%s&source=ts_mcp", apiKey, url.QueryEscape(t.Location), url.QueryEscape(t.Radius), url.QueryEscape(t.Keywords)) + url := fmt.Sprintf("http://restapi.amap.com/v3/place/around?key=%s&location=%s&radius=%s&keywords=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Location), url.QueryEscape(t.Radius), url.QueryEscape(t.Keywords)) return ctx.RouteCall(http.MethodGet, url, [][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { if statusCode != http.StatusOK { - ctx.OnMCPToolCallError(fmt.Errorf("around search call failed, status: %d", statusCode)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("around search call failed, status: %d", statusCode)) return } var response struct { Status string `json:"status"` Info string `json:"info"` - Pois []struct { + Pois []struct { ID string `json:"id"` Name string `json:"name"` Address string `json:"address"` @@ -67,14 +75,14 @@ func (t AroundSearchRequest) Call(ctx wrapper.HttpContext, config server.AmapMCP } err := json.Unmarshal(responseBody, &response) if err != nil { - ctx.OnMCPToolCallError(fmt.Errorf("failed to parse around search response: %v", err)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("failed to parse around search response: %v", err)) return } if response.Status != "1" { - ctx.OnMCPToolCallError(fmt.Errorf("around search failed: %s", response.Info)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("around search failed: %s", response.Info)) return } - result := fmt.Sprintf(`{"pois": %s}`, string(responseBody)) - ctx.SendMCPToolTextResult(result) + result, _ := json.MarshalIndent(response.Pois, "", " ") + utils.SendMCPToolTextResult(ctx, string(result)) }) -} \ No newline at end of file +} diff --git a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_bicycling.go b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_bicycling.go index 33b072314..cf34eb040 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_bicycling.go +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_bicycling.go @@ -1,17 +1,34 @@ +// 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" + "errors" "fmt" "net/http" "net/url" - "amap-tools/server" + "amap-tools/config" - "github.com/alibaba/higress/plugins/wasm-go/pkg/log" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" ) +var _ server.Tool = BicyclingRequest{} + type BicyclingRequest struct { Origin string `json:"origin" jsonschema_description:"出发点经纬度,坐标格式为:经度,纬度"` Destination string `json:"destination" jsonschema_description:"目的地经纬度,坐标格式为:经度,纬度"` @@ -22,66 +39,57 @@ func (t BicyclingRequest) Description() string { } func (t BicyclingRequest) InputSchema() map[string]any { - return wrapper.ToInputSchema(&BicyclingRequest{}) + return server.ToInputSchema(&BicyclingRequest{}) } -func (t BicyclingRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { +func (t BicyclingRequest) Create(params []byte) server.Tool { request := &BicyclingRequest{} json.Unmarshal(params, &request) return request } -func (t BicyclingRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServer) error { - err := server.ParseFromRequest(ctx, &config) - if err != nil { - log.Errorf("parse config from request failed, err:%s", err) - return err - } - err = config.ConfigHasError() - if err != nil { - return err +func (t BicyclingRequest) Call(ctx server.HttpContext, s server.Server) error { + serverConfig := &config.AmapServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("amap API-KEY is not configured") } - apiKey := config.ApiKey - if apiKey == "" { - return fmt.Errorf("amap API-KEY is not set") - } - - url := fmt.Sprintf("http://restapi.amap.com/v4/direction/bicycling?key=%s&origin=%s&destination=%s&source=ts_mcp", apiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination)) + url := fmt.Sprintf("http://restapi.amap.com/v4/direction/bicycling?key=%s&origin=%s&destination=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination)) return ctx.RouteCall(http.MethodGet, url, [][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { if statusCode != http.StatusOK { - ctx.OnMCPToolCallError(fmt.Errorf("bicycling call failed, status: %d", statusCode)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("bicycling call failed, status: %d", statusCode)) return } var response struct { Errcode int `json:"errcode"` - Data struct { - Origin string `json:"origin"` + Data struct { + Origin string `json:"origin"` Destination string `json:"destination"` - Paths []struct { + Paths []struct { Distance string `json:"distance"` Duration string `json:"duration"` - Steps []struct { + Steps []struct { Instruction string `json:"instruction"` - Road string `json:"road"` - Distance string `json:"distance"` + Road string `json:"road"` + Distance string `json:"distance"` Orientation string `json:"orientation"` - Duration string `json:"duration"` + Duration string `json:"duration"` } `json:"steps"` } `json:"paths"` } `json:"data"` } err := json.Unmarshal(responseBody, &response) if err != nil { - ctx.OnMCPToolCallError(fmt.Errorf("failed to parse bicycling response: %v", err)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("failed to parse bicycling response: %v", err)) return } if response.Errcode != 0 { - ctx.OnMCPToolCallError(fmt.Errorf("bicycling failed: %v", response)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("bicycling failed: %v", response)) return } - result := fmt.Sprintf(`{"origin": "%s", "destination": "%s", "paths": %s}`, response.Data.Origin, response.Data.Destination, string(responseBody)) - ctx.SendMCPToolTextResult(result) + result, _ := json.MarshalIndent(response.Data.Paths, "", " ") + utils.SendMCPToolTextResult(ctx, string(result)) }) -} \ No newline at end of file +} diff --git a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_driving.go b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_driving.go index c29b72f18..f67e873bf 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_driving.go +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_driving.go @@ -1,17 +1,34 @@ +// 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" + "errors" "fmt" "net/http" "net/url" - "amap-tools/server" + "amap-tools/config" - "github.com/alibaba/higress/plugins/wasm-go/pkg/log" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" ) +var _ server.Tool = DrivingRequest{} + type DrivingRequest struct { Origin string `json:"origin" jsonschema_description:"出发点经度,纬度,坐标格式为:经度,纬度"` Destination string `json:"destination" jsonschema_description:"目的地经纬度,坐标格式为:经度,纬度"` @@ -22,68 +39,59 @@ func (t DrivingRequest) Description() string { } func (t DrivingRequest) InputSchema() map[string]any { - return wrapper.ToInputSchema(&DrivingRequest{}) -} + return server.ToInputSchema(&DrivingRequest{}) -func (t DrivingRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { +} +func (t DrivingRequest) Create(params []byte) server.Tool { request := &DrivingRequest{} json.Unmarshal(params, &request) return request } -func (t DrivingRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServer) error { - err := server.ParseFromRequest(ctx, &config) - if err != nil { - log.Errorf("parse config from request failed, err:%s", err) - return err - } - err = config.ConfigHasError() - if err != nil { - return err +func (t DrivingRequest) Call(ctx server.HttpContext, s server.Server) error { + serverConfig := &config.AmapServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("amap API-KEY is not configured") } - apiKey := config.ApiKey - if apiKey == "" { - return fmt.Errorf("amap API-KEY is not set") - } - - url := fmt.Sprintf("http://restapi.amap.com/v3/direction/driving?key=%s&origin=%s&destination=%s&source=ts_mcp", apiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination)) + url := fmt.Sprintf("http://restapi.amap.com/v3/direction/driving?key=%s&origin=%s&destination=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination)) return ctx.RouteCall(http.MethodGet, url, [][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { if statusCode != http.StatusOK { - ctx.OnMCPToolCallError(fmt.Errorf("driving call failed, status: %d", statusCode)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("driving call failed, status: %d", statusCode)) return } var response struct { Status string `json:"status"` Info string `json:"info"` - Route struct { - Origin string `json:"origin"` + Route struct { + Origin string `json:"origin"` Destination string `json:"destination"` - Paths []struct { - Path string `json:"path"` + Paths []struct { + Path string `json:"path"` Distance string `json:"distance"` Duration string `json:"duration"` - Steps []struct { + Steps []struct { Instruction string `json:"instruction"` - Road string `json:"road"` - Distance string `json:"distance"` + Road string `json:"road"` + Distance string `json:"distance"` Orientation string `json:"orientation"` - Duration string `json:"duration"` + Duration string `json:"duration"` } `json:"steps"` } `json:"paths"` } `json:"route"` } err := json.Unmarshal(responseBody, &response) if err != nil { - ctx.OnMCPToolCallError(fmt.Errorf("failed to parse driving response: %v", err)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("failed to parse driving response: %v", err)) return } if response.Status != "1" { - ctx.OnMCPToolCallError(fmt.Errorf("driving failed: %s", response.Info)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("driving failed: %s", response.Info)) return } - result := fmt.Sprintf(`{"origin": "%s", "destination": "%s", "paths": %s}`, response.Route.Origin, response.Route.Destination, string(responseBody)) - ctx.SendMCPToolTextResult(result) + result, _ := json.MarshalIndent(response.Route.Paths, "", " ") + utils.SendMCPToolTextResult(ctx, string(result)) }) -} \ No newline at end of file +} diff --git a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_transit_integrated.go b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_transit_integrated.go index 9019a9ac9..1e3e25eab 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_transit_integrated.go +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_transit_integrated.go @@ -1,17 +1,35 @@ +// 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" + "errors" "fmt" "net/http" "net/url" - "amap-tools/server" + "amap-tools/config" - "github.com/alibaba/higress/plugins/wasm-go/pkg/log" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" + "github.com/tidwall/gjson" ) +var _ server.Tool = TransitIntegratedRequest{} + type TransitIntegratedRequest struct { Origin string `json:"origin" jsonschema_description:"出发点经纬度,坐标格式为:经度,纬度"` Destination string `json:"destination" jsonschema_description:"目的地经纬度,坐标格式为:经度,纬度"` @@ -24,65 +42,56 @@ func (t TransitIntegratedRequest) Description() string { } func (t TransitIntegratedRequest) InputSchema() map[string]any { - return wrapper.ToInputSchema(&TransitIntegratedRequest{}) + return server.ToInputSchema(&TransitIntegratedRequest{}) } -func (t TransitIntegratedRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { +func (t TransitIntegratedRequest) Create(params []byte) server.Tool { request := &TransitIntegratedRequest{} json.Unmarshal(params, &request) return request } -func (t TransitIntegratedRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServer) error { - err := server.ParseFromRequest(ctx, &config) - if err != nil { - log.Errorf("parse config from request failed, err:%s", err) - return err - } - err = config.ConfigHasError() - if err != nil { - return err +func (t TransitIntegratedRequest) Call(ctx server.HttpContext, s server.Server) error { + serverConfig := &config.AmapServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("amap API-KEY is not configured") } - apiKey := config.ApiKey - if apiKey == "" { - return fmt.Errorf("amap API-KEY is not set") - } - - url := fmt.Sprintf("http://restapi.amap.com/v3/direction/transit/integrated?key=%s&origin=%s&destination=%s&city=%s&cityd=%s&source=ts_mcp", apiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination), url.QueryEscape(t.City), url.QueryEscape(t.Cityd)) + url := fmt.Sprintf("http://restapi.amap.com/v3/direction/transit/integrated?key=%s&origin=%s&destination=%s&city=%s&cityd=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination), url.QueryEscape(t.City), url.QueryEscape(t.Cityd)) return ctx.RouteCall(http.MethodGet, url, [][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { if statusCode != http.StatusOK { - ctx.OnMCPToolCallError(fmt.Errorf("transit integrated call failed, status: %d", statusCode)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("transit integrated call failed, status: %d", statusCode)) return } var response struct { Status string `json:"status"` Info string `json:"info"` - Route struct { - Origin string `json:"origin"` + Route struct { + Origin string `json:"origin"` Destination string `json:"destination"` - Distance string `json:"distance"` - Transits []struct { - Duration string `json:"duration"` + Distance string `json:"distance"` + Transits []struct { + Duration string `json:"duration"` WalkingDistance string `json:"walking_distance"` - Segments []struct { + Segments []struct { Walking struct { - Origin string `json:"origin"` + Origin string `json:"origin"` Destination string `json:"destination"` - Distance string `json:"distance"` - Duration string `json:"duration"` - Steps []struct { - Instruction string `json:"instruction"` - Road string `json:"road"` - Distance string `json:"distance"` - Action string `json:"action"` + Distance string `json:"distance"` + Duration string `json:"duration"` + Steps []struct { + Instruction string `json:"instruction"` + Road string `json:"road"` + Distance string `json:"distance"` + Action string `json:"action"` AssistantAction string `json:"assistant_action"` } `json:"steps"` } `json:"walking"` Bus struct { Buslines []struct { - Name string `json:"name"` + Name string `json:"name"` DepartureStop struct { Name string `json:"name"` } `json:"departure_stop"` @@ -112,14 +121,14 @@ func (t TransitIntegratedRequest) Call(ctx wrapper.HttpContext, config server.Am } err := json.Unmarshal(responseBody, &response) if err != nil { - ctx.OnMCPToolCallError(fmt.Errorf("failed to parse transit integrated response: %v", err)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("failed to parse transit integrated response: %v", err)) return } if response.Status != "1" { - ctx.OnMCPToolCallError(fmt.Errorf("transit integrated failed: %s", response.Info)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("transit integrated failed: %s", response.Info)) return } - result := fmt.Sprintf(`{"origin": "%s", "destination": "%s", "distance": "%s", "transits": %s}`, response.Route.Origin, response.Route.Destination, response.Route.Distance, string(responseBody)) - ctx.SendMCPToolTextResult(result) + result := fmt.Sprintf(`{"origin": "%s", "destination": "%s", "distance": "%s", "transits": %s}`, response.Route.Origin, response.Route.Destination, response.Route.Distance, gjson.GetBytes(responseBody, "route.transits").Raw) + utils.SendMCPToolTextResult(ctx, result) }) -} \ No newline at end of file +} diff --git a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_walking.go b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_walking.go index 9a7e594e0..459cddca6 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_walking.go +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_walking.go @@ -1,17 +1,35 @@ +// 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" + "errors" "fmt" "net/http" "net/url" - "amap-tools/server" + "amap-tools/config" - "github.com/alibaba/higress/plugins/wasm-go/pkg/log" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" + "github.com/tidwall/gjson" ) +var _ server.Tool = WalkingRequest{} + type WalkingRequest struct { Origin string `json:"origin" jsonschema_description:"出发点经度,纬度,坐标格式为:经度,纬度"` Destination string `json:"destination" jsonschema_description:"目的地经纬度,坐标格式为:经度,纬度"` @@ -22,67 +40,58 @@ func (t WalkingRequest) Description() string { } func (t WalkingRequest) InputSchema() map[string]any { - return wrapper.ToInputSchema(&WalkingRequest{}) + return server.ToInputSchema(&WalkingRequest{}) } -func (t WalkingRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { +func (t WalkingRequest) Create(params []byte) server.Tool { request := &WalkingRequest{} json.Unmarshal(params, &request) return request } -func (t WalkingRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServer) error { - err := server.ParseFromRequest(ctx, &config) - if err != nil { - log.Errorf("parse config from request failed, err:%s", err) - return err - } - err = config.ConfigHasError() - if err != nil { - return err +func (t WalkingRequest) Call(ctx server.HttpContext, s server.Server) error { + serverConfig := &config.AmapServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("amap API-KEY is not configured") } - apiKey := config.ApiKey - if apiKey == "" { - return fmt.Errorf("amap API-KEY is not set") - } - - url := fmt.Sprintf("http://restapi.amap.com/v3/direction/walking?key=%s&origin=%s&destination=%s&source=ts_mcp", apiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination)) + url := fmt.Sprintf("http://restapi.amap.com/v3/direction/walking?key=%s&origin=%s&destination=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination)) return ctx.RouteCall(http.MethodGet, url, [][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { if statusCode != http.StatusOK { - ctx.OnMCPToolCallError(fmt.Errorf("walking call failed, status: %d", statusCode)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("walking call failed, status: %d", statusCode)) return } var response struct { Status string `json:"status"` Info string `json:"info"` - Route struct { - Origin string `json:"origin"` + Route struct { + Origin string `json:"origin"` Destination string `json:"destination"` - Paths []struct { + Paths []struct { Distance string `json:"distance"` Duration string `json:"duration"` - Steps []struct { + Steps []struct { Instruction string `json:"instruction"` - Road string `json:"road"` - Distance string `json:"distance"` + Road string `json:"road"` + Distance string `json:"distance"` Orientation string `json:"orientation"` - Duration string `json:"duration"` + Duration string `json:"duration"` } `json:"steps"` } `json:"paths"` } `json:"route"` } err := json.Unmarshal(responseBody, &response) if err != nil { - ctx.OnMCPToolCallError(fmt.Errorf("failed to parse walking response: %v", err)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("failed to parse walking response: %v", err)) return } if response.Status != "1" { - ctx.OnMCPToolCallError(fmt.Errorf("walking failed: %s", response.Info)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("walking failed: %s", response.Info)) return } - result := fmt.Sprintf(`{"origin": "%s", "destination": "%s", "paths": %s}`, response.Route.Origin, response.Route.Destination, string(responseBody)) - ctx.SendMCPToolTextResult(result) + result := fmt.Sprintf(`{"origin": "%s", "destination": "%s", "paths": %s}`, response.Route.Origin, response.Route.Destination, gjson.GetBytes(responseBody, "route.paths").Raw) + utils.SendMCPToolTextResult(ctx, result) }) -} \ No newline at end of file +} diff --git a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_distance.go b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_distance.go index 39ac9671a..4bfedba94 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_distance.go +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_distance.go @@ -1,17 +1,35 @@ +// 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" + "errors" "fmt" "net/http" "net/url" - "amap-tools/server" + "amap-tools/config" - "github.com/alibaba/higress/plugins/wasm-go/pkg/log" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" + "github.com/tidwall/gjson" ) +var _ server.Tool = DistanceRequest{} + type DistanceRequest struct { Origins string `json:"origins" jsonschema_description:"起点经度,纬度,可以传多个坐标,使用分号隔离,比如120,30;120,31,坐标格式为:经度,纬度"` Destination string `json:"destination" jsonschema_description:"终点经度,纬度,坐标格式为:经度,纬度"` @@ -23,41 +41,31 @@ func (t DistanceRequest) Description() string { } func (t DistanceRequest) InputSchema() map[string]any { - return wrapper.ToInputSchema(&DistanceRequest{}) + return server.ToInputSchema(&DistanceRequest{}) } - -func (t DistanceRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { +func (t DistanceRequest) Create(params []byte) server.Tool { request := &DistanceRequest{} json.Unmarshal(params, &request) return request } -func (t DistanceRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServer) error { - err := server.ParseFromRequest(ctx, &config) - if err != nil { - log.Errorf("parse config from request failed, err:%s", err) - return err - } - err = config.ConfigHasError() - if err != nil { - return err +func (t DistanceRequest) Call(ctx server.HttpContext, s server.Server) error { + serverConfig := &config.AmapServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("amap API-KEY is not configured") } - apiKey := config.ApiKey - if apiKey == "" { - return fmt.Errorf("amap API-KEY is not set") - } - - url := fmt.Sprintf("http://restapi.amap.com/v3/distance?key=%s&origins=%s&destination=%s&type=%s&source=ts_mcp", apiKey, url.QueryEscape(t.Origins), url.QueryEscape(t.Destination), url.QueryEscape(t.Type)) + url := fmt.Sprintf("http://restapi.amap.com/v3/distance?key=%s&origins=%s&destination=%s&type=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origins), url.QueryEscape(t.Destination), url.QueryEscape(t.Type)) return ctx.RouteCall(http.MethodGet, url, [][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { if statusCode != http.StatusOK { - ctx.OnMCPToolCallError(fmt.Errorf("distance call failed, status: %d", statusCode)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("distance call failed, status: %d", statusCode)) return } var response struct { - Status string `json:"status"` - Info string `json:"info"` + Status string `json:"status"` + Info string `json:"info"` Results []struct { OriginID string `json:"origin_id"` DestID string `json:"dest_id"` @@ -67,14 +75,14 @@ func (t DistanceRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServ } err := json.Unmarshal(responseBody, &response) if err != nil { - ctx.OnMCPToolCallError(fmt.Errorf("failed to parse distance response: %v", err)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("failed to parse distance response: %v", err)) return } if response.Status != "1" { - ctx.OnMCPToolCallError(fmt.Errorf("distance failed: %s", response.Info)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("distance failed: %s", response.Info)) return } - result := fmt.Sprintf(`{"results": %s}`, string(responseBody)) - ctx.SendMCPToolTextResult(result) + result := fmt.Sprintf(`{"results": %s}`, gjson.GetBytes(responseBody, "results").Raw) + utils.SendMCPToolTextResult(ctx, result) }) -} \ No newline at end of file +} diff --git a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_geo.go b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_geo.go index 33a331b5e..6253d7d58 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_geo.go +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_geo.go @@ -1,17 +1,34 @@ +// 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" + "errors" "fmt" "net/http" "net/url" - "amap-tools/server" + "amap-tools/config" - "github.com/alibaba/higress/plugins/wasm-go/pkg/log" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" ) +var _ server.Tool = GeoRequest{} + type GeoRequest struct { Address string `json:"address" jsonschema_description:"待解析的结构化地址信息"` City string `json:"city" jsonschema_description:"指定查询的城市"` @@ -22,41 +39,33 @@ func (t GeoRequest) Description() string { } func (t GeoRequest) InputSchema() map[string]any { - return wrapper.ToInputSchema(&GeoRequest{}) + return server.ToInputSchema(&GeoRequest{}) } -func (t GeoRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { +func (t GeoRequest) Create(params []byte) server.Tool { request := &GeoRequest{} json.Unmarshal(params, &request) return request } -func (t GeoRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServer) error { - err := server.ParseFromRequest(ctx, &config) - if err != nil { - log.Errorf("parse config from request failed, err:%s", err) - return err - } - err = config.ConfigHasError() - if err != nil { - return err - } - - apiKey := config.ApiKey - if apiKey == "" { - return fmt.Errorf("amap API-KEY is not set") +func (t GeoRequest) Call(ctx server.HttpContext, s server.Server) error { + serverConfig := &config.AmapServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("amap API-KEY is not configured") } + apiKey := serverConfig.ApiKey url := fmt.Sprintf("https://restapi.amap.com/v3/geocode/geo?key=%s&address=%s&city=%s&source=ts_mcp", apiKey, url.QueryEscape(t.Address), url.QueryEscape(t.City)) return ctx.RouteCall(http.MethodGet, url, [][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { if statusCode != http.StatusOK { - ctx.OnMCPToolCallError(fmt.Errorf("geo call failed, status: %d", statusCode)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("geo call failed, status: %d", statusCode)) return } var response struct { - Status string `json:"status"` - Info string `json:"info"` + Status string `json:"status"` + Info string `json:"info"` Geocodes []struct { Country string `json:"country"` Province string `json:"province"` @@ -72,11 +81,11 @@ func (t GeoRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServer) e } err := json.Unmarshal(responseBody, &response) if err != nil { - ctx.OnMCPToolCallError(fmt.Errorf("failed to parse geo response: %v", err)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("failed to parse geo response: %v", err)) return } if response.Status != "1" { - ctx.OnMCPToolCallError(fmt.Errorf("geo failed: %s", response.Info)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("geo failed: %s", response.Info)) return } var results []map[string]string @@ -96,6 +105,6 @@ func (t GeoRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServer) e results = append(results, result) } result, _ := json.Marshal(results) - ctx.SendMCPToolTextResult(string(result)) + utils.SendMCPToolTextResult(ctx, string(result)) }) -} \ No newline at end of file +} diff --git a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_ip_location.go b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_ip_location.go index 7fbc924d7..2d0d87b6d 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_ip_location.go +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_ip_location.go @@ -1,17 +1,34 @@ +// 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" + "errors" "fmt" "net/http" "net/url" - "amap-tools/server" + "amap-tools/config" - "github.com/alibaba/higress/plugins/wasm-go/pkg/log" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" ) +var _ server.Tool = IPLocationRequest{} + type IPLocationRequest struct { IP string `json:"ip" jsonschema_description:"IP地址"` } @@ -21,56 +38,47 @@ func (t IPLocationRequest) Description() string { } func (t IPLocationRequest) InputSchema() map[string]any { - return wrapper.ToInputSchema(&IPLocationRequest{}) + return server.ToInputSchema(&IPLocationRequest{}) } -func (t IPLocationRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { +func (t IPLocationRequest) Create(params []byte) server.Tool { request := &IPLocationRequest{} json.Unmarshal(params, &request) return request } -func (t IPLocationRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServer) error { - err := server.ParseFromRequest(ctx, &config) - if err != nil { - log.Errorf("parse config from request failed, err:%s", err) - return err - } - err = config.ConfigHasError() - if err != nil { - return err +func (t IPLocationRequest) Call(ctx server.HttpContext, s server.Server) error { + serverConfig := &config.AmapServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("amap API-KEY is not configured") } - apiKey := config.ApiKey - if apiKey == "" { - return fmt.Errorf("amap API-KEY is not set") - } - - url := fmt.Sprintf("https://restapi.amap.com/v3/ip?ip=%s&key=%s&source=ts_mcp", url.QueryEscape(t.IP), apiKey) + url := fmt.Sprintf("https://restapi.amap.com/v3/ip?ip=%s&key=%s&source=ts_mcp", url.QueryEscape(t.IP), serverConfig.ApiKey) return ctx.RouteCall(http.MethodGet, url, [][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { if statusCode != http.StatusOK { - ctx.OnMCPToolCallError(fmt.Errorf("ip location call failed, status: %d", statusCode)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("ip location call failed, status: %d", statusCode)) return } var response struct { - Status string `json:"status"` - Info string `json:"info"` - Province string `json:"province"` - City string `json:"city"` - Adcode string `json:"adcode"` + Status string `json:"status"` + Info string `json:"info"` + Province string `json:"province"` + City string `json:"city"` + Adcode string `json:"adcode"` Rectangle string `json:"rectangle"` } err := json.Unmarshal(responseBody, &response) if err != nil { - ctx.OnMCPToolCallError(fmt.Errorf("failed to parse ip location response: %v", err)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("failed to parse ip location response: %v", err)) return } if response.Status != "1" { - ctx.OnMCPToolCallError(fmt.Errorf("ip location failed: %s", response.Info)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("ip location failed: %s", response.Info)) return } result := fmt.Sprintf(`{"province": "%s", "city": "%s", "adcode": "%s", "rectangle": "%s"}`, response.Province, response.City, response.Adcode, response.Rectangle) - ctx.SendMCPToolTextResult(result) + utils.SendMCPToolTextResult(ctx, result) }) -} \ No newline at end of file +} diff --git a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_regeocode.go b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_regeocode.go index 028d76085..aa3958739 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_regeocode.go +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_regeocode.go @@ -1,17 +1,34 @@ +// 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" + "errors" "fmt" "net/http" "net/url" - "amap-tools/server" + "amap-tools/config" - "github.com/alibaba/higress/plugins/wasm-go/pkg/log" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" ) +var _ server.Tool = ReGeocodeRequest{} + type ReGeocodeRequest struct { Location string `json:"location" jsonschema_description:"经纬度"` } @@ -21,41 +38,32 @@ func (t ReGeocodeRequest) Description() string { } func (t ReGeocodeRequest) InputSchema() map[string]any { - return wrapper.ToInputSchema(&ReGeocodeRequest{}) + return server.ToInputSchema(&ReGeocodeRequest{}) } -func (t ReGeocodeRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { +func (t ReGeocodeRequest) Create(params []byte) server.Tool { request := &ReGeocodeRequest{} json.Unmarshal(params, &request) return request } -func (t ReGeocodeRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServer) error { - err := server.ParseFromRequest(ctx, &config) - if err != nil { - log.Errorf("parse config from request failed, err:%s", err) - return err - } - err = config.ConfigHasError() - if err != nil { - return err +func (t ReGeocodeRequest) Call(ctx server.HttpContext, s server.Server) error { + serverConfig := &config.AmapServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("amap API-KEY is not configured") } - apiKey := config.ApiKey - if apiKey == "" { - return fmt.Errorf("amap API-KEY is not set") - } - - url := fmt.Sprintf("http://restapi.amap.com/v3/geocode/regeo?location=%s&key=%s&source=ts_mcp", url.QueryEscape(t.Location), apiKey) + url := fmt.Sprintf("http://restapi.amap.com/v3/geocode/regeo?location=%s&key=%s&source=ts_mcp", url.QueryEscape(t.Location), serverConfig.ApiKey) return ctx.RouteCall(http.MethodGet, url, [][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { if statusCode != http.StatusOK { - ctx.OnMCPToolCallError(fmt.Errorf("regeocode call failed, status: %d", statusCode)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("regeocode call failed, status: %d", statusCode)) return } var response struct { - Status string `json:"status"` - Info string `json:"info"` + Status string `json:"status"` + Info string `json:"info"` Regeocode struct { AddressComponent struct { Province string `json:"province"` @@ -66,14 +74,14 @@ func (t ReGeocodeRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPSer } err := json.Unmarshal(responseBody, &response) if err != nil { - ctx.OnMCPToolCallError(fmt.Errorf("failed to parse regeocode response: %v", err)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("failed to parse regeocode response: %v", err)) return } if response.Status != "1" { - ctx.OnMCPToolCallError(fmt.Errorf("regeocode failed: %s", response.Info)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("regeocode failed: %s", response.Info)) return } result := fmt.Sprintf(`{"province": "%s", "city": "%s", "district": "%s"}`, response.Regeocode.AddressComponent.Province, response.Regeocode.AddressComponent.City, response.Regeocode.AddressComponent.District) - ctx.SendMCPToolTextResult(result) + utils.SendMCPToolTextResult(ctx, result) }) -} \ No newline at end of file +} diff --git a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_search_detail.go b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_search_detail.go index f5b14ec13..1da298031 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_search_detail.go +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_search_detail.go @@ -1,17 +1,34 @@ +// 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" + "errors" "fmt" "net/http" "net/url" - "amap-tools/server" + "amap-tools/config" - "github.com/alibaba/higress/plugins/wasm-go/pkg/log" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" ) +var _ server.Tool = SearchDetailRequest{} + type SearchDetailRequest struct { ID string `json:"id" jsonschema_description:"关键词搜或者周边搜获取到的POI ID"` } @@ -21,64 +38,55 @@ func (t SearchDetailRequest) Description() string { } func (t SearchDetailRequest) InputSchema() map[string]any { - return wrapper.ToInputSchema(&SearchDetailRequest{}) + return server.ToInputSchema(&SearchDetailRequest{}) } -func (t SearchDetailRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { +func (t SearchDetailRequest) Create(params []byte) server.Tool { request := &SearchDetailRequest{} json.Unmarshal(params, &request) return request } -func (t SearchDetailRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServer) error { - err := server.ParseFromRequest(ctx, &config) - if err != nil { - log.Errorf("parse config from request failed, err:%s", err) - return err - } - err = config.ConfigHasError() - if err != nil { - return err +func (t SearchDetailRequest) Call(ctx server.HttpContext, s server.Server) error { + serverConfig := &config.AmapServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("amap API-KEY is not configured") } - apiKey := config.ApiKey - if apiKey == "" { - return fmt.Errorf("amap API-KEY is not set") - } - - url := fmt.Sprintf("http://restapi.amap.com/v3/place/detail?id=%s&key=%s&source=ts_mcp", url.QueryEscape(t.ID), apiKey) + url := fmt.Sprintf("http://restapi.amap.com/v3/place/detail?id=%s&key=%s&source=ts_mcp", url.QueryEscape(t.ID), serverConfig.ApiKey) return ctx.RouteCall(http.MethodGet, url, [][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { if statusCode != http.StatusOK { - ctx.OnMCPToolCallError(fmt.Errorf("search detail call failed, status: %d", statusCode)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("search detail call failed, status: %d", statusCode)) return } var response struct { Status string `json:"status"` Info string `json:"info"` - Pois []struct { - ID string `json:"id"` - Name string `json:"name"` - Location string `json:"location"` - Address string `json:"address"` - BusinessArea string `json:"business_area"` - Cityname string `json:"cityname"` - Type string `json:"type"` - Alias string `json:"alias"` - BizExt map[string]string `json:"biz_ext"` + Pois []struct { + ID string `json:"id"` + Name string `json:"name"` + Location string `json:"location"` + Address string `json:"address"` + BusinessArea string `json:"business_area"` + Cityname string `json:"cityname"` + Type string `json:"type"` + Alias string `json:"alias"` + BizExt map[string]string `json:"biz_ext"` } `json:"pois"` } err := json.Unmarshal(responseBody, &response) if err != nil { - ctx.OnMCPToolCallError(fmt.Errorf("failed to parse search detail response: %v", err)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("failed to parse search detail response: %v", err)) return } if response.Status != "1" { - ctx.OnMCPToolCallError(fmt.Errorf("search detail failed: %s", response.Info)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("search detail failed: %s", response.Info)) return } poi := response.Pois[0] - result := fmt.Sprintf(`{"id": "%s", "name": "%s", "location": "%s", "address": "%s", "business_area": "%s", "city": "%s", "type": "%s", "alias": "%s", "biz_ext": %s}`, poi.ID, poi.Name, poi.Location, poi.Address, poi.BusinessArea, poi.Cityname, poi.Type, poi.Alias, string(responseBody)) - ctx.SendMCPToolTextResult(result) + result, _ := json.MarshalIndent(poi, "", " ") + utils.SendMCPToolTextResult(ctx, string(result)) }) -} \ No newline at end of file +} diff --git a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_text_search.go b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_text_search.go index 6a545d599..73b4fab3e 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_text_search.go +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_text_search.go @@ -1,21 +1,39 @@ +// 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" + "errors" "fmt" "net/http" "net/url" - "amap-tools/server" + "amap-tools/config" - "github.com/alibaba/higress/plugins/wasm-go/pkg/log" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" + "github.com/tidwall/gjson" ) +var _ server.Tool = TextSearchRequest{} + type TextSearchRequest struct { - Keywords string `json:"keywords" jsonschema_description:"搜索关键词"` - City string `json:"city" jsonschema_description:"查询城市"` - Citylimit string `json:"citylimit" jsonschema_description:"是否强制限制在设置的城市内搜索,默认值为false"` + Keywords string `json:"keywords" jsonschema_description:"搜索关键词"` + City string `json:"city" jsonschema_description:"查询城市"` + Citylimit string `json:"citylimit" jsonschema_description:"是否强制限制在设置的城市内搜索,默认值为false"` } func (t TextSearchRequest) Description() string { @@ -23,41 +41,32 @@ func (t TextSearchRequest) Description() string { } func (t TextSearchRequest) InputSchema() map[string]any { - return wrapper.ToInputSchema(&TextSearchRequest{}) + return server.ToInputSchema(&TextSearchRequest{}) } -func (t TextSearchRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { +func (t TextSearchRequest) Create(params []byte) server.Tool { request := &TextSearchRequest{} json.Unmarshal(params, &request) return request } -func (t TextSearchRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServer) error { - err := server.ParseFromRequest(ctx, &config) - if err != nil { - log.Errorf("parse config from request failed, err:%s", err) - return err - } - err = config.ConfigHasError() - if err != nil { - return err +func (t TextSearchRequest) Call(ctx server.HttpContext, s server.Server) error { + serverConfig := &config.AmapServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("amap API-KEY is not configured") } - apiKey := config.ApiKey - if apiKey == "" { - return fmt.Errorf("amap API-KEY is not set") - } - - url := fmt.Sprintf("http://restapi.amap.com/v3/place/text?key=%s&keywords=%s&city=%s&citylimit=%s&source=ts_mcp", apiKey, url.QueryEscape(t.Keywords), url.QueryEscape(t.City), url.QueryEscape(t.Citylimit)) + url := fmt.Sprintf("http://restapi.amap.com/v3/place/text?key=%s&keywords=%s&city=%s&citylimit=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Keywords), url.QueryEscape(t.City), url.QueryEscape(t.Citylimit)) return ctx.RouteCall(http.MethodGet, url, [][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { if statusCode != http.StatusOK { - ctx.OnMCPToolCallError(fmt.Errorf("text search call failed, status: %d", statusCode)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("text search call failed, status: %d", statusCode)) return } var response struct { - Status string `json:"status"` - Info string `json:"info"` + Status string `json:"status"` + Info string `json:"info"` Suggestion struct { Keywords []string `json:"keywords"` Cities []struct { @@ -73,18 +82,18 @@ func (t TextSearchRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPSe } err := json.Unmarshal(responseBody, &response) if err != nil { - ctx.OnMCPToolCallError(fmt.Errorf("failed to parse text search response: %v", err)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("failed to parse text search response: %v", err)) return } if response.Status != "1" { - ctx.OnMCPToolCallError(fmt.Errorf("text search failed: %s", response.Info)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("text search failed: %s", response.Info)) return } var cities []string for _, city := range response.Suggestion.Cities { cities = append(cities, city.Name) } - result := fmt.Sprintf(`{"suggestion": {"keywords": %s, "cities": %s}, "pois": %s}`, string(responseBody), string(responseBody), string(responseBody)) - ctx.SendMCPToolTextResult(result) + result := fmt.Sprintf(`{"suggestion": {"keywords": %s, "cities": %s}, "pois": %s}`, string(responseBody), string(responseBody), gjson.GetBytes(responseBody, "pois").Raw) + utils.SendMCPToolTextResult(ctx, result) }) -} \ No newline at end of file +} diff --git a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_weather.go b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_weather.go index 93229c115..01c9c92d6 100644 --- a/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_weather.go +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_weather.go @@ -1,17 +1,34 @@ +// 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" + "errors" "fmt" "net/http" "net/url" - "amap-tools/server" + "amap-tools/config" - "github.com/alibaba/higress/plugins/wasm-go/pkg/log" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" ) +var _ server.Tool = WeatherRequest{} + type WeatherRequest struct { City string `json:"city" jsonschema_description:"城市名称或者adcode"` } @@ -21,69 +38,60 @@ func (t WeatherRequest) Description() string { } func (t WeatherRequest) InputSchema() map[string]any { - return wrapper.ToInputSchema(&WeatherRequest{}) + return server.ToInputSchema(&WeatherRequest{}) } -func (t WeatherRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { +func (t WeatherRequest) Create(params []byte) server.Tool { request := &WeatherRequest{} json.Unmarshal(params, &request) return request } -func (t WeatherRequest) Call(ctx wrapper.HttpContext, config server.AmapMCPServer) error { - err := server.ParseFromRequest(ctx, &config) - if err != nil { - log.Errorf("parse config from request failed, err:%s", err) - return err - } - err = config.ConfigHasError() - if err != nil { - return err +func (t WeatherRequest) Call(ctx server.HttpContext, s server.Server) error { + serverConfig := &config.AmapServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("amap API-KEY is not configured") } - apiKey := config.ApiKey - if apiKey == "" { - return fmt.Errorf("amap API-KEY is not set") - } - - url := fmt.Sprintf("http://restapi.amap.com/v3/weather/weatherInfo?city=%s&key=%s&source=ts_mcp&extensions=all", url.QueryEscape(t.City), apiKey) + url := fmt.Sprintf("http://restapi.amap.com/v3/weather/weatherInfo?city=%s&key=%s&source=ts_mcp&extensions=all", url.QueryEscape(t.City), serverConfig.ApiKey) return ctx.RouteCall(http.MethodGet, url, [][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { if statusCode != http.StatusOK { - ctx.OnMCPToolCallError(fmt.Errorf("weather call failed, status: %d", statusCode)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("weather call failed, status: %d", statusCode)) return } var response struct { - Status string `json:"status"` - Info string `json:"info"` + Status string `json:"status"` + Info string `json:"info"` Forecasts []struct { - City string `json:"city"` + City string `json:"city"` Casts []struct { - Date string `json:"date"` - Week string `json:"week"` - DayWeather string `json:"dayweather"` + Date string `json:"date"` + Week string `json:"week"` + DayWeather string `json:"dayweather"` NightWeather string `json:"nightweather"` - DayTemp string `json:"daytemp"` - NightTemp string `json:"nighttemp"` - DayWind string `json:"daywind"` - NightWind string `json:"nightwind"` - DayPower string `json:"daypower"` - NightPower string `json:"nightpower"` - Humidity string `json:"humidity"` + DayTemp string `json:"daytemp"` + NightTemp string `json:"nighttemp"` + DayWind string `json:"daywind"` + NightWind string `json:"nightwind"` + DayPower string `json:"daypower"` + NightPower string `json:"nightpower"` + Humidity string `json:"humidity"` } `json:"casts"` } `json:"forecasts"` } err := json.Unmarshal(responseBody, &response) if err != nil { - ctx.OnMCPToolCallError(fmt.Errorf("failed to parse weather response: %v", err)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("failed to parse weather response: %v", err)) return } if response.Status != "1" { - ctx.OnMCPToolCallError(fmt.Errorf("weather failed: %s", response.Info)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("weather failed: %s", response.Info)) return } forecasts := response.Forecasts[0] - result := fmt.Sprintf(`{"city": "%s", "forecasts": %s}`, forecasts.City, string(responseBody)) - ctx.SendMCPToolTextResult(result) + result, _ := json.MarshalIndent(forecasts, "", " ") + utils.SendMCPToolTextResult(ctx, string(result)) }) -} \ No newline at end of file +} diff --git a/plugins/wasm-go/mcp-servers/quark-search/config/config.go b/plugins/wasm-go/mcp-servers/quark-search/config/config.go new file mode 100644 index 000000000..2333aeaaf --- /dev/null +++ b/plugins/wasm-go/mcp-servers/quark-search/config/config.go @@ -0,0 +1,19 @@ +// 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 config + +type QuarkServerConfig struct { + ApiKey string `json:"apiKey"` +} diff --git a/plugins/wasm-go/mcp-servers/quark-search/go.mod b/plugins/wasm-go/mcp-servers/quark-search/go.mod index c19453e4a..8ff60fe30 100644 --- a/plugins/wasm-go/mcp-servers/quark-search/go.mod +++ b/plugins/wasm-go/mcp-servers/quark-search/go.mod @@ -5,7 +5,7 @@ go 1.24 toolchain go1.24.1 require ( - github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250324133957-dab499f6ade6 + github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250329122411-e07be585ff55 github.com/tidwall/gjson v1.17.3 ) diff --git a/plugins/wasm-go/mcp-servers/quark-search/go.sum b/plugins/wasm-go/mcp-servers/quark-search/go.sum index b983448fa..feaa28469 100644 --- a/plugins/wasm-go/mcp-servers/quark-search/go.sum +++ b/plugins/wasm-go/mcp-servers/quark-search/go.sum @@ -1,5 +1,5 @@ -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/alibaba/higress/plugins/wasm-go v1.4.4-0.20250329122411-e07be585ff55 h1:yGPhs3VhC4Mj4SmbaLcKSZ1sV8wRW/eGf/13P0c2+S8= +github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250329122411-e07be585ff55/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= diff --git a/plugins/wasm-go/mcp-servers/quark-search/main.go b/plugins/wasm-go/mcp-servers/quark-search/main.go index 63ee9e6dd..439d9a608 100644 --- a/plugins/wasm-go/mcp-servers/quark-search/main.go +++ b/plugins/wasm-go/mcp-servers/quark-search/main.go @@ -15,18 +15,16 @@ package main import ( - "quark-search/server" "quark-search/tools" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" ) func main() {} func init() { - wrapper.SetCtx( - "quark-mcp-server", - wrapper.ParseRawConfig(server.ParseFromConfig), - wrapper.AddMCPTool("web_search", tools.WebSearch{}), - ) + quarkSearchServer := &server.MCPServer{} + server.Load(server.AddMCPServer( + "quark-search", + quarkSearchServer.AddMCPTool("web_search", &tools.WebSearch{}))) } diff --git a/plugins/wasm-go/mcp-servers/quark-search/tools/web_search.go b/plugins/wasm-go/mcp-servers/quark-search/tools/web_search.go index 770487e41..05370e581 100644 --- a/plugins/wasm-go/mcp-servers/quark-search/tools/web_search.go +++ b/plugins/wasm-go/mcp-servers/quark-search/tools/web_search.go @@ -16,18 +16,21 @@ package tools import ( "encoding/json" + "errors" "fmt" "net/http" "net/url" "strings" - "quark-search/server" + "quark-search/config" - "github.com/alibaba/higress/plugins/wasm-go/pkg/log" - "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server" + "github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils" "github.com/tidwall/gjson" ) +var _ server.Tool = WebSearch{} + type SearchResult struct { Title string Link string @@ -70,12 +73,12 @@ Because Quark search performs poorly for English searches, please use Chinese fo // 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{}) + return server.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] { +func (t WebSearch) Create(params []byte) server.Tool { webSearch := &WebSearch{ ContentMode: "summary", Number: 5, @@ -88,20 +91,17 @@ func (t WebSearch) Create(params []byte) wrapper.MCPTool[server.QuarkMCPServer] // 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 +func (t WebSearch) Call(ctx server.HttpContext, s server.Server) error { + serverConfig := &config.QuarkServerConfig{} + s.GetConfig(serverConfig) + if serverConfig.ApiKey == "" { + return errors.New("Quark search API key not configured") } 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) { + {"X-API-Key", serverConfig.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)) + utils.OnMCPToolCallError(ctx, fmt.Errorf("quark search call failed, status: %d", statusCode)) return } jsonObj := gjson.ParseBytes(responseBody) @@ -125,6 +125,6 @@ func (t WebSearch) Call(ctx wrapper.HttpContext, config server.QuarkMCPServer) e results = append(results, result.Format()) } } - ctx.SendMCPToolTextResult(fmt.Sprintf("# Search Results\n\n%s", strings.Join(results, "\n\n"))) + utils.SendMCPToolTextResult(ctx, fmt.Sprintf("# Search Results\n\n%s", strings.Join(results, "\n\n"))) }) }