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
btw_mcp_server()opens by testing whether itstoolsargument names an R file:https://github.com/posit-dev/btw/blob/main/R/mcp.R#L165-L172
Since R 4.3, an
&&whose operand has length > 1 is an error rather than a warning. So iftoolsis a character vector naming two or more tool groups,file.exists(tools)returns a length-n logical and the call dies immediately:A length-1 character vector is fine (
file.exists("docs")isFALSE, 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: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 theinitializehandshake, so the client sees only a closed pipe and reports a generic transport failure — Claude Code showsFailed 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 theRscriptinvocation by hand and running it in a terminal to see the real message.Suggested fix
Guard the length, e.g.
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
toolswould still be a large improvement over the coercion message.Workaround
Wrap the groups so
is.character()isFALSE— eitherbtw_mcp_server(btw_tools("docs", "env", "sessioninfo"))or thelist(...)form the docs use.A documentation note
Every example in
?btw_mcp_serverpasseslist(...)orbtw_tools(...), so the docs are consistent and correct. Butc(...)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