diff --git a/plugins/wasm-go/mcp-servers/amap-tools/go.mod b/plugins/wasm-go/mcp-servers/amap-tools/go.mod new file mode 100644 index 00000000..15a243f0 --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/go.mod @@ -0,0 +1,23 @@ +module amap-tools + +go 1.24 + +toolchain go1.24.1 + +require github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250324133957-dab499f6ade6 + +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/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 + 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 +) diff --git a/plugins/wasm-go/mcp-servers/amap-tools/go.sum b/plugins/wasm-go/mcp-servers/amap-tools/go.sum new file mode 100644 index 00000000..b983448f --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/go.sum @@ -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= diff --git a/plugins/wasm-go/mcp-servers/amap-tools/main.go b/plugins/wasm-go/mcp-servers/amap-tools/main.go new file mode 100644 index 00000000..e76acfc6 --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/main.go @@ -0,0 +1,32 @@ +// main.go +package main + +import ( + "amap-test/server" + "amap-test/tools" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +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 diff --git a/plugins/wasm-go/mcp-servers/amap-tools/server/server.go b/plugins/wasm-go/mcp-servers/amap-tools/server/server.go new file mode 100644 index 00000000..db36a572 --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/server/server.go @@ -0,0 +1,33 @@ +// server/server.go +package server + +import ( + "encoding/json" + "errors" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +// Define your server configuration structure +type AmapMCPServer struct { + ApiKey string `json:"apiKey"` + // Add other configuration fields as needed +} + +// Validate the configuration +func (s AmapMCPServer) ConfigHasError() error { + if s.ApiKey == "" { + return errors.New("missing api key") + } + return nil +} + +// Parse configuration from JSON +func ParseFromConfig(configBytes []byte, server *AmapMCPServer) error { + return json.Unmarshal(configBytes, server) +} + +// Parse configuration from HTTP request +func ParseFromRequest(ctx wrapper.HttpContext, server *AmapMCPServer) error { + return ctx.ParseMCPServerConfig(server) +} \ No newline at end of file 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 new file mode 100644 index 00000000..161140a8 --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_around_search.go @@ -0,0 +1,80 @@ +package tools + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "amap-tools/server" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/log" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +type AroundSearchRequest struct { + Location string `json:"location" jsonschema_description:"中心点经度纬度"` + Radius string `json:"radius" jsonschema_description:"搜索半径"` + Keywords string `json:"keywords" jsonschema_description:"搜索关键词"` +} + +func (t AroundSearchRequest) Description() string { + return "周边搜,根据用户传入关键词以及坐标location,搜索出radius半径范围的POI" +} + +func (t AroundSearchRequest) InputSchema() map[string]any { + return wrapper.ToInputSchema(&AroundSearchRequest{}) +} + +func (t AroundSearchRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { + 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 + } + + 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)) + 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)) + return + } + var response struct { + Status string `json:"status"` + Info string `json:"info"` + Pois []struct { + ID string `json:"id"` + Name string `json:"name"` + Address string `json:"address"` + Typecode string `json:"typecode"` + } `json:"pois"` + } + err := json.Unmarshal(responseBody, &response) + if err != nil { + ctx.OnMCPToolCallError(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)) + return + } + result := fmt.Sprintf(`{"pois": %s}`, string(responseBody)) + ctx.SendMCPToolTextResult(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 new file mode 100644 index 00000000..33b07231 --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_bicycling.go @@ -0,0 +1,87 @@ +package tools + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "amap-tools/server" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/log" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +type BicyclingRequest struct { + Origin string `json:"origin" jsonschema_description:"出发点经纬度,坐标格式为:经度,纬度"` + Destination string `json:"destination" jsonschema_description:"目的地经纬度,坐标格式为:经度,纬度"` +} + +func (t BicyclingRequest) Description() string { + return "骑行路径规划用于规划骑行通勤方案,规划时会考虑天桥、单行线、封路等情况。最大支持 500km 的骑行路线规划" +} + +func (t BicyclingRequest) InputSchema() map[string]any { + return wrapper.ToInputSchema(&BicyclingRequest{}) +} + +func (t BicyclingRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { + 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 + } + + 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)) + 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)) + return + } + var response struct { + Errcode int `json:"errcode"` + Data struct { + Origin string `json:"origin"` + Destination string `json:"destination"` + Paths []struct { + Distance string `json:"distance"` + Duration string `json:"duration"` + Steps []struct { + Instruction string `json:"instruction"` + Road string `json:"road"` + Distance string `json:"distance"` + Orientation string `json:"orientation"` + 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)) + return + } + if response.Errcode != 0 { + ctx.OnMCPToolCallError(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) + }) +} \ 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 new file mode 100644 index 00000000..c29b72f1 --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_driving.go @@ -0,0 +1,89 @@ +package tools + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "amap-tools/server" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/log" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +type DrivingRequest struct { + Origin string `json:"origin" jsonschema_description:"出发点经度,纬度,坐标格式为:经度,纬度"` + Destination string `json:"destination" jsonschema_description:"目的地经纬度,坐标格式为:经度,纬度"` +} + +func (t DrivingRequest) Description() string { + return "驾车路径规划 API 可以根据用户起终点经纬度坐标规划以小客车、轿车通勤出行的方案,并且返回通勤方案的数据" +} + +func (t DrivingRequest) InputSchema() map[string]any { + return wrapper.ToInputSchema(&DrivingRequest{}) +} + +func (t DrivingRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { + 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 + } + + 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)) + 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)) + return + } + var response struct { + Status string `json:"status"` + Info string `json:"info"` + Route struct { + Origin string `json:"origin"` + Destination string `json:"destination"` + Paths []struct { + Path string `json:"path"` + Distance string `json:"distance"` + Duration string `json:"duration"` + Steps []struct { + Instruction string `json:"instruction"` + Road string `json:"road"` + Distance string `json:"distance"` + Orientation string `json:"orientation"` + 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)) + return + } + if response.Status != "1" { + ctx.OnMCPToolCallError(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) + }) +} \ 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 new file mode 100644 index 00000000..9019a9ac --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_transit_integrated.go @@ -0,0 +1,125 @@ +package tools + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "amap-tools/server" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/log" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +type TransitIntegratedRequest struct { + Origin string `json:"origin" jsonschema_description:"出发点经纬度,坐标格式为:经度,纬度"` + Destination string `json:"destination" jsonschema_description:"目的地经纬度,坐标格式为:经度,纬度"` + City string `json:"city" jsonschema_description:"公共交通规划起点城市"` + Cityd string `json:"cityd" jsonschema_description:"公共交通规划终点城市"` +} + +func (t TransitIntegratedRequest) Description() string { + return "公交路径规划 API 可以根据用户起终点经纬度坐标规划综合各类公共(火车、公交、地铁)交通方式的通勤方案,并且返回通勤方案的数据,跨城场景下必须传起点城市与终点城市" +} + +func (t TransitIntegratedRequest) InputSchema() map[string]any { + return wrapper.ToInputSchema(&TransitIntegratedRequest{}) +} + +func (t TransitIntegratedRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { + 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 + } + + 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)) + 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)) + return + } + var response struct { + Status string `json:"status"` + Info string `json:"info"` + Route struct { + Origin string `json:"origin"` + Destination string `json:"destination"` + Distance string `json:"distance"` + Transits []struct { + Duration string `json:"duration"` + WalkingDistance string `json:"walking_distance"` + Segments []struct { + Walking struct { + 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"` + AssistantAction string `json:"assistant_action"` + } `json:"steps"` + } `json:"walking"` + Bus struct { + Buslines []struct { + Name string `json:"name"` + DepartureStop struct { + Name string `json:"name"` + } `json:"departure_stop"` + ArrivalStop struct { + Name string `json:"name"` + } `json:"arrival_stop"` + Distance string `json:"distance"` + Duration string `json:"duration"` + ViaStops []struct { + Name string `json:"name"` + } `json:"via_stops"` + } `json:"buslines"` + } `json:"bus"` + Entrance struct { + Name string `json:"name"` + } `json:"entrance"` + Exit struct { + Name string `json:"name"` + } `json:"exit"` + Railway struct { + Name string `json:"name"` + Trip string `json:"trip"` + } `json:"railway"` + } `json:"segments"` + } `json:"transits"` + } `json:"route"` + } + err := json.Unmarshal(responseBody, &response) + if err != nil { + ctx.OnMCPToolCallError(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)) + 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) + }) +} \ 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 new file mode 100644 index 00000000..9a7e594e --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_direction_walking.go @@ -0,0 +1,88 @@ +package tools + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "amap-tools/server" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/log" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +type WalkingRequest struct { + Origin string `json:"origin" jsonschema_description:"出发点经度,纬度,坐标格式为:经度,纬度"` + Destination string `json:"destination" jsonschema_description:"目的地经纬度,坐标格式为:经度,纬度"` +} + +func (t WalkingRequest) Description() string { + return "步行路径规划 API 可以根据输入起点终点经纬度坐标规划100km 以内的步行通勤方案,并且返回通勤方案的数据" +} + +func (t WalkingRequest) InputSchema() map[string]any { + return wrapper.ToInputSchema(&WalkingRequest{}) +} + +func (t WalkingRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { + 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 + } + + 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)) + 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)) + return + } + var response struct { + Status string `json:"status"` + Info string `json:"info"` + Route struct { + Origin string `json:"origin"` + Destination string `json:"destination"` + Paths []struct { + Distance string `json:"distance"` + Duration string `json:"duration"` + Steps []struct { + Instruction string `json:"instruction"` + Road string `json:"road"` + Distance string `json:"distance"` + Orientation string `json:"orientation"` + 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)) + return + } + if response.Status != "1" { + ctx.OnMCPToolCallError(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) + }) +} \ 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 new file mode 100644 index 00000000..39ac9671 --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_distance.go @@ -0,0 +1,80 @@ +package tools + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "amap-tools/server" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/log" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +type DistanceRequest struct { + Origins string `json:"origins" jsonschema_description:"起点经度,纬度,可以传多个坐标,使用分号隔离,比如120,30;120,31,坐标格式为:经度,纬度"` + Destination string `json:"destination" jsonschema_description:"终点经度,纬度,坐标格式为:经度,纬度"` + Type string `json:"type" jsonschema_description:"距离测量类型,1代表驾车距离测量,0代表直线距离测量,3步行距离测量"` +} + +func (t DistanceRequest) Description() string { + return "距离测量 API 可以测量两个经纬度坐标之间的距离,支持驾车、步行以及球面距离测量" +} + +func (t DistanceRequest) InputSchema() map[string]any { + return wrapper.ToInputSchema(&DistanceRequest{}) +} + +func (t DistanceRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { + 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 + } + + 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)) + 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)) + return + } + var response struct { + Status string `json:"status"` + Info string `json:"info"` + Results []struct { + OriginID string `json:"origin_id"` + DestID string `json:"dest_id"` + Distance string `json:"distance"` + Duration string `json:"duration"` + } `json:"results"` + } + err := json.Unmarshal(responseBody, &response) + if err != nil { + ctx.OnMCPToolCallError(fmt.Errorf("failed to parse distance response: %v", err)) + return + } + if response.Status != "1" { + ctx.OnMCPToolCallError(fmt.Errorf("distance failed: %s", response.Info)) + return + } + result := fmt.Sprintf(`{"results": %s}`, string(responseBody)) + ctx.SendMCPToolTextResult(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 new file mode 100644 index 00000000..33a331b5 --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_geo.go @@ -0,0 +1,101 @@ +package tools + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "amap-tools/server" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/log" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +type GeoRequest struct { + Address string `json:"address" jsonschema_description:"待解析的结构化地址信息"` + City string `json:"city" jsonschema_description:"指定查询的城市"` +} + +func (t GeoRequest) Description() string { + return "将详细的结构化地址转换为经纬度坐标。支持对地标性名胜景区、建筑物名称解析为经纬度坐标" +} + +func (t GeoRequest) InputSchema() map[string]any { + return wrapper.ToInputSchema(&GeoRequest{}) +} + +func (t GeoRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { + 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") + } + + 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)) + return + } + var response struct { + Status string `json:"status"` + Info string `json:"info"` + Geocodes []struct { + Country string `json:"country"` + Province string `json:"province"` + City string `json:"city"` + Citycode string `json:"citycode"` + District string `json:"district"` + Street string `json:"street"` + Number string `json:"number"` + Adcode string `json:"adcode"` + Location string `json:"location"` + Level string `json:"level"` + } `json:"geocodes"` + } + err := json.Unmarshal(responseBody, &response) + if err != nil { + ctx.OnMCPToolCallError(fmt.Errorf("failed to parse geo response: %v", err)) + return + } + if response.Status != "1" { + ctx.OnMCPToolCallError(fmt.Errorf("geo failed: %s", response.Info)) + return + } + var results []map[string]string + for _, geo := range response.Geocodes { + result := map[string]string{ + "country": geo.Country, + "province": geo.Province, + "city": geo.City, + "citycode": geo.Citycode, + "district": geo.District, + "street": geo.Street, + "number": geo.Number, + "adcode": geo.Adcode, + "location": geo.Location, + "level": geo.Level, + } + results = append(results, result) + } + result, _ := json.Marshal(results) + ctx.SendMCPToolTextResult(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 new file mode 100644 index 00000000..7fbc924d --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_ip_location.go @@ -0,0 +1,76 @@ +package tools + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "amap-tools/server" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/log" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +type IPLocationRequest struct { + IP string `json:"ip" jsonschema_description:"IP地址"` +} + +func (t IPLocationRequest) Description() string { + return "IP 定位根据用户输入的 IP 地址,定位 IP 的所在位置" +} + +func (t IPLocationRequest) InputSchema() map[string]any { + return wrapper.ToInputSchema(&IPLocationRequest{}) +} + +func (t IPLocationRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { + 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 + } + + 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) + 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)) + return + } + var response struct { + 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)) + return + } + if response.Status != "1" { + ctx.OnMCPToolCallError(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) + }) +} \ 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 new file mode 100644 index 00000000..028d7608 --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_regeocode.go @@ -0,0 +1,79 @@ +package tools + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "amap-tools/server" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/log" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +type ReGeocodeRequest struct { + Location string `json:"location" jsonschema_description:"经纬度"` +} + +func (t ReGeocodeRequest) Description() string { + return "将一个高德经纬度坐标转换为行政区划地址信息" +} + +func (t ReGeocodeRequest) InputSchema() map[string]any { + return wrapper.ToInputSchema(&ReGeocodeRequest{}) +} + +func (t ReGeocodeRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { + 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 + } + + 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) + 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)) + return + } + var response struct { + Status string `json:"status"` + Info string `json:"info"` + Regeocode struct { + AddressComponent struct { + Province string `json:"province"` + City string `json:"city"` + District string `json:"district"` + } `json:"addressComponent"` + } `json:"regeocode"` + } + err := json.Unmarshal(responseBody, &response) + if err != nil { + ctx.OnMCPToolCallError(fmt.Errorf("failed to parse regeocode response: %v", err)) + return + } + if response.Status != "1" { + ctx.OnMCPToolCallError(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) + }) +} \ 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 new file mode 100644 index 00000000..f5b14ec1 --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_search_detail.go @@ -0,0 +1,84 @@ +package tools + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "amap-tools/server" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/log" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +type SearchDetailRequest struct { + ID string `json:"id" jsonschema_description:"关键词搜或者周边搜获取到的POI ID"` +} + +func (t SearchDetailRequest) Description() string { + return "查询关键词搜或者周边搜获取到的POI ID的详细信息" +} + +func (t SearchDetailRequest) InputSchema() map[string]any { + return wrapper.ToInputSchema(&SearchDetailRequest{}) +} + +func (t SearchDetailRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { + 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 + } + + 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) + 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)) + 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"` + } `json:"pois"` + } + err := json.Unmarshal(responseBody, &response) + if err != nil { + ctx.OnMCPToolCallError(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)) + 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) + }) +} \ 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 new file mode 100644 index 00000000..6a545d59 --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_text_search.go @@ -0,0 +1,90 @@ +package tools + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "amap-tools/server" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/log" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +type TextSearchRequest struct { + Keywords string `json:"keywords" jsonschema_description:"搜索关键词"` + City string `json:"city" jsonschema_description:"查询城市"` + Citylimit string `json:"citylimit" jsonschema_description:"是否强制限制在设置的城市内搜索,默认值为false"` +} + +func (t TextSearchRequest) Description() string { + return "关键词搜,根据用户传入关键词,搜索出相关的POI" +} + +func (t TextSearchRequest) InputSchema() map[string]any { + return wrapper.ToInputSchema(&TextSearchRequest{}) +} + +func (t TextSearchRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { + 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 + } + + 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)) + 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)) + return + } + var response struct { + Status string `json:"status"` + Info string `json:"info"` + Suggestion struct { + Keywords []string `json:"keywords"` + Cities []struct { + Name string `json:"name"` + } `json:"cities"` + } `json:"suggestion"` + Pois []struct { + ID string `json:"id"` + Name string `json:"name"` + Address string `json:"address"` + Typecode string `json:"typecode"` + } `json:"pois"` + } + err := json.Unmarshal(responseBody, &response) + if err != nil { + ctx.OnMCPToolCallError(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)) + 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) + }) +} \ 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 new file mode 100644 index 00000000..93229c11 --- /dev/null +++ b/plugins/wasm-go/mcp-servers/amap-tools/tools/maps_weather.go @@ -0,0 +1,89 @@ +package tools + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "amap-tools/server" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/log" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +type WeatherRequest struct { + City string `json:"city" jsonschema_description:"城市名称或者adcode"` +} + +func (t WeatherRequest) Description() string { + return "根据城市名称或者标准adcode查询指定城市的天气" +} + +func (t WeatherRequest) InputSchema() map[string]any { + return wrapper.ToInputSchema(&WeatherRequest{}) +} + +func (t WeatherRequest) Create(params []byte) wrapper.MCPTool[server.AmapMCPServer] { + 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 + } + + 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) + 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)) + return + } + var response struct { + Status string `json:"status"` + Info string `json:"info"` + Forecasts []struct { + City string `json:"city"` + Casts []struct { + 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"` + } `json:"casts"` + } `json:"forecasts"` + } + err := json.Unmarshal(responseBody, &response) + if err != nil { + ctx.OnMCPToolCallError(fmt.Errorf("failed to parse weather response: %v", err)) + return + } + if response.Status != "1" { + ctx.OnMCPToolCallError(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) + }) +} \ No newline at end of file