Module: RobotLab::MCP::ServerDiscovery Private

Defined in:
lib/robot_lab/mcp/server_discovery.rb

Overview

This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.

Selects relevant MCP servers for a given user query using TF cosine similarity between the query and each server’s topic text (name + description).

This is used as a fallback mechanism when a robot has many MCP servers configured but only some are relevant to a particular user message. Instead of connecting to all servers upfront, the robot can enable discovery so only the semantically matching servers are connected.

Usage

robot = RobotLab.build(
  name: "assistant",
  mcp_discovery: true,
  mcp: [
    {
      name: "filesystem",
      description: "Read, write, and search local files and directories",
      transport: { type: "stdio", command: "mcp-server-fs" }
    },
    {
      name: "github",
      description: "GitHub repos, issues, pull requests, code search",
      transport: { type: "stdio", command: "mcp-server-github" }
    },
    {
      name: "brew",
      description: "Install, update, and manage macOS packages via Homebrew",
      transport: { type: "stdio", command: "mcp-server-brew" }
    }
  ]
)

# Discovery connects only the :brew server for this query:
robot.run("install imagemagick")

Fallback Behaviour

The full server list is returned unchanged when:

  • No server has a :description field

  • The ‘classifier’ gem is unavailable

  • The query is blank

  • No server scores at or above threshold (minimum relevance)

Constant Summary collapse

DEFAULT_THRESHOLD =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Minimum cosine similarity score for a server to be considered relevant. Low by design — server descriptions are short, so scores are naturally low even for on-topic queries.

0.05

Class Method Summary collapse

Class Method Details

.any_descriptions?(servers) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parameters:

Returns:

  • (Boolean)


82
83
84
# File 'lib/robot_lab/mcp/server_discovery.rb', line 82

def self.any_descriptions?(servers)
  servers.any? { |s| !description_for(s).empty? }
end

.description_for(server) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns description or empty string.

Parameters:

Returns:

  • (String)

    description or empty string



97
98
99
100
# File 'lib/robot_lab/mcp/server_discovery.rb', line 97

def self.description_for(server)
  desc = server.is_a?(Hash) ? server[:description] : server.respond_to?(:description) && server.description
  desc.to_s.strip
end

.score(query, server) ⇒ Float

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns cosine similarity in [0.0, 1.0].

Parameters:

Returns:

  • (Float)

    cosine similarity in [0.0, 1.0]



105
106
107
# File 'lib/robot_lab/mcp/server_discovery.rb', line 105

def self.score(query, server)
  TextAnalysis.tf_cosine_similarity(query, topic_text(server))
end

.select(query, from:, threshold: DEFAULT_THRESHOLD) ⇒ Array<Hash, MCP::Server>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Select MCP servers relevant to the given query.

Parameters:

  • query (String)

    user message or intent

  • from (Array<Hash, MCP::Server>)

    candidate server configs

  • threshold (Float) (defaults to: DEFAULT_THRESHOLD)

    minimum cosine score (default 0.05)

Returns:

  • (Array<Hash, MCP::Server>)

    matching servers, or from as fallback when no match is found



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/robot_lab/mcp/server_discovery.rb', line 63

def self.select(query, from:, threshold: DEFAULT_THRESHOLD)
  return from if from.empty?
  return from if query.to_s.strip.empty?
  return from unless any_descriptions?(from)

  TextAnalysis.require_classifier!

  scored = from.map { |server| [server, score(query, server)] }
  matches = scored.select { |_, s| s >= threshold }.map(&:first)

  matches.empty? ? from : matches
rescue DependencyError
  # Classifier gem not available — connect to all servers
  from
end

.topic_text(server) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Build the topic string used for similarity scoring: name + description.

Parameters:

Returns:

  • (String)


90
91
92
93
# File 'lib/robot_lab/mcp/server_discovery.rb', line 90

def self.topic_text(server)
  name = server.is_a?(Hash) ? server[:name].to_s : server.name.to_s
  "#{name} #{description_for(server)}".strip
end