mcp_on_ruby 0.3.0 → 1.0.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 +4 -4
- data/CHANGELOG.md +56 -28
- data/CODE_OF_CONDUCT.md +30 -58
- data/CONTRIBUTING.md +61 -67
- data/LICENSE.txt +2 -2
- data/README.md +159 -509
- data/bin/console +11 -0
- data/bin/setup +6 -0
- data/docs/advanced-usage.md +132 -0
- data/docs/api-reference.md +35 -0
- data/docs/testing.md +55 -0
- data/examples/claude/README.md +171 -0
- data/examples/claude/claude-bridge.js +122 -0
- data/lib/mcp_on_ruby/configuration.rb +74 -0
- data/lib/mcp_on_ruby/errors.rb +137 -0
- data/lib/mcp_on_ruby/generators/install_generator.rb +46 -0
- data/lib/mcp_on_ruby/generators/resource_generator.rb +63 -0
- data/lib/mcp_on_ruby/generators/templates/README +31 -0
- data/lib/mcp_on_ruby/generators/templates/application_resource.rb +20 -0
- data/lib/mcp_on_ruby/generators/templates/application_tool.rb +18 -0
- data/lib/mcp_on_ruby/generators/templates/initializer.rb +41 -0
- data/lib/mcp_on_ruby/generators/templates/resource.rb +50 -0
- data/lib/mcp_on_ruby/generators/templates/resource_spec.rb +67 -0
- data/lib/mcp_on_ruby/generators/templates/sample_resource.rb +57 -0
- data/lib/mcp_on_ruby/generators/templates/sample_tool.rb +59 -0
- data/lib/mcp_on_ruby/generators/templates/tool.rb +38 -0
- data/lib/mcp_on_ruby/generators/templates/tool_spec.rb +55 -0
- data/lib/mcp_on_ruby/generators/tool_generator.rb +51 -0
- data/lib/mcp_on_ruby/railtie.rb +108 -0
- data/lib/mcp_on_ruby/resource.rb +161 -0
- data/lib/mcp_on_ruby/server.rb +378 -0
- data/lib/mcp_on_ruby/tool.rb +134 -0
- data/lib/mcp_on_ruby/transport.rb +330 -0
- data/lib/mcp_on_ruby/version.rb +6 -0
- data/lib/mcp_on_ruby.rb +142 -0
- metadata +62 -173
- data/lib/ruby_mcp/client.rb +0 -43
- data/lib/ruby_mcp/configuration.rb +0 -90
- data/lib/ruby_mcp/errors.rb +0 -17
- data/lib/ruby_mcp/models/context.rb +0 -52
- data/lib/ruby_mcp/models/engine.rb +0 -31
- data/lib/ruby_mcp/models/message.rb +0 -60
- data/lib/ruby_mcp/providers/anthropic.rb +0 -269
- data/lib/ruby_mcp/providers/base.rb +0 -57
- data/lib/ruby_mcp/providers/openai.rb +0 -265
- data/lib/ruby_mcp/schemas.rb +0 -56
- data/lib/ruby_mcp/server/app.rb +0 -84
- data/lib/ruby_mcp/server/base_controller.rb +0 -49
- data/lib/ruby_mcp/server/content_controller.rb +0 -68
- data/lib/ruby_mcp/server/contexts_controller.rb +0 -67
- data/lib/ruby_mcp/server/controller.rb +0 -29
- data/lib/ruby_mcp/server/engines_controller.rb +0 -34
- data/lib/ruby_mcp/server/generate_controller.rb +0 -140
- data/lib/ruby_mcp/server/messages_controller.rb +0 -30
- data/lib/ruby_mcp/server/router.rb +0 -84
- data/lib/ruby_mcp/storage/active_record.rb +0 -414
- data/lib/ruby_mcp/storage/base.rb +0 -43
- data/lib/ruby_mcp/storage/error.rb +0 -8
- data/lib/ruby_mcp/storage/memory.rb +0 -69
- data/lib/ruby_mcp/storage/redis.rb +0 -197
- data/lib/ruby_mcp/storage_factory.rb +0 -43
- data/lib/ruby_mcp/validator.rb +0 -45
- data/lib/ruby_mcp/version.rb +0 -6
- data/lib/ruby_mcp.rb +0 -71
data/bin/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "mcp_on_ruby"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
require "irb"
|
11
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# Advanced Usage
|
2
|
+
|
3
|
+
## Custom Authorization
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
class ApplicationTool < McpOnRuby::Tool
|
7
|
+
def authorize(context)
|
8
|
+
token = context[:auth_token]
|
9
|
+
user = authenticate_token(token)
|
10
|
+
user&.has_permission?(:mcp_access)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def authenticate_token(token)
|
16
|
+
# Your authentication logic
|
17
|
+
JWT.decode(token, Rails.application.secret_key_base).first
|
18
|
+
rescue JWT::DecodeError
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
## Resource Caching
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
class ApplicationResource < McpOnRuby::Resource
|
28
|
+
def read(params = {}, context = {})
|
29
|
+
cache_key = "mcp:#{uri}:#{params.hash}"
|
30
|
+
Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
## Manual Server Configuration
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
# For advanced scenarios where auto-registration isn't sufficient
|
41
|
+
McpOnRuby.mount_in_rails(Rails.application) do |server|
|
42
|
+
# Register tools manually
|
43
|
+
server.register_tool(CustomTool.new)
|
44
|
+
|
45
|
+
# Define tools with DSL
|
46
|
+
server.tool 'database_query', 'Execute read-only database queries' do |args|
|
47
|
+
query = args['query']
|
48
|
+
raise 'Only SELECT allowed' unless query.strip.upcase.start_with?('SELECT')
|
49
|
+
|
50
|
+
result = ActiveRecord::Base.connection.execute(query)
|
51
|
+
{ rows: result.to_a }
|
52
|
+
end
|
53
|
+
|
54
|
+
# Define resources with DSL
|
55
|
+
server.resource 'health' do
|
56
|
+
{
|
57
|
+
status: 'healthy',
|
58
|
+
database: database_healthy?,
|
59
|
+
redis: redis_healthy?,
|
60
|
+
timestamp: Time.current
|
61
|
+
}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
## Production Configuration
|
67
|
+
|
68
|
+
### Security Setup
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
# config/initializers/mcp_on_ruby.rb
|
72
|
+
McpOnRuby.configure do |config|
|
73
|
+
# Authentication
|
74
|
+
config.authentication_required = true
|
75
|
+
config.authentication_token = ENV['MCP_AUTH_TOKEN']
|
76
|
+
|
77
|
+
# Security
|
78
|
+
config.dns_rebinding_protection = true
|
79
|
+
config.allowed_origins = [
|
80
|
+
ENV['ALLOWED_ORIGIN'],
|
81
|
+
/\A#{Regexp.escape(ENV['DOMAIN'])}\z/
|
82
|
+
]
|
83
|
+
config.localhost_only = false
|
84
|
+
|
85
|
+
# Rate limiting
|
86
|
+
config.rate_limit_per_minute = 100
|
87
|
+
|
88
|
+
# Features
|
89
|
+
config.cors_enabled = true
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
### Monitoring & Logging
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class ApplicationTool < McpOnRuby::Tool
|
97
|
+
def call(arguments = {}, context = {})
|
98
|
+
start_time = Time.current
|
99
|
+
result = super
|
100
|
+
duration = Time.current - start_time
|
101
|
+
|
102
|
+
Rails.logger.info("MCP Tool executed", {
|
103
|
+
tool: name,
|
104
|
+
duration: duration,
|
105
|
+
success: !result.key?(:error),
|
106
|
+
user_ip: context[:remote_ip]
|
107
|
+
})
|
108
|
+
|
109
|
+
result
|
110
|
+
end
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
### Error Monitoring
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
# config/initializers/mcp_on_ruby.rb
|
118
|
+
class CustomTool < ApplicationTool
|
119
|
+
def execute(arguments, context)
|
120
|
+
# Your tool logic
|
121
|
+
rescue => error
|
122
|
+
# Report to error monitoring service
|
123
|
+
Bugsnag.notify(error, {
|
124
|
+
tool: name,
|
125
|
+
arguments: arguments,
|
126
|
+
context: context
|
127
|
+
})
|
128
|
+
|
129
|
+
raise
|
130
|
+
end
|
131
|
+
end
|
132
|
+
```
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# API Reference
|
2
|
+
|
3
|
+
## Server Methods
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
server = McpOnRuby.server do |s|
|
7
|
+
s.tool(name, description, input_schema, **options, &block)
|
8
|
+
s.resource(uri, **options, &block)
|
9
|
+
s.register_tool(tool_instance)
|
10
|
+
s.register_resource(resource_instance)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Handle requests
|
14
|
+
server.handle_request(json_string, context)
|
15
|
+
```
|
16
|
+
|
17
|
+
## Tool Class
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
class MyTool < McpOnRuby::Tool
|
21
|
+
def initialize(name:, description: '', input_schema: {}, **options)
|
22
|
+
def execute(arguments, context) # Override this
|
23
|
+
def authorize(context) # Optional override
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
## Resource Class
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
class MyResource < McpOnRuby::Resource
|
31
|
+
def initialize(uri:, name: nil, description: '', mime_type: 'application/json', **options)
|
32
|
+
def fetch_content(params, context) # Override this
|
33
|
+
def authorize(context) # Optional override
|
34
|
+
end
|
35
|
+
```
|
data/docs/testing.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# Testing Guide
|
2
|
+
|
3
|
+
## RSpec Integration
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
# spec/tools/user_manager_tool_spec.rb
|
7
|
+
require 'rails_helper'
|
8
|
+
|
9
|
+
RSpec.describe UserManagerTool do
|
10
|
+
subject(:tool) { described_class.new }
|
11
|
+
|
12
|
+
describe '#execute' do
|
13
|
+
context 'creating a user' do
|
14
|
+
let(:arguments) do
|
15
|
+
{
|
16
|
+
'action' => 'create',
|
17
|
+
'attributes' => { 'name' => 'John Doe', 'email' => 'john@example.com' }
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'creates user successfully' do
|
22
|
+
result = tool.call(arguments, { authenticated: true })
|
23
|
+
|
24
|
+
expect(result[:success]).to be true
|
25
|
+
expect(result[:user]['name']).to eq 'John Doe'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
## Integration Testing
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
# spec/integration/mcp_server_spec.rb
|
36
|
+
require 'rails_helper'
|
37
|
+
|
38
|
+
RSpec.describe 'MCP Server Integration' do
|
39
|
+
let(:server) { Rails.application.config.mcp_server }
|
40
|
+
|
41
|
+
it 'handles tool calls' do
|
42
|
+
request = {
|
43
|
+
jsonrpc: '2.0',
|
44
|
+
method: 'tools/call',
|
45
|
+
params: { name: 'user_manager', arguments: { action: 'create' } },
|
46
|
+
id: 1
|
47
|
+
}
|
48
|
+
|
49
|
+
response = server.handle_request(request.to_json)
|
50
|
+
parsed = JSON.parse(response)
|
51
|
+
|
52
|
+
expect(parsed['result']).to be_present
|
53
|
+
end
|
54
|
+
end
|
55
|
+
```
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# Claude Desktop Integration with MCP on Ruby
|
2
|
+
|
3
|
+
This example demonstrates how to connect Claude Desktop to your Rails application using MCP on Ruby. We'll walk through the complete setup process that bridges the protocol difference between Claude Desktop (stdio) and Rails (HTTP).
|
4
|
+
|
5
|
+
## The Challenge
|
6
|
+
|
7
|
+
Claude Desktop communicates with MCP servers using **stdio** (standard input/output), but MCP on Ruby runs as an **HTTP server** within your Rails application. We need a bridge to convert between these two protocols.
|
8
|
+
|
9
|
+
## Complete Setup Guide
|
10
|
+
|
11
|
+
### Step 1: Prepare Your Rails Application
|
12
|
+
|
13
|
+
1. **Add MCP on Ruby to your Rails app:**
|
14
|
+
```bash
|
15
|
+
# In your Rails app directory
|
16
|
+
bundle add mcp_on_ruby
|
17
|
+
rails generate mcp_on_ruby:install
|
18
|
+
```
|
19
|
+
|
20
|
+
2. **Configure MCP settings:**
|
21
|
+
```ruby
|
22
|
+
# config/initializers/mcp_on_ruby.rb
|
23
|
+
McpOnRuby.configure do |config|
|
24
|
+
config.authentication_required = true
|
25
|
+
config.authentication_token = 'my-secure-token'
|
26
|
+
config.rate_limit_per_minute = 60
|
27
|
+
end
|
28
|
+
|
29
|
+
Rails.application.configure do
|
30
|
+
config.mcp.enabled = true
|
31
|
+
config.mcp.auto_register_tools = true
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
3. **Create a sample tool for testing:**
|
36
|
+
```bash
|
37
|
+
rails generate mcp_on_ruby:tool UserManager --description "Manage application users"
|
38
|
+
```
|
39
|
+
|
40
|
+
This creates `app/tools/user_manager_tool.rb`. You can customize it or use the default implementation to test the connection.
|
41
|
+
|
42
|
+
### Step 2: Set Up the Bridge Script
|
43
|
+
|
44
|
+
1. **Copy the bridge script:**
|
45
|
+
Copy `claude-bridge.js` from this directory to your preferred location (e.g., `~/mcp-bridge/claude-bridge.js`)
|
46
|
+
|
47
|
+
2. **Make it executable:**
|
48
|
+
```bash
|
49
|
+
chmod +x ~/mcp-bridge/claude-bridge.js
|
50
|
+
```
|
51
|
+
|
52
|
+
3. **Update the bridge script configuration:**
|
53
|
+
Edit the top of `claude-bridge.js` to match your setup:
|
54
|
+
```javascript
|
55
|
+
const RAILS_PORT = 3001; // Your Rails server port
|
56
|
+
const AUTH_TOKEN = 'my-secure-token'; // Must match your Rails config
|
57
|
+
```
|
58
|
+
|
59
|
+
### Step 3: Configure Claude Desktop
|
60
|
+
|
61
|
+
1. **Locate Claude Desktop config file:**
|
62
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
63
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
64
|
+
|
65
|
+
2. **Add your MCP server configuration:**
|
66
|
+
```json
|
67
|
+
{
|
68
|
+
"mcpServers": {
|
69
|
+
"my-rails-app": {
|
70
|
+
"command": "node",
|
71
|
+
"args": ["/Users/yourusername/mcp-bridge/claude-bridge.js"]
|
72
|
+
}
|
73
|
+
}
|
74
|
+
}
|
75
|
+
```
|
76
|
+
|
77
|
+
Replace `/Users/yourusername/mcp-bridge/claude-bridge.js` with the actual path to your bridge script.
|
78
|
+
|
79
|
+
### Step 4: Start Everything
|
80
|
+
|
81
|
+
1. **Start your Rails server:**
|
82
|
+
```bash
|
83
|
+
cd your-rails-app
|
84
|
+
rails server -p 3001
|
85
|
+
```
|
86
|
+
|
87
|
+
2. **Restart Claude Desktop** to load the new MCP server configuration
|
88
|
+
|
89
|
+
### Step 5: Test the Integration
|
90
|
+
|
91
|
+
1. Open Claude Desktop
|
92
|
+
2. Look for your MCP tools in the available tools (they should appear automatically)
|
93
|
+
3. Try asking Claude to use your tools:
|
94
|
+
- "Use the user manager tool to help me understand what it does"
|
95
|
+
- "What tools are available from my Rails application?"
|
96
|
+
- Test any specific functionality you've implemented in your tools
|
97
|
+
|
98
|
+
## How the Bridge Works
|
99
|
+
|
100
|
+
```
|
101
|
+
┌─────────────────┐ stdio ┌─────────────────┐ HTTP ┌─────────────────┐
|
102
|
+
│ Claude │ ←──────────→ │ Bridge │ ←─────────→ │ Rails MCP │
|
103
|
+
│ Desktop │ │ Script │ │ Server │
|
104
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
105
|
+
```
|
106
|
+
|
107
|
+
The bridge script (`claude-bridge.js`):
|
108
|
+
1. Receives JSON-RPC messages from Claude Desktop via stdin
|
109
|
+
2. Adds authentication headers (Bearer token from your configuration)
|
110
|
+
3. Forwards requests to Rails at `http://localhost:3001/mcp`
|
111
|
+
4. Returns responses to Claude Desktop via stdout
|
112
|
+
5. Handles errors and ensures proper JSON-RPC formatting
|
113
|
+
|
114
|
+
## Directory Structure
|
115
|
+
|
116
|
+
```
|
117
|
+
your-rails-app/
|
118
|
+
├── app/
|
119
|
+
│ └── tools/
|
120
|
+
│ └── user_manager_tool.rb # Your generated tool
|
121
|
+
├── config/
|
122
|
+
│ └── initializers/
|
123
|
+
│ └── mcp_on_ruby.rb # MCP configuration
|
124
|
+
└── config/routes.rb # MCP route added automatically
|
125
|
+
|
126
|
+
~/mcp-bridge/ # Or your preferred location
|
127
|
+
└── claude-bridge.js # Bridge script
|
128
|
+
```
|
129
|
+
|
130
|
+
## Troubleshooting
|
131
|
+
|
132
|
+
**Claude Desktop shows "Connection failed":**
|
133
|
+
- Ensure Rails server is running on port 3001
|
134
|
+
- Check that the bridge script path in Claude config is correct
|
135
|
+
|
136
|
+
**"Unexpected end of JSON input":**
|
137
|
+
- Verify authentication token matches exactly in both Rails config (`my-secure-token`) and bridge script (`AUTH_TOKEN`)
|
138
|
+
- Check Rails server logs for authentication errors
|
139
|
+
|
140
|
+
**Tools not appearing in Claude:**
|
141
|
+
- Restart Claude Desktop after configuration changes
|
142
|
+
- Verify tools are being auto-registered (check Rails logs)
|
143
|
+
|
144
|
+
**Bridge script not executing:**
|
145
|
+
- Ensure Node.js is installed and accessible
|
146
|
+
- Make sure the script has execute permissions
|
147
|
+
|
148
|
+
## Customizing the Setup
|
149
|
+
|
150
|
+
To modify ports or authentication:
|
151
|
+
|
152
|
+
1. **Change Rails port:**
|
153
|
+
```bash
|
154
|
+
rails server -p 3002 # Use different port
|
155
|
+
```
|
156
|
+
|
157
|
+
2. **Update bridge script:**
|
158
|
+
Edit `RAILS_PORT` in `claude-bridge.js`
|
159
|
+
|
160
|
+
3. **Change authentication token:**
|
161
|
+
Update both Rails config (`config.authentication_token`) and `AUTH_TOKEN` in bridge script
|
162
|
+
|
163
|
+
## What You Get
|
164
|
+
|
165
|
+
Once connected, Claude Desktop can:
|
166
|
+
- Discover and use any tools you create in `app/tools/`
|
167
|
+
- Access resources you define in `app/resources/`
|
168
|
+
- Interact with your Rails application's data and functionality
|
169
|
+
- Provide a natural language interface to your application's capabilities
|
170
|
+
|
171
|
+
This setup provides a secure, production-ready connection between Claude Desktop and your Rails application via MCP on Ruby.
|
@@ -0,0 +1,122 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
|
3
|
+
const http = require('http');
|
4
|
+
const { stdin, stdout, stderr } = process;
|
5
|
+
|
6
|
+
// MCP server configuration
|
7
|
+
const RAILS_HOST = 'localhost';
|
8
|
+
const RAILS_PORT = 3001;
|
9
|
+
const RAILS_PATH = '/mcp';
|
10
|
+
const AUTH_TOKEN = 'test-db-inspector-token';
|
11
|
+
|
12
|
+
// Set up stdio
|
13
|
+
stdin.setEncoding('utf8');
|
14
|
+
process.on('SIGTERM', () => process.exit(0));
|
15
|
+
process.on('SIGINT', () => process.exit(0));
|
16
|
+
|
17
|
+
let buffer = '';
|
18
|
+
|
19
|
+
stdin.on('data', (chunk) => {
|
20
|
+
buffer += chunk;
|
21
|
+
|
22
|
+
// Process complete JSON-RPC messages
|
23
|
+
const lines = buffer.split('\n');
|
24
|
+
buffer = lines.pop() || ''; // Keep incomplete line
|
25
|
+
|
26
|
+
for (const line of lines) {
|
27
|
+
if (line.trim()) {
|
28
|
+
handleRequest(line.trim());
|
29
|
+
}
|
30
|
+
}
|
31
|
+
});
|
32
|
+
|
33
|
+
stdin.on('end', () => {
|
34
|
+
if (buffer.trim()) {
|
35
|
+
handleRequest(buffer.trim());
|
36
|
+
}
|
37
|
+
});
|
38
|
+
|
39
|
+
async function handleRequest(requestLine) {
|
40
|
+
let requestId = 1; // Default ID
|
41
|
+
|
42
|
+
try {
|
43
|
+
// Parse and validate JSON-RPC request
|
44
|
+
const request = JSON.parse(requestLine);
|
45
|
+
requestId = request.id || 1; // Use request ID or default to 1
|
46
|
+
|
47
|
+
// Forward to Rails MCP server
|
48
|
+
const response = await forwardToRails(requestLine);
|
49
|
+
|
50
|
+
// Parse response to ensure it has correct ID
|
51
|
+
try {
|
52
|
+
const parsedResponse = JSON.parse(response);
|
53
|
+
if (parsedResponse.id === null || parsedResponse.id === undefined) {
|
54
|
+
parsedResponse.id = requestId;
|
55
|
+
}
|
56
|
+
stdout.write(JSON.stringify(parsedResponse) + '\n');
|
57
|
+
} catch {
|
58
|
+
// If response is not valid JSON, create proper response
|
59
|
+
const successResponse = {
|
60
|
+
jsonrpc: "2.0",
|
61
|
+
id: requestId,
|
62
|
+
result: { message: response }
|
63
|
+
};
|
64
|
+
stdout.write(JSON.stringify(successResponse) + '\n');
|
65
|
+
}
|
66
|
+
|
67
|
+
} catch (error) {
|
68
|
+
// Send JSON-RPC error response with proper ID
|
69
|
+
const errorResponse = {
|
70
|
+
jsonrpc: "2.0",
|
71
|
+
id: requestId,
|
72
|
+
error: {
|
73
|
+
code: -32700,
|
74
|
+
message: "Parse error",
|
75
|
+
data: error.message
|
76
|
+
}
|
77
|
+
};
|
78
|
+
stdout.write(JSON.stringify(errorResponse) + '\n');
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
function forwardToRails(requestData) {
|
83
|
+
return new Promise((resolve, reject) => {
|
84
|
+
const options = {
|
85
|
+
hostname: RAILS_HOST,
|
86
|
+
port: RAILS_PORT,
|
87
|
+
path: RAILS_PATH,
|
88
|
+
method: 'POST',
|
89
|
+
headers: {
|
90
|
+
'Content-Type': 'application/json',
|
91
|
+
'Authorization': `Bearer ${AUTH_TOKEN}`,
|
92
|
+
'Content-Length': Buffer.byteLength(requestData)
|
93
|
+
},
|
94
|
+
timeout: 30000
|
95
|
+
};
|
96
|
+
|
97
|
+
const req = http.request(options, (res) => {
|
98
|
+
let responseData = '';
|
99
|
+
|
100
|
+
res.on('data', (chunk) => {
|
101
|
+
responseData += chunk;
|
102
|
+
});
|
103
|
+
|
104
|
+
res.on('end', () => {
|
105
|
+
resolve(responseData);
|
106
|
+
});
|
107
|
+
});
|
108
|
+
|
109
|
+
req.on('error', (error) => {
|
110
|
+
// Return error as string to be wrapped in proper JSON-RPC format
|
111
|
+
resolve(`Connection failed: ${error.message}`);
|
112
|
+
});
|
113
|
+
|
114
|
+
req.on('timeout', () => {
|
115
|
+
req.destroy();
|
116
|
+
resolve('Request timeout');
|
117
|
+
});
|
118
|
+
|
119
|
+
req.write(requestData);
|
120
|
+
req.end();
|
121
|
+
});
|
122
|
+
}
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module McpOnRuby
|
6
|
+
# Configuration class for MCP server
|
7
|
+
class Configuration
|
8
|
+
attr_accessor :log_level,
|
9
|
+
:path,
|
10
|
+
:authentication_required,
|
11
|
+
:authentication_token,
|
12
|
+
:allowed_origins,
|
13
|
+
:localhost_only,
|
14
|
+
:rate_limit_per_minute,
|
15
|
+
:enable_sse,
|
16
|
+
:request_timeout,
|
17
|
+
:cors_enabled,
|
18
|
+
:dns_rebinding_protection
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@log_level = Logger::INFO
|
22
|
+
@path = '/mcp'
|
23
|
+
@authentication_required = false
|
24
|
+
@authentication_token = nil
|
25
|
+
@allowed_origins = []
|
26
|
+
@localhost_only = false
|
27
|
+
@rate_limit_per_minute = 60
|
28
|
+
@enable_sse = true
|
29
|
+
@request_timeout = 30
|
30
|
+
@cors_enabled = true
|
31
|
+
@dns_rebinding_protection = true
|
32
|
+
end
|
33
|
+
|
34
|
+
# Check if authentication is configured
|
35
|
+
# @return [Boolean] True if authentication is properly configured
|
36
|
+
def authentication_configured?
|
37
|
+
authentication_required && !authentication_token.nil?
|
38
|
+
end
|
39
|
+
|
40
|
+
# Check if origin is allowed
|
41
|
+
# @param origin [String] The origin to check
|
42
|
+
# @return [Boolean] True if origin is allowed
|
43
|
+
def origin_allowed?(origin)
|
44
|
+
return true if allowed_origins.empty?
|
45
|
+
|
46
|
+
allowed_origins.any? do |allowed|
|
47
|
+
case allowed
|
48
|
+
when String
|
49
|
+
origin == allowed
|
50
|
+
when Regexp
|
51
|
+
origin =~ allowed
|
52
|
+
else
|
53
|
+
false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check if localhost only mode and origin is localhost
|
59
|
+
# @param origin [String] The origin to check
|
60
|
+
# @return [Boolean] True if localhost only and origin is localhost
|
61
|
+
def localhost_allowed?(origin)
|
62
|
+
return true unless localhost_only
|
63
|
+
|
64
|
+
localhost_patterns = [
|
65
|
+
'http://localhost',
|
66
|
+
'https://localhost',
|
67
|
+
'http://127.0.0.1',
|
68
|
+
'https://127.0.0.1'
|
69
|
+
]
|
70
|
+
|
71
|
+
localhost_patterns.any? { |pattern| origin&.start_with?(pattern) }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|