Module: RubynCode::MCP::Discovery

Defined in:
lib/rubyn_code/mcp/discovery.rb

Overview

Discovers MCP server definitions from a project's .mcp.json file (the Claude Code–style layout at the project root) and merges them with the user-level .rubyn-code/mcp.json definitions parsed by MCP::Config.

Project-level entries are tagged source: :project so /mcp can distinguish them from user-level entries.

Defined Under Namespace

Classes: Entry

Constant Summary collapse

PROJECT_CONFIG_FILENAME =
'.mcp.json'

Class Method Summary collapse

Class Method Details

.build_entry(name, server_def, source) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/rubyn_code/mcp/discovery.rb', line 71

def build_entry(name, server_def, source)
  return nil unless server_def.is_a?(Hash)

  command = server_def['command'].to_s
  url     = server_def['url'].to_s

  if command.empty? && url.empty?
    RubynCode::Debug.warn("[MCP::Discovery] Skipping #{name}: no command or url")
    return nil
  end

  Entry.new(
    name: name,
    command: command,
    args: Array(server_def['args']),
    env: server_def['env'].is_a?(Hash) ? server_def['env'].transform_keys(&:to_s) : {},
    url: url,
    source: source
  )
end

.discover(project_root) ⇒ Array<Entry>

Parameters:

  • project_root (String, nil)

    project root (nil => nothing discovered)

Returns:



24
25
26
27
28
# File 'lib/rubyn_code/mcp/discovery.rb', line 24

def discover(project_root)
  user  = MCP::Config.load(project_root.to_s).map { |cfg| to_entry(cfg, :user) }
  proj  = load_project(project_root)
  user + proj
end

.load_project(project_root) ⇒ Array<Entry>

Parameters:

  • project_root (String, nil)

Returns:



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/rubyn_code/mcp/discovery.rb', line 32

def load_project(project_root)
  return [] if project_root.to_s.empty?

  path = File.join(project_root, PROJECT_CONFIG_FILENAME)
  return [] unless File.exist?(path)

  data = JSON.parse(File.read(path))
  Array(data['mcpServers']).filter_map do |name, server_def|
    build_entry(name, server_def, :project)
  end
rescue JSON::ParserError => e
  RubynCode::Debug.warn("[MCP::Discovery] Failed to parse #{path}: #{e.message}")
  []
rescue SystemCallError => e
  RubynCode::Debug.warn("[MCP::Discovery] Could not read #{path}: #{e.message}")
  []
end

.remote_servers(entries) ⇒ Array<Entry>

Returns entries that need a network protocol (deferred).

Returns:

  • (Array<Entry>)

    entries that need a network protocol (deferred)



56
57
58
# File 'lib/rubyn_code/mcp/discovery.rb', line 56

def remote_servers(entries)
  entries.select { |e| e.command.to_s.empty? && !e.url.to_s.empty? }
end

.stdio_servers(entries) ⇒ Array<Entry>

Returns entries that the local runner can start right now.

Returns:

  • (Array<Entry>)

    entries that the local runner can start right now



51
52
53
# File 'lib/rubyn_code/mcp/discovery.rb', line 51

def stdio_servers(entries)
  entries.reject { |e| e.command.to_s.empty? }
end

.to_entry(cfg, source) ⇒ Object



60
61
62
63
64
65
66
67
68
69
# File 'lib/rubyn_code/mcp/discovery.rb', line 60

def to_entry(cfg, source)
  Entry.new(
    name: cfg[:name],
    command: cfg[:command],
    args: cfg[:args],
    env: cfg[:env],
    url: cfg[:url],
    source: source
  )
end