A Rails application template that integrates the Model Context Protocol (MCP) with Ruby on Rails. During setup, the template asks whether to add OAuth 2.0 protection using Devise and Doorkeeper — so one template supports both plain MCP and fully authenticated setups.
git clone https://github.com/pstrzalk/mcp-on-rails.git
rails new myapp -m mcp-on-rails-oauth/mcp
cd myapp
rails db:migrate
rails serverThe template will prompt:
Add Devise + Doorkeeper OAuth 2.0 authentication? (y/n)
Answer n for a plain MCP server or y for full OAuth protection.
You may just as easily apply this template to an existing Rails app.
git clone https://github.com/pstrzalk/mcp-on-rails.git
cd your-project/
rails app:template LOCATION=../mcp-on-rails/mcpCreates a Rails app with an open MCP server — no authentication required.
mcpgem added to GemfileMcpControllerat/mcp— inheritsActionController::API, handles MCP protocol- MCP routes —
POST /mcp,GET /mcp,DELETE /mcp,OPTIONS /mcp - Scaffold hook —
rails generate scaffoldautomatically creates MCP tools - Custom tool generator —
rails generate mcp_tool ToolName field:type to_mcp_responseon ApplicationRecord for consistent text formattingrake mcp:toolsto list all registered tools
rails new myapp -m mcp-on-rails-oauth/mcp # answer n
cd myapp && rails db:migrate
rails generate scaffold Post title:string body:text
rails db:migrate
rails serverTest it:
# MCP initialize
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
# List tools
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'app/
├── controllers/
│ └── mcp_controller.rb # Open MCP endpoint (no auth)
├── models/
│ └── application_record.rb # Extended with to_mcp_response
└── tools/ # MCP tools (auto-generated per scaffold)
config/
├── initializers/
│ └── mcp.rb # MCP tool autoloading
└── routes.rb # MCP routes
{
"name": "my-rails-app",
"type": "StreamableHttp",
"url": "http://localhost:3000/mcp"
}No authentication needed — the /mcp endpoint is open.
Creates a Rails app with an OAuth 2.0-protected MCP server, including PKCE, dynamic client registration, and resource indicator support — everything needed for MCP's OAuth authorization flow.
Everything from plain mode, plus:
devise+doorkeepergems added to Gemfile- Devise user authentication (sign-up, sign-in, password reset)
- Doorkeeper OAuth 2.0 provider with PKCE enforcement (S256)
McpControllerprotected bydoorkeeper_authorize!with token audience validation (RFC 8707)- Dynamic client registration at
POST /oauth/register(RFC 7591) - Protected resource metadata at
GET /.well-known/oauth-protected-resource(RFC 9728) - Authorization server metadata at
GET /.well-known/oauth-authorization-server(RFC 8414) - Resource indicators — tokens are scoped to the
/mcpresource (RFC 8707)
rails new myapp -m mcp-on-rails-oauth/mcp # answer y
cd myapp && rails db:migrate
rails generate scaffold Post title:string body:text
rails db:migrate
rails serverThe /mcp endpoint now requires a Bearer token — unauthenticated requests return 401.
The full authorization flow follows MCP's OAuth specification:
- Discovery — Client fetches
GET /.well-known/oauth-protected-resourceto find the authorization server - Server metadata — Client fetches
GET /.well-known/oauth-authorization-serverfor endpoints and capabilities - Client registration —
POST /oauth/registerwith client metadata (RFC 7591) - Authorization —
GET /oauth/authorizewith PKCEcode_challenge(S256) andresourceparameter - User authentication — Devise handles sign-in/sign-up
- Token exchange —
POST /oauth/tokenwithcode_verifierandresourceparameter - MCP requests —
POST /mcpwithAuthorization: Bearer <token>
| RFC | Description | Endpoint |
|---|---|---|
| OAuth 2.0 + PKCE | Authorization with Proof Key for Code Exchange (S256) | /oauth/authorize, /oauth/token |
| RFC 7591 | Dynamic Client Registration | POST /oauth/register |
| RFC 8414 | Authorization Server Metadata | GET /.well-known/oauth-authorization-server |
| RFC 8707 | Resource Indicators | resource parameter in auth + token requests |
| RFC 9728 | Protected Resource Metadata | GET /.well-known/oauth-protected-resource |
app/
├── controllers/
│ ├── mcp_controller.rb # OAuth-protected MCP endpoint
│ ├── oauth_client_registration_controller.rb # RFC 7591
│ └── oauth_authorization_server_metadata_controller.rb # RFC 8414 + 9728
├── models/
│ ├── user.rb # Devise user with OAuth associations
│ ├── oauth_application.rb
│ ├── oauth_access_token.rb
│ └── oauth_access_grant.rb
├── tools/ # MCP tools (auto-generated per scaffold)
└── views/
└── devise/ # Customizable auth views
config/
├── initializers/
│ ├── doorkeeper.rb # OAuth + PKCE config
│ ├── devise.rb # User auth config
│ └── mcp.rb # MCP tool autoloading
└── routes.rb # All OAuth + MCP routes
db/migrate/
├── *_devise_create_users.rb
├── *_create_doorkeeper_tables.rb
├── *_enable_pkce.rb
└── *_add_resource_to_oauth_tables.rb
{
"name": "my-rails-app",
"type": "StreamableHttp",
"url": "http://localhost:3000/mcp"
}The client must complete the OAuth PKCE flow before making MCP requests — the /mcp endpoint returns 401 without a valid Bearer token.
rails generate scaffold Post title:string content:text
rails db:migrateThis creates standard Rails files plus 5 MCP tools in app/tools/posts/:
show_tool.rb— Retrieve a single post by IDindex_tool.rb— List posts with paginationcreate_tool.rb— Create new postsupdate_tool.rb— Update existing postsdelete_tool.rb— Delete posts
rails generate mcp_tool WeatherCheck location:stringrails generate mcp_prompt hotel_finder location:required check_in_date:required adults price_maxThis creates app/prompts/hotel_finder.rb with a prompt class inheriting from MCP::Prompt. Arguments are optional by default — append :required to make them required.
Prompts are automatically loaded from app/prompts/ and registered with the MCP server. Unlike tools, prompts are not auto-generated during scaffolding — they are created explicitly via the generator.
rake mcp:tools
rake mcp:promptsMIT License