diff --git a/README.md b/README.md index d46bc35..2ee409b 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ Fast MCP solves all these problems by providing a clean, Ruby-focused implementa - πŸš€ **Real-time Updates** - Subscribe to changes for interactive applications - 🎯 **Dynamic Filtering** - Control tool/resource access based on request context (permissions, API versions, etc.) - ## πŸ’Ž What Makes FastMCP Great + ```ruby # Define tools for AI models to use server = FastMcp::Server.new(name: 'popular-users', version: '1.0.0') @@ -109,7 +109,7 @@ Control which tools and resources are available based on request context: class AdminTool < FastMcp::Tool tags :admin, :dangerous description "Perform admin operations" - + def call # Admin only functionality end @@ -118,7 +118,7 @@ end # Filter tools based on user permissions server.filter_tools do |request, tools| user_role = request.params['role'] - + case user_role when 'admin' tools # Admins see all tools @@ -131,6 +131,7 @@ end ``` ### πŸš‚ Fast Ruby on Rails implementation + ```shell bundle add fast-mcp bin/rails generate fast_mcp:install @@ -155,20 +156,13 @@ FastMcp.mount_in_rails( # allowed_ips: ['127.0.0.1', '::1'] # authenticate: true, # Uncomment to enable authentication # auth_token: 'your-token' # Required if authenticate: true -) do |server| - Rails.application.config.after_initialize do - # FastMcp will automatically discover and register: - # - All classes that inherit from ApplicationTool (which uses ActionTool::Base) - # - All classes that inherit from ApplicationResource (which uses ActionResource::Base) - server.register_tools(*ApplicationTool.descendants) - server.register_resources(*ApplicationResource.descendants) - # alternatively, you can register tools and resources manually: - # server.register_tool(MyTool) - # server.register_resource(MyResource) - end -end + # tools_dir: Rails.root.join('app/tools'), # This is the default directory for tools + # resources_dir: Rails.root.join('app/resources'), # This is the default directory for resources +) ``` + The install script will also: + - add app/resources folder - add app/tools folder - add app/tools/sample_tool.rb @@ -187,7 +181,7 @@ These are automatically set up in Rails applications. You can use either naming ```ruby # Using Rails-style naming: -class MyTool < ActionTool::Base +class MyTool < ApplicationTool description "My awesome tool" arguments do @@ -219,7 +213,10 @@ class ApplicationResource < ActionResource::Base end ``` +Tools and resources are automatically registered in the server when they are inherited from ApplicationTool and ApplicationResource. Inheriting off of ActionTool::Base or FastMcp::Tool will not require you to register them manually. + ### Easy Sinatra setup + I'll let you check out the dedicated [sinatra integration docs](./docs/sinatra_integration.md). ## πŸš€ Quick Start @@ -282,17 +279,21 @@ Clone this project, then give it a go ! ```shell npx @modelcontextprotocol/inspector examples/server_with_stdio_transport.rb ``` + Or to test with an SSE transport using a rack middleware: + ```shell npx @modelcontextprotocol/inspector examples/rack_middleware.rb ``` Or to test over SSE with an authenticated rack middleware: + ```shell npx @modelcontextprotocol/inspector examples/authenticated_rack_middleware.rb ``` You can test your custom implementation with the official MCP inspector by using: + ```shell # Test with a stdio transport: npx @modelcontextprotocol/inspector path/to/your_ruby_file.rb @@ -321,6 +322,7 @@ end ### Integrating with Claude Desktop Add your server to your Claude Desktop configuration at: + - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` - Windows: `%APPDATA%\Claude\claude_desktop_config.json` @@ -329,28 +331,27 @@ Add your server to your Claude Desktop configuration at: "mcpServers": { "my-great-server": { "command": "ruby", - "args": [ - "/Users/path/to/your/awesome/fast-mcp/server.rb" - ] + "args": ["/Users/path/to/your/awesome/fast-mcp/server.rb"] } } } ``` ## How to add a MCP server to Claude, Cursor, or other MCP clients? + Please refer to [configuring_mcp_clients](docs/configuring_mcp_clients.md) ## πŸ“Š Supported Specifications -| Feature | Status | -|---------|--------| -| βœ… **JSON-RPC 2.0** | Full implementation for communication | -| βœ… **Tool Definition & Calling** | Define and call tools with rich argument types | -| βœ… **Resource & Resource Templates Management** | Create, read, update, and subscribe to resources | -| βœ… **Transport Options** | STDIO, HTTP, and SSE for flexible integration | -| βœ… **Framework Integration** | Rails, Sinatra, Hanami, and any Rack-compatible framework | -| βœ… **Authentication** | Secure your AI endpoints with token authentication | -| βœ… **Schema Support** | Full JSON Schema for tool arguments with validation | +| Feature | Status | +| ----------------------------------------------- | --------------------------------------------------------- | +| βœ… **JSON-RPC 2.0** | Full implementation for communication | +| βœ… **Tool Definition & Calling** | Define and call tools with rich argument types | +| βœ… **Resource & Resource Templates Management** | Create, read, update, and subscribe to resources | +| βœ… **Transport Options** | STDIO, HTTP, and SSE for flexible integration | +| βœ… **Framework Integration** | Rails, Sinatra, Hanami, and any Rack-compatible framework | +| βœ… **Authentication** | Secure your AI endpoints with token authentication | +| βœ… **Schema Support** | Full JSON Schema for tool arguments with validation | ## πŸ—ΊοΈ Use Cases @@ -406,6 +407,7 @@ FastMcp.authenticated_rack_middleware(app, Check out the [examples directory](examples) for more detailed examples: - **πŸ”¨ Basic Examples**: + - [Simple Server](examples/server_with_stdio_transport.rb) - [Tool Examples](examples/tool_examples.rb) diff --git a/lib/fast_mcp.rb b/lib/fast_mcp.rb index de75bf1..f305f69 100644 --- a/lib/fast_mcp.rb +++ b/lib/fast_mcp.rb @@ -7,6 +7,9 @@ module FastMcp class << self attr_accessor :server + + attr_accessor :tools_dir + attr_accessor :resources_dir end end @@ -102,18 +105,20 @@ def self.register_tools(*tools) # Register a resource with the MCP server # @param resource [FastMcp::Resource] The resource to register + # @option options [Boolean] :notify Whether to notify subscribers about the list change # @return [FastMcp::Resource] The registered resource - def self.register_resource(resource) + def self.register_resource(resource, notify: true) self.server ||= FastMcp::Server.new(name: 'mcp-server', version: '1.0.0') - self.server.register_resource(resource) + self.server.register_resource(resource, notify: notify) end # Register multiple resources at once # @param resources [Array] The resources to register + # @option options [Boolean] :notify Whether to notify subscribers about the list change # @return [Array] The registered resources - def self.register_resources(*resources) + def self.register_resources(*resources, notify: true) self.server ||= FastMcp::Server.new(name: 'mcp-server', version: '1.0.0') - self.server.register_resources(*resources) + self.server.register_resources(*resources, notify: notify) end # Mount the MCP middleware in a Rails application @@ -127,6 +132,8 @@ def self.register_resources(*resources) # @option options [Logger] :logger The logger to use # @option options [Boolean] :authenticate Whether to use authentication # @option options [String] :auth_token The authentication token + # @option options [String] :tools_dir The directory to load tools from + # @option options [String] :resources_dir The directory to load resources from # @option options [Array] :allowed_origins List of allowed origins for DNS rebinding protection # @yield [server] A block to configure the server # @yieldparam server [FastMcp::Server] The server to configure @@ -148,6 +155,9 @@ def self.mount_in_rails(app, options = {}) options[:logger] = logger options[:allowed_origins] = allowed_origins + self.tools_dir = options.delete(:tools_dir) || Rails.root.join('app/tools') + self.resources_dir = options.delete(:resources_dir) || Rails.root.join('app/resources') + # Create or get the server self.server = FastMcp::Server.new(name: name, version: version, logger: logger) yield self.server if block_given? diff --git a/lib/generators/fast_mcp/install/templates/fast_mcp_initializer.rb b/lib/generators/fast_mcp/install/templates/fast_mcp_initializer.rb index e2eac52..0531869 100644 --- a/lib/generators/fast_mcp/install/templates/fast_mcp_initializer.rb +++ b/lib/generators/fast_mcp/install/templates/fast_mcp_initializer.rb @@ -28,15 +28,6 @@ # allowed_ips: ['127.0.0.1', '::1'] # authenticate: true, # Uncomment to enable authentication # auth_token: 'your-token', # Required if authenticate: true -) do |server| - Rails.application.config.after_initialize do - # FastMcp will automatically discover and register: - # - All classes that inherit from ApplicationTool (which uses ActionTool::Base) - # - All classes that inherit from ApplicationResource (which uses ActionResource::Base) - server.register_tools(*ApplicationTool.descendants) - server.register_resources(*ApplicationResource.descendants) - # alternatively, you can register tools and resources manually: - # server.register_tool(MyTool) - # server.register_resource(MyResource) - end -end + # tools_dir: Rails.root.join('app/tools'), # This is the default directory for tools + # resources_dir: Rails.root.join('app/resources'), # This is the default directory for resources +) diff --git a/lib/mcp/railtie.rb b/lib/mcp/railtie.rb index 6938b89..0222fce 100644 --- a/lib/mcp/railtie.rb +++ b/lib/mcp/railtie.rb @@ -4,14 +4,14 @@ require 'fileutils' require_relative '../mcp/server' -# Create ActionTool and ActionResource modules at load time -unless defined?(ActionTool) +# Create ActionTool and ActionResource modules when using Rails +if !defined?(ActionTool) && defined?(Rails) module ::ActionTool Base = FastMcp::Tool end end -unless defined?(ActionResource) +if !defined?(ActionResource) && defined?(Rails) module ::ActionResource Base = FastMcp::Resource end @@ -20,19 +20,29 @@ module ::ActionResource module FastMcp # Railtie for integrating Fast MCP with Rails applications class Railtie < Rails::Railtie - # Add tools and resources directories to autoload paths - initializer 'fast_mcp.setup_autoload_paths' do |app| - app.config.autoload_paths += %W[ - #{app.root}/app/tools - #{app.root}/app/resources - ] + + # Auto-register all tools and resources after the application is fully loaded + config.after_initialize do |app| + FastMcp::Railtie.eager_load_and_register_tools_and_resources end # Auto-register all tools and resources after the application is fully loaded - config.after_initialize do - # Load all files in app/tools and app/resources directories - Dir[Rails.root.join('app', 'tools', '**', '*.rb')].each { |f| require f } - Dir[Rails.root.join('app', 'resources', '**', '*.rb')].each { |f| require f } + config.to_prepare do + FastMcp.server.clear_tools! + FastMcp.server.clear_resources! + + FastMcp::Railtie.eager_load_and_register_tools_and_resources + end + + def self.eager_load_and_register_tools_and_resources + Rails.autoloaders.main.eager_load_dir(FastMcp.tools_dir) + Rails.autoloaders.main.eager_load_dir(FastMcp.resources_dir) + + tools = ApplicationTool.descendants.sort_by(&:name) + resources = ApplicationResource.descendants.sort_by(&:name) + + FastMcp.server.register_tools(*tools) + FastMcp.server.register_resources(*resources, notify: false) end # Add rake tasks diff --git a/lib/mcp/server.rb b/lib/mcp/server.rb index 029808b..71a4da8 100644 --- a/lib/mcp/server.rb +++ b/lib/mcp/server.rb @@ -60,22 +60,30 @@ def register_tool(tool) tool.server = self end + # Clear all tools from the server + def clear_tools! + @tools = {} + end + # Register multiple resources at once # @param resources [Array] Resources to register - def register_resources(*resources) + # @option options [Boolean] :notify Whether to notify subscribers about the list change + def register_resources(*resources, notify: true) resources.each do |resource| - register_resource(resource) + register_resource(resource, notify: notify) end end # Register a resource with the server - def register_resource(resource) + # @param resource [Resource] Resource to register + # @option options [Boolean] :notify Whether to notify subscribers about the list change + def register_resource(resource, notify: true) @resources << resource @logger.debug("Registered resource: #{resource.resource_name} (#{resource.uri})") resource.server = self # Notify subscribers about the list change - notify_resource_list_changed if @transport + notify_resource_list_changed if @transport && notify resource end @@ -97,6 +105,11 @@ def remove_resource(uri) end end + # Clear all resources from the server + def clear_resources! + @resources = [] + end + # Start the server using stdio transport def start @logger.transport = :stdio