Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ gem 'turbo-rails'

# Start debugger with binding.b [https://github.com/ruby/debug]
gem 'debug', '>= 1.0.0'
gem 'doorkeeper'
8 changes: 5 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ GEM
irb (~> 1.10)
reline (>= 0.3.8)
diff-lcs (1.6.2)
doorkeeper (5.9.0)
railties (>= 5)
drb (2.2.3)
erb (6.0.1)
erubi (1.13.1)
Expand Down Expand Up @@ -124,7 +126,6 @@ GEM
net-smtp
marcel (1.1.0)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (6.0.1)
prism (~> 1.5)
net-imap (0.6.2)
Expand All @@ -137,8 +138,7 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.5)
nokogiri (1.19.0)
mini_portile2 (~> 2.8.2)
nokogiri (1.19.0-arm64-darwin)
racc (~> 1.4)
nokogiri (1.19.0-x86_64-linux-gnu)
racc (~> 1.4)
Expand Down Expand Up @@ -268,13 +268,15 @@ GEM
zeitwerk (2.7.4)

PLATFORMS
arm64-darwin-23
arm64-darwin-25
x86_64-linux

DEPENDENCIES
benchmark
bundler (~> 4)
debug (>= 1.0.0)
doorkeeper
generator_spec (~> 0.10)
pg
propshaft
Expand Down
1 change: 1 addition & 0 deletions lib/generators/rolemodel/all_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def run_all_the_generators
generate 'rolemodel:editors'
# generate 'rolemodel:tailored_select' # Not production ready
generate 'rolemodel:lograge'
generate 'rolemodel:mcp'
end
end
end
13 changes: 13 additions & 0 deletions lib/generators/rolemodel/mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# MCP Generator

Install boilerplate for your very own MCP server.

## What you get

### Doorkeeper

OAuth 2.1-enabled flow with dynamic application registration.

### MCP

A basic MCP controller that you can build on to serve tools, resources, and prompts.
8 changes: 8 additions & 0 deletions lib/generators/rolemodel/mcp/USAGE
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Description:
Sets up RoleModel MCP support and any required application wiring for the MCP endpoint.

Example:
rails generate rolemodel:mcp

This generator adds the files and route updates needed to enable RoleModel MCP
in your Rails application.
104 changes: 104 additions & 0 deletions lib/generators/rolemodel/mcp/mcp_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# frozen_string_literal: true

module Rolemodel
class MCPGenerator < BaseGenerator
source_root File.expand_path('templates', __dir__)

def install_mcp
bundle_command 'add mcp'
template 'app/controllers/mcp_controller.rb'
copy_file 'spec/requests/mcp_controller_spec.rb'

route <<~RUBY
match '/mcp', to: 'mcp#handle', via: %i[get post delete]
RUBY
end

def add_sample_mcp_resource
copy_file 'app/mcp/resources/docs/SAMPLE_DOC.md'
copy_file 'app/mcp/resources/handlers/handler.rb'
copy_file 'app/mcp/resources/handlers/docs.rb'
end

def add_sample_mcp_prompt
copy_file 'app/mcp/prompts/sample.rb'
end

def add_sample_mcp_tool
copy_file 'app/mcp/tools/sample.rb'
end

def install_doorkeeper
bundle_command 'add doorkeeper'
generate 'doorkeeper:install'
end

def configure_doorkeeper
copy_file 'config/initializers/doorkeeper.rb', force: true
copy_file 'app/controllers/doorkeeper/base_controller.rb'

copy_file 'app/views/layouts/doorkeeper.html.slim'
template 'app/views/doorkeeper/authorizations/new.html.slim'
template 'app/views/doorkeeper/authorizations/error.html.slim'

copy_file 'app/assets/stylesheets/components/doorkeeper.css'

route 'use_doorkeeper'
end

def apply_doorkeeper_css
css_manifest = if File.exist?(File.join(destination_root, 'app/assets/stylesheets/application.scss'))
'app/assets/stylesheets/application.scss'
else
'app/assets/stylesheets/application.css'
end

return if File.read(File.join(destination_root, css_manifest)).include?("@import 'components/doorkeeper.css';")

append_to_file css_manifest, <<~CSS
@import 'components/doorkeeper.css';
CSS
end

