rails-mcp-server 1.0.0

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: 7d6b13d9872252c1545e274913eb23db3d29d9e9df89e8943d6bc81b36bc530c
4
+ data.tar.gz: 888854cbf2bd61251c0908462deee4ed2579bade5ccb4fb28cf6e050fcd47bdd
5
+ SHA512:
6
+ metadata.gz: 7181f3eb02a5cc1d85bb0503df37d5c6985a95a26f3634d3c677e4be51204ed761a5e50041c66817fa341f460d083e036510699b380b119d0506bcadc6ee95fd
7
+ data.tar.gz: 7ed062c493c6b9071086a96161dc7f1c63f54f8227a48ad3aa5492c6fd81904abab548e1e4bc9438599ab9a69a068f2ebebe1b9c21a19a8195d3f20d08020748
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 [Mario Alberto Chávez Cárdenas]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,482 @@
1
+ # Rails MCP Server
2
+
3
+ A Ruby implementation of a Model Context Protocol (MCP) server for Rails projects. This server allows LLMs (Large Language Models) to interact with Rails projects through the Model Context Protocol.
4
+
5
+ ## What is MCP?
6
+
7
+ The Model Context Protocol (MCP) is a standardized way for AI models to interact with their environment. It defines a structured method for models to request and use tools, access resources, and maintain context during interactions.
8
+
9
+ This Rails MCP Server implements the MCP specification to give AI models access to Rails projects for code analysis, exploration, and assistance.
10
+
11
+ ## Features
12
+
13
+ - Manage multiple Rails projects
14
+ - Browse project files and structures
15
+ - View Rails routes
16
+ - Inspect model information
17
+ - Get database schema information
18
+ - Follow the Model Context Protocol standard
19
+
20
+ ## Installation
21
+
22
+ Install the gem:
23
+
24
+ ```bash
25
+ gem install rails-mcp-server
26
+ ```
27
+
28
+ After installation, the `rails-mcp-server` and `rails-mcp-setup-claude` executables will be available in your PATH.
29
+
30
+ ## Configuration
31
+
32
+ The Rails MCP Server follows the XDG Base Directory Specification for configuration files:
33
+
34
+ - On macOS: `$XDG_CONFIG_HOME/rails-mcp` or `~/.config/rails-mcp` if XDG_CONFIG_HOME is not set
35
+ - On Windows: `%APPDATA%\rails-mcp`
36
+
37
+ The server will automatically create these directories and an empty `projects.yml` file the first time it runs.
38
+
39
+ To configure your projects:
40
+
41
+ 1. Edit the `projects.yml` file in your config directory to include your Rails projects:
42
+
43
+ ```yaml
44
+ store: "~/projects/store"
45
+ blog: "~/projects/rails-blog"
46
+ ```
47
+
48
+ Each key in the YAML file is a project name (which will be used with the `switch_project` tool), and each value is the path to the project directory.
49
+
50
+ ## Claude Desktop Integration
51
+
52
+ The Rails MCP Server can be used with Claude Desktop. There are two options to set this up:
53
+
54
+ ### Option 1: Use the setup script (recommended)
55
+
56
+ Run the setup script which will automatically configure Claude Desktop and set up the proper XDG-compliant directory structure:
57
+
58
+ ```bash
59
+ rails-mcp-setup-claude
60
+ ```
61
+
62
+ The script will:
63
+
64
+ - Create the appropriate config directory for your platform
65
+ - Create an empty `projects.yml` file if it doesn't exist
66
+ - Update the Claude Desktop configuration
67
+
68
+ After running the script, restart Claude Desktop to apply the changes.
69
+
70
+ ### Option 2: Manual configuration
71
+
72
+ 1. Create the appropriate config directory for your platform:
73
+ - macOS: `$XDG_CONFIG_HOME/rails-mcp` or `~/.config/rails-mcp`
74
+ - Windows: `%APPDATA%\rails-mcp`
75
+
76
+ 2. Create a `projects.yml` file in that directory with your Rails projects.
77
+
78
+ 3. Find or create the Claude Desktop configuration file:
79
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
80
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
81
+
82
+ 4. Add or update the MCP server configuration:
83
+
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "railsMcpServer": {
88
+ "command": "ruby",
89
+ "args": ["/full/path/to/rails-mcp-server/exe/rails-mcp-server"]
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ 5. Restart Claude Desktop to apply the changes.
96
+
97
+ ### Ruby Version Manager Users
98
+
99
+ Claude Desktop launches the MCP server using your system's default Ruby environment, bypassing version manager initialization (e.g., rbenv, RVM). The MCP server needs to use the same Ruby version where it was installed, as MCP server startup failures can occur when using an incompatible Ruby version.
100
+
101
+ If you are using a Ruby version manager such as rbenv, you can create a symbolic link to your Ruby shim to ensure the correct version is used:
102
+
103
+ ```
104
+
105
+ sudo ln -s /home/your_user/.rbenv/shims/ruby /usr/local/bin/ruby
106
+
107
+ ```
108
+
109
+ Replace "/home/your_user/.rbenv/shims/ruby" with your actual path for the Ruby shim.
110
+
111
+ ## Usage
112
+
113
+ Start the server:
114
+
115
+ ```bash
116
+ rails-mcp-server
117
+ ```
118
+
119
+ ### Logging Options
120
+
121
+ The server logs to a file in the `./log` directory by default. You can customize logging with these options:
122
+
123
+ ```bash
124
+ # Set the log level (debug, info, warn, error, fatal)
125
+ rails-mcp-server --log-level debug
126
+ ```
127
+
128
+ ## How the Server Works
129
+
130
+ The Rails MCP Server implements the Model Context Protocol over standard input/output (stdio). It:
131
+
132
+ 1. Reads JSON-RPC 2.0 requests from standard input
133
+ 2. Processes the requests using the appropriate tools
134
+ 3. Returns JSON-RPC 2.0 responses to standard output
135
+
136
+ Each request includes a sequence number to match requests with responses, as defined in the MCP specification.
137
+
138
+ ## Available Tools
139
+
140
+ The server provides the following tools for interacting with Rails projects:
141
+
142
+ ### 1. `switch_project`
143
+
144
+ Switch the active Rails project.
145
+
146
+ **Parameters:**
147
+
148
+ - `project_name`: (String, required) Name of the project to switch to, as defined in the projects.yml file
149
+
150
+ **Example:**
151
+
152
+ ```json
153
+ {
154
+ "jsonrpc": "2.0",
155
+ "id": "123",
156
+ "method": "tools/call",
157
+ "params": {
158
+ "name": "switch_project",
159
+ "arguments": {
160
+ "project_name": "blog"
161
+ }
162
+ }
163
+ }
164
+ ```
165
+
166
+ **Description:** Change the active Rails project to interact with a different codebase. Must be called before using other tools. Available projects are defined in the projects.yml configuration file.
167
+
168
+ Examples:
169
+
170
+ ```
171
+ Can you switch to the "store" project so we can explore it?
172
+ ```
173
+
174
+ ```
175
+ I'd like to analyze my "blog" application. Please switch to that project first.
176
+ ```
177
+
178
+ ```
179
+ Switch to the "ecommerce" project and give me a summary of the codebase.
180
+ ```
181
+
182
+ ### 2. `get_project_info`
183
+
184
+ Get information about the current Rails project, including version, directory structure, and configuration.
185
+
186
+ **Parameters:** None
187
+
188
+ **Example:**
189
+
190
+ ```json
191
+ {
192
+ "jsonrpc": "2.0",
193
+ "id": "124",
194
+ "method": "tools/call",
195
+ "params": {
196
+ "name": "get_project_info",
197
+ "arguments": {}
198
+ }
199
+ }
200
+ ```
201
+
202
+ **Description:** Retrieve comprehensive information about the current Rails project, including Rails version, directory structure, API-only status, and overall project organization.
203
+
204
+ Examples:
205
+
206
+ ```
207
+ Now that we're in the blog project, can you give me an overview of the project structure and Rails version?
208
+ ```
209
+
210
+ ```
211
+ Tell me about this Rails application. What version is it running and how is it organized?
212
+ ```
213
+
214
+ ```
215
+ I'd like to understand the high-level architecture of this project. Can you provide the project information?
216
+ ```
217
+
218
+ ### 3. `list_files`
219
+
220
+ List files in the Rails project, with optional directory path and pattern filtering.
221
+
222
+ **Parameters:**
223
+
224
+ - `directory`: (String, optional) Directory path relative to the project root
225
+ - `pattern`: (String, optional) File pattern to match (e.g., "*.rb")
226
+
227
+ **Example:**
228
+
229
+ ```json
230
+ {
231
+ "jsonrpc": "2.0",
232
+ "id": "125",
233
+ "method": "tools/call",
234
+ "params": {
235
+ "name": "list_files",
236
+ "arguments": {
237
+ "directory": "app/models",
238
+ "pattern": "*.rb"
239
+ }
240
+ }
241
+ }
242
+ ```
243
+
244
+ **Description:** List files in the Rails project matching specific criteria. Use this to explore project directories or locate specific file types.
245
+
246
+ Examples:
247
+
248
+ ```
249
+ Can you list all the model files in this project?
250
+ ```
251
+
252
+ ```
253
+ Show me all the controller files in the app/controllers directory.
254
+ ```
255
+
256
+ ```
257
+ I need to see all the view templates in the users section. Can you list the files in app/views/users?
258
+ ```
259
+
260
+ ```
261
+ List all the JavaScript files in the app/javascript directory.
262
+ ```
263
+
264
+ ### 4. `get_file`
265
+
266
+ Get the content of a file in the Rails project with syntax highlighting.
267
+
268
+ **Parameters:**
269
+
270
+ - `path`: (String, required) File path relative to the project root
271
+
272
+ **Example:**
273
+
274
+ ```json
275
+ {
276
+ "jsonrpc": "2.0",
277
+ "id": "126",
278
+ "method": "tools/call",
279
+ "params": {
280
+ "name": "get_file",
281
+ "arguments": {
282
+ "path": "app/models/user.rb"
283
+ }
284
+ }
285
+ }
286
+ ```
287
+
288
+ **Description:** Retrieve the complete content of a specific file with syntax highlighting.
289
+
290
+ Examples:
291
+
292
+ ```
293
+ Can you show me the content of the User model file?
294
+ ```
295
+
296
+ ```
297
+ I need to see what's in app/controllers/products_controller.rb. Can you retrieve that file?
298
+ ```
299
+
300
+ ```
301
+ Please show me the application.rb file so I can check the configuration settings.
302
+ ```
303
+
304
+ ```
305
+ I'd like to examine the routes file. Can you display the content of config/routes.rb?
306
+ ```
307
+
308
+ ### 5. `get_routes`
309
+
310
+ Get the routes defined in the Rails project.
311
+
312
+ **Parameters:** None
313
+
314
+ **Example:**
315
+
316
+ ```json
317
+ {
318
+ "jsonrpc": "2.0",
319
+ "id": "127",
320
+ "method": "tools/call",
321
+ "params": {
322
+ "name": "get_routes",
323
+ "arguments": {}
324
+ }
325
+ }
326
+ ```
327
+
328
+ **Description:** Retrieve all HTTP routes defined in the Rails application with their associated controllers and actions.
329
+
330
+ Examples:
331
+
332
+ ```
333
+ Can you show me all the routes defined in this application?
334
+ ```
335
+
336
+ ```
337
+ I need to understand the API endpoints available in this project. Can you list the routes?
338
+ ```
339
+
340
+ ```
341
+ Show me the routing configuration for this Rails app so I can see how the URLs are structured.
342
+ ```
343
+
344
+ ### 6. `get_models`
345
+
346
+ Get information about the models in the Rails project, including schema, associations, and definitions.
347
+
348
+ **Parameters:**
349
+
350
+ - `model_name`: (String, optional) Name of a specific model to get information for
351
+
352
+ **Example:**
353
+
354
+ ```json
355
+ {
356
+ "jsonrpc": "2.0",
357
+ "id": "128",
358
+ "method": "tools/call",
359
+ "params": {
360
+ "name": "get_models",
361
+ "arguments": {
362
+ "model_name": "User"
363
+ }
364
+ }
365
+ }
366
+ ```
367
+
368
+ **Description:** Retrieve detailed information about Active Record models in the project.
369
+
370
+ Examples:
371
+
372
+ ```
373
+ Can you list all the models in this Rails project?
374
+ ```
375
+
376
+ ```
377
+ I'd like to understand the User model in detail. Can you show me its schema, associations, and code?
378
+ ```
379
+
380
+ ```
381
+ Show me the Product model's definition, including its relationships with other models.
382
+ ```
383
+
384
+ ```
385
+ What are all the models in this application, and can you then show me details for the Order model specifically?
386
+ ```
387
+
388
+ ### 7. `get_schema`
389
+
390
+ Get the database schema for the Rails project or for a specific table.
391
+
392
+ **Parameters:**
393
+
394
+ - `table_name`: (String, optional) Name of a specific table to get schema for
395
+
396
+ **Example:**
397
+
398
+ ```json
399
+ {
400
+ "jsonrpc": "2.0",
401
+ "id": "129",
402
+ "method": "tools/call",
403
+ "params": {
404
+ "name": "get_schema",
405
+ "arguments": {
406
+ "table_name": "users"
407
+ }
408
+ }
409
+ }
410
+ ```
411
+
412
+ **Description:** Retrieve database schema information for the Rails application.
413
+
414
+ Examples:
415
+
416
+ ```
417
+ Can you show me the complete database schema for this Rails application?
418
+ ```
419
+
420
+ ```
421
+ I'd like to see the structure of the users table. Can you retrieve that schema information?
422
+ ```
423
+
424
+ ```
425
+ Show me the columns and their data types in the products table.
426
+ ```
427
+
428
+ ```
429
+ I need to understand the database design. Can you first list all tables and then show me details for the orders table?
430
+ ```
431
+
432
+ ## Integration with LLM Clients
433
+
434
+ This server is designed to be integrated with LLM clients that support the Model Context Protocol, such as Claude Desktop or other MCP-compatible applications.
435
+
436
+ To use with an MCP client:
437
+
438
+ 1. Start the Rails MCP Server
439
+ 2. Connect your MCP-compatible client to the server
440
+ 3. The client will be able to use the available tools to interact with your Rails projects
441
+
442
+ ## Manual Testing
443
+
444
+ You can manually test the server by sending JSON-RPC requests to its standard input:
445
+
446
+ ```bash
447
+ echo '0 {"jsonrpc":"2.0","id":"test-123","method":"ping"}' | rails-mcp-server
448
+ ```
449
+
450
+ Expected response:
451
+
452
+ ```
453
+ 0 {"jsonrpc":"2.0","id":"test-123","result":{"version":"1.0.0"}}
454
+ ```
455
+
456
+ Or test multiple commands in sequence:
457
+
458
+ ```bash
459
+ (echo '0 {"jsonrpc":"2.0","id":"test-123","method":"tools/list"}'; sleep 1; echo '1 {"jsonrpc":"2.0","id":"test-456","method":"tools/call","params":{"name":"switch_project","arguments":{"project_name":"blog"}}}') | rails-mcp-server
460
+ ```
461
+
462
+ You can also use `jq` to parse the output and format it nicely:
463
+
464
+ ```bash
465
+ echo '0 {"jsonrpc":"2.0","id":"list-tools","method":"tools/list"}' | rails-mcp-server | sed 's/^[0-9]* //' | jq '.result.tools'
466
+ ```
467
+
468
+ ## License
469
+
470
+ This Rails MCP server is released under the MIT License, a permissive open-source license that allows for free use, modification, distribution, and private use.
471
+
472
+ Copyright (c) 2025 Mario Alberto Chávez Cárdenas
473
+
474
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
475
+
476
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
477
+
478
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
479
+
480
+ ## Contributing
481
+
482
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/maquina-app/rails-mcp-server>.
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "pathname"
4
+
5
+ # Get script directory and project root
6
+ bin_dir = Pathname.new(__FILE__).dirname
7
+ root_dir = bin_dir.parent
8
+
9
+ # Check if server script exists
10
+ server_script = root_dir.join("lib", "rails_mcp_server.rb")
11
+ unless server_script.exist?
12
+ puts "Error: rails_mcp_server.rb not found in lib directory (#{root_dir}/lib)"
13
+ exit 1
14
+ end
15
+
16
+ # Show version if requested
17
+ if ARGV[0] == "version"
18
+ puts "Rails MCP Server version 1.0.0"
19
+ exit 0
20
+ end
21
+
22
+ # Help message
23
+ if ARGV[0] == "--help" || ARGV[0] == "-h"
24
+ puts "Rails MCP Server - MCP protocol server for Rails projects"
25
+ puts ""
26
+ puts "Usage: #{File.basename($0)} [options]"
27
+ puts ""
28
+ puts "Options:"
29
+ puts " --log-level LEVEL Log level: debug, info, warn, error (default: info)"
30
+ puts " version Display version information"
31
+ puts " --help, -h Display this help message"
32
+ puts ""
33
+ puts "Example:"
34
+ puts " #{File.basename($0)} --log-level debug"
35
+ exit 0
36
+ end
37
+
38
+ # Change to root directory and execute server script
39
+ Dir.chdir(root_dir)
40
+ load server_script
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "pathname"
4
+ require "fileutils"
5
+ require "json"
6
+
7
+ # ANSI color codes
8
+ class String
9
+ def colorize(color_code)
10
+ "\e[#{color_code}m#{self}\e[0m"
11
+ end
12
+
13
+ def green
14
+ colorize(32)
15
+ end
16
+
17
+ def blue
18
+ colorize(34)
19
+ end
20
+
21
+ def yellow
22
+ colorize(33)
23
+ end
24
+
25
+ def red
26
+ colorize(31)
27
+ end
28
+ end
29
+
30
+ # Get the absolute path to the project directory
31
+ project_dir = Pathname.new(__FILE__).dirname.parent.expand_path
32
+
33
+ # Print banner
34
+ puts "============================================".blue
35
+ puts " Rails MCP Server Setup for Claude ".blue
36
+ puts "============================================".blue
37
+ puts
38
+
39
+ # Determine the Rails MCP config directory based on platform
40
+ def get_rails_mcp_config_dir
41
+ if RUBY_PLATFORM.match?(/mswin|mingw|cygwin/)
42
+ # Windows
43
+ File.join(ENV["APPDATA"], "rails-mcp")
44
+ else
45
+ # Linux/Unix - honor XDG_CONFIG_HOME
46
+ xdg_config_home = ENV["XDG_CONFIG_HOME"]
47
+ if xdg_config_home && !xdg_config_home.empty?
48
+ File.join(xdg_config_home, "rails-mcp")
49
+ else
50
+ File.join(Dir.home, ".config", "rails-mcp")
51
+ end
52
+ end
53
+ end
54
+
55
+ rails_mcp_config_dir = get_rails_mcp_config_dir
56
+ rails_mcp_log_dir = File.join(rails_mcp_config_dir, "log")
57
+
58
+ # Create config and log directories
59
+ FileUtils.mkdir_p(rails_mcp_log_dir)
60
+ puts "✓".green + " Created Rails MCP config directory: #{rails_mcp_config_dir}"
61
+ puts "✓".green + " Created Rails MCP log directory: #{rails_mcp_log_dir}"
62
+
63
+ # Create projects.yml in the config dir if it doesn't exist
64
+ projects_yml = File.join(rails_mcp_config_dir, "projects.yml")
65
+
66
+ if !File.exist?(projects_yml)
67
+ # Create a new projects.yml with explanatory comments
68
+ File.write(projects_yml, "# Rails MCP Projects\n# Format: project_name: /path/to/project\n")
69
+ puts "✓".green + " Created projects.yml file"
70
+ puts "!".yellow + " Please edit #{projects_yml} to add your Rails projects"
71
+ else
72
+ puts "✓".green + " projects.yml already exists at #{projects_yml}"
73
+ end
74
+
75
+ # Detect OS for Claude Desktop config
76
+ claude_config_dir = if RUBY_PLATFORM.match?(/mswin|mingw|cygwin/)
77
+ File.join(ENV["APPDATA"], "Claude")
78
+ else
79
+ File.expand_path("~/Library/Application Support/Claude")
80
+ end
81
+ claude_config_file = File.join(claude_config_dir, "claude_desktop_config.json")
82
+
83
+ puts
84
+ puts "This script will configure Claude Desktop to use your Rails MCP Server.".blue
85
+ puts "This will modify: #{claude_config_file}".blue
86
+ puts "Rails MCP config directory: #{rails_mcp_config_dir}".blue
87
+ puts
88
+
89
+ # Ask for confirmation
90
+ print "Do you want to continue? (y/n) "
91
+ unless gets.chomp.casecmp("y").zero?
92
+ puts "Setup canceled.".yellow
93
+ exit(0)
94
+ end
95
+
96
+ # Create the config directory if it doesn't exist
97
+ FileUtils.mkdir_p(claude_config_dir)
98
+
99
+ # Full path to the rails-mcp-server executable
100
+ server_path = File.join(project_dir, "exe", "rails-mcp-server")
101
+
102
+ # Ensure the server is executable
103
+ FileUtils.chmod("+x", server_path)
104
+ puts "✓".green + " Made server executable"
105
+
106
+ # Create or update the Claude Desktop config file
107
+ config = {}
108
+
109
+ if File.exist?(claude_config_file)
110
+ begin
111
+ config = JSON.parse(File.read(claude_config_file))
112
+ rescue JSON::ParserError
113
+ puts "Error:".red + " Existing config file is not valid JSON. Creating backup and new config."
114
+ FileUtils.cp(claude_config_file, "#{claude_config_file}.bak")
115
+ config = {}
116
+ end
117
+ end
118
+
119
+ # Update configuration
120
+ config["mcpServers"] ||= {}
121
+ config["mcpServers"]["railsMcpServer"] = {"command" => "ruby", "args" => [server_path]}
122
+
123
+ # Write configuration back to file
124
+ File.write(claude_config_file, JSON.pretty_generate(config))
125
+
126
+ puts "✓".green + " Updated Claude Desktop configuration"
127
+ puts
128
+ puts "Setup completed!".blue
129
+ puts "!".yellow + " Please restart Claude Desktop to apply the changes."
130
+ puts
131
+ puts "Your Rails MCP Server is now configured at:".blue
132
+ puts " #{server_path}"
133
+ puts
134
+ puts "Using config:".blue
135
+ puts " Projects file: #{projects_yml}"
136
+ puts " Log directory: #{rails_mcp_log_dir}"
137
+ puts
138
+ puts "To use it in Claude Desktop:".blue
139
+ puts "1. Restart Claude Desktop"
140
+ puts "2. Click on the plugin icon in the chat interface"
141
+ puts "3. Select 'Rails MCP Server' from the list"
142
+ puts
143
+ puts "!".yellow + " Don't forget to edit your projects.yml file to add your Rails projects:"
144
+ puts " #{projects_yml}"
145
+ puts
146
+ puts "============================================".blue
@@ -0,0 +1,3 @@
1
+ module RailsMcpServer
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,509 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "mcp"
4
+ require "yaml"
5
+ require "logger"
6
+ require "json"
7
+ require "fileutils"
8
+ require_relative "rails-mcp-server/version"
9
+
10
+ module RailsMcpServer
11
+ class Error < StandardError; end
12
+ end
13
+
14
+ # rubocop:disable Style/GlobalVars
15
+ # Initialize configuration
16
+ def get_config_dir
17
+ # Use XDG_CONFIG_HOME if set, otherwise use ~/.config
18
+ xdg_config_home = ENV["XDG_CONFIG_HOME"]
19
+ if xdg_config_home && !xdg_config_home.empty?
20
+ File.join(xdg_config_home, "rails-mcp")
21
+ else
22
+ File.join(Dir.home, ".config", "rails-mcp")
23
+ end
24
+ end
25
+
26
+ # Create config directory if it doesn't exist
27
+ config_dir = get_config_dir
28
+ FileUtils.mkdir_p(File.join(config_dir, "log"))
29
+
30
+ # Default paths
31
+ projects_file = File.join(config_dir, "projects.yml")
32
+ log_file = File.join(config_dir, "log", "rails_mcp_server.log")
33
+ log_level = :info
34
+
35
+ # Parse command-line arguments
36
+ i = 0
37
+ while i < ARGV.length
38
+ case ARGV[i]
39
+ when "--log-level"
40
+ log_level = ARGV[i + 1].to_sym
41
+ i += 2
42
+ else
43
+ i += 1
44
+ end
45
+ end
46
+
47
+ # Initialize logger
48
+ $logger = Logger.new(log_file)
49
+ $logger.level = Logger.const_get(log_level.to_s.upcase)
50
+
51
+ # Set a nicer formatter
52
+ $logger.formatter = proc do |severity, datetime, progname, msg|
53
+ "[#{datetime.strftime("%Y-%m-%d %H:%M:%S")}] #{severity}: #{msg}\n"
54
+ end
55
+
56
+ def log(level, message)
57
+ levels = {debug: Logger::DEBUG, info: Logger::INFO, warn: Logger::WARN, error: Logger::ERROR, fatal: Logger::FATAL}
58
+ log_level = levels[level] || Logger::INFO
59
+ $logger.add(log_level, message)
60
+ end
61
+
62
+ log(:info, "Starting Rails MCP Server...")
63
+ log(:info, "Using config directory: #{config_dir}")
64
+
65
+ # Create empty projects file if it doesn't exist
66
+ unless File.exist?(projects_file)
67
+ log(:info, "Creating empty projects file: #{projects_file}")
68
+ FileUtils.mkdir_p(File.dirname(projects_file))
69
+ File.write(projects_file, "# Rails MCP Projects\n# Format: project_name: /path/to/project\n")
70
+ end
71
+
72
+ # Load projects
73
+ projects_file = File.expand_path(projects_file)
74
+ projects = {}
75
+
76
+ if File.exist?(projects_file)
77
+ log(:info, "Loading projects from: #{projects_file}")
78
+ projects = YAML.load_file(projects_file) || {}
79
+ log(:info, "Loaded #{projects.size} projects: #{projects.keys.join(", ")}")
80
+ else
81
+ log(:warn, "Projects file not found: #{projects_file}")
82
+ end
83
+
84
+ # Initialize state
85
+ $active_project = nil
86
+ $active_project_path = nil
87
+
88
+ # Define MCP server using the mcp-rb DSL
89
+ name "rails-mcp-server"
90
+ version RailsMcpServer::VERSION
91
+
92
+ # Utility functions for Rails operations
93
+ def get_directory_structure(path, max_depth: 3, current_depth: 0, prefix: "")
94
+ return "" if current_depth > max_depth || !File.directory?(path)
95
+
96
+ output = ""
97
+ directories = []
98
+ files = []
99
+
100
+ Dir.foreach(path) do |entry|
101
+ next if entry == "." || entry == ".."
102
+ full_path = File.join(path, entry)
103
+
104
+ if File.directory?(full_path)
105
+ directories << entry
106
+ else
107
+ files << entry
108
+ end
109
+ end
110
+
111
+ directories.sort.each do |dir|
112
+ output << "#{prefix}└── #{dir}/\n"
113
+ full_path = File.join(path, dir)
114
+ output << get_directory_structure(full_path, max_depth: max_depth,
115
+ current_depth: current_depth + 1,
116
+ prefix: "#{prefix} ")
117
+ end
118
+
119
+ files.sort.each do |file|
120
+ output << "#{prefix}└── #{file}\n"
121
+ end
122
+
123
+ output
124
+ end
125
+
126
+ def get_file_extension(path)
127
+ case File.extname(path).downcase
128
+ when ".rb"
129
+ "ruby"
130
+ when ".js"
131
+ "javascript"
132
+ when ".html", ".erb"
133
+ "html"
134
+ when ".css"
135
+ "css"
136
+ when ".json"
137
+ "json"
138
+ when ".yml", ".yaml"
139
+ "yaml"
140
+ else
141
+ ""
142
+ end
143
+ end
144
+
145
+ def execute_rails_command(project_path, command)
146
+ full_command = "cd #{project_path} && bin/rails #{command}"
147
+ `#{full_command}`
148
+ end
149
+
150
+ def underscore(string)
151
+ string.gsub("::", "/")
152
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
153
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
154
+ .tr("-", "_")
155
+ .downcase
156
+ end
157
+
158
+ # Define tools using the mcp-rb DSL
159
+ tool "switch_project" do
160
+ description "Change the active Rails project to interact with a different codebase. Must be called before using other tools. Available projects are defined in the projects.yml configuration file."
161
+
162
+ argument :project_name, String, required: true,
163
+ description: "Name of the project as defined in the projects.yml file (case-sensitive)"
164
+
165
+ call do |args|
166
+ project_name = args[:project_name]
167
+
168
+ if projects.key?(project_name)
169
+ $active_project = project_name
170
+ $active_project_path = File.expand_path(projects[project_name])
171
+ log(:info, "Switched to project: #{project_name} at path: #{$active_project_path}")
172
+ "Switched to project: #{project_name} at path: #{$active_project_path}"
173
+ else
174
+ log(:warn, "Project not found: #{project_name}")
175
+ raise "Project '#{project_name}' not found. Available projects: #{projects.keys.join(", ")}"
176
+ end
177
+ end
178
+ end
179
+
180
+ tool "get_project_info" do
181
+ description "Retrieve comprehensive information about the current Rails project, including Rails version, directory structure, API-only status, and overall project organization. Useful for initial project exploration and understanding the codebase structure."
182
+
183
+ call do |args|
184
+ unless $active_project
185
+ raise "No active project. Please switch to a project first."
186
+ end
187
+
188
+ # Get additional project information
189
+ gemfile_path = File.join($active_project_path, "Gemfile")
190
+ gemfile_content = File.exist?(gemfile_path) ? File.read(gemfile_path) : "Gemfile not found"
191
+
192
+ # Get Rails version
193
+ rails_version = gemfile_content.match(/gem ['"]rails['"],\s*['"](.+?)['"]/)&.captures&.first || "Unknown"
194
+
195
+ # Check if it's an API-only app
196
+ config_application_path = File.join($active_project_path, "config", "application.rb")
197
+ is_api_only = File.exist?(config_application_path) &&
198
+ File.read(config_application_path).include?("config.api_only = true")
199
+
200
+ log(:info, "Project info: Rails v#{rails_version}, API-only: #{is_api_only}")
201
+
202
+ <<~INFO
203
+ Current project: #{$active_project}
204
+ Path: #{$active_project_path}
205
+ Rails version: #{rails_version}
206
+ API only: #{is_api_only ? "Yes" : "No"}
207
+
208
+ Project structure:
209
+ #{get_directory_structure($active_project_path, max_depth: 2)}
210
+ INFO
211
+ end
212
+ end
213
+
214
+ tool "list_files" do
215
+ description "List files in the Rails project matching specific criteria. Use this to explore project directories or locate specific file types. If no parameters are provided, lists files in the project root."
216
+
217
+ argument :directory, String, required: false,
218
+ description: "Directory path relative to the project root (e.g., 'app/models', 'config'). Leave empty to list files at the root."
219
+
220
+ argument :pattern, String, required: false,
221
+ description: "File pattern using glob syntax (e.g., '*.rb' for Ruby files, '*.erb' for ERB templates, '*_controller.rb' for controllers)"
222
+
223
+ call do |args|
224
+ unless $active_project
225
+ raise "No active project. Please switch to a project first."
226
+ end
227
+
228
+ directory = args[:directory] || ""
229
+ pattern = args[:pattern] || "*"
230
+
231
+ full_path = File.join($active_project_path, directory)
232
+
233
+ unless File.directory?(full_path)
234
+ raise "Directory '#{directory}' not found in the project."
235
+ end
236
+
237
+ # Use Dir.glob to get matching files
238
+ files = Dir.glob(File.join(full_path, pattern))
239
+ .map { |f| f.sub("#{$active_project_path}/", "") }
240
+ .sort # rubocop:disable Performance/ChainArrayAllocation
241
+
242
+ log(:debug, "Found #{files.size} files matching pattern")
243
+
244
+ "Files in #{directory.empty? ? "project root" : directory} matching '#{pattern}':\n\n#{files.join("\n")}"
245
+ end
246
+ end
247
+
248
+ tool "get_file" do
249
+ description "Retrieve the complete content of a specific file with syntax highlighting. Use this to examine implementation details, configurations, or any text file in the project."
250
+
251
+ argument :path, String, required: true,
252
+ description: "File path relative to the project root (e.g., 'app/models/user.rb', 'config/routes.rb'). Use list_files first if you're not sure about the exact path."
253
+
254
+ call do |args|
255
+ unless $active_project
256
+ raise "No active project. Please switch to a project first."
257
+ end
258
+
259
+ path = args[:path]
260
+ full_path = File.join($active_project_path, path)
261
+
262
+ unless File.exist?(full_path)
263
+ raise "File '#{path}' not found in the project."
264
+ end
265
+
266
+ content = File.read(full_path)
267
+ log(:debug, "Read file: #{path} (#{content.size} bytes)")
268
+
269
+ "File: #{path}\n\n```#{get_file_extension(path)}\n#{content}\n```"
270
+ end
271
+ end
272
+
273
+ tool "get_routes" do
274
+ description "Retrieve all HTTP routes defined in the Rails application with their associated controllers and actions. Equivalent to running 'rails routes' command. This helps understand the API endpoints or page URLs available in the application."
275
+
276
+ call do |args|
277
+ unless $active_project
278
+ raise "No active project. Please switch to a project first."
279
+ end
280
+
281
+ # Execute the Rails routes command
282
+ routes_output = execute_rails_command($active_project_path, "routes")
283
+ log(:debug, "Routes command completed, output size: #{routes_output.size} bytes")
284
+
285
+ "Rails Routes:\n\n```\n#{routes_output}\n```"
286
+ end
287
+ end
288
+
289
+ tool "get_models" do
290
+ description "Retrieve detailed information about Active Record models in the project. When called without parameters, lists all model files. When a specific model is specified, returns its schema, associations (has_many, belongs_to, has_one), and complete source code."
291
+
292
+ argument :model_name, String, required: false,
293
+ description: "Class name of a specific model to get detailed information for (e.g., 'User', 'Product'). Use CamelCase, not snake_case. If omitted, returns a list of all models."
294
+
295
+ call do |args|
296
+ unless $active_project
297
+ raise "No active project. Please switch to a project first."
298
+ end
299
+
300
+ model_name = args[:model_name]
301
+
302
+ if model_name
303
+ log(:info, "Getting info for specific model: #{model_name}")
304
+
305
+ # Check if the model file exists
306
+ model_file = File.join($active_project_path, "app", "models", "#{underscore(model_name)}.rb")
307
+ unless File.exist?(model_file)
308
+ log(:warn, "Model file not found: #{model_name}")
309
+ raise "Model '#{model_name}' not found."
310
+ end
311
+
312
+ log(:debug, "Reading model file: #{model_file}")
313
+
314
+ # Get the model file content
315
+ model_content = File.read(model_file)
316
+
317
+ # Try to get schema information
318
+ log(:debug, "Executing Rails runner to get schema information")
319
+ schema_info = execute_rails_command(
320
+ $active_project_path,
321
+ "runner \"puts #{model_name}.column_names\""
322
+ )
323
+
324
+ # Try to get associations
325
+ associations = []
326
+ if model_content.include?("has_many")
327
+ has_many = model_content.scan(/has_many\s+:(\w+)/).flatten
328
+ associations << "Has many: #{has_many.join(", ")}" unless has_many.empty?
329
+ end
330
+
331
+ if model_content.include?("belongs_to")
332
+ belongs_to = model_content.scan(/belongs_to\s+:(\w+)/).flatten
333
+ associations << "Belongs to: #{belongs_to.join(", ")}" unless belongs_to.empty?
334
+ end
335
+
336
+ if model_content.include?("has_one")
337
+ has_one = model_content.scan(/has_one\s+:(\w+)/).flatten
338
+ associations << "Has one: #{has_one.join(", ")}" unless has_one.empty?
339
+ end
340
+
341
+ log(:debug, "Found #{associations.size} associations for model: #{model_name}")
342
+
343
+ # Format the output
344
+ <<~INFO
345
+ Model: #{model_name}
346
+
347
+ Schema:
348
+ #{schema_info}
349
+
350
+ Associations:
351
+ #{associations.empty? ? "None found" : associations.join("\n")}
352
+
353
+ Model Definition:
354
+ ```ruby
355
+ #{model_content}
356
+ ```
357
+ INFO
358
+ else
359
+ log(:info, "Listing all models")
360
+
361
+ # List all models
362
+ models_dir = File.join($active_project_path, "app", "models")
363
+ unless File.directory?(models_dir)
364
+ raise "Models directory not found."
365
+ end
366
+
367
+ # Get all .rb files in the models directory and its subdirectories
368
+ model_files = Dir.glob(File.join(models_dir, "**", "*.rb"))
369
+ .map { |f| f.sub("#{models_dir}/", "").sub(/\.rb$/, "") }
370
+ .sort # rubocop:disable Performance/ChainArrayAllocation
371
+
372
+ log(:debug, "Found #{model_files.size} model files")
373
+
374
+ "Models in the project:\n\n#{model_files.join("\n")}"
375
+ end
376
+ end
377
+ end
378
+
379
+ tool "get_schema" do
380
+ description "Retrieve database schema information for the Rails application. Without parameters, returns all tables and the complete schema.rb. With a table name, returns detailed column information including data types, constraints, and foreign keys for that specific table."
381
+
382
+ argument :table_name, String, required: false,
383
+ description: "Database table name to get detailed schema information for (e.g., 'users', 'products'). Use snake_case, plural form. If omitted, returns complete database schema."
384
+
385
+ call do |args|
386
+ unless $active_project
387
+ raise "No active project. Please switch to a project first."
388
+ end
389
+
390
+ table_name = args[:table_name]
391
+
392
+ if table_name
393
+ log(:info, "Getting schema for table: #{table_name}")
394
+
395
+ # Execute the Rails schema command for a specific table
396
+ schema_output = execute_rails_command(
397
+ $active_project_path,
398
+ "runner \"require 'active_record'; puts ActiveRecord::Base.connection.columns('#{table_name}').map{|c| [c.name, c.type, c.null, c.default].inspect}.join('\n')\""
399
+ )
400
+
401
+ if schema_output.strip.empty?
402
+ raise "Table '#{table_name}' not found or has no columns."
403
+ end
404
+
405
+ # Parse the column information
406
+ columns = schema_output.strip.split("\n").map do |column_info|
407
+ eval(column_info) # This is safe because we're generating the string ourselves # rubocop:disable Security/Eval
408
+ end
409
+
410
+ # Format the output
411
+ formatted_columns = columns.map do |name, type, nullable, default|
412
+ "#{name} (#{type})#{nullable ? ", nullable" : ""}#{default ? ", default: #{default}" : ""}"
413
+ end
414
+
415
+ output = <<~SCHEMA
416
+ Table: #{table_name}
417
+
418
+ Columns:
419
+ #{formatted_columns.join("\n")}
420
+ SCHEMA
421
+
422
+ # Try to get foreign keys
423
+ begin
424
+ fk_output = execute_rails_command(
425
+ $active_project_path,
426
+ "runner \"require 'active_record'; puts ActiveRecord::Base.connection.foreign_keys('#{table_name}').map{|fk| [fk.from_table, fk.to_table, fk.column, fk.primary_key].inspect}.join('\n')\""
427
+ )
428
+
429
+ unless fk_output.strip.empty?
430
+ foreign_keys = fk_output.strip.split("\n").map do |fk_info|
431
+ eval(fk_info) # This is safe because we're generating the string ourselves # rubocop:disable Security/Eval
432
+ end
433
+
434
+ formatted_fks = foreign_keys.map do |from_table, to_table, column, primary_key|
435
+ "#{column} -> #{to_table}.#{primary_key}"
436
+ end
437
+
438
+ output += <<~FK
439
+
440
+ Foreign Keys:
441
+ #{formatted_fks.join("\n")}
442
+ FK
443
+ end
444
+ rescue => e
445
+ log(:warn, "Error fetching foreign keys: #{e.message}")
446
+ end
447
+
448
+ output
449
+ else
450
+ log(:info, "Getting full schema")
451
+
452
+ # Execute the Rails schema:dump command
453
+ # First, check if we need to create the schema file
454
+ schema_file = File.join($active_project_path, "db", "schema.rb")
455
+ unless File.exist?(schema_file)
456
+ log(:info, "Schema file not found, attempting to generate it")
457
+ execute_rails_command($active_project_path, "db:schema:dump")
458
+ end
459
+
460
+ if File.exist?(schema_file)
461
+ # Read the schema file
462
+ schema_content = File.read(schema_file)
463
+
464
+ # Try to get table list
465
+ tables_output = execute_rails_command(
466
+ $active_project_path,
467
+ "runner \"require 'active_record'; puts ActiveRecord::Base.connection.tables.sort.join('\n')\""
468
+ )
469
+
470
+ tables = tables_output.strip.split("\n")
471
+
472
+ <<~SCHEMA
473
+ Database Schema
474
+
475
+ Tables:
476
+ #{tables.join("\n")}
477
+
478
+ Schema Definition:
479
+ ```ruby
480
+ #{schema_content}
481
+ ```
482
+ SCHEMA
483
+
484
+ else
485
+ # If we can't get the schema file, try to get the table list
486
+ tables_output = execute_rails_command(
487
+ $active_project_path,
488
+ "runner \"require 'active_record'; puts ActiveRecord::Base.connection.tables.sort.join('\n')\""
489
+ )
490
+
491
+ if tables_output.strip.empty?
492
+ raise "Could not retrieve schema information. Try running 'rails db:schema:dump' in your project first."
493
+ end
494
+
495
+ tables = tables_output.strip.split("\n")
496
+
497
+ <<~SCHEMA
498
+ Database Schema
499
+
500
+ Tables:
501
+ #{tables.join("\n")}
502
+
503
+ Note: Full schema definition is not available. Run 'rails db:schema:dump' to generate the schema.rb file.
504
+ SCHEMA
505
+ end
506
+ end
507
+ end
508
+ end
509
+ # rubocop:enable Style/GlobalVars
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-mcp-server
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Mario Alberto Chávez Cárdenas
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-03-20 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: mcp-rb
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: logger
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: standard
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ description: A Ruby implementation of Model Context Protocol server for Rails projects
55
+ email:
56
+ - mario.chavez@gmail.com
57
+ executables:
58
+ - rails-mcp-server
59
+ - rails-mcp-setup-claude
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - LICENSE.txt
64
+ - README.md
65
+ - exe/rails-mcp-server
66
+ - exe/rails-mcp-setup-claude
67
+ - lib/rails-mcp-server/version.rb
68
+ - lib/rails_mcp_server.rb
69
+ homepage: https://github.com/maquina-app/rails-mcp-server
70
+ licenses:
71
+ - MIT
72
+ metadata:
73
+ allowed_push_host: https://rubygems.org
74
+ homepage_uri: https://github.com/maquina-app/rails-mcp-server
75
+ source_code_uri: https://github.com/maquina-app/rails-mcp-server
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 2.5.0
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubygems_version: 3.6.2
91
+ specification_version: 4
92
+ summary: MCP server for Rails projects
93
+ test_files: []