rolemodel-rails 1.1.0 → 2.0.1
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 +6 -7
- data/lib/generators/rolemodel/all_generator.rb +0 -1
- data/lib/generators/rolemodel/optics/icons/README.md +6 -1
- data/lib/generators/rolemodel/optics/icons/icons_generator.rb +14 -9
- data/lib/generators/rolemodel/optics/icons/templates/app/helpers/icon_helper.rb.tt +4 -2
- data/lib/generators/rolemodel/testing/README.md +4 -1
- data/lib/generators/rolemodel/testing/all_generator.rb +3 -3
- data/lib/generators/rolemodel/testing/jasmine_playwright/jasmine_playwright_generator.rb +1 -1
- data/lib/generators/rolemodel/testing/rspec/README.md +1 -1
- data/lib/generators/rolemodel/testing/rspec/USAGE +1 -1
- data/lib/generators/rolemodel/testing/rspec/rspec_generator.rb +15 -14
- data/lib/generators/rolemodel/testing/rspec/templates/spec/support/capybara_config.rb.tt +9 -0
- data/lib/{generators/rolemodel/optics/icons/templates/app/icon_builders → rolemodel/optics}/custom_icon_builder.rb +1 -1
- data/lib/{generators/rolemodel/optics/icons/templates/app/icon_builders → rolemodel/optics}/feather_icon_builder.rb +1 -1
- data/lib/{generators/rolemodel/optics/icons/templates/app/icon_builders → rolemodel/optics}/icon_builder.rb +10 -19
- data/lib/{generators/rolemodel/optics/icons/templates/app/icon_builders → rolemodel/optics}/lucide_icon_builder.rb +1 -1
- data/lib/{generators/rolemodel/optics/icons/templates/app/icon_builders → rolemodel/optics}/material_icon_builder.rb +1 -1
- data/lib/{generators/rolemodel/optics/icons/templates/app/icon_builders → rolemodel/optics}/phosphor_icon_builder.rb +1 -1
- data/lib/{generators/rolemodel/optics/icons/templates/app/icon_builders → rolemodel/optics}/tabler_icon_builder.rb +1 -1
- data/lib/rolemodel/optics.rb +9 -0
- data/lib/rolemodel/utility/task_tools.rb +42 -0
- data/lib/rolemodel/utility.rb +3 -0
- data/lib/rolemodel/version.rb +1 -1
- data/lib/rolemodel-rails.rb +2 -0
- metadata +35 -42
- data/lib/generators/rolemodel/mcp/README.md +0 -13
- data/lib/generators/rolemodel/mcp/USAGE +0 -8
- data/lib/generators/rolemodel/mcp/mcp_generator.rb +0 -110
- data/lib/generators/rolemodel/mcp/templates/app/assets/stylesheets/components/doorkeeper.css +0 -140
- data/lib/generators/rolemodel/mcp/templates/app/controllers/doorkeeper/base_controller.rb +0 -7
- data/lib/generators/rolemodel/mcp/templates/app/controllers/mcp_controller.rb.tt +0 -91
- data/lib/generators/rolemodel/mcp/templates/app/controllers/oauth_registrations_controller.rb +0 -46
- data/lib/generators/rolemodel/mcp/templates/app/controllers/well_known_controller.rb +0 -39
- data/lib/generators/rolemodel/mcp/templates/app/mcp/prompts/sample.rb +0 -36
- data/lib/generators/rolemodel/mcp/templates/app/mcp/resources/controller.rb +0 -57
- data/lib/generators/rolemodel/mcp/templates/app/mcp/resources/docs/SAMPLE_DOC.md +0 -4
- data/lib/generators/rolemodel/mcp/templates/app/mcp/resources/docs_controller.rb +0 -46
- data/lib/generators/rolemodel/mcp/templates/app/mcp/tools/sample.rb +0 -42
- data/lib/generators/rolemodel/mcp/templates/app/views/doorkeeper/authorizations/error.html.slim.tt +0 -13
- data/lib/generators/rolemodel/mcp/templates/app/views/doorkeeper/authorizations/new.html.slim.tt +0 -41
- data/lib/generators/rolemodel/mcp/templates/app/views/layouts/doorkeeper.html.slim +0 -7
- data/lib/generators/rolemodel/mcp/templates/config/initializers/doorkeeper.rb +0 -537
- data/lib/generators/rolemodel/mcp/templates/spec/mcp/prompts/sample_spec.rb +0 -15
- data/lib/generators/rolemodel/mcp/templates/spec/mcp/resources/controller_spec.rb +0 -16
- data/lib/generators/rolemodel/mcp/templates/spec/mcp/resources/docs_controller_spec.rb +0 -55
- data/lib/generators/rolemodel/mcp/templates/spec/mcp/tools/sample_spec.rb +0 -15
- data/lib/generators/rolemodel/mcp/templates/spec/requests/mcp_controller_spec.rb +0 -84
- data/lib/generators/rolemodel/mcp/templates/spec/requests/oauth_registrations_controller_spec.rb +0 -62
- data/lib/generators/rolemodel/mcp/templates/spec/requests/well_known_controller_spec.rb +0 -30
- data/lib/generators/rolemodel/testing/rspec/templates/support/capybara_testid.rb +0 -5
- /data/lib/generators/rolemodel/testing/rspec/templates/{rails_helper.rb → spec/rails_helper.rb.tt} +0 -0
- /data/lib/generators/rolemodel/testing/rspec/templates/{spec_helper.rb → spec/spec_helper.rb.tt} +0 -0
- /data/lib/generators/rolemodel/testing/rspec/templates/{support/capybara_drivers.rb → spec/support/capybara_drivers.rb.tt} +0 -0
- /data/lib/generators/rolemodel/testing/rspec/templates/{support → spec/support}/helpers/action_cable_helper.rb +0 -0
- /data/lib/generators/rolemodel/testing/rspec/templates/{support → spec/support}/helpers/capybara_helper.rb +0 -0
- /data/lib/generators/rolemodel/testing/rspec/templates/{support → spec/support}/helpers/playwright_helper.rb +0 -0
- /data/lib/generators/rolemodel/testing/rspec/templates/{support → spec/support}/helpers/select_helper.rb +0 -0
- /data/lib/generators/rolemodel/testing/rspec/templates/{support → spec/support}/helpers/test_element_helper.rb +0 -0
- /data/lib/generators/rolemodel/testing/rspec/templates/{support/helpers.rb → spec/support/helpers.rb.tt} +0 -0
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Rolemodel
|
|
4
|
-
class McpGenerator < GeneratorBase
|
|
5
|
-
source_root File.expand_path('templates', __dir__)
|
|
6
|
-
|
|
7
|
-
def update_inflections
|
|
8
|
-
inflections_path = File.join(destination_root, 'config/initializers/inflections.rb')
|
|
9
|
-
block_start = "\nActiveSupport::Inflector.inflections(:en) do |inflect|\n"
|
|
10
|
-
|
|
11
|
-
return if File.read(inflections_path).include?("inflect.acronym 'MCP'")
|
|
12
|
-
|
|
13
|
-
if File.read(inflections_path).include?(block_start)
|
|
14
|
-
inject_into_file inflections_path, " inflect.acronym 'MCP'\n", after: block_start
|
|
15
|
-
else
|
|
16
|
-
append_to_file inflections_path, <<~RUBY
|
|
17
|
-
|
|
18
|
-
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
19
|
-
inflect.acronym 'MCP'
|
|
20
|
-
end
|
|
21
|
-
RUBY
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def install_mcp
|
|
26
|
-
bundle_command 'add mcp'
|
|
27
|
-
template 'app/controllers/mcp_controller.rb'
|
|
28
|
-
copy_file 'spec/requests/mcp_controller_spec.rb'
|
|
29
|
-
|
|
30
|
-
route <<~RUBY
|
|
31
|
-
match '/mcp', to: 'mcp#handle', via: %i[get post delete]
|
|
32
|
-
RUBY
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def add_sample_mcp_resource
|
|
36
|
-
copy_file 'app/mcp/resources/controller.rb'
|
|
37
|
-
copy_file 'spec/mcp/resources/controller_spec.rb'
|
|
38
|
-
|
|
39
|
-
copy_file 'app/mcp/resources/docs/SAMPLE_DOC.md'
|
|
40
|
-
copy_file 'app/mcp/resources/docs_controller.rb'
|
|
41
|
-
copy_file 'spec/mcp/resources/docs_controller_spec.rb'
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def add_sample_mcp_prompt
|
|
45
|
-
copy_file 'app/mcp/prompts/sample.rb'
|
|
46
|
-
copy_file 'spec/mcp/prompts/sample_spec.rb'
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def add_sample_mcp_tool
|
|
50
|
-
copy_file 'app/mcp/tools/sample.rb'
|
|
51
|
-
copy_file 'spec/mcp/tools/sample_spec.rb'
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def install_doorkeeper
|
|
55
|
-
bundle_command 'add doorkeeper'
|
|
56
|
-
run_bundle
|
|
57
|
-
generate 'doorkeeper:install'
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def configure_doorkeeper
|
|
61
|
-
copy_file 'config/initializers/doorkeeper.rb', force: true
|
|
62
|
-
copy_file 'app/controllers/doorkeeper/base_controller.rb'
|
|
63
|
-
|
|
64
|
-
copy_file 'app/views/layouts/doorkeeper.html.slim'
|
|
65
|
-
template 'app/views/doorkeeper/authorizations/new.html.slim'
|
|
66
|
-
template 'app/views/doorkeeper/authorizations/error.html.slim'
|
|
67
|
-
|
|
68
|
-
copy_file 'app/assets/stylesheets/components/doorkeeper.css'
|
|
69
|
-
|
|
70
|
-
route 'use_doorkeeper'
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def apply_doorkeeper_css
|
|
74
|
-
css_manifest = if File.exist?(File.join(destination_root, 'app/assets/stylesheets/application.scss'))
|
|
75
|
-
'app/assets/stylesheets/application.scss'
|
|
76
|
-
else
|
|
77
|
-
'app/assets/stylesheets/application.css'
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
return if File.read(File.join(destination_root, css_manifest)).include?("@import 'components/doorkeeper.css';")
|
|
81
|
-
|
|
82
|
-
append_to_file css_manifest, <<~CSS
|
|
83
|
-
@import 'components/doorkeeper.css';
|
|
84
|
-
CSS
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def add_oauth_dynamic_registrations
|
|
88
|
-
copy_file 'app/controllers/oauth_registrations_controller.rb'
|
|
89
|
-
copy_file 'spec/requests/oauth_registrations_controller_spec.rb'
|
|
90
|
-
route <<~RUBY
|
|
91
|
-
post '/oauth/register', to: 'oauth_registrations#create'
|
|
92
|
-
RUBY
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def add_well_known_route
|
|
96
|
-
copy_file 'app/controllers/well_known_controller.rb'
|
|
97
|
-
copy_file 'spec/requests/well_known_controller_spec.rb'
|
|
98
|
-
route <<~RUBY
|
|
99
|
-
get '/.well-known/oauth-protected-resource', to: 'well_known#oauth_protected_resource'
|
|
100
|
-
get '/.well-known/oauth-authorization-server', to: 'well_known#oauth_authorization_server'
|
|
101
|
-
RUBY
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
private
|
|
105
|
-
|
|
106
|
-
def application_name
|
|
107
|
-
Rails.application.name
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
data/lib/generators/rolemodel/mcp/templates/app/assets/stylesheets/components/doorkeeper.css
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
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
|
data/lib/generators/rolemodel/mcp/templates/app/controllers/oauth_registrations_controller.rb
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
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
|
|
@@ -1,39 +0,0 @@
|
|
|
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
|
|
@@ -1,36 +0,0 @@
|
|
|
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
|
|
@@ -1,57 +0,0 @@
|
|
|
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
|
|
@@ -1,46 +0,0 @@
|
|
|
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
|
|
@@ -1,42 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
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'
|