diff --git a/DESCRIPTION b/DESCRIPTION index 5f96551..6fd2db1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -21,7 +21,7 @@ Suggests: Config/testthat/edition: 3 Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.2.9000 +RoxygenNote: 7.3.2 Imports: btw (>= 0.0.1.9000), cli, diff --git a/NAMESPACE b/NAMESPACE index 42f672e..7d4c825 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,5 @@ # Generated by roxygen2: do not edit by hand -export(mcp_proxy) -export(mcp_serve) +export(mcp_host) +export(mcp_server) import(rlang) diff --git a/R/host.R b/R/host.R new file mode 100644 index 0000000..12672e7 --- /dev/null +++ b/R/host.R @@ -0,0 +1,143 @@ +#' Model context protocol for your R session +#' +#' @description +#' Together, these functions implement a model context protocol server for your +#' R session. +#' +#' @section Configuration: +#' +#' [mcp_server()] should be configured with the MCP clients via the `Rscript` +#' command. For example, to use with Claude Desktop, paste the following in your +#' Claude Desktop configuration (on macOS, at +#' `file.edit("~/Library/Application Support/Claude/claude_desktop_config.json")`): +#' +#' ```json +#' { +#' "mcpServers": { +#' "r-acquaint": { +#' "command": "Rscript", +#' "args": ["-e", "acquaint::mcp_server()"] +#' } +#' } +#' } +#' ``` +#' +#' Or, to use with Claude Code, you might type in a terminal: +#' +#' ```bash +#' claude mcp add -s "user" r-acquaint Rscript -e "acquaint::mcp_server()" +#' ``` +#' +#' **mcp_server() is not intended for interactive use.** +#' +#' The server interfaces with the MCP client on behalf of the host in +#' your R session. **Use [mcp_host()] to start the host in your R session.** +#' Place a call to `acquaint::mcp_host()` in your `.Rprofile`, perhaps with +#' `usethis::edit_r_profile()`, to start a host for every interactive R session +#' you start. +#' +#' @examples +#' if (interactive()) { +#' mcp_host() +#' } +#' +#' @name mcp +#' @export +mcp_host <- function() { + # HACK: If a host is already running in one session via `.Rprofile`, + # `mcp_host()` will be called again when the client runs the command + # Rscript -e "acquaint::mcp_server()" and the existing host will be wiped. + # Returning early in this case allows for the desired R session host to be + # running already before the client initiates the server. + if (!interactive()) { + return(invisible()) + } + + the$host_socket <- nanonext::socket("poly") + i <- 1L + suppressWarnings( + while (i < 1024L) { + # prevent indefinite loop + nanonext::listen( + the$host_socket, + url = sprintf("%s%d", acquaint_socket, i) + ) || + break + i <- i + 1L + } + ) + + schedule_handle_message_from_server() +} + +handle_message_from_server <- function(msg) { + pipe <- nanonext::pipe_id(the$raio) + schedule_handle_message_from_server() + + # cat("RECV :", msg, "\n", sep = "", file = stderr()) + if (!nzchar(msg)) { + return(nanonext::send_aio(the$host_socket, commandArgs(), pipe = pipe)) + } + data <- jsonlite::parse_json(msg) + + if (data$method == "tools/call") { + name <- data$params$name + fn <- getNamespace("btw")[[name]] + args <- data$params$arguments + + # HACK for btw_tool_env_describe_environment. In the JSON, it will have + # `"items": []`, and that translates to an empty list, but we want NULL. + if (name == "btw_tool_env_describe_environment") { + if (identical(args$items, list())) { + args$items <- NULL + } + } + + tool_call_result <- do.call(fn, args) + # cat(paste(capture.output(str(body)), collapse="\n"), file=stderr()) + + body <- jsonrpc_response( + data$id, + list( + content = list( + list( + type = "text", + text = paste(tool_call_result, collapse = "\n") + ) + ), + isError = FALSE + ) + ) + } else { + body <- jsonrpc_response( + data$id, + error = list(code = -32601, message = "Method not found") + ) + } + # cat("SEND:", to_json(body), "\n", sep = "", file = stderr()) + + nanonext::send_aio( + the$host_socket, + to_json(body), + mode = "raw", + pipe = pipe + ) +} + +schedule_handle_message_from_server <- function() { + the$raio <- nanonext::recv_aio(the$host_socket, mode = "string") + promises::as.promise(the$raio)$then(handle_message_from_server)$catch( + function( + e + ) { + print(e) + } + ) +} + +# Create a jsonrpc-structured response object. + +# Given a vector or list, drop all the NULL items in it +drop_nulls <- function(x) { + x[!vapply(x, is.null, FUN.VALUE = logical(1))] +} diff --git a/R/proxy.R b/R/proxy.R deleted file mode 100644 index d48034a..0000000 --- a/R/proxy.R +++ /dev/null @@ -1,241 +0,0 @@ -# This R script is a proxy. It takes input on stdin, and when the input forms -# valid JSON, it will send the JSON to the server. Then, when it receives the -# response, it will print the response to stdout. -#' @rdname mcp -#' @export -mcp_proxy <- function() { - # TODO: should this actually be a check for being called within Rscript or not? - check_not_interactive() - - the$proxy_socket <- nanonext::socket("poly") - nanonext::dial(the$proxy_socket, url = sprintf("%s%d", acquaint_socket, 1L)) - - # Note that we're using file("stdin") instead of stdin(), which are not the - # same. - the$f <- file("stdin", open = "r") - - schedule_handle_message_from_client() - schedule_handle_message_from_server() - - # Pump the event loop - while (TRUE) { - later::run_now(Inf) - } -} - -handle_message_from_client <- function(fdstatus) { - buf <- "" - schedule_handle_message_from_client() - # TODO: Read multiple lines all at once (because the server can send - # multiple requests quickly), and then handle each line separately. - # Otherwise, the message throughput will be bound by the polling rate. - line <- readLines(the$f, n = 1) - # TODO: If stdin is closed, we should exit. Not sure there's a way to detect - # that stdin has been closed without writing C code, though. - - if (length(line) == 0) { - return() - } - - logcat("FROM CLIENT: ", line) - - buf <- paste0(c(buf, line), collapse = "\n") - - data <- NULL - - tryCatch( - { - data <- jsonlite::parse_json(buf) - }, - error = function(e) { - # Invalid JSON. Possibly unfinished multi-line JSON message? - } - ) - - if (is.null(data)) { - # Can get here if there's an empty line - return() - } - - if (!is.list(data) || is.null(data$method)) { - cat_json(jsonrpc_response( - data$id, - error = list(code = -32600, message = "Invalid Request") - )) - } - - # If we made it here, it's valid JSON - - if (data$method == "initialize") { - res <- jsonrpc_response(data$id, capabilities()) - cat_json(res) - } else if (data$method == "tools/list") { - res <- jsonrpc_response( - data$id, - list( - tools = get_all_btw_tools() - ) - ) - - cat_json(res) - } else if (data$method == "tools/call") { - result <- forward_request(buf) - - # } else if (data$method == "prompts/list") { - # } else if (data$method == "resources/list") { - } else if (is.null(data$id)) { - # If there is no `id` in the request, then this is a notification and the - # client does not expect a response. - if (data$method == "notifications/initialized") { - } - } else { - cat_json(jsonrpc_response( - data$id, - error = list(code = -32601, message = "Method not found") - )) - } - - buf <- "" -} - -schedule_handle_message_from_client <- function() { - # Schedule the callback to run when stdin (fd 0) has input. - later::later_fd(handle_message_from_client, readfds = 0L) -} - -handle_message_from_server <- function(data) { - if (!is.character(data)) { - return() - } - - schedule_handle_message_from_server() - - logcat("FROM SERVER: ", data) - - # The response_text is already JSON, so we'll use cat() instead of cat_json() - nanonext::write_stdout(data) -} - -schedule_handle_message_from_server <- function() { - r <- nanonext::recv_aio(the$proxy_socket, mode = "string") - promises::as.promise(r)$then(handle_message_from_server) -} - -forward_request <- function(data) { - logcat("TO SERVER: ", data) - - nanonext::send_aio(the$proxy_socket, data, mode = "raw") -} - -# This process will be launched by the MCP client, so stdout/stderr aren't -# visible. This function will log output to the `logfile` so that you can view -# it. -logcat <- function(x, ..., append = TRUE) { - log_file <- acquaint_log_file() - cat(x, "\n", sep = "", append = append, file = log_file) -} - -cat_json <- function(x) { - nanonext::write_stdout(to_json(x)) -} - -capabilities <- function() { - list( - protocolVersion = "2024-11-05", - capabilities = list( - # logging = named_list(), - prompts = named_list( - listChanged = FALSE - ), - resources = named_list( - subscribe = FALSE, - listChanged = FALSE - ), - tools = named_list( - listChanged = FALSE - ) - ), - serverInfo = list( - name = "R acquaint server", - version = "0.0.1" - ), - instructions = "This provides information about a running R session." - ) -} - -# Hacky way of getting tools from btw -get_all_btw_tools <- function() { - dummy_provider <- ellmer::Provider("dummy", "dummy", "dummy") - - .btw_tools <- getNamespace("btw")[[".btw_tools"]] - tools <- lapply(unname(.btw_tools), function(tool_obj) { - tool <- tool_obj$tool() - - if (is.null(tool)) { - return(NULL) - } - - as_json <- getNamespace("ellmer")[["as_json"]] - inputSchema <- compact(as_json(dummy_provider, tool@arguments)) - # This field is present but shouldn't be - inputSchema$description <- NULL - - list( - name = tool@name, - description = tool@description, - inputSchema = inputSchema - ) - }) - - compact(tools) -} - -compact <- function(.x) { - Filter(length, .x) -} - -check_not_interactive <- function(call = caller_env()) { - if (interactive()) { - cli::cli_abort( - c( - "This function is not intended for interactive use.", - "i" = "See {.help {.fn mcp_proxy}} for instructions on configuring this - function with applications" - ), - call = call - ) - } -} - -mcp_discover <- function() { - sock <- nanonext::socket("poly") - on.exit(nanonext:::reap(sock)) - cv <- nanonext::cv() - monitor <- nanonext::monitor(sock, cv) - suppressWarnings( - for (i in seq_len(1024L)) { - nanonext::dial( - sock, - url = sprintf("%s%d", acquaint_socket, i), - autostart = NA - ) && - break - } - ) - pipes <- nanonext::read_monitor(monitor) - res <- lapply(seq_along(pipes), function(x) nanonext::recv_aio(sock)) - lapply( - pipes, - function(x) nanonext::send_aio(sock, "", mode = "raw", pipe = x) - ) - nanonext::collect_aio_(res) -} - -select_server <- function(i) { - lapply(the$proxy_socket[["dialer"]], nanonext::reap) - attr(the$proxy_socket, "dialer") <- NULL - nanonext::dial( - the$proxy_socket, - url = sprintf("%s%d", acquaint_socket, as.integer(i)) - ) -} diff --git a/R/server.R b/R/server.R index 1274d09..71e357e 100644 --- a/R/server.R +++ b/R/server.R @@ -1,141 +1,241 @@ -#' Model context protocol for your R session -#' -#' @description -#' Together, these functions implement a model context protocol server for your -#' R session. -#' -#' @section Configuration: -#' -#' [mcp_proxy()] should be configured with the MCP clients via the `Rscript` -#' command. For example, to use with Claude Desktop, paste the following in your -#' Claude Desktop configuration (on macOS, at -#' `file.edit("~/Library/Application Support/Claude/claude_desktop_config.json")`): -#' -#' ```json -#' { -#' "mcpServers": { -#' "r-acquaint": { -#' "command": "Rscript", -#' "args": ["-e", "acquaint::mcp_proxy()"] -#' } -#' } -#' } -#' ``` -#' -#' Or, to use with Claude Code, you might type in a terminal: -#' -#' ```bash -#' claude mcp add -s "user" r-acquaint Rscript -e "acquaint::mcp_proxy()" -#' ``` -#' -#' **mcp_proxy() is not intended for interactive use.** -#' -#' The proxy interfaces with the MCP client on behalf of the server hosted in -#' your R session. **Use [mcp_serve()] to start the MCP server in your R session.** -#' Place a call to `acquaint::mcp_serve()` in your `.Rprofile`, perhaps with -#' `usethis::edit_r_profile()`, to start a server for your R session every time -#' you start R. -#' -#' @examples -#' if (interactive()) { -#' mcp_serve() -#' } -#' -#' @name mcp +# The MCP server is a proxy. It takes input on stdin, and when the input forms +# valid JSON, it will send the JSON to the host. Then, when it receives the +# response, it will print the response to stdout. +#' @rdname mcp #' @export -mcp_serve <- function() { - # HACK: If a server is already running in one session via `.Rprofile`, - # `mcp_serve()` will be called again when the client runs the command - # Rscript -e "acquaint::mcp_serve()" and the existing server will be wiped. - # Returning early in this case allows for the desired R session server to be - # running already before the client initiates the proxy. - if (!interactive()) { - return(invisible()) - } +mcp_server <- function() { + # TODO: should this actually be a check for being called within Rscript or not? + check_not_interactive() the$server_socket <- nanonext::socket("poly") - i <- 1L - suppressWarnings( - while (i < 1024L) { - # prevent indefinite loop - nanonext::listen( - the$server_socket, - url = sprintf("%s%d", acquaint_socket, i) - ) || - break - i <- i + 1L - } - ) + nanonext::dial(the$server_socket, url = sprintf("%s%d", acquaint_socket, 1L)) + + # Note that we're using file("stdin") instead of stdin(), which are not the + # same. + the$f <- file("stdin", open = "r") - schedule_handle_message_from_proxy() + schedule_handle_message_from_client() + schedule_handle_message_from_host() + + # Pump the event loop + while (TRUE) { + later::run_now(Inf) + } } -handle_message_from_proxy <- function(msg) { - pipe <- nanonext::pipe_id(the$raio) - schedule_handle_message_from_proxy() +handle_message_from_client <- function(fdstatus) { + buf <- "" + schedule_handle_message_from_client() + # TODO: Read multiple lines all at once (because the client can send + # multiple requests quickly), and then handle each line separately. + # Otherwise, the message throughput will be bound by the polling rate. + line <- readLines(the$f, n = 1) + # TODO: If stdin is closed, we should exit. Not sure there's a way to detect + # that stdin has been closed without writing C code, though. - # cat("RECV :", msg, "\n", sep = "", file = stderr()) - if (!nzchar(msg)) { - return(nanonext::send_aio(the$server_socket, commandArgs(), pipe = pipe)) + if (length(line) == 0) { + return() } - data <- jsonlite::parse_json(msg) - - if (data$method == "tools/call") { - name <- data$params$name - fn <- getNamespace("btw")[[name]] - args <- data$params$arguments - - # HACK for btw_tool_env_describe_environment. In the JSON, it will have - # `"items": []`, and that translates to an empty list, but we want NULL. - if (name == "btw_tool_env_describe_environment") { - if (identical(args$items, list())) { - args$items <- NULL - } + + logcat("FROM CLIENT: ", line) + + buf <- paste0(c(buf, line), collapse = "\n") + + data <- NULL + + tryCatch( + { + data <- jsonlite::parse_json(buf) + }, + error = function(e) { + # Invalid JSON. Possibly unfinished multi-line JSON message? } + ) + + if (is.null(data)) { + # Can get here if there's an empty line + return() + } + + if (!is.list(data) || is.null(data$method)) { + cat_json(jsonrpc_response( + data$id, + error = list(code = -32600, message = "Invalid Request") + )) + } - tool_call_result <- do.call(fn, args) - # cat(paste(capture.output(str(body)), collapse="\n"), file=stderr()) + # If we made it here, it's valid JSON - body <- jsonrpc_response( + if (data$method == "initialize") { + res <- jsonrpc_response(data$id, capabilities()) + cat_json(res) + } else if (data$method == "tools/list") { + res <- jsonrpc_response( data$id, list( - content = list( - list( - type = "text", - text = paste(tool_call_result, collapse = "\n") - ) - ), - isError = FALSE + tools = get_all_btw_tools() ) ) + + cat_json(res) + } else if (data$method == "tools/call") { + result <- forward_request(buf) + + # } else if (data$method == "prompts/list") { + # } else if (data$method == "resources/list") { + } else if (is.null(data$id)) { + # If there is no `id` in the request, then this is a notification and the + # client does not expect a response. + if (data$method == "notifications/initialized") { + } } else { - body <- jsonrpc_response( + cat_json(jsonrpc_response( data$id, error = list(code = -32601, message = "Method not found") - ) + )) } - # cat("SEND:", to_json(body), "\n", sep = "", file = stderr()) - nanonext::send_aio( - the$server_socket, - to_json(body), - mode = "raw", - pipe = pipe + buf <- "" +} + +schedule_handle_message_from_client <- function() { + # Schedule the callback to run when stdin (fd 0) has input. + later::later_fd(handle_message_from_client, readfds = 0L) +} + +handle_message_from_host <- function(data) { + if (!is.character(data)) { + return() + } + + schedule_handle_message_from_host() + + logcat("FROM HOST: ", data) + + # The response_text is already JSON, so we'll use cat() instead of cat_json() + nanonext::write_stdout(data) +} + +schedule_handle_message_from_host <- function() { + r <- nanonext::recv_aio(the$server_socket, mode = "string") + promises::as.promise(r)$then(handle_message_from_host) +} + +forward_request <- function(data) { + logcat("TO HOST: ", data) + + nanonext::send_aio(the$server_socket, data, mode = "raw") +} + +# This process will be launched by the MCP client, so stdout/stderr aren't +# visible. This function will log output to the `logfile` so that you can view +# it. +logcat <- function(x, ..., append = TRUE) { + log_file <- acquaint_log_file() + cat(x, "\n", sep = "", append = append, file = log_file) +} + +cat_json <- function(x) { + nanonext::write_stdout(to_json(x)) +} + +capabilities <- function() { + list( + protocolVersion = "2024-11-05", + capabilities = list( + # logging = named_list(), + prompts = named_list( + listChanged = FALSE + ), + resources = named_list( + subscribe = FALSE, + listChanged = FALSE + ), + tools = named_list( + listChanged = FALSE + ) + ), + serverInfo = list( + name = "R acquaint server", + version = "0.0.1" + ), + instructions = "This provides information about a running R session." ) } -schedule_handle_message_from_proxy <- function() { - the$raio <- nanonext::recv_aio(the$server_socket, mode = "string") - promises::as.promise(the$raio)$then(handle_message_from_proxy)$catch(function( - e - ) { - print(e) +# Hacky way of getting tools from btw +get_all_btw_tools <- function() { + dummy_provider <- ellmer::Provider("dummy", "dummy", "dummy") + + .btw_tools <- getNamespace("btw")[[".btw_tools"]] + tools <- lapply(unname(.btw_tools), function(tool_obj) { + tool <- tool_obj$tool() + + if (is.null(tool)) { + return(NULL) + } + + as_json <- getNamespace("ellmer")[["as_json"]] + inputSchema <- compact(as_json(dummy_provider, tool@arguments)) + # This field is present but shouldn't be + inputSchema$description <- NULL + + list( + name = tool@name, + description = tool@description, + inputSchema = inputSchema + ) }) + + compact(tools) +} + +compact <- function(.x) { + Filter(length, .x) +} + +check_not_interactive <- function(call = caller_env()) { + if (interactive()) { + cli::cli_abort( + c( + "This function is not intended for interactive use.", + "i" = "See {.help {.fn mcp_server}} for instructions on configuring this + function with applications" + ), + call = call + ) + } } -# Create a jsonrpc-structured response object. +mcp_discover <- function() { + sock <- nanonext::socket("poly") + on.exit(nanonext:::reap(sock)) + cv <- nanonext::cv() + monitor <- nanonext::monitor(sock, cv) + suppressWarnings( + for (i in seq_len(1024L)) { + nanonext::dial( + sock, + url = sprintf("%s%d", acquaint_socket, i), + autostart = NA + ) && + break + } + ) + pipes <- nanonext::read_monitor(monitor) + res <- lapply(seq_along(pipes), function(x) nanonext::recv_aio(sock)) + lapply( + pipes, + function(x) nanonext::send_aio(sock, "", mode = "raw", pipe = x) + ) + nanonext::collect_aio_(res) +} -# Given a vector or list, drop all the NULL items in it -drop_nulls <- function(x) { - x[!vapply(x, is.null, FUN.VALUE = logical(1))] +select_host <- function(i) { + lapply(the$server_socket[["dialer"]], nanonext::reap) + attr(the$server_socket, "dialer") <- NULL + nanonext::dial( + the$server_socket, + url = sprintf("%s%d", acquaint_socket, as.integer(i)) + ) } diff --git a/README.Rmd b/README.Rmd index fc85136..746e389 100644 --- a/README.Rmd +++ b/README.Rmd @@ -45,7 +45,7 @@ acquaint can be hooked up to any application that supports MCP. For example, to "mcpServers": { "r-acquaint": { "command": "Rscript", - "args": ["-e", "acquaint::mcp_proxy()"] + "args": ["-e", "acquaint::mcp_server()"] } } } @@ -54,10 +54,10 @@ acquaint can be hooked up to any application that supports MCP. For example, to Or, to use with Claude Code, you might type in a terminal: ```bash -claude mcp add -s "user" r-acquaint Rscript -e "acquaint::mcp_proxy()" +claude mcp add -s "user" r-acquaint Rscript -e "acquaint::mcp_server()" ``` -Then, in your R session, call `acquaint::mcp_server()`. +Then, in your R session, call `acquaint::mcp_host()`. ## Example diff --git a/README.md b/README.md index 17e699a..2036083 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ your Claude Desktop configuration (on macOS, at "mcpServers": { "r-acquaint": { "command": "Rscript", - "args": ["-e", "acquaint::mcp_proxy()"] + "args": ["-e", "acquaint::mcp_server()"] } } } @@ -50,10 +50,10 @@ your Claude Desktop configuration (on macOS, at Or, to use with Claude Code, you might type in a terminal: ``` bash -claude mcp add -s "user" r-acquaint Rscript -e "acquaint::mcp_proxy()" +claude mcp add -s "user" r-acquaint Rscript -e "acquaint::mcp_server()" ``` -Then, in your R session, call `acquaint::mcp_server()`. +Then, in your R session, call `acquaint::mcp_host()`. ## Example diff --git a/man/acquaint-package.Rd b/man/acquaint-package.Rd index 7c0ad26..85ca514 100644 --- a/man/acquaint-package.Rd +++ b/man/acquaint-package.Rd @@ -30,7 +30,7 @@ Authors: Other contributors: \itemize{ - \item Posit Software, PBC (\href{https://ror.org/03wc8by49}{ROR}) [copyright holder, funder] + \item Posit Software, PBC (03wc8by49) [copyright holder, funder] } } diff --git a/man/mcp.Rd b/man/mcp.Rd index 50af7c9..24c2572 100644 --- a/man/mcp.Rd +++ b/man/mcp.Rd @@ -1,14 +1,14 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/proxy.R, R/server.R -\name{mcp_proxy} -\alias{mcp_proxy} +% Please edit documentation in R/host.R, R/server.R +\name{mcp} \alias{mcp} -\alias{mcp_serve} +\alias{mcp_host} +\alias{mcp_server} \title{Model context protocol for your R session} \usage{ -mcp_proxy() +mcp_host() -mcp_serve() +mcp_server() } \description{ Together, these functions implement a model context protocol server for your @@ -17,7 +17,7 @@ R session. \section{Configuration}{ -\code{\link[=mcp_proxy]{mcp_proxy()}} should be configured with the MCP clients via the \code{Rscript} +\code{\link[=mcp_server]{mcp_server()}} should be configured with the MCP clients via the \code{Rscript} command. For example, to use with Claude Desktop, paste the following in your Claude Desktop configuration (on macOS, at \code{file.edit("~/Library/Application Support/Claude/claude_desktop_config.json")}): @@ -26,7 +26,7 @@ Claude Desktop configuration (on macOS, at "mcpServers": \{ "r-acquaint": \{ "command": "Rscript", - "args": ["-e", "acquaint::mcp_proxy()"] + "args": ["-e", "acquaint::mcp_server()"] \} \} \} @@ -34,21 +34,21 @@ Claude Desktop configuration (on macOS, at Or, to use with Claude Code, you might type in a terminal: -\if{html}{\out{
}}\preformatted{claude mcp add -s "user" r-acquaint Rscript -e "acquaint::mcp_proxy()" +\if{html}{\out{
}}\preformatted{claude mcp add -s "user" r-acquaint Rscript -e "acquaint::mcp_server()" }\if{html}{\out{
}} -\strong{mcp_proxy() is not intended for interactive use.} +\strong{mcp_server() is not intended for interactive use.} -The proxy interfaces with the MCP client on behalf of the server hosted in -your R session. \strong{Use \code{\link[=mcp_serve]{mcp_serve()}} to start the MCP server in your R session.} -Place a call to \code{acquaint::mcp_serve()} in your \code{.Rprofile}, perhaps with -\code{usethis::edit_r_profile()}, to start a server for your R session every time -you start R. +The server interfaces with the MCP client on behalf of the host in +your R session. \strong{Use \code{\link[=mcp_host]{mcp_host()}} to start the host in your R session.} +Place a call to \code{acquaint::mcp_host()} in your \code{.Rprofile}, perhaps with +\code{usethis::edit_r_profile()}, to start a host for every interactive R session +you start. } \examples{ if (interactive()) { -mcp_serve() +mcp_host() } } diff --git a/tests/testthat/_snaps/proxy.md b/tests/testthat/_snaps/proxy.md deleted file mode 100644 index 1cbd53e..0000000 --- a/tests/testthat/_snaps/proxy.md +++ /dev/null @@ -1,9 +0,0 @@ -# check_not_interactive errors informatively - - Code - mcp_proxy() - Condition - Error in `mcp_proxy()`: - ! This function is not intended for interactive use. - i See `mcp_proxy()` for instructions on configuring this function with applications - diff --git a/tests/testthat/_snaps/server.md b/tests/testthat/_snaps/server.md new file mode 100644 index 0000000..935f342 --- /dev/null +++ b/tests/testthat/_snaps/server.md @@ -0,0 +1,9 @@ +# check_not_interactive errors informatively + + Code + mcp_server() + Condition + Error in `mcp_server()`: + ! This function is not intended for interactive use. + i See `mcp_server()` for instructions on configuring this function with applications + diff --git a/tests/testthat/test-proxy.R b/tests/testthat/test-server.R similarity index 73% rename from tests/testthat/test-proxy.R rename to tests/testthat/test-server.R index 7c72ea9..9897d46 100644 --- a/tests/testthat/test-proxy.R +++ b/tests/testthat/test-server.R @@ -1,5 +1,5 @@ test_that("check_not_interactive errors informatively", { testthat::local_mocked_bindings(interactive = function(...) TRUE) - expect_snapshot(error = TRUE, mcp_proxy()) + expect_snapshot(error = TRUE, mcp_server()) })