|
1 | 1 | # RubyLLM::MCP |
2 | 2 |
|
3 | | -Aiming to make using MCP with RubyLLM as easy as possible. |
| 3 | +Aiming to make using MCPs with RubyLLM as easy as possible. |
4 | 4 |
|
5 | 5 | This project is a Ruby client for the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), designed to work seamlessly with [RubyLLM](https://github.com/crmne/ruby_llm). This gem enables Ruby applications to connect to MCP servers and use their tools, resources and prompts as part of LLM conversations. |
6 | 6 |
|
7 | 7 | **Note:** This project is still under development and the API is subject to change. |
8 | 8 |
|
9 | 9 | ## Features |
10 | 10 |
|
11 | | -- 🔌 **Multiple Transport Types**: Support for SSE (Server-Sent Events), Streamable HTTP, and stdio transports |
| 11 | +- 🔌 **Multiple Transport Types**: Streamable HTTP, and STDIO and legacy SSE transports |
12 | 12 | - 🛠️ **Tool Integration**: Automatically converts MCP tools into RubyLLM-compatible tools |
13 | 13 | - 📄 **Resource Management**: Access and include MCP resources (files, data) and resource templates in conversations |
14 | 14 | - 🎯 **Prompt Integration**: Use predefined MCP prompts with arguments for consistent interactions |
@@ -123,21 +123,19 @@ puts result # 3 |
123 | 123 | # If the human in the loop returns false, the tool call will be cancelled |
124 | 124 | result = tool.execute(a: 2, b: 2) |
125 | 125 | puts result # Tool execution error: Tool call was cancelled by the client |
126 | | -``` |
127 | 126 |
|
128 | 127 | tool = client.tool("add") |
129 | 128 | result = tool.execute(a: 1, b: 2) |
130 | 129 | puts result |
131 | | - |
132 | | -```` |
| 130 | +``` |
133 | 131 |
|
134 | 132 | ### Support Complex Parameters |
135 | 133 |
|
136 | 134 | If you want to support complex parameters, like an array of objects it currently requires a patch to RubyLLM itself. This is planned to be temporary until the RubyLLM is updated. |
137 | 135 |
|
138 | 136 | ```ruby |
139 | 137 | RubyLLM::MCP.support_complex_parameters! |
140 | | -```` |
| 138 | +``` |
141 | 139 |
|
142 | 140 | ### Streaming Responses with Tool Calls |
143 | 141 |
|
@@ -338,6 +336,69 @@ chat.with_prompt(prompt, arguments: { name: "Alice" }) |
338 | 336 | response = chat.ask_prompt(prompt, arguments: { name: "Alice" }) |
339 | 337 | ``` |
340 | 338 |
|
| 339 | +## Rails Integration |
| 340 | + |
| 341 | +RubyLLM MCP provides seamless Rails integration through a Railtie and generator system. |
| 342 | + |
| 343 | +### Setup |
| 344 | + |
| 345 | +Generate the configuration files: |
| 346 | + |
| 347 | +```bash |
| 348 | +rails generate ruby_llm:mcp:install |
| 349 | +``` |
| 350 | + |
| 351 | +This creates: |
| 352 | + |
| 353 | +- `config/initializers/ruby_llm_mcp.rb` - Main configuration |
| 354 | +- `config/mcps.yml` - MCP servers configuration |
| 355 | + |
| 356 | +### MCP Server Configuration |
| 357 | + |
| 358 | +Configure your MCP servers in `config/mcps.yml`: |
| 359 | + |
| 360 | +```yaml |
| 361 | +mcp_servers: |
| 362 | + filesystem: |
| 363 | + transport_type: stdio |
| 364 | + command: npx |
| 365 | + args: |
| 366 | + - "@modelcontextprotocol/server-filesystem" |
| 367 | + - "<%= Rails.root %>" |
| 368 | + env: {} |
| 369 | + with_prefix: true |
| 370 | + |
| 371 | + api_server: |
| 372 | + transport_type: sse |
| 373 | + url: "https://api.example.com/mcp/sse" |
| 374 | + headers: |
| 375 | + Authorization: "Bearer <%= ENV['API_TOKEN'] %>" |
| 376 | +``` |
| 377 | +
|
| 378 | +### Automatic Client Management |
| 379 | +
|
| 380 | +With `launch_control: :automatic`, Rails will: |
| 381 | + |
| 382 | +- Start all configured MCP clients when the application initializes |
| 383 | +- Gracefully shut down clients when the application exits |
| 384 | +- Handle client lifecycle automatically |
| 385 | + |
| 386 | +However, it's very command to due to the performace of LLM calls that are made in the background. |
| 387 | + |
| 388 | +For this, we recommend using `launch_control: :manual` and use `establish_connection` method to manage the client lifecycle manually inside your background jobs. It will provide you active connections to the MCP servers, and take care of closing them when the job is done. |
| 389 | + |
| 390 | +```ruby |
| 391 | +RubyLLM::MCP.establish_connection do |clients| |
| 392 | + chat = RubyLLM.chat(model: "gpt-4") |
| 393 | + chat.with_tools(*clients.tools) |
| 394 | +
|
| 395 | + response = chat.ask("Hello, world!") |
| 396 | + puts response |
| 397 | +end |
| 398 | +``` |
| 399 | + |
| 400 | +You can also avoid this completely manually start and stop the clients if you so choose. |
| 401 | + |
341 | 402 | ## Client Lifecycle Management |
342 | 403 |
|
343 | 404 | You can manage the MCP client connection lifecycle: |
@@ -576,6 +637,150 @@ client = RubyLLM::MCP.client( |
576 | 637 | ) |
577 | 638 | ``` |
578 | 639 |
|
| 640 | +## Creating Custom Transports |
| 641 | + |
| 642 | +Part of the MCP specification outlines that custom transports can be used for some MCP servers. Out of the box, RubyLLM::MCP supports Streamable HTTP transports, STDIO and the legacy SSE transport. |
| 643 | + |
| 644 | +You can create custom transport implementations to support additional communication protocols or specialized connection methods. |
| 645 | + |
| 646 | +### Transport Registration |
| 647 | + |
| 648 | +Register your custom transport with the transport factory: |
| 649 | + |
| 650 | +```ruby |
| 651 | +# Define your custom transport class |
| 652 | +class MyCustomTransport |
| 653 | + # Implementation details... |
| 654 | +end |
| 655 | +
|
| 656 | +# Register it with the factory |
| 657 | +RubyLLM::MCP::Transport.register_transport(:my_custom, MyCustomTransport) |
| 658 | +
|
| 659 | +# Now you can use it |
| 660 | +client = RubyLLM::MCP.client( |
| 661 | + name: "custom-server", |
| 662 | + transport_type: :my_custom, |
| 663 | + config: { |
| 664 | + # Your custom configuration |
| 665 | + } |
| 666 | +) |
| 667 | +``` |
| 668 | + |
| 669 | +### Required Interface |
| 670 | + |
| 671 | +All transport implementations must implement the following interface: |
| 672 | + |
| 673 | +```ruby |
| 674 | +class MyCustomTransport |
| 675 | + # Initialize the transport |
| 676 | + def initialize(coordinator:, **config) |
| 677 | + @coordinator = coordinator # Uses for communication between the client and the MCP server |
| 678 | + @config = config # Transport-specific configuration |
| 679 | + end |
| 680 | +
|
| 681 | + # Send a request and optionally wait for response |
| 682 | + # Returns a RubyLLM::MCP::Result object |
| 683 | + # body: the request body |
| 684 | + # add_id: true will add an id to the request |
| 685 | + # wait_for_response: true will wait for a response from the MCP server |
| 686 | + # Returns a RubyLLM::MCP::Result object |
| 687 | + def request(body, add_id: true, wait_for_response: true) |
| 688 | + # Implementation: send request and return result |
| 689 | + data = some_method_to_send_request_and_get_result(body) |
| 690 | + # Use Result object to make working with the protocol easier |
| 691 | + result = RubyLLM::MCP::Result.new(data) |
| 692 | +
|
| 693 | + # Call the coordinator to process the result |
| 694 | + @coordinator.process_result(result) |
| 695 | + return if result.nil? # Some results are related to notifications and should not be returned to the client, but processed by the coordinator instead |
| 696 | +
|
| 697 | + # Return the result |
| 698 | + result |
| 699 | + end |
| 700 | +
|
| 701 | + # Check if transport is alive/connected |
| 702 | + def alive? |
| 703 | + # Implementation: return true if connected |
| 704 | + end |
| 705 | +
|
| 706 | + # Start the transport connection |
| 707 | + def start |
| 708 | + # Implementation: establish connection |
| 709 | + end |
| 710 | +
|
| 711 | + # Close the transport connection |
| 712 | + def close |
| 713 | + # Implementation: cleanup and close connection |
| 714 | + end |
| 715 | +
|
| 716 | + # Set the MCP protocol version, used in some transports to identify the agreed upon protocol version |
| 717 | + def set_protocol_version(version) |
| 718 | + @protocol_version = version |
| 719 | + end |
| 720 | +end |
| 721 | +``` |
| 722 | + |
| 723 | +### The Result Object |
| 724 | + |
| 725 | +The `RubyLLM::MCP::Result` class wraps MCP responses and provides convenient methods: |
| 726 | + |
| 727 | +```ruby |
| 728 | +result = transport.request(body) |
| 729 | +
|
| 730 | +# Core properties |
| 731 | +result.id # Request ID |
| 732 | +result.method # Request method |
| 733 | +result.result # Result data (hash) |
| 734 | +result.params # Request parameters |
| 735 | +result.error # Error data (hash) |
| 736 | +result.session_id # Session ID (if applicable) |
| 737 | +
|
| 738 | +# Type checking |
| 739 | +result.success? # Has result data |
| 740 | +result.error? # Has error data |
| 741 | +result.notification? # Is a notification |
| 742 | +result.request? # Is a request |
| 743 | +result.response? # Is a response |
| 744 | +
|
| 745 | +# Specialized methods |
| 746 | +result.tool_success? # Successful tool execution |
| 747 | +result.execution_error? # Tool execution failed |
| 748 | +result.matching_id?(id) # Matches request ID |
| 749 | +result.next_cursor? # Has pagination cursor |
| 750 | +
|
| 751 | +# Error handling |
| 752 | +result.raise_error! # Raise exception if error |
| 753 | +result.to_error # Convert to Error object |
| 754 | +
|
| 755 | +# Notifications |
| 756 | +result.notification # Get notification object |
| 757 | +``` |
| 758 | + |
| 759 | +### Error Handling |
| 760 | + |
| 761 | +Custom transports should handle errors appropriately. If request fails, you should raise a `RubyLLM::MCP::Errors::TransportError` exception. If the request times out, you should raise a `RubyLLM::MCP::Errors::TimeoutError` exception. This will ensure that a cancellation notification is sent to the MCP server correctly. |
| 762 | + |
| 763 | +```ruby |
| 764 | +def request(body, add_id: true, wait_for_response: true) |
| 765 | + begin |
| 766 | + # Send request |
| 767 | + send_request(body) |
| 768 | + rescue SomeConnectionError => e |
| 769 | + # Convert to MCP transport error |
| 770 | + raise RubyLLM::MCP::Errors::TransportError.new( |
| 771 | + message: "Connection failed: #{e.message}", |
| 772 | + error: e |
| 773 | + ) |
| 774 | + rescue Timeout::Error => e |
| 775 | + # Convert to MCP timeout error |
| 776 | + raise RubyLLM::MCP::Errors::TimeoutError.new( |
| 777 | + message: "Request timeout after #{@request_timeout}ms", |
| 778 | + request_id: body["id"] |
| 779 | + ) |
| 780 | + end |
| 781 | +end |
| 782 | +``` |
| 783 | + |
579 | 784 | ## RubyLLM::MCP and Client Configuration Options |
580 | 785 |
|
581 | 786 | MCP comes with some common configuration options that can be set on the client. |
|
0 commit comments