mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-08 17:09:33 +08:00
refactor: move mcp to pkg/mcphost
This commit is contained in:
+1
-1
@@ -17,7 +17,7 @@ var CmdServer = &cobra.Command{
|
|||||||
router := server.NewRouter()
|
router := server.NewRouter()
|
||||||
mcpConfigPath = os.ExpandEnv(mcpConfigPath)
|
mcpConfigPath = os.ExpandEnv(mcpConfigPath)
|
||||||
if mcpConfigPath != "" {
|
if mcpConfigPath != "" {
|
||||||
router.InitMCPHub(mcpConfigPath)
|
router.InitMCPHost(mcpConfigPath)
|
||||||
}
|
}
|
||||||
return router.Run(port)
|
return router.Run(port)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ type TConfig struct {
|
|||||||
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
|
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
|
||||||
Path string `json:"path,omitempty" yaml:"path,omitempty"` // testcase file path
|
Path string `json:"path,omitempty" yaml:"path,omitempty"` // testcase file path
|
||||||
PluginSetting *PluginConfig `json:"plugin,omitempty" yaml:"plugin,omitempty"` // plugin config
|
PluginSetting *PluginConfig `json:"plugin,omitempty" yaml:"plugin,omitempty"` // plugin config
|
||||||
|
MCPConfigPath string `json:"mcp_config_path,omitempty" yaml:"mcp_config_path,omitempty"`
|
||||||
IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"`
|
IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"`
|
||||||
LLMService option.LLMServiceType `json:"llm_service,omitempty" yaml:"llm_service,omitempty"`
|
LLMService option.LLMServiceType `json:"llm_service,omitempty" yaml:"llm_service,omitempty"`
|
||||||
CVService option.CVServiceType `json:"cv_service,omitempty" yaml:"cv_service,omitempty"`
|
CVService option.CVServiceType `json:"cv_service,omitempty" yaml:"cv_service,omitempty"`
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
package mcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MCPSettings represents the main configuration structure
|
|
||||||
type MCPSettings struct {
|
|
||||||
MCPServers map[string]ServerConfig `json:"mcpServers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerConfig represents configuration for a single MCP server
|
|
||||||
type ServerConfig struct {
|
|
||||||
TransportType string `json:"transportType,omitempty"` // "sse" or "stdio"
|
|
||||||
AutoApprove []string `json:"autoApprove,omitempty"`
|
|
||||||
Disabled bool `json:"disabled,omitempty"`
|
|
||||||
Timeout time.Duration `json:"timeout,omitempty"`
|
|
||||||
|
|
||||||
// SSE specific config
|
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
|
|
||||||
// Stdio specific config
|
|
||||||
Command string `json:"command"`
|
|
||||||
Args []string `json:"args"`
|
|
||||||
Env map[string]string `json:"env,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultMCPTimeoutSeconds = 30
|
|
||||||
MinMCPTimeoutSeconds = 5
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetTimeoutDuration converts timeout seconds to time.Duration
|
|
||||||
func (c *ServerConfig) GetTimeoutDuration() time.Duration {
|
|
||||||
if c.Timeout == 0 {
|
|
||||||
return time.Duration(DefaultMCPTimeoutSeconds) * time.Second
|
|
||||||
}
|
|
||||||
return c.Timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadSettings loads MCP settings from the config file
|
|
||||||
func LoadSettings(path string) (*MCPSettings, error) {
|
|
||||||
log.Info().Str("path", path).Msg("load MCP settings")
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read settings file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var settings MCPSettings
|
|
||||||
if err := json.Unmarshal(data, &settings); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse settings: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateSettings(&settings); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid settings: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &settings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateSettings validates the MCP settings
|
|
||||||
func validateSettings(settings *MCPSettings) error {
|
|
||||||
if settings == nil {
|
|
||||||
return fmt.Errorf("settings cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, server := range settings.MCPServers {
|
|
||||||
if server.Timeout > 0 && server.Timeout < time.Duration(MinMCPTimeoutSeconds)*time.Second {
|
|
||||||
return fmt.Errorf("server %s: timeout must be at least %d seconds", name, MinMCPTimeoutSeconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch server.TransportType {
|
|
||||||
case "sse":
|
|
||||||
if server.URL == "" {
|
|
||||||
return fmt.Errorf("server %s: URL is required for SSE transport", name)
|
|
||||||
}
|
|
||||||
case "stdio", "":
|
|
||||||
if server.Command == "" {
|
|
||||||
return fmt.Errorf("server %s: command is required for stdio transport", name)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("server %s: unsupported transport type: %s", name, server.TransportType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package mcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLoadSettings(t *testing.T) {
|
|
||||||
// Load settings from test.mcp.json
|
|
||||||
settings, err := LoadSettings("testdata/test.mcp.json")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load settings: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify settings are loaded correctly
|
|
||||||
assert.NotNil(t, settings)
|
|
||||||
assert.Contains(t, settings.MCPServers, "filesystem")
|
|
||||||
assert.Contains(t, settings.MCPServers, "weather")
|
|
||||||
|
|
||||||
// Verify specific server configurations
|
|
||||||
filesystemConfig := settings.MCPServers["filesystem"]
|
|
||||||
assert.Equal(t, "npx", filesystemConfig.Command)
|
|
||||||
assert.Equal(t, []string{"-y", "@modelcontextprotocol/server-filesystem", "/tmp"}, filesystemConfig.Args)
|
|
||||||
|
|
||||||
weatherConfig := settings.MCPServers["weather"]
|
|
||||||
assert.Equal(t, "uv", weatherConfig.Command)
|
|
||||||
assert.Equal(t, []string{"--directory", "/Users/debugtalk/MyProjects/HttpRunner-dev/httprunner/internal/mcp/testdata", "run", "demo_weather.py"}, weatherConfig.Args)
|
|
||||||
assert.Equal(t, []string{"get_forecast"}, weatherConfig.AutoApprove)
|
|
||||||
assert.Equal(t, map[string]string{"ABC": "123"}, weatherConfig.Env)
|
|
||||||
}
|
|
||||||
@@ -1,432 +0,0 @@
|
|||||||
package mcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
mcpp "github.com/cloudwego/eino-ext/components/tool/mcp"
|
|
||||||
"github.com/cloudwego/eino/components/tool"
|
|
||||||
"github.com/httprunner/httprunner/v5/internal/version"
|
|
||||||
"github.com/mark3labs/mcp-go/client"
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MCPTools struct {
|
|
||||||
Name string
|
|
||||||
Tools []mcp.Tool
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
type MCPHub struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
connections map[string]*Connection
|
|
||||||
config *MCPSettings
|
|
||||||
}
|
|
||||||
|
|
||||||
type Connection struct {
|
|
||||||
Client client.MCPClient
|
|
||||||
Config ServerConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMCPHub(configPath string) (*MCPHub, error) {
|
|
||||||
settings, err := LoadSettings(configPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &MCPHub{
|
|
||||||
connections: make(map[string]*Connection),
|
|
||||||
config: settings,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitServers initializes all enabled MCP servers
|
|
||||||
func (h *MCPHub) InitServers(ctx context.Context) error {
|
|
||||||
for name, config := range h.config.MCPServers {
|
|
||||||
if config.Disabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.connectToServer(ctx, name, config); err != nil {
|
|
||||||
return fmt.Errorf("failed to connect to server %s: %w", name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetClient returns the client for the specified server
|
|
||||||
func (h *MCPHub) GetClient(serverName string) (client.MCPClient, error) {
|
|
||||||
h.mu.RLock()
|
|
||||||
defer h.mu.RUnlock()
|
|
||||||
|
|
||||||
conn, exists := h.connections[serverName]
|
|
||||||
if !exists {
|
|
||||||
return nil, fmt.Errorf("no connection found for server %s", serverName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conn.Config.Disabled {
|
|
||||||
return nil, fmt.Errorf("server %s is disabled", serverName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn.Client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// connectToServer establishes connection to a single MCP server
|
|
||||||
func (h *MCPHub) connectToServer(ctx context.Context, serverName string, config ServerConfig) error {
|
|
||||||
h.mu.Lock()
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
|
|
||||||
log.Debug().Str("server", serverName).Msg("connecting to MCP server")
|
|
||||||
|
|
||||||
// Close existing connection if any
|
|
||||||
if existing, exists := h.connections[serverName]; exists {
|
|
||||||
if err := existing.Client.Close(); err != nil {
|
|
||||||
return fmt.Errorf("failed to close existing connection: %w", err)
|
|
||||||
}
|
|
||||||
delete(h.connections, serverName)
|
|
||||||
}
|
|
||||||
|
|
||||||
var mcpClient *client.Client
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// create client
|
|
||||||
switch config.TransportType {
|
|
||||||
case "sse":
|
|
||||||
mcpClient, err = client.NewSSEMCPClient(config.URL)
|
|
||||||
|
|
||||||
case "stdio", "": // default to stdio
|
|
||||||
var env []string
|
|
||||||
for k, v := range config.Env {
|
|
||||||
env = append(env, fmt.Sprintf("%s=%s", k, v))
|
|
||||||
}
|
|
||||||
mcpClient, err = client.NewStdioMCPClient(config.Command,
|
|
||||||
env, config.Args...)
|
|
||||||
|
|
||||||
// print MCP Server logs for stdio transport
|
|
||||||
stderr, _ := client.GetStderr(mcpClient)
|
|
||||||
go func() {
|
|
||||||
scanner := bufio.NewScanner(stderr)
|
|
||||||
for scanner.Scan() {
|
|
||||||
fmt.Fprintf(os.Stderr, "MCP Server %s: %s\n",
|
|
||||||
serverName, scanner.Text())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported transport type: %s", config.TransportType)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare client init request
|
|
||||||
initRequest := mcp.InitializeRequest{}
|
|
||||||
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
|
|
||||||
initRequest.Params.Capabilities = mcp.ClientCapabilities{}
|
|
||||||
initRequest.Params.ClientInfo = mcp.Implementation{
|
|
||||||
Name: "HttpRunner",
|
|
||||||
Version: version.VERSION,
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize client
|
|
||||||
_, err = mcpClient.Initialize(ctx, initRequest)
|
|
||||||
if err != nil {
|
|
||||||
mcpClient.Close()
|
|
||||||
return errors.Wrapf(err, "initialize MCP client for %s failed", serverName)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("server", serverName).Msg("connected to MCP server")
|
|
||||||
h.connections[serverName] = &Connection{
|
|
||||||
Client: mcpClient,
|
|
||||||
Config: config,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTools fetches available tools from all connected MCP servers
|
|
||||||
func (h *MCPHub) GetTools(ctx context.Context) map[string]MCPTools {
|
|
||||||
h.mu.RLock()
|
|
||||||
defer h.mu.RUnlock()
|
|
||||||
|
|
||||||
results := make(map[string]MCPTools)
|
|
||||||
|
|
||||||
for serverName, conn := range h.connections {
|
|
||||||
if conn.Config.Disabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// get tools from MCP server tools
|
|
||||||
listResults, err := conn.Client.ListTools(ctx, mcp.ListToolsRequest{})
|
|
||||||
if err != nil {
|
|
||||||
results[serverName] = MCPTools{
|
|
||||||
Name: serverName,
|
|
||||||
Tools: nil,
|
|
||||||
Err: fmt.Errorf("failed to get tools: %w", err),
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
results[serverName] = MCPTools{
|
|
||||||
Name: serverName,
|
|
||||||
Tools: listResults.Tools,
|
|
||||||
Err: nil,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MCPHub) GetTool(ctx context.Context, serverName, toolName string) (*mcp.Tool, error) {
|
|
||||||
h.mu.RLock()
|
|
||||||
defer h.mu.RUnlock()
|
|
||||||
|
|
||||||
// filter MCP server by serverName
|
|
||||||
mcpTools, exists := h.GetTools(ctx)[serverName]
|
|
||||||
if !exists {
|
|
||||||
return nil, fmt.Errorf("no connection found for server %s", serverName)
|
|
||||||
} else if mcpTools.Err != nil {
|
|
||||||
return nil, mcpTools.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter tool by toolName
|
|
||||||
for _, tool := range mcpTools.Tools {
|
|
||||||
if tool.Name == toolName {
|
|
||||||
return &tool, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("tool %s not found", toolName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvokeTool calls a tool with the given arguments
|
|
||||||
func (h *MCPHub) InvokeTool(ctx context.Context,
|
|
||||||
serverName, toolName string, arguments map[string]interface{},
|
|
||||||
) (*mcp.CallToolResult, error) {
|
|
||||||
log.Info().Str("tool", toolName).Interface("args", arguments).
|
|
||||||
Str("server", serverName).Msg("invoke tool")
|
|
||||||
|
|
||||||
conn, err := h.GetClient(serverName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err,
|
|
||||||
"get mcp client for server %s failed", serverName)
|
|
||||||
}
|
|
||||||
|
|
||||||
mcpTool, err := h.GetTool(ctx, serverName, toolName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err,
|
|
||||||
"get mcp tool %s/%s failed", serverName, toolName)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := mcp.CallToolRequest{}
|
|
||||||
req.Params.Name = mcpTool.Name
|
|
||||||
req.Params.Arguments = arguments
|
|
||||||
callToolResult, err := conn.CallTool(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err,
|
|
||||||
"call tool %s/%s failed", serverName, toolName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return callToolResult, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEinoTool returns an eino tool from the MCP server
|
|
||||||
func (h *MCPHub) GetEinoTool(ctx context.Context, serverName, toolName string) (tool.BaseTool, error) {
|
|
||||||
h.mu.RLock()
|
|
||||||
defer h.mu.RUnlock()
|
|
||||||
|
|
||||||
// filter MCP server by serverName
|
|
||||||
conn, exists := h.connections[serverName]
|
|
||||||
if !exists {
|
|
||||||
return nil, fmt.Errorf("no connection found for server %s", serverName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conn.Config.Disabled {
|
|
||||||
return nil, fmt.Errorf("server %s is disabled", serverName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get tools from MCP server and convert to eino tools
|
|
||||||
tools, err := mcpp.GetTools(ctx, &mcpp.Config{
|
|
||||||
Cli: conn.Client,
|
|
||||||
ToolNameList: []string{toolName},
|
|
||||||
})
|
|
||||||
if err != nil || len(tools) == 0 {
|
|
||||||
log.Error().Err(err).
|
|
||||||
Str("server", serverName).Str("tool", toolName).
|
|
||||||
Msg("get MCP tool failed")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tools[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseServers closes all connected MCP servers
|
|
||||||
func (h *MCPHub) CloseServers() error {
|
|
||||||
h.mu.Lock()
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
|
|
||||||
log.Info().Msg("Shutting down MCP servers...")
|
|
||||||
for name, client := range h.connections {
|
|
||||||
if err := client.Client.Close(); err != nil {
|
|
||||||
log.Error().Str("name", name).Err(err).Msg("Failed to close server")
|
|
||||||
} else {
|
|
||||||
delete(h.connections, name)
|
|
||||||
log.Info().Str("name", name).Msg("Server closed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MCPToolRecord represents a single tool record in the database
|
|
||||||
// Each record contains detailed information about a tool and its server
|
|
||||||
type MCPToolRecord struct {
|
|
||||||
ToolID string `json:"tool_id"` // Unique identifier for the tool record
|
|
||||||
ServerName string `json:"mcp_server"` // Name of the MCP server
|
|
||||||
ToolName string `json:"tool_name"` // Name of the tool
|
|
||||||
Description string `json:"description"` // Tool description
|
|
||||||
Parameters string `json:"parameters"` // Tool input parameters in JSON format
|
|
||||||
Returns string `json:"returns"` // Tool return value format in JSON format
|
|
||||||
CreatedAt time.Time `json:"created_at"` // Record creation time
|
|
||||||
LastUpdatedAt time.Time `json:"last_updated_at"` // Record last update time
|
|
||||||
}
|
|
||||||
|
|
||||||
// DocStringInfo contains the parsed information from a Python docstring
|
|
||||||
type DocStringInfo struct {
|
|
||||||
Description string
|
|
||||||
Parameters map[string]string
|
|
||||||
Returns map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractDocStringInfo extracts information from a Python docstring
|
|
||||||
// Example input:
|
|
||||||
// """Get weather alerts for a US state.
|
|
||||||
//
|
|
||||||
// Args:
|
|
||||||
// state: Two-letter US state code (e.g. CA, NY)
|
|
||||||
//
|
|
||||||
// Returns:
|
|
||||||
// alerts: List of active weather alerts for the specified state
|
|
||||||
// error: Error message if the request fails
|
|
||||||
// """
|
|
||||||
func extractDocStringInfo(docstring string) DocStringInfo {
|
|
||||||
info := DocStringInfo{
|
|
||||||
Parameters: make(map[string]string),
|
|
||||||
Returns: make(map[string]string),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the Args and Returns sections
|
|
||||||
argsIndex := strings.Index(docstring, "Args:")
|
|
||||||
returnsIndex := strings.Index(docstring, "Returns:")
|
|
||||||
|
|
||||||
// Extract description (everything before Args)
|
|
||||||
if argsIndex != -1 {
|
|
||||||
info.Description = strings.TrimSpace(docstring[:argsIndex])
|
|
||||||
} else if returnsIndex != -1 {
|
|
||||||
info.Description = strings.TrimSpace(docstring[:returnsIndex])
|
|
||||||
} else {
|
|
||||||
info.Description = strings.TrimSpace(docstring)
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to extract key-value pairs from a section
|
|
||||||
extractSection := func(content string) map[string]string {
|
|
||||||
result := make(map[string]string)
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
|
|
||||||
for _, line := range lines {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(line, ":", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
key := strings.TrimSpace(parts[0])
|
|
||||||
value := strings.TrimSpace(parts[1])
|
|
||||||
|
|
||||||
if key != "" && value != "" {
|
|
||||||
result[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract Args section
|
|
||||||
if argsIndex != -1 {
|
|
||||||
endIndex := returnsIndex
|
|
||||||
if endIndex == -1 {
|
|
||||||
endIndex = len(docstring)
|
|
||||||
}
|
|
||||||
argsContent := docstring[argsIndex+len("Args:") : endIndex]
|
|
||||||
info.Parameters = extractSection(argsContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract Returns section
|
|
||||||
if returnsIndex != -1 {
|
|
||||||
returnsContent := docstring[returnsIndex+len("Returns:"):]
|
|
||||||
info.Returns = extractSection(returnsContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertToolsToRecords converts map[string]MCPTools to a list of database records
|
|
||||||
func ConvertToolsToRecords(toolsMap map[string]MCPTools) []MCPToolRecord {
|
|
||||||
var records []MCPToolRecord
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
for serverName, mcpTools := range toolsMap {
|
|
||||||
if mcpTools.Err != nil {
|
|
||||||
log.Error().Str("server", serverName).Err(mcpTools.Err).Msg("skip tools conversion due to error")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tool := range mcpTools.Tools {
|
|
||||||
// Generate unique ID by combining server name and tool name
|
|
||||||
id := fmt.Sprintf("%s_%s", serverName, tool.Name)
|
|
||||||
|
|
||||||
// Extract docstring information
|
|
||||||
info := extractDocStringInfo(tool.Description)
|
|
||||||
|
|
||||||
// Convert parameters and returns to JSON
|
|
||||||
paramsJSON, err := sonic.MarshalString(info.Parameters)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Interface("params", info.Parameters).Err(err).Msg("failed to marshal parameters to JSON")
|
|
||||||
paramsJSON = "{}"
|
|
||||||
}
|
|
||||||
|
|
||||||
returnsJSON, err := sonic.MarshalString(info.Returns)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Interface("returns", info.Returns).Err(err).Msg("failed to marshal returns to JSON")
|
|
||||||
returnsJSON = "{}"
|
|
||||||
}
|
|
||||||
|
|
||||||
record := MCPToolRecord{
|
|
||||||
ToolID: id,
|
|
||||||
ServerName: serverName,
|
|
||||||
ToolName: tool.Name,
|
|
||||||
Description: info.Description,
|
|
||||||
Parameters: paramsJSON,
|
|
||||||
Returns: returnsJSON,
|
|
||||||
CreatedAt: now,
|
|
||||||
LastUpdatedAt: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
records = append(records, record)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return records
|
|
||||||
}
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
package mcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/cloudwego/eino/components/tool"
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetTools(t *testing.T) {
|
|
||||||
hub, err := NewMCPHub("./testdata/test.mcp.json")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = hub.InitServers(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
tools := hub.GetTools(ctx)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, 2, len(tools))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCallTool(t *testing.T) {
|
|
||||||
hub, err := NewMCPHub("./testdata/test.mcp.json")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = hub.InitServers(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
result, err := hub.InvokeTool(ctx, "weather", "get_alerts",
|
|
||||||
map[string]interface{}{"state": "CA"},
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Logf("Result: %v", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCallEinoTool(t *testing.T) {
|
|
||||||
hub, err := NewMCPHub("./testdata/test.mcp.json")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = hub.InitServers(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
einoTool, err := hub.GetEinoTool(ctx, "weather", "get_alerts")
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Logf("Tool: %v", einoTool)
|
|
||||||
|
|
||||||
tool := einoTool.(tool.InvokableTool)
|
|
||||||
result, err := tool.InvokableRun(ctx, `{"state": "CA"}`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Logf("Result: %v", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvertToolsToRecordsFromFile(t *testing.T) {
|
|
||||||
hub, err := NewMCPHub("./testdata/test.mcp.json")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = hub.InitServers(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
tools := hub.GetTools(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
records := ConvertToolsToRecords(tools)
|
|
||||||
|
|
||||||
// Convert records to JSON
|
|
||||||
recordsJSON, err := json.Marshal(records)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Write JSON to file
|
|
||||||
err = os.WriteFile("./tools_records.json", recordsJSON, 0o644)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Logf("Tools records written to ./tools_records.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractDocStringInfo(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
docstring string
|
|
||||||
want DocStringInfo
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "complete docstring with args and returns",
|
|
||||||
docstring: `Get weather alerts for a US state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state: Two-letter US state code (e.g. CA, NY)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
alerts: List of active weather alerts for the specified state
|
|
||||||
error: Error message if the request fails
|
|
||||||
`,
|
|
||||||
want: DocStringInfo{
|
|
||||||
Description: "Get weather alerts for a US state.",
|
|
||||||
Parameters: map[string]string{
|
|
||||||
"state": "Two-letter US state code (e.g. CA, NY)",
|
|
||||||
},
|
|
||||||
Returns: map[string]string{
|
|
||||||
"alerts": "List of active weather alerts for the specified state",
|
|
||||||
"error": "Error message if the request fails",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "docstring with only args",
|
|
||||||
docstring: `Do screen swipe action.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
direction: swipe direction (up, down)
|
|
||||||
`,
|
|
||||||
want: DocStringInfo{
|
|
||||||
Description: "Do screen swipe action.",
|
|
||||||
Parameters: map[string]string{
|
|
||||||
"direction": "swipe direction (up, down)",
|
|
||||||
},
|
|
||||||
Returns: map[string]string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "docstring with only description",
|
|
||||||
docstring: "Simple tool with no parameters.",
|
|
||||||
want: DocStringInfo{
|
|
||||||
Description: "Simple tool with no parameters.",
|
|
||||||
Parameters: map[string]string{},
|
|
||||||
Returns: map[string]string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "docstring with multiple parameters",
|
|
||||||
docstring: `Perform complex operation.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
param1: first parameter description
|
|
||||||
param2: second parameter description
|
|
||||||
param3: third parameter description
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
result: operation result
|
|
||||||
`,
|
|
||||||
want: DocStringInfo{
|
|
||||||
Description: "Perform complex operation.",
|
|
||||||
Parameters: map[string]string{
|
|
||||||
"param1": "first parameter description",
|
|
||||||
"param2": "second parameter description",
|
|
||||||
"param3": "third parameter description",
|
|
||||||
},
|
|
||||||
Returns: map[string]string{
|
|
||||||
"result": "operation result",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := extractDocStringInfo(tt.docstring)
|
|
||||||
assert.Equal(t, tt.want.Description, got.Description)
|
|
||||||
assert.Equal(t, tt.want.Parameters, got.Parameters)
|
|
||||||
assert.Equal(t, tt.want.Returns, got.Returns)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvertToolsToRecords(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
toolsMap map[string]MCPTools
|
|
||||||
want []MCPToolRecord
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "convert weather tool",
|
|
||||||
toolsMap: map[string]MCPTools{
|
|
||||||
"weather": {
|
|
||||||
Name: "weather",
|
|
||||||
Tools: []mcp.Tool{
|
|
||||||
{
|
|
||||||
Name: "get_alerts",
|
|
||||||
Description: `Get weather alerts for a US state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state: Two-letter US state code (e.g. CA, NY)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
alerts: List of active weather alerts for the specified state
|
|
||||||
error: Error message if the request fails
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: []MCPToolRecord{
|
|
||||||
{
|
|
||||||
ToolID: "weather_get_alerts",
|
|
||||||
ServerName: "weather",
|
|
||||||
ToolName: "get_alerts",
|
|
||||||
Description: "Get weather alerts for a US state.",
|
|
||||||
Parameters: `{"state":"Two-letter US state code (e.g. CA, NY)"}`,
|
|
||||||
Returns: `{"alerts":"List of active weather alerts for the specified state","error":"Error message if the request fails"}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "convert multiple tools",
|
|
||||||
toolsMap: map[string]MCPTools{
|
|
||||||
"ui": {
|
|
||||||
Name: "ui",
|
|
||||||
Tools: []mcp.Tool{
|
|
||||||
{
|
|
||||||
Name: "swipe",
|
|
||||||
Description: `Do screen swipe action.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
direction: swipe direction (up, down)
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "tap",
|
|
||||||
Description: "Tap on screen at specified position.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: []MCPToolRecord{
|
|
||||||
{
|
|
||||||
ToolID: "ui_swipe",
|
|
||||||
ServerName: "ui",
|
|
||||||
ToolName: "swipe",
|
|
||||||
Description: "Do screen swipe action.",
|
|
||||||
Parameters: `{"direction":"swipe direction (up, down)"}`,
|
|
||||||
Returns: "{}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ToolID: "ui_tap",
|
|
||||||
ServerName: "ui",
|
|
||||||
ToolName: "tap",
|
|
||||||
Description: "Tap on screen at specified position.",
|
|
||||||
Parameters: "{}",
|
|
||||||
Returns: "{}",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := ConvertToolsToRecords(tt.toolsMap)
|
|
||||||
|
|
||||||
// Compare each record
|
|
||||||
require.Equal(t, len(tt.want), len(got))
|
|
||||||
for i := range tt.want {
|
|
||||||
assert.Equal(t, tt.want[i].ToolID, got[i].ToolID)
|
|
||||||
assert.Equal(t, tt.want[i].ServerName, got[i].ServerName)
|
|
||||||
assert.Equal(t, tt.want[i].ToolName, got[i].ToolName)
|
|
||||||
assert.Equal(t, tt.want[i].Description, got[i].Description)
|
|
||||||
|
|
||||||
// Compare JSON content (ignoring whitespace differences)
|
|
||||||
var wantParams, gotParams, wantReturns, gotReturns map[string]string
|
|
||||||
require.NoError(t, json.Unmarshal([]byte(tt.want[i].Parameters), &wantParams))
|
|
||||||
require.NoError(t, json.Unmarshal([]byte(got[i].Parameters), &gotParams))
|
|
||||||
require.NoError(t, json.Unmarshal([]byte(tt.want[i].Returns), &wantReturns))
|
|
||||||
require.NoError(t, json.Unmarshal([]byte(got[i].Returns), &gotReturns))
|
|
||||||
|
|
||||||
assert.Equal(t, wantParams, gotParams)
|
|
||||||
assert.Equal(t, wantReturns, gotReturns)
|
|
||||||
|
|
||||||
// Verify timestamps are recent (within last 5 seconds)
|
|
||||||
now := time.Now()
|
|
||||||
assert.True(t, now.Sub(got[i].CreatedAt) < 5*time.Second, "CreatedAt should be recent")
|
|
||||||
assert.True(t, now.Sub(got[i].LastUpdatedAt) < 5*time.Second, "LastUpdatedAt should be recent")
|
|
||||||
// CreatedAt and LastUpdatedAt should be the same
|
|
||||||
assert.Equal(t, got[i].CreatedAt, got[i].LastUpdatedAt)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-94
@@ -1,94 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
import httpx
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
|
||||||
|
|
||||||
# Initialize FastMCP server
|
|
||||||
mcp = FastMCP("weather")
|
|
||||||
|
|
||||||
# Constants
|
|
||||||
NWS_API_BASE = "https://api.weather.gov"
|
|
||||||
USER_AGENT = "weather-app/1.0"
|
|
||||||
|
|
||||||
async def make_nws_request(url: str) -> dict[str, Any] | None:
|
|
||||||
"""Make a request to the NWS API with proper error handling."""
|
|
||||||
headers = {
|
|
||||||
"User-Agent": USER_AGENT,
|
|
||||||
"Accept": "application/geo+json"
|
|
||||||
}
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
try:
|
|
||||||
response = await client.get(url, headers=headers, timeout=30.0)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def format_alert(feature: dict) -> str:
|
|
||||||
"""Format an alert feature into a readable string."""
|
|
||||||
props = feature["properties"]
|
|
||||||
return f"""
|
|
||||||
Event: {props.get('event', 'Unknown')}
|
|
||||||
Area: {props.get('areaDesc', 'Unknown')}
|
|
||||||
Severity: {props.get('severity', 'Unknown')}
|
|
||||||
Description: {props.get('description', 'No description available')}
|
|
||||||
Instructions: {props.get('instruction', 'No specific instructions provided')}
|
|
||||||
"""
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def get_alerts(state: str) -> str:
|
|
||||||
"""Get weather alerts for a US state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state: Two-letter US state code (e.g. CA, NY)
|
|
||||||
"""
|
|
||||||
url = f"{NWS_API_BASE}/alerts/active/area/{state}"
|
|
||||||
data = await make_nws_request(url)
|
|
||||||
|
|
||||||
if not data or "features" not in data:
|
|
||||||
return "Unable to fetch alerts or no alerts found."
|
|
||||||
|
|
||||||
if not data["features"]:
|
|
||||||
return "No active alerts for this state."
|
|
||||||
|
|
||||||
alerts = [format_alert(feature) for feature in data["features"]]
|
|
||||||
return "\n---\n".join(alerts)
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def get_forecast(latitude: float, longitude: float) -> str:
|
|
||||||
"""Get weather forecast for a location.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
latitude: Latitude of the location
|
|
||||||
longitude: Longitude of the location
|
|
||||||
"""
|
|
||||||
# First get the forecast grid endpoint
|
|
||||||
points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
|
|
||||||
points_data = await make_nws_request(points_url)
|
|
||||||
|
|
||||||
if not points_data:
|
|
||||||
return "Unable to fetch forecast data for this location."
|
|
||||||
|
|
||||||
# Get the forecast URL from the points response
|
|
||||||
forecast_url = points_data["properties"]["forecast"]
|
|
||||||
forecast_data = await make_nws_request(forecast_url)
|
|
||||||
|
|
||||||
if not forecast_data:
|
|
||||||
return "Unable to fetch detailed forecast."
|
|
||||||
|
|
||||||
# Format the periods into a readable forecast
|
|
||||||
periods = forecast_data["properties"]["periods"]
|
|
||||||
forecasts = []
|
|
||||||
for period in periods[:5]: # Only show next 5 periods
|
|
||||||
forecast = f"""
|
|
||||||
{period['name']}:
|
|
||||||
Temperature: {period['temperature']}°{period['temperatureUnit']}
|
|
||||||
Wind: {period['windSpeed']} {period['windDirection']}
|
|
||||||
Forecast: {period['detailedForecast']}
|
|
||||||
"""
|
|
||||||
forecasts.append(forecast)
|
|
||||||
|
|
||||||
return "\n---\n".join(forecasts)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Initialize and run the server
|
|
||||||
mcp.run(transport='stdio')
|
|
||||||
Vendored
-27
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"filesystem": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": [
|
|
||||||
"-y",
|
|
||||||
"@modelcontextprotocol/server-filesystem",
|
|
||||||
"/tmp"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"weather": {
|
|
||||||
"args": [
|
|
||||||
"--directory",
|
|
||||||
"/Users/debugtalk/MyProjects/HttpRunner-dev/httprunner/internal/mcp/testdata",
|
|
||||||
"run",
|
|
||||||
"demo_weather.py"
|
|
||||||
],
|
|
||||||
"autoApprove": [
|
|
||||||
"get_forecast"
|
|
||||||
],
|
|
||||||
"command": "uv",
|
|
||||||
"env": {
|
|
||||||
"ABC": "123"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +1 @@
|
|||||||
v5.0.0-beta-2505161406
|
v5.0.0-beta-2505161414
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package hrp
|
package hrp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -28,6 +29,7 @@ import (
|
|||||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||||
"github.com/httprunner/httprunner/v5/internal/sdk"
|
"github.com/httprunner/httprunner/v5/internal/sdk"
|
||||||
"github.com/httprunner/httprunner/v5/internal/version"
|
"github.com/httprunner/httprunner/v5/internal/version"
|
||||||
|
"github.com/httprunner/httprunner/v5/pkg/mcphost"
|
||||||
"github.com/httprunner/httprunner/v5/uixt"
|
"github.com/httprunner/httprunner/v5/uixt"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
)
|
)
|
||||||
@@ -315,6 +317,22 @@ func NewCaseRunner(testcase TestCase, hrpRunner *HRPRunner) (*CaseRunner, error)
|
|||||||
Msg("plugin info loaded")
|
Msg("plugin info loaded")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// init MCP servers
|
||||||
|
if config.MCPConfigPath != "" {
|
||||||
|
mcpHost, err := mcphost.NewMCPHost(config.MCPConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("init MCP hub failed")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = mcpHost.InitServers(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("init MCP servers failed")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
caseRunner.parser.MCPHost = mcpHost
|
||||||
|
log.Info().Str("mcpConfigPath", config.MCPConfigPath).Msg("mcp server loaded")
|
||||||
|
}
|
||||||
|
|
||||||
// parse testcase config
|
// parse testcase config
|
||||||
parsedConfig, err := caseRunner.parseConfig()
|
parsedConfig, err := caseRunner.parseConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+7
-7
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/internal/mcp"
|
"github.com/httprunner/httprunner/v5/pkg/mcphost"
|
||||||
"github.com/httprunner/httprunner/v5/uixt"
|
"github.com/httprunner/httprunner/v5/uixt"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -22,23 +22,23 @@ func NewRouter() *Router {
|
|||||||
|
|
||||||
type Router struct {
|
type Router struct {
|
||||||
*gin.Engine
|
*gin.Engine
|
||||||
mcpHub *mcp.MCPHub
|
mcpHost *mcphost.MCPHost
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) InitMCPHub(configPath string) error {
|
func (r *Router) InitMCPHost(configPath string) error {
|
||||||
mcpHub, err := mcp.NewMCPHub(configPath)
|
mcpHost, err := mcphost.NewMCPHost(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("init MCP hub failed")
|
log.Error().Err(err).Msg("init MCP host failed")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mcpHub.InitServers(context.Background())
|
err = mcpHost.InitServers(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("init MCP servers failed")
|
log.Error().Err(err).Msg("init MCP servers failed")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
r.mcpHub = mcpHub
|
r.mcpHost = mcpHost
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -13,8 +13,8 @@ type ToolRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) invokeToolHandler(c *gin.Context) {
|
func (r *Router) invokeToolHandler(c *gin.Context) {
|
||||||
if r.mcpHub == nil {
|
if r.mcpHost == nil {
|
||||||
RenderError(c, errors.New("mcp hub not initialized"))
|
RenderError(c, errors.New("mcp host not initialized"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ func (r *Router) invokeToolHandler(c *gin.Context) {
|
|||||||
req.Args["platform"] = c.Param("platform")
|
req.Args["platform"] = c.Param("platform")
|
||||||
req.Args["serial"] = c.Param("serial")
|
req.Args["serial"] = c.Param("serial")
|
||||||
|
|
||||||
result, err := r.mcpHub.InvokeTool(c.Request.Context(),
|
result, err := r.mcpHost.InvokeTool(c.Request.Context(),
|
||||||
req.ServerName, req.ToolName, req.Args)
|
req.ServerName, req.ToolName, req.Args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderError(c, err)
|
RenderError(c, err)
|
||||||
|
|||||||
+1
-1
@@ -74,7 +74,7 @@ func TestTapHandler(t *testing.T) {
|
|||||||
|
|
||||||
func TestInvokeToolHandler(t *testing.T) {
|
func TestInvokeToolHandler(t *testing.T) {
|
||||||
router := NewRouter()
|
router := NewRouter()
|
||||||
router.InitMCPHub("../internal/mcp/testdata/test.mcp.json")
|
router.InitMCPHost("../internal/mcp/testdata/test.mcp.json")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user