Class: Homebrew::McpServer Private
Overview
This class is part of a private API. This class may only be used in the Homebrew/brew repository. Third parties should avoid using this class if possible, as it may be removed or changed without warning.
Provides a Model Context Protocol (MCP) server for Homebrew. See https://modelcontextprotocol.io/introduction for more information.
https://modelcontextprotocol.io/docs/tools/inspector is useful for testing.
Constant Summary collapse
- HOMEBREW_BREW_FILE =
This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.
T.let(ENV.fetch("HOMEBREW_BREW_FILE").freeze, String)
- HOMEBREW_VERSION =
This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.
T.let(ENV.fetch("HOMEBREW_VERSION").freeze, String)
- JSON_RPC_VERSION =
This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.
T.let("2.0", String)
- MCP_PROTOCOL_VERSION =
This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.
T.let("2025-03-26", String)
- ERROR_CODE =
This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.
T.let(-32601, Integer)
- SERVER_INFO =
This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.
T.let({ name: "brew-mcp-server", version: HOMEBREW_VERSION, }.freeze, T::Hash[Symbol, String])
- FORMULA_OR_CASK_PROPERTIES =
This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.
T.let({ formula_or_cask: { type: "string", description: "Formula or cask name", }, }.freeze, T::Hash[Symbol, T.anything])
- TOOLS =
This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.
Note:Cursor (as of June 2025) will only query/use a maximum of 40 tools.
T.let({ search: { name: "search", description: "Perform a substring search of cask tokens and formula names for <text>. " \ "If <text> is flanked by slashes, it is interpreted as a regular expression.", command: "brew search", inputSchema: { type: "object", properties: { text_or_regex: { type: "string", description: "Text or regex to search for", }, }, }, required: ["text_or_regex"], }, info: { name: "info", description: "Display brief statistics for your Homebrew installation. " \ "If a <formula> or <cask> is provided, show summary of information about it.", command: "brew info", inputSchema: { type: "object", properties: FORMULA_OR_CASK_PROPERTIES }, }, install: { name: "install", description: "Install a <formula> or <cask>.", command: "brew install", inputSchema: { type: "object", properties: FORMULA_OR_CASK_PROPERTIES }, required: ["formula_or_cask"], }, update: { name: "update", description: "Fetch the newest version of Homebrew and all formulae from GitHub using `git` and " \ "perform any necessary migrations.", command: "brew update", inputSchema: { type: "object", properties: {} }, }, upgrade: { name: "upgrade", description: "Upgrade outdated casks and outdated, unpinned formulae using the same options they were " \ "originally installed with, plus any appended brew formula options. If <cask> or <formula> " \ "are specified, upgrade only the given <cask> or <formula> kegs (unless they are pinned).", command: "brew upgrade", inputSchema: { type: "object", properties: FORMULA_OR_CASK_PROPERTIES }, }, uninstall: { name: "uninstall", description: "Uninstall a <formula> or <cask>.", command: "brew uninstall", inputSchema: { type: "object", properties: FORMULA_OR_CASK_PROPERTIES }, required: ["formula_or_cask"], }, list: { name: "list", description: "List all installed formulae and casks. " \ "If <formula> is provided, summarise the paths within its current keg. " \ "If <cask> is provided, list its artifacts.", command: "brew list", inputSchema: { type: "object", properties: FORMULA_OR_CASK_PROPERTIES }, }, config: { name: "config", description: "Show Homebrew and system configuration info useful for debugging. " \ "If you file a bug report, you will be required to provide this information.", command: "brew config", inputSchema: { type: "object", properties: {} }, }, doctor: { name: "doctor", description: "Check your system for potential problems. Will exit with a non-zero status " \ "if any potential problems are found. " \ "Please note that these warnings are just used to help the Homebrew maintainers " \ "with debugging if you file an issue. If everything you use Homebrew for " \ "is working fine: please don't worry or file an issue; just ignore this.", command: "brew doctor", inputSchema: { type: "object", properties: {} }, }, typecheck: { name: "typecheck", description: "Check for typechecking errors using Sorbet.", command: "brew typecheck", inputSchema: { type: "object", properties: {} }, }, style: { name: "style", description: "Check formulae or files for conformance to Homebrew style guidelines.", command: "brew style", inputSchema: { type: "object", properties: { fix: { type: "boolean", description: "Fix style violations automatically using RuboCop's auto-correct feature", }, files: { type: "string", description: "Specific files to check (space-separated)", }, changed: { type: "boolean", description: "Only check files that were changed from the `main` branch", }, }, }, }, tests: { name: "tests", description: "Run Homebrew's unit and integration tests.", command: "brew tests", inputSchema: { type: "object", properties: { only: { type: "string", description: "Specific tests to run (comma-seperated) e.g. for `<file>_spec.rb` pass `<file>`. " \ "Appending `:<line_number>` will start at a specific line", }, fail_fast: { type: "boolean", description: "Exit early on the first failing test", }, changed: { type: "boolean", description: "Only runs tests on files that were changed from the `main` branch", }, online: { type: "boolean", description: "Run online tests", }, }, }, }, commands: { name: "commands", description: "Show lists of built-in and external commands.", command: "brew commands", inputSchema: { type: "object", properties: {} }, }, help: { name: "help", description: "Outputs the usage instructions for `brew` <command>.", command: "brew help", inputSchema: { type: "object", properties: { command: { type: "string", description: "Command to get help for", }, }, }, }, }.freeze, T::Hash[Symbol, T::Hash[Symbol, T.anything]])
Instance Method Summary collapse
- #debug(text) ⇒ void private
- #debug_logging? ⇒ Boolean private
- #handle_request(request) ⇒ Hash{Symbol => T.anything}? private
- #initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr) ⇒ void constructor private
- #log(text) ⇒ void private
- #ping_switch? ⇒ Boolean private
- #respond_error(id, message) ⇒ Hash{Symbol => T.anything} private
- #respond_result(id = nil, result = {}) ⇒ Hash{Symbol => T.anything}? private
- #respond_to_tools_call(id, request) ⇒ Hash{Symbol => T.anything}? private
- #run ⇒ void private
- #tool_command_arguments(tool_name, arguments) ⇒ Array<String> private
Constructor Details
#initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
191 192 193 194 195 196 197 |
# File 'mcp_server.rb', line 191 def initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr) @debug_logging = T.let(ARGV.include?("--debug") || ARGV.include?("-d"), T::Boolean) @ping_switch = T.let(ARGV.include?("--ping"), T::Boolean) @stdin = T.let(stdin, T.any(IO, StringIO)) @stdout = T.let(stdout, T.any(IO, StringIO)) @stderr = T.let(stderr, T.any(IO, StringIO)) end |
Instance Method Details
#debug(text) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
243 244 245 246 247 |
# File 'mcp_server.rb', line 243 def debug(text) return unless debug_logging? log(text) end |
#debug_logging? ⇒ Boolean
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
200 |
# File 'mcp_server.rb', line 200 def debug_logging? = @debug_logging |
#handle_request(request) ⇒ Hash{Symbol => T.anything}?
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'mcp_server.rb', line 256 def handle_request(request) id = request["id"] return if id.nil? case request["method"] when "initialize" respond_result(id, { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: { tools: { listChanged: false }, prompts: {}, resources: {}, logging: {}, roots: {}, }, serverInfo: SERVER_INFO, }) when "resources/list" respond_result(id, { resources: [] }) when "resources/templates/list" respond_result(id, { resourceTemplates: [] }) when "prompts/list" respond_result(id, { prompts: [] }) when "ping" respond_result(id) when "get_server_info" respond_result(id, SERVER_INFO) when "logging/setLevel" @debug_logging = request["params"]["level"] == "debug" respond_result(id) when "notifications/initialized", "notifications/cancelled" respond_result when "tools/list" respond_result(id, { tools: TOOLS.values }) when "tools/call" respond_to_tools_call(id, request) else respond_error(id, "Method not found") end end |
#log(text) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
250 251 252 253 |
# File 'mcp_server.rb', line 250 def log(text) @stderr.puts(text) @stderr.flush end |
#ping_switch? ⇒ Boolean
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
203 |
# File 'mcp_server.rb', line 203 def ping_switch? = @ping_switch |
#respond_error(id, message) ⇒ Hash{Symbol => T.anything}
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
399 400 401 |
# File 'mcp_server.rb', line 399 def respond_error(id, ) { jsonrpc: JSON_RPC_VERSION, id:, error: { code: ERROR_CODE, message: } } end |
#respond_result(id = nil, result = {}) ⇒ Hash{Symbol => T.anything}?
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
392 393 394 395 396 |
# File 'mcp_server.rb', line 392 def respond_result(id = nil, result = {}) return if id.nil? { jsonrpc: JSON_RPC_VERSION, id:, result: } end |
#respond_to_tools_call(id, request) ⇒ Hash{Symbol => T.anything}?
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 |
# File 'mcp_server.rb', line 298 def respond_to_tools_call(id, request) tool_name = request["params"]["name"].to_sym tool = TOOLS.fetch tool_name do return respond_error(id, "Unknown tool") end require "open3" command_args = tool_command_arguments(tool_name, request["params"]["arguments"]) progress_token = request["params"]["_meta"]&.fetch("progressToken", nil) brew_command = T.cast(tool.fetch(:command), String) .delete_prefix("brew ") buffer_size = 4096 # 4KB progress = T.let(0, Integer) done = T.let(false, T::Boolean) new_output = T.let(false, T::Boolean) output = +"" text = Open3.popen2e(HOMEBREW_BREW_FILE, brew_command, *command_args) do |stdin, io, _wait| stdin.close reader = Thread.new do loop do output << io.readpartial(buffer_size) progress += 1 new_output = true end rescue EOFError nil ensure done = true end until done break unless progress_token sleep 1 next unless new_output response = { jsonrpc: JSON_RPC_VERSION, method: "notifications/progress", params: { progressToken: progress_token, progress: }, } progress_output = JSON.dump(response).strip @stdout.puts(progress_output) @stdout.flush new_output = false end reader.join output end respond_result(id, { content: [{ type: "text", text: }] }) end |
#run ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
# File 'mcp_server.rb', line 206 def run @stderr.puts "==> Started Homebrew MCP server..." loop do input = if ping_switch? { jsonrpc: JSON_RPC_VERSION, id: 1, method: "ping" }.to_json else break if @stdin.eof? @stdin.gets end next if input.nil? || input.strip.empty? request = JSON.parse(input) debug("Request: #{JSON.pretty_generate(request)}") response = handle_request(request) if response.nil? debug("Response: nil") next end debug("Response: #{JSON.pretty_generate(response)}") output = JSON.dump(response).strip @stdout.puts(output) @stdout.flush break if ping_switch? end rescue Interrupt exit 0 rescue => e log("Error: #{e.}") exit 1 end |
#tool_command_arguments(tool_name, arguments) ⇒ Array<String>
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 |
# File 'mcp_server.rb', line 358 def tool_command_arguments(tool_name, arguments) require "shellwords" case tool_name when :style style_args = [] style_args << "--fix" if arguments["fix"] style_args << "--changed" if arguments["changed"] file_arguments = arguments.fetch("files", "").strip.split style_args.concat(file_arguments) unless file_arguments.empty? style_args when :tests tests_args = [] only_arguments = arguments.fetch("only", "").strip tests_args << "--only=#{only_arguments}" unless only_arguments.empty? tests_args << "--fail-fast" if arguments["fail_fast"] tests_args << "--changed" if arguments["changed"] tests_args << "--online" if arguments["online"] tests_args when :search [arguments["text_or_regex"]] when :help [arguments["command"]] else [arguments["formula_or_cask"]] end.compact .reject(&:empty?) .map { |arg| Shellwords.escape(arg) } end |