From cb11b7915960dcf4884e187bc032f84f4939fd7a Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:29:47 +0100 Subject: [PATCH 1/5] Use `nanonext::write_stdout()` --- DESCRIPTION | 3 ++- R/proxy.R | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 2614f07..3516c94 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) @@ -35,3 +35,4 @@ BugReports: https://github.com/simonpcouch/acquaint/issues Config/Needs/website: tidyverse/tidytemplate Remotes: posit-dev/btw + r-lib/nanonext diff --git a/R/proxy.R b/R/proxy.R index 29a3f60..6601a15 100644 --- a/R/proxy.R +++ b/R/proxy.R @@ -108,8 +108,8 @@ 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() { @@ -132,7 +132,7 @@ logcat <- function(x, ..., append = TRUE) { } cat_json <- function(x) { - cat(to_json(x), "\n", sep = "") + nanonext::write_stdout(to_json(x)) } capabilities <- function() { From df2e0e0c992dafe681ece4c5c3815bdaa07da5fb Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:32:10 +0100 Subject: [PATCH 2/5] Save send_aio reference --- R/proxy.R | 2 +- R/server.R | 31 ++++++++++++++++--------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/R/proxy.R b/R/proxy.R index 6601a15..cf14db3 100644 --- a/R/proxy.R +++ b/R/proxy.R @@ -120,7 +120,7 @@ schedule_handle_message_from_server <- function() { forward_request <- function(data) { logcat("TO SERVER: ", data) - nanonext::send_aio(the$proxy_socket, data) + the$saio <- nanonext::send_aio(the$proxy_socket, data) } # This process will be launched by the MCP client, so stdout/stderr aren't diff --git a/R/server.R b/R/server.R index 520eef0..4f2f54b 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,7 +99,8 @@ 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)) } schedule_handle_message_from_proxy <- function() { From 071ce559452d4388a2fcf139c5b942f1834d26b8 Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:38:50 +0100 Subject: [PATCH 3/5] Avoid R serialization completely --- R/proxy.R | 4 ++-- R/server.R | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/R/proxy.R b/R/proxy.R index cf14db3..38e36b2 100644 --- a/R/proxy.R +++ b/R/proxy.R @@ -113,14 +113,14 @@ handle_message_from_server <- function(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) - the$saio <- 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 diff --git a/R/server.R b/R/server.R index 4f2f54b..70a06c4 100644 --- a/R/server.R +++ b/R/server.R @@ -100,11 +100,11 @@ handle_message_from_proxy <- function(msg) { # cat("SEND:", to_json(body), "\n", sep = "", file = stderr()) # TODO: consider if better / more robust using synchronous sends - the$saio <- nanonext::send_aio(the$server_socket, to_json(body)) + 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) }) From 85521438066555444486fc266c2ffbe054f7378f Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:32:08 +0100 Subject: [PATCH 4/5] Open stdin in blocking mode to avoid potentially buggy behaviour on MacOS --- R/proxy.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/R/proxy.R b/R/proxy.R index 38e36b2..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() From 1be2a73f2f60b661497f8884bda21a3043ac0fb6 Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Wed, 30 Apr 2025 12:59:53 +0100 Subject: [PATCH 5/5] Correct remotes --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 3516c94..9315e21 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -34,5 +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