From a58ccffb280e902d678004c67b40b2b33a71278a Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Fri, 16 May 2025 14:30:24 +0800 Subject: [PATCH] feat: call CallMCPTool in parser --- internal/version/VERSION | 2 +- parser.go | 45 +++++++++++++++++++++++++++++++++++----- parser_test.go | 27 ++++++++++++++++++++---- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index afbae8b4..5886a8ba 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505161414 +v5.0.0-beta-2505161430 diff --git a/parser.go b/parser.go index 8a5cee32..8d2f6f1c 100644 --- a/parser.go +++ b/parser.go @@ -1,6 +1,7 @@ package hrp import ( + "context" builtinJSON "encoding/json" "fmt" "net/url" @@ -18,14 +19,20 @@ import ( "github.com/httprunner/funplugin/fungo" "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" + "github.com/httprunner/httprunner/v5/pkg/mcphost" + mcp2 "github.com/mark3labs/mcp-go/mcp" ) func NewParser() *Parser { - return &Parser{} + return &Parser{ + ctx: context.Background(), + } } type Parser struct { - Plugin funplugin.IPlugin // plugin is used to call functions + ctx context.Context + Plugin funplugin.IPlugin // plugin is used to call functions + MCPHost *mcphost.MCPHost } func buildURL(baseURL, stepURL string, queryParams url.Values) (fullUrl *url.URL) { @@ -213,7 +220,7 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} return raw, err } - result, err := p.callFunc(funcName, parsedArgs.([]interface{})...) + result, err := p.CallFunc(funcName, parsedArgs.([]interface{})...) if err != nil { log.Error().Str("funcName", funcName).Interface("arguments", arguments). Err(err).Msg("call function failed") @@ -275,9 +282,9 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} return parsedString, nil } -// callFunc calls function with arguments +// CallFunc calls function with arguments // only support return at most one result value -func (p *Parser) callFunc(funcName string, arguments ...interface{}) (interface{}, error) { +func (p *Parser) CallFunc(funcName string, arguments ...interface{}) (interface{}, error) { // call with plugin function if p.Plugin != nil { if p.Plugin.Has(funcName) { @@ -300,6 +307,34 @@ func (p *Parser) callFunc(funcName string, arguments ...interface{}) (interface{ return fungo.CallFunc(fn, arguments...) } +// CallMCPTool calls a MCP tool on a specific MCP server +func (p *Parser) CallMCPTool(serverName, funcName string, arguments map[string]interface{}) (interface{}, error) { + if p.MCPHost == nil { + return nil, fmt.Errorf("mcphost is not initialized") + } + + tools := p.MCPHost.GetTools(p.ctx) + log.Warn().Interface("tools", tools).Msg("tools") + + result, err := p.MCPHost.InvokeTool(p.ctx, serverName, funcName, arguments) + if err != nil { + return nil, errors.Wrapf(err, "invoke tool %s/%s failed", serverName, funcName) + } + if result.IsError { + return nil, fmt.Errorf("invoke tool %s/%s failed: %v", serverName, funcName, result.Content) + } + + // extract text content + var resultText string + for _, item := range result.Content { + if contentMap, ok := item.(mcp2.TextContent); ok { + resultText += fmt.Sprintf("%v ", contentMap.Text) + } + } + + return resultText, nil +} + // merge two variables mapping, the first variables have higher priority func mergeVariables(variables, overriddenVariables map[string]interface{}) map[string]interface{} { if overriddenVariables == nil { diff --git a/parser_test.go b/parser_test.go index c90b94e2..67353db3 100644 --- a/parser_test.go +++ b/parser_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/httprunner/httprunner/v5/internal/version" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -433,26 +434,44 @@ func TestCallBuiltinFunction(t *testing.T) { parser := NewParser() // call function without arguments - _, err := parser.callFunc("get_timestamp") + _, err := parser.CallFunc("get_timestamp") assert.Nil(t, err) // call function with one argument timeStart := time.Now() - _, err = parser.callFunc("sleep", 1) + _, err = parser.CallFunc("sleep", 1) assert.Nil(t, err) assert.Greater(t, time.Since(timeStart), time.Duration(1)*time.Second) // call function with one argument - result, err := parser.callFunc("gen_random_string", 10) + result, err := parser.CallFunc("gen_random_string", 10) assert.Nil(t, err) assert.Equal(t, 10, len(result.(string))) // call function with two argument - result, err = parser.callFunc("max", float64(10), 9.99) + result, err = parser.CallFunc("max", float64(10), 9.99) assert.Nil(t, err) assert.Equal(t, float64(10), result.(float64)) } +func TestCallMCPTool(t *testing.T) { + // Create a new case runner for testing + caseRunner, err := NewCaseRunner(TestCase{ + Config: &TConfig{ + MCPConfigPath: "pkg/mcphost/testdata/test.mcp.json", + }, + }, nil) + require.Nil(t, err) + + parser := caseRunner.GetParser() + + resp, err := parser.CallMCPTool("filesystem", "read_file", + map[string]interface{}{"path": "internal/version/VERSION"}) + assert.Nil(t, err) + t.Logf("resp: %v", resp) + assert.Contains(t, resp, version.VERSION) +} + func TestLiteralEval(t *testing.T) { testData := []struct { expr string