diff --git a/cmd/mcptools/commands/call.go b/cmd/mcptools/commands/call.go index 4039400..990e1ac 100644 --- a/cmd/mcptools/commands/call.go +++ b/cmd/mcptools/commands/call.go @@ -33,33 +33,7 @@ func CallCmd() *cobra.Command { os.Exit(1) } - cmdArgs := args - parsedArgs := []string{} - entityName := "" - - i := 0 - entityExtracted := false - - for i < len(cmdArgs) { - switch { - case (cmdArgs[i] == FlagFormat || cmdArgs[i] == FlagFormatShort) && i+1 < len(cmdArgs): - FormatOption = cmdArgs[i+1] - i += 2 - case (cmdArgs[i] == FlagParams || cmdArgs[i] == FlagParamsShort) && i+1 < len(cmdArgs): - ParamsString = cmdArgs[i+1] - i += 2 - case !entityExtracted: - entityName = cmdArgs[i] - entityExtracted = true - i++ - case cmdArgs[i] == FlagServerLogs: - ShowServerLogs = true - i++ - default: - parsedArgs = append(parsedArgs, cmdArgs[i]) - i++ - } - } + parsedArgs, entityName := parseCallArguments(args) if entityName == "" { fmt.Fprintln(os.Stderr, "Error: entity name is required") @@ -148,3 +122,38 @@ func CallCmd() *cobra.Command { }, } } + +// fix cyclomatic complexity. +func parseCallArguments(args []string) ([]string, string) { + cmdArgs := args + parsedArgs := []string{} + entityName := "" + + i := 0 + entityExtracted := false + + for i < len(cmdArgs) { + switch { + case (cmdArgs[i] == FlagFormat || cmdArgs[i] == FlagFormatShort) && i+1 < len(cmdArgs): + FormatOption = cmdArgs[i+1] + i += 2 + case (cmdArgs[i] == FlagParams || cmdArgs[i] == FlagParamsShort) && i+1 < len(cmdArgs): + ParamsString = cmdArgs[i+1] + i += 2 + case !entityExtracted: + entityName = cmdArgs[i] + entityExtracted = true + i++ + case cmdArgs[i] == FlagServerLogs: + ShowServerLogs = true + i++ + case cmdArgs[i] == FlagVerbose || cmdArgs[i] == FlagVerboseShort: + Verbose = true + i++ + default: + parsedArgs = append(parsedArgs, cmdArgs[i]) + i++ + } + } + return parsedArgs, entityName +} diff --git a/cmd/mcptools/commands/get_prompt.go b/cmd/mcptools/commands/get_prompt.go index 30ff06a..c2c6569 100644 --- a/cmd/mcptools/commands/get_prompt.go +++ b/cmd/mcptools/commands/get_prompt.go @@ -50,6 +50,9 @@ func GetPromptCmd() *cobra.Command { case cmdArgs[i] == FlagServerLogs: ShowServerLogs = true i++ + case args[i] == FlagVerbose || args[i] == FlagVerboseShort: + Verbose = true + i++ case !promptExtracted: promptName = cmdArgs[i] promptExtracted = true diff --git a/cmd/mcptools/commands/http.go b/cmd/mcptools/commands/http.go new file mode 100644 index 0000000..b5d5a6e --- /dev/null +++ b/cmd/mcptools/commands/http.go @@ -0,0 +1,35 @@ +package commands + +import ( + "fmt" + "net/http" + "os" +) + +var verbose = &http.Client{ + Transport: verbosed{http.DefaultTransport}, +} + +type verbosed struct { + impl http.RoundTripper +} + +func verboseHeader(dir byte, header map[string][]string) { + for k, a := range header { + for _, v := range a { + fmt.Fprintf(os.Stderr, "%c %s: %s\n", dir, k, v) + } + } +} + +func (v verbosed) RoundTrip(req *http.Request) (*http.Response, error) { + verboseHeader('>', req.Header) + fmt.Fprintf(os.Stderr, "> \n") + resp, err := v.impl.RoundTrip(req) + if resp != nil { + verboseHeader('<', resp.Header) + fmt.Fprintf(os.Stderr, "< \n") + } + fmt.Fprintf(os.Stderr, "\n") + return resp, err +} diff --git a/cmd/mcptools/commands/root.go b/cmd/mcptools/commands/root.go index 2bae0e2..125b5c3 100644 --- a/cmd/mcptools/commands/root.go +++ b/cmd/mcptools/commands/root.go @@ -17,6 +17,8 @@ const ( FlagHelpShort = "-h" FlagServerLogs = "--server-logs" FlagTransport = "--transport" + FlagVerbose = "--verbose" + FlagVerboseShort = "-v" ) // entity types. @@ -35,6 +37,8 @@ var ( ParamsString string // ShowServerLogs is a flag to show server logs. ShowServerLogs bool + // Verbose show http verbose info. + Verbose bool // TransportOption is the transport option for HTTP connections, valid values are "sse" and "http". // Default is "http" (streamable HTTP). TransportOption = "http" @@ -52,6 +56,7 @@ It allows you to discover and call tools, list resources, and interact with MCP- cmd.PersistentFlags().StringVarP(&FormatOption, "format", "f", "table", "Output format (table, json, pretty)") cmd.PersistentFlags(). StringVarP(&ParamsString, "params", "p", "{}", "JSON string of parameters to pass to the tool (for call command)") + cmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "Show http verbose info") cmd.PersistentFlags().StringVar(&TransportOption, "transport", "http", "HTTP transport type (http, sse)") return cmd diff --git a/cmd/mcptools/commands/shell.go b/cmd/mcptools/commands/shell.go index 280e87a..69df784 100644 --- a/cmd/mcptools/commands/shell.go +++ b/cmd/mcptools/commands/shell.go @@ -231,7 +231,9 @@ func callCommand(thisCmd *cobra.Command, mcpClient *client.Client, commandArgs [ var toolResponse *mcp.CallToolResult request := mcp.CallToolRequest{} request.Params.Name = entityName - request.Params.Arguments = params + if len(params) > 0 { + request.Params.Arguments = params // fix unittest "tool_name without params" + } toolResponse, execErr = mcpClient.CallTool(context.Background(), request) if execErr == nil && toolResponse != nil { resp = ConvertJSONToMap(toolResponse) diff --git a/cmd/mcptools/commands/trace.go b/cmd/mcptools/commands/trace.go new file mode 100644 index 0000000..37abb4c --- /dev/null +++ b/cmd/mcptools/commands/trace.go @@ -0,0 +1,28 @@ +package commands + +import ( + "context" + "crypto/rand" + + "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/client/transport" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +func generateTraceID() [16]byte { + var traceID [16]byte + _, _ = rand.Read(traceID[:]) + return traceID +} + +func injectTrace(ctx context.Context) transport.ClientOption { + sc := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: generateTraceID(), + SpanID: trace.SpanID([8]byte{255}), + }) + ctx = trace.ContextWithRemoteSpanContext(ctx, sc) + carrier := propagation.MapCarrier{} + propagation.TraceContext{}.Inject(ctx, carrier) + return client.WithHeaders(carrier) +} diff --git a/cmd/mcptools/commands/utils.go b/cmd/mcptools/commands/utils.go index 63031ea..2ced70a 100644 --- a/cmd/mcptools/commands/utils.go +++ b/cmd/mcptools/commands/utils.go @@ -11,8 +11,8 @@ import ( "github.com/f/mcptools/pkg/alias" "github.com/f/mcptools/pkg/jsonutils" "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/client/transport" "github.com/mark3labs/mcp-go/mcp" - "github.com/spf13/cobra" ) @@ -47,13 +47,17 @@ var CreateClientFunc = func(args []string, _ ...client.ClientOption) (*client.Cl var err error if len(args) == 1 && IsHTTP(args[0]) { + opts := []transport.ClientOption{injectTrace(context.Background())} + if Verbose { + opts = append(opts, transport.WithHTTPClient(verbose)) + } // Validate transport option for HTTP URLs if TransportOption != "http" && TransportOption != "sse" { return nil, fmt.Errorf("invalid transport option: %s (supported: http, sse)", TransportOption) } if TransportOption == "sse" { - c, err = client.NewSSEMCPClient(args[0]) + c, err = client.NewSSEMCPClient(args[0], opts...) } else { // Default to streamable HTTP c, err = client.NewStreamableHttpClient(args[0]) @@ -121,6 +125,9 @@ func ProcessFlags(args []string) []string { case args[i] == FlagServerLogs: ShowServerLogs = true i++ + case args[i] == FlagVerbose || args[i] == FlagVerboseShort: + Verbose = true + i++ default: parsedArgs = append(parsedArgs, args[i]) i++ diff --git a/cmd/mcptools/commands/version.go b/cmd/mcptools/commands/version.go index 7633d5c..852b92d 100644 --- a/cmd/mcptools/commands/version.go +++ b/cmd/mcptools/commands/version.go @@ -10,7 +10,7 @@ import ( // Version information placeholder. var Version = "dev" -// getHomeDirectory returns the user's home directory +// getHomeDirectory returns the user's home directory. // Tries HOME first, then falls back to USERPROFILE for Windows. func getHomeDirectory() string { homeDir := os.Getenv("HOME") diff --git a/cmd/mcptools/commands/web.go b/cmd/mcptools/commands/web.go index 8e61e77..1c10f2d 100644 --- a/cmd/mcptools/commands/web.go +++ b/cmd/mcptools/commands/web.go @@ -1269,20 +1269,20 @@ func handleCall(cache *MCPClientCache) http.HandlerFunc { defer cache.mutex.Unlock() switch requestData.Type { - case "tool": + case EntityTypeTool: var toolResponse *mcp.CallToolResult request := mcp.CallToolRequest{} request.Params.Name = requestData.Name request.Params.Arguments = requestData.Params toolResponse, callErr = cache.client.CallTool(context.Background(), request) resp = ConvertJSONToMap(toolResponse) - case "resource": + case EntityTypeRes: var resourceResponse *mcp.ReadResourceResult request := mcp.ReadResourceRequest{} request.Params.URI = requestData.Name resourceResponse, callErr = cache.client.ReadResource(context.Background(), request) resp = ConvertJSONToMap(resourceResponse) - case "prompt": + case EntityTypePrompt: var promptResponse *mcp.GetPromptResult request := mcp.GetPromptRequest{} request.Params.Name = requestData.Name diff --git a/go.mod b/go.mod index 96905dc..5b522cf 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,13 @@ module github.com/f/mcptools go 1.24.1 require ( - github.com/mark3labs/mcp-go v0.24.1 + github.com/mark3labs/mcp-go v0.32.0 github.com/peterh/liner v1.2.2 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 + go.opentelemetry.io/otel v1.29.0 + go.opentelemetry.io/otel/trace v1.29.0 golang.org/x/term v0.30.0 golang.org/x/text v0.23.0 ) @@ -15,6 +17,8 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -30,6 +34,7 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/sys v0.31.0 // indirect diff --git a/go.sum b/go.sum index 0426ba3..b2788b4 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,11 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -18,10 +23,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mark3labs/mcp-go v0.23.1 h1:RzTzZ5kJ+HxwnutKA4rll8N/pKV6Wh5dhCmiJUu5S9I= -github.com/mark3labs/mcp-go v0.23.1/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/mark3labs/mcp-go v0.24.1 h1:YV+5X/+W4oBdERLWgiA1uR7AIvenlKJaa5V4hqufI7E= github.com/mark3labs/mcp-go v0.24.1/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= +github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8= +github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -59,6 +64,12 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=