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
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
^pkgdown$
^examples$
^\.github$
^\.claude$
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.claude/
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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)
Expand Down
215 changes: 215 additions & 0 deletions R/ellmer-to-mcpr.R
Original file line number Diff line number Diff line change
@@ -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
)
}
}
12 changes: 11 additions & 1 deletion R/mcp.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -284,6 +293,7 @@ add_capability.tool <- function(mcp, capability) {
invisible(mcp)
}


#' @export
#' @method add_capability resource
add_capability.resource <- function(mcp, capability) {
Expand Down
42 changes: 22 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,5 @@ reference:
Functions for integrating with the Ellmer package
contents:
- register_mcpr_tools
- ellmer_to_mcpr_tool

34 changes: 18 additions & 16 deletions docs/index.html

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

2 changes: 1 addition & 1 deletion docs/pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion examples/ellmer/client.R
Original file line number Diff line number Diff line change
Expand Up @@ -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?",
)
Loading