mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00
Merge pull request #20041 from Homebrew/mcp_server
Add `brew mcp-server`: a MCP server for Homebrew.
This commit is contained in:
commit
e825ceea0a
@ -600,6 +600,11 @@ case "$1" in
|
||||
homebrew-version
|
||||
exit 0
|
||||
;;
|
||||
mcp-server)
|
||||
source "${HOMEBREW_LIBRARY}/Homebrew/cmd/mcp-server.sh"
|
||||
homebrew-mcp-server "$@"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# TODO: bump version when new macOS is released or announced and update references in:
|
||||
|
23
Library/Homebrew/cmd/mcp-server.rb
Normal file
23
Library/Homebrew/cmd/mcp-server.rb
Normal file
@ -0,0 +1,23 @@
|
||||
# typed: strong
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "abstract_command"
|
||||
require "shell_command"
|
||||
|
||||
module Homebrew
|
||||
module Cmd
|
||||
class McpServerCmd < AbstractCommand
|
||||
# This is a shell command as MCP servers need a faster startup time
|
||||
# than a normal Homebrew Ruby command allows.
|
||||
include ShellCommand
|
||||
|
||||
cmd_args do
|
||||
description <<~EOS
|
||||
Starts the Homebrew MCP (Model Context Protocol) server.
|
||||
EOS
|
||||
switch "-d", "--debug", description: "Enable debug logging to stderr."
|
||||
switch "--ping", description: "Start the server, act as if receiving a ping and then exit.", hidden: true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
14
Library/Homebrew/cmd/mcp-server.sh
Normal file
14
Library/Homebrew/cmd/mcp-server.sh
Normal file
@ -0,0 +1,14 @@
|
||||
# Documentation defined in Library/Homebrew/cmd/mcp-server.rb
|
||||
|
||||
# This is a shell command as MCP servers need a faster startup time
|
||||
# than a normal Homebrew Ruby command allows.
|
||||
|
||||
# HOMEBREW_LIBRARY is set by brew.sh
|
||||
# HOMEBREW_BREW_FILE is set by extend/ENV/super.rb
|
||||
# shellcheck disable=SC2154
|
||||
homebrew-mcp-server() {
|
||||
source "${HOMEBREW_LIBRARY}/Homebrew/utils/ruby.sh"
|
||||
setup-ruby-path
|
||||
export HOMEBREW_VERSION
|
||||
"${HOMEBREW_RUBY_PATH}" "-r${HOMEBREW_LIBRARY}/Homebrew/mcp_server.rb" -e "Homebrew::McpServer.new.run" "$@"
|
||||
}
|
272
Library/Homebrew/mcp_server.rb
Normal file
272
Library/Homebrew/mcp_server.rb
Normal file
@ -0,0 +1,272 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This is a standalone Ruby script as MCP servers need a faster startup time
|
||||
# than a normal Homebrew Ruby command allows.
|
||||
require_relative "standalone"
|
||||
require "json"
|
||||
require "stringio"
|
||||
|
||||
module Homebrew
|
||||
# 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.
|
||||
class McpServer
|
||||
HOMEBREW_BREW_FILE = T.let(ENV.fetch("HOMEBREW_BREW_FILE").freeze, String)
|
||||
HOMEBREW_VERSION = T.let(ENV.fetch("HOMEBREW_VERSION").freeze, String)
|
||||
JSON_RPC_VERSION = T.let("2.0", String)
|
||||
MCP_PROTOCOL_VERSION = T.let("2025-03-26", String)
|
||||
ERROR_CODE = T.let(-32601, Integer)
|
||||
|
||||
SERVER_INFO = T.let({
|
||||
name: "brew-mcp-server",
|
||||
version: HOMEBREW_VERSION,
|
||||
}.freeze, T::Hash[Symbol, String])
|
||||
|
||||
FORMULA_OR_CASK_PROPERTIES = T.let({
|
||||
formula_or_cask: {
|
||||
type: "string",
|
||||
description: "Formula or cask name",
|
||||
},
|
||||
}.freeze, T::Hash[Symbol, T.anything])
|
||||
|
||||
# NOTE: Cursor (as of June 2025) will only query/use a maximum of 40 tools.
|
||||
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]])
|
||||
|
||||
sig { params(stdin: T.any(IO, StringIO), stdout: T.any(IO, StringIO), stderr: T.any(IO, StringIO)).void }
|
||||
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
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def debug_logging? = @debug_logging
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def ping_switch? = @ping_switch
|
||||
|
||||
sig { void }
|
||||
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
|
||||
|
||||
sig { params(text: String).void }
|
||||
def debug(text)
|
||||
return unless debug_logging?
|
||||
|
||||
log(text)
|
||||
end
|
||||
|
||||
sig { params(text: String).void }
|
||||
def log(text)
|
||||
@stderr.puts(text)
|
||||
@stderr.flush
|
||||
end
|
||||
|
||||
sig { params(request: T::Hash[String, T.untyped]).returns(T.nilable(T::Hash[Symbol, T.anything])) }
|
||||
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
|
||||
|
||||
sig {
|
||||
params(id: T.nilable(Integer),
|
||||
result: T::Hash[Symbol, T.anything]).returns(T.nilable(T::Hash[Symbol, T.anything]))
|
||||
}
|
||||
def respond_result(id = nil, result = {})
|
||||
return if id.nil?
|
||||
|
||||
{ jsonrpc: JSON_RPC_VERSION, id:, result: }
|
||||
end
|
||||
|
||||
sig { params(id: T.nilable(Integer), message: String).returns(T::Hash[Symbol, T.anything]) }
|
||||
def respond_error(id, message)
|
||||
{ jsonrpc: JSON_RPC_VERSION, id:, error: { code: ERROR_CODE, message: } }
|
||||
end
|
||||
end
|
||||
end
|
13
Library/Homebrew/test/cmd/mcp-server_spec.rb
Normal file
13
Library/Homebrew/test/cmd/mcp-server_spec.rb
Normal file
@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe "brew mcp-server", type: :system do
|
||||
it "starts the MCP server", :integration_test do
|
||||
# This is the easiest way to handle a newline here.
|
||||
# rubocop:disable Style/StringConcatenation
|
||||
expect { brew_sh "mcp-server", "--ping" }
|
||||
.to output("==> Started Homebrew MCP server...\n").to_stderr
|
||||
.and output('{"jsonrpc":"2.0","id":1,"result":{}}' + "\n").to_stdout
|
||||
.and be_a_success
|
||||
# rubocop:enable Style/StringConcatenation
|
||||
end
|
||||
end
|
249
Library/Homebrew/test/mcp_server_spec.rb
Normal file
249
Library/Homebrew/test/mcp_server_spec.rb
Normal file
@ -0,0 +1,249 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "mcp_server"
|
||||
require "stringio"
|
||||
require "timeout"
|
||||
|
||||
RSpec.describe Homebrew::McpServer do
|
||||
let(:stdin) { StringIO.new }
|
||||
let(:stdout) { StringIO.new }
|
||||
let(:stderr) { StringIO.new }
|
||||
let(:server) { described_class.new(stdin:, stdout:, stderr:) }
|
||||
let(:jsonrpc) { Homebrew::McpServer::JSON_RPC_VERSION }
|
||||
let(:id) { Random.rand(1000) }
|
||||
let(:code) { Homebrew::McpServer::ERROR_CODE }
|
||||
|
||||
describe "#initialize" do
|
||||
it "sets debug_logging to false by default" do
|
||||
expect(server.debug_logging?).to be(false)
|
||||
end
|
||||
|
||||
it "sets debug_logging to true if --debug is in ARGV" do
|
||||
stub_const("ARGV", ["--debug"])
|
||||
expect(server.debug_logging?).to be(true)
|
||||
end
|
||||
|
||||
it "sets debug_logging to true if -d is in ARGV" do
|
||||
stub_const("ARGV", ["-d"])
|
||||
expect(server.debug_logging?).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#debug and #log" do
|
||||
it "logs debug output when debug_logging is true" do
|
||||
stub_const("ARGV", ["--debug"])
|
||||
server.debug("foo")
|
||||
expect(stderr.string).to include("foo")
|
||||
end
|
||||
|
||||
it "does not log debug output when debug_logging is false" do
|
||||
server.debug("foo")
|
||||
expect(stderr.string).to eq("")
|
||||
end
|
||||
|
||||
it "logs to stderr" do
|
||||
server.log("bar")
|
||||
expect(stderr.string).to include("bar")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#handle_request" do
|
||||
it "responds to initialize method" do
|
||||
request = { "id" => id, "method" => "initialize" }
|
||||
result = server.handle_request(request)
|
||||
expect(result).to eq({
|
||||
jsonrpc:,
|
||||
id:,
|
||||
result: {
|
||||
protocolVersion: Homebrew::McpServer::MCP_PROTOCOL_VERSION,
|
||||
capabilities: {
|
||||
tools: { listChanged: false },
|
||||
prompts: {},
|
||||
resources: {},
|
||||
logging: {},
|
||||
roots: {},
|
||||
},
|
||||
serverInfo: Homebrew::McpServer::SERVER_INFO,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
it "responds to resources/list" do
|
||||
request = { "id" => id, "method" => "resources/list" }
|
||||
result = server.handle_request(request)
|
||||
expect(result).to eq({ jsonrpc:, id:, result: { resources: [] } })
|
||||
end
|
||||
|
||||
it "responds to resources/templates/list" do
|
||||
request = { "id" => id, "method" => "resources/templates/list" }
|
||||
result = server.handle_request(request)
|
||||
expect(result).to eq({ jsonrpc:, id:, result: { resourceTemplates: [] } })
|
||||
end
|
||||
|
||||
it "responds to prompts/list" do
|
||||
request = { "id" => id, "method" => "prompts/list" }
|
||||
result = server.handle_request(request)
|
||||
expect(result).to eq({ jsonrpc:, id:, result: { prompts: [] } })
|
||||
end
|
||||
|
||||
it "responds to ping" do
|
||||
request = { "id" => id, "method" => "ping" }
|
||||
result = server.handle_request(request)
|
||||
expect(result).to eq({ jsonrpc:, id:, result: {} })
|
||||
end
|
||||
|
||||
it "responds to get_server_info" do
|
||||
request = { "id" => id, "method" => "get_server_info" }
|
||||
result = server.handle_request(request)
|
||||
expect(result).to eq({ jsonrpc:, id:, result: Homebrew::McpServer::SERVER_INFO })
|
||||
end
|
||||
|
||||
it "responds to logging/setLevel with debug" do
|
||||
request = { "id" => id, "method" => "logging/setLevel", "params" => { "level" => "debug" } }
|
||||
result = server.handle_request(request)
|
||||
expect(server.debug_logging?).to be(true)
|
||||
expect(result).to eq({ jsonrpc:, id:, result: {} })
|
||||
end
|
||||
|
||||
it "responds to logging/setLevel with non-debug" do
|
||||
request = { "id" => id, "method" => "logging/setLevel", "params" => { "level" => "info" } }
|
||||
result = server.handle_request(request)
|
||||
expect(server.debug_logging?).to be(false)
|
||||
expect(result).to eq({ jsonrpc:, id:, result: {} })
|
||||
end
|
||||
|
||||
it "responds to notifications/initialized" do
|
||||
request = { "id" => id, "method" => "notifications/initialized" }
|
||||
expect(server.handle_request(request)).to be_nil
|
||||
end
|
||||
|
||||
it "responds to notifications/cancelled" do
|
||||
request = { "id" => id, "method" => "notifications/cancelled" }
|
||||
expect(server.handle_request(request)).to be_nil
|
||||
end
|
||||
|
||||
it "responds to tools/list" do
|
||||
request = { "id" => id, "method" => "tools/list" }
|
||||
result = server.handle_request(request)
|
||||
expect(result[:result][:tools]).to match_array(Homebrew::McpServer::TOOLS.values)
|
||||
end
|
||||
|
||||
Homebrew::McpServer::TOOLS.each do |tool_name, tool_definition|
|
||||
it "responds to tools/call for #{tool_name}" do
|
||||
allow(server).to receive(:`).and_return("output for #{tool_name}")
|
||||
arguments = {}
|
||||
Array(tool_definition[:required]).each do |required_key|
|
||||
arguments[required_key] = "dummy"
|
||||
end
|
||||
request = {
|
||||
"id" => id,
|
||||
"method" => "tools/call",
|
||||
"params" => {
|
||||
"name" => tool_name.to_s,
|
||||
"arguments" => arguments,
|
||||
},
|
||||
}
|
||||
result = server.handle_request(request)
|
||||
expect(result).to eq({
|
||||
jsonrpc: jsonrpc,
|
||||
id: id,
|
||||
result: { content: [{ type: "text", text: "output for #{tool_name}" }] },
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
it "responds to tools/call for unknown tool" do
|
||||
request = { "id" => id, "method" => "tools/call", "params" => { "name" => "not_a_tool", "arguments" => {} } }
|
||||
result = server.handle_request(request)
|
||||
expect(result).to eq({ jsonrpc:, id:, error: { message: "Unknown tool", code: } })
|
||||
end
|
||||
|
||||
it "responds with error for unknown method" do
|
||||
request = { "id" => id, "method" => "not_a_method" }
|
||||
result = server.handle_request(request)
|
||||
expect(result).to eq({ jsonrpc:, id:, error: { message: "Method not found", code: } })
|
||||
end
|
||||
|
||||
it "returns nil if id is nil" do
|
||||
request = { "method" => "initialize" }
|
||||
expect(server.handle_request(request)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#respond_result" do
|
||||
it "returns nil if id is nil" do
|
||||
expect(server.send(:respond_result, nil, {})).to be_nil
|
||||
end
|
||||
|
||||
it "returns a result hash if id is present" do
|
||||
result = server.respond_result(id, { foo: "bar" })
|
||||
expect(result).to eq({ jsonrpc:, id:, result: { foo: "bar" } })
|
||||
end
|
||||
end
|
||||
|
||||
describe "#respond_error" do
|
||||
it "returns an error hash" do
|
||||
result = server.respond_error(id, "fail")
|
||||
expect(result).to eq({ jsonrpc:, id:, error: { message: "fail", code: } })
|
||||
end
|
||||
end
|
||||
|
||||
describe "#run" do
|
||||
let(:sleep_time) { 0.001 }
|
||||
|
||||
it "runs the loop and exits cleanly on interrupt" do
|
||||
stub_const("ARGV", ["--debug"])
|
||||
stdin.puts({ id:, method: "ping" }.to_json)
|
||||
stdin.rewind
|
||||
server_thread = Thread.new do
|
||||
server.run
|
||||
rescue SystemExit
|
||||
# expected, do nothing
|
||||
end
|
||||
|
||||
response_hash_string = "Response: {"
|
||||
sleep(sleep_time)
|
||||
server_thread.raise(Interrupt)
|
||||
server_thread.join
|
||||
|
||||
expect(stderr.string).to include(response_hash_string)
|
||||
end
|
||||
|
||||
it "runs the loop and logs 'Response: nil' when handle_request returns nil" do
|
||||
stub_const("ARGV", ["--debug"])
|
||||
stdin.puts({ id:, method: "notifications/initialized" }.to_json)
|
||||
stdin.rewind
|
||||
server_thread = Thread.new do
|
||||
server.run
|
||||
rescue SystemExit
|
||||
# expected, do nothing
|
||||
end
|
||||
|
||||
response_nil_string = "Response: nil"
|
||||
sleep(sleep_time)
|
||||
server_thread.raise(Interrupt)
|
||||
server_thread.join
|
||||
|
||||
expect(stderr.string).to include(response_nil_string)
|
||||
end
|
||||
|
||||
it "exits on Interrupt" do
|
||||
allow(stdin).to receive(:gets).and_raise(Interrupt)
|
||||
expect do
|
||||
server.run
|
||||
rescue
|
||||
SystemExit
|
||||
end.to raise_error(SystemExit)
|
||||
end
|
||||
|
||||
it "exits on error" do
|
||||
allow(stdin).to receive(:gets).and_raise(StandardError, "fail")
|
||||
expect do
|
||||
server.run
|
||||
rescue
|
||||
SystemExit
|
||||
end.to raise_error(SystemExit)
|
||||
expect(stderr.string).to match(/Error: fail/)
|
||||
end
|
||||
end
|
||||
end
|
@ -1797,6 +1797,22 @@ _brew_ls() {
|
||||
__brew_complete_installed_casks
|
||||
}
|
||||
|
||||
_brew_mcp_server() {
|
||||
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
case "${cur}" in
|
||||
-*)
|
||||
__brewcomp "
|
||||
--debug
|
||||
--help
|
||||
--quiet
|
||||
--verbose
|
||||
"
|
||||
return
|
||||
;;
|
||||
*) ;;
|
||||
esac
|
||||
}
|
||||
|
||||
_brew_migrate() {
|
||||
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
case "${cur}" in
|
||||
@ -3191,6 +3207,7 @@ _brew() {
|
||||
ln) _brew_ln ;;
|
||||
log) _brew_log ;;
|
||||
ls) _brew_ls ;;
|
||||
mcp-server) _brew_mcp_server ;;
|
||||
migrate) _brew_migrate ;;
|
||||
missing) _brew_missing ;;
|
||||
nodenv-sync) _brew_nodenv_sync ;;
|
||||
|
@ -1226,6 +1226,13 @@ __fish_brew_complete_arg 'ls; and not __fish_seen_argument -l cask -l casks' -a
|
||||
__fish_brew_complete_arg 'ls; and not __fish_seen_argument -l formula -l formulae' -a '(__fish_brew_suggest_casks_installed)'
|
||||
|
||||
|
||||
__fish_brew_complete_cmd 'mcp-server' 'Starts the Homebrew MCP (Model Context Protocol) server'
|
||||
__fish_brew_complete_arg 'mcp-server' -l debug -d 'Enable debug logging to stderr'
|
||||
__fish_brew_complete_arg 'mcp-server' -l help -d 'Show this message'
|
||||
__fish_brew_complete_arg 'mcp-server' -l quiet -d 'Make some output more quiet'
|
||||
__fish_brew_complete_arg 'mcp-server' -l verbose -d 'Make some output more verbose'
|
||||
|
||||
|
||||
__fish_brew_complete_cmd 'migrate' 'Migrate renamed packages to new names, where formula are old names of packages'
|
||||
__fish_brew_complete_arg 'migrate' -l cask -d 'Only migrate casks'
|
||||
__fish_brew_complete_arg 'migrate' -l debug -d 'Display any debugging information'
|
||||
|
@ -68,6 +68,7 @@ livecheck
|
||||
ln
|
||||
log
|
||||
ls
|
||||
mcp-server
|
||||
migrate
|
||||
missing
|
||||
nodenv-sync
|
||||
|
@ -191,6 +191,7 @@ __brew_internal_commands() {
|
||||
'list:List all installed formulae and casks'
|
||||
'livecheck:Check for newer versions of formulae and/or casks from upstream'
|
||||
'log:Show the `git log` for formula or cask, or show the log for the Homebrew repository if no formula or cask is provided'
|
||||
'mcp-server:Starts the Homebrew MCP (Model Context Protocol) server'
|
||||
'migrate:Migrate renamed packages to new names, where formula are old names of packages'
|
||||
'missing:Check the given formula kegs for missing dependencies'
|
||||
'nodenv-sync:Create symlinks for Homebrew'\''s installed NodeJS versions in `~/.nodenv/versions`'
|
||||
@ -1511,6 +1512,15 @@ _brew_ls() {
|
||||
'*:installed_cask:__brew_installed_casks'
|
||||
}
|
||||
|
||||
# brew mcp-server
|
||||
_brew_mcp_server() {
|
||||
_arguments \
|
||||
'--debug[Enable debug logging to stderr]' \
|
||||
'--help[Show this message]' \
|
||||
'--quiet[Make some output more quiet]' \
|
||||
'--verbose[Make some output more verbose]'
|
||||
}
|
||||
|
||||
# brew migrate
|
||||
_brew_migrate() {
|
||||
_arguments \
|
||||
|
@ -1052,6 +1052,14 @@ repository if no formula or cask is provided.
|
||||
|
||||
: Treat all named arguments as casks.
|
||||
|
||||
### `mcp-server` \[`--debug`\]
|
||||
|
||||
Starts the Homebrew MCP (Model Context Protocol) server.
|
||||
|
||||
`-d`, `--debug`
|
||||
|
||||
: Enable debug logging to stderr.
|
||||
|
||||
### `migrate` \[*`options`*\] *`installed_formula`*\|*`installed_cask`* \[...\]
|
||||
|
||||
Migrate renamed packages to new names, where *`formula`* are old names of
|
||||
|
@ -656,6 +656,11 @@ Treat all named arguments as formulae\.
|
||||
.TP
|
||||
\fB\-\-cask\fP
|
||||
Treat all named arguments as casks\.
|
||||
.SS "\fBmcp\-server\fP \fR[\fB\-\-debug\fP]"
|
||||
Starts the Homebrew MCP (Model Context Protocol) server\.
|
||||
.TP
|
||||
\fB\-d\fP, \fB\-\-debug\fP
|
||||
Enable debug logging to stderr\.
|
||||
.SS "\fBmigrate\fP \fR[\fIoptions\fP] \fIinstalled_formula\fP|\fIinstalled_cask\fP \fR[\.\.\.]"
|
||||
Migrate renamed packages to new names, where \fIformula\fP are old names of packages\.
|
||||
.TP
|
||||
|
Loading…
x
Reference in New Issue
Block a user