Skip to content

Commit f700559

Browse files
t-kalinowskisimonpcouchshikokuchuo
authored
Allow R session tools to be optional (#68)
* make R session tools optional. * Increment version to 0.1.1.9001 * Update R/server.R Co-authored-by: Simon P. Couch <[email protected]> * Update R/tools.R Co-authored-by: Simon P. Couch <[email protected]> * add NEWS * don't discover sessions if no session tools * early return if no sessions * update test snapshots * redocument * update docs * Rename some variables --------- Co-authored-by: Simon P. Couch <[email protected]> Co-authored-by: shikokuchuo <[email protected]>
1 parent f110b8e commit f700559

File tree

7 files changed

+77
-37
lines changed

7 files changed

+77
-37
lines changed

DESCRIPTION

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: mcptools
22
Title: Model Context Protocol Servers and Clients
3-
Version: 0.1.1.9000
3+
Version: 0.1.1.9001
44
Authors@R: c(
55
person("Simon", "Couch", , "[email protected]", role = c("aut", "cre"),
66
comment = c(ORCID = "0000-0001-5676-5107")),
@@ -43,4 +43,4 @@ Config/Needs/website: tidyverse/tidytemplate
4343
Config/testthat/edition: 3
4444
Encoding: UTF-8
4545
Roxygen: list(markdown = TRUE)
46-
RoxygenNote: 7.3.2
46+
RoxygenNote: 7.3.3

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# mcptools (development version)
22

3+
`mcp_server()` gains logical argument `session_tools`, allowing users to opt-out of presenting R sessions tools to clients.
4+
35
# mcptools 0.1.1
46

57
Addressed an issue in tests on `r-devel-linux-x86_64-fedora-clang`.

R/server.R

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,14 @@
5151
#' Examples for Claude Code on WSL and Claude Desktop on Windows are shown
5252
#' at <https://github.com/posit-dev/mcptools/issues/41#issuecomment-3036617046>.
5353
#'
54-
#' @param tools A list of tools created with [ellmer::tool()] that will be
55-
#' available from the server or a file path to an .R file that, when sourced,
56-
#' will return a list of tools. Any list that could be passed to
57-
#' `Chat$set_tools()` can be passed here. By default, the package won't serve
58-
#' any tools other than those needed to communicate with interactive R sessions.
54+
#' @param tools Optional collection of tools to expose. Supply either a list
55+
#' of objects created by [ellmer::tool()] or a path to an `.R` file that,
56+
#' when sourced, yields such a list. Defaults to `NULL`, which serves only
57+
#' the built-in session tools when `session_tools` is `TRUE`.
58+
#' @param ... Reserved for future use; currently ignored.
59+
#' @param session_tools Logical value whether to include the built-in session
60+
#' tools (`list_r_sessions`, `select_r_session`) that work with
61+
#' `mcp_session()`. Defaults to `TRUE`.
5962
#'
6063
#' @returns
6164
#' `mcp_server()` and `mcp_session()` are both called primarily for side-effects.
@@ -102,21 +105,32 @@
102105
#'
103106
#' @name server
104107
#' @export
105-
mcp_server <- function(tools = NULL) {
108+
mcp_server <- function(tools = NULL, ..., session_tools = TRUE) {
106109
# TODO: should this actually be a check for being called within Rscript or not?
107110
check_not_interactive()
108-
set_server_tools(tools)
111+
the$sessions_enabled <- isTRUE(session_tools)
112+
set_server_tools(tools, session_tools = the$sessions_enabled)
109113

110114
cv <- nanonext::cv()
115+
111116
reader_socket <- nanonext::read_stdin()
112117
on.exit(nanonext::reap(reader_socket))
113118
nanonext::pipe_notify(reader_socket, cv, remove = TRUE, flag = TRUE)
119+
client <- nanonext::recv_aio(reader_socket, mode = "string", cv = cv)
120+
121+
if (!the$sessions_enabled) {
122+
while (nanonext::wait(cv)) {
123+
if (!nanonext::unresolved(client)) {
124+
handle_message_from_client(client$data)
125+
client <- nanonext::recv_aio(reader_socket, mode = "string", cv = cv)
126+
}
127+
}
128+
return()
129+
}
114130

115131
the$server_socket <- nanonext::socket("poly")
116132
on.exit(nanonext::reap(the$server_socket), add = TRUE)
117133
nanonext::dial(the$server_socket, url = sprintf("%s%d", the$socket_url, 1L))
118-
119-
client <- nanonext::recv_aio(reader_socket, mode = "string", cv = cv)
120134
session <- nanonext::recv_aio(the$server_socket, mode = "string", cv = cv)
121135

122136
while (nanonext::wait(cv)) {
@@ -178,10 +192,11 @@ handle_message_from_client <- function(line) {
178192
} else if (data$method == "tools/call") {
179193
tool_name <- data$params$name
180194
if (
181-
# two tools provided by mcptools itself which must be executed in
182-
# the server rather than a session (#18)
183-
tool_name %in%
184-
c("list_r_sessions", "select_r_session") ||
195+
!the$sessions_enabled ||
196+
# two tools provided by mcptools itself which must be executed in
197+
# the server rather than a session (#18)
198+
tool_name %in% c("list_r_sessions", "select_r_session") ||
199+
# when session handling is disabled, never forward to sessions
185200
# with no sessions available, just execute tools in the server (#36)
186201
!nanonext::stat(the$server_socket, "pipes")
187202
) {

R/tools.R

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1-
set_server_tools <- function(x, x_arg = caller_arg(x), call = caller_env()) {
1+
set_server_tools <- function(
2+
x,
3+
session_tools = TRUE,
4+
x_arg = caller_arg(x),
5+
call = caller_env()
6+
) {
27
if (is.null(x)) {
3-
the$server_tools <- c(list(list_r_sessions_tool, select_r_session_tool))
4-
return()
8+
if (session_tools) {
9+
the$server_tools <- c(list(list_r_sessions_tool, select_r_session_tool))
10+
return()
11+
} else {
12+
cli::cli_abort("No tools selected to serve.", call = call)
13+
}
514
}
615

716
# evaluate eagerly so that caller arg is correct if `looks_like_r_file()`
@@ -22,7 +31,11 @@ set_server_tools <- function(x, x_arg = caller_arg(x), call = caller_env()) {
2231
)
2332
}
2433

25-
if (!is_list(x) || !all(vapply(x, inherits, logical(1), "ellmer::ToolDef"))) {
34+
if (!is.list(x)) {
35+
x <- list(x)
36+
}
37+
38+
if (!all(vapply(x, inherits, logical(1), "ellmer::ToolDef"))) {
2639
msg <-
2740
"{.arg {x_arg}} must be a list of tools created with {.fn ellmer::tool}
2841
or a .R file path that returns a list of ellmer tools when sourced."
@@ -39,19 +52,22 @@ set_server_tools <- function(x, x_arg = caller_arg(x), call = caller_env()) {
3952
)
4053
) {
4154
cli::cli_abort(
42-
"The tool names {.field list_r_sessions} and {.field select_r_session} are
55+
"The tool names {.field list_r_sessions} and {.field select_r_session} are
4356
reserved by {.pkg mcptools}.",
4457
call = call
4558
)
4659
}
4760

48-
the$server_tools <- c(
49-
x,
50-
list(
51-
list_r_sessions_tool,
52-
select_r_session_tool
61+
if (session_tools) {
62+
x <- c(
63+
x,
64+
list(
65+
list_r_sessions_tool,
66+
select_r_session_tool
67+
)
5368
)
54-
)
69+
}
70+
the$server_tools <- x
5571
}
5672

5773
looks_like_r_file <- function(x) {

man/server.Rd

Lines changed: 11 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/tools.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@
2727
# set_server_tools errors informatively
2828

2929
Code
30-
set_server_tools(tls$value[[1]])
30+
set_server_tools(123)
3131
Condition
3232
Error:
33-
! `tls$value[[1]]` must be a list of tools created with `ellmer::tool()` or a .R file path that returns a list of ellmer tools when sourced.
34-
i Did you mean to wrap `tls$value[[1]]` in `list()`?
33+
! `123` must be a list of tools created with `ellmer::tool()` or a .R file path that returns a list of ellmer tools when sourced.
3534

3635
---
3736

tests/testthat/test-tools.R

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ test_that("set_server_tools errors informatively", {
4545
local = TRUE
4646
)
4747

48-
# needs to be wrapped in `list()`
49-
expect_snapshot(set_server_tools(tls$value[[1]]), error = TRUE)
48+
# input must be a ToolDef or list of ToolDefs
49+
expect_snapshot(set_server_tools(123), error = TRUE)
50+
51+
# check can accept a single ToolDef
52+
expect_no_error(set_server_tools(tls$value[[1]]))
5053

5154
# select_r_session and list_r_sessions are reserved names
5255
tls$value[[1]]@name <- "select_r_session"

0 commit comments

Comments
 (0)