Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
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.
22 changes: 22 additions & 0 deletions lib/generators/rolemodel/mcp/USAGE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Description:
Sets up our standard ActionMailer configuration with layout using CSS base styles

Example:
rails generate rolemodel:mailers

This will create:
app/javascript/packs/mailer_stylesheets.scss
app/mailers/user_mailer.rb
app/views/layouts/mailer.html.slim
app/views/user_mailer/welcome_email.html.slim
config/initializers/premailer_rails.rb
public/logo.png
spec/mailers/previews/user_preview.rb

This will modify:
config/environments/development.rb
config/environments/production.rb

This will remove:
app/views/layouts/mailer.html.erb
app/views/layouts/mailer.text.erb
90 changes: 90 additions & 0 deletions lib/generators/rolemodel/mcp/mcp_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# 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 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,8 @@
# frozen_string_literal: true

module Doorkeeper
class BaseController < ::ApplicationController
skip_before_action :authenticate_oauth, raise: false
skip_forgery_protection
end
end
Loading