From ba1e2b90c09d75d48519b258fa31e5f594efb29d Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Thu, 1 May 2025 10:25:38 +0100 Subject: [PATCH 1/9] Upgrade to poly + automatically increment server socket url --- R/proxy.R | 6 +++++- R/server.R | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/R/proxy.R b/R/proxy.R index 2a19835..99d587c 100644 --- a/R/proxy.R +++ b/R/proxy.R @@ -7,7 +7,11 @@ 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("pair", dial = acquaint_socket) + the$proxy_socket <- nanonext::socket("poly") + i <- 1L + suppressWarnings( + nanonext::dial(the$proxy_socket, url = sprintf("%s%d", acquaint_socket, i)) + ) # Note that we're using file("stdin") instead of stdin(), which are not the # same. diff --git a/R/server.R b/R/server.R index 70a06c4..5fb5318 100644 --- a/R/server.R +++ b/R/server.R @@ -53,7 +53,15 @@ mcp_serve <- function() { return(invisible()) } - the$server_socket <- nanonext::socket("pair", listen = acquaint_socket) + the$server_socket <- nanonext::socket("poly") + i <- 1L + suppressWarnings( + repeat { + nanonext::listen(the$server_socket, url = sprintf("%s%d", acquaint_socket, i)) || break + i <- i + 1L + } + ) + schedule_handle_message_from_proxy() } From 8b65393c3135f3c18d51d23b79b20098d2456f9e Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Thu, 1 May 2025 11:24:54 +0100 Subject: [PATCH 2/9] Servers can now handle multiple proxies --- R/server.R | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/R/server.R b/R/server.R index 5fb5318..6059b29 100644 --- a/R/server.R +++ b/R/server.R @@ -66,6 +66,7 @@ mcp_serve <- function() { } handle_message_from_proxy <- function(msg) { + pipe <- the$raio[["aio"]] schedule_handle_message_from_proxy() # cat("RECV :", msg, "\n", sep = "", file = stderr()) @@ -108,12 +109,17 @@ 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), mode = "raw") + the$saio <- nanonext::send_aio( + the$server_socket, + to_json(body), + mode = "raw", + pipe = pipe + ) } schedule_handle_message_from_proxy <- function() { - r <- nanonext::recv_aio(the$server_socket, mode = "string") - promises::as.promise(r)$then(handle_message_from_proxy)$catch(function(e) { + 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) }) } From 11e73e3814590baf67f498635b88ef1026e68be4 Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Thu, 1 May 2025 12:00:30 +0100 Subject: [PATCH 3/9] Allow proxy to connect to a specific server instance --- R/proxy.R | 7 +++++-- man/mcp.Rd | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/R/proxy.R b/R/proxy.R index 99d587c..74b6af0 100644 --- a/R/proxy.R +++ b/R/proxy.R @@ -1,14 +1,17 @@ # 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. +#' @param i integer instance number. #' @rdname mcp #' @export -mcp_proxy <- function() { +mcp_proxy <- function(i = 1L) { # TODO: should this actually be a check for being called within Rscript or not? check_not_interactive() + i <- as.integer(i) + if (is.na(i)) + abort("`i` should be an integer value") the$proxy_socket <- nanonext::socket("poly") - i <- 1L suppressWarnings( nanonext::dial(the$proxy_socket, url = sprintf("%s%d", acquaint_socket, i)) ) diff --git a/man/mcp.Rd b/man/mcp.Rd index 50af7c9..11ef9b8 100644 --- a/man/mcp.Rd +++ b/man/mcp.Rd @@ -6,10 +6,13 @@ \alias{mcp_serve} \title{Model context protocol for your R session} \usage{ -mcp_proxy() +mcp_proxy(i = 1L) mcp_serve() } +\arguments{ +\item{i}{integer instance number.} +} \description{ Together, these functions implement a model context protocol server for your R session. From 6771c0c6bb67daed641c5186eda30cdbe9e0e028 Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Thu, 1 May 2025 13:54:36 +0100 Subject: [PATCH 4/9] Use nanonext interface for pipe IDs --- DESCRIPTION | 4 ++-- R/server.R | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 9315e21..39d8d33 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -19,14 +19,14 @@ Suggests: Config/testthat/edition: 3 Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.2 +RoxygenNote: 7.3.2.9000 Imports: btw (>= 0.0.1.9000), cli, ellmer, jsonlite, later, - nanonext (>= 1.5.2.9009), + nanonext (>= 1.5.2.9010), promises, rlang Depends: R (>= 4.1.0) diff --git a/R/server.R b/R/server.R index 6059b29..c0b4775 100644 --- a/R/server.R +++ b/R/server.R @@ -66,7 +66,7 @@ mcp_serve <- function() { } handle_message_from_proxy <- function(msg) { - pipe <- the$raio[["aio"]] + pipe <- nanonext::pipe_id(the$raio) schedule_handle_message_from_proxy() # cat("RECV :", msg, "\n", sep = "", file = stderr()) From eaab59bad6c644a6a063a5b5bfa3c4103450fd26 Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Thu, 1 May 2025 15:49:15 +0100 Subject: [PATCH 5/9] Prevent indefinite loop --- R/server.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/server.R b/R/server.R index c0b4775..062b1c7 100644 --- a/R/server.R +++ b/R/server.R @@ -56,7 +56,7 @@ mcp_serve <- function() { the$server_socket <- nanonext::socket("poly") i <- 1L suppressWarnings( - repeat { + while (i < 65536L) { # prevent indefinite loop nanonext::listen(the$server_socket, url = sprintf("%s%d", acquaint_socket, i)) || break i <- i + 1L } From 35eb442aefbc835eaa7bf3ca10f01039a8881b61 Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Thu, 1 May 2025 16:11:09 +0100 Subject: [PATCH 6/9] Adds Charlie as 'aut' --- DESCRIPTION | 2 ++ man/acquaint-package.Rd | 1 + 2 files changed, 3 insertions(+) diff --git a/DESCRIPTION b/DESCRIPTION index 39d8d33..ed1a18e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -6,6 +6,8 @@ Authors@R: c( comment = c(ORCID = "0000-0001-5676-5107")), person("Winston", "Chang", , "winston@posit.co", role = "aut", comment = c(ORCID = "0000-0001-5676-5107")), + person("Charlie", "Gao", , "charlie.gao@posit.co", role = "aut", + comment = c(ORCID = "0000-0002-0750-061X")), person("Posit Software, PBC", role = c("cph", "fnd")) ) Description: The goal of acquaint is to enable LLM-enabled tools like Claude Code to diff --git a/man/acquaint-package.Rd b/man/acquaint-package.Rd index 668a215..9ad8d92 100644 --- a/man/acquaint-package.Rd +++ b/man/acquaint-package.Rd @@ -25,6 +25,7 @@ Useful links: Authors: \itemize{ \item Winston Chang \email{winston@posit.co} (\href{https://orcid.org/0000-0001-5676-5107}{ORCID}) + \item Charlie Gao \email{charlie.gao@posit.co} (\href{https://orcid.org/0000-0002-0750-061X}{ORCID}) } Other contributors: From 456bc302ac03d02e9d9d757c7da7330bb131b75c Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Thu, 1 May 2025 16:14:18 +0100 Subject: [PATCH 7/9] Remove orcid for Winston, add Posit ror --- DESCRIPTION | 6 +++--- man/acquaint-package.Rd | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index ed1a18e..37a9b52 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -4,11 +4,11 @@ Version: 0.0.0.9000 Authors@R: c( person("Simon", "Couch", , "simon.couch@posit.co", role = c("aut", "cre"), comment = c(ORCID = "0000-0001-5676-5107")), - person("Winston", "Chang", , "winston@posit.co", role = "aut", - comment = c(ORCID = "0000-0001-5676-5107")), + person("Winston", "Chang", , "winston@posit.co", role = "aut"), person("Charlie", "Gao", , "charlie.gao@posit.co", role = "aut", comment = c(ORCID = "0000-0002-0750-061X")), - person("Posit Software, PBC", role = c("cph", "fnd")) + person("Posit Software, PBC", role = c("cph", "fnd"), + comment = c(ROR = "03wc8by49")) ) Description: The goal of acquaint is to enable LLM-enabled tools like Claude Code to learn about the R packages you have installed using the diff --git a/man/acquaint-package.Rd b/man/acquaint-package.Rd index 9ad8d92..7c0ad26 100644 --- a/man/acquaint-package.Rd +++ b/man/acquaint-package.Rd @@ -24,13 +24,13 @@ Useful links: Authors: \itemize{ - \item Winston Chang \email{winston@posit.co} (\href{https://orcid.org/0000-0001-5676-5107}{ORCID}) + \item Winston Chang \email{winston@posit.co} \item Charlie Gao \email{charlie.gao@posit.co} (\href{https://orcid.org/0000-0002-0750-061X}{ORCID}) } Other contributors: \itemize{ - \item Posit Software, PBC [copyright holder, funder] + \item Posit Software, PBC (\href{https://ror.org/03wc8by49}{ROR}) [copyright holder, funder] } } From 026b1d251130eb966af913842ca647f15e381902 Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Fri, 2 May 2025 11:19:54 +0100 Subject: [PATCH 8/9] Use more robust send_aio() from latest nanonext --- DESCRIPTION | 2 +- R/proxy.R | 2 +- R/server.R | 8 +------- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 37a9b52..5f96551 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -28,7 +28,7 @@ Imports: ellmer, jsonlite, later, - nanonext (>= 1.5.2.9010), + nanonext (>= 1.5.2.9012), promises, rlang Depends: R (>= 4.1.0) diff --git a/R/proxy.R b/R/proxy.R index 74b6af0..f266962 100644 --- a/R/proxy.R +++ b/R/proxy.R @@ -126,7 +126,7 @@ schedule_handle_message_from_server <- function() { forward_request <- function(data) { logcat("TO SERVER: ", data) - the$saio <- nanonext::send_aio(the$proxy_socket, data, mode = "raw") + 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 062b1c7..9663d7b 100644 --- a/R/server.R +++ b/R/server.R @@ -108,13 +108,7 @@ 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), - mode = "raw", - pipe = pipe - ) + nanonext::send_aio(the$server_socket, to_json(body), mode = "raw", pipe = pipe) } schedule_handle_message_from_proxy <- function() { From e0368c88618f90871f8f2ee1e05f92f7e9962948 Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Mon, 5 May 2025 18:59:42 +0100 Subject: [PATCH 9/9] Implement mcp_discover() and select_server() --- R/proxy.R | 40 ++++++++++++++++++++++++++++++++-------- R/server.R | 5 ++++- man/mcp.Rd | 5 +---- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/R/proxy.R b/R/proxy.R index f266962..485fbb9 100644 --- a/R/proxy.R +++ b/R/proxy.R @@ -1,20 +1,14 @@ # 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. -#' @param i integer instance number. #' @rdname mcp #' @export -mcp_proxy <- function(i = 1L) { +mcp_proxy <- function() { # TODO: should this actually be a check for being called within Rscript or not? check_not_interactive() - i <- as.integer(i) - if (is.na(i)) - abort("`i` should be an integer value") the$proxy_socket <- nanonext::socket("poly") - suppressWarnings( - nanonext::dial(the$proxy_socket, url = sprintf("%s%d", acquaint_socket, i)) - ) + 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. @@ -110,6 +104,10 @@ schedule_handle_message_from_client <- function() { } handle_message_from_server <- function(data) { + if (!is.character(data)) { + return() + } + schedule_handle_message_from_server() logcat("FROM SERVER: ", data) @@ -208,3 +206,29 @@ check_not_interactive <- function(call = caller_env()) { ) } } + +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 9663d7b..2017aa6 100644 --- a/R/server.R +++ b/R/server.R @@ -56,7 +56,7 @@ mcp_serve <- function() { the$server_socket <- nanonext::socket("poly") i <- 1L suppressWarnings( - while (i < 65536L) { # prevent indefinite loop + while (i < 1024L) { # prevent indefinite loop nanonext::listen(the$server_socket, url = sprintf("%s%d", acquaint_socket, i)) || break i <- i + 1L } @@ -70,6 +70,9 @@ handle_message_from_proxy <- function(msg) { schedule_handle_message_from_proxy() # cat("RECV :", msg, "\n", sep = "", file = stderr()) + if (!nzchar(msg)) { + return(nanonext::send_aio(the$server_socket, commandArgs(), pipe = pipe)) + } data <- jsonlite::parse_json(msg) if (data$method == "tools/call") { diff --git a/man/mcp.Rd b/man/mcp.Rd index 11ef9b8..50af7c9 100644 --- a/man/mcp.Rd +++ b/man/mcp.Rd @@ -6,13 +6,10 @@ \alias{mcp_serve} \title{Model context protocol for your R session} \usage{ -mcp_proxy(i = 1L) +mcp_proxy() mcp_serve() } -\arguments{ -\item{i}{integer instance number.} -} \description{ Together, these functions implement a model context protocol server for your R session.