Class: RailsAiContext::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_ai_context/server.rb

Overview

Configures and starts an MCP server using the official Ruby SDK. Registers all introspection tools and handles transport selection.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, transport: :stdio) ⇒ Server

Returns a new instance of Server.



32
33
34
35
# File 'lib/rails_ai_context/server.rb', line 32

def initialize(app, transport: :stdio)
  @app = app
  @transport_type = transport
end

Instance Attribute Details

#appObject (readonly)

Returns the value of attribute app.



9
10
11
# File 'lib/rails_ai_context/server.rb', line 9

def app
  @app
end

#transport_typeObject (readonly)

Returns the value of attribute transport_type.



9
10
11
# File 'lib/rails_ai_context/server.rb', line 9

def transport_type
  @transport_type
end

Class Method Details

.builtin_toolsObject

All built-in tools, auto-discovered from Tools::BaseTool subclasses. Kept as a class method (not a constant) so auto-registration works. Legacy constant accessor preserved for backwards compatibility.



14
15
16
# File 'lib/rails_ai_context/server.rb', line 14

def self.builtin_tools
  Tools::BaseTool.registered_tools
end

.const_missing(name) ⇒ Object

Backwards-compatible constant - delegates to the registry. Existing code referencing Server::TOOLS continues to work. Emits a deprecation notice once to guide migration.



21
22
23
24
25
26
27
28
29
30
# File 'lib/rails_ai_context/server.rb', line 21

def self.const_missing(name)
  if name == :TOOLS
    unless @tools_deprecation_warned
      @tools_deprecation_warned = true
      $stderr.puts "[rails-ai-context] DEPRECATION: Server::TOOLS is deprecated, use Server.builtin_tools instead" if ENV["DEBUG"]
    end
    return builtin_tools
  end
  super
end

.resolve_custom_tools(config = RailsAiContext.configuration) ⇒ Object

Resolve config.custom_tools into MCP::Tool classes. Entries may be classes or class-name strings: classes in app/ (e.g. app/mcp_tools/) are not autoloadable while config/initializers run, so referencing the constant there aborts boot - a string name defers resolution to here, where autoloading is ready. Invalid entries are warn-skipped so one bad entry cannot take down every tool invocation.



43
44
45
46
47
48
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
78
79
80
81
82
83
84
# File 'lib/rails_ai_context/server.rb', line 43

def self.resolve_custom_tools(config = RailsAiContext.configuration)
  config.custom_tools.filter_map do |entry|
    tool = entry
    if tool.is_a?(String)
      # NameError messages never carry a leading "::", so normalize it
      # away up front or a missing "::Foo::Bar" entry could never be
      # classified as class-not-found below.
      tool = tool.delete_prefix("::")
      begin
        tool = Object.const_get(tool)
      rescue NameError => e
        # "class not found" only when the missing constant IS the entry
        # (or a leading namespace of it); a NameError raised from inside
        # the tool file's class body about some other constant means the
        # class exists but is broken - say so. The full constant path in
        # the message disambiguates where a bare #name cannot (a body
        # error about `Elastic::Search` must not read as entry
        # "Tools::Search" being absent).
        missing = e.message[/\Auninitialized constant ([\w:]+)/, 1]
        entry_missing = missing && (tool == missing || tool.start_with?("#{missing}::"))
        if entry_missing
          $stderr.puts "[rails-ai-context] WARNING: Skipping custom_tool #{entry.inspect} (class not found)"
        else
          $stderr.puts "[rails-ai-context] WARNING: Skipping custom_tool #{entry.inspect} (#{e.class}: #{e.message.lines.first.to_s.strip})"
        end
        next nil
      rescue StandardError, ScriptError => e
        # A syntax error or raising class body in the autoloaded file must
        # cost that one entry, not the whole server/CLI.
        $stderr.puts "[rails-ai-context] WARNING: Skipping custom_tool #{entry.inspect} (#{e.class}: #{e.message.lines.first.to_s.strip})"
        next nil
      end
    end

    if tool.is_a?(Class) && tool < MCP::Tool
      tool
    else
      $stderr.puts "[rails-ai-context] WARNING: Skipping invalid custom_tool #{entry.inspect} (must be an MCP::Tool subclass or its class name)"
      nil
    end
  end
end

Instance Method Details

#buildObject

Build and return the configured MCP::Server instance



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/rails_ai_context/server.rb', line 87

def build
  config = RailsAiContext.configuration

  validated_custom_tools = self.class.resolve_custom_tools(config)

  mcp_config = MCP::Configuration.new(
    # Anything that still escapes a tool (schema validation bugs, SDK-level
    # failures) gets a stderr backtrace instead of vanishing into a bare
    # JSON-RPC internal error. Routine protocol-level errors (unknown
    # tool, invalid params) are expected traffic, not bugs - the mcp gem
    # already turns them into a proper JSON-RPC error response, so here
    # they get one quiet line instead of a scary 10-line backtrace.
    exception_reporter: lambda { |exception, _server_context|
      if exception.is_a?(MCP::Server::RequestHandlerError) && exception.error_type != :internal_error
        $stderr.puts "[rails-ai-context] request error (#{exception.error_type}): #{exception.message}"
      else
        $stderr.puts "[rails-ai-context] unhandled exception: #{exception.class}: #{exception.message}"
        Array(exception.backtrace).first(10).each { |line| $stderr.puts "    #{line}" }
      end
    },
    instrumentation_callback: Instrumentation.callback
  )

  server = MCP::Server.new(
    name: config.server_name,
    version: config.server_version,
    instructions: "Ground truth engine for Rails apps. Live Prism AST introspection. Zero stale data.",
    tools: active_tools(config) + validated_custom_tools,
    resource_templates: Resources.resource_templates,
    configuration: mcp_config
  )

  Resources.register(server)

  server
end

#startObject

Start the MCP server with the configured transport



125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/rails_ai_context/server.rb', line 125

def start
  server = build

  case transport_type
  when :stdio
    start_stdio(server)
  when :http, :streamable_http
    start_http(server)
  else
    raise ConfigurationError, "Unknown transport: #{transport_type}. Use :stdio or :http"
  end
end