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 =

Returns Mutex for thread-safe plugin registration.

Returns:

  • (Mutex)

    Mutex for thread-safe plugin registration.

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)


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

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



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

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



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

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



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

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



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

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

.register(name, plugin_class) ⇒ void

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)



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

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



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

def registry
  PLUGIN_MUTEX.synchronize { plugin_registry.dup }
end

.reset!void

This method returns an undefined value.

Reset the registry (for testing).



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

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