rolemodel-rails 0.26.0 → 1.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 +4 -4
- data/README.md +15 -4
- data/lib/generators/rolemodel/all_generator.rb +2 -1
- data/lib/generators/rolemodel/editors/editors_generator.rb +1 -1
- data/lib/generators/rolemodel/github/README.md +14 -17
- data/lib/generators/rolemodel/github/USAGE +4 -1
- data/lib/generators/rolemodel/github/github_generator.rb +24 -18
- data/lib/generators/rolemodel/github/templates/CODEOWNERS +8 -0
- data/lib/generators/rolemodel/github/templates/dependabot.yml +87 -0
- data/lib/generators/rolemodel/github/templates/instructions +1 -0
- data/lib/generators/rolemodel/github/templates/pull_request_template.md +18 -0
- data/lib/generators/rolemodel/github/templates/workflows/ci.yml.tt +81 -0
- data/lib/generators/rolemodel/good_job/good_job_generator.rb +1 -1
- data/lib/generators/rolemodel/heroku/heroku_generator.rb +1 -1
- data/lib/generators/rolemodel/kaminari/kaminari_generator.rb +1 -1
- data/lib/generators/rolemodel/linters/all_generator.rb +1 -1
- data/lib/generators/rolemodel/linters/eslint/eslint_generator.rb +1 -1
- data/lib/generators/rolemodel/linters/rubocop/rubocop_generator.rb +1 -1
- data/lib/generators/rolemodel/lograge/lograge_generator.rb +1 -1
- data/lib/generators/rolemodel/mailers/mailers_generator.rb +1 -1
- data/lib/generators/rolemodel/mcp/README.md +13 -0
- data/lib/generators/rolemodel/mcp/USAGE +8 -0
- data/lib/generators/rolemodel/mcp/mcp_generator.rb +110 -0
- data/lib/generators/rolemodel/mcp/templates/app/assets/stylesheets/components/doorkeeper.css +140 -0
- data/lib/generators/rolemodel/mcp/templates/app/controllers/doorkeeper/base_controller.rb +7 -0
- data/lib/generators/rolemodel/mcp/templates/app/controllers/mcp_controller.rb.tt +91 -0
- data/lib/generators/rolemodel/mcp/templates/app/controllers/oauth_registrations_controller.rb +46 -0
- data/lib/generators/rolemodel/mcp/templates/app/controllers/well_known_controller.rb +39 -0
- data/lib/generators/rolemodel/mcp/templates/app/mcp/prompts/sample.rb +36 -0
- data/lib/generators/rolemodel/mcp/templates/app/mcp/resources/controller.rb +57 -0
- data/lib/generators/rolemodel/mcp/templates/app/mcp/resources/docs/SAMPLE_DOC.md +4 -0
- data/lib/generators/rolemodel/mcp/templates/app/mcp/resources/docs_controller.rb +46 -0
- data/lib/generators/rolemodel/mcp/templates/app/mcp/tools/sample.rb +42 -0
- data/lib/generators/rolemodel/mcp/templates/app/views/doorkeeper/authorizations/error.html.slim.tt +13 -0
- data/lib/generators/rolemodel/mcp/templates/app/views/doorkeeper/authorizations/new.html.slim.tt +41 -0
- data/lib/generators/rolemodel/mcp/templates/app/views/layouts/doorkeeper.html.slim +7 -0
- data/lib/generators/rolemodel/mcp/templates/config/initializers/doorkeeper.rb +537 -0
- data/lib/generators/rolemodel/mcp/templates/spec/mcp/prompts/sample_spec.rb +15 -0
- data/lib/generators/rolemodel/mcp/templates/spec/mcp/resources/controller_spec.rb +16 -0
- data/lib/generators/rolemodel/mcp/templates/spec/mcp/resources/docs_controller_spec.rb +55 -0
- data/lib/generators/rolemodel/mcp/templates/spec/mcp/tools/sample_spec.rb +15 -0
- data/lib/generators/rolemodel/mcp/templates/spec/requests/mcp_controller_spec.rb +84 -0
- data/lib/generators/rolemodel/mcp/templates/spec/requests/oauth_registrations_controller_spec.rb +62 -0
- data/lib/generators/rolemodel/mcp/templates/spec/requests/well_known_controller_spec.rb +30 -0
- data/lib/generators/rolemodel/optics/all_generator.rb +1 -1
- data/lib/generators/rolemodel/optics/base/base_generator.rb +2 -2
- data/lib/generators/rolemodel/optics/icons/icons_generator.rb +1 -1
- data/lib/generators/rolemodel/react/react_generator.rb +1 -1
- data/lib/generators/rolemodel/readme/readme_generator.rb +1 -1
- data/lib/generators/rolemodel/saas/all_generator.rb +1 -1
- data/lib/generators/rolemodel/saas/devise/devise_generator.rb +1 -1
- data/lib/generators/rolemodel/semaphore/semaphore_generator.rb +1 -1
- data/lib/generators/rolemodel/simple_form/simple_form_generator.rb +1 -1
- data/lib/generators/rolemodel/slim/slim_generator.rb +1 -1
- data/lib/generators/rolemodel/soft_destroyable/soft_destroyable_generator.rb +1 -1
- data/lib/generators/rolemodel/source_map/source_map_generator.rb +1 -1
- data/lib/generators/rolemodel/tailored_select/tailored_select_generator.rb +1 -1
- data/lib/generators/rolemodel/testing/all_generator.rb +1 -1
- data/lib/generators/rolemodel/testing/factory_bot/factory_bot_generator.rb +1 -1
- data/lib/generators/rolemodel/testing/jasmine_playwright/jasmine_playwright_generator.rb +1 -1
- data/lib/generators/rolemodel/testing/parallel_tests/parallel_tests_generator.rb +1 -1
- data/lib/generators/rolemodel/testing/rspec/rspec_generator.rb +1 -2
- data/lib/generators/rolemodel/testing/rspec/templates/rails_helper.rb +4 -0
- data/lib/generators/rolemodel/testing/rspec/templates/support/capybara_drivers.rb +0 -2
- data/lib/generators/rolemodel/testing/rspec/templates/support/helpers/capybara_helper.rb +0 -46
- data/lib/generators/rolemodel/testing/rspec/templates/support/helpers/playwright_helper.rb +0 -60
- data/lib/generators/rolemodel/testing/test_prof/test_prof_generator.rb +1 -1
- data/lib/generators/rolemodel/testing/vitest/vitest_generator.rb +1 -1
- data/lib/generators/rolemodel/ui_components/all_generator.rb +1 -1
- data/lib/generators/rolemodel/ui_components/flash/flash_generator.rb +1 -1
- data/lib/generators/rolemodel/ui_components/modals/modals_generator.rb +1 -1
- data/lib/generators/rolemodel/ui_components/navbar/navbar_generator.rb +1 -1
- data/lib/generators/rolemodel/webpack/webpack_generator.rb +1 -1
- data/lib/rolemodel/engine.rb +3 -1
- data/lib/rolemodel/generator_base.rb +17 -0
- data/lib/rolemodel/version.rb +1 -1
- data/lib/rolemodel-rails.rb +2 -0
- metadata +32 -7
- data/lib/generators/rolemodel/base_generator.rb +0 -14
- data/lib/generators/templates/generator/%filename%.rb.tt +0 -11
- data/lib/generators/templates/generator/README.md.tt +0 -11
- data/lib/generators/templates/generator/USAGE.tt +0 -5
- data/lib/generators/templates/generator_spec/%filename%_spec.rb.tt +0 -5
- /data/lib/{generators/rolemodel → rolemodel}/replace_content_helper.rb +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
.doorkeeper {
|
|
2
|
+
position: fixed;
|
|
3
|
+
inset: 0;
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
padding: var(--op-space-x-large) var(--op-space-large);
|
|
9
|
+
overflow: auto;
|
|
10
|
+
background-color: var(--op-color-neutral-plus-eight);
|
|
11
|
+
color: var(--op-color-neutral-minus-max);
|
|
12
|
+
font-family: var(--op-font-family);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.doorkeeper__card {
|
|
16
|
+
width: min(100%, 48rem);
|
|
17
|
+
background-color: var(--op-color-white);
|
|
18
|
+
border: var(--op-border-width) solid var(--op-color-neutral-plus-six);
|
|
19
|
+
border-radius: var(--op-radius-x-large);
|
|
20
|
+
box-shadow: var(--op-shadow-large);
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
gap: var(--op-space-large);
|
|
24
|
+
padding: var(--op-space-2x-large);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.doorkeeper__brand {
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: var(--op-space-large);
|
|
32
|
+
margin-bottom: var(--op-space-small);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.doorkeeper__logo {
|
|
36
|
+
width: 180px;
|
|
37
|
+
height: auto;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.doorkeeper__title {
|
|
41
|
+
margin: 0;
|
|
42
|
+
font-size: var(--op-font-2x-large);
|
|
43
|
+
font-weight: var(--op-font-weight-bold);
|
|
44
|
+
color: var(--op-color-primary-minus-two);
|
|
45
|
+
text-align: center;
|
|
46
|
+
letter-spacing: -0.04em;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.doorkeeper__prompt {
|
|
50
|
+
margin: 0;
|
|
51
|
+
font-size: var(--op-font-large);
|
|
52
|
+
line-height: var(--op-line-height-loose);
|
|
53
|
+
color: var(--op-color-neutral-minus-three);
|
|
54
|
+
text-align: center;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.doorkeeper__client-name {
|
|
58
|
+
color: var(--op-color-primary-base);
|
|
59
|
+
font-weight: var(--op-font-weight-bold);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.doorkeeper__permissions {
|
|
63
|
+
background-color: var(--op-color-primary-plus-eight);
|
|
64
|
+
border: var(--op-border-width) solid var(--op-color-primary-plus-six);
|
|
65
|
+
border-radius: var(--op-radius-medium);
|
|
66
|
+
padding: var(--op-space-large);
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
gap: var(--op-space-medium);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.doorkeeper__permissions-label {
|
|
73
|
+
margin: 0;
|
|
74
|
+
font-size: var(--op-font-small);
|
|
75
|
+
text-transform: uppercase;
|
|
76
|
+
letter-spacing: 0.05em;
|
|
77
|
+
font-weight: var(--op-font-weight-bold);
|
|
78
|
+
color: var(--op-color-primary-minus-three);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.doorkeeper__scope-list {
|
|
82
|
+
margin: 0;
|
|
83
|
+
padding-left: var(--op-space-large);
|
|
84
|
+
display: grid;
|
|
85
|
+
gap: var(--op-space-small);
|
|
86
|
+
color: var(--op-color-primary-minus-max);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.doorkeeper__scope-item {
|
|
90
|
+
line-height: var(--op-line-height-base);
|
|
91
|
+
font-weight: var(--op-font-weight-medium);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.doorkeeper__actions {
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
gap: var(--op-space-medium);
|
|
98
|
+
margin-top: var(--op-space-medium);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.doorkeeper__error {
|
|
102
|
+
align-self: stretch;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.doorkeeper__error-description {
|
|
106
|
+
margin: 0;
|
|
107
|
+
font-size: var(--op-font-medium);
|
|
108
|
+
line-height: var(--op-line-height-base);
|
|
109
|
+
white-space: pre-wrap;
|
|
110
|
+
overflow-wrap: anywhere;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.doorkeeper__form {
|
|
114
|
+
margin: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.doorkeeper__button {
|
|
118
|
+
width: 100%;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
font-weight: var(--op-font-weight-semi-bold);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@media (max-width: 640px) {
|
|
124
|
+
.doorkeeper {
|
|
125
|
+
padding: var(--op-space-large) var(--op-space-medium);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.doorkeeper__card {
|
|
129
|
+
padding: var(--op-space-large);
|
|
130
|
+
border-radius: var(--op-radius-large);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.doorkeeper__logo {
|
|
134
|
+
width: 140px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.doorkeeper__title {
|
|
138
|
+
font-size: var(--op-font-x-large);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class MCPController < ApplicationController
|
|
4
|
+
# skip_before_action :authenticate_user!
|
|
5
|
+
skip_forgery_protection
|
|
6
|
+
|
|
7
|
+
before_action :authorize_mcp
|
|
8
|
+
before_action :set_current_user
|
|
9
|
+
|
|
10
|
+
def handle
|
|
11
|
+
server = build_server
|
|
12
|
+
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
|
|
13
|
+
server.transport = transport
|
|
14
|
+
|
|
15
|
+
status, response_headers, body = transport.handle_request(request)
|
|
16
|
+
respond_with(status, response_headers, body)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def respond_with(status, headers, body)
|
|
22
|
+
headers.each { |key, value| response.set_header(key, value) }
|
|
23
|
+
response.status = status
|
|
24
|
+
self.response_body = body
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def authorize_mcp
|
|
28
|
+
doorkeeper_authorize! :mcp
|
|
29
|
+
set_mcp_resource_metadata_header if response.status == 401
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def set_current_user
|
|
33
|
+
@current_user = User.find_by(id: doorkeeper_token&.resource_owner_id)
|
|
34
|
+
unauthorized_request if @current_user.blank?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def unauthorized_request
|
|
38
|
+
set_mcp_resource_metadata_header
|
|
39
|
+
render json: { error: 'Unauthorized' }, status: :unauthorized
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def set_mcp_resource_metadata_header
|
|
43
|
+
metadata = %(resource_metadata="#{request.base_url}/.well-known/oauth-protected-resource")
|
|
44
|
+
response.set_header('WWW-Authenticate', %(Bearer realm="<%= application_name.titleize %>", #{metadata}))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_server
|
|
48
|
+
server = MCP::Server.new(**mcp_server_config)
|
|
49
|
+
handle_resources(server)
|
|
50
|
+
|
|
51
|
+
server
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def handle_resources(server) # rubocop:disable Metrics/MethodLength
|
|
55
|
+
controllers = [
|
|
56
|
+
Resources::DocsController
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
server.resources_read_handler do |params|
|
|
60
|
+
uri = params[:uri].to_s
|
|
61
|
+
controller = controllers.find { |h| h.serves?(uri) }
|
|
62
|
+
|
|
63
|
+
unless controller
|
|
64
|
+
raise MCP::Server::RequestHandlerError.new(
|
|
65
|
+
"Unable to serve resource for URI: #{uri}. Supported schemas: #{controllers.map(&:schema).join(', ')}",
|
|
66
|
+
params,
|
|
67
|
+
error_type: :invalid_params
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
controller.call(params, server_context)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def mcp_server_config # rubocop:disable Metrics/MethodLength
|
|
76
|
+
{
|
|
77
|
+
name: '<%= application_name.underscore %>_mcp',
|
|
78
|
+
version: '1.0.0',
|
|
79
|
+
tools: [Tools::Sample],
|
|
80
|
+
prompts: [Prompts::Sample],
|
|
81
|
+
server_context:,
|
|
82
|
+
resources: [
|
|
83
|
+
*Resources::DocsController.resource_list,
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def server_context
|
|
89
|
+
@server_context ||= { current_user: @current_user }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class OauthRegistrationsController < ApplicationController
|
|
4
|
+
# skip_before_action :authenticate_user!
|
|
5
|
+
skip_forgery_protection
|
|
6
|
+
|
|
7
|
+
def create
|
|
8
|
+
app = Doorkeeper::Application.new(doorkeeper_params)
|
|
9
|
+
return client_metadata_error('redirect_uris is required') if app.redirect_uri.blank?
|
|
10
|
+
|
|
11
|
+
if app.save
|
|
12
|
+
render json: base_response(app), status: :created
|
|
13
|
+
else
|
|
14
|
+
client_metadata_error(app.errors.full_messages.join(', '))
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def base_response(app)
|
|
21
|
+
{
|
|
22
|
+
client_id: app.uid,
|
|
23
|
+
client_name: app.name,
|
|
24
|
+
redirect_uris: app.redirect_uri.split("\n"),
|
|
25
|
+
grant_types: %w[authorization_code refresh_token],
|
|
26
|
+
response_types: ['code'],
|
|
27
|
+
token_endpoint_auth_method: app.confidential? ? 'client_secret_basic' : 'none',
|
|
28
|
+
client_id_issued_at: app.created_at.to_i,
|
|
29
|
+
scope: 'mcp',
|
|
30
|
+
client_secret: app.confidential? ? app.secret : nil,
|
|
31
|
+
}.compact
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def client_metadata_error(description)
|
|
35
|
+
render json: { error: 'invalid_client_metadata', error_description: description }, status: :bad_request
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def doorkeeper_params
|
|
39
|
+
{
|
|
40
|
+
name: params[:client_name].presence || 'MCP Client',
|
|
41
|
+
redirect_uri: params[:redirect_uris].is_a?(Array) ? params[:redirect_uris].join("\n") : params[:redirect_uris],
|
|
42
|
+
scopes: 'mcp',
|
|
43
|
+
confidential: params[:token_endpoint_auth_method] != 'none',
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class WellKnownController < ApplicationController
|
|
4
|
+
# skip_before_action :authenticate_user!
|
|
5
|
+
before_action :set_base_url
|
|
6
|
+
|
|
7
|
+
def oauth_protected_resource
|
|
8
|
+
render json: {
|
|
9
|
+
resource: "#{@base_url}/mcp",
|
|
10
|
+
authorization_servers: [@base_url],
|
|
11
|
+
}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def oauth_authorization_server
|
|
15
|
+
render json: authorization_server_metadata
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def authorization_server_metadata # rubocop:disable Metrics/MethodLength
|
|
21
|
+
{
|
|
22
|
+
issuer: @base_url,
|
|
23
|
+
authorization_endpoint: "#{@base_url}/oauth/authorize",
|
|
24
|
+
token_endpoint: "#{@base_url}/oauth/token",
|
|
25
|
+
registration_endpoint: "#{@base_url}/oauth/register",
|
|
26
|
+
revocation_endpoint: "#{@base_url}/oauth/revoke",
|
|
27
|
+
introspection_endpoint: "#{@base_url}/oauth/introspect",
|
|
28
|
+
scopes_supported: ['mcp'],
|
|
29
|
+
response_types_supported: ['code'],
|
|
30
|
+
grant_types_supported: %w[authorization_code client_credentials refresh_token],
|
|
31
|
+
token_endpoint_auth_methods_supported: %w[none client_secret_basic client_secret_post],
|
|
32
|
+
code_challenge_methods_supported: ['S256'],
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def set_base_url
|
|
37
|
+
@base_url = request.base_url
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prompts
|
|
4
|
+
class Sample < ::MCP::Prompt
|
|
5
|
+
prompt_name 'sample_prompt'
|
|
6
|
+
title 'Sample Prompt'
|
|
7
|
+
description 'Sample prompt description'
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def template(_args, _server_context: nil)
|
|
11
|
+
::MCP::Prompt::Result.new(
|
|
12
|
+
description: 'Sample prompt result description',
|
|
13
|
+
messages: [
|
|
14
|
+
::MCP::Prompt::Message.new(
|
|
15
|
+
role: 'assistant',
|
|
16
|
+
content: ::MCP::Content::Text.new(instructions_text),
|
|
17
|
+
),
|
|
18
|
+
],
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def instructions_text
|
|
25
|
+
<<~TEXT
|
|
26
|
+
This is a sample prompt.
|
|
27
|
+
|
|
28
|
+
MCP prompts can return instructions for the agent, which can be used to guide the agent's behavior.
|
|
29
|
+
For example, you might include instructions on how to query a specific resource or use a specific tool.
|
|
30
|
+
Think of it like a system prompt in a conversational agent, but it can be dynamically generated based on the
|
|
31
|
+
context of the request.
|
|
32
|
+
TEXT
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Resources
|
|
4
|
+
class Controller
|
|
5
|
+
include ActiveModel::Attributes
|
|
6
|
+
include ActiveModel::API
|
|
7
|
+
|
|
8
|
+
attribute :server_context
|
|
9
|
+
attribute :path, :string
|
|
10
|
+
|
|
11
|
+
validates :path, presence: { message: 'is required' } # rubocop:disable Rails/I18nLocaleTexts
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def mime_type(mime_type = nil)
|
|
15
|
+
@mime_type = mime_type if mime_type
|
|
16
|
+
@mime_type
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def schema(schema = nil)
|
|
20
|
+
@schema = schema if schema
|
|
21
|
+
@schema
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def serves?(uri)
|
|
25
|
+
uri.start_with?(schema)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call(params, server_context)
|
|
29
|
+
controller = new(params[:uri].sub(schema, ''), server_context)
|
|
30
|
+
|
|
31
|
+
unless controller.valid?
|
|
32
|
+
raise ::MCP::Server::RequestHandlerError.new(
|
|
33
|
+
controller.errors.full_messages.join(', '),
|
|
34
|
+
params,
|
|
35
|
+
error_type: :invalid_params
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
[{ uri: params[:uri], mimeType: mime_type, text: controller.serve }]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(path, server_context = nil)
|
|
44
|
+
super()
|
|
45
|
+
self.path = path
|
|
46
|
+
self.server_context = server_context
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def no_extra_path_parts
|
|
52
|
+
return if @extra.blank?
|
|
53
|
+
|
|
54
|
+
errors.add(:base, "Too many uri parts: #{@extra.join('/')}.")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Resources
|
|
4
|
+
class DocsController < Controller
|
|
5
|
+
FILES = {
|
|
6
|
+
'SAMPLE_DOC.md' => Rails.root.join('app/mcp/resources/docs/blazer-documentation.md'),
|
|
7
|
+
}.freeze
|
|
8
|
+
|
|
9
|
+
mime_type 'text/markdown'
|
|
10
|
+
schema 'docs://'
|
|
11
|
+
|
|
12
|
+
attribute :file_path
|
|
13
|
+
|
|
14
|
+
validates :file_path, presence: { message: ->(controller, _) { "Unknown docs resource: #{controller.path}" } }
|
|
15
|
+
validate :file_exists
|
|
16
|
+
|
|
17
|
+
def initialize(path, _server_context = nil)
|
|
18
|
+
super
|
|
19
|
+
self.file_path = FILES[path]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.resource_list
|
|
23
|
+
[
|
|
24
|
+
::MCP::Resource.new(
|
|
25
|
+
uri: 'docs://SAMPLE_DOC.md',
|
|
26
|
+
name: 'sample_doc',
|
|
27
|
+
title: 'Sample Resource',
|
|
28
|
+
description: 'Sample resource',
|
|
29
|
+
mime_type: mime_type,
|
|
30
|
+
),
|
|
31
|
+
]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def serve
|
|
35
|
+
file_path.read
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def file_exists
|
|
41
|
+
return if file_path.blank? || file_path.exist?
|
|
42
|
+
|
|
43
|
+
errors.add(:file_path, "Missing docs file for #{path}")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tools
|
|
4
|
+
class Sample < ::MCP::Tool
|
|
5
|
+
tool_name 'sample_tool'
|
|
6
|
+
title 'Sample Tool'
|
|
7
|
+
description 'Sample tool description'
|
|
8
|
+
input_schema(
|
|
9
|
+
properties: {
|
|
10
|
+
name: { type: 'string', minLength: 1 },
|
|
11
|
+
},
|
|
12
|
+
required: ['name'],
|
|
13
|
+
)
|
|
14
|
+
annotations(
|
|
15
|
+
read_only_hint: true,
|
|
16
|
+
destructive_hint: false,
|
|
17
|
+
idempotent_hint: true,
|
|
18
|
+
open_world_hint: false,
|
|
19
|
+
title: 'Sample Tool',
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def call(name:, server_context:)
|
|
24
|
+
payload = payload_for(name)
|
|
25
|
+
|
|
26
|
+
::MCP::Tool::Response.new(
|
|
27
|
+
[{ type: 'text', text: payload.to_json }],
|
|
28
|
+
structured_content: { sample: payload },
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def payload_for(name)
|
|
35
|
+
{
|
|
36
|
+
time: Time.current.iso8601,
|
|
37
|
+
user_count: User.count
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/generators/rolemodel/mcp/templates/app/views/doorkeeper/authorizations/error.html.slim.tt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
- content_for :page_title do
|
|
2
|
+
= '<%= application_name.titleize %> - Error Authorizing Application'
|
|
3
|
+
|
|
4
|
+
main.doorkeeper role="main"
|
|
5
|
+
.doorkeeper__card.card.card--padded.card--shadow-medium
|
|
6
|
+
.doorkeeper__brand
|
|
7
|
+
= image_tag 'logo.svg', alt: '<%= application_name.titleize %>', class: 'doorkeeper__logo'
|
|
8
|
+
h1.doorkeeper__title= t('doorkeeper.authorizations.error.title')
|
|
9
|
+
|
|
10
|
+
.doorkeeper__error.alert.alert--danger role="alert"
|
|
11
|
+
.alert__messages
|
|
12
|
+
p.doorkeeper__error-description.alert__description
|
|
13
|
+
= (local_assigns[:error_response] ? error_response : @pre_auth.error_response).body[:error_description]
|
data/lib/generators/rolemodel/mcp/templates/app/views/doorkeeper/authorizations/new.html.slim.tt
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
- content_for :page_title do
|
|
2
|
+
= '<%= application_name.titleize %> - Authorize Application'
|
|
3
|
+
|
|
4
|
+
main.doorkeeper role="main"
|
|
5
|
+
.doorkeeper__card.card.card--padded.card--shadow-medium
|
|
6
|
+
.doorkeeper__brand
|
|
7
|
+
= image_tag 'logo.svg', alt: '<%= application_name.titleize %>', class: 'doorkeeper__logo'
|
|
8
|
+
h1.doorkeeper__title= t('.title')
|
|
9
|
+
|
|
10
|
+
p.doorkeeper__prompt
|
|
11
|
+
== t('.prompt', client_name: content_tag(:strong, @pre_auth.client.name, class: 'doorkeeper__client-name'))
|
|
12
|
+
|
|
13
|
+
- if @pre_auth.scopes.count > 0
|
|
14
|
+
#oauth-permissions.doorkeeper__permissions
|
|
15
|
+
p.doorkeeper__permissions-label= t('.able_to') + ":"
|
|
16
|
+
ul.doorkeeper__scope-list
|
|
17
|
+
- @pre_auth.scopes.each do |scope|
|
|
18
|
+
li.doorkeeper__scope-item= t scope, scope: [:doorkeeper, :scopes]
|
|
19
|
+
|
|
20
|
+
.doorkeeper__actions
|
|
21
|
+
= form_tag oauth_authorization_path, method: :post, class: 'doorkeeper__form', data: { turbo: false } do
|
|
22
|
+
= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil
|
|
23
|
+
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil
|
|
24
|
+
= hidden_field_tag :state, @pre_auth.state, id: nil
|
|
25
|
+
= hidden_field_tag :response_type, @pre_auth.response_type, id: nil
|
|
26
|
+
= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil
|
|
27
|
+
= hidden_field_tag :scope, @pre_auth.scope, id: nil
|
|
28
|
+
= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil
|
|
29
|
+
= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil
|
|
30
|
+
= submit_tag t('doorkeeper.authorizations.buttons.authorize'), class: 'btn btn--primary doorkeeper__button'
|
|
31
|
+
|
|
32
|
+
= form_tag oauth_authorization_path, method: :delete, class: 'doorkeeper__form' do
|
|
33
|
+
= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil
|
|
34
|
+
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil
|
|
35
|
+
= hidden_field_tag :state, @pre_auth.state, id: nil
|
|
36
|
+
= hidden_field_tag :response_type, @pre_auth.response_type, id: nil
|
|
37
|
+
= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil
|
|
38
|
+
= hidden_field_tag :scope, @pre_auth.scope, id: nil
|
|
39
|
+
= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil
|
|
40
|
+
= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil
|
|
41
|
+
= submit_tag t('doorkeeper.authorizations.buttons.deny'), class: 'btn btn--destructive doorkeeper__button'
|