This is an automated email from the ASF dual-hosted git repository.
zhangwenfeng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/dubbo-go-pixiu-samples.git
The following commit(s) were added to refs/heads/main by this push:
new 8aa37e1 feat: add Mcp server with nacos sample (#97)
8aa37e1 is described below
commit 8aa37e12e98249506deae915c65b577c731b0b84
Author: Zerui Yang <[email protected]>
AuthorDate: Thu Oct 30 15:06:02 2025 +0800
feat: add Mcp server with nacos sample (#97)
* feat: Enhance MCP Samples Documentation and Add Nacos Integration
* fix: add license
* feat: add MCP Nacos integration example in README files
* fix: update configuration path in MCP Nacos test file
---
README.md | 1 +
README_CN.md | 1 +
mcp/README.md | 12 +-
mcp/README_zh.md | 3 +-
mcp/nacos/README.md | 69 ++++++
mcp/nacos/README_zh.md | 69 ++++++
mcp/nacos/mcptools/mcptools.yaml | 159 ++++++++++++
mcp/nacos/pixiu/conf.yaml | 61 +++++
mcp/nacos/test/mcp_nacos_test.go | 439 ++++++++++++++++++++++++++++++++++
mcp/simple/README.md | 2 +-
mcp/simple/README_zh.md | 2 +-
mcp/simple/server/{ => app}/server.go | 0
12 files changed, 810 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index 9f5aa7e..2eefb71 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ samples for
[dubbo-go-pixiu](https://github.com/apache/dubbo-go-pixiu)
- mcp: demonstrates MCP (Model Context Protocol) filter to expose HTTP APIs as
LLM tools
- mcp/simple: basic MCP service integration example showing how to convert
HTTP APIs to MCP tools
- mcp/oauth: MCP OAuth authorization example demonstrating how to protect
MCP endpoints with OAuth2, supporting PKCE authorization code flow
+ - mcp/nacos: MCP Nacos integration example, demonstrating how to use Nacos
as the MCP Server.
- plugins: this directory contains some plugins for pixiu
- plugins/ratelimit: rate limit plugin for pixiu
diff --git a/README_CN.md b/README_CN.md
index 98fa666..0968e37 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -39,6 +39,7 @@
- mcp: 演示 MCP (Model Context Protocol) 过滤器,将 HTTP API 暴露为 LLM 工具
- mcp/simple: 基础的 MCP 服务集成示例,展示如何将 HTTP API 转换为 MCP 工具
- mcp/oauth: MCP OAuth 授权示例,演示如何使用 OAuth2 保护 MCP 端点,支持 PKCE 授权码流程
+ - mcp/nacos: MCP Nacos 集成示例,演示如何使用 Nacos 作为 MCP Server 的注册中心和配置中心
- plugins:此目录包含 pixiu 的一些插件
- plugins/ratelimit:pixiu 的 ratelimit 插件
diff --git a/mcp/README.md b/mcp/README.md
index 6ce0f4f..464402a 100644
--- a/mcp/README.md
+++ b/mcp/README.md
@@ -1,12 +1,14 @@
# MCP Samples Index
-English | [中文](./README_zh.md)
+[中文](./README_zh.md) | English
-This directory contains two MCP samples:
+This directory contains several MCP examples:
-- `simple/`: MCP without authorization
-- `oauth/`: MCP with authorization enabled (OAuth), protecting only `/mcp`,
using remote JWKS
+- `simple/`: A basic example demonstrating MCP interaction.
+- `oauth/`: A security example showing how to protect the MCP endpoint with
OAuth 2.0 and a remote JWKS.
+- `nacos/`: An integration example demonstrating how to use Nacos as the MCP
Server for dynamic configuration.
+
+Please navigate to each subdirectory and read the corresponding README for
detailed instructions.
-Please navigate into each subfolder and read the README for detailed usage.
diff --git a/mcp/README_zh.md b/mcp/README_zh.md
index 2c4193f..2d75478 100644
--- a/mcp/README_zh.md
+++ b/mcp/README_zh.md
@@ -4,7 +4,8 @@
本目录下包含两类 MCP 示例:
-- `simple/`:无鉴权示例
+- `simple/`:基础示例
- `oauth/`:启用 MCP 授权(OAuth)示例,仅保护 `/mcp`,远程 JWKS
+- `nacos/`:使用 Nacos 作为 MCP Server 的注册中心和配置中心的示例
请进入各子目录阅读对应的 README 获取详细使用方法。
\ No newline at end of file
diff --git a/mcp/nacos/README.md b/mcp/nacos/README.md
new file mode 100644
index 0000000..a1bfb86
--- /dev/null
+++ b/mcp/nacos/README.md
@@ -0,0 +1,69 @@
+# Dubbo-go-pixiu MCP with Nacos Example
+
+English | [中文](./README_zh.md)
+
+This example demonstrates how to use Nacos 3.0+ as the MCP (Model
Configuration Protocol) server for the Dubbo-go-pixiu gateway.
+
+## Prerequisites
+
+- Go programming environment
+- Nacos 3.0 or higher installed and running
+
+## Steps
+
+### 1. Configure Nacos MCP Service
+
+First, we need to configure and publish an MCP service in the Nacos console.
+
+1. **Login to Nacos Console**: Access `http://<nacos-server-ip>:8080/nacos`.
+2. **Navigate to MCP Management**: Find and click on "MCP Management" in the
left sidebar.
+3. **Create MCP Server**:
+ * Click "MCP List" -> "Create MCP Server".
+ * **Type**: Select `streamable`.
+ * **Tools**: Select "Import from OpenAPI", then upload the
`mcp/nacos/mcptools/mcptools.yaml` file.
+4. **Correct Backend Address (Important)**:
+ * After a successful upload, Nacos will automatically parse
`mcptools.yaml` and generate a list of tools.
+ * **Note**: There is a known issue in Nacos 3.0 where the imported
backend address `http://` might be incorrectly changed to `http:/`. Please
manually check and correct the backend address for all tools to ensure it is
`http://localhost:8081`.
+5. **Publish the Service**:
+ * After confirming all configurations are correct, click "Publish" to
start the MCP Server.
+ * *Note: Pixiu currently only supports connecting to a single MCP
Server.*
+
+### 2. Start the Mock Backend Server
+
+This server provides the API interfaces defined in the OpenAPI file.
+
+```bash
+cd mcp/simple/server/app
+go run .
+```
+
+After starting successfully, you will see output similar to the following:
+
+```
+🚀 Mock Backend Server starting on :8081
+📚 Available endpoints:
+ GET /api/users/{id} - Get user by ID
+ GET /api/users/search - Search users
+ POST /api/users - Create user
+ GET /api/users/{id}/posts - Get user posts
+ GET /api/health - Health check
+ GET / - Root endpoint
+```
+*Please execute the next step in a new terminal window, keeping this server
running.*
+
+### 3. Start the Pixiu Gateway
+
+Now, start the Pixiu gateway, which will connect to the Nacos MCP service and
route requests based on the fetched configuration.
+
+```shell
+cd /path/to/dubbo-go-pixiu
+go run cmd/pixiu/*.go gateway start -c
/path/to/dubbo-go-pixiu-samples/mcp/nacos/pixiu/conf.yaml
+```
+
+### 4. Install and Launch the MCP Inspector Client
+
+```shell
+npx @modelcontextprotocol/inspector
+```
+
+Open the Inspector interface in your browser and connect to
`http://localhost:8888/mcp` to start testing.
diff --git a/mcp/nacos/README_zh.md b/mcp/nacos/README_zh.md
new file mode 100644
index 0000000..136eb45
--- /dev/null
+++ b/mcp/nacos/README_zh.md
@@ -0,0 +1,69 @@
+# Dubbo-go-pixiu MCP 使用 Nacos 作为注册中心示例
+
+[English](./README.md) | 中文
+
+本示例演示了如何使用 Nacos 3.0+ 作为 Dubbo-go-pixiu 网关的 MCP(模型配置协议)服务器。
+
+## 准备工作
+
+- Go 编程环境
+- 已安装并启动 Nacos 3.0 或更高版本
+
+## 步骤
+
+### 1. 配置 Nacos MCP 服务
+
+首先,我们需要在 Nacos 控制台中配置和发布一个 MCP 服务。
+
+1. **登录 Nacos 控制台**:访问 `http://<nacos-server-ip>:8080/nacos`。
+2. **进入 MCP 管理**:在左侧菜单栏找到并点击 “MCP管理”。
+3. **创建 MCP Server**:
+ * 点击 “MCP列表” -> “创建MCP Server”。
+ * **类型**:选择 `streamable`。
+ * **工具(Tools)**:选择 “从OpenAPI导入”,然后上传 `mcp/nacos/mcptools/mcptools.yaml`
文件。
+4. **修正后端地址(重要)**:
+ * 上传成功后,Nacos 会自动解析 `mcptools.yaml` 并生成工具列表。
+ * **注意**:Nacos 3.0 版本存在一个已知问题,导入的后端地址 `http://` 可能会错误地变成
`http:/`。请手动检查并修正所有工具的后端地址,确保其为 `http://localhost:8081`。
+5. **发布服务**:
+ * 确认所有配置无误后,点击 “发布”,启动 MCP Server。
+ * *注意:目前 Pixiu 只支持连接单个 MCP Server。*
+
+### 2. 启动后端模拟服务器
+
+此服务器提供了 OpenAPI 文件中定义的 API 接口。
+
+```bash
+cd mcp/simple/server/app
+go run .
+```
+
+启动成功后,你将看到类似以下的输出:
+
+```
+🚀 Mock Backend Server starting on :8081
+📚 Available endpoints:
+ GET /api/users/{id} - Get user by ID
+ GET /api/users/search - Search users
+ POST /api/users - Create user
+ GET /api/users/{id}/posts - Get user posts
+ GET /api/health - Health check
+ GET / - Root endpoint
+```
+*请在新的终端窗口执行下一步,保持此服务器运行。*
+
+### 3. 启动 Pixiu 网关
+
+现在,启动 Pixiu 网关,它将连接到 Nacos MCP 服务并根据获取的配置进行路由。
+
+```shell
+cd /path/to/dubbo-go-pixiu
+go run cmd/pixiu/*.go gateway start -c
/path/to/dubbo-go-pixiu-samples/mcp/nacos/pixiu/conf.yaml
+```
+
+### 4. 安装并启动 MCP Inspector Client
+
+```shell
+npx @modelcontextprotocol/inspector
+```
+
+在浏览器中打开 Inspector 界面,连接到 `http://localhost:8888/mcp` 便可以进行测试。
\ No newline at end of file
diff --git a/mcp/nacos/mcptools/mcptools.yaml b/mcp/nacos/mcptools/mcptools.yaml
new file mode 100644
index 0000000..e103951
--- /dev/null
+++ b/mcp/nacos/mcptools/mcptools.yaml
@@ -0,0 +1,159 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+openapi: 3.0.0
+info:
+ title: User API
+ description: 一个用于管理用户和帖子的模拟 API。
+ version: 1.0.0
+servers:
+ - url: http://localhost:8081
+ description: 模拟服务器
+
+paths:
+ /api/users/{id}:
+ get:
+ summary: "get_user"
+ description: "通过 ID 获取用户信息,可选择是否包含个人资料详情"
+ operationId: "getUserById"
+ parameters:
+ - name: id
+ in: path
+ description: "要检索的用户 ID"
+ required: true
+ schema:
+ type: integer
+ - name: include_profile
+ in: query
+ description: "是否包含用户个人资料信息"
+ required: false
+ schema:
+ type: boolean
+ default: false
+ responses:
+ '200':
+ description: "成功获取用户信息"
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ id:
+ type: integer
+ name:
+ type: string
+ email:
+ type: string
+
+ /api/users/search:
+ get:
+ summary: "search_users"
+ description: "通过姓名或邮箱搜索用户,支持分页"
+ operationId: "searchUsers"
+ parameters:
+ - name: q
+ in: query
+ description: "搜索查询(姓名或邮箱)"
+ required: true
+ schema:
+ type: string
+ - name: page
+ in: query
+ description: "分页的页码"
+ required: false
+ schema:
+ type: integer
+ default: 1
+ - name: limit
+ in: query
+ description: "每页的结果数量 (1-100)"
+ required: false
+ schema:
+ type: integer
+ default: 10
+ responses:
+ '200':
+ description: "成功的搜索结果"
+
+ /api/users:
+ post:
+ summary: "create_user"
+ description: "创建一个新的用户账户"
+ operationId: "createUser"
+ requestBody:
+ description: "新用户的数据"
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - name
+ - email
+ properties:
+ name:
+ type: string
+ description: "用户的全名"
+ email:
+ type: string
+ description: "用户的电子邮件地址"
+ age:
+ type: integer
+ description: "用户的年龄"
+ responses:
+ '201':
+ description: "用户创建成功"
+
+ /api/users/{user_id}/posts:
+ get:
+ summary: "get_user_posts"
+ description: "获取特定用户的所有帖子,并按状态筛选"
+ operationId: "getUserPosts"
+ parameters:
+ - name: user_id
+ in: path
+ description: "要获取帖子的用户 ID"
+ required: true
+ schema:
+ type: integer
+ - name: status
+ in: query
+ description: "按状态筛选帖子"
+ required: false
+ schema:
+ type: string
+ default: "published"
+ enum: ["published", "draft", "all"]
+ responses:
+ '200':
+ description: "成功获取帖子列表"
+
+ /api/health:
+ get:
+ summary: "health_check"
+ description: "检查服务器服务的健康状况和状态"
+ operationId: "healthCheck"
+ responses:
+ '200':
+ description: "服务健康"
+
+ /:
+ get:
+ summary: "get_server_info"
+ description: "获取基本服务器信息和可用的端点"
+ operationId: "getServerInfo"
+ responses:
+ '200':
+ description: "服务器信息"
\ No newline at end of file
diff --git a/mcp/nacos/pixiu/conf.yaml b/mcp/nacos/pixiu/conf.yaml
new file mode 100644
index 0000000..890d7de
--- /dev/null
+++ b/mcp/nacos/pixiu/conf.yaml
@@ -0,0 +1,61 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+static_resources:
+ listeners:
+ - name: "net/http"
+ protocol_type: "HTTP"
+ address:
+ socket_address:
+ address: "0.0.0.0"
+ port: 8888
+ filter_chains:
+ filters:
+ - name: "dgp.filter.httpconnectionmanager"
+ config:
+ route_config:
+ routes:
+ # Protected MCP endpoint
+ - match:
+ prefix: "/"
+ route:
+ cluster: "mcp-protected"
+ cluster_not_found_response_code: 505
+ http_filters:
+ - name: "dgp.filter.mcp.mcpserver"
+ config:
+ server_info:
+ name: "MCP OAuth Sample Server"
+ version: "1.0.0"
+ description: "MCP Server protected by OAuth for tools
demonstration"
+ instructions: "Use read/write tokens to interact with
the mock server API via MCP"
+
+ # Downstream HTTP proxy
+ - name: "dgp.filter.http.httpproxy"
+
+ adapters:
+ - id: test
+ name: dgp.adapter.mcpserver
+ config:
+ registries:
+ nacos:
+ protocol: nacos
+ address: "127.0.0.1:8848"
+ timeout: "5s"
+ username: "nacos"
+ password: "nacos"
+
+
+
diff --git a/mcp/nacos/test/mcp_nacos_test.go b/mcp/nacos/test/mcp_nacos_test.go
new file mode 100644
index 0000000..6b330e4
--- /dev/null
+++ b/mcp/nacos/test/mcp_nacos_test.go
@@ -0,0 +1,439 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+// Basic tests for the MCP tools (no authorization). These tests exercise the
+// JSON-RPC initialize flow, tools discovery, and typical tool calls against
the
+// mock backend. They are intended to validate the sample configuration in
+// mcp/nacos/pixiu/conf.yaml.
+package test
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "testing"
+ "time"
+)
+
+const (
+ // Test service addresses
+ pixiuURL = "http://localhost:8888"
+ mcpEndpoint = "/mcp"
+ backendURL = "http://localhost:8081"
+)
+
+// JSONRPCRequest represents a minimal JSON-RPC 2.0 request envelope.
+type JSONRPCRequest struct {
+ JSONRPC string `json:"jsonrpc"`
+ ID any `json:"id"`
+ Method string `json:"method"`
+ Params any `json:"params,omitempty"`
+}
+
+// JSONRPCResponse represents a minimal JSON-RPC 2.0 response envelope.
+type JSONRPCResponse struct {
+ JSONRPC string `json:"jsonrpc"`
+ ID any `json:"id"`
+ Result any `json:"result,omitempty"`
+ Error any `json:"error,omitempty"`
+}
+
+// ToolCallParams represents parameters for tools/call requests.
+type ToolCallParams struct {
+ Name string `json:"name"`
+ Arguments map[string]any `json:"arguments"`
+}
+
+// sendJSONRPCRequest posts a JSON-RPC request to /mcp and returns the decoded
+// response. It fails the test on transport or protocol errors.
+func sendJSONRPCRequest(t *testing.T, method string, params any)
*JSONRPCResponse {
+ req := JSONRPCRequest{
+ JSONRPC: "2.0",
+ ID: 1,
+ Method: method,
+ Params: params,
+ }
+
+ reqBody, err := json.Marshal(req)
+ if err != nil {
+ t.Fatalf("Failed to marshal request: %v", err)
+ }
+
+ resp, err := http.Post(pixiuURL+mcpEndpoint, "application/json",
bytes.NewBuffer(reqBody))
+ if err != nil {
+ t.Fatalf("Failed to send request: %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("Expected status 200, got %d", resp.StatusCode)
+ }
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatalf("Failed to read response: %v", err)
+ }
+
+ var jsonResp JSONRPCResponse
+ if err := json.Unmarshal(body, &jsonResp); err != nil {
+ t.Fatalf("Failed to unmarshal response: %v", err)
+ }
+
+ if jsonResp.Error != nil {
+ t.Fatalf("JSON-RPC error: %v", jsonResp.Error)
+ }
+
+ return &jsonResp
+}
+
+// checkServiceAvailable performs a quick HTTP GET and returns true if 200 OK.
+func checkServiceAvailable(url string) bool {
+ client := &http.Client{Timeout: 5 * time.Second}
+ resp, err := client.Get(url)
+ if err != nil {
+ return false
+ }
+ defer resp.Body.Close()
+ return resp.StatusCode == http.StatusOK
+}
+
+// TestServiceAvailability tests service availability
+func TestServiceAvailability(t *testing.T) {
+ t.Log("Checking backend service availability...")
+ if !checkServiceAvailable(backendURL) {
+ t.Skip("Backend service is not available, skipping test. Please
start backend service first: go run mcp/server/server.go")
+ }
+
+ t.Log("Checking Pixiu gateway availability...")
+ if !checkServiceAvailable(pixiuURL) {
+ t.Skip("Pixiu gateway is not available, skipping test. Please
start Pixiu gateway first")
+ }
+}
+
+// TestMCPInitialize tests MCP initialization
+func TestMCPInitialize(t *testing.T) {
+ if !checkServiceAvailable(pixiuURL) {
+ t.Skip("Pixiu gateway is not available")
+ }
+
+ t.Log("Testing MCP initialization...")
+
+ params := map[string]any{
+ "protocolVersion": "2024-11-05",
+ "capabilities": map[string]any{
+ "roots": map[string]any{
+ "listChanged": true,
+ },
+ },
+ "clientInfo": map[string]any{
+ "name": "test-client",
+ "version": "1.0.0",
+ },
+ }
+
+ resp := sendJSONRPCRequest(t, "initialize", params)
+
+ // Validate response structure
+ result, ok := resp.Result.(map[string]any)
+ if !ok {
+ t.Fatalf("Expected result to be an object")
+ }
+
+ // Check server info
+ serverInfo, ok := result["serverInfo"].(map[string]any)
+ if !ok {
+ t.Fatalf("Expected serverInfo in result")
+ }
+
+ if name, ok := serverInfo["name"].(string); !ok || name == "" {
+ t.Fatalf("Expected server name")
+ }
+
+ t.Logf("MCP server initialized successfully: %s", serverInfo["name"])
+}
+
+// TestToolsList tests getting tools list
+func TestToolsList(t *testing.T) {
+ if !checkServiceAvailable(pixiuURL) {
+ t.Skip("Pixiu gateway is not available")
+ }
+
+ t.Log("Testing tools list...")
+
+ resp := sendJSONRPCRequest(t, "tools/list", nil)
+
+ result, ok := resp.Result.(map[string]any)
+ if !ok {
+ t.Fatalf("Expected result to be an object")
+ }
+
+ tools, ok := result["tools"].([]any)
+ if !ok {
+ t.Fatalf("Expected tools array in result")
+ }
+
+ if len(tools) == 0 {
+ t.Fatalf("Expected at least one tool")
+ }
+
+ t.Logf("Found %d tools", len(tools))
+
+ // Validate tool structure
+ for i, tool := range tools {
+ toolObj, ok := tool.(map[string]any)
+ if !ok {
+ t.Fatalf("Tool %d is not an object", i)
+ }
+
+ if name, ok := toolObj["name"].(string); !ok || name == "" {
+ t.Fatalf("Tool %d missing name", i)
+ }
+
+ if desc, ok := toolObj["description"].(string); !ok || desc ==
"" {
+ t.Fatalf("Tool %d missing description", i)
+ }
+
+ t.Logf("Tool %d: %s - %s", i+1, toolObj["name"],
toolObj["description"])
+ }
+}
+
+// TestGetUser tests get user tool
+func TestGetUser(t *testing.T) {
+ if !checkServiceAvailable(pixiuURL) ||
!checkServiceAvailable(backendURL) {
+ t.Skip("Services are not available")
+ }
+
+ t.Log("Testing get user tool...")
+
+ params := ToolCallParams{
+ Name: "getUserById",
+ Arguments: map[string]any{
+ "id": 1,
+ "include_profile": true,
+ },
+ }
+
+ resp := sendJSONRPCRequest(t, "tools/call", params)
+
+ result, ok := resp.Result.(map[string]any)
+ if !ok {
+ t.Fatalf("Expected result to be an object")
+ }
+
+ content, ok := result["content"].([]any)
+ if !ok || len(content) == 0 {
+ t.Fatalf("Expected content array in result")
+ }
+
+ contentItem, ok := content[0].(map[string]any)
+ if !ok {
+ t.Fatalf("Expected content item to be an object")
+ }
+
+ text, ok := contentItem["text"].(string)
+ if !ok {
+ t.Fatalf("Expected text in content item")
+ }
+
+ // Parse returned user data
+ var userData map[string]any
+ if err := json.Unmarshal([]byte(text), &userData); err != nil {
+ t.Fatalf("Failed to parse user data: %v", err)
+ }
+
+ // Validate user data
+ if id, ok := userData["id"].(float64); !ok || id != 1 {
+ t.Fatalf("Expected user ID to be 1")
+ }
+
+ if name, ok := userData["name"].(string); !ok || name == "" {
+ t.Fatalf("Expected user name")
+ }
+
+ t.Logf("Successfully got user: %s", userData["name"])
+}
+
+// TestSearchUsers tests search users tool
+func TestSearchUsers(t *testing.T) {
+ if !checkServiceAvailable(pixiuURL) ||
!checkServiceAvailable(backendURL) {
+ t.Skip("Services are not available")
+ }
+
+ t.Log("Testing search users tool...")
+
+ params := ToolCallParams{
+ Name: "searchUsers",
+ Arguments: map[string]any{
+ "q": "alice",
+ "page": 1,
+ "limit": 10,
+ },
+ }
+
+ resp := sendJSONRPCRequest(t, "tools/call", params)
+
+ result, ok := resp.Result.(map[string]any)
+ if !ok {
+ t.Fatalf("Expected result to be an object")
+ }
+
+ content, ok := result["content"].([]any)
+ if !ok || len(content) == 0 {
+ t.Fatalf("Expected content array in result")
+ }
+
+ contentItem, ok := content[0].(map[string]any)
+ if !ok {
+ t.Fatalf("Expected content item to be an object")
+ }
+
+ text, ok := contentItem["text"].(string)
+ if !ok {
+ t.Fatalf("Expected text in content item")
+ }
+
+ // Parse search result
+ var searchResult map[string]any
+ if err := json.Unmarshal([]byte(text), &searchResult); err != nil {
+ t.Fatalf("Failed to parse search result: %v", err)
+ }
+
+ // Validate search result
+ users, ok := searchResult["users"].([]any)
+ if !ok {
+ t.Fatalf("Expected users array in search result")
+ }
+
+ total, ok := searchResult["total"].(float64)
+ if !ok {
+ t.Fatalf("Expected total in search result")
+ }
+
+ t.Logf("Found %d users, total %.0f", len(users), total)
+}
+
+// TestCreateUser tests create user tool
+func TestCreateUser(t *testing.T) {
+ if !checkServiceAvailable(pixiuURL) ||
!checkServiceAvailable(backendURL) {
+ t.Skip("Services are not available")
+ }
+
+ t.Log("Testing create user tool...")
+
+ // Use timestamp to ensure unique email
+ timestamp := time.Now().Unix()
+ email := fmt.Sprintf("[email protected]", timestamp)
+
+ params := ToolCallParams{
+ Name: "createUser",
+ Arguments: map[string]any{
+ "name": "Test User",
+ "email": email,
+ "age": 25,
+ },
+ }
+
+ resp := sendJSONRPCRequest(t, "tools/call", params)
+
+ result, ok := resp.Result.(map[string]any)
+ if !ok {
+ t.Fatalf("Expected result to be an object")
+ }
+
+ content, ok := result["content"].([]any)
+ if !ok || len(content) == 0 {
+ t.Fatalf("Expected content array in result")
+ }
+
+ contentItem, ok := content[0].(map[string]any)
+ if !ok {
+ t.Fatalf("Expected content item to be an object")
+ }
+
+ text, ok := contentItem["text"].(string)
+ if !ok {
+ t.Fatalf("Expected text in content item")
+ }
+
+ // Parse created user data
+ var userData map[string]any
+ if err := json.Unmarshal([]byte(text), &userData); err != nil {
+ t.Fatalf("Failed to parse user data: %v", err)
+ }
+
+ // Validate created user data
+ if name, ok := userData["name"].(string); !ok || name != "Test User" {
+ t.Fatalf("Expected user name to be 'Test User'")
+ }
+
+ if userEmail, ok := userData["email"].(string); !ok || userEmail !=
email {
+ t.Fatalf("Expected user email to be '%s'", email)
+ }
+
+ t.Logf("Successfully created user: %s (%s)", userData["name"],
userData["email"])
+}
+
+// TestHealthCheck tests health check tool
+func TestHealthCheck(t *testing.T) {
+ if !checkServiceAvailable(pixiuURL) ||
!checkServiceAvailable(backendURL) {
+ t.Skip("Services are not available")
+ }
+
+ t.Log("Testing health check tool...")
+
+ params := ToolCallParams{
+ Name: "healthCheck",
+ Arguments: map[string]any{},
+ }
+
+ resp := sendJSONRPCRequest(t, "tools/call", params)
+
+ result, ok := resp.Result.(map[string]any)
+ if !ok {
+ t.Fatalf("Expected result to be an object")
+ }
+
+ content, ok := result["content"].([]any)
+ if !ok || len(content) == 0 {
+ t.Fatalf("Expected content array in result")
+ }
+
+ contentItem, ok := content[0].(map[string]any)
+ if !ok {
+ t.Fatalf("Expected content item to be an object")
+ }
+
+ text, ok := contentItem["text"].(string)
+ if !ok {
+ t.Fatalf("Expected text in content item")
+ }
+
+ // Parse health check result
+ var healthData map[string]any
+ if err := json.Unmarshal([]byte(text), &healthData); err != nil {
+ t.Fatalf("Failed to parse health data: %v", err)
+ }
+
+ // Validate health check result
+ if status, ok := healthData["status"].(string); !ok || status !=
"healthy" {
+ t.Fatalf("Expected status to be 'healthy'")
+ }
+
+ t.Logf("Health check passed: %s", healthData["status"])
+}
diff --git a/mcp/simple/README.md b/mcp/simple/README.md
index a9fcc3b..a7f577a 100644
--- a/mcp/simple/README.md
+++ b/mcp/simple/README.md
@@ -13,7 +13,7 @@ MCP is a protocol designed for LLMs to interact with external
tools, implemented
### 1. Start Backend Service
```shell
-cd mcp/simple/server
+cd mcp/simple/server/app
go run server.go
```
diff --git a/mcp/simple/README_zh.md b/mcp/simple/README_zh.md
index 2255bfe..b492687 100644
--- a/mcp/simple/README_zh.md
+++ b/mcp/simple/README_zh.md
@@ -13,7 +13,7 @@ MCP 是一种专为 LLM 与外部工具交互而设计的协议,基于 JSON-RP
### 1. 启动后端服务
```shell
-cd mcp/simple/server
+cd mcp/simple/server/app
go run server.go
```
diff --git a/mcp/simple/server/server.go b/mcp/simple/server/app/server.go
similarity index 100%
rename from mcp/simple/server/server.go
rename to mcp/simple/server/app/server.go