diff --git a/NAMESPACE b/NAMESPACE index bf7b513..26fd980 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,4 +2,5 @@ export(mcp_server) export(mcp_session) +export(mcp_set_tools) import(rlang) diff --git a/R/tools.R b/R/tools.R index d58b89a..6426081 100644 --- a/R/tools.R +++ b/R/tools.R @@ -1,3 +1,75 @@ +#' Set the tools available to run in your R session +#' +#' @description +#' By default, acquaint supplies tools from [btw::btw_tools()] to allow clients +#' to peruse package documentation, inspect your global environment, and query +#' session details. This function allows you register any tools created with +#' [ellmer::tool()] instead. +#' +#' A call to this function must be placed in your `.Rprofile` and the client +#' (i.e. Claude Desktop or Claude Code) restarted in order for the new tools +#' to be registered. +#' +#' acquaint will always register the tools "list_r_sessions" and +#' "select_r_session" in addition to the tools provided here; those tool names +#' are thus reserved for the package. +#' +#' @param x A list of tools created with [ellmer::tool()]. Any list that could +#' be passed to `chat$set_tools()` can be passed here. +#' +#' @returns +#' `x`, invisibly. Called for side effects. The function will error if `x` is +#' not a list of `ellmer::ToolDef` objects or if any tool name is one of the +#' reserved names "list_r_sessions" or "select_r_session". +#' +#' @examples +#' library(ellmer) +#' +#' tool_rnorm <- tool( +#' rnorm, +#' "Draw numbers from a random normal distribution", +#' n = type_integer("The number of observations. Must be a positive integer."), +#' mean = type_number("The mean value of the distribution."), +#' sd = type_number("The standard deviation of the distribution. Must be a non-negative number.") +#' ) +#' +#' # supply only one tool, tool_rnorm +#' mcp_set_tools(list(tool_rnorm)) +#' +#' # supply both tool_rnorm and `btw_tools()` +#' mcp_set_tools(c(list(tool_rnorm), btw::btw_tools())) +#' @export +mcp_set_tools <- function(x) { + check_acquaint_tools(x) + + options(.acquaint_tools = x) + + invisible(x) +} + +check_acquaint_tools <- function(x, call = caller_env()) { + if (!is_list(x) || !all(vapply(x, inherits, logical(1), "ellmer::ToolDef"))) { + msg <- "{.arg x} must be a list of tools created with {.fn ellmer::tool}." + if (inherits(x, "ellmer::ToolDef")) { + msg <- c(msg, "i" = "Did you mean to wrap {.arg x} in `list()`?") + } + cli::cli_abort(msg, call = call) + } + + if ( + any( + vapply(x, \(.x) .x@name, character(1)) %in% + c("list_r_sessions", "select_r_session") + ) + ) { + cli::cli_abort( + "The tool names {.field list_r_sessions} and {.field select_r_session} are + reserved by {.pkg acquaint}.", + call = call + ) + } +} + # These two functions are supplied to the client as tools and allow the client # to discover R sessions which have called `acquaint::mcp_session()`. They # are "model-facing" rather than user-facing. @@ -77,7 +149,7 @@ select_r_session_tool <- get_acquaint_tools <- function() { res <- c( - btw::btw_tools(), + getOption(".acquaint_tools", default = btw::btw_tools()), list( list_r_sessions_tool, select_r_session_tool diff --git a/man/mcp_set_tools.Rd b/man/mcp_set_tools.Rd new file mode 100644 index 0000000..4253b2c --- /dev/null +++ b/man/mcp_set_tools.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tools.R +\name{mcp_set_tools} +\alias{mcp_set_tools} +\title{Set the tools available to run in your R session} +\usage{ +mcp_set_tools(x) +} +\arguments{ +\item{x}{A list of tools created with \code{\link[ellmer:tool]{ellmer::tool()}}. Any list that could +be passed to \code{chat$set_tools()} can be passed here.} +} +\value{ +\code{x}, invisibly. Called for side effects. The function will error if \code{x} is +not a list of \code{ellmer::ToolDef} objects or if any tool name is one of the +reserved names "list_r_sessions" or "select_r_session". +} +\description{ +By default, acquaint supplies tools from \code{\link[btw:btw_tools]{btw::btw_tools()}} to allow clients +to peruse package documentation, inspect your global environment, and query +session details. This function allows you register any tools created with +\code{\link[ellmer:tool]{ellmer::tool()}} instead. + +A call to this function must be placed in your \code{.Rprofile} and the client +(i.e. Claude Desktop or Claude Code) restarted in order for the new tools +to be registered. + +acquaint will always register the tools "list_r_sessions" and +"select_r_session" in addition to the tools provided here; those tool names +are thus reserved for the package. +} +\examples{ +library(ellmer) + +tool_rnorm <- tool( + rnorm, + "Draw numbers from a random normal distribution", + n = type_integer("The number of observations. Must be a positive integer."), + mean = type_number("The mean value of the distribution."), + sd = type_number("The standard deviation of the distribution. Must be a non-negative number.") +) + +# supply only one tool, tool_rnorm +mcp_set_tools(list(tool_rnorm)) + +# supply both tool_rnorm and `btw_tools()` +mcp_set_tools(c(list(tool_rnorm), btw::btw_tools())) +} diff --git a/tests/testthat/_snaps/tools.md b/tests/testthat/_snaps/tools.md new file mode 100644 index 0000000..07060fa --- /dev/null +++ b/tests/testthat/_snaps/tools.md @@ -0,0 +1,25 @@ +# mcp_set_tools works + + Code + mcp_set_tools("boop") + Condition + Error in `mcp_set_tools()`: + ! `x` must be a list of tools created with `ellmer::tool()`. + +--- + + Code + mcp_set_tools(tool_rnorm) + Condition + Error in `mcp_set_tools()`: + ! `x` must be a list of tools created with `ellmer::tool()`. + i Did you mean to wrap `x` in `list()`? + +--- + + Code + mcp_set_tools(list(tool_rnorm)) + Condition + Error in `mcp_set_tools()`: + ! The tool names list_r_sessions and select_r_session are reserved by acquaint. + diff --git a/tests/testthat/test-tools.R b/tests/testthat/test-tools.R new file mode 100644 index 0000000..d8cbd84 --- /dev/null +++ b/tests/testthat/test-tools.R @@ -0,0 +1,33 @@ +test_that("mcp_set_tools works", { + old_option <- getOption("acquaint_tools") + on.exit(options(.acquaint_tools = old_option)) + + # must be a list + expect_snapshot(error = TRUE, mcp_set_tools("boop")) + + tool_rnorm <- ellmer::tool( + rnorm, + "Draw numbers from a random normal distribution", + n = ellmer::type_integer( + "The number of observations. Must be a positive integer." + ), + mean = ellmer::type_number("The mean value of the distribution."), + sd = ellmer::type_number( + "The standard deviation of the distribution. Must be a non-negative number." + ) + ) + tool_rnorm_list <- list(tool_rnorm) + + # tools themselves need to be in a list + expect_snapshot(error = TRUE, mcp_set_tools(tool_rnorm)) + + # uses reserved name + tool_rnorm@name <- "list_r_sessions" + expect_snapshot(error = TRUE, mcp_set_tools(list(tool_rnorm))) + + expect_equal(mcp_set_tools(tool_rnorm_list), tool_rnorm_list) + expect_equal( + names(get_acquaint_tools()), + c("rnorm", "list_r_sessions", "select_r_session") + ) +})