otto 1.6.0 → 2.0.0.pre1
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/.github/workflows/ci.yml +1 -1
- data/.github/workflows/claude-code-review.yml +53 -0
- data/.github/workflows/claude.yml +49 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +24 -345
- data/CHANGELOG.rst +83 -0
- data/CLAUDE.md +56 -0
- data/Gemfile +10 -3
- data/Gemfile.lock +23 -28
- data/README.md +2 -0
- data/bin/rspec +4 -4
- data/changelog.d/20250911_235619_delano_next.rst +28 -0
- data/changelog.d/20250912_123055_delano_remove_ostruct.rst +21 -0
- data/changelog.d/20250912_175625_claude_delano_remove_ostruct.rst +21 -0
- data/changelog.d/README.md +120 -0
- data/changelog.d/scriv.ini +5 -0
- data/docs/.gitignore +1 -0
- data/docs/migrating/v2.0.0-pre1.md +276 -0
- data/examples/.gitignore +1 -0
- data/examples/advanced_routes/README.md +33 -0
- data/examples/advanced_routes/app/controllers/handlers/async.rb +9 -0
- data/examples/advanced_routes/app/controllers/handlers/dynamic.rb +9 -0
- data/examples/advanced_routes/app/controllers/handlers/static.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/auth.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/transformer.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/validator.rb +9 -0
- data/examples/advanced_routes/app/controllers/routes_app.rb +232 -0
- data/examples/advanced_routes/app/controllers/v2/admin.rb +9 -0
- data/examples/advanced_routes/app/controllers/v2/config.rb +9 -0
- data/examples/advanced_routes/app/controllers/v2/settings.rb +9 -0
- data/examples/advanced_routes/app/logic/admin/logic/manager.rb +27 -0
- data/examples/advanced_routes/app/logic/admin/panel.rb +27 -0
- data/examples/advanced_routes/app/logic/analytics_processor.rb +25 -0
- data/examples/advanced_routes/app/logic/complex/business/handler.rb +27 -0
- data/examples/advanced_routes/app/logic/data_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/data_processor.rb +25 -0
- data/examples/advanced_routes/app/logic/input_validator.rb +24 -0
- data/examples/advanced_routes/app/logic/nested/feature/logic.rb +27 -0
- data/examples/advanced_routes/app/logic/reports_generator.rb +27 -0
- data/examples/advanced_routes/app/logic/simple_logic.rb +25 -0
- data/examples/advanced_routes/app/logic/system/config/manager.rb +27 -0
- data/examples/advanced_routes/app/logic/test_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/transform_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/upload_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/v2/logic/dashboard.rb +27 -0
- data/examples/advanced_routes/app/logic/v2/logic/processor.rb +27 -0
- data/examples/advanced_routes/app.rb +33 -0
- data/examples/advanced_routes/config.rb +23 -0
- data/examples/advanced_routes/config.ru +7 -0
- data/examples/advanced_routes/puma.rb +20 -0
- data/examples/advanced_routes/routes +167 -0
- data/examples/advanced_routes/run.rb +39 -0
- data/examples/advanced_routes/test.rb +58 -0
- data/examples/authentication_strategies/README.md +32 -0
- data/examples/authentication_strategies/app/auth.rb +68 -0
- data/examples/authentication_strategies/app/controllers/auth_controller.rb +29 -0
- data/examples/authentication_strategies/app/controllers/main_controller.rb +28 -0
- data/examples/authentication_strategies/config.ru +24 -0
- data/examples/authentication_strategies/routes +37 -0
- data/examples/basic/README.md +29 -0
- data/examples/basic/app.rb +7 -35
- data/examples/basic/routes +0 -9
- data/examples/mcp_demo/README.md +87 -0
- data/examples/mcp_demo/app.rb +29 -34
- data/examples/mcp_demo/config.ru +9 -60
- data/examples/security_features/README.md +46 -0
- data/examples/security_features/app.rb +23 -24
- data/examples/security_features/config.ru +8 -10
- data/lib/otto/core/configuration.rb +167 -0
- data/lib/otto/core/error_handler.rb +86 -0
- data/lib/otto/core/file_safety.rb +61 -0
- data/lib/otto/core/middleware_stack.rb +157 -0
- data/lib/otto/core/router.rb +183 -0
- data/lib/otto/core/uri_generator.rb +44 -0
- data/lib/otto/design_system.rb +7 -5
- data/lib/otto/helpers/base.rb +3 -0
- data/lib/otto/helpers/request.rb +10 -8
- data/lib/otto/helpers/response.rb +5 -4
- data/lib/otto/helpers/validation.rb +9 -7
- data/lib/otto/mcp/auth/token.rb +10 -9
- data/lib/otto/mcp/protocol.rb +24 -27
- data/lib/otto/mcp/rate_limiting.rb +8 -3
- data/lib/otto/mcp/registry.rb +7 -2
- data/lib/otto/mcp/route_parser.rb +10 -15
- data/lib/otto/mcp/server.rb +21 -11
- data/lib/otto/mcp/validation.rb +14 -10
- data/lib/otto/response_handlers/auto.rb +39 -0
- data/lib/otto/response_handlers/base.rb +16 -0
- data/lib/otto/response_handlers/default.rb +16 -0
- data/lib/otto/response_handlers/factory.rb +39 -0
- data/lib/otto/response_handlers/json.rb +28 -0
- data/lib/otto/response_handlers/redirect.rb +25 -0
- data/lib/otto/response_handlers/view.rb +24 -0
- data/lib/otto/response_handlers.rb +9 -135
- data/lib/otto/route.rb +9 -9
- data/lib/otto/route_definition.rb +15 -18
- data/lib/otto/route_handlers/base.rb +121 -0
- data/lib/otto/route_handlers/class_method.rb +89 -0
- data/lib/otto/route_handlers/factory.rb +29 -0
- data/lib/otto/route_handlers/instance_method.rb +69 -0
- data/lib/otto/route_handlers/lambda.rb +59 -0
- data/lib/otto/route_handlers/logic_class.rb +93 -0
- data/lib/otto/route_handlers.rb +10 -405
- data/lib/otto/security/authentication/auth_strategy.rb +44 -0
- data/lib/otto/security/authentication/authentication_middleware.rb +123 -0
- data/lib/otto/security/authentication/failure_result.rb +36 -0
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
- data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
- data/lib/otto/security/authentication/strategies/public_strategy.rb +19 -0
- data/lib/otto/security/authentication/strategies/role_strategy.rb +57 -0
- data/lib/otto/security/authentication/strategies/session_strategy.rb +41 -0
- data/lib/otto/security/authentication/strategy_result.rb +223 -0
- data/lib/otto/security/authentication.rb +28 -282
- data/lib/otto/security/config.rb +14 -12
- data/lib/otto/security/configurator.rb +219 -0
- data/lib/otto/security/csrf.rb +8 -143
- data/lib/otto/security/middleware/csrf_middleware.rb +151 -0
- data/lib/otto/security/middleware/rate_limit_middleware.rb +38 -0
- data/lib/otto/security/middleware/validation_middleware.rb +252 -0
- data/lib/otto/security/rate_limiter.rb +86 -0
- data/lib/otto/security/rate_limiting.rb +10 -105
- data/lib/otto/security/validator.rb +8 -253
- data/lib/otto/static.rb +3 -0
- data/lib/otto/utils.rb +14 -0
- data/lib/otto/version.rb +3 -1
- data/lib/otto.rb +142 -498
- data/otto.gemspec +2 -2
- metadata +89 -28
- data/examples/dynamic_pages/app.rb +0 -115
- data/examples/dynamic_pages/config.ru +0 -30
- data/examples/dynamic_pages/routes +0 -21
- data/examples/helpers_demo/app.rb +0 -244
- data/examples/helpers_demo/config.ru +0 -26
- data/examples/helpers_demo/routes +0 -7
- data/lib/concurrent_cache_store.rb +0 -68
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'webrick'
|
5
|
+
|
6
|
+
# The shared config file returns the configured Otto app
|
7
|
+
otto_app = require_relative 'config'
|
8
|
+
|
9
|
+
# Create a simple WEBrick server
|
10
|
+
server = WEBrick::HTTPServer.new(
|
11
|
+
Port: 9292,
|
12
|
+
Logger: WEBrick::Log.new($stdout, WEBrick::Log::INFO)
|
13
|
+
)
|
14
|
+
|
15
|
+
# Mount the Otto app
|
16
|
+
server.mount '/', WEBrick::HTTPServlet::ProcHandler.new(proc { |req, res|
|
17
|
+
env = req.meta_vars.merge({
|
18
|
+
'REQUEST_METHOD' => req.request_method,
|
19
|
+
'PATH_INFO' => req.path_info,
|
20
|
+
'QUERY_STRING' => req.query_string || '',
|
21
|
+
'rack.input' => StringIO.new(req.body || ''),
|
22
|
+
'CONTENT_TYPE' => req.content_type,
|
23
|
+
'CONTENT_LENGTH' => req.content_length&.to_s
|
24
|
+
})
|
25
|
+
|
26
|
+
status, headers, body = otto_app.call(env)
|
27
|
+
|
28
|
+
res.status = status
|
29
|
+
headers.each { |k, v| res[k] = v }
|
30
|
+
body.each { |chunk| res.body << chunk }
|
31
|
+
})
|
32
|
+
|
33
|
+
# Handle Ctrl+C gracefully
|
34
|
+
trap('INT') { server.shutdown }
|
35
|
+
|
36
|
+
puts "Otto Advanced Routes Example running on http://localhost:9292"
|
37
|
+
puts "Press Ctrl+C to stop"
|
38
|
+
|
39
|
+
server.start
|
@@ -0,0 +1,58 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# The shared config file returns the configured Otto app
|
5
|
+
otto_app = require_relative 'config'
|
6
|
+
|
7
|
+
# Simple test runner to demonstrate routes without a server
|
8
|
+
def test_route(method, path, params = {})
|
9
|
+
query_string = params.map { |k, v| "#{k}=#{v}" }.join('&')
|
10
|
+
|
11
|
+
env = {
|
12
|
+
'REQUEST_METHOD' => method.to_s.upcase,
|
13
|
+
'PATH_INFO' => path,
|
14
|
+
'QUERY_STRING' => query_string,
|
15
|
+
'rack.input' => StringIO.new(''),
|
16
|
+
'HTTP_ACCEPT' => 'application/json',
|
17
|
+
}
|
18
|
+
|
19
|
+
status, headers, body = otto_app.call(env)
|
20
|
+
|
21
|
+
puts "#{method.to_s.upcase} #{path}#{query_string.empty? ? '' : '?' + query_string}"
|
22
|
+
puts "Status: #{status}"
|
23
|
+
puts "Content-Type: #{headers['content-type'] || headers['Content-Type']}"
|
24
|
+
puts "Body: #{body.join}"
|
25
|
+
puts "---"
|
26
|
+
end
|
27
|
+
|
28
|
+
puts "Otto Advanced Routes Syntax Test"
|
29
|
+
puts "================================"
|
30
|
+
|
31
|
+
# Test basic routes
|
32
|
+
test_route(:get, '/')
|
33
|
+
test_route(:post, '/feedback')
|
34
|
+
|
35
|
+
# Test JSON routes
|
36
|
+
test_route(:get, '/api/users')
|
37
|
+
test_route(:get, '/api/health')
|
38
|
+
|
39
|
+
# Test Logic classes
|
40
|
+
test_route(:get, '/logic/simple')
|
41
|
+
test_route(:post, '/logic/process')
|
42
|
+
|
43
|
+
# Test namespaced Logic classes
|
44
|
+
test_route(:get, '/logic/admin')
|
45
|
+
test_route(:get, '/logic/v2/dashboard')
|
46
|
+
|
47
|
+
# Test CSRF exempt routes
|
48
|
+
test_route(:post, '/api/webhook')
|
49
|
+
test_route(:put, '/api/external')
|
50
|
+
|
51
|
+
# Test custom parameters
|
52
|
+
test_route(:get, '/config/env')
|
53
|
+
test_route(:get, '/api/v1')
|
54
|
+
|
55
|
+
# Test complex routes
|
56
|
+
test_route(:post, '/test/everything')
|
57
|
+
|
58
|
+
puts "All tests completed!"
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Otto - Authentication Strategies Example
|
2
|
+
|
3
|
+
This example demonstrates how to use Otto's powerful authentication features.
|
4
|
+
|
5
|
+
## Structure
|
6
|
+
|
7
|
+
* `config.ru`: The Rackup file. It initializes Otto and loads the authentication strategies.
|
8
|
+
* `routes`: Defines the application's routes and the authentication required for each.
|
9
|
+
* `app/auth.rb`: Contains the definitions for all authentication strategies. This is where you would add your own.
|
10
|
+
* `app/controllers/`: Contains the controller classes that handle requests.
|
11
|
+
|
12
|
+
## Running the Demo
|
13
|
+
|
14
|
+
1. Make sure you have the necessary gems installed (`bundle install`).
|
15
|
+
2. Run the application from the root of the `otto` project:
|
16
|
+
|
17
|
+
```sh
|
18
|
+
rackup examples/authentication_strategies/config.ru
|
19
|
+
```
|
20
|
+
|
21
|
+
3. Open your browser and navigate to `http://localhost:9292`.
|
22
|
+
|
23
|
+
## Trying the Authentication Strategies
|
24
|
+
|
25
|
+
You can test the different authentication strategies by providing a `token` or `api_key` parameter in the URL.
|
26
|
+
|
27
|
+
* **Authenticated User:** [http://localhost:9292/profile?token=demo_token](http://localhost:9292/profile?token=demo_token)
|
28
|
+
* **Admin User:** [http://localhost:9292/admin?token=admin_token](http://localhost:9292/admin?token=admin_token)
|
29
|
+
* **User with 'write' permission:** [http://localhost:9292/edit?token=demo_token](http://localhost:9292/edit?token=demo_token)
|
30
|
+
* **API Key:** [http://localhost:9292/api/data?api_key=demo_api_key_123](http://localhost:9292/api/data?api_key=demo_api_key_123)
|
31
|
+
|
32
|
+
If you try to access a protected route without the correct token, you'll get an authentication error.
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Authentication result data class to contain the session, user and anything
|
4
|
+
# else we want to make available to the route handlers/controllers/logic classes.
|
5
|
+
AuthResultData = Data.define(:session, :user) do
|
6
|
+
def initialize(session: {}, user: {})
|
7
|
+
super(session: session, user: user)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Provide user_context method for compatibility with existing AuthResult
|
11
|
+
def user_context
|
12
|
+
{ session: session, user: user }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module AuthenticationSetup
|
17
|
+
def self.configure(otto)
|
18
|
+
# Simple auth strategy that checks for a token parameter or header
|
19
|
+
otto.add_auth_strategy('authenticated', lambda do |req|
|
20
|
+
token = req.params['token'] || req.get_header('HTTP_AUTHORIZATION')
|
21
|
+
if token == 'demo_token'
|
22
|
+
AuthResultData.new(
|
23
|
+
session: { session_id: 'demo_session_123', user_id: 1 },
|
24
|
+
user: { name: 'Demo User', role: 'user', permissions: %w[read write] }
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end)
|
28
|
+
|
29
|
+
# Role-based auth strategy
|
30
|
+
otto.add_auth_strategy('role:admin', lambda do |req|
|
31
|
+
token = req.params['token'] || req.get_header('HTTP_AUTHORIZATION')
|
32
|
+
if token == 'admin_token'
|
33
|
+
AuthResultData.new(
|
34
|
+
session: { session_id: 'admin_session_456', user_id: 2 },
|
35
|
+
user: { name: 'Admin User', role: 'admin', permissions: %w[read write admin delete] }
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end)
|
39
|
+
|
40
|
+
# Permission-based auth strategy
|
41
|
+
otto.add_auth_strategy('permission:write', lambda do |req|
|
42
|
+
token = req.params['token'] || req.get_header('HTTP_AUTHORIZATION')
|
43
|
+
case token
|
44
|
+
when 'demo_token'
|
45
|
+
AuthResultData.new(
|
46
|
+
session: { session_id: 'demo_session_123', user_id: 1 },
|
47
|
+
user: { name: 'Demo User', role: 'user', permissions: %w[read write] }
|
48
|
+
)
|
49
|
+
when 'admin_token'
|
50
|
+
AuthResultData.new(
|
51
|
+
session: { session_id: 'admin_session_456', user_id: 2 },
|
52
|
+
user: { name: 'Admin User', role: 'admin', permissions: %w[read write admin delete] }
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end)
|
56
|
+
|
57
|
+
# API key auth strategy
|
58
|
+
otto.add_auth_strategy('api_key', lambda do |req|
|
59
|
+
api_key = req.params['api_key'] || req.get_header('HTTP_X_API_KEY')
|
60
|
+
if api_key == 'demo_api_key_123'
|
61
|
+
AuthResultData.new(
|
62
|
+
session: { api_session: 'api_session_abc' },
|
63
|
+
user: { name: 'API Client', type: 'api', permissions: ['api_access'] }
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end)
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class AuthController
|
4
|
+
def self.login_form
|
5
|
+
[200, { 'content-type' => 'text/html' }, ['<h1>Login</h1><p>Use ?token=demo_token, ?token=admin_token, or ?api_key=demo_api_key_123</p>']]
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.login
|
9
|
+
# In a real app, you'd handle login logic here.
|
10
|
+
# For this demo, we redirect to the profile.
|
11
|
+
[302, { 'location' => '/profile' }, ['']]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.show_profile
|
15
|
+
[200, { 'content-type' => 'text/html' }, ['<h1>User Profile</h1><p>You are authenticated.</p>']]
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.admin_panel
|
19
|
+
[200, { 'content-type' => 'text/html' }, ['<h1>Admin Panel</h1><p>Role: admin required</p>']]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.edit_content
|
23
|
+
[200, { 'content-type' => 'text/html' }, ['<h1>Edit Content</h1><p>Permission: write required</p>']]
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.api_data
|
27
|
+
[200, { 'content-type' => 'application/json' }, ['{"data": "This is some secret API data."}']]
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
class MainController
|
5
|
+
def self.index
|
6
|
+
[200, { 'content-type' => 'text/html' }, ['<h1>Authentication Strategies Example</h1>']]
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.receive_feedback
|
10
|
+
[200, { 'content-type' => 'text/plain' }, ['Feedback received']]
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.dashboard
|
14
|
+
[200, { 'content-type' => 'text/html' }, ['<h1>Dashboard</h1><p>Welcome to your dashboard!</p>']]
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.reports
|
18
|
+
[200, { 'content-type' => 'text/html' }, ['<h1>Reports</h1><p>Admin-only reports section</p>']]
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.not_found
|
22
|
+
[404, { 'content-type' => 'text/html' }, ['<h1>404 - Page Not Found</h1><p>Advanced routes example</p>']]
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.server_error
|
26
|
+
[500, { 'content-type' => 'text/html' }, ['<h1>500 - Server Error</h1><p>Something went wrong</p>']]
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
require_relative '../../lib/otto'
|
5
|
+
require_relative 'app/auth'
|
6
|
+
require_relative 'app/controllers/main_controller'
|
7
|
+
require_relative 'app/controllers/auth_controller'
|
8
|
+
|
9
|
+
# Configure Otto with advanced features
|
10
|
+
otto = Otto.new('routes')
|
11
|
+
|
12
|
+
# Enable security features to demonstrate advanced route parameters
|
13
|
+
otto.enable_csrf_protection!
|
14
|
+
otto.enable_request_validation!
|
15
|
+
otto.enable_authentication!
|
16
|
+
|
17
|
+
# Load and configure authentication strategies
|
18
|
+
AuthenticationSetup.configure(otto)
|
19
|
+
|
20
|
+
# Set error handlers
|
21
|
+
otto.not_found = ->(_env) { MainController.not_found }
|
22
|
+
otto.server_error = ->(_env, _error) { MainController.server_error }
|
23
|
+
|
24
|
+
run otto
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Authentication Strategies Example
|
2
|
+
|
3
|
+
# ========================================
|
4
|
+
# PUBLIC ROUTES
|
5
|
+
# ========================================
|
6
|
+
|
7
|
+
GET / MainController#index
|
8
|
+
GET /login AuthController#login_form
|
9
|
+
POST /login AuthController#login
|
10
|
+
|
11
|
+
# ========================================
|
12
|
+
# AUTHENTICATED ROUTES
|
13
|
+
# ========================================
|
14
|
+
|
15
|
+
# Requires a user to be logged in.
|
16
|
+
# Try: /profile?token=demo_token
|
17
|
+
GET /profile AuthController#show_profile auth=authenticated
|
18
|
+
|
19
|
+
# Requires the 'admin' role.
|
20
|
+
# Try: /admin?token=admin_token
|
21
|
+
GET /admin AuthController#admin_panel auth=role:admin
|
22
|
+
|
23
|
+
# Requires the 'write' permission.
|
24
|
+
# A user with the 'user' role has this.
|
25
|
+
# Try: /edit?token=demo_token
|
26
|
+
GET /edit AuthController#edit_content auth=permission:write
|
27
|
+
|
28
|
+
# A route protected by an API key
|
29
|
+
# Try: /api/data?api_key=demo_api_key_123
|
30
|
+
GET /api/data AuthController#api_data auth=api_key response=json
|
31
|
+
|
32
|
+
# ========================================
|
33
|
+
# ERROR HANDLERS
|
34
|
+
# ========================================
|
35
|
+
|
36
|
+
GET /404 MainController#not_found response=view
|
37
|
+
GET /500 MainController#server_error response=view
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Otto - Basic Example
|
2
|
+
|
3
|
+
This example demonstrates a basic Otto application with a single route that accepts both GET and POST requests.
|
4
|
+
|
5
|
+
## How to Run
|
6
|
+
|
7
|
+
1. Make sure you have `bundler` and `thin` installed:
|
8
|
+
```sh
|
9
|
+
gem install bundler thin
|
10
|
+
```
|
11
|
+
|
12
|
+
2. Install the dependencies from the root of the project:
|
13
|
+
```sh
|
14
|
+
bundle install
|
15
|
+
```
|
16
|
+
|
17
|
+
3. Start the server from this directory (`examples/basic`):
|
18
|
+
```sh
|
19
|
+
thin -e dev -R config.ru -p 10770 start
|
20
|
+
```
|
21
|
+
|
22
|
+
4. Open your browser and navigate to `http://localhost:10770`.
|
23
|
+
|
24
|
+
## File Structure
|
25
|
+
|
26
|
+
* `README.md`: This file.
|
27
|
+
* `app.rb`: Contains the application logic. It has two methods: `index` to display the main page and `receive_feedback` to handle form submissions.
|
28
|
+
* `config.ru`: The Rack configuration file that loads the Otto framework and the application.
|
29
|
+
* `routes`: Defines the URL routes and maps them to methods in the `App` class.
|
data/examples/basic/app.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
# examples/basic/app.rb
|
1
|
+
# examples/basic/app.rb
|
2
2
|
|
3
3
|
require_relative '../../lib/otto/design_system'
|
4
4
|
|
5
|
+
# Basic example application demonstrating Otto framework features.
|
5
6
|
class App
|
6
7
|
include Otto::DesignSystem
|
7
8
|
|
@@ -37,42 +38,13 @@ class App
|
|
37
38
|
message = req.params['msg']&.strip
|
38
39
|
|
39
40
|
content = if message.nil? || message.empty?
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
41
|
+
otto_alert('error', 'Empty Message', 'Please enter a message before submitting.')
|
42
|
+
else
|
43
|
+
otto_alert('success', 'Feedback Received', 'Thanks for your message!') +
|
44
|
+
otto_card('Your Message') { otto_code_block(message, 'text') }
|
45
|
+
end
|
45
46
|
|
46
47
|
content += "<p>#{otto_link('← Back', '/')}</p>"
|
47
48
|
res.body = otto_page(content, 'Feedback')
|
48
49
|
end
|
49
|
-
|
50
|
-
def redirect
|
51
|
-
res.redirect '/robots.txt'
|
52
|
-
end
|
53
|
-
|
54
|
-
def robots_text
|
55
|
-
res.headers['content-type'] = 'text/plain'
|
56
|
-
res.body = ['User-agent: *', 'Disallow: /private'].join($/)
|
57
|
-
end
|
58
|
-
|
59
|
-
def display_product
|
60
|
-
res.headers['content-type'] = 'application/json; charset=utf-8'
|
61
|
-
prodid = req.params[:prodid]
|
62
|
-
res.body = format('{"product":%s,"msg":"Hint: try another value"}', prodid)
|
63
|
-
end
|
64
|
-
|
65
|
-
def not_found
|
66
|
-
res.status = 404
|
67
|
-
content = otto_alert('error', 'Not Found', 'The requested page could not be found.')
|
68
|
-
content += "<p>#{otto_link('← Home', '/')}</p>"
|
69
|
-
res.body = otto_page(content, '404')
|
70
|
-
end
|
71
|
-
|
72
|
-
def server_error
|
73
|
-
res.status = 500
|
74
|
-
content = otto_alert('error', 'Server Error', 'An internal server error occurred.')
|
75
|
-
content += "<p>#{otto_link('← Home', '/')}</p>"
|
76
|
-
res.body = otto_page(content, '500')
|
77
|
-
end
|
78
50
|
end
|
data/examples/basic/routes
CHANGED
@@ -9,12 +9,3 @@
|
|
9
9
|
|
10
10
|
GET / App#index
|
11
11
|
POST / App#receive_feedback
|
12
|
-
GET /redirect App#redirect
|
13
|
-
GET /robots.txt App#robots_text
|
14
|
-
|
15
|
-
GET /bogus App#no_such_method
|
16
|
-
|
17
|
-
# You can also define these handlers when no
|
18
|
-
# route can be found or there's a server error. (optional)
|
19
|
-
GET /404 App#not_found
|
20
|
-
GET /500 App#server_error
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# Otto MCP Demo
|
2
|
+
|
3
|
+
This example demonstrates Otto's Model-Controller-Protocol (MCP) feature. MCP provides a standardized JSON-RPC 2.0 endpoint (`/_mcp`) that allows you to expose application resources and tools securely.
|
4
|
+
|
5
|
+
This is useful for building CLIs, admin interfaces, or allowing other services to interact with your application programmatically.
|
6
|
+
|
7
|
+
## Features Demonstrated
|
8
|
+
|
9
|
+
* **MCP Endpoint:** A single `POST /_mcp` endpoint for all API interactions.
|
10
|
+
* **Authentication:** Requests to the MCP endpoint are protected by bearer token authentication.
|
11
|
+
* **Rate Limiting:** The endpoint has its own rate limiting to prevent abuse.
|
12
|
+
* **Resources:** Read-only data exposed via `MCP` routes (e.g., listing users).
|
13
|
+
* **Tools:** Actions or operations exposed via `TOOL` routes (e.g., creating a user).
|
14
|
+
|
15
|
+
## How to Run
|
16
|
+
|
17
|
+
1. Make sure you have `bundler` and `thin` installed:
|
18
|
+
```sh
|
19
|
+
gem install bundler thin
|
20
|
+
```
|
21
|
+
|
22
|
+
2. Install the dependencies from the root of the project:
|
23
|
+
```sh
|
24
|
+
bundle install
|
25
|
+
```
|
26
|
+
|
27
|
+
3. Start the server from this directory (`examples/mcp_demo`):
|
28
|
+
```sh
|
29
|
+
thin -R config.ru -p 9292 start
|
30
|
+
```
|
31
|
+
*Note: This demo uses port 9292 as is conventional for Rack apps.*
|
32
|
+
|
33
|
+
4. Open your browser and navigate to `http://localhost:9292` to see the welcome page.
|
34
|
+
|
35
|
+
## Interacting with the MCP Endpoint
|
36
|
+
|
37
|
+
All interactions happen via `POST` requests to `http://localhost:9292/_mcp`. You must provide an `Authorization: Bearer <token>` header and a `Content-Type: application/json` header.
|
38
|
+
|
39
|
+
Valid tokens are `demo-token-123` and `another-token-456`.
|
40
|
+
|
41
|
+
### MCP: Initialize
|
42
|
+
|
43
|
+
The `initialize` method is a built-in MCP method that returns information about the available resources and tools.
|
44
|
+
|
45
|
+
```sh
|
46
|
+
curl -X POST http://localhost:9292/_mcp \
|
47
|
+
-H 'Authorization: Bearer demo-token-123' \
|
48
|
+
-H 'Content-Type: application/json' \
|
49
|
+
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}'
|
50
|
+
```
|
51
|
+
|
52
|
+
### Resource: List Users
|
53
|
+
|
54
|
+
This calls the `UserAPI.mcp_list_users` method defined as an `MCP` route. The method name for the JSON-RPC call is derived from the route path (`/users` -> `users/list`).
|
55
|
+
|
56
|
+
```sh
|
57
|
+
curl -X POST http://localhost:9292/_mcp \
|
58
|
+
-H 'Authorization: Bearer demo-token-123' \
|
59
|
+
-H 'Content-Type: application/json' \
|
60
|
+
-d '{"jsonrpc":"2.0","method":"users/list","id":2}'
|
61
|
+
```
|
62
|
+
|
63
|
+
### Tool: Create User
|
64
|
+
|
65
|
+
This calls the `UserAPI.mcp_create_user` method defined as a `TOOL` route. The method name is derived from the route path (`/create_user` -> `create_user`).
|
66
|
+
|
67
|
+
```sh
|
68
|
+
curl -X POST http://localhost:9292/_mcp \
|
69
|
+
-H 'Authorization: Bearer demo-token-123' \
|
70
|
+
-H 'Content-Type: application/json' \
|
71
|
+
-d '{
|
72
|
+
"jsonrpc": "2.0",
|
73
|
+
"method": "create_user",
|
74
|
+
"id": 3,
|
75
|
+
"params": {
|
76
|
+
"name": "Charlie",
|
77
|
+
"email": "charlie@example.com"
|
78
|
+
}
|
79
|
+
}'
|
80
|
+
```
|
81
|
+
|
82
|
+
## File Structure
|
83
|
+
|
84
|
+
* `README.md`: This file.
|
85
|
+
* `app.rb`: Contains the application logic, including the `DemoApp` for web pages and the `UserAPI` for MCP handlers.
|
86
|
+
* `config.ru`: The Rack configuration file. It loads the Otto framework, enables MCP, and runs the application.
|
87
|
+
* `routes`: Defines the standard web routes as well as the `MCP` and `TOOL` routes for the MCP endpoint.
|
data/examples/mcp_demo/app.rb
CHANGED
@@ -1,22 +1,41 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
# examples/mcp_demo/app.rb
|
2
|
+
require 'json'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
# DemoApp provides basic HTML pages for the demo.
|
6
|
+
class DemoApp
|
7
|
+
def self.index(_req, res)
|
8
|
+
res.headers['content-type'] = 'text/html; charset=utf-8'
|
9
|
+
res.body = <<~HTML
|
10
|
+
<h1>Otto MCP Demo</h1>
|
11
|
+
<p>This example demonstrates Otto's Model-Controller-Protocol (MCP) feature, which provides a JSON-RPC 2.0 endpoint for interacting with your application.</p>
|
12
|
+
<p>The MCP endpoint is available at: <code>POST /_mcp</code></p>
|
13
|
+
<p>See the <code>README.md</code> file for detailed `curl` commands to test the API.</p>
|
14
|
+
HTML
|
15
|
+
end
|
5
16
|
|
6
|
-
|
17
|
+
def self.health(_req, res)
|
18
|
+
res.headers['content-type'] = 'text/plain'
|
19
|
+
res.body = 'OK'
|
20
|
+
end
|
21
|
+
end
|
7
22
|
|
23
|
+
# UserAPI provides handlers for the MCP tool and resource routes.
|
8
24
|
class UserAPI
|
25
|
+
# MCP Resource: mcp_list_users
|
26
|
+
# Accessible via JSON-RPC method "users/list"
|
9
27
|
def self.mcp_list_users
|
10
28
|
{
|
11
29
|
users: [
|
12
30
|
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
13
|
-
{ id: 2, name: 'Bob', email: 'bob@example.com' }
|
14
|
-
]
|
31
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
|
32
|
+
],
|
15
33
|
}.to_json
|
16
34
|
end
|
17
35
|
|
18
|
-
|
19
|
-
|
36
|
+
# MCP Tool: mcp_create_user
|
37
|
+
# Accessible via JSON-RPC method "create_user"
|
38
|
+
def self.mcp_create_user(arguments, _env)
|
20
39
|
name = arguments['name'] || 'Anonymous'
|
21
40
|
email = arguments['email'] || "#{name.downcase}@example.com"
|
22
41
|
|
@@ -24,33 +43,9 @@ class UserAPI
|
|
24
43
|
id: rand(1000..9999),
|
25
44
|
name: name,
|
26
45
|
email: email,
|
27
|
-
created_at: Time.now.iso8601
|
46
|
+
created_at: Time.now.iso8601,
|
28
47
|
}
|
29
48
|
|
30
49
|
"Created user: #{new_user.to_json}"
|
31
50
|
end
|
32
51
|
end
|
33
|
-
|
34
|
-
# Initialize Otto with MCP support
|
35
|
-
otto = Otto.new('routes', {
|
36
|
-
mcp_enabled: true,
|
37
|
-
auth_tokens: ['demo-token-123'], # Simple token auth
|
38
|
-
requests_per_minute: 10, # Lower for demo
|
39
|
-
tools_per_minute: 5
|
40
|
-
})
|
41
|
-
|
42
|
-
# Enable MCP with authentication tokens
|
43
|
-
otto.enable_mcp!({
|
44
|
-
auth_tokens: ['demo-token-123', 'another-token-456'],
|
45
|
-
enable_validation: true,
|
46
|
-
enable_rate_limiting: true
|
47
|
-
})
|
48
|
-
|
49
|
-
puts "Otto MCP Demo Server starting..."
|
50
|
-
puts "MCP endpoint: POST /_mcp"
|
51
|
-
puts "Auth tokens: demo-token-123, another-token-456"
|
52
|
-
puts "Usage: curl -H 'Authorization: Bearer demo-token-123' -H 'Content-Type: application/json' \\"
|
53
|
-
puts " -d '{\"jsonrpc\":\"2.0\",\"method\":\"initialize\",\"id\":1,\"params\":{}}' \\"
|
54
|
-
puts " http://localhost:9292/_mcp"
|
55
|
-
|
56
|
-
otto
|
data/examples/mcp_demo/config.ru
CHANGED
@@ -1,68 +1,17 @@
|
|
1
|
-
|
1
|
+
# examples/mcp_demo/config.ru
|
2
2
|
|
3
3
|
require_relative '../../lib/otto'
|
4
|
-
|
5
|
-
class DemoApp
|
6
|
-
def self.index(req, res)
|
7
|
-
res.body = <<-HTML
|
8
|
-
<h1>Otto MCP Demo</h1>
|
9
|
-
<p>MCP endpoint available at: <code>POST /_mcp</code></p>
|
10
|
-
<p>Auth tokens: <code>demo-token-123</code>, <code>another-token-456</code></p>
|
11
|
-
|
12
|
-
<h2>Test MCP Initialize</h2>
|
13
|
-
<pre>curl -H 'Authorization: Bearer demo-token-123' -H 'Content-Type: application/json' \\
|
14
|
-
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}' \\
|
15
|
-
http://localhost:9292/_mcp</pre>
|
16
|
-
|
17
|
-
<h2>List Resources</h2>
|
18
|
-
<pre>curl -H 'Authorization: Bearer demo-token-123' -H 'Content-Type: application/json' \\
|
19
|
-
-d '{"jsonrpc":"2.0","method":"resources/list","id":2}' \\
|
20
|
-
http://localhost:9292/_mcp</pre>
|
21
|
-
|
22
|
-
<h2>List Tools</h2>
|
23
|
-
<pre>curl -H 'Authorization: Bearer demo-token-123' -H 'Content-Type: application/json' \\
|
24
|
-
-d '{"jsonrpc":"2.0","method":"tools/list","id":3}' \\
|
25
|
-
http://localhost:9292/_mcp</pre>
|
26
|
-
HTML
|
27
|
-
end
|
28
|
-
|
29
|
-
def self.health(req, res)
|
30
|
-
res.body = 'OK'
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
class UserAPI
|
35
|
-
def self.mcp_list_users
|
36
|
-
{
|
37
|
-
users: [
|
38
|
-
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
39
|
-
{ id: 2, name: 'Bob', email: 'bob@example.com' }
|
40
|
-
]
|
41
|
-
}.to_json
|
42
|
-
end
|
43
|
-
|
44
|
-
def self.mcp_create_user(arguments, env)
|
45
|
-
# Tool handler that creates a user
|
46
|
-
name = arguments['name'] || 'Anonymous'
|
47
|
-
email = arguments['email'] || "#{name.downcase}@example.com"
|
48
|
-
|
49
|
-
new_user = {
|
50
|
-
id: rand(1000..9999),
|
51
|
-
name: name,
|
52
|
-
email: email,
|
53
|
-
created_at: Time.now.iso8601
|
54
|
-
}
|
55
|
-
|
56
|
-
"Created user: #{new_user.to_json}"
|
57
|
-
end
|
58
|
-
end
|
4
|
+
require_relative 'app'
|
59
5
|
|
60
6
|
# Initialize Otto with MCP support
|
61
|
-
|
7
|
+
app = Otto.new('routes', {
|
62
8
|
mcp_enabled: true,
|
63
9
|
auth_tokens: ['demo-token-123', 'another-token-456'],
|
64
|
-
requests_per_minute: 60,
|
65
|
-
tools_per_minute: 20
|
10
|
+
requests_per_minute: 60, # Rate limiting for the MCP endpoint
|
11
|
+
tools_per_minute: 20,
|
66
12
|
})
|
67
13
|
|
68
|
-
|
14
|
+
# The `mcp_enabled: true` flag automatically sets up the /_mcp endpoint.
|
15
|
+
# The routes file maps MCP and TOOL methods to classes.
|
16
|
+
|
17
|
+
run app
|