Module: RosettAi::Mcp::Plugins

Defined in:
lib/rosett_ai/mcp/plugins.rb

Overview

Plugin auto-discovery and loading for MCP server.

Discovers installed gems named rosett-ai-mcp-* and loads them as MCP server plugins. Convention: gem rosett-ai-mcp-foo defines RosettAiMcp::Plugins::Foo with a .register(server) method.

Governance (built-in tools) is always loaded first.

Author:

  • hugo

  • claude

Constant Summary collapse

PLUGIN_MUTEX =
Mutex.new
PLUGIN_NAMESPACE =

Allowlist prefix for plugin constant resolution.

'RosettAiMcp::Plugins::'

Class Method Summary collapse

Class Method Details

.available?(name) ⇒ Boolean

Check if a plugin is available (registered or gem installed).

Parameters:

  • name (Symbol, String)

    plugin name

Returns:

  • (Boolean)


36
37
38
# File 'lib/rosett_ai/mcp/plugins.rb', line 36

def available?(name)
  PLUGIN_MUTEX.synchronize { plugin_registry.key?(name.to_sym) } || gem_available?(name)
end

.discover_plugin_gemsArray<Symbol>

Discover installed gems named rosett-ai-mcp-*.

Returns:

  • (Array<Symbol>)

    discovered plugin names



79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/rosett_ai/mcp/plugins.rb', line 79

def discover_plugin_gems
  discovered = []
  Gem::Specification.each do |spec|
    next unless spec.name.start_with?('rosett-ai-mcp-') && spec.name != 'rosett-ai-mcp'

    plugin_name = spec.name.sub('rosett-ai-mcp-', '')
    discovered << plugin_name.to_sym
  end
  discovered
rescue StandardError
  []
end

.load_all(server, plugin_names = []) ⇒ Array<Symbol>

Load all discovered plugins and register with server.

Parameters:

  • server (MCP::Server)

    the MCP server instance

  • plugin_names (Array<String>) (defaults to: [])

    explicit plugins to load

Returns:

  • (Array<Symbol>)

    loaded plugin names



45
46
47
48
49
50
51
52
53
54
# File 'lib/rosett_ai/mcp/plugins.rb', line 45

def load_all(server, plugin_names = [])
  discovered = discover_plugin_gems
  all_plugins = (discovered + plugin_names.map(&:to_sym)).uniq
  loaded = []

  all_plugins.each do |name|
    loaded << name if load_plugin(server, name)
  end
  loaded
end

.load_plugin(server, name) ⇒ Boolean

Load a single plugin by name.

Parameters:

  • server (MCP::Server)

    the MCP server instance

  • name (Symbol, String)

    plugin name

Returns:

  • (Boolean)

    true if loaded successfully



61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/rosett_ai/mcp/plugins.rb', line 61

def load_plugin(server, name)
  sym = name.to_sym
  plugin_class = PLUGIN_MUTEX.synchronize { plugin_registry[sym] }

  if plugin_class
    plugin_class.register(server)
    return true
  end

  require_and_register_gem(server, sym)
rescue StandardError, LoadError => e
  warn "[rai-mcp] Plugin load failed for #{name}: #{e.message}"
  false
end

.plugin_registryHash

Thread-safe access to the plugin store.

Returns:

  • (Hash)

    mutable registry hash



109
110
111
# File 'lib/rosett_ai/mcp/plugins.rb', line 109

def plugin_registry
  @plugin_registry ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
end

.register(name, plugin_class)

This method returns an undefined value.

Register a plugin class by name.

Parameters:

  • name (Symbol)

    plugin name

  • plugin_class (Class)

    plugin class with .register(server)



28
29
30
# File 'lib/rosett_ai/mcp/plugins.rb', line 28

def register(name, plugin_class)
  PLUGIN_MUTEX.synchronize { plugin_registry[name.to_sym] = plugin_class }
end

.registryHash

Read-only access to the plugin registry.

Returns:

  • (Hash)

    registered plugins



95
96
97
# File 'lib/rosett_ai/mcp/plugins.rb', line 95

def registry
  PLUGIN_MUTEX.synchronize { plugin_registry.dup }
end

.reset!

This method returns an undefined value.

Reset the registry (for testing).



102
103
104
# File 'lib/rosett_ai/mcp/plugins.rb', line 102

def reset!
  PLUGIN_MUTEX.synchronize { plugin_registry.clear }
end