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 @@

Client

ellmer integration

-

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"
-)
+# Start the server (listening on stdin/stdout) +serve_io(mcp)

Using mcpr diff --git a/docs/pkgdown.yml b/docs/pkgdown.yml index b537ab5..41e41fd 100644 --- a/docs/pkgdown.yml +++ b/docs/pkgdown.yml @@ -6,7 +6,7 @@ articles: client-usage: client-usage.html get-started: get-started.html practical-examples: practical-examples.html -last_built: 2025-06-16T08:31Z +last_built: 2025-06-19T16:39Z urls: reference: https://mcpr.opifex.org/reference article: https://mcpr.opifex.org/articles diff --git a/examples/ellmer/client.R b/examples/ellmer/client.R index b8d9ff7..b913aa2 100644 --- a/examples/ellmer/client.R +++ b/examples/ellmer/client.R @@ -31,5 +31,5 @@ chat <- register_mcpr_tools(chat, client) ## Try using the tools in a chat chat$chat( - "Subtract 2 from 44" + "What is the current time in the US/Eastern timezone?", ) diff --git a/examples/ellmer/server.R b/examples/ellmer/server.R index 764e75c..f69416f 100644 --- a/examples/ellmer/server.R +++ b/examples/ellmer/server.R @@ -30,6 +30,17 @@ calculator <- new_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 an MCP server and add the calculator tool mcp <- new_server( name = "R Calculator Server", @@ -38,6 +49,7 @@ mcp <- new_server( ) mcp <- add_capability(mcp, calculator) +mcp <- add_capability(mcp, current_time) # Start the server (listening on stdin/stdout) serve_io(mcp) diff --git a/man/convert_ellmer_type_to_mcpr_property.Rd b/man/convert_ellmer_type_to_mcpr_property.Rd new file mode 100644 index 0000000..9b5089b --- /dev/null +++ b/man/convert_ellmer_type_to_mcpr_property.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ellmer-to-mcpr.R +\name{convert_ellmer_type_to_mcpr_property} +\alias{convert_ellmer_type_to_mcpr_property} +\title{Convert a single ellmer type to an mcpr property} +\usage{ +convert_ellmer_type_to_mcpr_property(ellmer_type, prop_name) +} +\arguments{ +\item{ellmer_type}{An ellmer Type object (TypeBasic, TypeArray, etc.)} + +\item{prop_name}{The name of the property (for better error messages)} +} +\value{ +An mcpr property object +} +\description{ +Convert a single ellmer type to an mcpr property +} +\keyword{internal} diff --git a/man/convert_ellmer_types_to_mcpr.Rd b/man/convert_ellmer_types_to_mcpr.Rd new file mode 100644 index 0000000..cb11725 --- /dev/null +++ b/man/convert_ellmer_types_to_mcpr.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ellmer-to-mcpr.R +\name{convert_ellmer_types_to_mcpr} +\alias{convert_ellmer_types_to_mcpr} +\title{Convert ellmer TypeObject to mcpr schema} +\usage{ +convert_ellmer_types_to_mcpr(type_object) +} +\arguments{ +\item{type_object}{An ellmer TypeObject containing the function arguments} +} +\value{ +An mcpr schema object +} +\description{ +Convert ellmer TypeObject to mcpr schema +} +\keyword{internal} diff --git a/man/create_ellmer_handler.Rd b/man/create_ellmer_handler.Rd index 881f91a..b449ba2 100644 --- a/man/create_ellmer_handler.Rd +++ b/man/create_ellmer_handler.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ellmer.R +% Please edit documentation in R/mcpr-to-ellmer.R \name{create_ellmer_handler} \alias{create_ellmer_handler} \title{Create an ellmer handler function for an MCP tool} diff --git a/man/create_ellmer_type.Rd b/man/create_ellmer_type.Rd index 2539ca2..7393e97 100644 --- a/man/create_ellmer_type.Rd +++ b/man/create_ellmer_type.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ellmer.R +% Please edit documentation in R/mcpr-to-ellmer.R \name{create_ellmer_type} \alias{create_ellmer_type} \title{Create an ellmer type function for a specific MCP property} diff --git a/man/create_ellmer_types.Rd b/man/create_ellmer_types.Rd index 3db86d2..a797aaa 100644 --- a/man/create_ellmer_types.Rd +++ b/man/create_ellmer_types.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ellmer.R +% Please edit documentation in R/mcpr-to-ellmer.R \name{create_ellmer_types} \alias{create_ellmer_types} \title{Create ellmer type functions from MCP schema properties} diff --git a/man/ellmer_to_mcpr_tool.Rd b/man/ellmer_to_mcpr_tool.Rd new file mode 100644 index 0000000..b3814d2 --- /dev/null +++ b/man/ellmer_to_mcpr_tool.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ellmer-to-mcpr.R +\name{ellmer_to_mcpr_tool} +\alias{ellmer_to_mcpr_tool} +\title{Convert ellmer tools to mcpr tools} +\usage{ +ellmer_to_mcpr_tool(ellmer_tool) +} +\arguments{ +\item{ellmer_tool}{An ellmer ToolDef object created with \code{ellmer::tool()}} +} +\value{ +An mcpr tool object compatible with \code{new_tool()} +} +\description{ +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. +} +\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{ +\code{\link[=new_tool]{new_tool()}}, \code{\link[ellmer:tool]{ellmer::tool()}}, \code{\link[=add_capability]{add_capability()}} +} diff --git a/man/extract_mcp_result.Rd b/man/extract_mcp_result.Rd index 4736989..6a5650e 100644 --- a/man/extract_mcp_result.Rd +++ b/man/extract_mcp_result.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ellmer.R +% Please edit documentation in R/mcpr-to-ellmer.R \name{extract_mcp_result} \alias{extract_mcp_result} \title{Extract and convert an MCP result to ellmer-compatible format} diff --git a/man/mcpr_clients_to_ellmer_tools.Rd b/man/mcpr_clients_to_ellmer_tools.Rd index 8331db5..7a65022 100644 --- a/man/mcpr_clients_to_ellmer_tools.Rd +++ b/man/mcpr_clients_to_ellmer_tools.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ellmer.R +% Please edit documentation in R/mcpr-to-ellmer.R \name{mcpr_clients_to_ellmer_tools} \alias{mcpr_clients_to_ellmer_tools} \title{Convert multiple MCPR clients to ellmer tools} diff --git a/man/mcpr_to_ellmer_tools.Rd b/man/mcpr_to_ellmer_tools.Rd index 65b4492..5821229 100644 --- a/man/mcpr_to_ellmer_tools.Rd +++ b/man/mcpr_to_ellmer_tools.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ellmer.R +% Please edit documentation in R/mcpr-to-ellmer.R \name{mcpr_to_ellmer_tools} \alias{mcpr_to_ellmer_tools} \title{Convert MCPR tools to ellmer tools} diff --git a/man/register_mcpr_tools.Rd b/man/register_mcpr_tools.Rd index 5de8d90..9105da1 100644 --- a/man/register_mcpr_tools.Rd +++ b/man/register_mcpr_tools.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ellmer.R +% Please edit documentation in R/mcpr-to-ellmer.R \name{register_mcpr_tools} \alias{register_mcpr_tools} \title{Register MCPR tools with an ellmer chat}