mirror of
https://github.com/alibaba/higress.git
synced 2026-03-04 08:30:48 +08:00
Add remote mcp server sdk (#1946)
This commit is contained in:
@@ -17,5 +17,6 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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=
|
||||
@@ -20,6 +21,8 @@ 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=
|
||||
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=
|
||||
|
||||
33
plugins/wasm-go/mcp-servers/Dockerfile
Normal file
33
plugins/wasm-go/mcp-servers/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
# Build stage
|
||||
FROM golang:1.24 AS builder
|
||||
|
||||
ARG SERVER_NAME=quark-search
|
||||
ARG GOPROXY
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the server code
|
||||
COPY ${SERVER_NAME}/ .
|
||||
|
||||
# Set GOPROXY if provided
|
||||
RUN if [ -n "$GOPROXY" ]; then go env -w GOPROXY=${GOPROXY}; fi
|
||||
|
||||
# Build the WASM binary
|
||||
RUN GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm main.go
|
||||
|
||||
# Final stage
|
||||
FROM scratch
|
||||
|
||||
ARG SERVER_NAME=quark-search
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# Copy the WASM binary from the builder stage
|
||||
COPY --from=builder /app/main.wasm /main.wasm
|
||||
|
||||
# Metadata
|
||||
LABEL org.opencontainers.image.title="${SERVER_NAME}"
|
||||
LABEL org.opencontainers.image.description="Higress MCP Server - ${SERVER_NAME}"
|
||||
LABEL org.opencontainers.image.source="https://github.com/alibaba/higress"
|
||||
|
||||
# The WASM binary is the only artifact in the image
|
||||
51
plugins/wasm-go/mcp-servers/Makefile
Normal file
51
plugins/wasm-go/mcp-servers/Makefile
Normal file
@@ -0,0 +1,51 @@
|
||||
# MCP Server Makefile
|
||||
|
||||
# Variables
|
||||
SERVER_NAME ?= quark-search
|
||||
REGISTRY ?= higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/
|
||||
GO_VERSION ?= 1.24
|
||||
BUILD_TIME := $(shell date "+%Y%m%d-%H%M%S")
|
||||
COMMIT_ID := $(shell git rev-parse --short HEAD 2>/dev/null)
|
||||
IMAGE_TAG = $(if $(strip $(SERVER_VERSION)),${SERVER_VERSION},${BUILD_TIME}-${COMMIT_ID})
|
||||
IMG ?= ${REGISTRY}${SERVER_NAME}:${IMAGE_TAG}
|
||||
GOPROXY := $(shell go env GOPROXY)
|
||||
|
||||
# Default target
|
||||
.DEFAULT:
|
||||
build:
|
||||
@echo "Building WASM binary for ${SERVER_NAME}..."
|
||||
cd ${SERVER_NAME} && GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm main.go
|
||||
@echo ""
|
||||
@echo "Output WASM file: ${SERVER_NAME}/main.wasm"
|
||||
|
||||
# Build Docker image
|
||||
build-image:
|
||||
@echo "Building Docker image for ${SERVER_NAME}..."
|
||||
docker build -t ${IMG} \
|
||||
--build-arg SERVER_NAME=${SERVER_NAME} \
|
||||
--build-arg GOPROXY=${GOPROXY} \
|
||||
-f Dockerfile .
|
||||
@echo ""
|
||||
@echo "Image: ${IMG}"
|
||||
|
||||
# Build and push Docker image
|
||||
build-push: build-image
|
||||
docker push ${IMG}
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -f ${SERVER_NAME}/main.wasm
|
||||
|
||||
# Help
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " build - Build WASM binary"
|
||||
@echo " build-image - Build Docker image"
|
||||
@echo " build-push - Build and push Docker image"
|
||||
@echo " clean - Remove build artifacts"
|
||||
@echo ""
|
||||
@echo "Variables:"
|
||||
@echo " SERVER_NAME - Name of the MCP server (default: ${SERVER_NAME})"
|
||||
@echo " REGISTRY - Docker registry (default: ${REGISTRY})"
|
||||
@echo " SERVER_VERSION - Version tag for the image (default: timestamp-commit)"
|
||||
@echo " IMG - Full image name (default: ${IMG})"
|
||||
247
plugins/wasm-go/mcp-servers/README.md
Normal file
247
plugins/wasm-go/mcp-servers/README.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# MCP Server Implementation Guide
|
||||
|
||||
This guide explains how to implement a Model Context Protocol (MCP) server using the Higress WASM Go SDK. MCP servers provide tools and resources that extend the capabilities of AI assistants.
|
||||
|
||||
## Overview
|
||||
|
||||
An MCP server is a standalone application that communicates with AI assistants through the Model Context Protocol. It can provide:
|
||||
|
||||
- **Tools**: Functions that can be called by the AI to perform specific tasks
|
||||
- **Resources**: Data that can be accessed by the AI
|
||||
|
||||
> **Note**: MCP server plugins require Higress version 2.1.0 or higher to be used.
|
||||
|
||||
## Project Structure
|
||||
|
||||
A typical MCP server project has the following structure:
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
```go
|
||||
// server/server.go
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
// Define your server configuration structure
|
||||
type MyMCPServer 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)
|
||||
}
|
||||
```
|
||||
|
||||
## Tool Implementation
|
||||
|
||||
Each tool should be implemented as a struct with the following methods:
|
||||
|
||||
1. `Description()`: Returns a description of the tool
|
||||
2. `InputSchema()`: Returns the JSON schema for the tool's input parameters
|
||||
3. `Create()`: Creates a new instance of the tool with the provided parameters
|
||||
4. `Call()`: Executes the tool's functionality
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
// tools/my_tool.go
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"my-mcp-server/server"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
// Define your tool structure with input parameters
|
||||
type MyTool struct {
|
||||
Param1 string `json:"param1" jsonschema_description:"Description of param1" jsonschema:"example=example value"`
|
||||
Param2 int `json:"param2,omitempty" jsonschema_description:"Description of param2" jsonschema:"default=5"`
|
||||
}
|
||||
|
||||
// Description returns the description field for the MCP tool definition.
|
||||
// This corresponds to the "description" field in the MCP tool JSON response,
|
||||
// which provides a human-readable explanation of the tool's purpose and usage.
|
||||
func (t MyTool) Description() string {
|
||||
return `Detailed description of what this tool does and when to use it.`
|
||||
}
|
||||
|
||||
// InputSchema returns the inputSchema field for the MCP tool definition.
|
||||
// This corresponds to the "inputSchema" field in the MCP tool JSON response,
|
||||
// which defines the JSON Schema for the tool's input parameters, including
|
||||
// property types, descriptions, and required fields.
|
||||
func (t MyTool) InputSchema() map[string]any {
|
||||
return wrapper.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] {
|
||||
myTool := &MyTool{
|
||||
Param2: 5, // Default value
|
||||
}
|
||||
json.Unmarshal(params, &myTool)
|
||||
return myTool
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Implement your tool's logic here
|
||||
// ...
|
||||
|
||||
// Return results
|
||||
ctx.SendMCPToolTextResult(fmt.Sprintf("Result: %s, %d", t.Param1, t.Param2))
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Main Entry Point
|
||||
|
||||
The main.go file is the entry point for your MCP server. It registers your tools and resources:
|
||||
|
||||
```go
|
||||
// main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"my-mcp-server/server"
|
||||
"my-mcp-server/tools"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"my-mcp-server", // Server name
|
||||
wrapper.ParseRawConfig(server.ParseFromConfig),
|
||||
wrapper.AddMCPTool("my_tool", tools.MyTool{}), // Register tools
|
||||
// Add more tools as needed
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## Building the WASM Binary
|
||||
|
||||
To compile your Go code into a WebAssembly (WASM) file, use the following command:
|
||||
|
||||
```bash
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm main.go
|
||||
```
|
||||
|
||||
This command sets the target operating system to `wasip1` (WebAssembly System Interface) and architecture to `wasm` (WebAssembly), then builds your code as a C-shared library and outputs it as `main.wasm`.
|
||||
|
||||
## Using the Makefile
|
||||
|
||||
A Makefile is provided to simplify the build process. It includes the following targets:
|
||||
|
||||
- `make build`: Builds the WASM binary for your MCP server
|
||||
- `make build-image`: Builds a Docker image containing your MCP server
|
||||
- `make build-push`: Builds and pushes the Docker image to a registry
|
||||
- `make clean`: Removes build artifacts
|
||||
- `make help`: Shows available targets and variables
|
||||
|
||||
You can customize the build by setting the following variables:
|
||||
|
||||
```bash
|
||||
# Build with a custom server name
|
||||
make SERVER_NAME=my-mcp-server build
|
||||
|
||||
# Build with a custom registry
|
||||
make REGISTRY=my-registry.example.com/ build-image
|
||||
|
||||
# Build with a specific version tag
|
||||
make SERVER_VERSION=1.0.0 build-image
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
You can create unit tests for your tools to verify their functionality:
|
||||
|
||||
```go
|
||||
// tools/my_tool_test.go
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMyToolInputSchema(t *testing.T) {
|
||||
myTool := MyTool{}
|
||||
schema := myTool.InputSchema()
|
||||
|
||||
schemaJSON, err := json.MarshalIndent(schema, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal schema to JSON: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("MyTool InputSchema:\n%s\n", string(schemaJSON))
|
||||
|
||||
if len(schema) == 0 {
|
||||
t.Error("InputSchema returned an empty schema")
|
||||
}
|
||||
}
|
||||
```
|
||||
243
plugins/wasm-go/mcp-servers/README_zh.md
Normal file
243
plugins/wasm-go/mcp-servers/README_zh.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# MCP 服务器实现指南
|
||||
|
||||
本指南介绍如何使用 Higress WASM Go SDK 实现 Model Context Protocol (MCP) 服务器。MCP 服务器提供工具和资源,扩展 AI 助手的能力。
|
||||
|
||||
## 概述
|
||||
|
||||
MCP 服务器是一个独立的应用程序,通过 Model Context Protocol 与 AI 助手通信。它可以提供:
|
||||
|
||||
- **工具**:可以被 AI 调用以执行特定任务的函数
|
||||
- **资源**:可以被 AI 访问的数据
|
||||
|
||||
> **注意**:MCP 服务器插件需要 Higress 2.1.0 或更高版本才能使用。
|
||||
|
||||
## 项目结构
|
||||
|
||||
一个典型的 MCP 服务器项目具有以下结构:
|
||||
|
||||
```
|
||||
my-mcp-server/
|
||||
├── go.mod # Go 模块定义
|
||||
├── go.sum # Go 模块校验和
|
||||
├── main.go # 注册工具和资源的入口点
|
||||
├── server/
|
||||
│ └── server.go # 服务器配置和解析
|
||||
└── tools/
|
||||
└── my_tool.go # 工具实现
|
||||
```
|
||||
|
||||
## 服务器配置
|
||||
|
||||
服务器配置定义了服务器运行所需的参数。例如:
|
||||
|
||||
```go
|
||||
// server/server.go
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
// 定义服务器配置结构
|
||||
type MyMCPServer 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)
|
||||
}
|
||||
```
|
||||
|
||||
## 工具实现
|
||||
|
||||
每个工具应该实现为一个具有以下方法的结构体:
|
||||
|
||||
1. `Description()`:返回工具的描述
|
||||
2. `InputSchema()`:返回工具输入参数的 JSON schema
|
||||
3. `Create()`:使用提供的参数创建工具的新实例
|
||||
4. `Call()`:执行工具的功能
|
||||
|
||||
示例:
|
||||
|
||||
```go
|
||||
// tools/my_tool.go
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"my-mcp-server/server"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
// 定义带有输入参数的工具结构
|
||||
type MyTool struct {
|
||||
Param1 string `json:"param1" jsonschema_description:"参数1的描述" jsonschema:"example=示例值"`
|
||||
Param2 int `json:"param2,omitempty" jsonschema_description:"参数2的描述" jsonschema:"default=5"`
|
||||
}
|
||||
|
||||
// Description 返回 MCP 工具定义的描述字段。
|
||||
// 这对应于 MCP 工具 JSON 响应中的 "description" 字段,
|
||||
// 提供了工具目的和用法的人类可读解释。
|
||||
func (t MyTool) Description() string {
|
||||
return `详细描述这个工具做什么以及何时使用它。`
|
||||
}
|
||||
|
||||
// InputSchema 返回 MCP 工具定义的 inputSchema 字段。
|
||||
// 这对应于 MCP 工具 JSON 响应中的 "inputSchema" 字段,
|
||||
// 定义了工具输入参数的 JSON Schema,包括属性类型、描述和必填字段。
|
||||
func (t MyTool) InputSchema() map[string]any {
|
||||
return wrapper.ToInputSchema(&MyTool{})
|
||||
}
|
||||
|
||||
// Create 基于 MCP 工具调用的输入参数实例化一个新的工具实例。
|
||||
// 它将 JSON 参数反序列化为结构体,为可选字段应用默认值,并返回配置好的工具实例。
|
||||
func (t MyTool) Create(params []byte) wrapper.MCPTool[server.MyMCPServer] {
|
||||
myTool := &MyTool{
|
||||
Param2: 5, // 默认值
|
||||
}
|
||||
json.Unmarshal(params, &myTool)
|
||||
return myTool
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 在这里实现工具的逻辑
|
||||
// ...
|
||||
|
||||
// 返回结果
|
||||
ctx.SendMCPToolTextResult(fmt.Sprintf("结果: %s, %d", t.Param1, t.Param2))
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## 主入口点
|
||||
|
||||
main.go 文件是 MCP 服务器的入口点。它注册工具和资源:
|
||||
|
||||
```go
|
||||
// main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"my-mcp-server/server"
|
||||
"my-mcp-server/tools"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"my-mcp-server", // 服务器名称
|
||||
wrapper.ParseRawConfig(server.ParseFromConfig),
|
||||
wrapper.AddMCPTool("my_tool", tools.MyTool{}), // 注册工具
|
||||
// 根据需要添加更多工具
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖项
|
||||
|
||||
您的 MCP 服务器必须使用支持 Go 1.24 WebAssembly 编译功能的特定版本的 wasm-go SDK:
|
||||
|
||||
```bash
|
||||
# 添加具有特定版本标签的必需依赖项
|
||||
go get github.com/alibaba/higress/plugins/wasm-go@wasm-go-1.24
|
||||
```
|
||||
|
||||
## 构建 WASM 二进制文件
|
||||
|
||||
要将 Go 代码编译为 WebAssembly (WASM) 文件,请使用以下命令:
|
||||
|
||||
```bash
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm main.go
|
||||
```
|
||||
|
||||
此命令将目标操作系统设置为 `wasip1`(WebAssembly 系统接口)和架构设置为 `wasm`(WebAssembly),然后将代码构建为 C 共享库并输出为 `main.wasm`。
|
||||
|
||||
## 使用 Makefile
|
||||
|
||||
提供了 Makefile 以简化构建过程。它包括以下目标:
|
||||
|
||||
- `make build`:为 MCP 服务器构建 WASM 二进制文件
|
||||
- `make build-image`:构建包含 MCP 服务器的 Docker 镜像
|
||||
- `make build-push`:构建并将 Docker 镜像推送到注册表
|
||||
- `make clean`:删除构建产物
|
||||
- `make help`:显示可用的目标和变量
|
||||
|
||||
您可以通过设置以下变量来自定义构建:
|
||||
|
||||
```bash
|
||||
# 使用自定义服务器名称构建
|
||||
make SERVER_NAME=my-mcp-server build
|
||||
|
||||
# 使用自定义注册表构建
|
||||
make REGISTRY=my-registry.example.com/ build-image
|
||||
|
||||
# 使用特定版本标签构建
|
||||
make SERVER_VERSION=1.0.0 build-image
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
您可以为工具创建单元测试以验证其功能:
|
||||
|
||||
```go
|
||||
// tools/my_tool_test.go
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMyToolInputSchema(t *testing.T) {
|
||||
myTool := MyTool{}
|
||||
schema := myTool.InputSchema()
|
||||
|
||||
schemaJSON, err := json.MarshalIndent(schema, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("无法将 schema 序列化为 JSON: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("MyTool InputSchema:\n%s\n", string(schemaJSON))
|
||||
|
||||
if len(schema) == 0 {
|
||||
t.Error("InputSchema 返回了空 schema")
|
||||
}
|
||||
}
|
||||
25
plugins/wasm-go/mcp-servers/quark-search/go.mod
Normal file
25
plugins/wasm-go/mcp-servers/quark-search/go.mod
Normal file
@@ -0,0 +1,25 @@
|
||||
module quark-search
|
||||
|
||||
go 1.24
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250324133957-dab499f6ade6
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250323151219-d75620c61711 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
38
plugins/wasm-go/mcp-servers/quark-search/go.sum
Normal file
38
plugins/wasm-go/mcp-servers/quark-search/go.sum
Normal file
@@ -0,0 +1,38 @@
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250324133957-dab499f6ade6 h1:/iHNur+B0lHmcy97XYwHb6QrnHJichzKs37gnTyGP3k=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250324133957-dab499f6ade6/go.mod h1:csP9Mpkc+gVgbZsizCdcYSy0LJrQA+//RcnZBInyknc=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250323151219-d75620c61711 h1:n5sZwSZWQ5uKS69hu50/0gliTFrIJ1w+g/FSdIIiZIs=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250323151219-d75620c61711/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
32
plugins/wasm-go/mcp-servers/quark-search/main.go
Normal file
32
plugins/wasm-go/mcp-servers/quark-search/main.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"quark-search/server"
|
||||
"quark-search/tools"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"quark-mcp-server",
|
||||
wrapper.ParseRawConfig(server.ParseFromConfig),
|
||||
wrapper.AddMCPTool("web_search", tools.WebSearch{}),
|
||||
)
|
||||
}
|
||||
41
plugins/wasm-go/mcp-servers/quark-search/server/server.go
Normal file
41
plugins/wasm-go/mcp-servers/quark-search/server/server.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
type QuarkMCPServer struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
func (s QuarkMCPServer) ConfigHasError() error {
|
||||
if s.ApiKey == "" {
|
||||
return errors.New("missing api key")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseFromConfig(configBytes []byte, server *QuarkMCPServer) error {
|
||||
return json.Unmarshal(configBytes, server)
|
||||
}
|
||||
|
||||
func ParseFromRequest(ctx wrapper.HttpContext, server *QuarkMCPServer) error {
|
||||
return ctx.ParseMCPServerConfig(server)
|
||||
}
|
||||
130
plugins/wasm-go/mcp-servers/quark-search/tools/web_search.go
Normal file
130
plugins/wasm-go/mcp-servers/quark-search/tools/web_search.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"quark-search/server"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type SearchResult struct {
|
||||
Title string
|
||||
Link string
|
||||
Content string
|
||||
}
|
||||
|
||||
func (result SearchResult) Valid() bool {
|
||||
return result.Title != "" && result.Link != "" && result.Content != ""
|
||||
}
|
||||
|
||||
func (result SearchResult) Format() string {
|
||||
return fmt.Sprintf(`
|
||||
## Title: %s
|
||||
|
||||
### Reference URL
|
||||
%s
|
||||
|
||||
### Content
|
||||
%s
|
||||
`, result.Title, result.Link, result.Content)
|
||||
}
|
||||
|
||||
type WebSearch struct {
|
||||
Query string `json:"query" jsonschema_description:"Search query, please use Chinese" jsonschema:"example=黄金价格走势"`
|
||||
ContentMode string `json:"contentMode,omitempty" jsonschema_description:"Return the level of content detail, choose to use summary or full text" jsonschema:"enum=full,enum=summary,default=summary"`
|
||||
Number uint32 `json:"number,omitempty" jsonschema_description:"Number of results" jsonschema:"default=5"`
|
||||
}
|
||||
|
||||
// Description returns the description field for the MCP tool definition.
|
||||
// This corresponds to the "description" field in the MCP tool JSON response,
|
||||
// which provides a human-readable explanation of the tool's purpose and usage.
|
||||
func (t WebSearch) Description() string {
|
||||
return `Performs a web search using the Quark Search API, ideal for general queries, news, articles, and online content.
|
||||
Use this for broad information gathering, recent events, or when you need diverse web sources.
|
||||
Because Quark search performs poorly for English searches, please use Chinese for the query parameters.`
|
||||
}
|
||||
|
||||
// InputSchema returns the inputSchema field for the MCP tool definition.
|
||||
// This corresponds to the "inputSchema" field in the MCP tool JSON response,
|
||||
// which defines the JSON Schema for the tool's input parameters, including
|
||||
// property types, descriptions, and required fields.
|
||||
func (t WebSearch) InputSchema() map[string]any {
|
||||
return wrapper.ToInputSchema(&WebSearch{})
|
||||
}
|
||||
|
||||
// Create instantiates a new WebSearch tool instance based on the input parameters
|
||||
// from an MCP tool call.
|
||||
func (t WebSearch) Create(params []byte) wrapper.MCPTool[server.QuarkMCPServer] {
|
||||
webSearch := &WebSearch{
|
||||
ContentMode: "summary",
|
||||
Number: 5,
|
||||
}
|
||||
json.Unmarshal(params, &webSearch)
|
||||
return webSearch
|
||||
}
|
||||
|
||||
// Call implements the core logic for handling an MCP tool call. This method is executed
|
||||
// when the tool is invoked through the MCP framework. It processes the configured parameters,
|
||||
// makes the actual API request to the service, parses the response,
|
||||
// and formats the results to be returned to the caller.
|
||||
func (t WebSearch) Call(ctx wrapper.HttpContext, config server.QuarkMCPServer) error {
|
||||
err := server.ParseFromRequest(ctx, &config)
|
||||
if err != nil {
|
||||
log.Errorf("parse config from request failed, err:%s", err)
|
||||
}
|
||||
err = config.ConfigHasError()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.RouteCall(http.MethodGet, fmt.Sprintf("https://cloud-iqs.aliyuncs.com/search/genericSearch?query=%s", url.QueryEscape(t.Query)),
|
||||
[][2]string{{"Accept", "application/json"},
|
||||
{"X-API-Key", config.ApiKey}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
ctx.OnMCPToolCallError(fmt.Errorf("quark search call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
jsonObj := gjson.ParseBytes(responseBody)
|
||||
var results []string
|
||||
for index, item := range jsonObj.Get("pageItems").Array() {
|
||||
var content string
|
||||
if t.ContentMode == "full" {
|
||||
content = item.Get("markdownText").String()
|
||||
if content == "" {
|
||||
content = item.Get("mainText").String()
|
||||
}
|
||||
} else if t.ContentMode == "summary" {
|
||||
content = item.Get("snippet").String()
|
||||
}
|
||||
result := SearchResult{
|
||||
Title: item.Get("title").String(),
|
||||
Link: item.Get("link").String(),
|
||||
Content: content,
|
||||
}
|
||||
if result.Valid() && index < int(t.Number) {
|
||||
results = append(results, result.Format())
|
||||
}
|
||||
}
|
||||
ctx.SendMCPToolTextResult(fmt.Sprintf("# Search Results\n\n%s", strings.Join(results, "\n\n")))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestWebSearchInputSchema tests the InputSchema method of WebSearch
|
||||
// to verify that the JSON schema configuration is correct.
|
||||
func TestWebSearchInputSchema(t *testing.T) {
|
||||
// Create a WebSearch instance
|
||||
webSearch := WebSearch{}
|
||||
|
||||
// Get the input schema
|
||||
schema := webSearch.InputSchema()
|
||||
|
||||
// Marshal the schema to JSON for better readability
|
||||
schemaJSON, err := json.MarshalIndent(schema, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal schema to JSON: %v", err)
|
||||
}
|
||||
|
||||
// Print the schema
|
||||
fmt.Printf("WebSearch InputSchema:\n%s\n", string(schemaJSON))
|
||||
|
||||
// Basic validation to ensure the schema is not empty
|
||||
if len(schema) == 0 {
|
||||
t.Error("InputSchema returned an empty schema")
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,19 @@ import (
|
||||
type Cluster interface {
|
||||
ClusterName() string
|
||||
HostName() string
|
||||
HttpCallNotify() HttpCallNotify
|
||||
}
|
||||
|
||||
type BaseCluster struct {
|
||||
notify HttpCallNotify
|
||||
}
|
||||
|
||||
func (base BaseCluster) HttpCallNotify() HttpCallNotify {
|
||||
return base.notify
|
||||
}
|
||||
|
||||
type RouteCluster struct {
|
||||
BaseCluster
|
||||
Host string
|
||||
}
|
||||
|
||||
@@ -46,6 +56,7 @@ func (c RouteCluster) HostName() string {
|
||||
}
|
||||
|
||||
type TargetCluster struct {
|
||||
BaseCluster
|
||||
Host string
|
||||
Cluster string
|
||||
}
|
||||
@@ -59,6 +70,7 @@ func (c TargetCluster) HostName() string {
|
||||
}
|
||||
|
||||
type K8sCluster struct {
|
||||
BaseCluster
|
||||
ServiceName string
|
||||
Namespace string
|
||||
Port int64
|
||||
@@ -83,6 +95,7 @@ func (c K8sCluster) HostName() string {
|
||||
}
|
||||
|
||||
type NacosCluster struct {
|
||||
BaseCluster
|
||||
ServiceName string
|
||||
// use DEFAULT-GROUP by default
|
||||
Group string
|
||||
@@ -115,6 +128,7 @@ func (c NacosCluster) HostName() string {
|
||||
}
|
||||
|
||||
type StaticIpCluster struct {
|
||||
BaseCluster
|
||||
ServiceName string
|
||||
Port int64
|
||||
Host string
|
||||
@@ -132,6 +146,7 @@ func (c StaticIpCluster) HostName() string {
|
||||
}
|
||||
|
||||
type DnsCluster struct {
|
||||
BaseCluster
|
||||
ServiceName string
|
||||
Domain string
|
||||
Port int64
|
||||
@@ -146,6 +161,7 @@ func (c DnsCluster) HostName() string {
|
||||
}
|
||||
|
||||
type ConsulCluster struct {
|
||||
BaseCluster
|
||||
ServiceName string
|
||||
Datacenter string
|
||||
Port int64
|
||||
@@ -166,6 +182,7 @@ func (c ConsulCluster) HostName() string {
|
||||
}
|
||||
|
||||
type FQDNCluster struct {
|
||||
BaseCluster
|
||||
FQDN string
|
||||
Host string
|
||||
Port int64
|
||||
|
||||
@@ -25,6 +25,11 @@ import (
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
)
|
||||
|
||||
type HttpCallNotify interface {
|
||||
HttpCallStart(uint32)
|
||||
HttpCallEnd(uint32)
|
||||
}
|
||||
|
||||
type ResponseCallback func(statusCode int, responseHeaders http.Header, responseBody []byte)
|
||||
|
||||
type HttpClient interface {
|
||||
@@ -108,7 +113,9 @@ func HttpCall(cluster Cluster, method, rawURL string, headers [][2]string, body
|
||||
}
|
||||
headers = append(headers, [2]string{":method", method}, [2]string{":path", path}, [2]string{":authority", authority})
|
||||
requestID := uuid.New().String()
|
||||
_, err = proxywasm.DispatchHttpCall(cluster.ClusterName(), headers, body, nil, timeout, func(numHeaders, bodySize, numTrailers int) {
|
||||
httpCallNotify := cluster.HttpCallNotify()
|
||||
var callID uint32
|
||||
callID, err = proxywasm.DispatchHttpCall(cluster.ClusterName(), headers, body, nil, timeout, func(numHeaders, bodySize, numTrailers int) {
|
||||
respBody, err := proxywasm.GetHttpCallResponseBody(0, bodySize)
|
||||
if err != nil {
|
||||
proxywasm.LogCriticalf("failed to get response body: %v", err)
|
||||
@@ -135,8 +142,12 @@ func HttpCall(cluster Cluster, method, rawURL string, headers [][2]string, body
|
||||
proxywasm.LogDebugf("http call end, id: %s, code: %d, normal: %t, body: %s",
|
||||
requestID, code, normalResponse, respBody)
|
||||
callback(code, headers, respBody)
|
||||
httpCallNotify.HttpCallEnd(callID)
|
||||
})
|
||||
proxywasm.LogDebugf("http call start, id: %s, cluster: %s, method: %s, url: %s, headers: %#v, body: %s, timeout: %d",
|
||||
requestID, cluster.ClusterName(), method, rawURL, headers, body, timeout)
|
||||
if err == nil {
|
||||
httpCallNotify.HttpCallStart(callID)
|
||||
proxywasm.LogDebugf("http call start, id: %s, cluster: %s, method: %s, url: %s, headers: %#v, body: %s, timeout: %d",
|
||||
requestID, cluster.ClusterName(), method, rawURL, headers, body, timeout)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
123
plugins/wasm-go/pkg/wrapper/jsonrpc_wrapper.go
Normal file
123
plugins/wasm-go/pkg/wrapper/jsonrpc_wrapper.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// 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 wrapper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
const (
|
||||
CtxJsonRpcID = "jsonRpcID"
|
||||
JError = "error"
|
||||
JCode = "code"
|
||||
JMessage = "message"
|
||||
JResult = "result"
|
||||
|
||||
ErrParseError = -32700
|
||||
ErrInvalidRequest = -32600
|
||||
ErrMethodNotFound = -32601
|
||||
ErrInvalidParams = -32602
|
||||
ErrInternalError = -32603
|
||||
)
|
||||
|
||||
type JsonRpcRequestHandler[PluginConfig any] func(context HttpContext, config PluginConfig, id int64, method string, params gjson.Result) types.Action
|
||||
|
||||
type JsonRpcResponseHandler[PluginConfig any] func(context HttpContext, config PluginConfig, id int64, result gjson.Result, error gjson.Result) types.Action
|
||||
|
||||
type JsonRpcMethodHandler[PluginConfig any] func(context HttpContext, config PluginConfig, id int64, params gjson.Result) error
|
||||
|
||||
type MethodHandlers[PluginConfig any] map[string]JsonRpcMethodHandler[PluginConfig]
|
||||
|
||||
func sendJsonRpcResponse(id int64, extras map[string]any, debugInfo string) {
|
||||
body := []byte(`{"jsonrpc": "2.0"}`)
|
||||
body, _ = sjson.SetBytes(body, "id", id)
|
||||
for key, value := range extras {
|
||||
body, _ = sjson.SetBytes(body, key, value)
|
||||
}
|
||||
proxywasm.SendHttpResponseWithDetail(200, debugInfo, [][2]string{{"Content-Type", "application/json; charset=utf-8"}}, body, -1)
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) OnJsonRpcResponseSuccess(result map[string]any) {
|
||||
var (
|
||||
id int64
|
||||
ok bool
|
||||
)
|
||||
if id, ok = ctx.userContext[CtxJsonRpcID].(int64); !ok {
|
||||
proxywasm.SendHttpResponseWithDetail(500, "not_found_json_rpc_id", nil, []byte("not found json rpc id"), -1)
|
||||
return
|
||||
}
|
||||
sendJsonRpcResponse(id, map[string]any{JResult: result}, "json_rpc_success")
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) OnJsonRpcResponseError(err error, code ...int) {
|
||||
var (
|
||||
id int64
|
||||
ok bool
|
||||
)
|
||||
if id, ok = ctx.userContext[CtxJsonRpcID].(int64); !ok {
|
||||
proxywasm.SendHttpResponseWithDetail(500, "not_found_json_rpc_id", nil, []byte("not found json rpc id"), -1)
|
||||
return
|
||||
}
|
||||
errorCode := ErrInternalError
|
||||
if len(code) > 0 {
|
||||
errorCode = code[0]
|
||||
}
|
||||
sendJsonRpcResponse(id, map[string]any{JError: map[string]any{
|
||||
JMessage: err.Error(),
|
||||
JCode: errorCode,
|
||||
}}, "json_rpc_error")
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) HandleJsonRpcMethod(context HttpContext, config PluginConfig, body []byte, handles MethodHandlers[PluginConfig]) types.Action {
|
||||
id := gjson.GetBytes(body, "id").Int()
|
||||
ctx.userContext[CtxJsonRpcID] = id
|
||||
method := gjson.GetBytes(body, "method").String()
|
||||
params := gjson.GetBytes(body, "params")
|
||||
if handle, ok := handles[method]; ok {
|
||||
log.Debugf("json rpc call id[%d] method[%s] with params[%s]", id, method, params.Raw)
|
||||
err := handle(context, config, id, params)
|
||||
if err != nil {
|
||||
ctx.OnJsonRpcResponseError(err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
// Waiting for the response
|
||||
return types.ActionPause
|
||||
}
|
||||
ctx.OnJsonRpcResponseError(fmt.Errorf("method not found:%s", method), ErrMethodNotFound)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) HandleJsonRpcRequest(context HttpContext, config PluginConfig, body []byte, handle JsonRpcRequestHandler[PluginConfig]) types.Action {
|
||||
id := gjson.GetBytes(body, "id").Int()
|
||||
ctx.userContext[CtxJsonRpcID] = id
|
||||
method := gjson.GetBytes(body, "method").String()
|
||||
params := gjson.GetBytes(body, "params")
|
||||
log.Debugf("json rpc call id[%d] method[%s] with params[%s]", id, method, params.Raw)
|
||||
return handle(context, config, id, method, params)
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) HandleJsonRpcResponse(context HttpContext, config PluginConfig, body []byte, handle JsonRpcResponseHandler[PluginConfig]) types.Action {
|
||||
id := gjson.GetBytes(body, "id").Int()
|
||||
error := gjson.GetBytes(body, "error")
|
||||
result := gjson.GetBytes(body, "result")
|
||||
log.Debugf("json rpc response id[%d] error[%s] result[%s]", id, error.Raw, result.Raw)
|
||||
return handle(context, config, id, result, error)
|
||||
}
|
||||
221
plugins/wasm-go/pkg/wrapper/mcp_wrapper.go
Normal file
221
plugins/wasm-go/pkg/wrapper/mcp_wrapper.go
Normal file
@@ -0,0 +1,221 @@
|
||||
// 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 wrapper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type MCPTool[PluginConfig any] interface {
|
||||
Create(params []byte) MCPTool[PluginConfig]
|
||||
Call(context HttpContext, config PluginConfig) error
|
||||
Description() string
|
||||
InputSchema() map[string]any
|
||||
}
|
||||
|
||||
type MCPTools[PluginConfig any] map[string]MCPTool[PluginConfig]
|
||||
|
||||
type addMCPToolOption[PluginConfig any] struct {
|
||||
name string
|
||||
tool MCPTool[PluginConfig]
|
||||
}
|
||||
|
||||
func AddMCPTool[PluginConfig any](name string, tool MCPTool[PluginConfig]) CtxOption[PluginConfig] {
|
||||
return &addMCPToolOption[PluginConfig]{
|
||||
name: name,
|
||||
tool: tool,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *addMCPToolOption[PluginConfig]) Apply(ctx *CommonVmCtx[PluginConfig]) {
|
||||
ctx.isJsonRpcSever = true
|
||||
ctx.handleJsonRpcMethod = true
|
||||
if _, exist := ctx.mcpTools[o.name]; exist {
|
||||
panic(fmt.Sprintf("Conflict! There is a tool with the same name:%s",
|
||||
o.name))
|
||||
}
|
||||
ctx.mcpTools[o.name] = o.tool
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) OnMCPResponseSuccess(result map[string]any) {
|
||||
ctx.OnJsonRpcResponseSuccess(result)
|
||||
// TODO: support pub to redis when use POST + SSE
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) OnMCPResponseError(err error, code ...int) {
|
||||
ctx.OnJsonRpcResponseError(err, code...)
|
||||
// TODO: support pub to redis when use POST + SSE
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) OnMCPToolCallSuccess(content []map[string]any) {
|
||||
ctx.OnMCPResponseSuccess(map[string]any{
|
||||
"content": content,
|
||||
"isError": false,
|
||||
})
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) OnMCPToolCallError(err error) {
|
||||
ctx.OnMCPResponseSuccess(map[string]any{
|
||||
"content": []map[string]any{
|
||||
{
|
||||
"type": "text",
|
||||
"text": err.Error(),
|
||||
},
|
||||
},
|
||||
"isError": true,
|
||||
})
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) SendMCPToolTextResult(result string) {
|
||||
ctx.OnMCPToolCallSuccess([]map[string]any{
|
||||
{
|
||||
"type": "text",
|
||||
"text": result,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) registerMCPTools(mcpTools MCPTools[PluginConfig]) {
|
||||
if !ctx.plugin.vm.isJsonRpcSever {
|
||||
return
|
||||
}
|
||||
if !ctx.plugin.vm.handleJsonRpcMethod {
|
||||
return
|
||||
}
|
||||
ctx.plugin.vm.jsonRpcMethodHandlers["tools/list"] = func(context HttpContext, config PluginConfig, id int64, params gjson.Result) error {
|
||||
var tools []map[string]any
|
||||
for name, tool := range mcpTools {
|
||||
tools = append(tools, map[string]any{
|
||||
"name": name,
|
||||
"description": tool.Description(),
|
||||
"inputSchema": tool.InputSchema(),
|
||||
})
|
||||
}
|
||||
ctx.OnMCPResponseSuccess(map[string]any{
|
||||
"tools": tools,
|
||||
"nextCursor": "",
|
||||
})
|
||||
return nil
|
||||
}
|
||||
ctx.plugin.vm.jsonRpcMethodHandlers["tools/call"] = func(context HttpContext, config PluginConfig, id int64, params gjson.Result) error {
|
||||
name := params.Get("name").String()
|
||||
args := params.Get("arguments")
|
||||
if tool, ok := mcpTools[name]; ok {
|
||||
log.Debugf("mcp call tool[%s] with arguments[%s]", name, args.Raw)
|
||||
toolInstance := tool.Create([]byte(args.Raw))
|
||||
err := toolInstance.Call(context, config)
|
||||
// TODO: validate the json schema through github.com/kaptinlin/jsonschema
|
||||
if err != nil {
|
||||
ctx.OnMCPToolCallError(err)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
ctx.OnMCPResponseError(errors.New("Unknown tool: invalid_tool_name"), ErrInvalidParams)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type mcpToolRequestFunc[PluginConfig any] func(context HttpContext, config PluginConfig, toolName string, toolArgs gjson.Result) types.Action
|
||||
type mcpToolResponseFunc[PluginConfig any] func(context HttpContext, config PluginConfig, isError bool, content gjson.Result) types.Action
|
||||
type jsonRpcErrorFunc[PluginConfig any] func(context HttpContext, config PluginConfig, errorCode int64, errorMessage string) types.Action
|
||||
|
||||
type mcpToolRequestOption[PluginConfig any] struct {
|
||||
f mcpToolRequestFunc[PluginConfig]
|
||||
}
|
||||
|
||||
func OnMCPToolRequest[PluginConfig any](f mcpToolRequestFunc[PluginConfig]) CtxOption[PluginConfig] {
|
||||
return &mcpToolRequestOption[PluginConfig]{f}
|
||||
}
|
||||
|
||||
func (o *mcpToolRequestOption[PluginConfig]) Apply(ctx *CommonVmCtx[PluginConfig]) {
|
||||
ctx.isJsonRpcSever = true
|
||||
ctx.onMcpToolRequest = o.f
|
||||
}
|
||||
|
||||
type mcpToolResponseOption[PluginConfig any] struct {
|
||||
f mcpToolResponseFunc[PluginConfig]
|
||||
}
|
||||
|
||||
func OnMCPToolResponse[PluginConfig any](f mcpToolResponseFunc[PluginConfig]) CtxOption[PluginConfig] {
|
||||
return &mcpToolResponseOption[PluginConfig]{f}
|
||||
}
|
||||
|
||||
func (o *mcpToolResponseOption[PluginConfig]) Apply(ctx *CommonVmCtx[PluginConfig]) {
|
||||
ctx.isJsonRpcSever = true
|
||||
ctx.onMcpToolResponse = o.f
|
||||
}
|
||||
|
||||
type jsonRpcErrorOption[PluginConfig any] struct {
|
||||
f jsonRpcErrorFunc[PluginConfig]
|
||||
}
|
||||
|
||||
func OnJsonRpcError[PluginConfig any](f jsonRpcErrorFunc[PluginConfig]) CtxOption[PluginConfig] {
|
||||
return &jsonRpcErrorOption[PluginConfig]{f}
|
||||
}
|
||||
|
||||
func (o *jsonRpcErrorOption[PluginConfig]) Apply(ctx *CommonVmCtx[PluginConfig]) {
|
||||
ctx.isJsonRpcSever = true
|
||||
ctx.onJsonRpcError = o.f
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) registerMCPToolProcessor() {
|
||||
if !ctx.plugin.vm.isJsonRpcSever {
|
||||
return
|
||||
}
|
||||
if ctx.plugin.vm.handleJsonRpcMethod {
|
||||
return
|
||||
}
|
||||
if ctx.plugin.vm.onMcpToolRequest != nil {
|
||||
ctx.plugin.vm.jsonRpcRequestHandler = func(context HttpContext, config PluginConfig, id int64, method string, params gjson.Result) types.Action {
|
||||
toolName := params.Get("name").String()
|
||||
toolArgs := params.Get("arguments")
|
||||
return ctx.plugin.vm.onMcpToolRequest(context, config, toolName, toolArgs)
|
||||
}
|
||||
}
|
||||
if ctx.plugin.vm.onMcpToolResponse != nil {
|
||||
ctx.plugin.vm.jsonRpcResponseHandler = func(context HttpContext, config PluginConfig, id int64, result, error gjson.Result) types.Action {
|
||||
if result.Exists() {
|
||||
isError := result.Get("isError").Bool()
|
||||
content := result.Get("content")
|
||||
return ctx.plugin.vm.onMcpToolResponse(context, config, isError, content)
|
||||
}
|
||||
if error.Exists() && ctx.plugin.vm.onJsonRpcError != nil {
|
||||
return ctx.plugin.vm.onJsonRpcError(context, config, error.Get("code").Int(), error.Get("message").String())
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ToInputSchema(v any) map[string]any {
|
||||
t := reflect.TypeOf(v)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
inputSchema := jsonschema.Reflect(v).Definitions[t.Name()]
|
||||
inputSchemaBytes, _ := json.Marshal(inputSchema)
|
||||
var result map[string]any
|
||||
json.Unmarshal(inputSchemaBytes, &result)
|
||||
return result
|
||||
}
|
||||
@@ -70,6 +70,11 @@ type HttpContext interface {
|
||||
SetRequestBodyBufferLimit(byteSize uint32)
|
||||
// Note that this parameter affects the gateway's memory usage! Support setting a maximum buffer size for each response body individually in response phase.
|
||||
SetResponseBodyBufferLimit(byteSize uint32)
|
||||
// Make a request to the target service of the current route using the specified URL and header.
|
||||
RouteCall(method, url string, headers [][2]string, body []byte, callback ResponseCallback, timeoutMillisecond ...uint32) error
|
||||
OnMCPToolCallSuccess(content []map[string]any)
|
||||
OnMCPToolCallError(err error)
|
||||
SendMCPToolTextResult(result string)
|
||||
}
|
||||
|
||||
type oldParseConfigFunc[PluginConfig any] func(json gjson.Result, config *PluginConfig, log log.Log) error
|
||||
@@ -100,6 +105,15 @@ type CommonVmCtx[PluginConfig any] struct {
|
||||
onHttpResponseBody onHttpBodyFunc[PluginConfig]
|
||||
onHttpStreamingResponseBody onHttpStreamingBodyFunc[PluginConfig]
|
||||
onHttpStreamDone onHttpStreamDoneFunc[PluginConfig]
|
||||
isJsonRpcSever bool
|
||||
handleJsonRpcMethod bool
|
||||
jsonRpcMethodHandlers MethodHandlers[PluginConfig]
|
||||
mcpTools MCPTools[PluginConfig]
|
||||
onMcpToolRequest mcpToolRequestFunc[PluginConfig]
|
||||
onMcpToolResponse mcpToolResponseFunc[PluginConfig]
|
||||
onJsonRpcError jsonRpcErrorFunc[PluginConfig]
|
||||
jsonRpcRequestHandler JsonRpcRequestHandler[PluginConfig]
|
||||
jsonRpcResponseHandler JsonRpcResponseHandler[PluginConfig]
|
||||
}
|
||||
|
||||
type TickFuncEntry struct {
|
||||
@@ -393,8 +407,10 @@ func NewCommonVmCtx[PluginConfig any](pluginName string, options ...CtxOption[Pl
|
||||
|
||||
func NewCommonVmCtxWithOptions[PluginConfig any](pluginName string, options ...CtxOption[PluginConfig]) *CommonVmCtx[PluginConfig] {
|
||||
ctx := &CommonVmCtx[PluginConfig]{
|
||||
pluginName: pluginName,
|
||||
hasCustomConfig: true,
|
||||
pluginName: pluginName,
|
||||
hasCustomConfig: true,
|
||||
jsonRpcMethodHandlers: make(MethodHandlers[PluginConfig]),
|
||||
mcpTools: make(MCPTools[PluginConfig]),
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt.Apply(ctx)
|
||||
@@ -403,7 +419,6 @@ func NewCommonVmCtxWithOptions[PluginConfig any](pluginName string, options ...C
|
||||
var config PluginConfig
|
||||
if unsafe.Sizeof(config) != 0 {
|
||||
msg := "the `parseConfig` is missing in NewCommonVmCtx's arguments"
|
||||
ctx.log.Critical(msg)
|
||||
panic(msg)
|
||||
}
|
||||
ctx.hasCustomConfig = false
|
||||
@@ -495,10 +510,12 @@ func (ctx *CommonPluginCtx[PluginConfig]) NewHttpContext(contextID uint32) types
|
||||
userContext: map[string]interface{}{},
|
||||
userAttribute: map[string]interface{}{},
|
||||
}
|
||||
if ctx.vm.onHttpRequestBody != nil || ctx.vm.onHttpStreamingRequestBody != nil {
|
||||
httpCtx.registerMCPTools(ctx.vm.mcpTools)
|
||||
httpCtx.registerMCPToolProcessor()
|
||||
if ctx.vm.onHttpRequestBody != nil || ctx.vm.onHttpStreamingRequestBody != nil || len(ctx.vm.jsonRpcMethodHandlers) > 0 || ctx.vm.jsonRpcRequestHandler != nil {
|
||||
httpCtx.needRequestBody = true
|
||||
}
|
||||
if ctx.vm.onHttpResponseBody != nil || ctx.vm.onHttpStreamingResponseBody != nil {
|
||||
if ctx.vm.onHttpResponseBody != nil || ctx.vm.onHttpStreamingResponseBody != nil || ctx.vm.jsonRpcResponseHandler != nil {
|
||||
httpCtx.needResponseBody = true
|
||||
}
|
||||
if ctx.vm.onHttpStreamingRequestBody != nil {
|
||||
@@ -507,7 +524,6 @@ func (ctx *CommonPluginCtx[PluginConfig]) NewHttpContext(contextID uint32) types
|
||||
if ctx.vm.onHttpStreamingResponseBody != nil {
|
||||
httpCtx.streamingResponseBody = true
|
||||
}
|
||||
|
||||
return httpCtx
|
||||
}
|
||||
|
||||
@@ -524,6 +540,18 @@ type CommonHttpCtx[PluginConfig any] struct {
|
||||
contextID uint32
|
||||
userContext map[string]interface{}
|
||||
userAttribute map[string]interface{}
|
||||
pendingCall int
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) HttpCallStart(uint32) {
|
||||
ctx.pendingCall++
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) HttpCallEnd(uint32) {
|
||||
if ctx.pendingCall == 0 {
|
||||
return
|
||||
}
|
||||
ctx.pendingCall--
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) SetContext(key string, value interface{}) {
|
||||
@@ -599,6 +627,13 @@ func (ctx *CommonHttpCtx[PluginConfig]) WriteUserAttributeToTrace() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) GetIntContext(key string, defaultValue int) int {
|
||||
if b, ok := ctx.userContext[key].(int); ok {
|
||||
return b
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) GetBoolContext(key string, defaultValue bool) bool {
|
||||
if b, ok := ctx.userContext[key].(bool); ok {
|
||||
return b
|
||||
@@ -686,6 +721,9 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestHeaders(numHeaders int, end
|
||||
if IsBinaryRequestBody() {
|
||||
ctx.needRequestBody = false
|
||||
}
|
||||
if ctx.plugin.vm.isJsonRpcSever && HasRequestBody() {
|
||||
return types.HeaderStopIteration
|
||||
}
|
||||
if ctx.plugin.vm.onHttpRequestHeaders == nil {
|
||||
return types.ActionContinue
|
||||
}
|
||||
@@ -709,7 +747,9 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestBody(bodySize int, endOfStr
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
if ctx.plugin.vm.onHttpRequestBody != nil {
|
||||
if ctx.plugin.vm.onHttpRequestBody != nil ||
|
||||
len(ctx.plugin.vm.jsonRpcMethodHandlers) > 0 ||
|
||||
ctx.plugin.vm.jsonRpcRequestHandler != nil {
|
||||
ctx.requestBodySize += bodySize
|
||||
if !endOfStream {
|
||||
return types.ActionPause
|
||||
@@ -719,7 +759,14 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestBody(bodySize int, endOfStr
|
||||
ctx.plugin.vm.log.Warnf("get request body failed: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
return ctx.plugin.vm.onHttpRequestBody(ctx, *ctx.config, body)
|
||||
if ctx.plugin.vm.onHttpRequestBody != nil {
|
||||
return ctx.plugin.vm.onHttpRequestBody(ctx, *ctx.config, body)
|
||||
}
|
||||
if len(ctx.plugin.vm.jsonRpcMethodHandlers) > 0 {
|
||||
return ctx.HandleJsonRpcMethod(ctx, *ctx.config, body, ctx.plugin.vm.jsonRpcMethodHandlers)
|
||||
}
|
||||
// ctx.plugin.vm.jsonRpcRequestHandler not nil
|
||||
return ctx.HandleJsonRpcRequest(ctx, *ctx.config, body, ctx.plugin.vm.jsonRpcRequestHandler)
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
@@ -755,7 +802,7 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseBody(bodySize int, endOfSt
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
if ctx.plugin.vm.onHttpResponseBody != nil {
|
||||
if ctx.plugin.vm.onHttpResponseBody != nil || ctx.plugin.vm.jsonRpcResponseHandler != nil {
|
||||
ctx.responseBodySize += bodySize
|
||||
if !endOfStream {
|
||||
return types.ActionPause
|
||||
@@ -765,7 +812,11 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseBody(bodySize int, endOfSt
|
||||
ctx.plugin.vm.log.Warnf("get response body failed: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
return ctx.plugin.vm.onHttpResponseBody(ctx, *ctx.config, body)
|
||||
if ctx.plugin.vm.onHttpResponseBody != nil {
|
||||
return ctx.plugin.vm.onHttpResponseBody(ctx, *ctx.config, body)
|
||||
}
|
||||
// ctx.plugin.vm.jsonRpcResponseHandler not nil
|
||||
return ctx.HandleJsonRpcResponse(ctx, *ctx.config, body, ctx.plugin.vm.jsonRpcResponseHandler)
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
@@ -779,3 +830,15 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpStreamDone() {
|
||||
}
|
||||
ctx.plugin.vm.onHttpStreamDone(ctx, *ctx.config)
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) RouteCall(method, url string, headers [][2]string, body []byte, callback ResponseCallback, timeoutMillisecond ...uint32) error {
|
||||
// Since the HttpCall here is a substitute for route invocation, the default timeout is slightly longer, at 1 minute.
|
||||
var timeout uint32 = 60000
|
||||
if len(timeoutMillisecond) > 0 {
|
||||
timeout = timeoutMillisecond[0]
|
||||
}
|
||||
cluster := RouteCluster{
|
||||
BaseCluster: BaseCluster{notify: ctx},
|
||||
}
|
||||
return HttpCall(cluster, method, url, headers, body, callback, timeout)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user