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: {} },
  },
  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)


136
137
138
139
140
141
142
# File 'mcp_server.rb', line 136

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:



186
187
188
189
190
# File 'mcp_server.rb', line 186

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)


145
# File 'mcp_server.rb', line 145

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:



199
200
201
202
203
204
205
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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'mcp_server.rb', line 199

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"
    if (tool = TOOLS.fetch(request["params"]["name"].to_sym, nil))
      require "shellwords"

      arguments = request["params"]["arguments"]
      argument = arguments.fetch("formula_or_cask", "")
      argument = arguments.fetch("text_or_regex", "") if argument.strip.empty?
      argument = arguments.fetch("command", "") if argument.strip.empty?
      argument = nil if argument.strip.empty?
      brew_command = T.cast(tool.fetch(:command), String)
                      .delete_prefix("brew ")
      full_command = [HOMEBREW_BREW_FILE, brew_command, argument].compact
                                                                 .map { |arg| Shellwords.escape(arg) }
                                                                 .join(" ")
      output = `#{full_command} 2>&1`.strip
      respond_result(id, { content: [{ type: "text", text: output }] })
    else
      respond_error(id, "Unknown tool")
    end
  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:



193
194
195
196
# File 'mcp_server.rb', line 193

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)


148
# File 'mcp_server.rb', line 148

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:



268
269
270
# File 'mcp_server.rb', line 268

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:



261
262
263
264
265
# File 'mcp_server.rb', line 261

def respond_result(id = nil, result = {})
  return if id.nil?

  { jsonrpc: JSON_RPC_VERSION, id:, result: }
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.



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'mcp_server.rb', line 151

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
      @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