Skip to contents

Connects to a Model Context Protocol server and returns its tools as LLMR::llm_tool() objects ready for agent(), wrapped so the agent's use of them is safe and recorded. The defaults are strict because MCP's documented attack surface (tool/schema poisoning, line jumping, rug pulls, confused-deputy) lives in exactly the trust an agent places in a server's tool descriptions.

Usage

mcp_tools(
  config,
  policy = c("read_only", "read_write"),
  approve_writes = TRUE,
  audit = TRUE,
  allow = NULL,
  pin_schemas = TRUE,
  transport = NULL,
  timeout_s = 30,
  max_bytes = 1e+06
)

Arguments

config

A connection spec: a list with url (HTTP) or command (stdio), or anything your transport understands.

policy

"read_only" (default) or "read_write".

approve_writes

If TRUE (default), write/external calls pass a human gate.

audit

If TRUE (default), schemas/calls/results are recorded.

allow

Optional character vector of tool names to expose (others are dropped and logged).

pin_schemas

If TRUE (default), pin and re-check tool schemas.

transport

Test/extension seam: a function (method, params) -> result speaking JSON-RPC to the server. Default builds a real HTTP/stdio client.

timeout_s, max_bytes

Per-call limits.

Value

A list of llmr_tool objects, each carrying MCP governance metadata.

Details

Defenses applied:

  • Read-only floor (policy = "read_only"): the floor is an allowlist, not a denylist. A tool is exposed only when it is positively known to be read-only (its annotations$readOnlyHint is TRUE). Any tool that is not positively read-only (one with no annotations, or one that looks write-like) is refused unless policy = "read_write". A malicious server cannot slip a writing tool past the floor by giving it a benign name and no annotations.

  • Human gate for writes (approve_writes = TRUE): any write/external call pauses for sign-off (see human_gate()).

  • Schema pinning (pin_schemas = TRUE): each tool's full advertised signature (input schema, description, and annotations) is hashed at first listing and re-verified before every call. A later change, or a server that refuses to let us re-verify (a tools/get that errors or returns no schema), raises llmragent_mcp_schema_drift rather than trusting the new definition or failing open (the schema-drift defense). The re-check fails closed: if it cannot confirm the tool is unchanged, it refuses.

  • Description sanitation: server descriptions are treated as untrusted, never spliced into a system prompt, and scanned for injection patterns; a flagged tool is downgraded to require approval (the line-jumping defense).

  • Audit (audit = TRUE): tool schemas, call argument hashes, and result hashes are recorded.

Examples

if (FALSE) { # \dontrun{
# offline, via a fake transport returning canned JSON-RPC
fake <- function(method, params) {
  if (method == "tools/list") list(tools = list(list(
    name = "search", description = "Search docs.",
    inputSchema = list(type = "object",
                       properties = list(q = list(type = "string"))))))
  else list(content = list(list(type = "text", text = "result")))
}
tools <- mcp_tools(list(url = "http://localhost:9000"), transport = fake)
} # }