Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ Imports:
ellmer,
jsonlite,
later,
nanonext,
nanonext (>= 1.5.2.9009),
promises,
rlang
Depends: R (>= 4.1.0)
URL: https://github.com/simonpcouch/acquaint, https://simonpcouch.github.io/acquaint/
BugReports: https://github.com/simonpcouch/acquaint/issues
Config/Needs/website: tidyverse/tidytemplate
Remotes:
posit-dev/btw
posit-dev/btw,
r-lib/nanonext
13 changes: 6 additions & 7 deletions R/proxy.R
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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() {
Expand Down
33 changes: 17 additions & 16 deletions R/server.R
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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.
Expand Down Expand Up @@ -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)
})
Expand Down