Skip to content

[Bug] security_score MCP tool returns schema validation error when no suggestions #178

@finn941

Description

@finn941

Repo: forest6511/secretctl
Version observed: v0.8.8 (built from source, Linux amd64)
MCP SDK: github.com/modelcontextprotocol/go-sdk v1.1.0

Summary

The security_score MCP tool fails schema validation when the vault's security score has no suggestions to return. The suggestions field serializes as JSON null (Go nil slice) instead of an empty [] array, violating the tool's output schema which declares it as array.

Reproduction

# 1. Init vault with strong master password + import healthy .env
secretctl init                                    # strong unique password
secretctl import healthy.env                      # ~10 unique well-formed secrets

# 2. Configure MCP policy + register secretctl in any MCP client
# Place mcp-policy.yaml at ~/.secretctl/mcp-policy.yaml (regular file, 0600, current user)
# Add to client's mcp config:
#   { "secretctl": { "command": "bash", "args": ["wrapper.sh"] } }

# 3. From MCP client, invoke security_score
# Expected: { "overall_score": 90+, "suggestions": [] }
# Actual:   MCP error: validating root: validating /properties/suggestions:
#                     type: <invalid reflect.Value> has type "null", want "array"

Tested under Claude Code v2.x as MCP client. Suspect any MCP client with strict schema validation (which the official Go SDK now enforces) will fail.

Root cause

internal/mcp/tools.go line 1374:

output := SecurityScoreOutput{
    ...
    Suggestions: score.Suggestions,   // ← if nil slice, marshals to JSON null
    Limited:     score.Limited,
}

SecurityScoreOutput.Suggestions is declared as []string (line 165). When the security calculator returns no suggestions (e.g., healthy vault), score.Suggestions is a nil slice. Go encoder marshals nil slice as JSON null, which violates the implied array schema that modelcontextprotocol/go-sdk validates against on tool output.

The other slice field TopIssues []SecurityIssueInfo is OK because the handler explicitly builds it via append(topIssues, info), producing a non-nil empty slice when there are no issues.

Suggested fix

Force empty slice when nil (1-line guard at line 1374):

suggestions := score.Suggestions
if suggestions == nil {
    suggestions = []string{}
}

output := SecurityScoreOutput{
    ...
    Suggestions: suggestions,
    Limited:     score.Limited,
}

Alternative: do the same normalization at score.Suggestions construction site in the security calculator (closer to source of nil). Either works.

Why this matters

  • security_score becomes unusable from MCP clients with output validation
  • Healthy vault is the most common steady state — exactly when no suggestions → bug always fires
  • Same pattern likely affects future tools that return slices with all-empty edge cases (secret_list_fields etc.) — worth a cross-cutting scan

Minimal test case

func TestSecurityScore_EmptySuggestions(t *testing.T) {
    // Setup: vault with strong password, 0 issues
    ...
    var out SecurityScoreOutput
    _, _, err := s.handleSecurityScore(ctx, req, input)
    require.NoError(t, err)
    require.NotNil(t, out.Suggestions, "suggestions must be non-nil empty slice, not nil")
    require.IsType(t, []string{}, out.Suggestions)

    // JSON round-trip
    data, _ := json.Marshal(out)
    require.Contains(t, string(data), `"suggestions":[]`, "must serialize as empty array")
    require.NotContains(t, string(data), `"suggestions":null`)
}

Environment

  • secretctl: v0.8.8 (built from source via make build with GOPROXY=https://goproxy.cn,direct GOFLAGS=-buildvcs=false)
  • Go: 1.24.3 (docker golang:1.24 image)
  • OS: Linux x86_64 (Debian 12, kernel 6.12.x)
  • MCP client: Claude Code v2.x

Happy to send a PR if the suggested fix direction is OK.


Exact error string verified 2026-05-12 via mcp__secretctl__security_score returning:
MCP error 0: validating tool output: validating root: validating /properties/suggestions: type: <invalid reflect.Value> has type "null", want "array"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions