Module: McpToolkit

Defined in:
lib/mcp_toolkit.rb,
lib/mcp_toolkit/version.rb,
lib/mcp_toolkit/engine_controllers.rb,
sig/mcp_toolkit.rbs

Overview

Lazy parent_controller builder (Constraint B).

The engine's controllers (McpToolkit::ServerController, McpToolkit::TokensController) and the authority base (McpToolkit::Authority::ServerController) all subclass config.parent_controller. If that superclass were resolved in a class body of an autoloaded/eager-loaded file, it could be read BEFORE the host's initializer/to_prepare had set it — defaulting to ActionController::Base and, in turn, breaking CSRF handling on the introspection endpoint.

Instead, none of these controllers is a Zeitwerk-managed file. They are built here from the CURRENT config, and the build is triggered LAZILY:

* `const_missing` (below, and on McpToolkit::Authority) builds them the first
time they are referenced — which, in a host, is at eager-load or first-
request time, i.e. AFTER the app's initializers/to_prepare have run;
* the engine's `config.to_prepare` RESETS them on every code reload so a
changed `parent_controller` (or a reloaded app parent class) takes effect on
the next reference.

The whole config/initializers/mcp_toolkit.rb of a host can therefore live in to_prepare: the parent is only ever read at build time.

This file reopens McpToolkit to add module methods, so it is Zeitwerk-IGNORED (like engine.rb) and required explicitly from the gem entry point.

Defined Under Namespace

Modules: Authority, Filtering, Protocol, Server, TokenKinds Classes: Configuration, Dispatcher, Engine, Error, FieldSelection, GetExecutor, ListExecutor, RateLimiter, Registry, Resource, ResourceSchema, Serialization, Session, SqlSanitizer, UnknownResourceMessage

Constant Summary collapse

VERSION =

Returns:

  • (String)
"0.4.0"
ENGINE_CONTROLLER_NAMES =

The controllers built directly under McpToolkit (the engine's routes point at these). McpToolkit::Authority::ServerController is built alongside them but is fetched through McpToolkit::Authority's own const_missing.

%i[ServerController TokensController].freeze

Class Method Summary collapse

Class Method Details

.build_authority_server_controller(parent) ⇒ Object

The AUTHORITY base controller a host subclasses (the recommended path for a host whose rate-limit/usage/account hooks touch app models).



93
94
95
# File 'lib/mcp_toolkit/engine_controllers.rb', line 93

def self.build_authority_server_controller(parent)
  Class.new(parent) { include McpToolkit::Authority::ControllerMethods }
end

.build_engine_controllers!Object

(Re)builds the engine controllers + the authority base from the current config. Idempotent: an existing constant is replaced so a rebuild reflects a changed parent_controller. Reads config.parent_controller at call time.



36
37
38
39
40
41
42
# File 'lib/mcp_toolkit/engine_controllers.rb', line 36

def self.build_engine_controllers!
  parent = config.parent_controller.constantize
  define_controller(self, :ServerController, build_server_controller(parent))
  define_controller(self, :TokensController, build_tokens_controller(parent))
  define_controller(Authority, :ServerController, build_authority_server_controller(parent))
  ServerController
end

.build_server_controller(parent) ⇒ Object

The SATELLITE transport controller the engine mounts (unchanged behavior; the SDK-backed path). Built lazily only so its parent is read after config.



54
55
56
# File 'lib/mcp_toolkit/engine_controllers.rb', line 54

def self.build_server_controller(parent)
  Class.new(parent) { include McpToolkit::Transport::ControllerMethods }
end

.build_tokens_controller(parent) ⇒ Object

The AUTHORITY introspection endpoint the engine mounts at POST /mcp/tokens/introspect. Behavior is preserved exactly: it authenticates the bearer against config.token_authenticator (via Auth::Authority) and renders the introspection payload; a non-authority app answers { valid: false } rather than erroring.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/mcp_toolkit/engine_controllers.rb', line 63

def self.build_tokens_controller(parent)
  Class.new(parent) do
    def introspect
      token = McpToolkit::Auth::Authority.authenticate(mcp_extract_token, config: mcp_config)
      return render(json: McpToolkit::Auth::Authority.invalid_payload, status: :unauthorized) if token.nil?

      render json: McpToolkit::Auth::Authority.introspection_payload(token)
    rescue McpToolkit::Errors::ConfigurationError
      # Not configured as an authority (no token_authenticator): behave as if the
      # token were invalid instead of surfacing a 500.
      render json: McpToolkit::Auth::Authority.invalid_payload, status: :unauthorized
    end

    private

    def mcp_config
      McpToolkit.config
    end

    def mcp_extract_token
      auth_header = request.headers["Authorization"]
      return auth_header.sub("Bearer ", "") if auth_header&.start_with?("Bearer ")

      request.headers["X-MCP-Token"].presence || params[:token].presence
    end
  end
end

.configObject

The active Configuration (created on first access).



107
108
109
# File 'lib/mcp_toolkit.rb', line 107

def self.config
  @config ||= Configuration.new
end

.configure {|config| ... } ⇒ Object

Yields the active configuration for mutation, returning it.

McpToolkit.configure do |c|
c.server_name = "my-app-mcp"
c.central_app_url = ENV.fetch("MCP_CENTRAL_APP_URL")
c.registry.default_required_permissions_scope "my_app__read"
end

Yields:



101
102
103
104
# File 'lib/mcp_toolkit.rb', line 101

def self.configure
  yield(config) if block_given?
  config
end

.const_missing(name) ⇒ Object

Backstop: build the engine controllers the first time one is referenced before any to_prepare/eager-load pass has built them (e.g. a bespoke route that names McpToolkit::ServerController directly). McpToolkit::Authority defines the sibling backstop for its ServerController.



108
109
110
111
112
113
114
115
# File 'lib/mcp_toolkit/engine_controllers.rb', line 108

def self.const_missing(name)
  if ENGINE_CONTROLLER_NAMES.include?(name)
    build_engine_controllers!
    return const_get(name) if const_defined?(name, false)
  end

  super
end

.define_controller(mod, name, klass) ⇒ Object

Removes an existing same-named constant (avoiding a redefinition warning on a rebuild) before setting the freshly-built class.



99
100
101
102
# File 'lib/mcp_toolkit/engine_controllers.rb', line 99

def self.define_controller(mod, name, klass)
  mod.send(:remove_const, name) if mod.const_defined?(name, false)
  mod.const_set(name, klass)
end

.registryObject

The active config's resource registry. Register resources against this in a boot initializer.



113
114
115
# File 'lib/mcp_toolkit.rb', line 113

def self.registry
  config.registry
end

.reset_config!Object

Replaces the active configuration with a fresh default. Primarily for tests.



118
119
120
# File 'lib/mcp_toolkit.rb', line 118

def self.reset_config!
  @config = Configuration.new
end

.reset_engine_controllers!Object

Undefines the built controllers so the next reference rebuilds them from the then-current config. Called from the engine's to_prepare on every reload.



46
47
48
49
50
# File 'lib/mcp_toolkit/engine_controllers.rb', line 46

def self.reset_engine_controllers!
  [[self, :ServerController], [self, :TokensController], [Authority, :ServerController]].each do |mod, name|
    mod.send(:remove_const, name) if mod.const_defined?(name, false)
  end
end