mcp-rails 0.4.11

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b6e405c23ef4fae7964591eb567ee9f383b436f3d602cd02b2f2d088ba5c256a
4
+ data.tar.gz: 55ee2b62e45a44c2b6f9a1a9b172414c8bfbb5c98db5e0b68132594df81b9f87
5
+ SHA512:
6
+ metadata.gz: 88491aab2b61efd838a5e32ee9da92eebef4c642fd294200a125071d853916d3e97dcf2d983ef373321ca9cee2a10ab010b1fa945fb8438a19185dcb13e4ef94
7
+ data.tar.gz: 03aa0d2516a2144794fc9b6c6328734a45f23debc92ef6b19ee926db2a13b942262dbc2cc9902a883dd8facc6dd9512d6fba7e9065f712568fa1903231aebe26
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Tonksthebear
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,403 @@
1
+ # MCP-Rails
2
+
3
+ **Enhance Rails routing and parameter handling for LLM agents with MCP (Machine Control Protocol) integration.**
4
+
5
+ `mcp-rails` is a Ruby on Rails gem that builds on top of the [mcp-rb](https://github.com/funwarioisii/mcp-rb) library to seamlessly integrate MCP (Model Context Protocol) servers into your Rails application. It enhances Rails routes by allowing you to tag them with MCP-specific metadata and generates a valid Ruby MCP server (in `tmp/server.rb`) that LLM agents, such as Goose, can connect to. Additionally, it provides a powerful way to define and manage strong parameters in your controllers, which doubles as both MCP server configuration and Rails strong parameter enforcement.
6
+
7
+ This was inspired during the creation of [Gaggle](https://github.com/Tonksthebear/gaggle).
8
+
9
+ ---
10
+
11
+ ## Features
12
+
13
+ - **Tagged Routes**: Tag Rails routes with `mcp: true` or specific actions (e.g., `mcp: [:index, :create]`) to expose them to an MCP server.
14
+ - **Automatic MCP Server Generation**: Generates a Ruby MCP server in `tmp/server.rb` for LLM agents to interact with your application.
15
+ - **Parameter Definition**: Define permitted parameters in controllers with rich metadata (e.g., types, examples, required flags) that are used for both MCP server generation and Rails strong parameters.
16
+ - **HTTP Bridge for LLM Agents**: Generates a ruby based MCP server to interact with your application through HTTP requests, ensuring LLM agents follow the same paths as human users.
17
+ - **Environment Variable Integration**: Automatically includes specified environment variables in MCP tool calls.
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ Add this line to your application's `Gemfile`:
24
+
25
+ ```ruby
26
+ gem 'mcp-rails'
27
+ ```
28
+
29
+ Then run:
30
+
31
+ ```bash
32
+ bundle install
33
+ ```
34
+
35
+ Ensure you also have the `mcp-rb` gem installed, as `mcp-rails` depends on it:
36
+
37
+ ```ruby
38
+ gem 'mcp-rb'
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Configuration
44
+
45
+ MCP-Rails can be configured in an initializer file. Create `config/initializers/mcp_rails.rb`:
46
+
47
+ ```ruby
48
+ MCP::Rails.configure do |config|
49
+ # Server Configuration
50
+ config.server_name = "my-app-server" # Default: 'mcp-server'
51
+ config.server_version = "2.0.0" # Default: '1.0.0'
52
+
53
+ # Output Configuration
54
+ config.output_directory = Rails.root.join("tmp/mcp") # Default: Rails.root.join('tmp', 'mcp')
55
+ config.bypass_key_path = Rails.root.join("tmp/mcp/bypass_key.txt") # Default: Rails.root.join('tmp', 'mcp', 'bypass_key.txt')
56
+
57
+ # Environment Variables
58
+ config.env_vars = ["API_KEY", "ORGANIZATION_ID"] # Default: ['API_KEY', 'ORGANIZATION_ID']
59
+
60
+ # Base URL Configuration
61
+ config.base_url = "http://localhost:3000" # Default: Uses action_mailer.default_url_options
62
+ end
63
+ ```
64
+
65
+ If you are an engine developer, you can register your engine's configuration with MCP-Rails:
66
+
67
+ ```ruby
68
+ MCP::Rails.configure do |config|
69
+ config.register_engine(YourEngine, env_vars: ["YOUR_ENGINE_KEY"])
70
+ end
71
+ ```
72
+
73
+ ### Environment Variables
74
+
75
+ The `env_vars` configuration option specifies which environment variables should be automatically included in every MCP tool call. For example, if you configure:
76
+
77
+ ```ruby
78
+ config.env_vars = ["API_KEY", "ORGANIZATION_ID"]
79
+ ```
80
+
81
+ Then every MCP tool call will automatically include these environment variables as parameters:
82
+
83
+ ```ruby
84
+ # If ENV['API_KEY'] = 'xyz123' and ENV['ORGANIZATION_ID'] = '456'
85
+ # A tool call like:
86
+ create_channel(name: "General")
87
+ # Will effectively become:
88
+ create_channel(name: "General", api_key: "xyz123", organization_id: "456")
89
+ ```
90
+
91
+ This is useful for automatically including authentication tokens, organization IDs, or other environment-specific values in your MCP tool calls without explicitly defining them in your controller parameters.
92
+
93
+ ### Base URL
94
+
95
+ The base URL for API requests is determined in the following order:
96
+ 1. Custom configuration via `config.base_url = "http://example.com"`
97
+ 2. Rails `action_mailer.default_url_options` settings
98
+ 3. Default fallback to `"http://localhost:3000"`
99
+
100
+ ---
101
+
102
+ ## Usage
103
+
104
+ ### 1. Tagging Routes
105
+
106
+ In your `config/routes.rb`, tag routes that should be exposed to the MCP server:
107
+
108
+ ```ruby
109
+ Rails.application.routes.draw do
110
+ resources :channels, mcp: true # Exposes all RESTful actions to MCP
111
+ # OR
112
+ resources :channels, mcp: [:index, :create] # Exposes only specified actions
113
+ end
114
+ ```
115
+
116
+ ### 2. Defining Parameters
117
+
118
+ In your controllers, use the `permitted_params_for` DSL to define parameters for MCP actions. These definitions serve a dual purpose: they configure the MCP server and enable strong parameter enforcement in Rails.
119
+
120
+ Example:
121
+
122
+ ```ruby
123
+ class ChannelsController < ApplicationController
124
+ # Define parameters for the :create action
125
+ permitted_params_for :create do
126
+ param :channel, required: true do
127
+ param :name, type: :string, example: "Channel Name", required: true
128
+ param :user_ids, type: :array, item_type: :string, example: ["1", "2"]
129
+ end
130
+ end
131
+
132
+ def create
133
+ @channel = Channel.new(resource_params) # Automatically uses the defined params
134
+ if @channel.save
135
+ render json: @channel, status: :created
136
+ else
137
+ render json: @channel.errors, status: :unprocessable_entity
138
+ end
139
+ end
140
+ end
141
+ ```
142
+
143
+ The LLM will now provide the exact parameters you're used to with default rails routes, inluding the nesting of resources. For example, the LLM will create a channel with the following params
144
+ ```
145
+ { channel: { name: "Channel Name", user_ids: ["1", "2"] } }
146
+ ```
147
+
148
+ - **MCP Server**: The generated `tmp/server.rb` will include these parameters, making them available to LLM agents.
149
+ - **Rails Strong Parameters**: Calling `resource_params` in your controller action automatically permits and fetches the defined parameters.
150
+
151
+ ### 3. MCP Response Format
152
+
153
+ MCP-Rails registers a custom MIME type `application/vnd.mcp+json` for MCP-specific responses. This enables:
154
+ - Automatic key camelization for MCP protocol compatibility
155
+ - Standardized response wrapping with status and data
156
+ - View template fallbacks
157
+
158
+ #### View Templates
159
+
160
+ You can create MCP-specific views using the `.mcp.jbuilder` extension:
161
+
162
+ ```ruby
163
+ # app/views/channels/show.mcp.jbuilder
164
+ json.name @channel.name
165
+ json.user_ids @channel.user_ids
166
+ ```
167
+
168
+ If an MCP view doesn't exist, it will automatically fall back to the corresponding `.json.jbuilder` view:
169
+
170
+ ```ruby
171
+ # app/views/channels/show.json.jbuilder
172
+ json.name @channel.name
173
+ json.user_ids @channel.user_ids
174
+ ```
175
+
176
+ #### Explicit Rendering
177
+
178
+ You can also render MCP responses directly in your controllers:
179
+
180
+ ```ruby
181
+ class ChannelsController < ApplicationController
182
+ def show
183
+ @channel = Channel.find(params[:id])
184
+
185
+ respond_to do |format|
186
+ format.json { render json: @channel }
187
+ format.mcp { render mcp: @channel } # Keys will be automatically camelized
188
+ end
189
+ end
190
+ end
191
+ ```
192
+
193
+ #### Response Format
194
+
195
+ All MCP responses are automatically wrapped in a standardized format:
196
+
197
+ ```json
198
+ {
199
+ "status": 200,
200
+ "data": {
201
+ "name": "General",
202
+ "userIds": ["1", "2"] # Note: automatically camelized
203
+ }
204
+ }
205
+ ```
206
+
207
+ This format ensures compatibility with the generated MCP server
208
+
209
+ ### 4. Customizing Tool Descriptions
210
+
211
+ By default, MCP-Rails generates tool descriptions in the format "Handles [action] for [controller]". You can customize these descriptions to be more specific and informative using the `tool_description_for` method in your controllers:
212
+
213
+ ```ruby
214
+ class ChannelsController < ApplicationController
215
+ tool_description_for :create, "Create a new channel with the specified name and members"
216
+ tool_description_for :index, "List all available channels"
217
+ tool_description_for :show, "Get detailed information about a specific channel"
218
+
219
+ # ... rest of controller code
220
+ end
221
+ ```
222
+
223
+ These descriptions will be used when generating the MCP server, making it clearer to LLM agents what each endpoint does.
224
+
225
+ ### 5. Running the MCP Server
226
+
227
+ After tagging routes and defining parameters, run
228
+
229
+ ```bash
230
+ bin/rails mcp:rails:generate_server
231
+ ```
232
+ The MCP server will be generated in `tmp/server.rb`. The server.rb is an executable that attempts to find the closest Gemfile to the file and executes the server using that Gemfile.
233
+
234
+ If any engines are registered, the server will be generated for each engine as well.
235
+
236
+ LLM agents can now connect to this server and interact with your application via HTTP requests.
237
+
238
+ For an agent like Goose, you can use this new server with
239
+ ```
240
+ goose session --with-extension "path_to/tmp/server.rb"
241
+ ```
242
+ ---
243
+
244
+ ## Testing Your MCP Server
245
+
246
+ MCP-Rails provides a test helper module that makes it easy to integration test your MCP server responses. The helper automatically handles server generation, initialization, and cleanup while providing convenient methods to simulate MCP tool calls.
247
+
248
+ ### Setup
249
+
250
+ Include the test helper in your test class:
251
+
252
+ ```ruby
253
+ require "mcp/rails/test_helper"
254
+
255
+ class MCPChannelTest < ActionDispatch::IntegrationTest
256
+ include MCP::Rails::TestHelper
257
+ end
258
+ ```
259
+
260
+ The helper automatically:
261
+ - Creates a temporary directory for server files
262
+ - Configures MCP-Rails to use this directory
263
+ - Generates the MCP server files
264
+ - Cleans up after each test
265
+
266
+ ### Available Methods
267
+
268
+ - `mcp_servers`: Returns all generated MCP servers (main app and engines)
269
+ - `mcp_server(name: "mcp-server")`: Returns a specific server by name
270
+ - `mcp_response_body`: Returns the body of the last MCP response
271
+ - `mcp_tool_list(server)`: Gets the list of available tools from a server
272
+ - `mcp_tool_call(server, name, arguments = {})`: Makes a tool call to the server
273
+
274
+ ### Example Usage
275
+
276
+ ```ruby
277
+ class MCPChannelTest < ActionDispatch::IntegrationTest
278
+ include MCP::Rails::TestHelper
279
+
280
+ test "creates a channel via MCP" do
281
+ server = mcp_server
282
+
283
+ mcp_tool_call(
284
+ server,
285
+ "create_channel",
286
+ channel: { name: "General", user_ids: ["1", "2"] }
287
+ )
288
+
289
+ assert_equal false, mcp_response_body.dig(:result, :isError)
290
+ assert_equal "Channel created successfully", mcp_response_body.dig(:result, :message)
291
+ end
292
+ end
293
+ ```
294
+
295
+ This approach allows you to verify that your MCP server correctly handles requests and integrates properly with your Rails application.
296
+
297
+ ---
298
+
299
+ ## How It Works
300
+
301
+ 1. **Route Tagging**: The `mcp` option in your routes tells `mcp-rails` which endpoints to expose to the MCP server.
302
+ 2. **Parameter Definition**: The `permitted_params_for` block defines the structure and metadata of parameters, which are used to generate the MCP server's API and enforce strong parameters in Rails.
303
+ 3. **Server Generation**: `mcp-rails` leverages `mcp-rb` to create a Ruby MCP server in `tmp/server.rb`, translating tagged routes and parameters into an interface for LLM agents.
304
+ 4. **HTTP Integration**: The generated server converts MCP tool calls into HTTP requests, allowing you to reuse all of the same logic for interacting with your application.
305
+
306
+ ---
307
+
308
+ ## Bypassing CSRF Protection
309
+
310
+ The MCP server generates new HTTP requests on the fly. In standard Rails applications, this is protected by a CSRF (Cross-Site Request Forgery) key that is provided to the client during normal interactions. Since we can't leverage this, `mcp-rails` will generate a unique key to bypass this protection. This is a rudementary way to provide protection and should not be depended upon in production. As such, the gem will not automatically skip this protection on your behalf. You will have to add the following to your `ApplicationController`:
311
+
312
+ ```ruby
313
+ # app/controllers/application_controller.rb
314
+ class ApplicationController < ActionController::Base
315
+ skip_before_action :verify_authenticity_token, if: :mcp_invocation?
316
+ end
317
+ ```
318
+
319
+ The server adds a `X-Bypass-CSRF` header to all requests. This token gets regenerated and re-applied every time the server is generated. The key is stored in `/tmp/mcp/bypass_key.txt`
320
+
321
+ ## Example
322
+
323
+ ### Routes
324
+
325
+ ```ruby
326
+ # config/routes.rb
327
+ Rails.application.routes.draw do
328
+ resources :channels, only: [:index, :create], mcp: true
329
+
330
+ resources :posts, mcp: [:create]
331
+ end
332
+ ```
333
+
334
+ ### Controller
335
+
336
+ ```ruby
337
+ # app/controllers/channels_controller.rb
338
+ class ChannelsController < ApplicationController
339
+ permitted_params_for :create do
340
+ param :channel, required: true do
341
+ param :name, type: :string, example: "General Chat", required: true
342
+ param :goose_ids, type: :array, example: ["goose-123", "goose-456"]
343
+ end
344
+ end
345
+
346
+ def index
347
+ @channels = Channel.all
348
+ render json: @channels
349
+ end
350
+
351
+ def create
352
+ @channel = Channel.new(resource_params)
353
+ if @channel.save
354
+ render json: @channel, status: :created
355
+ else
356
+ render json: @channel.errors, status: :unprocessable_entity
357
+ end
358
+ end
359
+ end
360
+ ```
361
+
362
+ ### Generated MCP Server
363
+
364
+ The `tmp/server.rb` file will include an MCP server that exposes `/channels` (GET) and `/channels` (POST) with the defined parameters, allowing an LLM agent to interact with your app.
365
+
366
+ For use with something like [Goose](https://github.com/block/goose):
367
+ ```
368
+ goose session --with-extension "ruby path_to/tmp/server.rb"
369
+ ```
370
+ ---
371
+
372
+ ## Requirements
373
+
374
+ - Ruby 3.0 or higher
375
+ - Rails 7.0 or higher
376
+ - `mcp-rb` gem
377
+
378
+ ---
379
+
380
+ ## Contributing
381
+
382
+ Bug reports and pull requests are welcome! Please submit them to the [GitHub repository](https://github.com/yourusername/mcp-rails).
383
+
384
+ 1. Fork the repository.
385
+ 2. Create a feature branch (`git checkout -b my-new-feature`).
386
+ 3. Commit your changes (`git commit -am 'Add some feature'`).
387
+ 4. Push to the branch (`git push origin my-new-feature`).
388
+ 5. Create a new pull request.
389
+
390
+ ---
391
+
392
+ ## License
393
+
394
+ This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
395
+
396
+ ---
397
+
398
+ ## Acknowledgments
399
+
400
+ - Built on top of the excellent `mcp-rb` library.
401
+ - Designed with LLM agents like Goose in mind.
402
+
403
+ ---
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,25 @@
1
+ module MCP::Rails::ErrorHandling
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ rescue_from StandardError, with: :handle_mcp_error
6
+ end
7
+
8
+ private
9
+
10
+ def handle_mcp_error(exception)
11
+ if mcp_request?
12
+ render json: {
13
+ status: "error",
14
+ message: exception.message,
15
+ code: exception.class.name.underscore
16
+ }, status: :unprocessable_entity
17
+ else
18
+ raise exception # Re-raise the exception for non-mcp requests
19
+ end
20
+ end
21
+
22
+ def mcp_request?
23
+ request.format == :mcp || request.accept.include?("application/vnd.mcp+json")
24
+ end
25
+ end
@@ -0,0 +1,163 @@
1
+ module MCP::Rails::Parameters
2
+ extend ActiveSupport::Concern
3
+
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ base.class_eval do
7
+ # Initialize instance variables for each class
8
+ @shared_params_defs = {}
9
+ @action_params_defs = {}
10
+
11
+ # Define class-level accessors
12
+ def self.shared_params_defs
13
+ @shared_params_defs ||= {}
14
+ end
15
+
16
+ def self.action_params_defs
17
+ @action_params_defs ||= {}
18
+ end
19
+
20
+ # Optionally, define setters if needed
21
+ def self.shared_params_defs=(value)
22
+ @shared_params_defs = value
23
+ end
24
+
25
+ def self.action_params_defs=(value)
26
+ @action_params_defs = value
27
+ end
28
+
29
+ # Ensure subclasses get their own fresh instances
30
+ def self.inherited(subclass)
31
+ super
32
+ subclass.instance_variable_set(:@shared_params_defs, {})
33
+ subclass.instance_variable_set(:@action_params_defs, {})
34
+ end
35
+ end
36
+
37
+ def mcp_invocation?
38
+ bypass_key = request.headers["X-Bypass-CSRF"]
39
+ stored_key = File.read(MCP::Rails.configuration.bypass_key_path).strip rescue nil
40
+ bypass_key.present? && bypass_key == stored_key
41
+ end
42
+ end
43
+
44
+ class_methods do
45
+ # Define shared parameters
46
+ def shared_params(name, &block)
47
+ builder = ParamsBuilder.new
48
+ builder.instance_eval(&block)
49
+ shared_params_defs[name] = builder.params
50
+ end
51
+
52
+ # Define parameters for an action
53
+ def permitted_params_for(action, shared: [], &block)
54
+ # Gather shared params
55
+ shared_params = shared.map { |name| shared_params_defs[name] }.flatten.compact
56
+ # Build action-specific params
57
+ action_builder = ParamsBuilder.new
58
+ action_builder.instance_eval(&block) if block_given?
59
+ action_params = action_builder.params
60
+ # Merge, with action-specific overriding shared
61
+ merged_params = merge_params(shared_params, action_params)
62
+ action_params_defs[action.to_sym] = merged_params
63
+ end
64
+
65
+ # Get permitted parameters for an action
66
+ def permitted_params(action)
67
+ action_params_defs[action.to_sym] || []
68
+ end
69
+
70
+ def mcp_hash(action)
71
+ extract_permitted_mcp_hash(permitted_params(action))
72
+ end
73
+
74
+ def extract_permitted_mcp_hash(params_def)
75
+ param_hash = {}
76
+ params_def.each do |param|
77
+ param_hash[param[:name]] = {
78
+ type: param[:type],
79
+ required: param[:required]
80
+ }
81
+ if param[:nested]
82
+ param_hash[param[:name]][:type] = "object"
83
+ param_hash[param[:name]][:properties] = extract_permitted_mcp_hash(param[:nested])
84
+ end
85
+ end
86
+ param_hash
87
+ end
88
+
89
+ private
90
+
91
+ # Merge shared and action-specific params, with action-specific taking precedence
92
+ def merge_params(shared, specific)
93
+ specific_keys = specific.map { |p| p[:name] }
94
+ shared.reject { |p| specific_keys.include?(p[:name]) } + specific
95
+ end
96
+ end
97
+
98
+ # Instance method to get strong parameters, keeping controller clean
99
+ def resource_params
100
+ permitted = extract_permitted_keys(self.class.permitted_params(action_name))
101
+ if (model_hash = permitted.select { |p| p.is_a?(Hash) }) && model_hash.length == 1
102
+ params.require(model_hash.first.keys.first).permit(*model_hash.first.values)
103
+ else
104
+ params.permit(permitted)
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+
111
+ # Helper to extract permitted keys for strong parameters
112
+ def extract_permitted_keys(params_def)
113
+ params_def.map do |param|
114
+ if param[:type] == :array
115
+ if param[:item_type]
116
+ { param[:name] => [] } # Scalar array
117
+ elsif param[:nested]
118
+ { param[:name] => extract_permitted_keys(param[:nested]) } # Array of hashes
119
+ else
120
+ raise "Invalid array parameter definition"
121
+ end
122
+ elsif param[:nested]
123
+ { param[:name] => extract_permitted_keys(param[:nested]) } # Nested object
124
+ else
125
+ param[:name] # Scalar
126
+ end
127
+ end
128
+ end
129
+
130
+ # Builder class for parameter definitions
131
+ class ParamsBuilder
132
+ attr_reader :params
133
+
134
+ def initialize
135
+ @params = []
136
+ end
137
+
138
+ def param(name, type: nil, item_type: nil, example: nil, required: false, &block)
139
+ param_def = { name: name, required: required }
140
+ if type == :array
141
+ param_def[:type] = :array
142
+ if block_given?
143
+ nested_builder = ParamsBuilder.new
144
+ nested_builder.instance_eval(&block)
145
+ param_def[:nested] = nested_builder.params
146
+ elsif item_type
147
+ param_def[:item_type] = item_type
148
+ else
149
+ raise ArgumentError, "Must provide item_type or a block for array type"
150
+ end
151
+ elsif block_given?
152
+ param_def[:type] = :object
153
+ nested_builder = ParamsBuilder.new
154
+ nested_builder.instance_eval(&block)
155
+ param_def[:nested] = nested_builder.params
156
+ else
157
+ param_def[:type] = type if type
158
+ end
159
+ param_def[:example] = example if example
160
+ @params << param_def
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,69 @@
1
+ # lib/mcp/rails/renderer.rb
2
+ module MCP
3
+ module Rails
4
+ module Renderer
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ alias_method :original_render, :render
9
+ alias_method :render, :mcp_render
10
+ end
11
+
12
+ def mcp_render(*args)
13
+ return original_render(*args) unless request.format.mcp?
14
+
15
+ options = args.extract_options!
16
+ if implicit_jbuilder_render?(options)
17
+ process_implicit_jbuilder_render(options)
18
+ elsif options[:json] || options[:mcp]
19
+ process_explicit_render(options)
20
+ end
21
+ original_render(*args, options)
22
+ end
23
+
24
+ private
25
+
26
+ def implicit_jbuilder_render?(options)
27
+ options.empty? &&
28
+ lookup_context.exists?(action_name, lookup_context.prefixes, false, [], formats: [ :mcp, :json ], handlers: [ :jbuilder ])
29
+ end
30
+
31
+ def process_implicit_jbuilder_render(options)
32
+ template = lookup_context.find(action_name, lookup_context.prefixes, false, [], formats: [ :mcp, :json ], handlers: [ :jbuilder ])
33
+ data = JbuilderTemplate.new(view_context, key_format: :camelize) do |json|
34
+ json.key_format! camelize: :lower
35
+ view_context.instance_exec(json) do |j|
36
+ eval(template.source, binding, template.identifier)
37
+ end
38
+ end.attributes!
39
+ # Wrap in status/data structure
40
+ options[:json] = {
41
+ status: Rack::Utils.status_code(response.status || :ok),
42
+ data: data
43
+ }
44
+ end
45
+
46
+ def process_explicit_render(options)
47
+ status_code = Rack::Utils.status_code(options[:status] || :ok)
48
+ data = options.delete(:mcp) || options.delete(:json)
49
+
50
+ # Wrap in status/data structure
51
+ options[:json] = {
52
+ status: status_code,
53
+ data: format_keys(data)
54
+ }
55
+ end
56
+
57
+ def format_keys(data)
58
+ case data
59
+ when Hash
60
+ data.deep_transform_keys { |k| k.to_s.camelize(:lower) }
61
+ when Array
62
+ data.map { |item| format_keys(item) }
63
+ else
64
+ data
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end