Class: LocalVault::MCP::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/localvault/mcp/server.rb

Instance Method Summary collapse

Constructor Details

#initialize(input: $stdin, output: $stdout) ⇒ Server

Create an MCP server reading JSON-RPC from input, writing responses to output.

Parameters:

  • input (IO) (defaults to: $stdin)

    input stream for JSON-RPC messages (default: $stdin)

  • output (IO) (defaults to: $stdout)

    output stream for JSON-RPC responses (default: $stdout)



18
19
20
21
22
23
# File 'lib/localvault/mcp/server.rb', line 18

def initialize(input: $stdin, output: $stdout)
  @input        = input
  @output       = output
  @vault_cache  = {}  # name => Vault — lazily populated per-call
  @session_vault = load_session_vault  # LOCALVAULT_SESSION fast-path
end

Instance Method Details

#handle_message(json_string) ⇒ Hash?

Parse and dispatch a single JSON-RPC message.

Handles initialize, tools/list, and tools/call methods. Notifications (no “id” field) return nil.

Parameters:

  • json_string (String)

    raw JSON-RPC message

Returns:

  • (Hash, nil)

    JSON-RPC response hash, or nil for notifications



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/localvault/mcp/server.rb', line 61

def handle_message(json_string)
  message = JSON.parse(json_string)

  # Notifications have no id — no response
  return nil unless message.key?("id")

  id     = message["id"]
  method = message["method"]
  params = message["params"] || {}

  case method
  when "initialize"
    success_response(id, {
      "protocolVersion" => "2025-11-25",
      "capabilities"    => { "tools" => {} },
      "serverInfo"      => { "name" => "localvault", "version" => LocalVault::VERSION }
    })
  when "tools/list"
    success_response(id, { "tools" => Tools::DEFINITIONS })
  when "tools/call"
    tool_name  = params["name"]
    arguments  = params["arguments"] || {}

    unless Tools::DEFINITIONS.any? { |t| t["name"] == tool_name }
      return error_response(id, -32602, "Unknown tool: #{tool_name}")
    end

    result = Tools.call(tool_name, arguments, method(:vault_for))
    success_response(id, result)
  else
    error_response(id, -32601, "Method not found: #{method}")
  end
rescue JSON::ParserError
  error_response(nil, -32700, "Parse error")
end

#startvoid

This method returns an undefined value.

Start the MCP server loop, reading JSON-RPC messages line-by-line.

Logs available unlocked vaults to stderr on startup. Blocks until input is exhausted or interrupted (Ctrl-C).



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/localvault/mcp/server.rb', line 31

def start
  unlocked = unlocked_vault_names
  label = unlocked.empty? ? "no unlocked vaults (run: localvault show)" : "vaults=#{unlocked.join(', ')}"
  $stderr.puts "[localvault-mcp] started  v#{LocalVault::VERSION}  #{label}"
  $stderr.flush

  @input.each_line do |line|
    line = line.strip
    next if line.empty?

    response = handle_message(line)
    if response
      @output.puts(JSON.generate(response))
      @output.flush
    end
  end
rescue Interrupt
  # Clean shutdown on Ctrl-C
ensure
  $stderr.puts "[localvault-mcp] stopped"
  $stderr.flush
end