// 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 ( "encoding/json" "strings" "testing" "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/proxytest" "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types" "github.com/higress-group/wasm-go/pkg/test" "github.com/stretchr/testify/require" ) // REST MCP服务器配置 var restMCPServerConfig = func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "server": map[string]interface{}{ "name": "rest-test-server", "type": "rest", }, "tools": []map[string]interface{}{ { "name": "get_weather", "description": "获取天气信息", "args": []map[string]interface{}{ { "name": "location", "description": "城市名称", "type": "string", "required": true, }, }, "requestTemplate": map[string]interface{}{ "url": "https://httpbin.org/get?city={{.location}}", "method": "GET", }, }, }, }) return data }() // MCP代理服务器配置 var mcpProxyServerConfig = func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "server": map[string]interface{}{ "name": "proxy-test-server", "type": "mcp-proxy", "transport": "http", "mcpServerURL": "http://backend-mcp.example.com/mcp", "timeout": 5000, }, "tools": []map[string]interface{}{ { "name": "get_product", "description": "获取产品信息", "args": []map[string]interface{}{ { "name": "product_id", "description": "产品ID", "type": "string", "required": true, }, }, }, }, }) return data }() // MCP代理服务器带认证配置 var mcpProxyServerWithAuthConfig = func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "server": map[string]interface{}{ "name": "proxy-auth-test-server", "type": "mcp-proxy", "transport": "http", "mcpServerURL": "http://backend-mcp.example.com/mcp", "timeout": 5000, "defaultUpstreamSecurity": map[string]interface{}{ "id": "BackendApiKey", }, "securitySchemes": []map[string]interface{}{ { "id": "BackendApiKey", "type": "apiKey", "in": "header", "name": "X-API-Key", "defaultCredential": "test-default-key", }, }, }, "tools": []map[string]interface{}{ { "name": "get_secure_product", "description": "获取安全产品信息", "args": []map[string]interface{}{ { "name": "product_id", "description": "产品ID", "type": "string", "required": true, }, }, "requestTemplate": map[string]interface{}{ "security": map[string]interface{}{ "id": "BackendApiKey", }, }, }, }, }) return data }() // 内置天气MCP服务器配置 var weatherMCPServerConfig = func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "server": map[string]interface{}{ "name": "weather-test-server", "config": map[string]interface{}{ "apiKey": "test-api-key", "baseUrl": "https://api.openweathermap.org/data/2.5", }, }, }) return data }() // TestRestMCPServerConfig 测试REST MCP服务器配置解析 func TestRestMCPServerConfig(t *testing.T) { test.RunTest(t, func(t *testing.T) { t.Run("valid rest mcp server config", func(t *testing.T) { host, status := test.NewTestHost(restMCPServerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 对于配置解析测试,主要验证插件启动状态 // GetMatchConfig在WASM模式下可能有限制,我们主要关注启动成功 }) }) } // TestMcpProxyServerConfig 测试MCP代理服务器配置解析 func TestMcpProxyServerConfig(t *testing.T) { test.RunTest(t, func(t *testing.T) { t.Run("valid mcp proxy server config", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 对于配置解析测试,主要验证插件启动状态 // GetMatchConfig在WASM模式下可能有限制,我们主要关注启动成功 }) }) } // TestRestMCPServerBasicFlow 测试REST MCP服务器基本流程 func TestRestMCPServerBasicFlow(t *testing.T) { test.RunTest(t, func(t *testing.T) { t.Run("tools/list request", func(t *testing.T) { host, status := test.NewTestHost(restMCPServerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsListRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }` // 初始化HTTP上下文 host.InitHttp() // 处理请求头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) // 处理请求体 action = host.CallOnHttpRequestBody([]byte(toolsListRequest)) require.Equal(t, types.ActionContinue, action) // 验证响应 localResponse := host.GetLocalResponse() if localResponse != nil && len(localResponse.Data) > 0 { var response map[string]interface{} err := json.Unmarshal(localResponse.Data, &response) require.NoError(t, err) // 验证JSON-RPC格式 require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(1), response["id"]) // 验证tools列表存在 result, ok := response["result"].(map[string]interface{}) require.True(t, ok) tools, ok := result["tools"].([]interface{}) require.True(t, ok) require.Greater(t, len(tools), 0) // 验证第一个工具 tool, ok := tools[0].(map[string]interface{}) require.True(t, ok) require.Equal(t, "get_weather", tool["name"]) require.Equal(t, "获取天气信息", tool["description"]) } host.CompleteHttp() }) }) } // TestRestMCPServerToolsCall 测试REST MCP服务器的tools/call功能 func TestRestMCPServerToolsCall(t *testing.T) { test.RunTest(t, func(t *testing.T) { host, status := test.NewTestHost(restMCPServerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsCallRequest := `{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "get_weather", "arguments": { "location": "北京" } } }` // 初始化HTTP上下文 host.InitHttp() // 处理请求头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) // 处理请求体 - 这会触发外部HTTP调用 action = host.CallOnHttpRequestBody([]byte(toolsCallRequest)) require.Equal(t, types.ActionContinue, action) // Mock HTTP响应头 action = host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"Content-Type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) // 处理外部API响应体 externalAPIResponse := `{ "args": {"city": "北京"}, "url": "https://httpbin.org/get?city=北京", "headers": { "Host": "httpbin.org" } }` action = host.CallOnHttpResponseBody([]byte(externalAPIResponse)) require.Equal(t, types.ActionContinue, action) // 验证最终MCP响应 responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) // 验证JSON-RPC格式 require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(2), response["id"]) // 验证结果存在(REST MCP server会将外部API响应包装为MCP格式) result, ok := response["result"].(map[string]interface{}) require.True(t, ok) content, ok := result["content"].([]interface{}) require.True(t, ok) require.Greater(t, len(content), 0) host.CompleteHttp() }) } // TestMcpProxyServerToolsList 测试MCP代理服务器的tools/list功能 func TestMcpProxyServerToolsList(t *testing.T) { test.RunTest(t, func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsListRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }` // 初始化HTTP上下文 host.InitHttp() // 处理请求头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) // 处理请求体 - 这会触发MCP初始化流程 action = host.CallOnHttpRequestBody([]byte(toolsListRequest)) require.Equal(t, types.ActionPause, action) // 应该暂停等待后端响应 // Mock MCP初始化阶段的HTTP调用响应 // 第一步:Initialize请求的响应 initResponse := `{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-03-26", "capabilities": { "tools": {} }, "serverInfo": { "name": "BackendMCPServer", "version": "1.0.0" } } }` // Mock initialize响应(带session ID) host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "test-session-123"}, }, []byte(initResponse)) // 第二步:notifications/initialized请求的响应 notificationResponse := `{"jsonrpc": "2.0"}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "test-session-123"}, }, []byte(notificationResponse)) // 第三步:实际的tools/list请求的响应(这是executeToolsList中ctx.RouteCall的响应) toolsListResponse := `{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "get_product", "description": "获取产品信息", "inputSchema": { "type": "object", "properties": { "product_id": { "type": "string", "description": "产品ID" } }, "required": ["product_id"] } } ] } }` // 这是对executeToolsList中ctx.RouteCall的响应 host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }) host.CallOnHttpResponseBody([]byte(toolsListResponse)) // 验证最终MCP响应 responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) // 验证JSON-RPC格式 require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(1), response["id"]) // 验证代理转发的结果 result, ok := response["result"].(map[string]interface{}) require.True(t, ok) tools, ok := result["tools"].([]interface{}) require.True(t, ok) require.Greater(t, len(tools), 0) // 验证工具信息 tool, ok := tools[0].(map[string]interface{}) require.True(t, ok) require.Equal(t, "get_product", tool["name"]) require.Equal(t, "获取产品信息", tool["description"]) host.CompleteHttp() }) } // TestMcpProxyServerToolsCall 测试MCP代理服务器的tools/call功能 func TestMcpProxyServerToolsCall(t *testing.T) { test.RunTest(t, func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsCallRequest := `{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "get_product", "arguments": { "product_id": "12345" } } }` // 初始化HTTP上下文 host.InitHttp() // 处理请求头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) // 处理请求体 - 这会触发MCP初始化流程和工具调用 action = host.CallOnHttpRequestBody([]byte(toolsCallRequest)) require.Equal(t, types.ActionPause, action) // 应该暂停等待后端响应 // Mock MCP初始化阶段的HTTP调用响应 // 第一步:Initialize请求的响应 initResponse := `{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-03-26", "capabilities": { "tools": {} }, "serverInfo": { "name": "BackendMCPServer", "version": "1.0.0" } } }` // Mock initialize响应(带session ID) host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "test-session-456"}, }, []byte(initResponse)) // 第二步:notifications/initialized请求的响应 notificationResponse := `{"jsonrpc": "2.0"}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "test-session-456"}, }, []byte(notificationResponse)) // 第三步:实际的tools/call请求的响应 toolsCallResponse := `{ "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "Product ID: 12345\nName: Sample Product\nPrice: $99.99\nDescription: This is a sample product for testing" } ], "isError": false } }` // 这是对executeToolsCall中ctx.RouteCall的响应 host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }) host.CallOnHttpResponseBody([]byte(toolsCallResponse)) // 验证最终MCP响应 responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) // 验证JSON-RPC格式 require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(3), response["id"]) // 验证代理转发的结果 result, ok := response["result"].(map[string]interface{}) require.True(t, ok) content, ok := result["content"].([]interface{}) require.True(t, ok) require.Greater(t, len(content), 0) // 验证内容 textContent, ok := content[0].(map[string]interface{}) require.True(t, ok) require.Equal(t, "text", textContent["type"]) require.Contains(t, textContent["text"], "Product ID: 12345") // 验证isError字段 require.Equal(t, false, result["isError"]) host.CompleteHttp() }) } // TestMcpProxyServerAuthentication 测试MCP代理服务器认证功能 func TestMcpProxyServerAuthentication(t *testing.T) { test.RunTest(t, func(t *testing.T) { // 测试tools/list请求的认证头 t.Run("tools/list authentication headers", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerWithAuthConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsListRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }` // 初始化HTTP上下文 host.InitHttp() // 处理请求头(带用户API Key) action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, {"x-api-key", "user-provided-key"}, // 用户提供的API Key }) require.Equal(t, types.HeaderStopIteration, action) // 处理请求体 action = host.CallOnHttpRequestBody([]byte(toolsListRequest)) require.Equal(t, types.ActionPause, action) // Mock MCP初始化响应 initResponse := `{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-03-26", "capabilities": { "tools": {} }, "serverInfo": { "name": "SecureBackendMCPServer", "version": "1.0.0" } } }` // 验证初始化请求的认证头 host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "secure-session-list-123"}, }, []byte(initResponse)) // 验证初始化成功(从日志中可以确认发送了正确的认证头 [X-API-Key test-default-key]) // 实际的HTTP请求包含了正确的默认凭据用于上游认证 // Mock notifications/initialized响应 host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "secure-session-list-123"}, }, []byte(`{"jsonrpc": "2.0"}`)) // Mock tools/list响应 toolsListResponse := `{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "get_secure_product", "description": "获取安全产品信息", "inputSchema": { "type": "object", "properties": { "product_id": { "type": "string", "description": "产品ID" } }, "required": ["product_id"] } } ] } }` // 验证tools/list请求的认证头(在响应处理前获取发送给后端的请求头) requestHeaders := host.GetRequestHeaders() pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path") require.True(t, hasPath, "Path header should exist") require.Contains(t, pathValue, "/mcp", "Path should be MCP endpoint") apiKeyValue, hasApiKey := test.GetHeaderValue(requestHeaders, "x-api-key") require.True(t, hasApiKey, "X-API-Key header should exist in tools/list request") require.Equal(t, "test-default-key", apiKeyValue, "Should use default credential for tools/list") // 这是对executeToolsList中ctx.RouteCall的响应 host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }) host.CallOnHttpResponseBody([]byte(toolsListResponse)) // 验证响应 responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) // 验证响应格式 require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(1), response["id"]) result, ok := response["result"].(map[string]interface{}) require.True(t, ok) tools, ok := result["tools"].([]interface{}) require.True(t, ok) require.Greater(t, len(tools), 0) host.CompleteHttp() }) // 测试tools/call请求的认证头 t.Run("tools/call authentication headers", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerWithAuthConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsCallRequest := `{ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "get_secure_product", "arguments": { "product_id": "secure-123" } } }` // 初始化HTTP上下文 host.InitHttp() // 处理请求头(带用户API Key) action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, {"x-api-key", "user-provided-key"}, // 用户提供的API Key }) require.Equal(t, types.HeaderStopIteration, action) // 处理请求体 action = host.CallOnHttpRequestBody([]byte(toolsCallRequest)) require.Equal(t, types.ActionPause, action) // Mock MCP初始化响应 initResponse := `{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-03-26", "capabilities": { "tools": {} }, "serverInfo": { "name": "SecureBackendMCPServer", "version": "1.0.0" } } }` // 验证初始化请求的认证头 host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "secure-session-call-456"}, }, []byte(initResponse)) // 验证初始化成功(从日志中可以确认发送了正确的认证头 [X-API-Key test-default-key]) // 实际的HTTP请求包含了正确的默认凭据用于上游认证 // Mock notifications/initialized响应 host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "secure-session-call-456"}, }, []byte(`{"jsonrpc": "2.0"}`)) // Mock工具调用响应 secureToolResponse := `{ "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "Secure Product ID: secure-123\nName: Confidential Product\nAccess Level: Premium" } ], "isError": false } }` // 验证tools/call请求的认证头(在响应处理前获取发送给后端的请求头) requestHeaders := host.GetRequestHeaders() pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path") require.True(t, hasPath, "Path header should exist") require.Contains(t, pathValue, "/mcp", "Path should be MCP endpoint") apiKeyValue, hasApiKey := test.GetHeaderValue(requestHeaders, "x-api-key") require.True(t, hasApiKey, "X-API-Key header should exist in tools/call request") require.Equal(t, "test-default-key", apiKeyValue, "Should use default credential for tools/call") // 这是对executeToolsCall中ctx.RouteCall的响应 host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }) host.CallOnHttpResponseBody([]byte(secureToolResponse)) // 验证响应 responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) // 验证响应格式 require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(4), response["id"]) result, ok := response["result"].(map[string]interface{}) require.True(t, ok) content, ok := result["content"].([]interface{}) require.True(t, ok) textContent, ok := content[0].(map[string]interface{}) require.True(t, ok) require.Contains(t, textContent["text"], "Secure Product ID: secure-123") host.CompleteHttp() }) }) } // TestMcpProxyServerErrorHandling 测试MCP代理服务器错误处理 func TestMcpProxyServerErrorHandling(t *testing.T) { test.RunTest(t, func(t *testing.T) { // 测试协议版本不匹配 t.Run("protocol version mismatch", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsListRequest := `{ "jsonrpc": "2.0", "id": 5, "method": "tools/list", "params": {} }` // 初始化HTTP上下文 host.InitHttp() // 处理请求头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) // 处理请求体 action = host.CallOnHttpRequestBody([]byte(toolsListRequest)) require.Equal(t, types.ActionPause, action) // Mock协议版本不匹配的错误响应 versionErrorResponse := `{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "Unsupported protocol version", "data": { "supported": ["2024-11-05"], "requested": "2025-03-26" } } }` host.CallOnHttpCall([][2]string{ {":status", "400"}, {"content-type", "application/json"}, }, []byte(versionErrorResponse)) // 验证错误响应 localResponse := host.GetLocalResponse() if localResponse != nil && len(localResponse.Data) > 0 { var response map[string]interface{} err := json.Unmarshal(localResponse.Data, &response) require.NoError(t, err) // 验证错误被正确包装 require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(5), response["id"]) errorField, ok := response["error"].(map[string]interface{}) require.True(t, ok) require.Contains(t, errorField["message"], "backend") } host.CompleteHttp() }) // 测试后端服务器超时 t.Run("backend timeout", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsCallRequest := `{ "jsonrpc": "2.0", "id": 6, "method": "tools/call", "params": { "name": "get_product", "arguments": { "product_id": "timeout-test" } } }` // 初始化HTTP上下文 host.InitHttp() // 处理请求头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) // 处理请求体 action = host.CallOnHttpRequestBody([]byte(toolsCallRequest)) require.Equal(t, types.ActionPause, action) // Mock超时错误 - 不提供响应,模拟超时 // 在实际实现中,这会触发超时处理逻辑 host.CompleteHttp() }) // 测试后端工具执行错误 t.Run("backend tool error", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsCallRequest := `{ "jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": { "name": "get_product", "arguments": { "product_id": "error-test" } } }` // 初始化HTTP上下文 host.InitHttp() // 处理请求头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) // 处理请求体 action = host.CallOnHttpRequestBody([]byte(toolsCallRequest)) require.Equal(t, types.ActionPause, action) // Mock正常的初始化流程 initResponse := `{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-03-26", "capabilities": {"tools": {}}, "serverInfo": {"name": "TestServer", "version": "1.0.0"} } }` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "error-session-001"}, }, []byte(initResponse)) host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "error-session-001"}, }, []byte(`{"jsonrpc": "2.0"}`)) // Mock工具执行错误响应 toolErrorResponse := `{ "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "Failed to fetch product: Database connection error" } ], "isError": true } }` // 这是对executeToolsCall中ctx.RouteCall的响应 host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }) host.CallOnHttpResponseBody([]byte(toolErrorResponse)) // 验证错误被正确传播 responseBody := host.GetResponseBody() if len(responseBody) > 0 { var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) // 验证响应格式 require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(7), response["id"]) result, ok := response["result"].(map[string]interface{}) require.True(t, ok) require.Equal(t, true, result["isError"]) content, ok := result["content"].([]interface{}) require.True(t, ok) textContent, ok := content[0].(map[string]interface{}) require.True(t, ok) require.Contains(t, textContent["text"], "Database connection error") } host.CompleteHttp() }) }) } // TestMcpProxyServerSessionManagement 测试MCP代理服务器会话管理 func TestMcpProxyServerSessionManagement(t *testing.T) { test.RunTest(t, func(t *testing.T) { // 测试单个HTTP Context内的会话状态管理 t.Run("context session state", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 在同一个HTTP Context中,先进行tools/list然后tools/call // 这将测试Context内的CtxMcpProxyInitialized状态管理 toolsListRequest := `{ "jsonrpc": "2.0", "id": 8, "method": "tools/list", "params": {} }` // 初始化HTTP上下文 host.InitHttp() // 处理第一个请求(tools/list) action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) action = host.CallOnHttpRequestBody([]byte(toolsListRequest)) require.Equal(t, types.ActionPause, action) // Mock初始化过程(第一次应该触发初始化) initResponse := `{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-03-26", "capabilities": {"tools": {}}, "serverInfo": {"name": "TestServer", "version": "1.0.0"} } }` sessionID := "context-session-999" host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", sessionID}, }, []byte(initResponse)) host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", sessionID}, }, []byte(`{"jsonrpc": "2.0"}`)) // Mock tools/list响应 toolsListResponse := `{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "get_product", "description": "获取产品信息", "inputSchema": { "type": "object", "properties": { "product_id": {"type": "string", "description": "产品ID"} }, "required": ["product_id"] } } ] } }` // 这是对executeToolsList中ctx.RouteCall的响应 host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }) host.CallOnHttpResponseBody([]byte(toolsListResponse)) // 验证tools/list响应 responseBody := host.GetResponseBody() if len(responseBody) > 0 { var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(8), response["id"]) } host.CompleteHttp() }) // 测试每个新HTTP请求都需要重新初始化(符合实际实现) t.Run("new request requires new initialization", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 第一个独立的HTTP请求 toolsCallRequest1 := `{ "jsonrpc": "2.0", "id": 9, "method": "tools/call", "params": { "name": "get_product", "arguments": { "product_id": "request-1" } } }` host.InitHttp() action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) action = host.CallOnHttpRequestBody([]byte(toolsCallRequest1)) require.Equal(t, types.ActionPause, action) // Mock第一个请求的完整初始化流程 initResponse := `{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-03-26", "capabilities": {"tools": {}}, "serverInfo": {"name": "TestServer", "version": "1.0.0"} } }` sessionID1 := "session-request-1" host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", sessionID1}, }, []byte(initResponse)) host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", sessionID1}, }, []byte(`{"jsonrpc": "2.0"}`)) // Mock tools/call响应 toolsCallResponse1 := `{ "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "Product from request 1: request-1" } ], "isError": false } }` // 这是对executeToolsCall中ctx.RouteCall的响应 host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }) host.CallOnHttpResponseBody([]byte(toolsCallResponse1)) // 验证第一个请求的响应 responseBody := host.GetResponseBody() if len(responseBody) > 0 { var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(9), response["id"]) } host.CompleteHttp() // 第二个独立的HTTP请求(新的Context,需要重新初始化) toolsCallRequest2 := `{ "jsonrpc": "2.0", "id": 10, "method": "tools/call", "params": { "name": "get_product", "arguments": { "product_id": "request-2" } } }` // 新的HTTP Context host.InitHttp() action = host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) action = host.CallOnHttpRequestBody([]byte(toolsCallRequest2)) require.Equal(t, types.ActionPause, action) // Mock第二个请求的完整初始化流程(每个新Context都需要重新初始化) sessionID2 := "session-request-2" host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", sessionID2}, // 不同的session ID }, []byte(initResponse)) host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", sessionID2}, }, []byte(`{"jsonrpc": "2.0"}`)) toolsCallResponse2 := `{ "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "Product from request 2: request-2" } ], "isError": false } }` // 这是对executeToolsCall中ctx.RouteCall的响应 host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }) host.CallOnHttpResponseBody([]byte(toolsCallResponse2)) // 验证第二个请求的响应 responseBody = host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(10), response["id"]) result, ok := response["result"].(map[string]interface{}) require.True(t, ok) content, ok := result["content"].([]interface{}) require.True(t, ok) textContent, ok := content[0].(map[string]interface{}) require.True(t, ok) require.Contains(t, textContent["text"], "request-2") host.CompleteHttp() }) // 测试初始化失败处理 t.Run("initialization failure", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsCallRequest := `{ "jsonrpc": "2.0", "id": 11, "method": "tools/call", "params": { "name": "get_product", "arguments": { "product_id": "init-failure-test" } } }` host.InitHttp() action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) action = host.CallOnHttpRequestBody([]byte(toolsCallRequest)) require.Equal(t, types.ActionPause, action) // Mock初始化失败响应 initErrorResponse := `{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32001, "message": "Backend server unavailable" } }` host.CallOnHttpCall([][2]string{ {":status", "500"}, {"content-type", "application/json"}, }, []byte(initErrorResponse)) // 验证错误被正确处理 localResponse := host.GetLocalResponse() if localResponse != nil && len(localResponse.Data) > 0 { var response map[string]interface{} err := json.Unmarshal(localResponse.Data, &response) require.NoError(t, err) // 验证错误响应格式 require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(11), response["id"]) errorField, ok := response["error"].(map[string]interface{}) require.True(t, ok) require.Contains(t, errorField["message"], "backend") } host.CompleteHttp() }) }) } // BenchmarkRestMCPServer 性能基准测试 func BenchmarkRestMCPServer(b *testing.B) { host, status := test.NewTestHost(restMCPServerConfig) defer host.Reset() require.Equal(b, types.OnPluginStartStatusOK, status) toolsListRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }` b.ResetTimer() for i := 0; i < b.N; i++ { host.InitHttp() host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) host.CallOnHttpRequestBody([]byte(toolsListRequest)) host.CompleteHttp() } } // TestMcpProxyServerAllowTools 测试MCP代理服务器allowTools功能 func TestMcpProxyServerAllowTools(t *testing.T) { // 创建包含allowTools配置的测试配置 mcpProxyServerWithAllowToolsConfig := func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "server": map[string]interface{}{ "name": "proxy-allow-tools-server", "type": "mcp-proxy", "transport": "http", "mcpServerURL": "http://backend-mcp.example.com/mcp", "timeout": 5000, }, "allowTools": []string{"get_product", "create_order"}, // 只允许这两个工具 "tools": []map[string]interface{}{ { "name": "get_product", "type": "mcp-proxy", "description": "Get product information", }, { "name": "create_order", "type": "mcp-proxy", "description": "Create a new order", }, { "name": "delete_user", "type": "mcp-proxy", "description": "Delete a user account", }, }, }) return data }() test.RunTest(t, func(t *testing.T) { // 测试配置级别的allowTools过滤 t.Run("config level allowTools filtering", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerWithAllowToolsConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsListRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }` host.InitHttp() action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) action = host.CallOnHttpRequestBody([]byte(toolsListRequest)) require.Equal(t, types.ActionPause, action) // 应该暂停等待异步响应 // Mock MCP initialization sequence host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "test-session-123"}, }, []byte(`{ "jsonrpc": "2.0", "id": "init-1", "result": { "capabilities": { "tools": {"listChanged": true} }, "protocolVersion": "2024-11-05" } }`)) host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "test-session-123"}, }, []byte(`{ "jsonrpc": "2.0", "id": "notify-1", "result": {} }`)) // Mock tools/list response with 3 tools (但只有2个会被返回) // 这是对executeToolsList中ctx.RouteCall的响应 host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }) host.CallOnHttpResponseBody([]byte(`{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "get_product", "description": "Get product information", "inputSchema": {"type": "object"} }, { "name": "create_order", "description": "Create a new order", "inputSchema": {"type": "object"} }, { "name": "delete_user", "description": "Delete a user account", "inputSchema": {"type": "object"} } ] } }`)) host.CompleteHttp() // 验证响应只包含允许的工具 responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) result, hasResult := response["result"] require.True(t, hasResult) resultMap := result.(map[string]interface{}) tools, hasTools := resultMap["tools"] require.True(t, hasTools) toolsArray := tools.([]interface{}) // 应该只返回2个允许的工具,delete_user被过滤掉 require.Len(t, toolsArray, 2) toolNames := make([]string, 0) for _, tool := range toolsArray { toolMap := tool.(map[string]interface{}) toolNames = append(toolNames, toolMap["name"].(string)) } require.Contains(t, toolNames, "get_product") require.Contains(t, toolNames, "create_order") require.NotContains(t, toolNames, "delete_user") }) // 测试请求头级别的allowTools过滤 t.Run("header level allowTools filtering", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerConfig) // 使用没有allowTools配置的基本配置 defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsListRequest := `{ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} }` host.InitHttp() // 设置请求头只允许get_product工具 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, {"x-envoy-allow-mcp-tools", "get_product"}, }) require.Equal(t, types.HeaderStopIteration, action) action = host.CallOnHttpRequestBody([]byte(toolsListRequest)) require.Equal(t, types.ActionPause, action) // Mock MCP initialization sequence host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "test-session-456"}, }, []byte(`{ "jsonrpc": "2.0", "id": "init-2", "result": { "capabilities": { "tools": {"listChanged": true} }, "protocolVersion": "2024-11-05" } }`)) host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "test-session-456"}, }, []byte(`{ "jsonrpc": "2.0", "id": "notify-2", "result": {} }`)) // Mock tools/list response with multiple tools // 这是对executeToolsList中ctx.RouteCall的响应 host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }) host.CallOnHttpResponseBody([]byte(`{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "get_product", "description": "Get product information", "inputSchema": {"type": "object"} }, { "name": "create_order", "description": "Create a new order", "inputSchema": {"type": "object"} } ] } }`)) host.CompleteHttp() // 验证响应只包含请求头中允许的工具 responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) result, hasResult := response["result"] require.True(t, hasResult) resultMap := result.(map[string]interface{}) tools, hasTools := resultMap["tools"] require.True(t, hasTools) toolsArray := tools.([]interface{}) // 应该只返回1个工具(get_product) require.Len(t, toolsArray, 1) toolMap := toolsArray[0].(map[string]interface{}) require.Equal(t, "get_product", toolMap["name"]) }) // 测试配置和请求头都存在时的组合过滤 t.Run("combined config and header allowTools filtering", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerWithAllowToolsConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsListRequest := `{ "jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {} }` host.InitHttp() // 配置允许:get_product, create_order // 请求头允许:get_product, delete_user // 交集应该只有:get_product action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, {"x-envoy-allow-mcp-tools", "get_product,delete_user"}, }) require.Equal(t, types.HeaderStopIteration, action) action = host.CallOnHttpRequestBody([]byte(toolsListRequest)) require.Equal(t, types.ActionPause, action) // Mock MCP initialization sequence host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "test-session-789"}, }, []byte(`{ "jsonrpc": "2.0", "id": "init-3", "result": { "capabilities": { "tools": {"listChanged": true} }, "protocolVersion": "2024-11-05" } }`)) host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "test-session-789"}, }, []byte(`{ "jsonrpc": "2.0", "id": "notify-3", "result": {} }`)) // Mock tools/list response // 这是对executeToolsList中ctx.RouteCall的响应 host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }) host.CallOnHttpResponseBody([]byte(`{ "jsonrpc": "2.0", "id": 3, "result": { "tools": [ { "name": "get_product", "description": "Get product information", "inputSchema": {"type": "object"} }, { "name": "create_order", "description": "Create a new order", "inputSchema": {"type": "object"} }, { "name": "delete_user", "description": "Delete a user account", "inputSchema": {"type": "object"} } ] } }`)) host.CompleteHttp() // 验证响应只包含交集中的工具 responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) result, hasResult := response["result"] require.True(t, hasResult) resultMap := result.(map[string]interface{}) tools, hasTools := resultMap["tools"] require.True(t, hasTools) toolsArray := tools.([]interface{}) // 应该只返回1个工具(get_product),因为它是唯一在两个allowTools列表中的工具 require.Len(t, toolsArray, 1) toolMap := toolsArray[0].(map[string]interface{}) require.Equal(t, "get_product", toolMap["name"]) }) // 测试空白的请求头allowTools t.Run("empty header allowTools", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsListRequest := `{ "jsonrpc": "2.0", "id": 4, "method": "tools/list", "params": {} }` host.InitHttp() // 设置空的allowTools头 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, {"x-envoy-allow-mcp-tools", " , , "}, // 只有空白和逗号 }) require.Equal(t, types.HeaderStopIteration, action) action = host.CallOnHttpRequestBody([]byte(toolsListRequest)) require.Equal(t, types.ActionPause, action) // Mock MCP initialization sequence host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "test-session-empty"}, }, []byte(`{ "jsonrpc": "2.0", "id": "init-4", "result": { "capabilities": { "tools": {"listChanged": true} }, "protocolVersion": "2024-11-05" } }`)) host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "test-session-empty"}, }, []byte(`{ "jsonrpc": "2.0", "id": "notify-4", "result": {} }`)) // Mock tools/list response // 这是对executeToolsList中ctx.RouteCall的响应 host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }) host.CallOnHttpResponseBody([]byte(`{ "jsonrpc": "2.0", "id": 4, "result": { "tools": [ { "name": "get_product", "description": "Get product information", "inputSchema": {"type": "object"} }, { "name": "create_order", "description": "Create a new order", "inputSchema": {"type": "object"} } ] } }`)) host.CompleteHttp() // 验证响应不包含任何工具(空白header应该被当作配置为空,禁止所有工具) responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) result, hasResult := response["result"] require.True(t, hasResult) resultMap := result.(map[string]interface{}) tools, hasTools := resultMap["tools"] require.True(t, hasTools) toolsArray := tools.([]interface{}) // 应该返回0个工具,因为空白header等于配置为空数组,禁止所有工具 require.Len(t, toolsArray, 0) }) // 测试不存在的allowTools header(应该允许所有工具) t.Run("no header allowTools", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsListRequest := `{ "jsonrpc": "2.0", "id": 5, "method": "tools/list", "params": {} }` host.InitHttp() // 不设置x-envoy-allow-mcp-tools header action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) action = host.CallOnHttpRequestBody([]byte(toolsListRequest)) require.Equal(t, types.ActionPause, action) // Mock MCP initialization sequence host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "test-session-no-header"}, }, []byte(`{ "jsonrpc": "2.0", "id": "init-5", "result": { "capabilities": { "tools": {"listChanged": true} }, "protocolVersion": "2024-11-05" } }`)) host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, {"mcp-session-id", "test-session-no-header"}, }, []byte(`{ "jsonrpc": "2.0", "id": "notify-5", "result": {} }`)) // Mock tools/list response with multiple tools host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }) host.CallOnHttpResponseBody([]byte(`{ "jsonrpc": "2.0", "id": 5, "result": { "tools": [ { "name": "get_product", "description": "Get product information", "inputSchema": {"type": "object"} }, { "name": "create_order", "description": "Create a new order", "inputSchema": {"type": "object"} }, { "name": "delete_user", "description": "Delete a user account", "inputSchema": {"type": "object"} } ] } }`)) host.CompleteHttp() // 验证响应包含所有工具(header不存在时允许所有工具) responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) result, hasResult := response["result"] require.True(t, hasResult) resultMap := result.(map[string]interface{}) tools, hasTools := resultMap["tools"] require.True(t, hasTools) toolsArray := tools.([]interface{}) // 应该返回所有3个工具,因为header不存在意味着没有限制 require.Len(t, toolsArray, 3) toolNames := make([]string, 0) for _, tool := range toolsArray { toolMap := tool.(map[string]interface{}) toolNames = append(toolNames, toolMap["name"].(string)) } require.Contains(t, toolNames, "get_product") require.Contains(t, toolNames, "create_order") require.Contains(t, toolNames, "delete_user") }) }) } // MCP Proxy Server with SSE transport configuration var mcpProxyServerSSEConfig = func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "server": map[string]interface{}{ "name": "proxy-sse-test-server", "type": "mcp-proxy", "transport": "sse", "mcpServerURL": "http://backend-mcp.example.com/sse", "timeout": 5000, }, "tools": []map[string]interface{}{ { "name": "get_product", "description": "Get product information", "args": []map[string]interface{}{ { "name": "product_id", "description": "Product ID", "type": "string", "required": true, }, }, }, }, }) return data }() // TestMcpProxyServerSSEToolsList tests tools/list with SSE transport func TestMcpProxyServerSSEToolsList(t *testing.T) { test.RunTest(t, func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerSSEConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsListRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }` // Initialize HTTP context host.InitHttp() // Process request headers action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) // Process request body - this triggers SSE channel establishment // SSE protocol converts the POST request to GET in the request phase action = host.CallOnHttpRequestBody([]byte(toolsListRequest)) require.Equal(t, types.ActionContinue, action) // Should continue (not pause) as request is converted to GET // Step 1: Mock SSE channel response headers (GET request response) // The server returns text/event-stream for SSE channel action = host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "text/event-stream"}, {"cache-control", "no-cache"}, }) require.Equal(t, types.HeaderStopIteration, action) // Should stop to process streaming body // Step 2: Send SSE endpoint message // This is the first message in the SSE stream indicating the endpoint URL endpointSSEMessage := `event: endpoint data: /sse/session-abc123 ` action = host.CallOnHttpStreamingResponseBody([]byte(endpointSSEMessage), false) // After receiving endpoint, the proxy will send initialize request via RouteCluster // The state machine is now waiting for initialize response // Step 3: Mock initialize response via RouteCluster callback initResponse := `{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"BackendSSEServer","version":"1.0.0"}}}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(initResponse)) // Step 4: Send SSE initialize response in the stream // This matches the initialize request ID and triggers notification initSSEMessage := `event: message data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"BackendSSEServer","version":"1.0.0"}}} ` action = host.CallOnHttpStreamingResponseBody([]byte(initSSEMessage), false) // After receiving init response, proxy sends notification via RouteCluster // Step 5: Mock notification response via RouteCluster callback notificationResponse := `{"jsonrpc":"2.0"}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(notificationResponse)) // After notification completes, proxy sends tools/list via RouteCluster // Step 6: Mock tools/list response via RouteCluster callback toolsListResponse := `{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"get_product","description":"Get product information","inputSchema":{"type":"object","properties":{"product_id":{"type":"string","description":"Product ID"}},"required":["product_id"]}}]}}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(toolsListResponse)) // Step 7: Send SSE tools/list response in the stream // This matches the tools/list request ID and triggers final response toolsListSSEMessage := `event: message data: {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"get_product","description":"Get product information","inputSchema":{"type":"object","properties":{"product_id":{"type":"string","description":"Product ID"}},"required":["product_id"]}}]}} ` action = host.CallOnHttpStreamingResponseBody([]byte(toolsListSSEMessage), true) // This should inject the final response to client // Verify the final response responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) // Verify JSON-RPC format require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(1), response["id"]) // Verify the proxied result result, ok := response["result"].(map[string]interface{}) require.True(t, ok) tools, ok := result["tools"].([]interface{}) require.True(t, ok) require.Greater(t, len(tools), 0) // Verify tool information tool, ok := tools[0].(map[string]interface{}) require.True(t, ok) require.Equal(t, "get_product", tool["name"]) require.Equal(t, "Get product information", tool["description"]) host.CompleteHttp() }) } // TestMcpProxyServerSSEToolsCall tests tools/call with SSE transport func TestMcpProxyServerSSEToolsCall(t *testing.T) { test.RunTest(t, func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerSSEConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsCallRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_product", "arguments": { "product_id": "12345" } } }` // Initialize HTTP context host.InitHttp() // Process request headers action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) // Process request body - this triggers SSE channel establishment action = host.CallOnHttpRequestBody([]byte(toolsCallRequest)) require.Equal(t, types.ActionContinue, action) // Step 1: Mock SSE channel response headers action = host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "text/event-stream"}, {"cache-control", "no-cache"}, }) require.Equal(t, types.HeaderStopIteration, action) // Step 2: Send SSE endpoint message endpointSSEMessage := `event: endpoint data: /sse/session-xyz789 ` action = host.CallOnHttpStreamingResponseBody([]byte(endpointSSEMessage), false) // Step 3: Mock initialize response via RouteCluster callback initResponse := `{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"BackendSSEServer","version":"1.0.0"}}}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(initResponse)) // Step 4: Send SSE initialize response in the stream initSSEMessage := `event: message data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"BackendSSEServer","version":"1.0.0"}}} ` action = host.CallOnHttpStreamingResponseBody([]byte(initSSEMessage), false) // Step 5: Mock notification response via RouteCluster callback notificationResponse := `{"jsonrpc":"2.0"}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(notificationResponse)) // Step 6: Mock tools/call response via RouteCluster callback toolsCallResponse := `{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Product ID: 12345, Name: Sample Product, Price: $99.99"}]}}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(toolsCallResponse)) // Step 7: Send SSE tools/call response in the stream toolsCallSSEMessage := `event: message data: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Product ID: 12345, Name: Sample Product, Price: $99.99"}]}} ` action = host.CallOnHttpStreamingResponseBody([]byte(toolsCallSSEMessage), true) // Verify the final response responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) // Verify JSON-RPC format require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(1), response["id"]) // Verify the proxied result result, ok := response["result"].(map[string]interface{}) require.True(t, ok) content, ok := result["content"].([]interface{}) require.True(t, ok) require.Greater(t, len(content), 0) // Verify content contentItem, ok := content[0].(map[string]interface{}) require.True(t, ok) require.Equal(t, "text", contentItem["type"]) require.Contains(t, contentItem["text"], "Product ID: 12345") host.CompleteHttp() }) } // TestMcpProxyServerSSEAllowTools tests allowTools functionality with SSE transport func TestMcpProxyServerSSEAllowTools(t *testing.T) { // Create config with allowTools mcpProxyServerSSEWithAllowToolsConfig := func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "server": map[string]interface{}{ "name": "proxy-sse-allow-tools-server", "type": "mcp-proxy", "transport": "sse", "mcpServerURL": "http://backend-mcp.example.com/sse", "timeout": 5000, }, "allowTools": []string{"get_product", "create_order"}, // Only allow these two tools "tools": []map[string]interface{}{ { "name": "get_product", "type": "mcp-proxy", "description": "Get product information", }, { "name": "create_order", "type": "mcp-proxy", "description": "Create a new order", }, { "name": "delete_user", "type": "mcp-proxy", "description": "Delete a user account", }, }, }) return data }() test.RunTest(t, func(t *testing.T) { // Test config level allowTools filtering t.Run("config level allowTools filtering with SSE", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerSSEWithAllowToolsConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsListRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }` host.InitHttp() action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) action = host.CallOnHttpRequestBody([]byte(toolsListRequest)) require.Equal(t, types.ActionContinue, action) // Mock SSE channel response action = host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "text/event-stream"}, }) require.Equal(t, types.HeaderStopIteration, action) // Send endpoint endpointSSEMessage := `event: endpoint data: /sse/session-allow-tools ` host.CallOnHttpStreamingResponseBody([]byte(endpointSSEMessage), false) // Mock initialize initResponse := `{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"BackendSSEServer","version":"1.0.0"}}}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(initResponse)) initSSEMessage := `event: message data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"BackendSSEServer","version":"1.0.0"}}} ` host.CallOnHttpStreamingResponseBody([]byte(initSSEMessage), false) // Mock notification notificationResponse := `{"jsonrpc":"2.0"}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(notificationResponse)) // Mock tools/list response with all 3 tools from backend toolsListResponse := `{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"get_product","description":"Get product information"},{"name":"create_order","description":"Create a new order"},{"name":"delete_user","description":"Delete a user account"}]}}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(toolsListResponse)) // Send SSE tools/list response toolsListSSEMessage := `event: message data: {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"get_product","description":"Get product information"},{"name":"create_order","description":"Create a new order"},{"name":"delete_user","description":"Delete a user account"}]}} ` host.CallOnHttpStreamingResponseBody([]byte(toolsListSSEMessage), true) // Verify response - should only contain allowed tools responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) result, hasResult := response["result"] require.True(t, hasResult) resultMap := result.(map[string]interface{}) tools, hasTools := resultMap["tools"] require.True(t, hasTools) toolsArray := tools.([]interface{}) // Should only return 2 allowed tools (get_product, create_order) require.Len(t, toolsArray, 2) toolNames := make([]string, 0) for _, tool := range toolsArray { toolMap := tool.(map[string]interface{}) toolNames = append(toolNames, toolMap["name"].(string)) } require.Contains(t, toolNames, "get_product") require.Contains(t, toolNames, "create_order") require.NotContains(t, toolNames, "delete_user") // Should be filtered out host.CompleteHttp() }) // Test header level allowTools filtering t.Run("header level allowTools filtering with SSE", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerSSEWithAllowToolsConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsListRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }` host.InitHttp() action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, {"x-envoy-allow-mcp-tools", "get_product"}, // Only allow get_product via header }) require.Equal(t, types.HeaderStopIteration, action) action = host.CallOnHttpRequestBody([]byte(toolsListRequest)) require.Equal(t, types.ActionContinue, action) // Mock SSE channel response action = host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "text/event-stream"}, }) require.Equal(t, types.HeaderStopIteration, action) // Send endpoint endpointSSEMessage := `event: endpoint data: /sse/session-header-filter ` host.CallOnHttpStreamingResponseBody([]byte(endpointSSEMessage), false) // Mock initialize initResponse := `{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"BackendSSEServer","version":"1.0.0"}}}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(initResponse)) initSSEMessage := `event: message data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"BackendSSEServer","version":"1.0.0"}}} ` host.CallOnHttpStreamingResponseBody([]byte(initSSEMessage), false) // Mock notification notificationResponse := `{"jsonrpc":"2.0"}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(notificationResponse)) // Mock tools/list response with all 3 tools toolsListResponse := `{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"get_product","description":"Get product information"},{"name":"create_order","description":"Create a new order"},{"name":"delete_user","description":"Delete a user account"}]}}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(toolsListResponse)) // Send SSE tools/list response toolsListSSEMessage := `event: message data: {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"get_product","description":"Get product information"},{"name":"create_order","description":"Create a new order"},{"name":"delete_user","description":"Delete a user account"}]}} ` host.CallOnHttpStreamingResponseBody([]byte(toolsListSSEMessage), true) // Verify response - should only contain get_product (intersection of config and header) responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) result, hasResult := response["result"] require.True(t, hasResult) resultMap := result.(map[string]interface{}) tools, hasTools := resultMap["tools"] require.True(t, hasTools) toolsArray := tools.([]interface{}) // Should only return 1 tool (get_product - intersection of config and header) require.Len(t, toolsArray, 1) tool := toolsArray[0].(map[string]interface{}) require.Equal(t, "get_product", tool["name"]) host.CompleteHttp() }) }) } // TestMcpProxyServerSSEAuthentication tests authentication functionality with SSE transport func TestMcpProxyServerSSEAuthentication(t *testing.T) { // Create SSE config with authentication mcpProxyServerSSEWithAuthConfig := func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "server": map[string]interface{}{ "name": "proxy-sse-auth-server", "type": "mcp-proxy", "transport": "sse", "mcpServerURL": "http://backend-mcp.example.com/sse", "timeout": 5000, "defaultUpstreamSecurity": map[string]interface{}{ "id": "BackendApiKey", }, "securitySchemes": []map[string]interface{}{ { "id": "BackendApiKey", "type": "apiKey", "in": "header", "name": "X-API-Key", "defaultCredential": "backend-default-key", }, }, }, "tools": []map[string]interface{}{ { "name": "get_secure_data", "type": "mcp-proxy", "description": "Get secure data with authentication", }, }, }) return data }() test.RunTest(t, func(t *testing.T) { // Test authentication headers in SSE requests t.Run("SSE requests with authentication headers", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerSSEWithAuthConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsListRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }` host.InitHttp() action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) action = host.CallOnHttpRequestBody([]byte(toolsListRequest)) require.Equal(t, types.ActionContinue, action) // Mock SSE channel response action = host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "text/event-stream"}, }) require.Equal(t, types.HeaderStopIteration, action) // Send endpoint endpointSSEMessage := `event: endpoint data: /sse/session-auth-test ` host.CallOnHttpStreamingResponseBody([]byte(endpointSSEMessage), false) // Verify initialize request has authentication header httpCallouts := host.GetHttpCalloutAttributes() require.Greater(t, len(httpCallouts), 0, "Expected at least one HTTP callout for initialize") initCallout := httpCallouts[0] require.True(t, test.HasHeaderWithValue(initCallout.Headers, "X-API-Key", "backend-default-key"), "Initialize request should have X-API-Key header with default credential") require.True(t, test.HasHeaderWithValue(initCallout.Headers, ":method", "POST"), "Initialize request should use POST method") require.True(t, test.HasHeaderWithValue(initCallout.Headers, "Content-Type", "application/json"), "Initialize request should have Content-Type header") // Mock initialize response initResponse := `{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"SecureSSEServer","version":"1.0.0"}}}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(initResponse)) initSSEMessage := `event: message data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"SecureSSEServer","version":"1.0.0"}}} ` host.CallOnHttpStreamingResponseBody([]byte(initSSEMessage), false) // After sending initSSEMessage, notification HTTP call should be triggered // Let's respond to initialize callback first, then respond to notification callback // This will trigger the tools/list request // Mock notification response (this responds to the notification HTTP call that was just sent) notificationResponse := `{"jsonrpc":"2.0"}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(notificationResponse)) // After responding to notification, tools/list request should have been sent // Verify tools/list request has authentication header httpCallouts = host.GetHttpCalloutAttributes() // Find the tools/list callout (it should be the last one, or one of the recent ones) var toolsListCallout *proxytest.HttpCalloutAttribute for i := len(httpCallouts) - 1; i >= 0; i-- { if strings.Contains(string(httpCallouts[i].Body), "tools/list") { toolsListCallout = &httpCallouts[i] break } } require.NotNil(t, toolsListCallout, "Expected to find tools/list HTTP callout") require.True(t, test.HasHeaderWithValue(toolsListCallout.Headers, "X-API-Key", "backend-default-key"), "Tools/list request should have X-API-Key header with default credential") require.True(t, test.HasHeaderWithValue(toolsListCallout.Headers, ":method", "POST"), "Tools/list request should use POST method") require.True(t, test.HasHeaderWithValue(toolsListCallout.Headers, "Content-Type", "application/json"), "Tools/list request should have Content-Type header") // Verify request body contains tools/list require.Contains(t, string(toolsListCallout.Body), "tools/list", "Request body should contain tools/list method") // Mock tools/list response toolsListResponse := `{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"get_secure_data","description":"Get secure data with authentication"}]}}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(toolsListResponse)) toolsListSSEMessage := `event: message data: {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"get_secure_data","description":"Get secure data with authentication"}]}} ` host.CallOnHttpStreamingResponseBody([]byte(toolsListSSEMessage), true) // Verify final response responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) require.Equal(t, "2.0", response["jsonrpc"]) require.Equal(t, float64(1), response["id"]) result, ok := response["result"].(map[string]interface{}) require.True(t, ok) tools, ok := result["tools"].([]interface{}) require.True(t, ok) require.Len(t, tools, 1) host.CompleteHttp() }) // Test tools/call with authentication t.Run("SSE tools/call with authentication headers", func(t *testing.T) { host, status := test.NewTestHost(mcpProxyServerSSEWithAuthConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) toolsCallRequest := `{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_secure_data", "arguments": { "data_id": "secret-123" } } }` host.InitHttp() action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "mcp-server.example.com"}, {":method", "POST"}, {":path", "/mcp"}, {"content-type", "application/json"}, }) require.Equal(t, types.HeaderStopIteration, action) action = host.CallOnHttpRequestBody([]byte(toolsCallRequest)) require.Equal(t, types.ActionContinue, action) // Mock SSE channel response action = host.CallOnHttpResponseHeaders([][2]string{ {":status", "200"}, {"content-type", "text/event-stream"}, }) require.Equal(t, types.HeaderStopIteration, action) // Send endpoint - this triggers initialize request endpointSSEMessage := `event: endpoint data: /sse/session-call-auth ` host.CallOnHttpStreamingResponseBody([]byte(endpointSSEMessage), false) // Verify initialize request has authentication header httpCallouts := host.GetHttpCalloutAttributes() require.Greater(t, len(httpCallouts), 0, "Expected at least one HTTP callout for initialize") initCallout := httpCallouts[0] require.True(t, test.HasHeaderWithValue(initCallout.Headers, "X-API-Key", "backend-default-key"), "Initialize request should have X-API-Key header with default credential") require.True(t, test.HasHeaderWithValue(initCallout.Headers, ":method", "POST"), "Initialize request should use POST method") // Mock initialize response initResponse := `{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"SecureSSEServer","version":"1.0.0"}}}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(initResponse)) // Send initialize SSE message - this triggers notification request initSSEMessage := `event: message data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"SecureSSEServer","version":"1.0.0"}}} ` host.CallOnHttpStreamingResponseBody([]byte(initSSEMessage), false) // Mock notification response - this will trigger tools/call request notificationResponse := `{"jsonrpc":"2.0"}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(notificationResponse)) // After responding to notification, tools/call request should have been sent // Verify tools/call request has authentication header httpCallouts = host.GetHttpCalloutAttributes() // Find the tools/call callout var toolsCallCallout *proxytest.HttpCalloutAttribute for i := len(httpCallouts) - 1; i >= 0; i-- { if strings.Contains(string(httpCallouts[i].Body), "tools/call") { toolsCallCallout = &httpCallouts[i] break } } require.NotNil(t, toolsCallCallout, "Expected to find tools/call HTTP callout") require.True(t, test.HasHeaderWithValue(toolsCallCallout.Headers, "X-API-Key", "backend-default-key"), "Tools/call request should have X-API-Key header with default credential") require.True(t, test.HasHeaderWithValue(toolsCallCallout.Headers, ":method", "POST"), "Tools/call request should use POST method") // Verify request body contains tools/call and arguments require.Contains(t, string(toolsCallCallout.Body), "tools/call", "Request body should contain tools/call method") require.Contains(t, string(toolsCallCallout.Body), "get_secure_data", "Request body should contain tool name") require.Contains(t, string(toolsCallCallout.Body), "secret-123", "Request body should contain arguments") // Mock tools/call response toolsCallResponse := `{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Secure data for secret-123"}]}}` host.CallOnHttpCall([][2]string{ {":status", "200"}, {"content-type", "application/json"}, }, []byte(toolsCallResponse)) toolsCallSSEMessage := `event: message data: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Secure data for secret-123"}]}} ` host.CallOnHttpStreamingResponseBody([]byte(toolsCallSSEMessage), true) // Verify final response responseBody := host.GetResponseBody() require.NotEmpty(t, responseBody) var response map[string]interface{} err := json.Unmarshal(responseBody, &response) require.NoError(t, err) require.Equal(t, "2.0", response["jsonrpc"]) result, ok := response["result"].(map[string]interface{}) require.True(t, ok) content, ok := result["content"].([]interface{}) require.True(t, ok) require.Greater(t, len(content), 0) host.CompleteHttp() }) }) }