Add remote mcp server sdk (#1946)

This commit is contained in:
澄潭
2025-03-24 22:11:45 +08:00
committed by GitHub
parent f5d20b72e0
commit d9f16f7d5e
17 changed files with 1337 additions and 13 deletions

View File

@@ -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
)

View File

@@ -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=

View 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

View 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})"

View 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")
}
}
```

View 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")
}
}

View 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
)

View 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=

View 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{}),
)
}

View 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)
}

View 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")))
})
}

View File

@@ -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")
}
}

View File

@@ -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

View File

@@ -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
}

View 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)
}

View 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
}

View File

@@ -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)
}