Skip to contents

Introduction

The Model Context Protocol (MCP) is a standardized way to connect AI assistants with external tools and data sources. The llmr package provides seamless integration with MCP through the mcpr package, enabling a powerful “lift and shift” approach to tool development.

This vignette demonstrates how to take the calculator tool from the “Get Started” guide and deploy it as a production-ready MCP server, while maintaining the same simple client interface.

What You’ll Learn

By the end of this guide, you’ll understand how to:

  • Transform inline tools into standalone MCP servers
  • Connect llmr agents to external MCP services
  • Leverage the “lift and shift” approach for production deployment
  • Scale your tool architecture from development to production

The Development-to-Production Journey

Development Phase: Inline Tools

In the “Get Started” vignette, we created a calculator tool directly within our agent:

# Development approach - tool embedded in agent
calculator <- new_tool(
  name = "calculator",
  description = "Performs basic arithmetic operations",
  input_schema = schema(
    properties = properties(
      operation = property_enum(
        "Operation",
        "Math operation to perform",
        values = c("add", "subtract", "multiply", "divide"),
        required = TRUE
      ),
      a = property_number("First number", "First operand", required = TRUE),
      b = property_number("Second number", "Second operand", required = TRUE)
    )
  ),
  handler = function(params) {
    result <- switch(
      params$operation,
      "add" = params$a + params$b,
      "subtract" = params$a - params$b,
      "multiply" = params$a * params$b,
      "divide" = params$a / params$b
    )
    response_text(result)
  }
)
agent <- add_tool(agent, calculator)

This approach is perfect for: - Rapid prototyping - Testing new functionality - Simple, single-use tools

Production Phase: MCP Servers

For production deployment, we can “lift and shift” the same tool logic into an MCP server:

# Production approach - tool as external MCP server
client <- new_client_io(command = "Rscript", args = "calculator-server.R")
agent <- register_mcp(agent, client)

This approach provides: - Scalability - Tools run as separate processes - Reusability - Multiple agents can share the same tools - Maintainability - Tools can be updated independently - Security - Tools run in isolated environments

Step 1: Understanding the MCP Server

Let’s transform our calculator tool into a standalone MCP server. Here’s what the server code looks like:

library(mcpr)

# Create the same calculator tool from the Get Started guide
calculator <- new_tool(
  name = "calculator",
  description = "Performs basic arithmetic operations",
  input_schema = schema(
    properties = properties(
      operation = property_enum(
        "Operation",
        "Math operation to perform",
        values = c("add", "subtract", "multiply", "divide"),
        required = TRUE
      ),
      a = property_number("First number", "First operand", required = TRUE),
      b = property_number("Second number", "Second operand", required = TRUE)
    )
  ),
  handler = function(params) {
    result <- switch(
      params$operation,
      "add" = params$a + params$b,
      "subtract" = params$a - params$b,
      "multiply" = params$a * params$b,
      "divide" = params$a / params$b
    )
    response_text(result)
  }
)

# Create an MCP server and add the calculator tool
mcp_server <- new_server(
  name = "Calculator Server",
  description = "A production-ready calculator service",
  version = "1.0.0"
)

# Add the tool to the server
mcp_server <- add_capability(mcp_server, calculator)

# Start the server (listening on stdin/stdout)
serve_io(mcp_server)

Key Insight: Notice how the tool definition is identical to the inline version. This is the power of the “lift and shift” approach - no code changes required!

The llmr package includes this exact server as a ready-to-use example:

Step 2: Create the MCP Client

Now let’s create an agent that connects to our MCP server:

library(llmr)
#> 
#> Attaching package: 'llmr'
#> The following object is masked from 'package:stats':
#> 
#>     step
library(mcpr)
#> 
#> Attaching package: 'mcpr'
#> The following object is masked from 'package:methods':
#> 
#>     initialize
#> The following object is masked from 'package:base':
#> 
#>     write
library(ellmer)

# Create an agent with ellmer chat object
agent <- new_agent("calculator", ellmer::chat_anthropic())
#> Using model = "claude-sonnet-4-20250514".

Step 3: Connect to the MCP Server

Instead of adding tools directly, we’ll connect to our external MCP server. The llmr package includes a ready-to-use calculator server:

# Get the path to the example calculator server included with llmr
server_path <- system.file("examples", "calculator-server.R", package = "llmr")

# Create an MCP client that connects to our calculator server
calculator_client <- new_client_io(
  command = "Rscript",
  args = server_path,
  name = "calculator"
)

# Register the MCP client with our agent
agent <- register_mcp(agent, calculator_client)

