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.
"2.0"- 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.
"2025-06-18"- SUPPORTED_PROTOCOL_VERSIONS =
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.
Earlier revisions a client may request; the tools below are valid for all of them.
T.let([MCP_PROTOCOL_VERSION, "2025-03-26"].freeze, T::Array[String])
- PARSE_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.
-32700
- INVALID_REQUEST_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.
-32600
- METHOD_NOT_FOUND_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.
-32601
- INVALID_PARAMS_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.
-32602
- PACKAGE_TYPES =
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.
%w[formula cask].freeze
- INLINE_CASK_DSL_REGEX =
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.
Reject inline
cask ... do/cask(...) {}blocks the cask loader would eval as Ruby. /\A\s*cask\s*['"(]/- 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])
- PACKAGE_TYPE_PROPERTY =
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({ type: "string", enum: PACKAGE_TYPES, description: "Only consider formulae or only casks", }.freeze, T::Hash[Symbol, T.anything])
- 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", }, package_type: PACKAGE_TYPE_PROPERTY, }.freeze, T::Hash[Symbol, T.anything])
- READ_ONLY_ANNOTATIONS =
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.
Hints for clients about what a tool does, see https://modelcontextprotocol.io/specification/2025-06-18/server/tools#tool-annotations
T.let({ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }.freeze, T::Hash[Symbol, T::Boolean])
- READ_ONLY_ONLINE_ANNOTATIONS =
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.
Read-only tools that may also reach the network, e.g. to refresh the API cache.
T.let(READ_ONLY_ANNOTATIONS.merge(openWorldHint: true).freeze, T::Hash[Symbol, T::Boolean])
- 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", title: "Search formulae and casks", 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.", inputSchema: { type: "object", properties: { text_or_regex: { type: "string", description: "Text or regex to search for", }, package_type: PACKAGE_TYPE_PROPERTY, }, required: ["text_or_regex"], }, annotations: READ_ONLY_ONLINE_ANNOTATIONS, }, info: { name: "info", title: "Formula or cask information", description: "Display brief statistics for your Homebrew installation. " \ "If a <formula> or <cask> is provided, show summary of information about it.", inputSchema: { type: "object", properties: FORMULA_OR_CASK_PROPERTIES }, annotations: READ_ONLY_ONLINE_ANNOTATIONS, }, install: { name: "install", title: "Install a formula or cask", description: "Install a <formula> or <cask>.", inputSchema: { type: "object", properties: FORMULA_OR_CASK_PROPERTIES, required: ["formula_or_cask"] }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, }, update: { name: "update", title: "Update Homebrew", description: "Fetch the newest version of Homebrew and all formulae from GitHub using `git` and " \ "perform any necessary migrations.", inputSchema: { type: "object", properties: {} }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, }, upgrade: { name: "upgrade", title: "Upgrade outdated packages", description: "Upgrade outdated, unpinned packages 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> (unless they are pinned).", inputSchema: { type: "object", properties: FORMULA_OR_CASK_PROPERTIES }, annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, }, uninstall: { name: "uninstall", title: "Uninstall a formula or cask", description: "Uninstall a <formula> or <cask>.", inputSchema: { type: "object", properties: FORMULA_OR_CASK_PROPERTIES, required: ["formula_or_cask"] }, annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, }, list: { name: "list", title: "List installed packages", 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.", inputSchema: { type: "object", properties: FORMULA_OR_CASK_PROPERTIES }, annotations: READ_ONLY_ANNOTATIONS, }, config: { name: "config", title: "Homebrew configuration", description: "Show Homebrew and system configuration info useful for debugging. " \ "If you file a bug report, you will be required to provide this information.", inputSchema: { type: "object", properties: {} }, annotations: READ_ONLY_ANNOTATIONS, }, doctor: { name: "doctor", title: "Check for problems", 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.", inputSchema: { type: "object", properties: {} }, annotations: READ_ONLY_ANNOTATIONS, }, typecheck: { name: "typecheck", title: "Typecheck Homebrew's code", description: "Check for typechecking errors using Sorbet.", inputSchema: { type: "object", properties: {} }, annotations: READ_ONLY_ANNOTATIONS, }, style: { name: "style", title: "Check or fix code style", description: "Check formulae or files for conformance to Homebrew style guidelines.", 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", }, }, }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, tests: { name: "tests", title: "Run Homebrew's tests", description: "Run Homebrew's unit and integration tests.", inputSchema: { type: "object", properties: { only: { type: "string", description: "Specific tests to run (comma-separated) 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", }, }, }, annotations: READ_ONLY_ONLINE_ANNOTATIONS, }, commands: { name: "commands", title: "List brew commands", description: "Show lists of built-in and external commands.", inputSchema: { type: "object", properties: {} }, annotations: READ_ONLY_ANNOTATIONS, }, help: { name: "help", title: "Show command usage", description: "Outputs the usage instructions for a built-in `brew` <command>.", inputSchema: { type: "object", properties: { command: { type: "string", description: "Built-in command or alias to get help for", }, }, }, annotations: READ_ONLY_ANNOTATIONS, }, }.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, code, message) ⇒ Hash{Symbol => T.anything} private
- #respond_result(id = nil, result = {}) ⇒ Hash{Symbol => T.anything}? private
- #respond_to_tools_call(id, params) ⇒ Hash{Symbol => T.anything}? private
- #run ⇒ void private
- #tool_command_arguments(tool_name, arguments) ⇒ Array<String> private
- #tool_result(text, error:) ⇒ Hash{Symbol => T.anything} 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.
236 237 238 239 240 241 242 |
# File 'mcp_server.rb', line 236 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 = stdin @stdout = stdout @stderr = stderr 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.
296 297 298 299 300 |
# File 'mcp_server.rb', line 296 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.
245 |
# File 'mcp_server.rb', line 245 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.
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 309 def handle_request(request) id = request["id"] return if id.nil? return respond_error(nil, INVALID_REQUEST_CODE, "Invalid request") if !id.is_a?(Numeric) && !id.is_a?(String) params = request["params"] || {} return respond_error(id, INVALID_PARAMS_CODE, "Invalid params") unless params.is_a?(Hash) case request["method"] when "initialize" requested_version = params["protocolVersion"] protocol_version = if SUPPORTED_PROTOCOL_VERSIONS.include?(requested_version) requested_version else MCP_PROTOCOL_VERSION end respond_result(id, { protocolVersion: protocol_version, capabilities: { tools: { listChanged: false }, prompts: {}, resources: {}, logging: {}, }, 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 "logging/setLevel" @debug_logging = 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, params) else respond_error(id, METHOD_NOT_FOUND_CODE, "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.
303 304 305 306 |
# File 'mcp_server.rb', line 303 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.
248 |
# File 'mcp_server.rb', line 248 def ping_switch? = @ping_switch |
#respond_error(id, code, 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.
501 502 503 |
# File 'mcp_server.rb', line 501 def respond_error(id, code, ) { jsonrpc: JSON_RPC_VERSION, id:, 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.
485 486 487 488 489 |
# File 'mcp_server.rb', line 485 def respond_result(id = nil, result = {}) return if id.nil? { jsonrpc: JSON_RPC_VERSION, id:, result: } end |
#respond_to_tools_call(id, params) ⇒ 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.
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 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 |
# File 'mcp_server.rb', line 361 def respond_to_tools_call(id, params) tool_name = params["name"].to_s.to_sym return respond_error(id, INVALID_PARAMS_CODE, "Unknown tool") unless TOOLS.key?(tool_name) arguments = params["arguments"] arguments = {} if arguments.nil? return respond_error(id, INVALID_PARAMS_CODE, "Invalid tool arguments") unless arguments.is_a?(Hash) # Invalid arguments are tool errors rather than protocol errors so that # the client's model can correct them. # An empty name is a mistake rather than a request for every package: # `upgrade` without a name upgrades everything. if arguments.values_at("formula_or_cask", "text_or_regex").any? { |name| !name.nil? && name.to_s.strip.empty? } return respond_result(id, tool_result("Empty formula, cask or search argument", error: true)) end package_type = arguments["package_type"] if package_type && PACKAGE_TYPES.none?(package_type) return respond_result(id, tool_result("Invalid package_type argument: expected formula or cask", error: true)) end if tool_name == :help command = arguments["command"].to_s command = Commands::HOMEBREW_INTERNAL_COMMAND_ALIASES.fetch(command, command) if !command.empty? && (command.match?(%r{[/\0]}) || [Commands::HOMEBREW_CMD_PATH, Commands::HOMEBREW_DEV_CMD_PATH].none? do |path| (path/"#{command}.rb").file? end) return respond_result(id, tool_result("Invalid command argument: expected a built-in command", error: true)) end end command_args = tool_command_arguments(tool_name, arguments) if command_args.any? { |argument| INLINE_CASK_DSL_REGEX.match?(argument) } return respond_result(id, tool_result("Invalid formula or cask argument", error: true)) end require "open3" = params["_meta"] progress_token = ["progressToken"] if .is_a?(Hash) buffer_size = 4096 # 4KB progress = T.let(0, Integer) done = T.let(false, T::Boolean) new_output = T.let(false, T::Boolean) output = +"" Open3.popen2e(HOMEBREW_BREW_FILE, tool_name.to_s, *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 respond_result(id, tool_result(output, error: !wait.value.success?)) end 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.
251 252 253 254 255 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 |
# File 'mcp_server.rb', line 251 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? response = begin request = JSON.parse(input) debug("Request: #{JSON.pretty_generate(request)}") if request.is_a?(Hash) handle_request(request) else respond_error(nil, INVALID_REQUEST_CODE, "Invalid request") end rescue JSON::ParserError respond_error(nil, PARSE_ERROR_CODE, "Parse error") end 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.
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 |
# File 'mcp_server.rb', line 449 def tool_command_arguments(tool_name, arguments) flags = [] names = [] case tool_name when :style flags << "--fix" if arguments["fix"] flags << "--changed" if arguments["changed"] names = arguments["files"].to_s.split when :tests only_arguments = arguments["only"].to_s.strip flags << "--only=#{only_arguments}" unless only_arguments.empty? flags << "--fail-fast" if arguments["fail_fast"] flags << "--changed" if arguments["changed"] flags << "--online" if arguments["online"] when :search, :info, :install, :upgrade, :uninstall, :list package_type = arguments["package_type"] flags << "--#{package_type}" if package_type name_argument = (tool_name == :search) ? "text_or_regex" : "formula_or_cask" names = [arguments[name_argument]] when :help # `brew help` takes command names such as `--prefix` and does not # understand `--`, so its argument is passed through unchanged. return [arguments["command"].to_s].reject(&:empty?) end names = names.map(&:to_s).reject(&:empty?) return flags if names.empty? # `--` stops names such as `--HEAD` or `--zap` being parsed as options. [*flags, "--", *names] end |
#tool_result(text, error:) ⇒ 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.
492 493 494 |
# File 'mcp_server.rb', line 492 def tool_result(text, error:) { content: [{ type: "text", text: }], isError: error } end |