rolemodel-rails 1.0.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/github_generator.rb +1 -1
- 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/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
- metadata +27 -7
- data/lib/generators/rolemodel/base_generator.rb +0 -16
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3e11cf41b7d9e5afb68a8eaca61a30785b10dcb9032fc327a52f40ea8271a2e
|
|
4
|
+
data.tar.gz: 4b4388ba6e3b2608135be47000bc9ba655b32bd4936e096d2e3ba8cafe4904c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 173f38c6b5f5f0101a143aaf8a5301afd7a69295d68e1c8eb54a5bd2cf06b57254f658e312a19a3a2605f6920ddab05f6f8941a0797ce7400e83c2e3ff13ac38
|
|
7
|
+
data.tar.gz: 77b6be040efd20cd738c14c44c92c57ac84241a75685cc157bfa86ddc3b6b7b6675eec3b2ad8a3b8c6dfb83c7e568aca7420746b6b81ef7d1172d6392c490c47
|
data/README.md
CHANGED
|
@@ -28,9 +28,7 @@ rails db:create
|
|
|
28
28
|
Add this line to your application's Gemfile:
|
|
29
29
|
|
|
30
30
|
```ruby
|
|
31
|
-
group :development
|
|
32
|
-
gem 'rolemodel-rails', github: 'RoleModel/rolemodel_rails'
|
|
33
|
-
end
|
|
31
|
+
gem 'rolemodel-rails', group: :development
|
|
34
32
|
```
|
|
35
33
|
|
|
36
34
|
And then execute:
|
|
@@ -115,7 +113,7 @@ e.g.
|
|
|
115
113
|
bin/new_generator testing/fantasitic_specs 'A Fantastic Testing Framework'
|
|
116
114
|
```
|
|
117
115
|
|
|
118
|
-
We use the embeded Rails apps (`
|
|
116
|
+
We use the embeded Rails apps (`example_rails_current` & `example_rails_legacy`) to test generators against. They reference the rolemodel-rails gem by local path,
|
|
119
117
|
so you can navigate into one of them and run your generator for immediate feedback while developing.
|
|
120
118
|
|
|
121
119
|
> [!IMPORTANT]
|
|
@@ -146,6 +144,19 @@ RSpec.describe Rolemodel::Testing::JasminePlaywrightGenerator, type: :generator
|
|
|
146
144
|
end
|
|
147
145
|
```
|
|
148
146
|
|
|
147
|
+
If the generator you're testing depends on being run after another generator, you should run that one first.
|
|
148
|
+
|
|
149
|
+
e.g.
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
RSpec.describe Rolemodel::MyGenerator, type: :generator do
|
|
153
|
+
before do
|
|
154
|
+
run_generator_against_test_app(generator: ::Rolemodel::PrereqGenerator)
|
|
155
|
+
run_generator_against_test_app
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
149
160
|
Additional information about testing generators and the available assertions & matchers can be found at the following resources.
|
|
150
161
|
|
|
151
162
|
* [Rails Guide](https://guides.rubyonrails.org/generators.html#testing-generators)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module Rolemodel
|
|
2
|
-
class AllGenerator <
|
|
2
|
+
class AllGenerator < GeneratorBase
|
|
3
3
|
source_root File.expand_path('templates', __dir__)
|
|
4
4
|
|
|
5
5
|
def run_all_the_generators
|
|
@@ -24,6 +24,7 @@ module Rolemodel
|
|
|
24
24
|
generate 'rolemodel:editors'
|
|
25
25
|
# generate 'rolemodel:tailored_select' # Not production ready
|
|
26
26
|
generate 'rolemodel:lograge'
|
|
27
|
+
generate 'rolemodel:mcp'
|
|
27
28
|
end
|
|
28
29
|
end
|
|
29
30
|
end
|
|
@@ -4,7 +4,7 @@ require_relative 'vscode_helpers'
|
|
|
4
4
|
|
|
5
5
|
module Rolemodel
|
|
6
6
|
# Add standard editorconfig and any extensions to enable it
|
|
7
|
-
class EditorsGenerator <
|
|
7
|
+
class EditorsGenerator < GeneratorBase
|
|
8
8
|
include Rolemodel::VSCodeHelpers
|
|
9
9
|
|
|
10
10
|
# This is bringing in the root from this gem, so we only modify
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Rolemodel
|
|
4
|
-
class GithubGenerator <
|
|
4
|
+
class GithubGenerator < GeneratorBase
|
|
5
5
|
GITHUB_ACTIONS_REPO = 'https://github.com/RoleModel/actions.git'
|
|
6
6
|
# Files which are both used by the gem source and copied to the target app without modification
|
|
7
7
|
# are placed in the `.github` folder at the top level of this repository. This folder is then
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# MCP Generator
|
|
2
|
+
|
|
3
|
+
Install boilerplate for your very own MCP server.
|
|
4
|
+
|
|
5
|
+
## What you get
|
|
6
|
+
|
|
7
|
+
### Doorkeeper
|
|
8
|
+
|
|
9
|
+
OAuth 2.1-enabled flow with dynamic application registration.
|
|
10
|
+
|
|
11
|
+
### MCP
|
|
12
|
+
|
|
13
|
+
A basic MCP controller that you can build on to serve tools, resources, and prompts.
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
|
@@ -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
|