What’s happening here: - new_client_io() creates a client that launches our server as a subprocess - The server communicates via stdin/stdout (standard MCP protocol) - register_mcp() makes all server tools available to the agent

Step 4: Use the Agent (Same Interface!)

The beauty of this approach is that the client interface remains exactly the same:

# Same usage as the inline tool version
request(agent, "What is 25 multiplied by 4?")
#> Warning in read.client_io(x, timeout): Timeout waiting for client response
get_last_message(agent)
#> assistant: 25 multiplied by 4 equals 100.

# Complex calculations work the same way
request(agent, "I have 150 dollars, spend 47.50 on dinner and 23.75 on a book. How much is left?")
#> Warning in read.client_io(x, timeout): Timeout waiting for client response
#> Warning in read.client_io(x, timeout): Timeout waiting for client response
get_last_message(agent)
#> assistant: After spending $47.50 on dinner and $23.75 on a book, you have $78.75 left from your original $150.

The “Lift and Shift” Advantage

Development Workflow

  1. Start with inline tools for rapid prototyping
  2. Test and refine your tool logic
  3. Lift the tool definition to an MCP server
  4. Shift your agent to use the MCP client
  5. Deploy to production with no client code changes

Benefits of This Approach

🚀 Scalability
# Multiple agents can share the same calculator service
agent1 <- new_agent("financial_advisor", ellmer::chat_anthropic())
agent2 <- new_agent("math_tutor", ellmer::chat_anthropic())
agent3 <- new_agent("data_analyst", ellmer::chat_anthropic())

# All connect to the same MCP server
register_mcp(agent1, calculator_client)
register_mcp(agent2, calculator_client)
register_mcp(agent3, calculator_client)
🔄 Reusability
  • One calculator server serves multiple use cases
  • Tools become organizational assets, not agent-specific code
  • Easy to discover and share tools across teams
🛡️ Security & Isolation
  • Tools run in separate processes with controlled permissions
  • Server crashes don’t affect the main agent
  • Easy to implement resource limits and monitoring
🔧 Maintainability
  • Update tool logic without touching agent code
  • Version control tools independently
  • Deploy tool updates without agent restarts

Advanced MCP Patterns

Multiple MCP Services

You can connect to multiple MCP servers for different capabilities:

# Connect to different specialized services
calculator_client <- new_client_io(command = "Rscript", args = "calculator-server.R")
weather_client <- new_client_io(command = "Rscript", args = "weather-server.R")
database_client <- new_client_io(command = "Rscript", args = "database-server.R")

# Register all services with the agent
agent <- register_mcp(agent, calculator_client)
agent <- register_mcp(agent, weather_client)  
agent <- register_mcp(agent, database_client)

Hybrid Approach

You can mix inline tools with MCP services:

# Quick inline tool for simple tasks
simple_tool <- new_tool(name = "timestamp", ...)
agent <- add_tool(agent, simple_tool)

# External MCP service for complex operations
complex_client <- new_client_io(...)
agent <- register_mcp(agent, complex_client)

Production Deployment Considerations

Server Management

  • Use process managers (systemd, Docker, etc.) for MCP servers
  • Implement health checks and automatic restarts
  • Monitor server performance and resource usage

Security

  • Run MCP servers with minimal required permissions
  • Use network isolation where appropriate
  • Implement authentication for sensitive tools

Scaling

  • Deploy MCP servers on separate machines for heavy workloads
  • Use load balancers for high-availability tool services
  • Implement caching for frequently-used operations

Migration Strategy

Phase 1: Development

# Start with inline tools
agent <- add_tool(agent, my_tool)

Phase 2: Testing

# Test MCP version alongside inline version
agent <- add_tool(agent, my_tool)  # Keep original
agent <- register_mcp(agent, mcp_client)  # Add MCP version

Phase 3: Production

# Remove inline tool, use only MCP
agent <- register_mcp(agent, mcp_client)

Summary

The MCP integration in llmr provides a seamless path from development to production:

  1. Develop quickly with inline tools
  2. Lift tool definitions to MCP servers without code changes
  3. Shift agents to use MCP clients with minimal changes
  4. Scale confidently with production-ready architecture

This approach gives you the best of both worlds: the speed of inline development and the robustness of distributed production systems.

Key Takeaways

  • Same tool logic works in both inline and MCP modes
  • Client interface remains unchanged during migration
  • MCP provides production benefits without development complexity
  • Gradual migration is possible and recommended

The Model Context Protocol integration makes llmr a powerful platform for building scalable AI agent systems that can grow from prototype to production seamlessly.