Class: Homebrew::McpServer Private

Inherits:
Object show all
Defined in:
mcp_server.rb

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

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.

Parameters:

  • stdin (IO, StringIO) (defaults to: $stdin)
  • stdout (IO, StringIO) (defaults to: $stdout)
  • stderr (IO, StringIO) (defaults to: $stderr)


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.

Parameters:



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.

Returns:

  • (Boolean)


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.

Parameters:

Returns:



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.

Parameters:



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.

Returns:

  • (Boolean)


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.

Parameters:

  • id (Integer, nil)
  • message (String)

Returns:



399
400
401
# File 'mcp_server.rb', line 399

def respond_error(id, message)
  { 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.

Parameters:

  • id (Integer, nil) (defaults to: nil)
  • result (Hash{Symbol => T.anything}) (defaults to: {})

Returns:



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.

Parameters:

  • id (Integer)
  • request (Hash{String => T.untyped})

Returns:



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

#runvoid

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.message}")
  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.

Parameters:

Returns:



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