def add_oauth_dynamic_registrations
copy_file 'app/controllers/oauth_registrations_controller.rb'
copy_file 'spec/requests/oauth_registrations_controller_spec.rb'
route <<~RUBY
post '/oauth/register', to: 'oauth_registrations#create'
RUBY
end

def add_well_known_route
copy_file 'app/controllers/well_known_controller.rb'
copy_file 'spec/requests/well_known_controller_spec.rb'
route <<~RUBY
get '/.well-known/oauth-protected-resource', to: 'well_known#oauth_protected_resource'
get '/.well-known/oauth-authorization-server', to: 'well_known#oauth_authorization_server'
RUBY
end

def update_inflections
inflections_path = File.join(destination_root, 'config/initializers/inflections.rb')
block_start = "\nActiveSupport::Inflector.inflections(:en) do |inflect|\n"

return if File.read(inflections_path).include?("inflect.acronym 'MCP'")

if File.read(inflections_path).include?(block_start)
inject_into_file inflections_path, " inflect.acronym 'MCP'\n", after: block_start
else
append_to_file inflections_path, <<~RUBY

ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'MCP'
end
RUBY
end
end

private

def application_name
Rails.application.class.try(:parent_name) || Rails.application.class.module_parent_name
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
.doorkeeper {
position: fixed;
inset: 0;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
padding: var(--op-space-x-large) var(--op-space-large);
overflow: auto;
background-color: var(--op-color-neutral-plus-eight);
color: var(--op-color-neutral-minus-max);
font-family: var(--op-font-family);
}

.doorkeeper__card {
width: min(100%, 48rem);
background-color: var(--op-color-white);
border: var(--op-border-width) solid var(--op-color-neutral-plus-six);
border-radius: var(--op-radius-x-large);
box-shadow: var(--op-shadow-large);
display: flex;
flex-direction: column;
gap: var(--op-space-large);
padding: var(--op-space-2x-large);
}

.doorkeeper__brand {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--op-space-large);
margin-bottom: var(--op-space-small);
}

.doorkeeper__logo {
width: 180px;
height: auto;
}

.doorkeeper__title {
margin: 0;
font-size: var(--op-font-2x-large);
font-weight: var(--op-font-weight-bold);
color: var(--op-color-primary-minus-two);
text-align: center;
letter-spacing: -0.04em;
}

.doorkeeper__prompt {
margin: 0;
font-size: var(--op-font-large);
line-height: var(--op-line-height-loose);
color: var(--op-color-neutral-minus-three);
text-align: center;
}

.doorkeeper__client-name {
color: var(--op-color-primary-base);
font-weight: var(--op-font-weight-bold);
}

.doorkeeper__permissions {
background-color: var(--op-color-primary-plus-eight);
border: var(--op-border-width) solid var(--op-color-primary-plus-six);
border-radius: var(--op-radius-medium);
padding: var(--op-space-large);
display: flex;
flex-direction: column;
gap: var(--op-space-medium);
}

.doorkeeper__permissions-label {
margin: 0;
font-size: var(--op-font-small);
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: var(--op-font-weight-bold);
color: var(--op-color-primary-minus-three);
}

.doorkeeper__scope-list {
margin: 0;
padding-left: var(--op-space-large);
display: grid;
gap: var(--op-space-small);
color: var(--op-color-primary-minus-max);
}

.doorkeeper__scope-item {
line-height: var(--op-line-height-base);
font-weight: var(--op-font-weight-medium);
}

.doorkeeper__actions {
display: flex;
flex-direction: column;
gap: var(--op-space-medium);
margin-top: var(--op-space-medium);
}

.doorkeeper__error {
align-self: stretch;
}

.doorkeeper__error-description {
margin: 0;
font-size: var(--op-font-medium);
line-height: var(--op-line-height-base);
white-space: pre-wrap;
overflow-wrap: anywhere;
}

.doorkeeper__form {
margin: 0;
}

.doorkeeper__button {
width: 100%;
justify-content: center;
font-weight: var(--op-font-weight-semi-bold);
}

@media (max-width: 640px) {
.doorkeeper {
padding: var(--op-space-large) var(--op-space-medium);
}

.doorkeeper__card {
padding: var(--op-space-large);
border-radius: var(--op-radius-large);
}

.doorkeeper__logo {
width: 140px;
}

.doorkeeper__title {
font-size: var(--op-font-x-large);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

module Doorkeeper
class BaseController < ::ApplicationController
skip_forgery_protection
end
end
Loading
Loading