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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -28
  3. data/CODE_OF_CONDUCT.md +30 -58
  4. data/CONTRIBUTING.md +61 -67
  5. data/LICENSE.txt +2 -2
  6. data/README.md +159 -509
  7. data/bin/console +11 -0
  8. data/bin/setup +6 -0
  9. data/docs/advanced-usage.md +132 -0
  10. data/docs/api-reference.md +35 -0
  11. data/docs/testing.md +55 -0
  12. data/examples/claude/README.md +171 -0
  13. data/examples/claude/claude-bridge.js +122 -0
  14. data/lib/mcp_on_ruby/configuration.rb +74 -0
  15. data/lib/mcp_on_ruby/errors.rb +137 -0
  16. data/lib/mcp_on_ruby/generators/install_generator.rb +46 -0
  17. data/lib/mcp_on_ruby/generators/resource_generator.rb +63 -0
  18. data/lib/mcp_on_ruby/generators/templates/README +31 -0
  19. data/lib/mcp_on_ruby/generators/templates/application_resource.rb +20 -0
  20. data/lib/mcp_on_ruby/generators/templates/application_tool.rb +18 -0
  21. data/lib/mcp_on_ruby/generators/templates/initializer.rb +41 -0
  22. data/lib/mcp_on_ruby/generators/templates/resource.rb +50 -0
  23. data/lib/mcp_on_ruby/generators/templates/resource_spec.rb +67 -0
  24. data/lib/mcp_on_ruby/generators/templates/sample_resource.rb +57 -0
  25. data/lib/mcp_on_ruby/generators/templates/sample_tool.rb +59 -0
  26. data/lib/mcp_on_ruby/generators/templates/tool.rb +38 -0
  27. data/lib/mcp_on_ruby/generators/templates/tool_spec.rb +55 -0
  28. data/lib/mcp_on_ruby/generators/tool_generator.rb +51 -0
  29. data/lib/mcp_on_ruby/railtie.rb +108 -0
  30. data/lib/mcp_on_ruby/resource.rb +161 -0
  31. data/lib/mcp_on_ruby/server.rb +378 -0
  32. data/lib/mcp_on_ruby/tool.rb +134 -0
  33. data/lib/mcp_on_ruby/transport.rb +330 -0
  34. data/lib/mcp_on_ruby/version.rb +6 -0
  35. data/lib/mcp_on_ruby.rb +142 -0
  36. metadata +62 -173
  37. data/lib/ruby_mcp/client.rb +0 -43
  38. data/lib/ruby_mcp/configuration.rb +0 -90
  39. data/lib/ruby_mcp/errors.rb +0 -17
  40. data/lib/ruby_mcp/models/context.rb +0 -52
  41. data/lib/ruby_mcp/models/engine.rb +0 -31
  42. data/lib/ruby_mcp/models/message.rb +0 -60
  43. data/lib/ruby_mcp/providers/anthropic.rb +0 -269
  44. data/lib/ruby_mcp/providers/base.rb +0 -57
  45. data/lib/ruby_mcp/providers/openai.rb +0 -265
  46. data/lib/ruby_mcp/schemas.rb +0 -56
  47. data/lib/ruby_mcp/server/app.rb +0 -84
  48. data/lib/ruby_mcp/server/base_controller.rb +0 -49
  49. data/lib/ruby_mcp/server/content_controller.rb +0 -68
  50. data/lib/ruby_mcp/server/contexts_controller.rb +0 -67
  51. data/lib/ruby_mcp/server/controller.rb +0 -29
  52. data/lib/ruby_mcp/server/engines_controller.rb +0 -34
  53. data/lib/ruby_mcp/server/generate_controller.rb +0 -140
  54. data/lib/ruby_mcp/server/messages_controller.rb +0 -30
  55. data/lib/ruby_mcp/server/router.rb +0 -84
  56. data/lib/ruby_mcp/storage/active_record.rb +0 -414
  57. data/lib/ruby_mcp/storage/base.rb +0 -43
  58. data/lib/ruby_mcp/storage/error.rb +0 -8
  59. data/lib/ruby_mcp/storage/memory.rb +0 -69
  60. data/lib/ruby_mcp/storage/redis.rb +0 -197
  61. data/lib/ruby_mcp/storage_factory.rb +0 -43
  62. data/lib/ruby_mcp/validator.rb +0 -45
  63. data/lib/ruby_mcp/version.rb +0 -6
  64. 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,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -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