diff --git a/CHANGELOG.md b/CHANGELOG.md index b6cbdba..b1ddaf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ # https-wrench - changelog +## Unreleased + +### Feat + + MCP: update all MCP tool descriptions and parameter schemas to consistently suggest `--format json` CLI examples with standard `path/to/...` placeholders, and default `build_cli_command` to `--format json` output. + + MCP: add rich `jsonschema` parameter annotations to `certinfoInput` and `jwtinfoInput` for agent schema discovery. + ## 0.15.3 (2026-09-15) ### Dependencies diff --git a/README.md b/README.md index 74b00a2..1f3b24a 100644 --- a/README.md +++ b/README.md @@ -412,12 +412,13 @@ commands for all subcommands (`author_requests_config`, `inspect_certificate`, `inspect_jwt`, `generate_jwks`). **Tools (assist):** `validate_requests_config`, `requests_config_template`, -`build_cli_command` (suggesting `format: "json"` for machine-readable output). +`build_cli_command` (defaults to `--format json` for machine-readable output; +all tool descriptions provide `--format json` CLI examples). **Tools (execution):** `run_requests`, `certinfo`, `jwtinfo`, `generate_jwks` -(all return structured JSON results). Encrypted private keys for `certinfo` -require the `CERTINFO_PKEY_PW` environment variable (no interactive prompt -under MCP). +(all return structured JSON results and reference equivalent `--format json` CLI commands). +Encrypted private keys for `certinfo` require the `CERTINFO_PKEY_PW` environment +variable (no interactive prompt under MCP). ## Sample output diff --git a/internal/cmd/requests.go b/internal/cmd/requests.go index 324e17f..e6a2173 100644 --- a/internal/cmd/requests.go +++ b/internal/cmd/requests.go @@ -43,6 +43,7 @@ https://github.com/xenOs76/https-wrench/blob/main/https-wrench.schema.json Examples: https-wrench requests --show-sample-config > https-wrench-sample-config.yaml https-wrench requests --config https-wrench-sample-config.yaml + https-wrench requests --config https-wrench-sample-config.yaml --format json `, Run: func(cmd *cobra.Command, _ []string) { diff --git a/internal/mcp/coverage_test.go b/internal/mcp/coverage_test.go index 375c2fe..68e7f89 100644 --- a/internal/mcp/coverage_test.go +++ b/internal/mcp/coverage_test.go @@ -137,6 +137,22 @@ func TestBuildCLICommand(t *testing.T) { require.Contains(t, cmd, "https-wrench jwks") require.Contains(t, cmd, "--format json") + // Omitting format defaults to format: json + cmd, errs = buildCLICommand("jwks", map[string]string{ + "public-key-file": "/keys/pub.pem", + }) + require.Empty(t, errs) + require.Contains(t, cmd, "--format json") + + // Explicit format: text is preserved + cmd, errs = buildCLICommand("jwks", map[string]string{ + "public-key-file": "/keys/pub.pem", + "format": "text", + }) + require.Empty(t, errs) + require.Contains(t, cmd, "--format text") + require.NotContains(t, cmd, "--format json") + cmd, errs = buildCLICommand("jwtinfo", map[string]string{"token-file": "t.jwt", "format": "json"}) require.Empty(t, errs) require.Contains(t, cmd, "--format json") diff --git a/internal/mcp/prompts.go b/internal/mcp/prompts.go index 305877e..0f97588 100644 --- a/internal/mcp/prompts.go +++ b/internal/mcp/prompts.go @@ -14,7 +14,10 @@ func registerPrompts(server *sdkmcp.Server) { Arguments: []*sdkmcp.PromptArgument{ {Name: "hostname", Description: "Application hostname (hosts[].name)", Required: true}, {Name: "paths", Description: "Comma-separated URI paths starting with / (default /)"}, - {Name: "transport_override_url", Description: "Optional https:// dial URL for transportOverrideUrl"}, + { + Name: "transport_override_url", + Description: "Optional https:// dial URL for transportOverrideUrl", + }, {Name: "insecure", Description: "Set to true to skip TLS verification for this request"}, {Name: "method", Description: "HTTP method (default HEAD)"}, }, diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go index 59fa23f..5887361 100644 --- a/internal/mcp/server_test.go +++ b/internal/mcp/server_test.go @@ -42,7 +42,7 @@ func TestMCPServer_listsFeatures(t *testing.T) { "jwtinfo", "generate_jwks", }, toolNames) - require.Contains(t, buildCLIDesc, `format: "json"`) + require.Contains(t, buildCLIDesc, "--format json") var resourceURIs []string @@ -175,6 +175,7 @@ func TestBuildCLICommand_quotedFlag(t *testing.T) { }) require.Empty(t, out["errors"]) require.Contains(t, out["command"], `'host with spaces:443'`) + require.Contains(t, out["command"], `--format json`) } func TestValidateRequestsConfig_valid(t *testing.T) { @@ -240,7 +241,24 @@ func TestBuildCLICommand_certinfo(t *testing.T) { }) require.Empty(t, out["errors"]) - require.Equal(t, "https-wrench certinfo --tls-endpoint example.com:443 --tls-info true", out["command"]) + require.Equal(t, + "https-wrench certinfo --format json --tls-endpoint example.com:443 --tls-info true", + out["command"], + ) + + outText := callBuildCLITool(t, map[string]any{ + "command": "certinfo", + "flags": map[string]any{ + "tls-endpoint": "example.com:443", + "tls-info": "true", + "format": "text", + }, + }) + require.Empty(t, outText["errors"]) + require.Equal(t, + "https-wrench certinfo --format text --tls-endpoint example.com:443 --tls-info true", + outText["command"], + ) } func TestBuildCLICommand_jwksMissingRequired(t *testing.T) { @@ -255,6 +273,27 @@ func TestBuildCLICommand_jwksMissingRequired(t *testing.T) { require.Empty(t, out["command"]) } +func TestMCPTools_descriptionsSuggestFormatJSON(t *testing.T) { + t.Parallel() + + ctx := context.Background() + session, cleanup, err := mcpserver.RunInMemory(ctx, "test") + require.NoError(t, err) + + defer cleanup() + + for tool, err := range session.Tools(ctx, nil) { + require.NoError(t, err) + require.NotEmpty(t, tool.Description) + require.Contains(t, + tool.Description, + "--format json", + "tool %s description should suggest --format json", + tool.Name, + ) + } +} + func TestRequestsConfigTemplate(t *testing.T) { t.Parallel() diff --git a/internal/mcp/tools.go b/internal/mcp/tools.go index d0b6caf..9988999 100644 --- a/internal/mcp/tools.go +++ b/internal/mcp/tools.go @@ -36,7 +36,7 @@ type requestsConfigTemplateOutput struct { type buildCLICommandInput struct { Command string `json:"command" jsonschema:"Subcommand: certinfo, jwtinfo, jwks, or requests"` - Flags map[string]string `json:"flags" jsonschema:"Flag names to values (use format: json for JSON output)"` + Flags map[string]string `json:"flags" jsonschema:"Flag names to values (defaults to format: json)"` } type buildCLICommandOutput struct { @@ -57,19 +57,22 @@ type parsedRequestsConfig struct { func registerTools(server *sdkmcp.Server) { sdkmcp.AddTool(server, &sdkmcp.Tool{ - Name: "validate_requests_config", - Description: "Parse and structurally validate a https-wrench requests YAML configuration", + Name: "validate_requests_config", + Description: "Parse and structurally validate a https-wrench requests YAML configuration " + + "(run with 'https-wrench requests --config path/to/file.yaml --format json')", }, validateRequestsConfigHandler) sdkmcp.AddTool(server, &sdkmcp.Tool{ - Name: "requests_config_template", - Description: "Generate a starter requests YAML configuration from high-level parameters", + Name: "requests_config_template", + Description: "Generate a starter requests YAML configuration from high-level parameters " + + "(use with 'https-wrench requests --config path/to/file.yaml --format json')", }, requestsConfigTemplateHandler) sdkmcp.AddTool(server, &sdkmcp.Tool{ Name: "build_cli_command", Description: "Build a shell-ready https-wrench CLI command for certinfo, jwtinfo, jwks, or requests " + - "(pass format: \"json\" for machine-readable output)", + "(defaults to --format json, " + + "e.g. 'https-wrench certinfo --tls-endpoint example.com:443 --format json')", }, buildCLICommandHandler) registerExecTools(server) @@ -198,7 +201,9 @@ func validateRequestHost(prefix string, index int, host requests.Host) []string for ui, uri := range host.URIList { if !uri.Parse() { - errs = append(errs, fmt.Sprintf("%s.uriList[%d]: path %q must start with /", hostPrefix, ui, uri)) + errs = append(errs, + fmt.Sprintf("%s.uriList[%d]: path %q must start with /", hostPrefix, ui, uri), + ) } } @@ -333,18 +338,29 @@ func buildCLICommand(command string, flags map[string]string) (string, []string) } } - errs := validateCLIFlags(command, def, flags) + effectiveFlags := make(map[string]string, len(flags)+1) + for k, v := range flags { + effectiveFlags[k] = v + } + + if _, hasFormat := effectiveFlags["format"]; !hasFormat { + if _, allowed := def.allowedFlags["format"]; allowed { + effectiveFlags["format"] = "json" + } + } + + errs := validateCLIFlags(command, def, effectiveFlags) if len(errs) > 0 { return "", errs } - names := sortedAllowedFlagNames(def, flags) + names := sortedAllowedFlagNames(def, effectiveFlags) var parts []string parts = append(parts, "https-wrench", command) for _, name := range names { - parts = append(parts, "--"+name, shellQuote(flags[name])) + parts = append(parts, "--"+name, shellQuote(effectiveFlags[name])) } return strings.Join(parts, " "), nil diff --git a/internal/mcp/tools_exec.go b/internal/mcp/tools_exec.go index fb4144c..ad0d624 100644 --- a/internal/mcp/tools_exec.go +++ b/internal/mcp/tools_exec.go @@ -38,22 +38,22 @@ type runRequestsInput struct { } type certinfoInput struct { - CaBundle string `json:"caBundle,omitempty"` - CertBundle string `json:"certBundle,omitempty"` - KeyFile string `json:"keyFile,omitempty"` - TLSEndpoint string `json:"tlsEndpoint,omitempty"` - TLSServername string `json:"tlsServername,omitempty"` - TLSInsecure bool `json:"tlsInsecure,omitempty"` - TLSInfo bool `json:"tlsInfo,omitempty"` - TimeoutSec int `json:"timeoutSec,omitempty"` + CaBundle string `json:"caBundle,omitempty" jsonschema:"Optional CA bundle PEM file path"` + CertBundle string `json:"certBundle,omitempty" jsonschema:"PEM certificate bundle file path"` + KeyFile string `json:"keyFile,omitempty" jsonschema:"PEM key path (use CERTINFO_PKEY_PW env)"` + TLSEndpoint string `json:"tlsEndpoint,omitempty" jsonschema:"TLS endpoint host:port"` + TLSServername string `json:"tlsServername,omitempty" jsonschema:"Optional SNI server name"` + TLSInsecure bool `json:"tlsInsecure,omitempty" jsonschema:"Skip TLS certificate verification"` + TLSInfo bool `json:"tlsInfo,omitempty" jsonschema:"Probe negotiated and supported TLS info"` + TimeoutSec int `json:"timeoutSec,omitempty" jsonschema:"Timeout in seconds (default 60)"` } type jwtinfoInput struct { - TokenFile string `json:"tokenFile,omitempty"` - RequestURL string `json:"requestUrl,omitempty"` - RequestValues map[string]string `json:"requestValues,omitempty"` - ValidationURL string `json:"validationUrl,omitempty"` - TimeoutSec int `json:"timeoutSec,omitempty"` + TokenFile string `json:"tokenFile,omitempty" jsonschema:"File path containing JWT token string"` + RequestURL string `json:"requestUrl,omitempty" jsonschema:"OAuth/OIDC token endpoint URL"` + RequestValues map[string]string `json:"requestValues,omitempty" jsonschema:"Key-value pairs for token request"` + ValidationURL string `json:"validationUrl,omitempty" jsonschema:"Remote JWKS URL for verification"` + TimeoutSec int `json:"timeoutSec,omitempty" jsonschema:"Timeout in seconds (default 60)"` } type generateJWKSInput struct { @@ -82,23 +82,27 @@ func (mcpFileReader) ReadPassword(_ int) ([]byte, error) { func registerExecTools(server *sdkmcp.Server) { sdkmcp.AddTool(server, &sdkmcp.Tool{ - Name: "run_requests", - Description: "Execute https-wrench requests from inline YAML or a config file path", + Name: "run_requests", + Description: "Execute https-wrench requests from inline YAML or a config file path " + + "(returns JSON; CLI: https-wrench requests --config path/to/file.yaml --format json)", }, runRequestsHandler) sdkmcp.AddTool(server, &sdkmcp.Tool{ - Name: "certinfo", - Description: "Inspect x.509 certificates and keys from local files or a TLS endpoint", + Name: "certinfo", + Description: "Inspect x.509 certificates and keys from local files or a TLS endpoint " + + "(returns JSON; CLI: https-wrench certinfo --tls-endpoint example.com:443 --format json)", }, certinfoHandler) sdkmcp.AddTool(server, &sdkmcp.Tool{ - Name: "jwtinfo", - Description: "Inspect JWT tokens from a file or token endpoint (no refresh loop)", + Name: "jwtinfo", + Description: "Inspect JWT tokens from a file or token endpoint (no refresh loop) " + + "(returns JSON; CLI: https-wrench jwtinfo --token-file path/to/token.jwt --format json)", }, jwtinfoHandler) sdkmcp.AddTool(server, &sdkmcp.Tool{ - Name: "generate_jwks", - Description: "Generate a JSON Web Key Set from a PEM public key file", + Name: "generate_jwks", + Description: "Generate a JSON Web Key Set from a PEM public key file " + + "(returns JSON; CLI: https-wrench jwks --public-key-file path/to/public.pem --format json)", }, generateJWKSHandler) } diff --git a/internal/mcp/tools_exec_test.go b/internal/mcp/tools_exec_test.go index 72d793d..4dda695 100644 --- a/internal/mcp/tools_exec_test.go +++ b/internal/mcp/tools_exec_test.go @@ -235,6 +235,7 @@ func TestBuildCLICommand_jwtinfo(t *testing.T) { }) require.Empty(t, out["errors"]) require.Contains(t, out["command"], "jwtinfo") + require.Contains(t, out["command"], "--format json") } func TestBuildCLICommand_requests(t *testing.T) { @@ -248,6 +249,7 @@ func TestBuildCLICommand_requests(t *testing.T) { }) require.Empty(t, out["errors"]) require.Contains(t, out["command"], "requests") + require.Contains(t, out["command"], "--format json") } func TestResources_readDocs(t *testing.T) {