Skip to content

Commit 0d4ba4c

Browse files
taekopgadenbuie
andauthored
fix(mcp): Allow several tool groups in btw_mcp_server() (#221)
* fix(mcp): Allow several tool groups in btw_mcp_server() The R file detection chained `is.character(tools) && file.exists(tools)`, which errors under R >= 4.3 when `tools` names more than one tool group. Use `is_string()` so the file branch only considers a single path. Fixes #206 * test(mcp): assert flattened tools and R-script passthrough Capture the `tools` argument passed to mcptools::mcp_server() and check that a character vector of tool groups is flattened to tools from those groups, and that a path to an .R script is passed through unflattened. Follows up on #206, PR #221. * docs(mcp): recommend tools.R script and document script-path semantics --------- Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
1 parent 1025d26 commit 0d4ba4c

4 files changed

Lines changed: 114 additions & 4 deletions

File tree

‎NEWS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
* Package tests now support a compact reporter that shows per-file progress, timing, warning and failure details, and final counts. `btw pkg test` uses it by default, while `btw_tool_pkg_test()` defaults to concise output for non-streaming clients; both accept other testthat reporter names (#220).
66

7+
* `btw_mcp_server()` no longer errors when `tools` is a character vector naming more than one tool group, e.g. `btw_mcp_server(c("docs", "env", "sessioninfo"))` (@taekop, #206).
8+
79
# btw 1.5.0
810

911
## New features

‎R/mcp.R‎

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,36 @@
4343
#' See [btw_tools()] for a complete list of available tool groups and their
4444
#' contents.
4545
#'
46+
#' To combine btw's tools with your own custom tools, we recommend writing a
47+
#' small R script that returns the combined list of tools and passing its path
48+
#' to `btw_mcp_server()`.
49+
#'
50+
#' MCP servers are usually launched by your coding harness, e.g. via
51+
#' `Rscript -e "btw::btw_mcp_server(...)"` in a client configuration file.
52+
#' Composing the tool list in an R script is much easier to read and maintain
53+
#' than cramming the same code into an inline `Rscript` command. The script
54+
#' below combines btw's tools with your own custom tools:
55+
#'
56+
#' ```r
57+
#' # tools.R
58+
#' my_custom_tool <- ellmer::tool(
59+
#' function() R.version.string,
60+
#' "Report the version of R running the MCP server"
61+
#' )
62+
#'
63+
#' c(btw_tools("docs", "pkg"), list(my_custom_tool))
64+
#' ```
65+
#'
66+
#' The script is responsible for the complete list of tools, so include
67+
#' [btw_tools()] calls in the script to pull in btw's tools.
68+
#'
69+
#' Use an absolute path to the script in most cases, e.g.
70+
#' `btw_mcp_server("/path/to/tools.R")`. Relative paths are resolved against
71+
#' the working directory of the process launched by the coding harness, which
72+
#' is usually the project directory but isn't guaranteed. A project-specific
73+
#' tools script in the project directory is a reasonable use of a relative
74+
#' path, but absolute paths are the safest choice otherwise.
75+
#'
4676
#' @section Configuration:
4777
#' To configure this server with MCP clients, use the command `Rscript` and the
4878
#' args `-e "btw::btw_mcp_server()"`. We recommend customizing the tool set as
@@ -155,7 +185,9 @@
155185
#' to the tools provided by [btw_tools()]. Use [btw_tools()] to subset to
156186
#' specific list of \pkg{btw} tools that can be augmented with additional
157187
#' tools. Alternatively, you can pass a path to an R script that returns a
158-
#' list of tools as supported by [mcptools::mcp_server()].
188+
#' list of tools as supported by [mcptools::mcp_server()]. In that case the
189+
#' script provides the complete set of tools -- btw's tools are not included
190+
#' unless the script itself adds them with [btw_tools()].
159191
#'
160192
#' @return Returns the result of [mcptools::mcp_server()] or
161193
#' [mcptools::mcp_session()].
@@ -165,7 +197,7 @@
165197
btw_mcp_server <- function(tools = NULL) {
166198
# If given a path to an R script, we'll pass it on to mcp_server()
167199
is_likely_r_file <-
168-
is.character(tools) &&
200+
is_string(tools) &&
169201
file.exists(tools) &&
170202
grepl("[.]r$", tools, ignore.case = TRUE)
171203

@@ -194,4 +226,4 @@ btw_mcp_tools <- function() {
194226
function(tool) !identical(tool@annotations$btw_group, "skills"),
195227
all_tools
196228
)
197-
}
229+
}

‎man/mcp.Rd‎

Lines changed: 32 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tests/testthat/test-mcp.R‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,51 @@ test_that("btw_mcp_server errors informatively with bad `tools`", {
1212
)
1313
})
1414

15+
test_that("btw_mcp_server works with a character vector of multiple tool groups", {
16+
local_enable_tools()
17+
18+
captured <- new.env(parent = emptyenv())
19+
local_mocked_bindings(
20+
mcp_server = function(tools, ...) {
21+
captured$tools <- tools
22+
"mocked"
23+
},
24+
.package = "mcptools"
25+
)
26+
27+
expect_equal(btw_mcp_server(tools = c("docs", "env")), "mocked")
28+
29+
groups <- unique(vapply(
30+
captured$tools,
31+
function(tool) tool@annotations$btw_group,
32+
character(1)
33+
))
34+
expect_setequal(groups, c("docs", "env"))
35+
})
36+
37+
test_that("btw_mcp_server passes an R script path through to mcptools", {
38+
local_enable_tools()
39+
40+
# The script is sourced by mcptools::mcp_server(), not by btw_mcp_server(),
41+
# so here we only assert that the path is handed over unflattened.
42+
path_tools <- withr::local_tempfile(
43+
lines = "btw::btw_tools('docs')",
44+
fileext = ".R"
45+
)
46+
47+
captured <- new.env(parent = emptyenv())
48+
local_mocked_bindings(
49+
mcp_server = function(tools, ...) {
50+
captured$tools <- tools
51+
"mocked"
52+
},
53+
.package = "mcptools"
54+
)
55+
56+
expect_equal(btw_mcp_server(tools = path_tools), "mocked")
57+
expect_equal(captured$tools, path_tools)
58+
})
59+
1560
test_that("btw_mcp_tools() excludes skills group by default", {
1661
local_enable_tools()
1762
tools <- btw_mcp_tools()

0 commit comments

Comments
 (0)