diff --git a/.Rbuildignore b/.Rbuildignore index c3812a5..4e653dc 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -6,3 +6,4 @@ ^pkgdown$ ^examples$ ^\.github$ +^\.claude$ diff --git a/.gitignore b/.gitignore index e69de29..4c5f206 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +.claude/ diff --git a/NAMESPACE b/NAMESPACE index e5bd937..ba3ebf7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +S3method(add_capability,"ellmer::ToolDef") S3method(add_capability,prompt) S3method(add_capability,resource) S3method(add_capability,tool) @@ -25,6 +26,7 @@ S3method(tools_list,server) S3method(write,client_http) S3method(write,client_io) export(add_capability) +export(ellmer_to_mcpr_tool) export(get_name) export(initialize) export(new_client_io) diff --git a/R/ellmer-to-mcpr.R b/R/ellmer-to-mcpr.R new file mode 100644 index 0000000..a49b759 --- /dev/null +++ b/R/ellmer-to-mcpr.R @@ -0,0 +1,215 @@ +#' Convert ellmer tools to mcpr tools +#' +#' This function converts tools from the ellmer package to a format compatible +#' with the mcpr package. It takes an ellmer ToolDef object and creates an +#' mcpr tool object with the appropriate input schema and handler function. +#' +#' @param ellmer_tool An ellmer ToolDef object created with `ellmer::tool()` +#' @return An mcpr tool object compatible with `new_tool()` +#' @export +#' +#' @examples +#' \dontrun{ +#' # Create an ellmer tool +#' ellmer_rnorm <- ellmer::tool( +#' rnorm, +#' "Generate random normal numbers", +#' n = ellmer::type_integer("Number of observations"), +#' mean = ellmer::type_number("Mean value", required = FALSE), +#' sd = ellmer::type_number("Standard deviation", required = FALSE) +#' ) +#' +#' # Convert to mcpr format +#' mcpr_tool <- ellmer_to_mcpr_tool(ellmer_rnorm) +#' +#' # Add to an mcpr server +#' server <- new_server("MyServer", "Test server", "1.0.0") +#' add_capability(server, mcpr_tool) +#' } +#' +#' @seealso [new_tool()], [ellmer::tool()], [add_capability()] +#' @export +ellmer_to_mcpr_tool <- function(ellmer_tool) { + # Validate input + if (!inherits(ellmer_tool, "ellmer::ToolDef")) { + stop("ellmer_tool must be a ToolDef object created with ellmer::tool()") + } + + # Extract basic information + name <- ellmer_tool@name + description <- ellmer_tool@description + fun <- ellmer_tool@fun + convert <- ellmer_tool@convert + + # Convert ellmer type schema to mcpr input schema + input_schema <- convert_ellmer_types_to_mcpr(ellmer_tool@arguments) + + # Create handler function that wraps the original ellmer function + handler <- function(arguments) { + # Convert arguments if needed (similar to ellmer's behavior) + if (convert) { + # Apply any argument conversion logic here if needed + processed_args <- arguments + } else { + processed_args <- arguments + } + + # Call the original function + result <- tryCatch( + { + do.call(fun, processed_args) + }, + error = function(e) { + stop("Tool execution error: ", e$message) + } + ) + + response( + response_text( + yyjsonr::write_json_str(result, list(auto_unbox = TRUE)) + ) + ) + } + + # Create the mcpr tool + new_tool( + name = name, + description = description, + input_schema = input_schema, + handler = handler + ) +} + +#' Convert ellmer TypeObject to mcpr schema +#' +#' @param type_object An ellmer TypeObject containing the function arguments +#' @return An mcpr schema object +#' @keywords internal +convert_ellmer_types_to_mcpr <- function(type_object) { + if (!inherits(type_object, "ellmer::TypeObject")) { + stop("Expected TypeObject from ellmer") + } + + # Convert each property + mcpr_properties <- list() + for (prop_name in names(type_object@properties)) { + ellmer_type <- type_object@properties[[prop_name]] + mcpr_prop <- convert_ellmer_type_to_mcpr_property(ellmer_type, prop_name) + mcpr_properties[[prop_name]] <- mcpr_prop + } + + # Create the schema + schema( + properties = do.call(properties, mcpr_properties), + additional_properties = type_object@additional_properties + ) +} + +#' Convert a single ellmer type to an mcpr property +#' +#' @param ellmer_type An ellmer Type object (TypeBasic, TypeArray, etc.) +#' @param prop_name The name of the property (for better error messages) +#' @return An mcpr property object +#' @keywords internal +convert_ellmer_type_to_mcpr_property <- function(ellmer_type, prop_name) { + description <- ellmer_type@description %||% prop_name + required <- ellmer_type@required + + if (inherits(ellmer_type, "ellmer::TypeBasic")) { + type <- ellmer_type@type + + switch( + type, + "boolean" = property_boolean( + title = prop_name, + description = description, + required = required + ), + "integer" = property_number( + title = prop_name, + description = description, + required = required, + integer = TRUE + ), + "number" = property_number( + title = prop_name, + description = description, + required = required, + integer = FALSE + ), + "string" = property_string( + title = prop_name, + description = description, + required = required + ), + # Default to string for unknown basic types + property_string( + title = prop_name, + description = description, + required = required + ) + ) + } else if (inherits(ellmer_type, "ellmer::TypeEnum")) { + property_enum( + title = prop_name, + description = description, + values = ellmer_type@values, + required = required + ) + } else if (inherits(ellmer_type, "ellmer::TypeArray")) { + # Convert the items type + items_prop <- convert_ellmer_type_to_mcpr_property( + ellmer_type@items, + paste0(prop_name, "_item") + ) + + property_array( + title = prop_name, + description = description, + items = items_prop, + required = required + ) + } else if (inherits(ellmer_type, "ellmer::TypeObject")) { + # Convert nested object properties + nested_properties <- list() + for (nested_name in names(ellmer_type@properties)) { + nested_type <- ellmer_type@properties[[nested_name]] + nested_prop <- convert_ellmer_type_to_mcpr_property( + nested_type, + nested_name + ) + nested_properties[[nested_name]] <- nested_prop + } + + property_object( + title = prop_name, + description = description, + properties = nested_properties, + required = required, + additional_properties = ellmer_type@additional_properties + ) + } else if (inherits(ellmer_type, "ellmer::TypeJsonSchema")) { + # For custom JSON schemas, we'll need to interpret them + # This is a complex case that would need custom handling + warning( + "TypeJsonSchema conversion not fully implemented, converting to string" + ) + property_string( + title = prop_name, + description = description, + required = required + ) + } else { + # Unknown type, default to string + warning(paste( + "Unknown ellmer type:", + class(ellmer_type)[1], + "- converting to string" + )) + property_string( + title = prop_name, + description = description, + required = required + ) + } +} diff --git a/R/mcp.R b/R/mcp.R index 49fd9a9..2352847 100644 --- a/R/mcp.R +++ b/R/mcp.R @@ -274,8 +274,17 @@ new_capability <- function( #' #' @return The MCP object with the capability added #' @export -add_capability <- function(mcp, capability) +add_capability <- function(mcp, capability) { UseMethod("add_capability", capability) +} + +#' @export +#' @method add_capability ellmer::ToolDef +`add_capability.ellmer::ToolDef` <- function(mcp, capability) { + capability <- ellmer_to_mcpr_tool(capability) + mcp$tools[[capability$name]] <- capability + invisible(mcp) +} #' @export #' @method add_capability tool @@ -284,6 +293,7 @@ add_capability.tool <- function(mcp, capability) { invisible(mcp) } + #' @export #' @method add_capability resource add_capability.resource <- function(mcp, capability) { diff --git a/README.md b/README.md index de84da7..1a4971b 100644 --- a/README.md +++ b/README.md @@ -154,33 +154,35 @@ print(resource_content) ## ellmer integration -Use the `register_mcpr_tools` function to convert MCPR tools to ellmer tools and -register them with an ellmer chat session. +Now supports ellmer tools, you can use ellmer's or mcpr's tools, +interchangeably. -Note that this integration is not standard since ellmer does not currently support -MCPs. Here we re-recreate the tools obtained via `tools_list` as ellmer tools which -themselves call `tools_call` to execute the tool. -The tools are currently not namespaced. +See the example below, taken from the +[ellmer documentation](https://ellmer.tidyverse.org/articles/tool-calling.html#defining-a-tool-function) ```r -# Create an MCPR client connected to the calculator server -client <- new_client_io( - "Rscript", - "path/to/server.R", - name = "calculator", - version = "1.0.0" +# create an ellmer tool +current_time <- ellmer::tool( + \(tz = "UTC") { + format(Sys.time(), tz = tz, usetz = TRUE) + }, + "Gets the current time in the given time zone.", + tz = ellmer::type_string( + "The time zone to get the current time in. Defaults to `\"UTC\"`.", + required = FALSE + ) ) -# Create a Claude chat session with ellmer -chat <- ellmer::chat_anthropic() +mcp <- new_server( + name = "R Calculator Server", + description = "A simple calculator server implemented in R", + version = "1.0.0" +) -## Convert MCPR tools to ellmer tools and register them with the chat -chat <- register_mcpr_tools(chat, client) +# register ellmer tool with mcpr server +mcp <- add_capability(mcp, current_time) -## Try using the tools in a chat -chat$chat( - "Subtract 2 from 44" -) +serve_io(mcp) ``` ## Using mcpr diff --git a/_pkgdown.yml b/_pkgdown.yml index 2d968ca..c503d9d 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -68,4 +68,5 @@ reference: Functions for integrating with the Ellmer package contents: - register_mcpr_tools + - ellmer_to_mcpr_tool diff --git a/docs/index.html b/docs/index.html index fe7689d..7e1c1bd 100644 --- a/docs/index.html +++ b/docs/index.html @@ -201,27 +201,29 @@
Use the register_mcpr_tools function to convert MCPR tools to ellmer tools and register them with an ellmer chat session.
Note that this integration is not standard since ellmer does not currently support MCPs. Here we re-recreate the tools obtained via tools_list as ellmer tools which themselves call tools_call to execute the tool. The tools are currently not namespaced.
Now supports ellmer tools, you can use ellmer’s or mcpr’s tools, interchangeably.
-# Create an MCPR client connected to the calculator server
-client <- new_client_io(
- "Rscript",
- "path/to/server.R",
- name = "calculator",
- version = "1.0.0"
+current_time <- ellmer::tool(
+ \(tz = "UTC") {
+ format(Sys.time(), tz = tz, usetz = TRUE)
+ },
+ "Gets the current time in the given time zone.",
+ tz = ellmer::type_string(
+ "The time zone to get the current time in. Defaults to `\"UTC\"`.",
+ required = FALSE
+ )
)
-# Create a Claude chat session with ellmer
-chat <- ellmer::chat_anthropic()
+mcp <- new_server(
+ name = "R Calculator Server",
+ description = "A simple calculator server implemented in R",
+ version = "1.0.0"
+)
-## Convert MCPR tools to ellmer tools and register them with the chat
-chat <- register_mcpr_tools(chat, client)
+mcp <- add_capability(mcp, current_time)
-## Try using the tools in a chat
-chat$chat(
- "Subtract 2 from 44"
-)