Module: LocalVault::MCP::Tools

Defined in:
lib/localvault/mcp/tools.rb

Constant Summary collapse

VAULT_PARAM =
{
  "vault" => {
    "type" => "string",
    "description" => "Vault name to use (uses default vault if omitted)"
  }
}.freeze
DEFINITIONS =

MCP tool definitions conforming to the MCP tools/list schema. Each entry specifies a tool name, description, and JSON Schema for input.

[
  {
    "name" => "get_secret",
    "description" => "Reveal plaintext only when the task truly requires the value in model context. Prefer localvault_build_exec for commands, evaluations, and API calls.",
    "inputSchema" => {
      "type" => "object",
      "properties" => {
        "key"   => { "type" => "string", "description" => "The exact secret key to reveal" },
        "allow_plaintext" => { "type" => "boolean", "description" => "Must be true to acknowledge that plaintext will enter model context" },
        **VAULT_PARAM
      },
      "required" => ["key", "allow_plaintext"]
    },
    "annotations" => { "readOnlyHint" => true, "openWorldHint" => false }
  },
  {
    "name" => "list_secrets",
    "description" => "Discover secret names without values. After selecting names, use localvault_build_exec to inject them into a process.",
    "inputSchema" => {
      "type" => "object",
      "properties" => {
        "prefix" => { "type" => "string", "description" => "Only return keys starting with this prefix" },
        "query" => { "type" => "string", "description" => "Case-insensitive substring filter for key names" },
        **VAULT_PARAM
      },
      "required" => []
    },
    "annotations" => { "readOnlyHint" => true, "openWorldHint" => false }
  },
  {
    "name" => "localvault_build_exec",
    "description" => "Build, but never execute, a shell-safe localvault exec command that injects secrets directly into a subprocess without exposing values to the model.",
    "inputSchema" => {
      "type" => "object",
      "properties" => {
        "command" => { "type" => "array", "items" => { "type" => "string" }, "description" => "Command argv to run with injected secrets" },
        "vault" => { "type" => "string", "description" => "Vault name" },
        "project" => { "type" => "string", "description" => "Dot-notation project group to inject without a prefix" },
        "only" => { "type" => "array", "items" => { "type" => "string" }, "description" => "Exact keys or GROUP.* selectors" },
        "except" => { "type" => "array", "items" => { "type" => "string" }, "description" => "Selectors to exclude" },
        "map" => { "type" => "object", "additionalProperties" => { "type" => "string" }, "description" => "Vault-key to environment-variable mappings" },
        "profile" => { "type" => "string", "enum" => ["aws"], "description" => "Built-in mapping profile" }
      },
      "required" => ["command"]
    },
    "outputSchema" => {
      "type" => "object",
      "properties" => {
        "command" => { "type" => "string" },
        "exposes_plaintext" => { "type" => "boolean" },
        "executes_command" => { "type" => "boolean" },
        "next_action" => { "type" => "string" }
      },
      "required" => %w[command exposes_plaintext executes_command next_action],
      "additionalProperties" => false
    },
    "annotations" => { "readOnlyHint" => true, "openWorldHint" => false }
  },
  {
    "name" => "localvault_whoami",
    "description" => "Show which localvault home, vault, and unlocked sessions the MCP server can see",
    "inputSchema" => {
      "type" => "object",
      "properties" => { **VAULT_PARAM },
      "required" => []
    },
    "annotations" => { "readOnlyHint" => true, "openWorldHint" => false }
  },
  {
    "name" => "set_secret",
    "description" => "Store a secret key-value pair in a localvault vault. Use dot-notation (project.KEY) for namespaced secrets.",
    "inputSchema" => {
      "type" => "object",
      "properties" => {
        "key"   => { "type" => "string", "description" => "The secret key (supports dot-notation: project.KEY)" },
        "value" => { "type" => "string", "description" => "The secret value" },
        **VAULT_PARAM
      },
      "required" => ["key", "value"]
    },
    "annotations" => {
      "readOnlyHint" => false,
      "destructiveHint" => true,
      "idempotentHint" => false,
      "openWorldHint" => false
    }
  },
  {
    "name" => "delete_secret",
    "description" => "Delete a secret by key from a localvault vault",
    "inputSchema" => {
      "type" => "object",
      "properties" => {
        "key"   => { "type" => "string", "description" => "The secret key to delete" },
        **VAULT_PARAM
      },
      "required" => ["key"]
    },
    "annotations" => {
      "readOnlyHint" => false,
      "destructiveHint" => true,
      "idempotentHint" => true,
      "openWorldHint" => false
    }
  }
].freeze

Class Method Summary collapse

Class Method Details

.call(name, arguments, vault_resolver, status_resolver = nil) ⇒ Hash

Dispatch an MCP tool call by name.

Resolves the target vault via the provided callable, then executes the requested tool (get_secret, list_secrets, set_secret, or delete_secret).

Parameters:

  • name (String)

    tool name (must match a DEFINITIONS entry)

  • arguments (Hash)

    tool arguments (e.g. => "API_KEY", "vault" => "prod")

  • vault_resolver (#call)

    callable that accepts a vault name (String or nil) and returns a Vault instance or nil

Returns:

  • (Hash)

    MCP content result with "content" array and optional "isError"

Raises:

  • (ArgumentError)

    if the tool name is unknown



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/localvault/mcp/tools.rb', line 136

def self.call(name, arguments, vault_resolver, status_resolver = nil)
  unless DEFINITIONS.any? { |t| t["name"] == name }
    raise ArgumentError, "Unknown tool: #{name}"
  end

  return error_result("Invalid arguments; expected object") unless arguments.is_a?(Hash)

  vault_name = arguments["vault"]
  return whoami(status_resolver.call(vault_name)) if name == "localvault_whoami"
  return build_exec(arguments) if name == "localvault_build_exec"
  return required_argument_error("key") if name == "get_secret" && !present_string?(arguments["key"])
  if name == "get_secret" && arguments["allow_plaintext"] != true
    return error_result(
      "Plaintext retrieval is blocked by default. For authentication or an external command, call " \
      "localvault_build_exec instead. Retry get_secret with allow_plaintext=true only when the user " \
      "explicitly needs the secret text or injection cannot complete the task."
    )
  end

  vault = vault_resolver.call(vault_name)

  unless vault
    hint = vault_name ? "localvault show -v #{vault_name}" : "localvault show"
    return error_result("No unlocked vault session. Run `localvault mcp --check`, then unlock with: #{hint}")
  end

  case name
  when "get_secret"    then get_secret(arguments["key"], vault)
  when "list_secrets"  then list_secrets(vault, prefix: arguments["prefix"], query: arguments["query"])
  when "set_secret"    then set_secret(arguments["key"], arguments["value"], vault)
  when "delete_secret" then delete_secret(arguments["key"], vault)
  end
rescue StandardError => e
  error_result(e.message)
end