ruby-mcp-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +218 -0
- data/lib/mcp_client/client.rb +73 -0
- data/lib/mcp_client/errors.rb +27 -0
- data/lib/mcp_client/server_base.rb +31 -0
- data/lib/mcp_client/server_factory.rb +24 -0
- data/lib/mcp_client/server_sse.rb +413 -0
- data/lib/mcp_client/server_stdio.rb +198 -0
- data/lib/mcp_client/tool.rb +50 -0
- data/lib/mcp_client/version.rb +6 -0
- data/lib/mcp_client.rb +47 -0
- metadata +112 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0fd821eaf2b01a8628e7d54f2b39826b3d980d9e0d183ac0d81d5fb2ae135a36
|
4
|
+
data.tar.gz: 0d2afee24c1ebbb49f597056b9257546256e3152a4d789b7bc2789b3afc40a4f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b21d59cb475b4202442658dd81b3ee394f317b767fb0a1bacba3c575dc96ac4004a0849a4fc59b2d7481c75ae55181cca21f11a5ebbe2d15179eeb2a30aba242
|
7
|
+
data.tar.gz: 6a06a38dd0066331efcd1ec3a1eb5d6178dd12091a7f217199a314c891aef4b0100d3d7da9d12e3f69ac01da42d3eee60135f68ac43bf79cebf14f9fd43308e4
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Szymon Kurcab
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,218 @@
|
|
1
|
+
# ruby-mcp-client
|
2
|
+
|
3
|
+
This gem provides a Ruby client for the Model Context Protocol (MCP),
|
4
|
+
enabling integration with external tools and services via a standardized protocol.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'ruby-mcp-client'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
```bash
|
17
|
+
bundle install
|
18
|
+
```
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
```bash
|
23
|
+
gem install ruby-mcp-client
|
24
|
+
```
|
25
|
+
|
26
|
+
## Overview
|
27
|
+
|
28
|
+
MCP enables AI assistants and other services to discover and invoke external tools
|
29
|
+
via different transport mechanisms:
|
30
|
+
|
31
|
+
- **Standard I/O**: Local processes implementing the MCP protocol
|
32
|
+
- **Server-Sent Events (SSE)**: Remote MCP servers over HTTP
|
33
|
+
|
34
|
+
The core client resides in `MCPClient::Client` and provides helper methods for integrating
|
35
|
+
with popular AI services with built-in conversions:
|
36
|
+
|
37
|
+
- `to_openai_tools()` - Formats tools for OpenAI API
|
38
|
+
- `to_anthropic_tools()` - Formats tools for Anthropic Claude API
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
### Basic Client Usage
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
require 'mcp_client'
|
46
|
+
|
47
|
+
client = MCPClient.create_client(
|
48
|
+
mcp_server_configs: [
|
49
|
+
# Local stdio server
|
50
|
+
MCPClient.stdio_config(command: 'npx -y @modelcontextprotocol/server-filesystem /home/user'),
|
51
|
+
# Remote HTTP SSE server
|
52
|
+
MCPClient.sse_config(
|
53
|
+
base_url: 'https://api.example.com/sse',
|
54
|
+
headers: { 'Authorization' => 'Bearer YOUR_TOKEN' },
|
55
|
+
read_timeout: 30 # Optional timeout in seconds (default: 30)
|
56
|
+
)
|
57
|
+
]
|
58
|
+
)
|
59
|
+
|
60
|
+
# List available tools
|
61
|
+
tools = client.list_tools
|
62
|
+
|
63
|
+
# Call a specific tool by name
|
64
|
+
result = client.call_tool('example_tool', { param1: 'value1', param2: 42 })
|
65
|
+
|
66
|
+
# Format tools for specific AI services
|
67
|
+
openai_tools = client.to_openai_tools
|
68
|
+
anthropic_tools = client.to_anthropic_tools
|
69
|
+
|
70
|
+
# Clean up connections
|
71
|
+
client.cleanup
|
72
|
+
```
|
73
|
+
|
74
|
+
### Integration Examples
|
75
|
+
|
76
|
+
The repository includes examples for integrating with popular AI APIs:
|
77
|
+
|
78
|
+
#### OpenAI Integration
|
79
|
+
|
80
|
+
Ruby-MCP-Client works with both official and community OpenAI gems:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
# Using the openai/openai-ruby gem (official)
|
84
|
+
require 'mcp_client'
|
85
|
+
require 'openai'
|
86
|
+
|
87
|
+
# Create MCP client
|
88
|
+
mcp_client = MCPClient.create_client(
|
89
|
+
mcp_server_configs: [
|
90
|
+
MCPClient.stdio_config(
|
91
|
+
command: %W[npx -y @modelcontextprotocol/server-filesystem #{Dir.pwd}]
|
92
|
+
)
|
93
|
+
]
|
94
|
+
)
|
95
|
+
|
96
|
+
# Convert tools to OpenAI format
|
97
|
+
tools = mcp_client.to_openai_tools
|
98
|
+
|
99
|
+
# Use with OpenAI client
|
100
|
+
client = OpenAI::Client.new(api_key: ENV['OPENAI_API_KEY'])
|
101
|
+
response = client.chat.completions.create(
|
102
|
+
model: 'gpt-4',
|
103
|
+
messages: [
|
104
|
+
{ role: 'user', content: 'List files in current directory' }
|
105
|
+
],
|
106
|
+
tools: tools
|
107
|
+
)
|
108
|
+
|
109
|
+
# Process tool calls and results
|
110
|
+
# See examples directory for complete implementation
|
111
|
+
```
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
# Using the alexrudall/ruby-openai gem (community)
|
115
|
+
require 'mcp_client'
|
116
|
+
require 'openai'
|
117
|
+
|
118
|
+
# Create MCP client
|
119
|
+
mcp_client = MCPClient.create_client(
|
120
|
+
mcp_server_configs: [
|
121
|
+
MCPClient.stdio_config(command: 'npx @playwright/mcp@latest')
|
122
|
+
]
|
123
|
+
)
|
124
|
+
|
125
|
+
# Convert tools to OpenAI format
|
126
|
+
tools = mcp_client.to_openai_tools
|
127
|
+
|
128
|
+
# Use with Ruby-OpenAI client
|
129
|
+
client = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
|
130
|
+
# See examples directory for complete implementation
|
131
|
+
```
|
132
|
+
|
133
|
+
#### Anthropic Integration
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
require 'mcp_client'
|
137
|
+
require 'anthropic'
|
138
|
+
|
139
|
+
# Create MCP client
|
140
|
+
mcp_client = MCPClient.create_client(
|
141
|
+
mcp_server_configs: [
|
142
|
+
MCPClient.stdio_config(
|
143
|
+
command: %W[npx -y @modelcontextprotocol/server-filesystem #{Dir.pwd}]
|
144
|
+
)
|
145
|
+
]
|
146
|
+
)
|
147
|
+
|
148
|
+
# Convert tools to Anthropic format
|
149
|
+
claude_tools = mcp_client.to_anthropic_tools
|
150
|
+
|
151
|
+
# Use with Anthropic client
|
152
|
+
client = Anthropic::Client.new(access_token: ENV['ANTHROPIC_API_KEY'])
|
153
|
+
# See examples directory for complete implementation
|
154
|
+
```
|
155
|
+
|
156
|
+
Complete examples can be found in the `examples/` directory:
|
157
|
+
- `ruby_openai_mcp.rb` - Integration with alexrudall/ruby-openai gem
|
158
|
+
- `openai_ruby_mcp.rb` - Integration with official openai/openai-ruby gem
|
159
|
+
- `ruby_anthropic_mcp.rb` - Integration with alexrudall/ruby-anthropic gem
|
160
|
+
|
161
|
+
## MCP Server Compatibility
|
162
|
+
|
163
|
+
This client works with any MCP-compatible server, including:
|
164
|
+
|
165
|
+
- [@modelcontextprotocol/server-filesystem](https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem) - File system access
|
166
|
+
- [@playwright/mcp](https://www.npmjs.com/package/@playwright/mcp) - Browser automation
|
167
|
+
- Custom servers implementing the MCP protocol
|
168
|
+
|
169
|
+
### Server-Sent Events (SSE) Implementation
|
170
|
+
|
171
|
+
The SSE client implementation provides these key features:
|
172
|
+
|
173
|
+
- **Robust connection handling**: Properly manages HTTP/HTTPS connections with configurable timeouts
|
174
|
+
- **Thread safety**: All operations are thread-safe using monitors and synchronized access
|
175
|
+
- **Reliable error handling**: Comprehensive error handling for network issues, timeouts, and malformed responses
|
176
|
+
- **JSON-RPC over SSE**: Full implementation of JSON-RPC 2.0 over SSE transport
|
177
|
+
|
178
|
+
## Requirements
|
179
|
+
|
180
|
+
- Ruby >= 2.7.0
|
181
|
+
- No runtime dependencies
|
182
|
+
|
183
|
+
## Implementing an MCP Server
|
184
|
+
|
185
|
+
To implement a compatible MCP server you must:
|
186
|
+
|
187
|
+
- Listen on your chosen transport (JSON-RPC stdio, or HTTP SSE)
|
188
|
+
- Respond to `list_tools` requests with a JSON list of tools
|
189
|
+
- Respond to `call_tool` requests by executing the specified tool
|
190
|
+
- Return results (or errors) in JSON format
|
191
|
+
|
192
|
+
## Tool Schema
|
193
|
+
|
194
|
+
Each tool is defined by a name, description, and a JSON Schema for its parameters:
|
195
|
+
|
196
|
+
```json
|
197
|
+
{
|
198
|
+
"name": "example_tool",
|
199
|
+
"description": "Does something useful",
|
200
|
+
"schema": {
|
201
|
+
"type": "object",
|
202
|
+
"properties": {
|
203
|
+
"param1": { "type": "string" },
|
204
|
+
"param2": { "type": "number" }
|
205
|
+
},
|
206
|
+
"required": ["param1"]
|
207
|
+
}
|
208
|
+
}
|
209
|
+
```
|
210
|
+
|
211
|
+
## License
|
212
|
+
|
213
|
+
This gem is available as open source under the [MIT License](LICENSE).
|
214
|
+
|
215
|
+
## Contributing
|
216
|
+
|
217
|
+
Bug reports and pull requests are welcome on GitHub at
|
218
|
+
https://github.com/simonx1/ruby-mcp-client.
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MCPClient
|
4
|
+
# MCP Client for integrating with the Model Context Protocol
|
5
|
+
# This is the main entry point for using MCP tools
|
6
|
+
class Client
|
7
|
+
attr_reader :servers, :tool_cache
|
8
|
+
|
9
|
+
def initialize(mcp_server_configs: [])
|
10
|
+
@servers = mcp_server_configs.map { |config| MCPClient::ServerFactory.create(config) }
|
11
|
+
@tool_cache = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
# Lists all available tools from all connected MCP servers
|
15
|
+
# @param cache [Boolean] whether to use cached tools or fetch fresh
|
16
|
+
# @return [Array<MCPClient::Tool>] list of available tools
|
17
|
+
def list_tools(cache: true)
|
18
|
+
return @tool_cache.values if cache && !@tool_cache.empty?
|
19
|
+
|
20
|
+
tools = []
|
21
|
+
servers.each do |server|
|
22
|
+
server.list_tools.each do |tool|
|
23
|
+
@tool_cache[tool.name] = tool
|
24
|
+
tools << tool
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
tools
|
29
|
+
end
|
30
|
+
|
31
|
+
# Calls a specific tool by name with the given parameters
|
32
|
+
# @param tool_name [String] the name of the tool to call
|
33
|
+
# @param parameters [Hash] the parameters to pass to the tool
|
34
|
+
# @return [Object] the result of the tool invocation
|
35
|
+
def call_tool(tool_name, parameters)
|
36
|
+
tools = list_tools
|
37
|
+
tool = tools.find { |t| t.name == tool_name }
|
38
|
+
|
39
|
+
raise MCPClient::Errors::ToolNotFound, "Tool '#{tool_name}' not found" unless tool
|
40
|
+
|
41
|
+
# Find the server that owns this tool
|
42
|
+
server = find_server_for_tool(tool)
|
43
|
+
raise MCPClient::Errors::ServerNotFound, "No server found for tool '#{tool_name}'" unless server
|
44
|
+
|
45
|
+
server.call_tool(tool_name, parameters)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Convert MCP tools to OpenAI function specifications
|
49
|
+
# @return [Array<Hash>] OpenAI function specifications
|
50
|
+
def to_openai_tools
|
51
|
+
list_tools.map(&:to_openai_tool)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Convert MCP tools to Anthropic Claude tool specifications
|
55
|
+
# @return [Array<Hash>] Anthropic Claude tool specifications
|
56
|
+
def to_anthropic_tools
|
57
|
+
list_tools.map(&:to_anthropic_tool)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Clean up all server connections
|
61
|
+
def cleanup
|
62
|
+
servers.each(&:cleanup)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def find_server_for_tool(tool)
|
68
|
+
servers.find do |server|
|
69
|
+
server.list_tools.any? { |t| t.name == tool.name }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MCPClient
|
4
|
+
# Collection of error classes used by the MCP client
|
5
|
+
module Errors
|
6
|
+
# Base error class for all MCP-related errors
|
7
|
+
class MCPError < StandardError; end
|
8
|
+
|
9
|
+
# Raised when a tool is not found
|
10
|
+
class ToolNotFound < MCPError; end
|
11
|
+
|
12
|
+
# Raised when a server is not found
|
13
|
+
class ServerNotFound < MCPError; end
|
14
|
+
|
15
|
+
# Raised when there's an error calling a tool
|
16
|
+
class ToolCallError < MCPError; end
|
17
|
+
|
18
|
+
# Raised when there's a connection error with an MCP server
|
19
|
+
class ConnectionError < MCPError; end
|
20
|
+
|
21
|
+
# Raised when the MCP server returns an error response
|
22
|
+
class ServerError < MCPError; end
|
23
|
+
|
24
|
+
# Raised when there's an error in the MCP server transport
|
25
|
+
class TransportError < MCPError; end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MCPClient
|
4
|
+
# Base class for MCP servers - serves as the interface for different server implementations
|
5
|
+
class ServerBase
|
6
|
+
# Initialize a connection to the MCP server
|
7
|
+
# @return [Boolean] true if connection successful
|
8
|
+
def connect
|
9
|
+
raise NotImplementedError, 'Subclasses must implement connect'
|
10
|
+
end
|
11
|
+
|
12
|
+
# List all tools available from the MCP server
|
13
|
+
# @return [Array<MCPClient::Tool>] list of available tools
|
14
|
+
def list_tools
|
15
|
+
raise NotImplementedError, 'Subclasses must implement list_tools'
|
16
|
+
end
|
17
|
+
|
18
|
+
# Call a tool with the given parameters
|
19
|
+
# @param tool_name [String] the name of the tool to call
|
20
|
+
# @param parameters [Hash] the parameters to pass to the tool
|
21
|
+
# @return [Object] the result of the tool invocation
|
22
|
+
def call_tool(tool_name, parameters)
|
23
|
+
raise NotImplementedError, 'Subclasses must implement call_tool'
|
24
|
+
end
|
25
|
+
|
26
|
+
# Clean up the server connection
|
27
|
+
def cleanup
|
28
|
+
raise NotImplementedError, 'Subclasses must implement cleanup'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MCPClient
|
4
|
+
# Factory for creating MCP server instances based on configuration
|
5
|
+
class ServerFactory
|
6
|
+
# Create a server instance based on configuration
|
7
|
+
# @param config [Hash] server configuration
|
8
|
+
# @return [MCPClient::ServerBase] server instance
|
9
|
+
def self.create(config)
|
10
|
+
case config[:type]
|
11
|
+
when 'stdio'
|
12
|
+
MCPClient::ServerStdio.new(command: config[:command])
|
13
|
+
when 'sse'
|
14
|
+
MCPClient::ServerSSE.new(
|
15
|
+
base_url: config[:base_url],
|
16
|
+
headers: config[:headers] || {},
|
17
|
+
read_timeout: config[:read_timeout] || 30
|
18
|
+
)
|
19
|
+
else
|
20
|
+
raise ArgumentError, "Unknown server type: #{config[:type]}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,413 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'net/http'
|
5
|
+
require 'json'
|
6
|
+
require 'openssl'
|
7
|
+
require 'monitor'
|
8
|
+
|
9
|
+
module MCPClient
|
10
|
+
# Implementation of MCP server that communicates via Server-Sent Events (SSE)
|
11
|
+
# Useful for communicating with remote MCP servers over HTTP
|
12
|
+
class ServerSSE < ServerBase
|
13
|
+
attr_reader :base_url, :tools, :session_id, :http_client
|
14
|
+
|
15
|
+
# @param base_url [String] The base URL of the MCP server
|
16
|
+
# @param headers [Hash] Additional headers to include in requests
|
17
|
+
# @param read_timeout [Integer] Read timeout in seconds (default: 30)
|
18
|
+
def initialize(base_url:, headers: {}, read_timeout: 30)
|
19
|
+
super()
|
20
|
+
@base_url = base_url.end_with?('/') ? base_url : "#{base_url}/"
|
21
|
+
@headers = headers.merge({
|
22
|
+
'Accept' => 'text/event-stream',
|
23
|
+
'Cache-Control' => 'no-cache',
|
24
|
+
'Connection' => 'keep-alive'
|
25
|
+
})
|
26
|
+
@http_client = nil
|
27
|
+
@tools = nil
|
28
|
+
@read_timeout = read_timeout
|
29
|
+
@session_id = nil
|
30
|
+
@tools_data = nil
|
31
|
+
@request_id = 0
|
32
|
+
@sse_results = {}
|
33
|
+
@mutex = Monitor.new
|
34
|
+
@buffer = ''
|
35
|
+
@sse_connected = false
|
36
|
+
@connection_established = false
|
37
|
+
@connection_cv = @mutex.new_cond
|
38
|
+
end
|
39
|
+
|
40
|
+
# List all tools available from the MCP server
|
41
|
+
# @return [Array<MCPClient::Tool>] list of available tools
|
42
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
43
|
+
# @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
|
44
|
+
# @raise [MCPClient::Errors::ToolCallError] for other errors during tool listing
|
45
|
+
def list_tools
|
46
|
+
@mutex.synchronize do
|
47
|
+
return @tools if @tools
|
48
|
+
end
|
49
|
+
|
50
|
+
connect
|
51
|
+
|
52
|
+
begin
|
53
|
+
tools_data = request_tools_list
|
54
|
+
@mutex.synchronize do
|
55
|
+
@tools = tools_data.map do |tool_data|
|
56
|
+
MCPClient::Tool.from_json(tool_data)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
@mutex.synchronize { @tools }
|
61
|
+
rescue MCPClient::Errors::TransportError
|
62
|
+
# Re-raise TransportError directly
|
63
|
+
raise
|
64
|
+
rescue JSON::ParserError => e
|
65
|
+
raise MCPClient::Errors::TransportError, "Invalid JSON response from server: #{e.message}"
|
66
|
+
rescue StandardError => e
|
67
|
+
raise MCPClient::Errors::ToolCallError, "Error listing tools: #{e.message}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Call a tool with the given parameters
|
72
|
+
# @param tool_name [String] the name of the tool to call
|
73
|
+
# @param parameters [Hash] the parameters to pass to the tool
|
74
|
+
# @return [Object] the result of the tool invocation
|
75
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
76
|
+
# @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
|
77
|
+
# @raise [MCPClient::Errors::ToolCallError] for other errors during tool execution
|
78
|
+
def call_tool(tool_name, parameters)
|
79
|
+
connect
|
80
|
+
|
81
|
+
begin
|
82
|
+
request_id = @mutex.synchronize { @request_id += 1 }
|
83
|
+
|
84
|
+
json_rpc_request = {
|
85
|
+
jsonrpc: '2.0',
|
86
|
+
id: request_id,
|
87
|
+
method: 'tools/call',
|
88
|
+
params: {
|
89
|
+
name: tool_name,
|
90
|
+
arguments: parameters
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
send_jsonrpc_request(json_rpc_request)
|
95
|
+
rescue MCPClient::Errors::TransportError
|
96
|
+
# Re-raise TransportError directly
|
97
|
+
raise
|
98
|
+
rescue JSON::ParserError => e
|
99
|
+
raise MCPClient::Errors::TransportError, "Invalid JSON response from server: #{e.message}"
|
100
|
+
rescue StandardError => e
|
101
|
+
raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Connect to the MCP server over HTTP/HTTPS with SSE
|
106
|
+
# @return [Boolean] true if connection was successful
|
107
|
+
# @raise [MCPClient::Errors::ConnectionError] if connection fails
|
108
|
+
def connect
|
109
|
+
@mutex.synchronize do
|
110
|
+
return true if @connection_established
|
111
|
+
|
112
|
+
uri = URI.parse(@base_url)
|
113
|
+
@http_client = Net::HTTP.new(uri.host, uri.port)
|
114
|
+
|
115
|
+
if uri.scheme == 'https'
|
116
|
+
@http_client.use_ssl = true
|
117
|
+
@http_client.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
118
|
+
end
|
119
|
+
|
120
|
+
@http_client.open_timeout = 10
|
121
|
+
@http_client.read_timeout = @read_timeout
|
122
|
+
@http_client.keep_alive_timeout = 60
|
123
|
+
|
124
|
+
@http_client.start
|
125
|
+
start_sse_thread
|
126
|
+
|
127
|
+
timeout = 10
|
128
|
+
success = @connection_cv.wait(timeout) { @connection_established }
|
129
|
+
|
130
|
+
unless success
|
131
|
+
cleanup
|
132
|
+
raise MCPClient::Errors::ConnectionError, 'Timed out waiting for SSE connection to be established'
|
133
|
+
end
|
134
|
+
|
135
|
+
@connection_established
|
136
|
+
end
|
137
|
+
rescue StandardError => e
|
138
|
+
cleanup
|
139
|
+
raise MCPClient::Errors::ConnectionError, "Failed to connect to MCP server at #{@base_url}: #{e.message}"
|
140
|
+
end
|
141
|
+
|
142
|
+
# Clean up the server connection
|
143
|
+
# Properly closes HTTP connections and clears cached tools
|
144
|
+
def cleanup
|
145
|
+
@mutex.synchronize do
|
146
|
+
begin
|
147
|
+
@sse_thread&.kill
|
148
|
+
rescue StandardError
|
149
|
+
nil
|
150
|
+
end
|
151
|
+
@sse_thread = nil
|
152
|
+
|
153
|
+
if @http_client
|
154
|
+
@http_client.finish if @http_client.started?
|
155
|
+
@http_client = nil
|
156
|
+
end
|
157
|
+
|
158
|
+
@tools = nil
|
159
|
+
@session_id = nil
|
160
|
+
@connection_established = false
|
161
|
+
@sse_connected = false
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
# Start the SSE thread to listen for events
|
168
|
+
def start_sse_thread
|
169
|
+
return if @sse_thread&.alive?
|
170
|
+
|
171
|
+
@sse_thread = Thread.new do
|
172
|
+
sse_http = nil
|
173
|
+
begin
|
174
|
+
uri = URI.parse(@base_url)
|
175
|
+
sse_http = Net::HTTP.new(uri.host, uri.port)
|
176
|
+
|
177
|
+
if uri.scheme == 'https'
|
178
|
+
sse_http.use_ssl = true
|
179
|
+
sse_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
180
|
+
end
|
181
|
+
|
182
|
+
sse_http.open_timeout = 10
|
183
|
+
sse_http.read_timeout = @read_timeout
|
184
|
+
sse_http.keep_alive_timeout = 60
|
185
|
+
|
186
|
+
sse_http.start do |http|
|
187
|
+
request = Net::HTTP::Get.new(uri)
|
188
|
+
@headers.each { |k, v| request[k] = v }
|
189
|
+
|
190
|
+
http.request(request) do |response|
|
191
|
+
unless response.is_a?(Net::HTTPSuccess) && response['content-type']&.start_with?('text/event-stream')
|
192
|
+
@mutex.synchronize do
|
193
|
+
@connection_established = false
|
194
|
+
@connection_cv.broadcast
|
195
|
+
end
|
196
|
+
raise MCPClient::Errors::ServerError, 'Server response not OK or not text/event-stream'
|
197
|
+
end
|
198
|
+
|
199
|
+
@mutex.synchronize do
|
200
|
+
@sse_connected = true
|
201
|
+
end
|
202
|
+
|
203
|
+
response.read_body do |chunk|
|
204
|
+
process_sse_chunk(chunk.dup)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
rescue StandardError
|
209
|
+
nil
|
210
|
+
ensure
|
211
|
+
sse_http&.finish if sse_http&.started?
|
212
|
+
@mutex.synchronize do
|
213
|
+
@sse_connected = false
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Process an SSE chunk from the server
|
220
|
+
# @param chunk [String] the chunk to process
|
221
|
+
def process_sse_chunk(chunk)
|
222
|
+
local_buffer = nil
|
223
|
+
|
224
|
+
@mutex.synchronize do
|
225
|
+
@buffer += chunk
|
226
|
+
|
227
|
+
while (event_end = @buffer.index("\n\n"))
|
228
|
+
event_data = @buffer.slice!(0, event_end + 2)
|
229
|
+
local_buffer = event_data
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
parse_and_handle_sse_event(local_buffer) if local_buffer
|
234
|
+
end
|
235
|
+
|
236
|
+
# Parse and handle an SSE event
|
237
|
+
# @param event_data [String] the event data to parse
|
238
|
+
def parse_and_handle_sse_event(event_data)
|
239
|
+
event = parse_sse_event(event_data)
|
240
|
+
return if event.nil?
|
241
|
+
|
242
|
+
case event[:event]
|
243
|
+
when 'endpoint'
|
244
|
+
if event[:data].include?('sessionId=')
|
245
|
+
session_id = event[:data].split('sessionId=').last
|
246
|
+
|
247
|
+
@mutex.synchronize do
|
248
|
+
@session_id = session_id
|
249
|
+
@connection_established = true
|
250
|
+
@connection_cv.broadcast
|
251
|
+
end
|
252
|
+
end
|
253
|
+
when 'message'
|
254
|
+
begin
|
255
|
+
data = JSON.parse(event[:data])
|
256
|
+
|
257
|
+
@mutex.synchronize do
|
258
|
+
@tools_data = data['result']['tools'] if data['result'] && data['result']['tools']
|
259
|
+
|
260
|
+
if data['id']
|
261
|
+
if data['error']
|
262
|
+
@sse_results[data['id']] = {
|
263
|
+
'isError' => true,
|
264
|
+
'content' => [{ 'type' => 'text', 'text' => data['error'].to_json }]
|
265
|
+
}
|
266
|
+
elsif data['result']
|
267
|
+
@sse_results[data['id']] = data['result']
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
rescue JSON::ParserError
|
272
|
+
nil
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Parse an SSE event
|
278
|
+
# @param event_data [String] the event data to parse
|
279
|
+
# @return [Hash, nil] the parsed event, or nil if the event is invalid
|
280
|
+
def parse_sse_event(event_data)
|
281
|
+
event = { event: 'message', data: '', id: nil }
|
282
|
+
data_lines = []
|
283
|
+
|
284
|
+
event_data.each_line do |line|
|
285
|
+
line = line.chomp
|
286
|
+
next if line.empty?
|
287
|
+
|
288
|
+
if line.start_with?('event:')
|
289
|
+
event[:event] = line[6..].strip
|
290
|
+
elsif line.start_with?('data:')
|
291
|
+
data_lines << line[5..].strip
|
292
|
+
elsif line.start_with?('id:')
|
293
|
+
event[:id] = line[3..].strip
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
event[:data] = data_lines.join("\n")
|
298
|
+
event[:data].empty? ? nil : event
|
299
|
+
end
|
300
|
+
|
301
|
+
# Request the tools list using JSON-RPC
|
302
|
+
# @return [Array<Hash>] the tools data
|
303
|
+
def request_tools_list
|
304
|
+
@mutex.synchronize do
|
305
|
+
return @tools_data if @tools_data
|
306
|
+
end
|
307
|
+
|
308
|
+
request_id = @mutex.synchronize { @request_id += 1 }
|
309
|
+
|
310
|
+
json_rpc_request = {
|
311
|
+
jsonrpc: '2.0',
|
312
|
+
id: request_id,
|
313
|
+
method: 'tools/list',
|
314
|
+
params: {}
|
315
|
+
}
|
316
|
+
|
317
|
+
result = send_jsonrpc_request(json_rpc_request)
|
318
|
+
|
319
|
+
if result && result['tools']
|
320
|
+
@mutex.synchronize do
|
321
|
+
@tools_data = result['tools']
|
322
|
+
end
|
323
|
+
return @mutex.synchronize { @tools_data.dup }
|
324
|
+
elsif result
|
325
|
+
@mutex.synchronize do
|
326
|
+
@tools_data = result
|
327
|
+
end
|
328
|
+
return @mutex.synchronize { @tools_data.dup }
|
329
|
+
end
|
330
|
+
|
331
|
+
raise MCPClient::Errors::ToolCallError, 'Failed to get tools list from JSON-RPC request'
|
332
|
+
end
|
333
|
+
|
334
|
+
# Send a JSON-RPC request to the server and wait for result
|
335
|
+
# @param request [Hash] the JSON-RPC request
|
336
|
+
# @return [Hash] the result of the request
|
337
|
+
def send_jsonrpc_request(request)
|
338
|
+
uri = URI.parse(@base_url)
|
339
|
+
rpc_http = Net::HTTP.new(uri.host, uri.port)
|
340
|
+
|
341
|
+
if uri.scheme == 'https'
|
342
|
+
rpc_http.use_ssl = true
|
343
|
+
rpc_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
344
|
+
end
|
345
|
+
|
346
|
+
rpc_http.open_timeout = 10
|
347
|
+
rpc_http.read_timeout = @read_timeout
|
348
|
+
rpc_http.keep_alive_timeout = 60
|
349
|
+
|
350
|
+
begin
|
351
|
+
rpc_http.start do |http|
|
352
|
+
session_id = @mutex.synchronize { @session_id }
|
353
|
+
|
354
|
+
url = if session_id
|
355
|
+
"#{@base_url.sub(%r{/sse/?$}, '')}/messages?sessionId=#{session_id}"
|
356
|
+
else
|
357
|
+
"#{@base_url.sub(%r{/sse/?$}, '')}/messages"
|
358
|
+
end
|
359
|
+
|
360
|
+
uri = URI.parse(url)
|
361
|
+
http_request = Net::HTTP::Post.new(uri)
|
362
|
+
http_request.content_type = 'application/json'
|
363
|
+
http_request.body = request.to_json
|
364
|
+
|
365
|
+
headers = @mutex.synchronize { @headers.dup }
|
366
|
+
headers.except('Accept', 'Cache-Control')
|
367
|
+
.each { |k, v| http_request[k] = v }
|
368
|
+
|
369
|
+
response = http.request(http_request)
|
370
|
+
|
371
|
+
unless response.is_a?(Net::HTTPSuccess)
|
372
|
+
raise MCPClient::Errors::ServerError, "Server returned error: #{response.code} #{response.message}"
|
373
|
+
end
|
374
|
+
|
375
|
+
if response.code == '202'
|
376
|
+
request_id = request[:id]
|
377
|
+
|
378
|
+
start_time = Time.now
|
379
|
+
timeout = 10
|
380
|
+
result = nil
|
381
|
+
|
382
|
+
loop do
|
383
|
+
@mutex.synchronize do
|
384
|
+
if @sse_results[request_id]
|
385
|
+
result = @sse_results[request_id]
|
386
|
+
@sse_results.delete(request_id)
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
break if result || (Time.now - start_time > timeout)
|
391
|
+
|
392
|
+
sleep 0.1
|
393
|
+
end
|
394
|
+
|
395
|
+
return result if result
|
396
|
+
|
397
|
+
raise MCPClient::Errors::ToolCallError, "Timeout waiting for SSE result for request #{request_id}"
|
398
|
+
|
399
|
+
else
|
400
|
+
begin
|
401
|
+
data = JSON.parse(response.body)
|
402
|
+
return data['result']
|
403
|
+
rescue JSON::ParserError => e
|
404
|
+
raise MCPClient::Errors::TransportError, "Invalid JSON response from server: #{e.message}"
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
ensure
|
409
|
+
rpc_http.finish if rpc_http.started?
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'json'
|
5
|
+
require_relative 'version'
|
6
|
+
|
7
|
+
module MCPClient
|
8
|
+
# JSON-RPC implementation of MCP server over stdio.
|
9
|
+
class ServerStdio < ServerBase
|
10
|
+
attr_reader :command
|
11
|
+
|
12
|
+
# Timeout in seconds for responses
|
13
|
+
READ_TIMEOUT = 15
|
14
|
+
|
15
|
+
# @param command [String, Array] the stdio command to launch the MCP JSON-RPC server
|
16
|
+
def initialize(command:)
|
17
|
+
super()
|
18
|
+
@command = command.is_a?(Array) ? command.join(' ') : command
|
19
|
+
@mutex = Mutex.new
|
20
|
+
@cond = ConditionVariable.new
|
21
|
+
@next_id = 1
|
22
|
+
@pending = {}
|
23
|
+
@initialized = false
|
24
|
+
end
|
25
|
+
|
26
|
+
# Connect to the MCP server by launching the command process via stdout/stdin
|
27
|
+
# @return [Boolean] true if connection was successful
|
28
|
+
# @raise [MCPClient::Errors::ConnectionError] if connection fails
|
29
|
+
def connect
|
30
|
+
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(@command)
|
31
|
+
true
|
32
|
+
rescue StandardError => e
|
33
|
+
raise MCPClient::Errors::ConnectionError, "Failed to connect to MCP server: #{e.message}"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Spawn a reader thread to collect JSON-RPC responses
|
37
|
+
def start_reader
|
38
|
+
@reader_thread = Thread.new do
|
39
|
+
@stdout.each_line do |line|
|
40
|
+
handle_line(line)
|
41
|
+
end
|
42
|
+
rescue StandardError
|
43
|
+
# Reader thread aborted unexpectedly
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Handle a line of output from the stdio server
|
48
|
+
# Parses JSON-RPC messages and adds them to pending responses
|
49
|
+
# @param line [String] line of output to parse
|
50
|
+
def handle_line(line)
|
51
|
+
msg = JSON.parse(line)
|
52
|
+
id = msg['id']
|
53
|
+
return unless id
|
54
|
+
|
55
|
+
@mutex.synchronize do
|
56
|
+
@pending[id] = msg
|
57
|
+
@cond.broadcast
|
58
|
+
end
|
59
|
+
rescue JSON::ParserError
|
60
|
+
# Skip non-JSONRPC lines in the output stream
|
61
|
+
end
|
62
|
+
|
63
|
+
# List all tools available from the MCP server
|
64
|
+
# @return [Array<MCPClient::Tool>] list of available tools
|
65
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
66
|
+
# @raise [MCPClient::Errors::ToolCallError] for other errors during tool listing
|
67
|
+
def list_tools
|
68
|
+
ensure_initialized
|
69
|
+
req_id = next_id
|
70
|
+
# JSON-RPC method for listing tools
|
71
|
+
req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'tools/list', 'params' => {} }
|
72
|
+
send_request(req)
|
73
|
+
res = wait_response(req_id)
|
74
|
+
if (err = res['error'])
|
75
|
+
raise MCPClient::Errors::ServerError, err['message']
|
76
|
+
end
|
77
|
+
|
78
|
+
(res.dig('result', 'tools') || []).map { |td| MCPClient::Tool.from_json(td) }
|
79
|
+
rescue StandardError => e
|
80
|
+
raise MCPClient::Errors::ToolCallError, "Error listing tools: #{e.message}"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Call a tool with the given parameters
|
84
|
+
# @param tool_name [String] the name of the tool to call
|
85
|
+
# @param parameters [Hash] the parameters to pass to the tool
|
86
|
+
# @return [Object] the result of the tool invocation
|
87
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
88
|
+
# @raise [MCPClient::Errors::ToolCallError] for other errors during tool execution
|
89
|
+
def call_tool(tool_name, parameters)
|
90
|
+
ensure_initialized
|
91
|
+
req_id = next_id
|
92
|
+
# JSON-RPC method for calling a tool
|
93
|
+
req = {
|
94
|
+
'jsonrpc' => '2.0',
|
95
|
+
'id' => req_id,
|
96
|
+
'method' => 'tools/call',
|
97
|
+
'params' => { 'name' => tool_name, 'arguments' => parameters }
|
98
|
+
}
|
99
|
+
send_request(req)
|
100
|
+
res = wait_response(req_id)
|
101
|
+
if (err = res['error'])
|
102
|
+
raise MCPClient::Errors::ServerError, err['message']
|
103
|
+
end
|
104
|
+
|
105
|
+
res['result']
|
106
|
+
rescue StandardError => e
|
107
|
+
raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
|
108
|
+
end
|
109
|
+
|
110
|
+
# Clean up the server connection
|
111
|
+
# Closes all stdio handles and terminates any running processes and threads
|
112
|
+
def cleanup
|
113
|
+
return unless @stdin
|
114
|
+
|
115
|
+
@stdin.close unless @stdin.closed?
|
116
|
+
@stdout.close unless @stdout.closed?
|
117
|
+
@stderr.close unless @stderr.closed?
|
118
|
+
if @wait_thread&.alive?
|
119
|
+
Process.kill('TERM', @wait_thread.pid)
|
120
|
+
@wait_thread.join(1)
|
121
|
+
end
|
122
|
+
@reader_thread&.kill
|
123
|
+
rescue StandardError
|
124
|
+
# Clean up resources during unexpected termination
|
125
|
+
ensure
|
126
|
+
@stdin = @stdout = @stderr = @wait_thread = @reader_thread = nil
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
# Ensure the server process is started and initialized (handshake)
|
132
|
+
def ensure_initialized
|
133
|
+
return if @initialized
|
134
|
+
|
135
|
+
connect
|
136
|
+
start_reader
|
137
|
+
perform_initialize
|
138
|
+
|
139
|
+
@initialized = true
|
140
|
+
end
|
141
|
+
|
142
|
+
# Handshake: send initialize request and initialized notification
|
143
|
+
def perform_initialize
|
144
|
+
# Initialize request
|
145
|
+
init_id = next_id
|
146
|
+
init_req = {
|
147
|
+
'jsonrpc' => '2.0',
|
148
|
+
'id' => init_id,
|
149
|
+
'method' => 'initialize',
|
150
|
+
'params' => {
|
151
|
+
'protocolVersion' => '2024-11-05',
|
152
|
+
'capabilities' => {},
|
153
|
+
'clientInfo' => { 'name' => 'ruby-mcp-client', 'version' => MCPClient::VERSION }
|
154
|
+
}
|
155
|
+
}
|
156
|
+
send_request(init_req)
|
157
|
+
res = wait_response(init_id)
|
158
|
+
if (err = res['error'])
|
159
|
+
raise MCPClient::Errors::ConnectionError, "Initialize failed: #{err['message']}"
|
160
|
+
end
|
161
|
+
|
162
|
+
# Send initialized notification
|
163
|
+
notif = { 'jsonrpc' => '2.0', 'method' => 'notifications/initialized', 'params' => {} }
|
164
|
+
@stdin.puts(notif.to_json)
|
165
|
+
end
|
166
|
+
|
167
|
+
def next_id
|
168
|
+
@mutex.synchronize do
|
169
|
+
id = @next_id
|
170
|
+
@next_id += 1
|
171
|
+
id
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def send_request(req)
|
176
|
+
@stdin.puts(req.to_json)
|
177
|
+
rescue StandardError => e
|
178
|
+
raise MCPClient::Errors::TransportError, "Failed to send JSONRPC request: #{e.message}"
|
179
|
+
end
|
180
|
+
|
181
|
+
def wait_response(id)
|
182
|
+
deadline = Time.now + READ_TIMEOUT
|
183
|
+
@mutex.synchronize do
|
184
|
+
until @pending.key?(id)
|
185
|
+
remaining = deadline - Time.now
|
186
|
+
break if remaining <= 0
|
187
|
+
|
188
|
+
@cond.wait(@mutex, remaining)
|
189
|
+
end
|
190
|
+
msg = @pending[id]
|
191
|
+
@pending[id] = nil
|
192
|
+
raise MCPClient::Errors::TransportError, "Timeout waiting for JSONRPC response id=#{id}" unless msg
|
193
|
+
|
194
|
+
msg
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MCPClient
|
4
|
+
# Representation of an MCP tool
|
5
|
+
class Tool
|
6
|
+
attr_reader :name, :description, :schema
|
7
|
+
|
8
|
+
def initialize(name:, description:, schema:)
|
9
|
+
@name = name
|
10
|
+
@description = description
|
11
|
+
@schema = schema
|
12
|
+
end
|
13
|
+
|
14
|
+
# Create a Tool instance from JSON data
|
15
|
+
# @param data [Hash] JSON data from MCP server
|
16
|
+
# @return [MCPClient::Tool] tool instance
|
17
|
+
def self.from_json(data)
|
18
|
+
# Some servers (Playwright MCP CLI) use 'inputSchema' instead of 'schema'
|
19
|
+
schema = data['inputSchema'] || data['schema']
|
20
|
+
new(
|
21
|
+
name: data['name'],
|
22
|
+
description: data['description'],
|
23
|
+
schema: schema
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Convert tool to OpenAI function specification format
|
28
|
+
# @return [Hash] OpenAI function specification
|
29
|
+
def to_openai_tool
|
30
|
+
{
|
31
|
+
type: 'function',
|
32
|
+
function: {
|
33
|
+
name: @name,
|
34
|
+
description: @description,
|
35
|
+
parameters: @schema
|
36
|
+
}
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
# Convert tool to Anthropic Claude tool specification format
|
41
|
+
# @return [Hash] Anthropic Claude tool specification
|
42
|
+
def to_anthropic_tool
|
43
|
+
{
|
44
|
+
name: @name,
|
45
|
+
description: @description,
|
46
|
+
input_schema: @schema
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/mcp_client.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Load all MCPClient components
|
4
|
+
require_relative 'mcp_client/errors'
|
5
|
+
require_relative 'mcp_client/tool'
|
6
|
+
require_relative 'mcp_client/server_base'
|
7
|
+
require_relative 'mcp_client/server_stdio'
|
8
|
+
require_relative 'mcp_client/server_sse'
|
9
|
+
require_relative 'mcp_client/server_factory'
|
10
|
+
require_relative 'mcp_client/client'
|
11
|
+
require_relative 'mcp_client/version'
|
12
|
+
|
13
|
+
# Model Context Protocol (MCP) Client module
|
14
|
+
# Provides a standardized way for agents to communicate with external tools and services
|
15
|
+
# through a protocol-based approach
|
16
|
+
module MCPClient
|
17
|
+
# Create a new MCPClient client
|
18
|
+
# @param mcp_server_configs [Array<Hash>] configurations for MCP servers
|
19
|
+
# @return [MCPClient::Client] new client instance
|
20
|
+
def self.create_client(mcp_server_configs: [])
|
21
|
+
MCPClient::Client.new(mcp_server_configs: mcp_server_configs)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Create a standard server configuration for stdio
|
25
|
+
# @param command [String, Array<String>] command to execute
|
26
|
+
# @return [Hash] server configuration
|
27
|
+
def self.stdio_config(command:)
|
28
|
+
{
|
29
|
+
type: 'stdio',
|
30
|
+
command: command
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
# Create a standard server configuration for SSE
|
35
|
+
# @param base_url [String] base URL for the server
|
36
|
+
# @param headers [Hash] HTTP headers to include in requests
|
37
|
+
# @param read_timeout [Integer] read timeout in seconds (default: 30)
|
38
|
+
# @return [Hash] server configuration
|
39
|
+
def self.sse_config(base_url:, headers: {}, read_timeout: 30)
|
40
|
+
{
|
41
|
+
type: 'sse',
|
42
|
+
base_url: base_url,
|
43
|
+
headers: headers,
|
44
|
+
read_timeout: read_timeout
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby-mcp-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Szymon Kurcab
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-04-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rdoc
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.12'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.12'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubocop
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.62'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.62'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: yard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.9.34
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.9.34
|
69
|
+
description: Ruby client library for integrating with Model Context Protocol (MCP)
|
70
|
+
servers to access and invoke tools from AI assistants
|
71
|
+
email:
|
72
|
+
- szymon.kurcab@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- LICENSE
|
78
|
+
- README.md
|
79
|
+
- lib/mcp_client.rb
|
80
|
+
- lib/mcp_client/client.rb
|
81
|
+
- lib/mcp_client/errors.rb
|
82
|
+
- lib/mcp_client/server_base.rb
|
83
|
+
- lib/mcp_client/server_factory.rb
|
84
|
+
- lib/mcp_client/server_sse.rb
|
85
|
+
- lib/mcp_client/server_stdio.rb
|
86
|
+
- lib/mcp_client/tool.rb
|
87
|
+
- lib/mcp_client/version.rb
|
88
|
+
homepage: https://github.com/simonx1/ruby-mcp-client
|
89
|
+
licenses:
|
90
|
+
- MIT
|
91
|
+
metadata:
|
92
|
+
rubygems_mfa_required: 'true'
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 2.7.0
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubygems_version: 3.5.16
|
109
|
+
signing_key:
|
110
|
+
specification_version: 4
|
111
|
+
summary: A Ruby client for the Model Context Protocol (MCP)
|
112
|
+
test_files: []
|