Skip to content

btw_mcp_server() errors at startup when tools is a character vector of length > 1 #206

Description

@gustavdelius

btw_mcp_server() opens by testing whether its tools argument names an R file:

https://github.com/posit-dev/btw/blob/main/R/mcp.R#L165-L172

is_likely_r_file <-
  is.character(tools) &&
  file.exists(tools) &&
  grepl("[.]r$", tools, ignore.case = TRUE)

Since R 4.3, an && whose operand has length > 1 is an error rather than a warning. So if tools is a character vector naming two or more tool groups, file.exists(tools) returns a length-n logical and the call dies immediately:

btw::btw_mcp_server(c("docs", "env", "sessioninfo"))
#> Error in is.character(tools) && file.exists(tools) :
#>   'length = 3' in coercion to 'logical(1)'

A length-1 character vector is fine (file.exists("docs") is FALSE, so the guard short-circuits), which makes the failure look arbitrary — btw_mcp_server("docs") works, adding one more group breaks it.

Nothing else in the function objects to a character vector. The guard is the only obstacle; flatten_and_check_tools() handles one happily:

length(btw:::flatten_and_check_tools(c("docs", "env", "ide")))
#> [1] 8

Why this is worse than an ordinary error

The intended use is as an MCP server behind Rscript -e, where nothing has a terminal. The process dies before the initialize handshake, so the client sees only a closed pipe and reports a generic transport failure — Claude Code shows Failed to reconnect to r-btw: -32000 — while the R message goes nowhere. There is no indication that the argument is at fault, or even that R produced an error. It took reconstructing the Rscript invocation by hand and running it in a terminal to see the real message.

Suggested fix

Guard the length, e.g.

is_likely_r_file <-
  is.character(tools) &&
  length(tools) == 1 &&
  file.exists(tools) &&
  grepl("[.]r$", tools, ignore.case = TRUE)

which also matches the intent, since the file branch only makes sense for a single path. If a character vector is meant to be rejected rather than accepted, an explicit error naming tools would still be a large improvement over the coercion message.

Workaround

Wrap the groups so is.character() is FALSE — either btw_mcp_server(btw_tools("docs", "env", "sessioninfo")) or the list(...) form the docs use.

A documentation note

Every example in ?btw_mcp_server passes list(...) or btw_tools(...), so the docs are consistent and correct. But c(...) is a natural thing to reach for, it is what a character-vector-shaped argument invites, and the rest of the function supports it — so people will hit this. Rejecting it loudly or accepting it both seem better than the current behaviour.

Session info

R version 4.6.1 (2026-06-24)
btw 1.3.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions