Module: RailVerdict::MCP::Serializers

Defined in:
lib/rail_verdict/mcp/serializers.rb

Constant Summary collapse

MAX_TOOL_RESPONSE_BYTES =
256 * 1024

Class Method Summary collapse

Class Method Details

.bounded_findings(findings, limit, offset) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
# File 'lib/rail_verdict/mcp/serializers.rb', line 85

def self.bounded_findings(findings, limit, offset)
  total = findings.length
  slice = findings.slice(offset, limit) || []
  {
    "findings" => slice.map(&:to_schema_h),
    "total" => total,
    "limit" => limit,
    "offset" => offset,
    "truncated" => (offset + limit) < total
  }
end

.error_response(message, code: "invalid_arguments") ⇒ Object



79
80
81
82
83
# File 'lib/rail_verdict/mcp/serializers.rb', line 79

def self.error_response(message, code: "invalid_arguments")
  scrubbed = scrub(message)
  payload = { "code" => code, "message" => scrubbed }
  ::MCP::Tool::Response.new([{ type: "text", text: JSON.generate(payload) }], error: true, structured_content: payload)
end

.gate_result_to_structured(outcome) ⇒ Object



21
22
23
24
25
26
# File 'lib/rail_verdict/mcp/serializers.rb', line 21

def self.gate_result_to_structured(outcome)
  result = outcome.result
  h = result.to_schema_h
  h["findings"] = h["findings"] || []
  h
end

.scrub(value) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
# File 'lib/rail_verdict/mcp/serializers.rb', line 8

def self.scrub(value)
  case value
  when String
    value.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "\uFFFD").gsub(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/, "?")
  when Hash
    value.transform_keys { |k| scrub(k) }.transform_values { |v| scrub(v) }
  when Array
    value.map { |v| scrub(v) }
  else
    value
  end
end

.text_content(structured) ⇒ Object



28
29
30
# File 'lib/rail_verdict/mcp/serializers.rb', line 28

def self.text_content(structured)
  scrub(JSON.generate(structured))
end

.tool_response(structured, error: false) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/rail_verdict/mcp/serializers.rb', line 34

def self.tool_response(structured, error: false)
  text = text_content(structured)
  if text.bytesize > MAX_TOOL_RESPONSE_BYTES
    truncated = truncate_structured(structured)
    if truncated
      structured = truncated
      text = text_content(structured)
    else
      payload = { "code" => "response_too_large", "message" => "response exceeds #{MAX_TOOL_RESPONSE_BYTES} bytes; use list_findings/get_finding with pagination" }
      return ::MCP::Tool::Response.new([{ type: "text", text: JSON.generate(payload) }], error: true, structured_content: payload)
    end
  end
  ::MCP::Tool::Response.new([{ type: "text", text: text }], error: error, structured_content: structured)
end

.truncate_structured(structured) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/rail_verdict/mcp/serializers.rb', line 49

def self.truncate_structured(structured)
  return nil unless structured.is_a?(Hash)

  if structured.key?("findings") && structured["findings"].is_a?(Array) && structured["findings"].length > 10
    total = structured["findings"].length
    copy = structured.dup
    copy["findings"] = structured["findings"].first(20)
    copy["findings_truncated"] = true
    copy["total_findings"] = total
    copy["returned_findings"] = 20
    copy["truncated_due_to_size"] = true
    return copy if JSON.generate(copy).bytesize <= MAX_TOOL_RESPONSE_BYTES
  end
  if structured.key?("gate_result") && structured["gate_result"].is_a?(Hash)
    gr = structured["gate_result"]
    if gr["findings"] && gr["findings"].is_a?(Array) && gr["findings"].length > 10
      total = gr["findings"].length
      copy = structured.dup
      copy["gate_result"] = gr.dup
      copy["gate_result"]["findings"] = gr["findings"].first(20)
      copy["gate_result"]["findings_truncated"] = true
      copy["gate_result"]["total_findings"] = total
      copy["gate_result"]["returned_findings"] = 20
      copy["gate_result"]["truncated_due_to_size"] = true
      return copy if JSON.generate(copy).bytesize <= MAX_TOOL_RESPONSE_BYTES
    end
  end
  nil
end