diff --git a/DESCRIPTION b/DESCRIPTION index 2614f07..9315e21 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -26,7 +26,7 @@ Imports: ellmer, jsonlite, later, - nanonext, + nanonext (>= 1.5.2.9009), promises, rlang Depends: R (>= 4.1.0) @@ -34,4 +34,5 @@ URL: https://github.com/simonpcouch/acquaint, https://simonpcouch.github.io/acqu BugReports: https://github.com/simonpcouch/acquaint/issues Config/Needs/website: tidyverse/tidytemplate Remotes: - posit-dev/btw + posit-dev/btw, + r-lib/nanonext diff --git a/R/proxy.R b/R/proxy.R index 29a3f60..2a19835 100644 --- a/R/proxy.R +++ b/R/proxy.R @@ -11,8 +11,7 @@ mcp_proxy <- function() { # Note that we're using file("stdin") instead of stdin(), which are not the # same. - the$f <- file("stdin") - open(the$f, blocking = FALSE) + the$f <- file("stdin", open = "r") schedule_handle_message_from_client() schedule_handle_message_from_server() @@ -108,19 +107,19 @@ handle_message_from_server <- function(data) { logcat("FROM SERVER: ", data) - # The response_text is alredy JSON, so we'll use cat() instead of cat_json() - cat(data, "\n", sep = "") + # 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) + 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) + the$saio <- nanonext::send_aio(the$proxy_socket, data, mode = "raw") } # This process will be launched by the MCP client, so stdout/stderr aren't @@ -132,7 +131,7 @@ logcat <- function(x, ..., append = TRUE) { } cat_json <- function(x) { - cat(to_json(x), "\n", sep = "") + nanonext::write_stdout(to_json(x)) } capabilities <- function() { diff --git a/R/server.R b/R/server.R index 520eef0..70a06c4 100644 --- a/R/server.R +++ b/R/server.R @@ -1,16 +1,16 @@ #' Model context protocol for your R session -#' +#' #' @description #' Together, these functions implement a model context protocol server for your #' R session. -#' -#' @section Configuration: -#' +#' +#' @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 +#' Claude Desktop configuration (on macOS, at #' `file.edit("~/Library/Application Support/Claude/claude_desktop_config.json")`): -#' +#' #' ```json #' { #' "mcpServers": { @@ -21,31 +21,31 @@ #' } #' } #' ``` -#' +#' #' 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 #' @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 + # 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. @@ -99,11 +99,12 @@ handle_message_from_proxy <- function(msg) { } # cat("SEND:", to_json(body), "\n", sep = "", file = stderr()) - nanonext::send_aio(the$server_socket, to_json(body)) + # TODO: consider if better / more robust using synchronous sends + the$saio <- nanonext::send_aio(the$server_socket, to_json(body), mode = "raw") } schedule_handle_message_from_proxy <- function() { - r <- nanonext::recv_aio(the$server_socket) + r <- nanonext::recv_aio(the$server_socket, mode = "string") promises::as.promise(r)$then(handle_message_from_proxy)$catch(function(e) { print(e) })