diff --git a/Gemfile b/Gemfile index 51d969f2..a8b4dd98 100644 --- a/Gemfile +++ b/Gemfile @@ -13,9 +13,5 @@ gem 'puma' gem 'stimulus-rails' gem 'turbo-rails' -# TODO: figure out why these need to be here for the specs to run -gem 'doorkeeper' -gem 'mcp' - # Start debugger with binding.b [https://github.com/ruby/debug] gem 'debug', '>= 1.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index e235a589..f7dcd59e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - rolemodel-rails (1.1.0) + rolemodel-rails (1.2.0) rails (> 7.1) GEM @@ -81,8 +81,6 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - addressable (2.8.9) - public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) base64 (0.3.0) benchmark (0.5.0) @@ -96,8 +94,6 @@ 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) @@ -114,9 +110,6 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.18.0) - json-schema (6.2.0) - addressable (~> 2.8) - bigdecimal (>= 3.1, < 5) language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) @@ -130,8 +123,6 @@ GEM net-pop net-smtp marcel (1.1.0) - mcp (0.10.0) - json-schema (>= 4.1) mini_mime (1.1.5) minitest (6.0.1) prism (~> 1.5) @@ -166,7 +157,6 @@ GEM psych (5.3.1) date stringio - public_suffix (7.0.5) puma (7.2.0) nio4r (~> 2.0) racc (1.8.1) @@ -284,9 +274,7 @@ DEPENDENCIES benchmark bundler (~> 4) debug (>= 1.0.0) - doorkeeper generator_spec (~> 0.10) - mcp pg propshaft puma diff --git a/example_rails_legacy/Gemfile.lock b/example_rails_legacy/Gemfile.lock index bca04a45..3722911d 100644 --- a/example_rails_legacy/Gemfile.lock +++ b/example_rails_legacy/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: .. specs: - rolemodel-rails (1.1.0) + rolemodel-rails (1.2.0) rails (> 7.1) GEM diff --git a/lib/generators/rolemodel/all_generator.rb b/lib/generators/rolemodel/all_generator.rb index 98f2f0a2..ce5cae4f 100644 --- a/lib/generators/rolemodel/all_generator.rb +++ b/lib/generators/rolemodel/all_generator.rb @@ -24,7 +24,6 @@ def run_all_the_generators generate 'rolemodel:editors' # generate 'rolemodel:tailored_select' # Not production ready generate 'rolemodel:lograge' - generate 'rolemodel:mcp' end end end diff --git a/lib/generators/rolemodel/mcp/README.md b/lib/generators/rolemodel/mcp/README.md deleted file mode 100644 index 4b15f0b9..00000000 --- a/lib/generators/rolemodel/mcp/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# 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. diff --git a/lib/generators/rolemodel/mcp/USAGE b/lib/generators/rolemodel/mcp/USAGE deleted file mode 100644 index 2fc3902b..00000000 --- a/lib/generators/rolemodel/mcp/USAGE +++ /dev/null @@ -1,8 +0,0 @@ -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. diff --git a/lib/generators/rolemodel/mcp/mcp_generator.rb b/lib/generators/rolemodel/mcp/mcp_generator.rb deleted file mode 100644 index 5b063e74..00000000 --- a/lib/generators/rolemodel/mcp/mcp_generator.rb +++ /dev/null @@ -1,110 +0,0 @@ -# frozen_string_literal: true - -module Rolemodel - class McpGenerator < GeneratorBase - source_root File.expand_path('templates', __dir__) - - 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 - - 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/controller.rb' - copy_file 'spec/mcp/resources/controller_spec.rb' - - copy_file 'app/mcp/resources/docs/SAMPLE_DOC.md' - copy_file 'app/mcp/resources/docs_controller.rb' - copy_file 'spec/mcp/resources/docs_controller_spec.rb' - end - - def add_sample_mcp_prompt - copy_file 'app/mcp/prompts/sample.rb' - copy_file 'spec/mcp/prompts/sample_spec.rb' - end - - def add_sample_mcp_tool - copy_file 'app/mcp/tools/sample.rb' - copy_file 'spec/mcp/tools/sample_spec.rb' - end - - def install_doorkeeper - bundle_command 'add doorkeeper' - run_bundle - 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 - - private - - def application_name - Rails.application.name - end - end -end diff --git a/lib/generators/rolemodel/mcp/templates/app/assets/stylesheets/components/doorkeeper.css b/lib/generators/rolemodel/mcp/templates/app/assets/stylesheets/components/doorkeeper.css deleted file mode 100644 index 4a1862fc..00000000 --- a/lib/generators/rolemodel/mcp/templates/app/assets/stylesheets/components/doorkeeper.css +++ /dev/null @@ -1,140 +0,0 @@ -.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); - } -} diff --git a/lib/generators/rolemodel/mcp/templates/app/controllers/doorkeeper/base_controller.rb b/lib/generators/rolemodel/mcp/templates/app/controllers/doorkeeper/base_controller.rb deleted file mode 100644 index 626b7e34..00000000 --- a/lib/generators/rolemodel/mcp/templates/app/controllers/doorkeeper/base_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module Doorkeeper - class BaseController < ::ApplicationController - skip_forgery_protection - end -end diff --git a/lib/generators/rolemodel/mcp/templates/app/controllers/mcp_controller.rb.tt b/lib/generators/rolemodel/mcp/templates/app/controllers/mcp_controller.rb.tt deleted file mode 100644 index b2606e66..00000000 --- a/lib/generators/rolemodel/mcp/templates/app/controllers/mcp_controller.rb.tt +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -class MCPController < ApplicationController - # skip_before_action :authenticate_user! - skip_forgery_protection - - before_action :authorize_mcp - before_action :set_current_user - - def handle - server = build_server - transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true) - server.transport = transport - - status, response_headers, body = transport.handle_request(request) - respond_with(status, response_headers, body) - end - - private - - def respond_with(status, headers, body) - headers.each { |key, value| response.set_header(key, value) } - response.status = status - self.response_body = body - end - - def authorize_mcp - doorkeeper_authorize! :mcp - set_mcp_resource_metadata_header if response.status == 401 - end - - def set_current_user - @current_user = User.find_by(id: doorkeeper_token&.resource_owner_id) - unauthorized_request if @current_user.blank? - end - - def unauthorized_request - set_mcp_resource_metadata_header - render json: { error: 'Unauthorized' }, status: :unauthorized - end - - def set_mcp_resource_metadata_header - metadata = %(resource_metadata="#{request.base_url}/.well-known/oauth-protected-resource") - response.set_header('WWW-Authenticate', %(Bearer realm="<%= application_name.titleize %>", #{metadata})) - end - - def build_server - server = MCP::Server.new(**mcp_server_config) - handle_resources(server) - - server - end - - def handle_resources(server) # rubocop:disable Metrics/MethodLength - controllers = [ - Resources::DocsController - ] - - server.resources_read_handler do |params| - uri = params[:uri].to_s - controller = controllers.find { |h| h.serves?(uri) } - - unless controller - raise MCP::Server::RequestHandlerError.new( - "Unable to serve resource for URI: #{uri}. Supported schemas: #{controllers.map(&:schema).join(', ')}", - params, - error_type: :invalid_params - ) - end - - controller.call(params, server_context) - end - end - - def mcp_server_config # rubocop:disable Metrics/MethodLength - { - name: '<%= application_name.underscore %>_mcp', - version: '1.0.0', - tools: [Tools::Sample], - prompts: [Prompts::Sample], - server_context:, - resources: [ - *Resources::DocsController.resource_list, - ], - } - end - - def server_context - @server_context ||= { current_user: @current_user } - end -end diff --git a/lib/generators/rolemodel/mcp/templates/app/controllers/oauth_registrations_controller.rb b/lib/generators/rolemodel/mcp/templates/app/controllers/oauth_registrations_controller.rb deleted file mode 100644 index 6b548327..00000000 --- a/lib/generators/rolemodel/mcp/templates/app/controllers/oauth_registrations_controller.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -class OauthRegistrationsController < ApplicationController - # skip_before_action :authenticate_user! - skip_forgery_protection - - def create - app = Doorkeeper::Application.new(doorkeeper_params) - return client_metadata_error('redirect_uris is required') if app.redirect_uri.blank? - - if app.save - render json: base_response(app), status: :created - else - client_metadata_error(app.errors.full_messages.join(', ')) - end - end - - private - - def base_response(app) - { - client_id: app.uid, - client_name: app.name, - redirect_uris: app.redirect_uri.split("\n"), - grant_types: %w[authorization_code refresh_token], - response_types: ['code'], - token_endpoint_auth_method: app.confidential? ? 'client_secret_basic' : 'none', - client_id_issued_at: app.created_at.to_i, - scope: 'mcp', - client_secret: app.confidential? ? app.secret : nil, - }.compact - end - - def client_metadata_error(description) - render json: { error: 'invalid_client_metadata', error_description: description }, status: :bad_request - end - - def doorkeeper_params - { - name: params[:client_name].presence || 'MCP Client', - redirect_uri: params[:redirect_uris].is_a?(Array) ? params[:redirect_uris].join("\n") : params[:redirect_uris], - scopes: 'mcp', - confidential: params[:token_endpoint_auth_method] != 'none', - } - end -end diff --git a/lib/generators/rolemodel/mcp/templates/app/controllers/well_known_controller.rb b/lib/generators/rolemodel/mcp/templates/app/controllers/well_known_controller.rb deleted file mode 100644 index 53063bf7..00000000 --- a/lib/generators/rolemodel/mcp/templates/app/controllers/well_known_controller.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -class WellKnownController < ApplicationController - # skip_before_action :authenticate_user! - before_action :set_base_url - - def oauth_protected_resource - render json: { - resource: "#{@base_url}/mcp", - authorization_servers: [@base_url], - } - end - - def oauth_authorization_server - render json: authorization_server_metadata - end - - private - - def authorization_server_metadata # rubocop:disable Metrics/MethodLength - { - issuer: @base_url, - authorization_endpoint: "#{@base_url}/oauth/authorize", - token_endpoint: "#{@base_url}/oauth/token", - registration_endpoint: "#{@base_url}/oauth/register", - revocation_endpoint: "#{@base_url}/oauth/revoke", - introspection_endpoint: "#{@base_url}/oauth/introspect", - scopes_supported: ['mcp'], - response_types_supported: ['code'], - grant_types_supported: %w[authorization_code client_credentials refresh_token], - token_endpoint_auth_methods_supported: %w[none client_secret_basic client_secret_post], - code_challenge_methods_supported: ['S256'], - } - end - - def set_base_url - @base_url = request.base_url - end -end diff --git a/lib/generators/rolemodel/mcp/templates/app/mcp/prompts/sample.rb b/lib/generators/rolemodel/mcp/templates/app/mcp/prompts/sample.rb deleted file mode 100644 index 8d34f3ba..00000000 --- a/lib/generators/rolemodel/mcp/templates/app/mcp/prompts/sample.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -module Prompts - class Sample < ::MCP::Prompt - prompt_name 'sample_prompt' - title 'Sample Prompt' - description 'Sample prompt description' - - class << self - def template(_args, _server_context: nil) - ::MCP::Prompt::Result.new( - description: 'Sample prompt result description', - messages: [ - ::MCP::Prompt::Message.new( - role: 'assistant', - content: ::MCP::Content::Text.new(instructions_text), - ), - ], - ) - end - - private - - def instructions_text - <<~TEXT - This is a sample prompt. - - MCP prompts can return instructions for the agent, which can be used to guide the agent's behavior. - For example, you might include instructions on how to query a specific resource or use a specific tool. - Think of it like a system prompt in a conversational agent, but it can be dynamically generated based on the - context of the request. - TEXT - end - end - end -end diff --git a/lib/generators/rolemodel/mcp/templates/app/mcp/resources/controller.rb b/lib/generators/rolemodel/mcp/templates/app/mcp/resources/controller.rb deleted file mode 100644 index 2bde96c8..00000000 --- a/lib/generators/rolemodel/mcp/templates/app/mcp/resources/controller.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -module Resources - class Controller - include ActiveModel::Attributes - include ActiveModel::API - - attribute :server_context - attribute :path, :string - - validates :path, presence: { message: 'is required' } # rubocop:disable Rails/I18nLocaleTexts - - class << self - def mime_type(mime_type = nil) - @mime_type = mime_type if mime_type - @mime_type - end - - def schema(schema = nil) - @schema = schema if schema - @schema - end - - def serves?(uri) - uri.start_with?(schema) - end - - def call(params, server_context) - controller = new(params[:uri].sub(schema, ''), server_context) - - unless controller.valid? - raise ::MCP::Server::RequestHandlerError.new( - controller.errors.full_messages.join(', '), - params, - error_type: :invalid_params - ) - end - - [{ uri: params[:uri], mimeType: mime_type, text: controller.serve }] - end - end - - def initialize(path, server_context = nil) - super() - self.path = path - self.server_context = server_context - end - - private - - def no_extra_path_parts - return if @extra.blank? - - errors.add(:base, "Too many uri parts: #{@extra.join('/')}.") - end - end -end diff --git a/lib/generators/rolemodel/mcp/templates/app/mcp/resources/docs/SAMPLE_DOC.md b/lib/generators/rolemodel/mcp/templates/app/mcp/resources/docs/SAMPLE_DOC.md deleted file mode 100644 index 6e3268aa..00000000 --- a/lib/generators/rolemodel/mcp/templates/app/mcp/resources/docs/SAMPLE_DOC.md +++ /dev/null @@ -1,4 +0,0 @@ -# Hello World - -This is a sample doc that the MCP can return as a resource. -It's URI will be doc://SAMPLE_DOC.md as set in the handler. diff --git a/lib/generators/rolemodel/mcp/templates/app/mcp/resources/docs_controller.rb b/lib/generators/rolemodel/mcp/templates/app/mcp/resources/docs_controller.rb deleted file mode 100644 index 617ff3da..00000000 --- a/lib/generators/rolemodel/mcp/templates/app/mcp/resources/docs_controller.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module Resources - class DocsController < Controller - FILES = { - 'SAMPLE_DOC.md' => Rails.root.join('app/mcp/resources/docs/blazer-documentation.md'), - }.freeze - - mime_type 'text/markdown' - schema 'docs://' - - attribute :file_path - - validates :file_path, presence: { message: ->(controller, _) { "Unknown docs resource: #{controller.path}" } } - validate :file_exists - - def initialize(path, _server_context = nil) - super - self.file_path = FILES[path] - end - - def self.resource_list - [ - ::MCP::Resource.new( - uri: 'docs://SAMPLE_DOC.md', - name: 'sample_doc', - title: 'Sample Resource', - description: 'Sample resource', - mime_type: mime_type, - ), - ] - end - - def serve - file_path.read - end - - private - - def file_exists - return if file_path.blank? || file_path.exist? - - errors.add(:file_path, "Missing docs file for #{path}") - end - end -end diff --git a/lib/generators/rolemodel/mcp/templates/app/mcp/tools/sample.rb b/lib/generators/rolemodel/mcp/templates/app/mcp/tools/sample.rb deleted file mode 100644 index 9ec4b65f..00000000 --- a/lib/generators/rolemodel/mcp/templates/app/mcp/tools/sample.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module Tools - class Sample < ::MCP::Tool - tool_name 'sample_tool' - title 'Sample Tool' - description 'Sample tool description' - input_schema( - properties: { - name: { type: 'string', minLength: 1 }, - }, - required: ['name'], - ) - annotations( - read_only_hint: true, - destructive_hint: false, - idempotent_hint: true, - open_world_hint: false, - title: 'Sample Tool', - ) - - class << self - def call(name:, server_context:) - payload = payload_for(name) - - ::MCP::Tool::Response.new( - [{ type: 'text', text: payload.to_json }], - structured_content: { sample: payload }, - ) - end - - private - - def payload_for(name) - { - time: Time.current.iso8601, - user_count: User.count - } - end - end - end -end diff --git a/lib/generators/rolemodel/mcp/templates/app/views/doorkeeper/authorizations/error.html.slim.tt b/lib/generators/rolemodel/mcp/templates/app/views/doorkeeper/authorizations/error.html.slim.tt deleted file mode 100644 index 0994caad..00000000 --- a/lib/generators/rolemodel/mcp/templates/app/views/doorkeeper/authorizations/error.html.slim.tt +++ /dev/null @@ -1,13 +0,0 @@ -- content_for :page_title do - = '<%= application_name.titleize %> - Error Authorizing Application' - -main.doorkeeper role="main" - .doorkeeper__card.card.card--padded.card--shadow-medium - .doorkeeper__brand - = image_tag 'logo.svg', alt: '<%= application_name.titleize %>', class: 'doorkeeper__logo' - h1.doorkeeper__title= t('doorkeeper.authorizations.error.title') - - .doorkeeper__error.alert.alert--danger role="alert" - .alert__messages - p.doorkeeper__error-description.alert__description - = (local_assigns[:error_response] ? error_response : @pre_auth.error_response).body[:error_description] diff --git a/lib/generators/rolemodel/mcp/templates/app/views/doorkeeper/authorizations/new.html.slim.tt b/lib/generators/rolemodel/mcp/templates/app/views/doorkeeper/authorizations/new.html.slim.tt deleted file mode 100644 index cd0feb3d..00000000 --- a/lib/generators/rolemodel/mcp/templates/app/views/doorkeeper/authorizations/new.html.slim.tt +++ /dev/null @@ -1,41 +0,0 @@ -- content_for :page_title do - = '<%= application_name.titleize %> - Authorize Application' - -main.doorkeeper role="main" - .doorkeeper__card.card.card--padded.card--shadow-medium - .doorkeeper__brand - = image_tag 'logo.svg', alt: '<%= application_name.titleize %>', class: 'doorkeeper__logo' - h1.doorkeeper__title= t('.title') - - p.doorkeeper__prompt - == t('.prompt', client_name: content_tag(:strong, @pre_auth.client.name, class: 'doorkeeper__client-name')) - - - if @pre_auth.scopes.count > 0 - #oauth-permissions.doorkeeper__permissions - p.doorkeeper__permissions-label= t('.able_to') + ":" - ul.doorkeeper__scope-list - - @pre_auth.scopes.each do |scope| - li.doorkeeper__scope-item= t scope, scope: [:doorkeeper, :scopes] - - .doorkeeper__actions - = form_tag oauth_authorization_path, method: :post, class: 'doorkeeper__form', data: { turbo: false } do - = hidden_field_tag :client_id, @pre_auth.client.uid, id: nil - = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil - = hidden_field_tag :state, @pre_auth.state, id: nil - = hidden_field_tag :response_type, @pre_auth.response_type, id: nil - = hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil - = hidden_field_tag :scope, @pre_auth.scope, id: nil - = hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil - = hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil - = submit_tag t('doorkeeper.authorizations.buttons.authorize'), class: 'btn btn--primary doorkeeper__button' - - = form_tag oauth_authorization_path, method: :delete, class: 'doorkeeper__form' do - = hidden_field_tag :client_id, @pre_auth.client.uid, id: nil - = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil - = hidden_field_tag :state, @pre_auth.state, id: nil - = hidden_field_tag :response_type, @pre_auth.response_type, id: nil - = hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil - = hidden_field_tag :scope, @pre_auth.scope, id: nil - = hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil - = hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil - = submit_tag t('doorkeeper.authorizations.buttons.deny'), class: 'btn btn--destructive doorkeeper__button' diff --git a/lib/generators/rolemodel/mcp/templates/app/views/layouts/doorkeeper.html.slim b/lib/generators/rolemodel/mcp/templates/app/views/layouts/doorkeeper.html.slim deleted file mode 100644 index 45050508..00000000 --- a/lib/generators/rolemodel/mcp/templates/app/views/layouts/doorkeeper.html.slim +++ /dev/null @@ -1,7 +0,0 @@ -doctype html -html - / Use whatever partial you have for the head - = render 'head' - - body - = yield diff --git a/lib/generators/rolemodel/mcp/templates/config/initializers/doorkeeper.rb b/lib/generators/rolemodel/mcp/templates/config/initializers/doorkeeper.rb deleted file mode 100644 index d80626c3..00000000 --- a/lib/generators/rolemodel/mcp/templates/config/initializers/doorkeeper.rb +++ /dev/null @@ -1,537 +0,0 @@ -# frozen_string_literal: true - -Rails.application.config.to_prepare do - Doorkeeper::AuthorizationsController.layout 'doorkeeper' -end - -Doorkeeper.configure do - # Change the ORM that doorkeeper will use (requires ORM extensions installed). - # Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms - orm :active_record - - # Enable support for multiple database configurations with read replicas. - # When enabled, Doorkeeper will wrap database write operations to ensure they - # use the primary (writable) database when automatic role switching is enabled. - # - # For ActiveRecord (Rails 6.1+), this uses `ActiveRecord::Base.connected_to(role: :writing)`. - # Other ORM extensions can implement their own primary database targeting logic. - # - # enable_multiple_database_roles - # - # This prevents `ActiveRecord::ReadOnlyError` when using read replicas with Rails - # automatic role switching. Enable this if your application uses multiple databases - # with automatic role switching for read replicas. - # - # See: https://guides.rubyonrails.org/active_record_multiple_databases.html#activating-automatic-role-switching - - # This block will be called to check whether the resource owner is authenticated or not. - resource_owner_authenticator do - current_user || warden.authenticate!(scope: :user) - end - - # If you didn't skip applications controller from Doorkeeper routes in your application routes.rb - # file then you need to declare this block in order to restrict access to the web interface for - # adding oauth authorized applications. In other case it will return 403 Forbidden response - # every time somebody will try to access the admin web interface. - # - admin_authenticator do - current_user || warden.authenticate!(scope: :user) - end - - # You can use your own model classes if you need to extend (or even override) default - # Doorkeeper models such as `Application`, `AccessToken` and `AccessGrant. - # - # By default Doorkeeper ActiveRecord ORM uses its own classes: - # - # access_token_class "Doorkeeper::AccessToken" - # access_grant_class "Doorkeeper::AccessGrant" - # application_class "Doorkeeper::Application" - # - # Don't forget to include Doorkeeper ORM mixins into your custom models: - # - # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken - for access token - # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant - for access grant - # * ::Doorkeeper::Orm::ActiveRecord::Mixins::Application - for application (OAuth2 clients) - # - # For example: - # - # access_token_class "MyAccessToken" - # - # class MyAccessToken < ApplicationRecord - # include ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken - # - # self.table_name = "hey_i_wanna_my_name" - # - # def destroy_me! - # destroy - # end - # end - - # Enables polymorphic Resource Owner association for Access Tokens and Access Grants. - # By default this option is disabled. - # - # Make sure you properly setup you database and have all the required columns (run - # `bundle exec rails generate doorkeeper:enable_polymorphic_resource_owner` and execute Rails - # migrations). - # - # If this option enabled, Doorkeeper will store not only Resource Owner primary key - # value, but also it's type (class name). See "Polymorphic Associations" section of - # Rails guides: https://guides.rubyonrails.org/association_basics.html#polymorphic-associations - # - # [NOTE] If you apply this option on already existing project don't forget to manually - # update `resource_owner_type` column in the database and fix migration template as it will - # set NOT NULL constraint for Access Grants table. - # - # use_polymorphic_resource_owner - - # If you are planning to use Doorkeeper in Rails 5 API-only application, then you might - # want to use API mode that will skip all the views management and change the way how - # Doorkeeper responds to a requests. - # - # api_only - - # Enforce token request content type to application/x-www-form-urlencoded. - # It is not enabled by default to not break prior versions of the gem. - # - # enforce_content_type - - # Authorization Code expiration time (default: 10 minutes). - # - # authorization_code_expires_in 10.minutes - - # Access token expiration time (default: 2 hours). - # If you set this to `nil` Doorkeeper will not expire the token and omit expires_in in response. - # It is RECOMMENDED to set expiration time explicitly. - # Prefer access_token_expires_in 100.years or similar, - # which would be functionally equivalent and avoid the risk of unexpected behavior by callers. - # - # access_token_expires_in 2.hours - - # Assign custom TTL for access tokens. Will be used instead of access_token_expires_in - # option if defined. In case the block returns `nil` value Doorkeeper fallbacks to - # +access_token_expires_in+ configuration option value. If you really need to issue a - # non-expiring access token (which is not recommended) then you need to return - # Float::INFINITY from this block. - # - # `context` has the following properties available: - # - # * `client` - the OAuth client application (see Doorkeeper::OAuth::Client) - # * `grant_type` - the grant type of the request (see Doorkeeper::OAuth) - # * `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) - # * `resource_owner` - authorized resource owner instance (if present) - # - # custom_access_token_expires_in do |context| - # context.client.additional_settings.implicit_oauth_expiration - # end - - # Use a custom class for generating the access token. - # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-access-token-generator - # - # access_token_generator '::Doorkeeper::JWT' - - # The controller +Doorkeeper::ApplicationController+ inherits from. - # Defaults to +ActionController::Base+ unless +api_only+ is set, which changes the default to - # +ActionController::API+. The return value of this option must be a stringified class name. - # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-controllers - # - base_controller 'Doorkeeper::BaseController' - - # Reuse access token for the same resource owner within an application (disabled by default). - # - # This option protects your application from creating new tokens before old **valid** one becomes - # expired so your database doesn't bloat. Keep in mind that when this option is enabled Doorkeeper - # doesn't update existing token expiration time, it will create a new token instead if no active matching - # token found for the application, resources owner and/or set of scopes. - # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 - # - # You can not enable this option together with +hash_token_secrets+. - # - # reuse_access_token - - # In case you enabled `reuse_access_token` option Doorkeeper will try to find matching - # token using `matching_token_for` Access Token API that searches for valid records - # in batches in order not to pollute the memory with all the database records. By default - # Doorkeeper uses batch size of 10 000 records. You can increase or decrease this value - # depending on your needs and server capabilities. - # - # token_lookup_batch_size 10_000 - - # Set a limit for token_reuse if using reuse_access_token option - # - # This option limits token_reusability to some extent. - # If not set then access_token will be reused unless it expires. - # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1189 - # - # This option should be a percentage(i.e. (0,100]) - # - # token_reuse_limit 100 - - # Only allow one valid access token obtained via client credentials - # per client. If a new access token is obtained before the old one - # expired, the old one gets revoked (disabled by default) - # - # When enabling this option, make sure that you do not expect multiple processes - # using the same credentials at the same time (e.g. web servers spanning - # multiple machines and/or processes). - # - # revoke_previous_client_credentials_token - - # Only allow one valid access token obtained via authorization code - # per client. If a new access token is obtained before the old one - # expired, the old one gets revoked (disabled by default) - # - # revoke_previous_authorization_code_token - - # Require non-confidential clients to use PKCE when using an authorization code - # to obtain an access_token (disabled by default) - # - force_pkce - - # Hash access and refresh tokens before persisting them. - # This will disable the possibility to use +reuse_access_token+ - # since plain values can no longer be retrieved. - # - # Note: If you are already a user of doorkeeper and have existing tokens - # in your installation, they will be invalid without adding 'fallback: :plain'. - # - # hash_token_secrets - # By default, token secrets will be hashed using the - # +Doorkeeper::Hashing::SHA256+ strategy. - # - # If you wish to use another hashing implementation, you can override - # this strategy as follows: - # - # hash_token_secrets using: '::Doorkeeper::Hashing::MyCustomHashImpl' - # - # Keep in mind that changing the hashing function will invalidate all existing - # secrets, if there are any. - - # Hash application secrets before persisting them. - # - # hash_application_secrets - # - # By default, applications will be hashed - # with the +Doorkeeper::SecretStoring::SHA256+ strategy. - # - # If you wish to use bcrypt for application secret hashing, uncomment - # this line instead: - # - # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt' - - # When the above option is enabled, and a hashed token or secret is not found, - # you can allow to fall back to another strategy. For users upgrading - # doorkeeper and wishing to enable hashing, you will probably want to enable - # the fallback to plain tokens. - # - # This will ensure that old access tokens and secrets - # will remain valid even if the hashing above is enabled. - # - # This can be done by adding 'fallback: plain', e.g. : - # - # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt', fallback: :plain - - # Issue access tokens with refresh token (disabled by default), you may also - # pass a block which accepts `context` to customize when to give a refresh - # token or not. Similar to +custom_access_token_expires_in+, `context` has - # the following properties: - # - # `client` - the OAuth client application (see Doorkeeper::OAuth::Client) - # `grant_type` - the grant type of the request (see Doorkeeper::OAuth) - # `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) - # - use_refresh_token - - # Provide support for an owner to be assigned to each registered application (disabled by default) - # Optional parameter confirmation: true (default: false) if you want to enforce ownership of - # a registered application - # NOTE: you must also run the rails g doorkeeper:application_owner generator - # to provide the necessary support - # - # enable_application_owner confirmation: false - - # Define access token scopes for your provider - # For more information go to - # https://doorkeeper.gitbook.io/guides/ruby-on-rails/scopes - # - default_scopes :mcp - - # Allows to restrict only certain scopes for grant_type. - # By default, all the scopes will be available for all the grant types. - # - # Keys to this hash should be the name of grant_type and - # values should be the array of scopes for that grant type. - # Note: scopes should be from configured_scopes (i.e. default or optional) - # - # scopes_by_grant_type password: [:write], client_credentials: [:update] - - # Forbids creating/updating applications with arbitrary scopes that are - # not in configuration, i.e. +default_scopes+ or +optional_scopes+. - # (disabled by default) - # - # enforce_configured_scopes - - # Change the way client credentials are retrieved from the request object. - # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then - # falls back to the `:client_id` and `:client_secret` params from the `params` object. - # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated - # for more information on customization - # - # client_credentials :from_basic, :from_params - - # Change the way access token is authenticated from the request object. - # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then - # falls back to the `:access_token` or `:bearer_token` params from the `params` object. - # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated - # for more information on customization - # - # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param - - # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled - # by default in non-development environments). OAuth2 delegates security in - # communication to the HTTPS protocol so it is wise to keep this enabled. - # - # Callable objects such as proc, lambda, block or any object that responds to - # #call can be used in order to allow conditional checks (to allow non-SSL - # redirects to localhost for example). - # - force_ssl_in_redirect_uri do |uri| - Rails.env.production? && !(uri.host == 'localhost' || uri.host == '127.0.0.1' || uri.host == '::1') - end - - # Specify what redirect URI's you want to block during Application creation. - # Any redirect URI is allowed by default. - # - # You can use this option in order to forbid URI's with 'javascript' scheme - # for example. - # - # forbid_redirect_uri { |uri| uri.scheme.to_s.downcase == 'javascript' } - - # Allows to set blank redirect URIs for Applications in case Doorkeeper configured - # to use URI-less OAuth grant flows like Client Credentials or Resource Owner - # Password Credentials. The option is on by default and checks configured grant - # types, but you **need** to manually drop `NOT NULL` constraint from `redirect_uri` - # column for `oauth_applications` database table. - # - # You can completely disable this feature with: - # - # allow_blank_redirect_uri false - # - # Or you can define your custom check: - # - # allow_blank_redirect_uri do |grant_flows, client| - # client.superapp? - # end - - # Specify how authorization errors should be handled. - # By default, doorkeeper renders json errors when access token - # is invalid, expired, revoked or has invalid scopes. - # - # If you want to render error response yourself (i.e. rescue exceptions), - # set +handle_auth_errors+ to `:raise` and rescue Doorkeeper::Errors::InvalidToken - # or following specific errors: - # - # Doorkeeper::Errors::TokenForbidden, Doorkeeper::Errors::TokenExpired, - # Doorkeeper::Errors::TokenRevoked, Doorkeeper::Errors::TokenUnknown - # - # handle_auth_errors :raise - # - # If you want to redirect back to the client application in accordance with - # https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1, you can set - # +handle_auth_errors+ to :redirect - # - # handle_auth_errors :redirect - - # Customize token introspection response. - # Allows to add your own fields to default one that are required by the OAuth spec - # for the introspection response. It could be `sub`, `aud` and so on. - # This configuration option can be a proc, lambda or any Ruby object responds - # to `.call` method and result of it's invocation must be a Hash. - # - # custom_introspection_response do |token, context| - # { - # "sub": "Z5O3upPC88QrAjx00dis", - # "aud": "https://protected.example.net/resource", - # "username": User.find(token.resource_owner_id).username - # } - # end - # - # or - # - # custom_introspection_response CustomIntrospectionResponder - - # Specify what grant flows are enabled in array of Strings. The valid - # strings and the flows they enable are: - # - # "authorization_code" => Authorization Code Grant Flow - # "implicit" => Implicit Grant Flow - # "password" => Resource Owner Password Credentials Grant Flow - # "client_credentials" => Client Credentials Grant Flow - # - # If not specified, Doorkeeper enables authorization_code and - # client_credentials. - # - # implicit and password grant flows have risks that you should understand - # before enabling: - # https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.2 - # https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.3 - # - # grant_flows %w[authorization_code client_credentials] - - # Allows to customize OAuth grant flows that +each+ application support. - # You can configure a custom block (or use a class respond to `#call`) that must - # return `true` in case Application instance supports requested OAuth grant flow - # during the authorization request to the server. This configuration +doesn't+ - # set flows per application, it only allows to check if application supports - # specific grant flow. - # - # For example you can add an additional database column to `oauth_applications` table, - # say `t.array :grant_flows, default: []`, and store allowed grant flows that can - # be used with this application there. Then when authorization requested Doorkeeper - # will call this block to check if specific Application (passed with client_id and/or - # client_secret) is allowed to perform the request for the specific grant type - # (authorization, password, client_credentials, etc). - # - # Example of the block: - # - # ->(flow, client) { client.grant_flows.include?(flow) } - # - # In case this option invocation result is `false`, Doorkeeper server returns - # :unauthorized_client error and stops the request. - # - # @param allow_grant_flow_for_client [Proc] Block or any object respond to #call - # @return [Boolean] `true` if allow or `false` if forbid the request - # - # allow_grant_flow_for_client do |grant_flow, client| - # # `grant_flows` is an Array column with grant - # # flows that application supports - # - # client.grant_flows.include?(grant_flow) - # end - - # If you need arbitrary Resource Owner-Client authorization you can enable this option - # and implement the check your need. Config option must respond to #call and return - # true in case resource owner authorized for the specific application or false in other - # cases. - # - # By default all Resource Owners are authorized to any Client (application). - # - # authorize_resource_owner_for_client do |client, resource_owner| - # resource_owner.admin? || client.owners_allowlist.include?(resource_owner) - # end - - # Allows additional data fields to be sent while granting access to an application, - # and for this additional data to be included in subsequently generated access tokens. - # The 'authorizations/new' page will need to be overridden to include this additional data - # in the request params when granting access. The access grant and access token models - # will both need to respond to these additional data fields, and have a database column - # to store them in. - # - # Example: - # You have a multi-tenanted platform and want to be able to grant access to a specific - # tenant, rather than all the tenants a user has access to. You can use this config - # option to specify that a ':tenant_id' will be passed when authorizing. This tenant_id - # will be included in the access tokens. When a request is made with one of these access - # tokens, you can check that the requested data belongs to the specified tenant. - # - # Default value is an empty Array: [] - # custom_access_token_attributes [:tenant_id] - - # Hook into the strategies' request & response life-cycle in case your - # application needs advanced customization or logging: - # - # before_successful_strategy_response do |request| - # puts "BEFORE HOOK FIRED! #{request}" - # end - # - # after_successful_strategy_response do |request, response| - # puts "AFTER HOOK FIRED! #{request}, #{response}" - # end - - # Hook into Authorization flow in order to implement Single Sign Out - # or add any other functionality. Inside the block you have an access - # to `controller` (authorizations controller instance) and `context` - # (Doorkeeper::OAuth::Hooks::Context instance) which provides pre auth - # or auth objects with issued token based on hook type (before or after). - # - # before_successful_authorization do |controller, context| - # Rails.logger.info(controller.request.params.inspect) - # - # Rails.logger.info(context.pre_auth.inspect) - # end - # - # after_successful_authorization do |controller, context| - # controller.session[:logout_urls] << - # Doorkeeper::Application - # .find_by(controller.request.params.slice(:redirect_uri)) - # .logout_uri - # - # Rails.logger.info(context.auth.inspect) - # Rails.logger.info(context.issued_token) - # end - - # Under some circumstances you might want to have applications auto-approved, - # so that the user skips the authorization step. - # For example if dealing with a trusted application. - # - # skip_authorization do |resource_owner, client| - # client.superapp? or resource_owner.admin? - # end - - # Configure custom constraints for the Token Introspection request. - # By default this configuration option allows to introspect a token by another - # token of the same application, OR to introspect the token that belongs to - # authorized client (from authenticated client) OR when token doesn't - # belong to any client (public token). Otherwise requester has no access to the - # introspection and it will return response as stated in the RFC. - # - # Block arguments: - # - # @param token [Doorkeeper::AccessToken] - # token to be introspected - # - # @param authorized_client [Doorkeeper::Application] - # authorized client (if request is authorized using Basic auth with - # Client Credentials for example) - # - # @param authorized_token [Doorkeeper::AccessToken] - # Bearer token used to authorize the request - # - # In case the block returns `nil` or `false` introspection responses with 401 status code - # when using authorized token to introspect, or you'll get 200 with { "active": false } body - # when using authorized client to introspect as stated in the - # RFC 7662 section 2.2. Introspection Response. - # - # Using with caution: - # Keep in mind that these three parameters pass to block can be nil as following case: - # `authorized_client` is nil if and only if `authorized_token` is present, and vice versa. - # `token` will be nil if and only if `authorized_token` is present. - # So remember to use `&` or check if it is present before calling method on - # them to make sure you doesn't get NoMethodError exception. - # - # You can define your custom check: - # - # allow_token_introspection do |token, authorized_client, authorized_token| - # if authorized_token - # # customize: require `introspection` scope - # authorized_token.application == token&.application || - # authorized_token.scopes.include?("introspection") - # elsif token.application - # # `protected_resource` is a new database boolean column, for example - # authorized_client == token.application || authorized_client.protected_resource? - # else - # # public token (when token.application is nil, token doesn't belong to any application) - # true - # end - # end - # - # Or you can completely disable any token introspection: - # - # allow_token_introspection false - # - # If you need to block the request at all, then configure your routes.rb or web-server - # like nginx to forbid the request. - - # WWW-Authenticate Realm (default: "Doorkeeper"). - # - # realm "Doorkeeper" -end diff --git a/lib/generators/rolemodel/mcp/templates/spec/mcp/prompts/sample_spec.rb b/lib/generators/rolemodel/mcp/templates/spec/mcp/prompts/sample_spec.rb deleted file mode 100644 index 6c9e1e9f..00000000 --- a/lib/generators/rolemodel/mcp/templates/spec/mcp/prompts/sample_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Prompts::Sample do - describe '.template' do - subject(:result) { described_class.template({}) } - - it 'has the correct description' do - expect(result.messages.length).to eq(1) - expect(result.messages.first.role).to eq('assistant') - expect(result.description).to include('This is a sample prompt.') - end - end -end diff --git a/lib/generators/rolemodel/mcp/templates/spec/mcp/resources/controller_spec.rb b/lib/generators/rolemodel/mcp/templates/spec/mcp/resources/controller_spec.rb deleted file mode 100644 index c530958d..00000000 --- a/lib/generators/rolemodel/mcp/templates/spec/mcp/resources/controller_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Resources::Controller do - it 'returns error messages when invalid' do - allow_any_instance_of(described_class).to receive(:valid?).and_return(false) - allow_any_instance_of(described_class).to receive_message_chain(:errors, :full_messages).and_return(['Invalid params']) - expect do - described_class.call({ uri: 'docs://missing-doc' }, { current_user: nil }) - end.to raise_error( - MCP::Server::RequestHandlerError, - /Invalid params/ - ) - end -end diff --git a/lib/generators/rolemodel/mcp/templates/spec/mcp/resources/docs_controller_spec.rb b/lib/generators/rolemodel/mcp/templates/spec/mcp/resources/docs_controller_spec.rb deleted file mode 100644 index 887406ed..00000000 --- a/lib/generators/rolemodel/mcp/templates/spec/mcp/resources/docs_controller_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Resources::DocsController do - describe 'class methods' do - it 'returns the correct values' do - expect(described_class.schema).to eq('docs://') - expect(described_class.mime_type).to eq('text/markdown') - end - end - - describe '.resource_list' do - it 'registers the sample resource' do - expect(described_class.resource_list.map(&:uri)).to contain_exactly('docs://SAMPLE_DOC.md') - end - end - - describe 'validations' do - it 'is valid for a known docs resource' do - controller = described_class.new('SAMPLE_DOC.md') - - expect(controller).to be_valid - end - - it 'is invalid for an unknown docs resource' do - controller = described_class.new('missing-doc') - - expect(controller).not_to be_valid - expect(controller.errors[:file_path]).to eq(['Unknown docs resource: missing-doc']) - end - - it 'is invalid when the mapped file is missing' do - stub_const( - 'Resources::DocsController::FILES', - { 'SAMPLE_DOC.md' => Rails.root.join('app/mcp/resources/docs/missing.md') }.freeze - ) - - controller = described_class.new('SAMPLE_DOC.md') - - expect(controller).not_to be_valid - expect(controller.errors[:file_path]).to eq(['Missing docs file for SAMPLE_DOC.md']) - end - end - - describe '#serve' do - it 'returns the markdown for the requested docs resource' do - controller = described_class.new('SAMPLE_DOC.md') - - content = controller.serve - - expect(content).to include('# Hello World') - end - end -end diff --git a/lib/generators/rolemodel/mcp/templates/spec/mcp/tools/sample_spec.rb b/lib/generators/rolemodel/mcp/templates/spec/mcp/tools/sample_spec.rb deleted file mode 100644 index 680a22af..00000000 --- a/lib/generators/rolemodel/mcp/templates/spec/mcp/tools/sample_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Tools::Sample do - describe '.call' do - it 'returns a hash with expected fields' do - result = described_class.call(name: 'Alice', server_context: {}) - - expect(result).not_to be_error - - expect(result.structured_content).to have_key(:sample) - end - end -end diff --git a/lib/generators/rolemodel/mcp/templates/spec/requests/mcp_controller_spec.rb b/lib/generators/rolemodel/mcp/templates/spec/requests/mcp_controller_spec.rb deleted file mode 100644 index c412155a..00000000 --- a/lib/generators/rolemodel/mcp/templates/spec/requests/mcp_controller_spec.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'MCPController', type: :request do - let(:user) { create(:user, name: 'Jane Smith', active: true, employee: true) } - let(:application) do - Doorkeeper::Application.create!( - name: 'MCP Test Client', - redirect_uri: 'https://example.com/callback', - scopes: 'mcp', - ) - end - - describe 'POST /mcp' do - let(:token) do - Doorkeeper::AccessToken.create!( - application: application, - resource_owner_id: user.id, - scopes: 'mcp', - expires_in: 2.hours.to_i, - ) - end - - let(:initialize_request) do - { - jsonrpc: '2.0', - id: 1, - method: 'initialize', - params: { - protocolVersion: '2025-11-25', - capabilities: {}, - clientInfo: { - name: 'rspec', - version: '1.0', - }, - }, - } - end - - let(:headers) do - { - 'CONTENT_TYPE' => 'application/json', - 'ACCEPT' => 'application/json, text/event-stream', - } - end - - let(:authorized_headers) do - headers.merge('Authorization' => "Bearer #{token.token}") - end - - it 'returns unauthorized when no bearer token is provided' do - post '/mcp', params: initialize_request.to_json, headers: headers - - expect(response).to have_http_status(:unauthorized) - expect(response.headers['WWW-Authenticate']).to include('resource_metadata=') - end - - it 'returns forbidden when token does not include mcp scope' do - token = Doorkeeper::AccessToken.create!( - application: application, - resource_owner_id: user.id, - scopes: 'other', - expires_in: 2.hours.to_i, - ) - - post '/mcp', - params: initialize_request.to_json, - headers: headers.merge('Authorization' => "Bearer #{token.token}") - - expect(response).to have_http_status(:forbidden) - end - - it 'returns initialize response for a valid bearer token' do - post '/mcp', - params: initialize_request.to_json, - headers: authorized_headers - - expect(response).to have_http_status(:ok) - expect(response.parsed_body['result']).to be_present - expect(response.parsed_body['result']['capabilities']).to include('tools') - end - end -end diff --git a/lib/generators/rolemodel/mcp/templates/spec/requests/oauth_registrations_controller_spec.rb b/lib/generators/rolemodel/mcp/templates/spec/requests/oauth_registrations_controller_spec.rb deleted file mode 100644 index de58c9d8..00000000 --- a/lib/generators/rolemodel/mcp/templates/spec/requests/oauth_registrations_controller_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'OauthRegistrationsController', type: :request do - describe 'POST /oauth/register' do - let(:params) do - { - client_name: 'GitHub Copilot', - redirect_uris: ['http://localhost:12345/callback'], - grant_types: ['authorization_code'], - response_types: ['code'], - token_endpoint_auth_method: 'none', - scope: 'mcp', - } - end - - it 'creates an OAuth application and returns client_id' do - expect { post '/oauth/register', params: params, as: :json } - .to change(Doorkeeper::Application, :count).by(1) - - expect(response).to have_http_status(:created) - body = response.parsed_body - expect(body['client_id']).to be_present - expect(body['client_name']).to eq('GitHub Copilot') - expect(body['redirect_uris']).to eq(['http://localhost:12345/callback']) - expect(body['token_endpoint_auth_method']).to eq('none') - expect(body).not_to have_key('client_secret') - - app = Doorkeeper::Application.last - expect(app.name).to eq('GitHub Copilot') - expect(app.redirect_uri).to eq('http://localhost:12345/callback') - expect(app.scopes).to contain_exactly('mcp') - expect(app).not_to be_confidential - end - - it 'returns bad_request when redirect_uris is missing' do - post '/oauth/register', params: { client_name: 'Test' }, as: :json - - expect(response).to have_http_status(:bad_request) - expect(response.parsed_body['error']).to eq('invalid_client_metadata') - end - - it 'creates a confidential client when token_endpoint_auth_method is not none' do - params[:token_endpoint_auth_method] = 'client_secret_basic' - post '/oauth/register', params: params, as: :json - - expect(response).to have_http_status(:created) - body = response.parsed_body - expect(body['client_secret']).to be_present - expect(body['token_endpoint_auth_method']).to eq('client_secret_basic') - end - - it 'allows loopback redirect URIs for native clients' do - params[:redirect_uris] = ['http://127.0.0.1:33418/'] - post '/oauth/register', params: params, as: :json - - expect(response).to have_http_status(:created) - expect(response.parsed_body['redirect_uris']).to eq(['http://127.0.0.1:33418/']) - end - end -end diff --git a/lib/generators/rolemodel/mcp/templates/spec/requests/well_known_controller_spec.rb b/lib/generators/rolemodel/mcp/templates/spec/requests/well_known_controller_spec.rb deleted file mode 100644 index c7149c4f..00000000 --- a/lib/generators/rolemodel/mcp/templates/spec/requests/well_known_controller_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'WellKnownController', type: :request do - describe 'GET /.well-known/oauth-protected-resource' do - it 'returns the MCP resource and authorization server' do - get '/.well-known/oauth-protected-resource' - - expect(response).to have_http_status(:ok) - body = response.parsed_body - expect(body['resource']).to end_with('/mcp') - expect(body['authorization_servers']).to be_an(Array) - end - end - - describe 'GET /.well-known/oauth-authorization-server' do - it 'returns authorization server metadata with registration_endpoint' do - get '/.well-known/oauth-authorization-server' - - expect(response).to have_http_status(:ok) - body = response.parsed_body - expect(body['registration_endpoint']).to end_with('/oauth/register') - expect(body['authorization_endpoint']).to end_with('/oauth/authorize') - expect(body['token_endpoint']).to end_with('/oauth/token') - expect(body['code_challenge_methods_supported']).to include('S256') - expect(body['scopes_supported']).to include('mcp') - end - end -end diff --git a/lib/rolemodel/version.rb b/lib/rolemodel/version.rb index 1a8d23a7..c944f749 100644 --- a/lib/rolemodel/version.rb +++ b/lib/rolemodel/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Rolemodel - VERSION = '1.1.0' + VERSION = '1.2.0' end diff --git a/spec/generators/rolemodel/mcp_generator_spec.rb b/spec/generators/rolemodel/mcp_generator_spec.rb deleted file mode 100644 index a24dd6ff..00000000 --- a/spec/generators/rolemodel/mcp_generator_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -RSpec.describe Rolemodel::McpGenerator, type: :generator do - before { run_generator_against_test_app } - - it 'adds the MCP controller' do - assert_file 'app/controllers/mcp_controller.rb' do |content| - expect(content).to include('Example Rails Current') - expect(content).to include('example_rails_current') - end - assert_file 'spec/requests/mcp_controller_spec.rb' - - assert_file 'config/routes.rb' do |content| - expect(content).to include("match '/mcp', to: 'mcp#handle', via: %i[get post delete]") - end - assert_file 'Gemfile' do |content| - expect(content).to include('gem "mcp"') - end - end - - it 'adds sample MCP resource, tool, and prompt' do - assert_file 'app/mcp/resources/docs/SAMPLE_DOC.md' - assert_file 'app/mcp/resources/controller.rb' - assert_file 'spec/mcp/resources/controller_spec.rb' - assert_file 'app/mcp/resources/docs_controller.rb' - assert_file 'spec/mcp/resources/docs_controller_spec.rb' - assert_file 'app/mcp/prompts/sample.rb' - assert_file 'spec/mcp/prompts/sample_spec.rb' - assert_file 'app/mcp/tools/sample.rb' - assert_file 'spec/mcp/tools/sample_spec.rb' - end - - it 'adds doorkeeper' do - assert_file 'Gemfile' do |content| - expect(content).to include('gem "doorkeeper"') - end - assert_file 'config/initializers/doorkeeper.rb' - assert_file 'config/locales/doorkeeper.en.yml' - assert_file 'app/views/layouts/doorkeeper.html.slim' - assert_file 'app/views/doorkeeper/authorizations/new.html.slim' do |content| - expect(content).to include('Example Rails Current - Authorize Application') - end - assert_file 'app/views/doorkeeper/authorizations/error.html.slim' do |content| - expect(content).to include('Example Rails Current - Error Authorizing Application') - end - assert_file 'app/controllers/doorkeeper/base_controller.rb' - assert_file 'app/assets/stylesheets/components/doorkeeper.css' - - assert_file 'config/routes.rb' do |content| - expect(content).to include('use_doorkeeper') - end - assert_file 'app/assets/stylesheets/application.css' do |content| - expect(content).to include("@import 'components/doorkeeper.css';") - end - end - - it 'adds the dynamic registration controller' do - assert_file 'app/controllers/oauth_registrations_controller.rb' - assert_file 'spec/requests/oauth_registrations_controller_spec.rb' - - assert_file 'config/routes.rb' do |content| - expect(content).to include("post '/oauth/register', to: 'oauth_registrations#create'") - end - end - - it 'adds the well-known route' do - assert_file 'app/controllers/well_known_controller.rb' - assert_file 'spec/requests/well_known_controller_spec.rb' - - assert_file 'config/routes.rb' do |content| - expect(content).to include("get '/.well-known/oauth-protected-resource', to: 'well_known#oauth_protected_resource'") - expect(content).to include("get '/.well-known/oauth-authorization-server', to: 'well_known#oauth_authorization_server'") - end - end - - it 'updates inflections' do - assert_file 'config/initializers/inflections.rb' do |content| - expect(content).to include("\nActiveSupport::Inflector.inflections(:en) do |inflect|") - expect(content).to include(" inflect.acronym 'MCP'") - end - end -end