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
2 changes: 1 addition & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Generated by roxygen2: do not edit by hand

export(mcp_host)
export(mcp_server)
export(mcp_session)
import(rlang)
23 changes: 9 additions & 14 deletions R/server.R
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# 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
# valid JSON, it will send the JSON to the session. Then, when it receives the
# response, it will print the response to stdout.
#' @rdname mcp
#' @export
Expand All @@ -17,25 +17,21 @@ mcp_server <- function() {
nanonext::dial(the$server_socket, url = sprintf("%s%d", acquaint_socket, 1L))

client <- nanonext::recv_aio(reader_socket, mode = "string", cv = cv)
host <- nanonext::recv_aio(the$server_socket, mode = "string", cv = cv)
session <- nanonext::recv_aio(the$server_socket, mode = "string", cv = cv)

while (nanonext::wait(cv)) {

if (!nanonext::unresolved(host)) {
handle_message_from_host(host$data)
host <- nanonext::recv_aio(the$server_socket, mode = "string", cv = cv)
if (!nanonext::unresolved(session)) {
handle_message_from_session(session$data)
session <- nanonext::recv_aio(the$server_socket, mode = "string", cv = cv)
}
if (!nanonext::unresolved(client)) {
handle_message_from_client(client$data)
client <- nanonext::recv_aio(reader_socket, mode = "string", cv = cv)
}

}

}

handle_message_from_client <- function(line) {

if (length(line) == 0) {
return()
}
Expand Down Expand Up @@ -83,7 +79,7 @@ handle_message_from_client <- function(line) {
tool_name <- data$params$name
if (tool_name %in% c("list_r_sessions", "select_r_session")) {
# two tools provided by acquaint itself which must be executed in
# the server session rather than a host (#18)
# the server rather than a session (#18)
result <- as_tool_call_result(
data,
do.call(tool_name, data$params$arguments)
Expand All @@ -104,22 +100,21 @@ handle_message_from_client <- function(line) {
error = list(code = -32601, message = "Method not found")
))
}

}

handle_message_from_host <- function(data) {
handle_message_from_session <- function(data) {
if (!is.character(data)) {
return()
}

logcat(c("FROM HOST: ", data))
logcat(c("FROM SESSION: ", data))

# The response_text is already JSON, so we don't need to use cat_json()
nanonext::write_stdout(data)
}

forward_request <- function(data) {
logcat(c("TO HOST: ", data))
logcat(c("TO SESSION: ", data))

nanonext::send_aio(the$server_socket, data, mode = "raw")
}
Expand Down
36 changes: 18 additions & 18 deletions R/host.R → R/session.R
Original file line number Diff line number Diff line change
Expand Up @@ -30,36 +30,36 @@
#'
#' **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.
#' The server interfaces with the MCP client on behalf of your R session.
#' **Use [mcp_session()] to make your R session available to the server.**
#' Place a call to `acquaint::mcp_session()` in your `.Rprofile`, perhaps with
#' `usethis::edit_r_profile()`, to make every interactive R session you start
#' available to the server.
#'
#' @examples
#' if (interactive()) {
#' mcp_host()
#' mcp_session()
#' }
#'
#' @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.
mcp_session <- function() {
# HACK: If a session is already available from another session via `.Rprofile`,
# `mcp_session()` will be called again when the client runs the command
# Rscript -e "acquaint::mcp_server()" and the existing session connection
# will be wiped. Returning early in this case allows for the desired R
# session to be running already before the client initiates the server.
if (!interactive()) {
return(invisible())
}

the$host_socket <- nanonext::socket("poly")
the$session_socket <- nanonext::socket("poly")
i <- 1L
suppressWarnings(
while (i < 1024L) {
# prevent indefinite loop
nanonext::listen(
the$host_socket,
the$session_socket,
url = sprintf("%s%d", acquaint_socket, i)
) ||
break
Expand All @@ -79,7 +79,7 @@ handle_message_from_server <- function(msg) {
if (!nzchar(msg)) {
return(
nanonext::send_aio(
the$host_socket,
the$session_socket,
describe_session(),
mode = "raw",
pipe = pipe
Expand Down Expand Up @@ -117,7 +117,7 @@ handle_message_from_server <- function(msg) {
# cat("SEND:", to_json(body), "\n", sep = "", file = stderr())

nanonext::send_aio(
the$host_socket,
the$session_socket,
to_json(body),
mode = "raw",
pipe = pipe
Expand All @@ -140,7 +140,7 @@ as_tool_call_result <- function(data, result) {
}

schedule_handle_message_from_server <- function() {
the$raio <- nanonext::recv_aio(the$host_socket, mode = "string")
the$raio <- nanonext::recv_aio(the$session_socket, mode = "string")
promises::as.promise(the$raio)$then(handle_message_from_server)$catch(
function(
e
Expand All @@ -160,7 +160,7 @@ drop_nulls <- function(x) {
# Enough information for the user to be able to identify which
# session is which when using `list_r_sessions()` (#18)
describe_session <- function() {
sprintf("%d: %s (%s)", the$session, basename(getwd()), infer_ide())
sprintf("%d: %s (%s)", the$session, basename(getwd()), infer_ide())
}

infer_ide <- function() {
Expand Down
11 changes: 6 additions & 5 deletions R/tools.R
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# These two functions are supplied to the client as tools and allow the client
# to discover R sessions which have called `acquaint::mcp_host()`. They
# to discover R sessions which have called `acquaint::mcp_session()`. They
# are "model-facing" rather than user-facing.
list_r_sessions <- function() {
sock <- nanonext::socket("poly")
Expand All @@ -13,7 +13,8 @@ list_r_sessions <- function() {
sock,
url = sprintf("%s%d", acquaint_socket, i),
autostart = NA
) && i > 8L
) &&
i > 8L
)
break
}
Expand All @@ -35,7 +36,7 @@ list_r_sessions_tool <-
.fun = list_r_sessions,
.description = paste(
"List the R sessions that are available to access.",
"R sessions which have run `acquaint::mcp_host()` will appear here.",
"R sessions which have run `acquaint::mcp_session()` will appear here.",
"In the output, start each session with 'Session #' and do NOT otherwise",
"prefix any index numbers to the output.",
"In general, do not use this tool unless asked to list or",
Expand All @@ -60,7 +61,7 @@ select_r_session_tool <-
ellmer::tool(
.fun = select_r_session,
.description = paste(
"Choose the R session host of interest.",
"Choose the R session of interest.",
"Use the `list_r_sessions` tool to discover potential sessions.",
"In general, do not use this tool unless asked to select a specific R",
"session; the tools available to you have a default R session",
Expand All @@ -71,7 +72,7 @@ select_r_session_tool <-
"Your choice of session will persist after the tool is called; only",
"call this tool more than once if you need to switch between sessions."
),
i = ellmer::type_integer("The index of the host session to select.")
i = ellmer::type_integer("The index of the R session to select.")
)

.acquaint_tools <- list(list_r_sessions_tool, select_r_session_tool)
2 changes: 1 addition & 1 deletion README.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Or, to use with Claude Code, you might type in a terminal:
claude mcp add -s "user" r-acquaint Rscript -e "acquaint::mcp_server()"
```

Then, in your R session, call `acquaint::mcp_host()`.
Then, in your R session, call `acquaint::mcp_session()`.

## Example

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Or, to use with Claude Code, you might type in a terminal:
claude mcp add -s "user" r-acquaint Rscript -e "acquaint::mcp_server()"
```

Then, in your R session, call `acquaint::mcp_host()`.
Then, in your R session, call `acquaint::mcp_session()`.

## Example

Expand Down
24 changes: 12 additions & 12 deletions man/mcp.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.