Module: McpAuthorization::Diagnostics

Defined in:
lib/mcp_authorization/diagnostics.rb

Overview

Shared diagnostic helpers used by both the field-level RbsSchemaCompiler#predicate_excluded? and the tool-level Tool#gates_pass? paths.

Lives in its own module so the two predicate sites — field-level (compile time) and tool-level (request time) — share a single “Did you mean?” warning implementation. Avoids two copies of Levenshtein drifting.

Class Method Summary collapse

Class Method Details

.levenshtein(a, b) ⇒ Integer

Minimal Levenshtein distance for typo suggestions.

Iterative two-row implementation — O(m*n) time, O(m) extra space. Used only in development for “Did you mean?” suggestions, so the naive version is fine.

: (String, String) -> Integer

Parameters:

  • a (String)
  • b (String)

Returns:

  • (Integer)


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/mcp_authorization/diagnostics.rb', line 68

def levenshtein(a, b)
  m, n = a.length, b.length
  d = Array.new(m + 1) { |i| i }
  (1..n).each do |j|
    prev = d[0]
    d[0] = j
    (1..m).each do |i|
      cost = a[i - 1] == b[j - 1] ? 0 : 1
      temp = d[i]
      d[i] = [d[i] + 1, d[i - 1] + 1, prev + cost].min
      prev = temp
    end
  end
  d[m]
end

.warn_unknown_predicate(name, server_context, site:) ⇒ void

This method returns an undefined value.

Emit a development-mode warning when a predicate method is not found on the server context. Helps catch typos like @feture(:x) or gate :feture, :sms.

: (String | Symbol, untyped, Symbol) -> void

Parameters:

  • name (String, Symbol)

    The predicate name attempted (e.g. “feature” or :gate).

  • server_context (Object)

    The context the predicate was looked up on.

  • site (Symbol)

    :field or :tool — controls the suggestion phrasing.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/mcp_authorization/diagnostics.rb', line 22

def warn_unknown_predicate(name, server_context, site:)
  return unless defined?(Rails) && Rails.respond_to?(:env) && Rails.env.local?

  str = name.to_s
  available = server_context.class.public_instance_methods(true)
    .select { |m| m.to_s.end_with?("?") }
    .map { |m| m.to_s.chomp("?") }
  best = available.min_by { |a| levenshtein(a, str) }
  suggestion = best && levenshtein(best, str) <= 3 ? best : nil

  hint =
    case site
    when :tool  then suggestion ? " Did you mean gate :#{suggestion}?" : ""
    when :field then suggestion ? " Did you mean @#{suggestion}?" : ""
    else             suggestion ? " Did you mean #{suggestion}?" : ""
    end

  surface =
    case site
    when :tool  then "Gate predicate"
    when :field then "Predicate"
    else             "Predicate"
    end

  fallthrough =
    case site
    when :tool  then "Tool will be shown to all users."
    when :field then "Field will be shown to all users."
    else             ""
    end

  Rails.logger&.warn(
    "[McpAuthorization] #{surface} '#{str}?' not found on #{server_context.class}.#{hint} #{fallthrough}".strip
  )
end