fast-mcp 0.1.0 → 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 +4 -4
- data/CHANGELOG.md +38 -2
- data/README.md +134 -100
- data/lib/fast_mcp.rb +93 -6
- data/lib/generators/fast_mcp/install/install_generator.rb +50 -0
- data/lib/generators/fast_mcp/install/templates/application_resource.rb +5 -0
- data/lib/generators/fast_mcp/install/templates/application_tool.rb +5 -0
- data/lib/generators/fast_mcp/install/templates/fast_mcp_initializer.rb +35 -0
- data/lib/generators/fast_mcp/install/templates/sample_resource.rb +12 -0
- data/lib/generators/fast_mcp/install/templates/sample_tool.rb +16 -0
- data/lib/mcp/logger.rb +13 -14
- data/lib/mcp/railtie.rb +45 -0
- data/lib/mcp/resource.rb +5 -10
- data/lib/mcp/server.rb +40 -74
- data/lib/mcp/tool.rb +8 -2
- data/lib/mcp/transports/authenticated_rack_transport.rb +2 -2
- data/lib/mcp/transports/base_transport.rb +1 -1
- data/lib/mcp/transports/rack_transport.rb +19 -16
- data/lib/mcp/transports/stdio_transport.rb +1 -1
- data/lib/mcp/version.rb +2 -2
- metadata +24 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bedabe91d57ecb3800b625ad786389c7e842b73e5bcd9df621f8f31b401fe93f
|
4
|
+
data.tar.gz: b8711a78f8f8928e5f66d94a1f271f9f6e6a5e101e0386eff81ed62e7153d2ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 564cadd64c076e784bc0779a3f697d82d50eed14c77b1403f03abeee0935972103e68bf82bb94ec7cd3443b90ddfa75d739dd6a5a0bda32590e8b31d8a708869
|
7
|
+
data.tar.gz: 731cc93d551842dfcec399d7d75d86c59d4a6af88d415925757a3ced8c5947dda7ae26eafc12b71423af496c6669a084cdbaabdc05cbbacef68476d39925991e
|
data/CHANGELOG.md
CHANGED
@@ -5,13 +5,49 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [1.0.0] - 2025-03-30
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Rails integration improvements via enhanced Railtie support
|
12
|
+
- Automatic tool and resource registration in Rails applications
|
13
|
+
- Extended Rails autoload paths for tools and resources directories
|
14
|
+
- Sample generator templates for resources and tools
|
15
|
+
- MCP Client configuration documentation as reported by [#8 @sivag-csod](https://github.com/yjacquin/fast-mcp/issues/8)
|
16
|
+
- Example Ruby on Rails app in the documentation
|
17
|
+
- `FastMcp.server` now exposes the MCP server to apps that may need it to access resources
|
18
|
+
- Automated Github Releases through Github Workflow
|
19
|
+
|
20
|
+
### Fixed
|
21
|
+
- Fixed bug with Rack middlewares not being initialized properly.
|
22
|
+
- Fixed bug with STDIO logging preventing a proper connection with clients [# 11 @cs3b](https://github.com/yjacquin/fast-mcp/issues/11)
|
23
|
+
- Fixed Rails SSE streaming detection and handling
|
24
|
+
- Improved error handling in client reconnection scenarios
|
25
|
+
- Namespace consistency correction (FastMCP -> FastMcp) throughout the codebase
|
26
|
+
|
27
|
+
### Improved
|
28
|
+
- ⚠️ [Breaking] Resource content declaration changes
|
29
|
+
- Now resources implement `content` over `default_content`
|
30
|
+
- `content` is dynamically called when calling a resource, this implies we can declare dynamic resource contents like:
|
31
|
+
```ruby
|
32
|
+
class HighestScoringUsersResource < FastMcp::Resource
|
33
|
+
...
|
34
|
+
def content
|
35
|
+
User.order(score: :desc).last(5).map(&:as_json)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
```
|
39
|
+
- More robust SSE connection lifecycle management
|
40
|
+
- Optimized test suite with faster execution times
|
41
|
+
- Better logging for debugging connection issues
|
42
|
+
- Documentation had outdated examples
|
43
|
+
|
8
44
|
## [0.1.0] - 2025-03-12
|
9
45
|
|
10
46
|
### Added
|
11
47
|
|
12
48
|
- Initial release of the Fast MCP library
|
13
|
-
-
|
14
|
-
-
|
49
|
+
- FastMcp::Tool class with multiple definition styles
|
50
|
+
- FastMcp::Server class with STDIO transport and HTTP / SSE transport
|
15
51
|
- Rack Integration with authenticated and standard middleware options
|
16
52
|
- Resource management with subscription capabilities
|
17
53
|
- Binary resource support
|
data/README.md
CHANGED
@@ -28,44 +28,49 @@ Fast MCP solves all these problems by providing a clean, Ruby-focused implementa
|
|
28
28
|
- 🛠️ **Tools API** - Let AI models call your Ruby functions securely, with in-depth argument validation through [Dry-Schema](https://github.com/dry-rb/dry-schema).
|
29
29
|
- 📚 **Resources API** - Share data between your app and AI models
|
30
30
|
- 🔄 **Multiple Transports** - Choose from STDIO, HTTP, or SSE based on your needs
|
31
|
-
- 🧩 **Framework Integration** - Works seamlessly with Rails, Sinatra
|
32
|
-
- 🔒 **Authentication Support** - Secure your AI endpoints with ease
|
31
|
+
- 🧩 **Framework Integration** - Works seamlessly with Rails, Sinatra or any Rack app.
|
32
|
+
- 🔒 **Authentication Support** - Secure your AI-powered endpoints with ease
|
33
33
|
- 🚀 **Real-time Updates** - Subscribe to changes for interactive applications
|
34
34
|
|
35
|
-
## 💎 What Makes FastMCP Great
|
36
35
|
|
36
|
+
## 💎 What Makes FastMCP Great
|
37
37
|
```ruby
|
38
38
|
# Define tools for AI models to use
|
39
|
-
server =
|
39
|
+
server = FastMcp::Server.new(name: 'recipe-ai', version: '1.0.0')
|
40
40
|
|
41
|
-
# Define a tool by inheriting from
|
42
|
-
class
|
41
|
+
# Define a tool by inheriting from FastMcp::Tool
|
42
|
+
class CreateUserTool < FastMcp::Tool
|
43
43
|
description "Find recipes based on ingredients"
|
44
44
|
|
45
|
-
arguments do
|
46
45
|
# These arguments will generate the needed JSON to be presented to the MCP Client
|
47
|
-
# And they will be
|
46
|
+
# And they will be validated at run time.
|
48
47
|
# The validation is based off Dry-Schema, with the addition of the description.
|
49
|
-
|
50
|
-
|
48
|
+
arguments do
|
49
|
+
required(:first_name).filled(:string).description("First name of the user")
|
50
|
+
optional(:age).filled(:integer).description("Age of the user")
|
51
|
+
required(:address).hash do
|
52
|
+
optional(:street).filled(:string)
|
53
|
+
optional(:city).filled(:string)
|
54
|
+
optional(:zipcode).filled(:string)
|
55
|
+
end
|
51
56
|
end
|
52
57
|
|
53
|
-
def call(
|
54
|
-
|
58
|
+
def call(first_name:, age: nil, address: {})
|
59
|
+
User.create!(first_name:, age:, address:)
|
55
60
|
end
|
56
61
|
end
|
57
62
|
|
58
63
|
# Register the tool with the server
|
59
|
-
server.register_tool(
|
64
|
+
server.register_tool(CreateUserTool)
|
60
65
|
|
61
|
-
# Share data resources with AI models by inheriting from
|
62
|
-
class
|
63
|
-
uri "
|
64
|
-
|
66
|
+
# Share data resources with AI models by inheriting from FastMcp::Resource
|
67
|
+
class PopularUsers < FastMcp::Resource
|
68
|
+
uri "file://popular_users.json"
|
69
|
+
resource_name "Popular Users"
|
65
70
|
mime_type "application/json"
|
66
71
|
|
67
|
-
def
|
68
|
-
JSON.generate(
|
72
|
+
def content
|
73
|
+
JSON.generate(User.popular.limit(5).as_json)
|
69
74
|
end
|
70
75
|
end
|
71
76
|
|
@@ -73,88 +78,109 @@ end
|
|
73
78
|
server.register_resource(IngredientsResource)
|
74
79
|
|
75
80
|
# Accessing the resource through the server
|
76
|
-
server.read_resource(
|
81
|
+
server.read_resource(IngredientsResource.uri)
|
77
82
|
|
78
|
-
#
|
79
|
-
server.
|
83
|
+
# Notify the resource content has been updated to clients
|
84
|
+
server.notify_resource_updated(IngredientsResource.uri)
|
85
|
+
```
|
80
86
|
|
87
|
+
### 🚂 Fast Ruby on Rails implementation
|
88
|
+
```shell
|
89
|
+
bundle add fast-mcp
|
90
|
+
bin/rails generate fast_mcp:install
|
91
|
+
```
|
81
92
|
|
82
|
-
|
83
|
-
# config/application.rb (Rails)
|
84
|
-
config.middleware.use MCP::RackMiddleware.new(
|
85
|
-
name: 'recipe-ai',
|
86
|
-
version: '1.0.0'
|
87
|
-
) do |server|
|
88
|
-
# Register tools and resources here
|
89
|
-
server.register_tool(GetRecipesTool)
|
90
|
-
end
|
93
|
+
This will add a configurable `fast_mcp.rb` initializer
|
91
94
|
|
92
|
-
|
93
|
-
|
94
|
-
name: 'recipe-ai',
|
95
|
-
version: '1.0.0',
|
96
|
-
token: ENV['MCP_AUTH_TOKEN']
|
97
|
-
)
|
95
|
+
```ruby
|
96
|
+
require 'fast_mcp'
|
98
97
|
|
99
|
-
|
100
|
-
|
101
|
-
|
98
|
+
FastMcp.mount_in_rails(
|
99
|
+
Rails.application,
|
100
|
+
name: Rails.application.class.module_parent_name.underscore.dasherize,
|
101
|
+
version: '1.0.0',
|
102
|
+
path_prefix: '/mcp' # This is the default path prefix
|
103
|
+
# authenticate: true, # Uncomment to enable authentication
|
104
|
+
# auth_token: 'your-token', # Required if authenticate: true
|
105
|
+
) do |server|
|
106
|
+
Rails.application.config.after_initialize do
|
107
|
+
# FastMcp will automatically discover and register:
|
108
|
+
# - All classes that inherit from ApplicationTool (which uses ActionTool::Base)
|
109
|
+
# - All classes that inherit from ApplicationResource (which uses ActionResource::Base)
|
110
|
+
server.register_tools(*ApplicationTool.descendants)
|
111
|
+
server.register_resources(*ApplicationResource.descendants)
|
112
|
+
# alternatively, you can register tools and resources manually:
|
113
|
+
# server.register_tool(MyTool)
|
114
|
+
# server.register_resource(MyResource)
|
115
|
+
end
|
102
116
|
end
|
103
117
|
```
|
118
|
+
The install script will also:
|
119
|
+
- add app/resources folder
|
120
|
+
- add app/tools folder
|
121
|
+
- add app/tools/sample_tool.rb
|
122
|
+
- add app/resources/sample_resource.rb
|
123
|
+
- add ApplicationTool to inherit from
|
124
|
+
- add ApplicationResource to inherit from as well
|
104
125
|
|
105
|
-
|
126
|
+
#### Rails-friendly class naming conventions
|
106
127
|
|
107
|
-
|
108
|
-
# In your Gemfile
|
109
|
-
gem 'fast-mcp'
|
128
|
+
For Rails applications, FastMCP provides Rails-style class names to better fit with Rails conventions:
|
110
129
|
|
111
|
-
|
112
|
-
|
130
|
+
- `ActionTool::Base` - An alias for `FastMcp::Tool`
|
131
|
+
- `ActionResource::Base` - An alias for `FastMcp::Resource`
|
113
132
|
|
114
|
-
|
115
|
-
gem install fast-mcp
|
116
|
-
```
|
133
|
+
These are automatically set up in Rails applications. You can use either naming convention in your code:
|
117
134
|
|
118
|
-
|
135
|
+
```ruby
|
136
|
+
# Using Rails-style naming:
|
137
|
+
class MyTool < ActionTool::Base
|
138
|
+
description "My awesome tool"
|
139
|
+
|
140
|
+
arguments do
|
141
|
+
required(:input).filled(:string)
|
142
|
+
end
|
143
|
+
|
144
|
+
def call(input:)
|
145
|
+
# Your implementation
|
146
|
+
end
|
147
|
+
end
|
119
148
|
|
120
|
-
|
149
|
+
# Using standard FastMcp naming:
|
150
|
+
class AnotherTool < FastMcp::Tool
|
151
|
+
# Both styles work interchangeably in Rails apps
|
152
|
+
end
|
153
|
+
```
|
121
154
|
|
122
|
-
|
123
|
-
You can use it to validate your implementation. I suggest you use the examples I provided with this project as an easy boilerplate.
|
124
|
-
Clone this project, then give it a go !
|
155
|
+
When creating new tools or resources, the generators will use the Rails naming convention by default:
|
125
156
|
|
126
|
-
```
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
npx @modelcontextprotocol/inspector examples/rack_middleware.rb
|
132
|
-
```
|
157
|
+
```ruby
|
158
|
+
# app/tools/application_tool.rb
|
159
|
+
class ApplicationTool < ActionTool::Base
|
160
|
+
# Base methods for all tools
|
161
|
+
end
|
133
162
|
|
134
|
-
|
135
|
-
|
136
|
-
|
163
|
+
# app/resources/application_resource.rb
|
164
|
+
class ApplicationResource < ActionResource::Base
|
165
|
+
# Base methods for all resources
|
166
|
+
end
|
137
167
|
```
|
138
168
|
|
139
|
-
|
140
|
-
|
141
|
-
# Test with a stdio transport:
|
142
|
-
npx @modelcontextprotocol/inspector path/to/your_ruby_file.rb
|
169
|
+
### Easy Sinatra setup
|
170
|
+
I'll let you check out the dedicated [sinatra integration docs](./docs/sinatra_integration.md).
|
143
171
|
|
144
|
-
|
145
|
-
npx @modelcontextprotocol/inspector
|
146
|
-
```
|
172
|
+
## 🚀 Quick Start
|
147
173
|
|
148
|
-
### Create a Server with Tools and Resources
|
174
|
+
### Create a Server with Tools and Resources and STDIO transport
|
149
175
|
|
150
176
|
```ruby
|
151
177
|
require 'fast_mcp'
|
152
178
|
|
153
179
|
# Create an MCP server
|
154
|
-
server =
|
180
|
+
server = FastMcp::Server.new(name: 'my-ai-server', version: '1.0.0')
|
155
181
|
|
156
|
-
# Define a tool by inheriting from
|
157
|
-
class SummarizeTool <
|
182
|
+
# Define a tool by inheriting from FastMcp::Tool
|
183
|
+
class SummarizeTool < FastMcp::Tool
|
158
184
|
description "Summarize a given text"
|
159
185
|
|
160
186
|
arguments do
|
@@ -171,14 +197,14 @@ end
|
|
171
197
|
# Register the tool with the server
|
172
198
|
server.register_tool(SummarizeTool)
|
173
199
|
|
174
|
-
# Create a resource by inheriting from
|
175
|
-
class StatisticsResource <
|
200
|
+
# Create a resource by inheriting from FastMcp::Resource
|
201
|
+
class StatisticsResource < FastMcp::Resource
|
176
202
|
uri "data/statistics"
|
177
|
-
|
203
|
+
resource_name "Usage Statistics"
|
178
204
|
description "Current system statistics"
|
179
205
|
mime_type "application/json"
|
180
206
|
|
181
|
-
def
|
207
|
+
def content
|
182
208
|
JSON.generate({
|
183
209
|
users_online: 120,
|
184
210
|
queries_per_minute: 250,
|
@@ -188,30 +214,38 @@ class StatisticsResource < MCP::Resource
|
|
188
214
|
end
|
189
215
|
|
190
216
|
# Register the resource with the server
|
191
|
-
server.register_resource(StatisticsResource
|
217
|
+
server.register_resource(StatisticsResource)
|
192
218
|
|
193
219
|
# Start the server
|
194
220
|
server.start
|
195
221
|
```
|
196
222
|
|
197
|
-
|
223
|
+
## 🧪 Testing with the inspector
|
198
224
|
|
199
|
-
|
225
|
+
MCP has developed a very [useful inspector](https://github.com/modelcontextprotocol/inspector).
|
226
|
+
You can use it to validate your implementation. I suggest you use the examples I provided with this project as an easy boilerplate.
|
227
|
+
Clone this project, then give it a go !
|
200
228
|
|
201
|
-
```
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
229
|
+
```shell
|
230
|
+
npx @modelcontextprotocol/inspector examples/server_with_stdio_transport.rb
|
231
|
+
```
|
232
|
+
Or to test with an SSE transport using a rack middleware:
|
233
|
+
```shell
|
234
|
+
npx @modelcontextprotocol/inspector examples/rack_middleware.rb
|
235
|
+
```
|
236
|
+
|
237
|
+
Or to test over SSE with an authenticated rack middleware:
|
238
|
+
```shell
|
239
|
+
npx @modelcontextprotocol/inspector examples/authenticated_rack_middleware.rb
|
240
|
+
```
|
241
|
+
|
242
|
+
You can test your custom implementation with the official MCP inspector by using:
|
243
|
+
```shell
|
244
|
+
# Test with a stdio transport:
|
245
|
+
npx @modelcontextprotocol/inspector path/to/your_ruby_file.rb
|
246
|
+
|
247
|
+
# Test with an HTTP / SSE server. In the UI select SSE and input your address.
|
248
|
+
npx @modelcontextprotocol/inspector
|
215
249
|
```
|
216
250
|
|
217
251
|
#### Sinatra
|
@@ -221,7 +255,7 @@ end
|
|
221
255
|
require 'sinatra'
|
222
256
|
require 'fast_mcp'
|
223
257
|
|
224
|
-
use
|
258
|
+
use FastMcp::RackMiddleware.new(name: 'my-ai-server', version: '1.0.0') do |server|
|
225
259
|
# Register tools and resources here
|
226
260
|
server.register_tool(SummarizeTool)
|
227
261
|
end
|
@@ -250,6 +284,9 @@ Add your server to your Claude Desktop configuration at:
|
|
250
284
|
}
|
251
285
|
```
|
252
286
|
|
287
|
+
## How to add a MCP server to Claude, Cursor, or other MCP clients?
|
288
|
+
Please refer to [configuring_mcp_clients](docs/configuring_mcp_clients.md)
|
289
|
+
|
253
290
|
## 📊 Supported Specifications
|
254
291
|
|
255
292
|
| Feature | Status |
|
@@ -276,11 +313,8 @@ Add your server to your Claude Desktop configuration at:
|
|
276
313
|
- [🧩 Integration Guide](docs/integration_guide.md)
|
277
314
|
- [🛤️ Rails Integration](docs/rails_integration.md)
|
278
315
|
- [🌐 Sinatra Integration](docs/sinatra_integration.md)
|
279
|
-
- [🌸 Hanami Integration](docs/hanami_integration.md)
|
280
316
|
- [📚 Resources](docs/resources.md)
|
281
317
|
- [🛠️ Tools](docs/tools.md)
|
282
|
-
- [🔌 Transports](docs/transports.md)
|
283
|
-
- [📘 API Reference](docs/api_reference.md)
|
284
318
|
|
285
319
|
## 💻 Examples
|
286
320
|
|
data/lib/fast_mcp.rb
CHANGED
@@ -4,13 +4,20 @@
|
|
4
4
|
# https://modelcontextprotocol.io/introduction
|
5
5
|
|
6
6
|
# Define the MCP module
|
7
|
-
module
|
7
|
+
module FastMcp
|
8
|
+
class << self
|
9
|
+
attr_accessor :server
|
10
|
+
end
|
8
11
|
end
|
9
12
|
|
10
13
|
# Require the core components
|
11
14
|
require_relative 'mcp/tool'
|
12
15
|
require_relative 'mcp/server'
|
13
16
|
require_relative 'mcp/resource'
|
17
|
+
require_relative 'mcp/railtie' if defined?(Rails::Railtie)
|
18
|
+
|
19
|
+
# Load generators if Rails is available
|
20
|
+
require_relative 'generators/fast_mcp/install/install_generator' if defined?(Rails::Generators)
|
14
21
|
|
15
22
|
# Require all transport files
|
16
23
|
require_relative 'mcp/transports/base_transport'
|
@@ -22,7 +29,7 @@ end
|
|
22
29
|
require_relative 'mcp/version'
|
23
30
|
|
24
31
|
# Convenience method to create a Rack middleware
|
25
|
-
module
|
32
|
+
module FastMcp
|
26
33
|
# Create a Rack middleware for the MCP server
|
27
34
|
# @param app [#call] The Rack application
|
28
35
|
# @param options [Hash] Options for the middleware
|
@@ -31,19 +38,22 @@ module MCP
|
|
31
38
|
# @option options [String] :path_prefix The path prefix for the MCP endpoints
|
32
39
|
# @option options [Logger] :logger The logger to use
|
33
40
|
# @yield [server] A block to configure the server
|
34
|
-
# @yieldparam server [
|
41
|
+
# @yieldparam server [FastMcp::Server] The server to configure
|
35
42
|
# @return [#call] The Rack middleware
|
36
43
|
def self.rack_middleware(app, options = {})
|
37
44
|
name = options.delete(:name) || 'mcp-server'
|
38
45
|
version = options.delete(:version) || '1.0.0'
|
39
46
|
logger = options.delete(:logger) || Logger.new
|
40
47
|
|
41
|
-
server =
|
48
|
+
server = FastMcp::Server.new(name: name, version: version, logger: logger)
|
42
49
|
yield server if block_given?
|
43
50
|
|
44
51
|
# Store the server in the Sinatra settings if available
|
45
52
|
app.settings.set(:mcp_server, server) if app.respond_to?(:settings) && app.settings.respond_to?(:mcp_server=)
|
46
53
|
|
54
|
+
# Store the server in the FastMcp module
|
55
|
+
self.server = server
|
56
|
+
|
47
57
|
server.start_rack(app, options)
|
48
58
|
end
|
49
59
|
|
@@ -54,16 +64,93 @@ module MCP
|
|
54
64
|
# @option options [String] :version The version of the server
|
55
65
|
# @option options [String] :auth_token The authentication token
|
56
66
|
# @yield [server] A block to configure the server
|
57
|
-
# @yieldparam server [
|
67
|
+
# @yieldparam server [FastMcp::Server] The server to configure
|
58
68
|
# @return [#call] The Rack middleware
|
59
69
|
def self.authenticated_rack_middleware(app, options = {})
|
60
70
|
name = options.delete(:name) || 'mcp-server'
|
61
71
|
version = options.delete(:version) || '1.0.0'
|
62
72
|
logger = options.delete(:logger) || Logger.new
|
63
73
|
|
64
|
-
server =
|
74
|
+
server = FastMcp::Server.new(name: name, version: version, logger: logger)
|
65
75
|
yield server if block_given?
|
66
76
|
|
77
|
+
# Store the server in the FastMcp module
|
78
|
+
self.server = server
|
79
|
+
|
67
80
|
server.start_authenticated_rack(app, options)
|
68
81
|
end
|
82
|
+
|
83
|
+
# Register a tool with the MCP server
|
84
|
+
# @param tool [FastMcp::Tool] The tool to register
|
85
|
+
# @return [FastMcp::Tool] The registered tool
|
86
|
+
def self.register_tool(tool)
|
87
|
+
self.server ||= FastMcp::Server.new(name: 'mcp-server', version: '1.0.0')
|
88
|
+
self.server.register_tool(tool)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Register multiple tools at once
|
92
|
+
# @param tools [Array<FastMcp::Tool>] The tools to register
|
93
|
+
# @return [Array<FastMcp::Tool>] The registered tools
|
94
|
+
def self.register_tools(*tools)
|
95
|
+
self.server ||= FastMcp::Server.new(name: 'mcp-server', version: '1.0.0')
|
96
|
+
self.server.register_tools(*tools)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Register a resource with the MCP server
|
100
|
+
# @param resource [FastMcp::Resource] The resource to register
|
101
|
+
# @return [FastMcp::Resource] The registered resource
|
102
|
+
def self.register_resource(resource)
|
103
|
+
self.server ||= FastMcp::Server.new(name: 'mcp-server', version: '1.0.0')
|
104
|
+
self.server.register_resource(resource)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Register multiple resources at once
|
108
|
+
# @param resources [Array<FastMcp::Resource>] The resources to register
|
109
|
+
# @return [Array<FastMcp::Resource>] The registered resources
|
110
|
+
def self.register_resources(*resources)
|
111
|
+
self.server ||= FastMcp::Server.new(name: 'mcp-server', version: '1.0.0')
|
112
|
+
self.server.register_resources(*resources)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Mount the MCP middleware in a Rails application
|
116
|
+
# @param app [Rails::Application] The Rails application
|
117
|
+
# @param options [Hash] Options for the middleware
|
118
|
+
# @option options [String] :name The name of the server
|
119
|
+
# @option options [String] :version The version of the server
|
120
|
+
# @option options [String] :path_prefix The path prefix for the MCP endpoints
|
121
|
+
# @option options [Logger] :logger The logger to use
|
122
|
+
# @option options [Boolean] :authenticate Whether to use authentication
|
123
|
+
# @option options [String] :auth_token The authentication token
|
124
|
+
# @yield [server] A block to configure the server
|
125
|
+
# @yieldparam server [FastMcp::Server] The server to configure
|
126
|
+
# @return [#call] The Rack middleware
|
127
|
+
def self.mount_in_rails(app, options = {})
|
128
|
+
# Default options
|
129
|
+
name = options.delete(:name) || app.class.module_parent_name.underscore.dasherize
|
130
|
+
version = options.delete(:version) || '1.0.0'
|
131
|
+
logger = options[:logger] || Rails.logger
|
132
|
+
path_prefix = options.delete(:path_prefix) || '/mcp'
|
133
|
+
authenticate = options.delete(:authenticate) || false
|
134
|
+
|
135
|
+
options[:logger] = logger
|
136
|
+
# Create or get the server
|
137
|
+
self.server = FastMcp::Server.new(name: name, version: version, logger: logger)
|
138
|
+
yield self.server if block_given?
|
139
|
+
|
140
|
+
# Choose the right middleware based on authentication
|
141
|
+
self.server.transport_klass = if authenticate
|
142
|
+
FastMcp::Transports::AuthenticatedRackTransport
|
143
|
+
else
|
144
|
+
FastMcp::Transports::RackTransport
|
145
|
+
end
|
146
|
+
|
147
|
+
# Insert the middleware in the Rails middleware stack
|
148
|
+
app.middleware.use self.server.transport_klass, self.server, options.merge(path_prefix: path_prefix)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Notify the server that a resource has been updated
|
152
|
+
# @param uri [String] The URI of the resource
|
153
|
+
def self.notify_resource_updated(uri)
|
154
|
+
self.server.notify_resource_updated(uri)
|
155
|
+
end
|
69
156
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/base'
|
4
|
+
|
5
|
+
module FastMcp
|
6
|
+
module Generators
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
9
|
+
|
10
|
+
desc 'Creates a FastMcp initializer for Rails applications'
|
11
|
+
|
12
|
+
def copy_initializer
|
13
|
+
template 'fast_mcp_initializer.rb', 'config/initializers/fast_mcp.rb'
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_directories
|
17
|
+
empty_directory 'app/tools'
|
18
|
+
empty_directory 'app/resources'
|
19
|
+
end
|
20
|
+
|
21
|
+
def copy_application_tool
|
22
|
+
template 'application_tool.rb', 'app/tools/application_tool.rb'
|
23
|
+
end
|
24
|
+
|
25
|
+
def copy_application_resource
|
26
|
+
template 'application_resource.rb', 'app/resources/application_resource.rb'
|
27
|
+
end
|
28
|
+
|
29
|
+
def copy_sample_tool
|
30
|
+
template 'sample_tool.rb', 'app/tools/sample_tool.rb'
|
31
|
+
end
|
32
|
+
|
33
|
+
def copy_sample_resource
|
34
|
+
template 'sample_resource.rb', 'app/resources/sample_resource.rb'
|
35
|
+
end
|
36
|
+
|
37
|
+
def display_post_install_message
|
38
|
+
say "\n========================================================="
|
39
|
+
say 'FastMcp was successfully installed! 🎉'
|
40
|
+
say "=========================================================\n"
|
41
|
+
say 'You can now create:'
|
42
|
+
say ' • Tools in app/tools/'
|
43
|
+
say ' • Resources in app/resources/'
|
44
|
+
say "\n"
|
45
|
+
say 'Check config/initializers/fast_mcp.rb to configure the middleware.'
|
46
|
+
say "=========================================================\n"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# FastMcp - Model Context Protocol for Rails
|
4
|
+
# This initializer sets up the MCP middleware in your Rails application.
|
5
|
+
#
|
6
|
+
# In Rails applications, you can use:
|
7
|
+
# - ActionTool::Base as an alias for FastMcp::Tool
|
8
|
+
# - ActionResource::Base as an alias for FastMcp::Resource
|
9
|
+
#
|
10
|
+
# All your tools should inherit from ApplicationTool which already uses ActionTool::Base,
|
11
|
+
# and all your resources should inherit from ApplicationResource which uses ActionResource::Base.
|
12
|
+
|
13
|
+
# Mount the MCP middleware in your Rails application
|
14
|
+
# You can customize the options below to fit your needs.
|
15
|
+
require 'fast_mcp'
|
16
|
+
|
17
|
+
FastMcp.mount_in_rails(
|
18
|
+
Rails.application,
|
19
|
+
name: Rails.application.class.module_parent_name.underscore.dasherize,
|
20
|
+
version: '1.0.0',
|
21
|
+
path_prefix: '/mcp' # This is the default path prefix
|
22
|
+
# authenticate: true, # Uncomment to enable authentication
|
23
|
+
# auth_token: 'your-token', # Required if authenticate: true
|
24
|
+
) do |server|
|
25
|
+
Rails.application.config.after_initialize do
|
26
|
+
# FastMcp will automatically discover and register:
|
27
|
+
# - All classes that inherit from ApplicationTool (which uses ActionTool::Base)
|
28
|
+
# - All classes that inherit from ApplicationResource (which uses ActionResource::Base)
|
29
|
+
server.register_tools(*ApplicationTool.descendants)
|
30
|
+
server.register_resources(*ApplicationResource.descendants)
|
31
|
+
# alternatively, you can register tools and resources manually:
|
32
|
+
# server.register_tool(MyTool)
|
33
|
+
# server.register_resource(MyResource)
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SampleResource < ApplicationResource
|
4
|
+
uri 'examples/users'
|
5
|
+
resource_name 'Users'
|
6
|
+
description 'A user resource for demonstration'
|
7
|
+
mime_type 'application/json'
|
8
|
+
|
9
|
+
def content
|
10
|
+
JSON.generate(User.all.as_json)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SampleTool < ApplicationTool
|
4
|
+
description 'Greet a user'
|
5
|
+
|
6
|
+
arguments do
|
7
|
+
required(:id).filled(:integer).description('ID of the user to greet')
|
8
|
+
optional(:prefix).filled(:string).description('Prefix to add to the greeting')
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(id:, prefix: 'Hey')
|
12
|
+
user = User.find(id)
|
13
|
+
|
14
|
+
"#{prefix} #{user.first_name} !"
|
15
|
+
end
|
16
|
+
end
|
data/lib/mcp/logger.rb
CHANGED
@@ -1,33 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# This class is not used yet.
|
4
|
-
module
|
4
|
+
module FastMcp
|
5
5
|
class Logger < Logger
|
6
|
-
def initialize
|
6
|
+
def initialize(transport: :stdio)
|
7
7
|
@client_initialized = false
|
8
|
-
@transport =
|
8
|
+
@transport = transport
|
9
9
|
|
10
|
-
|
10
|
+
# we don't want to log to stdout if we're using the stdio transport
|
11
|
+
super($stdout) unless stdio_transport?
|
11
12
|
end
|
12
13
|
|
13
14
|
attr_accessor :transport, :client_initialized
|
14
|
-
|
15
|
-
def client_initialized?
|
16
|
-
client_initialized
|
17
|
-
end
|
15
|
+
alias client_initialized? client_initialized
|
18
16
|
|
19
17
|
def stdio_transport?
|
20
18
|
transport == :stdio
|
21
19
|
end
|
22
20
|
|
21
|
+
def add(severity, message = nil, progname = nil, &block)
|
22
|
+
return if stdio_transport? # we don't want to log to stdout if we're using the stdio transport
|
23
|
+
|
24
|
+
# TODO: implement logging as the specification requires
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
23
28
|
def rack_transport?
|
24
29
|
transport == :rack
|
25
30
|
end
|
26
|
-
|
27
|
-
# def add(severity, message = nil, progname = nil, &block)
|
28
|
-
# # return unless client_initialized? && rack_transport?
|
29
|
-
|
30
|
-
# super
|
31
|
-
# end
|
32
31
|
end
|
33
32
|
end
|
data/lib/mcp/railtie.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'fileutils'
|
5
|
+
require_relative '../mcp/server'
|
6
|
+
|
7
|
+
# Create ActionTool and ActionResource modules at load time
|
8
|
+
unless defined?(ActionTool)
|
9
|
+
module ::ActionTool
|
10
|
+
Base = FastMcp::Tool
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
unless defined?(ActionResource)
|
15
|
+
module ::ActionResource
|
16
|
+
Base = FastMcp::Resource
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module FastMcp
|
21
|
+
# Railtie for integrating Fast MCP with Rails applications
|
22
|
+
class Railtie < Rails::Railtie
|
23
|
+
# Add tools and resources directories to autoload paths
|
24
|
+
initializer 'fast_mcp.setup_autoload_paths' do |app|
|
25
|
+
app.config.autoload_paths += %W[
|
26
|
+
#{app.root}/app/tools
|
27
|
+
#{app.root}/app/resources
|
28
|
+
]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Auto-register all tools and resources after the application is fully loaded
|
32
|
+
config.after_initialize do
|
33
|
+
# Load all files in app/tools and app/resources directories
|
34
|
+
Dir[Rails.root.join('app', 'tools', '**', '*.rb')].each { |f| require f }
|
35
|
+
Dir[Rails.root.join('app', 'resources', '**', '*.rb')].each { |f| require f }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Add rake tasks
|
39
|
+
rake_tasks do
|
40
|
+
# Path to the tasks directory in the gem
|
41
|
+
path = File.expand_path('../tasks', __dir__)
|
42
|
+
Dir.glob("#{path}/**/*.rake").each { |f| load f }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/mcp/resource.rb
CHANGED
@@ -5,11 +5,13 @@ require 'base64'
|
|
5
5
|
require 'mime/types'
|
6
6
|
require 'singleton'
|
7
7
|
|
8
|
-
module
|
8
|
+
module FastMcp
|
9
9
|
# Resource class for MCP Resources feature
|
10
10
|
# Represents a resource that can be exposed to clients
|
11
11
|
class Resource
|
12
12
|
class << self
|
13
|
+
attr_accessor :server
|
14
|
+
|
13
15
|
# Define URI for this resource
|
14
16
|
# @param value [String, nil] The URI for this resource
|
15
17
|
# @return [String] The URI for this resource
|
@@ -74,7 +76,7 @@ module MCP
|
|
74
76
|
end
|
75
77
|
|
76
78
|
# Override content method to load from file
|
77
|
-
define_method :
|
79
|
+
define_method :content do
|
78
80
|
if binary?
|
79
81
|
File.binread(file_path)
|
80
82
|
else
|
@@ -87,13 +89,6 @@ module MCP
|
|
87
89
|
|
88
90
|
include Singleton
|
89
91
|
|
90
|
-
attr_accessor :content
|
91
|
-
|
92
|
-
# Initialize a resource singleton instance
|
93
|
-
def initialize
|
94
|
-
@content = default_content
|
95
|
-
end
|
96
|
-
|
97
92
|
# URI of the resource - delegates to class method
|
98
93
|
# @return [String, nil] The URI for this resource
|
99
94
|
def uri
|
@@ -120,7 +115,7 @@ module MCP
|
|
120
115
|
|
121
116
|
# Method to be overridden by subclasses to dynamically generate content
|
122
117
|
# @return [String, nil] Generated content for this resource
|
123
|
-
def
|
118
|
+
def content
|
124
119
|
raise NotImplementedError, 'Subclasses must implement content'
|
125
120
|
end
|
126
121
|
|
data/lib/mcp/server.rb
CHANGED
@@ -9,9 +9,9 @@ require_relative 'transports/rack_transport'
|
|
9
9
|
require_relative 'transports/authenticated_rack_transport'
|
10
10
|
require_relative 'logger'
|
11
11
|
|
12
|
-
module
|
12
|
+
module FastMcp
|
13
13
|
class Server
|
14
|
-
attr_reader :name, :version, :tools, :resources, :
|
14
|
+
attr_reader :name, :version, :tools, :resources, :capabilities
|
15
15
|
|
16
16
|
DEFAULT_CAPABILITIES = {
|
17
17
|
resources: {
|
@@ -23,7 +23,7 @@ module MCP
|
|
23
23
|
}
|
24
24
|
}.freeze
|
25
25
|
|
26
|
-
def initialize(name:, version:, logger:
|
26
|
+
def initialize(name:, version:, logger: FastMcp::Logger.new, capabilities: {})
|
27
27
|
@name = name
|
28
28
|
@version = version
|
29
29
|
@tools = {}
|
@@ -32,13 +32,17 @@ module MCP
|
|
32
32
|
@logger = logger
|
33
33
|
@logger.level = Logger::INFO
|
34
34
|
@request_id = 0
|
35
|
+
@transport_klass = nil
|
35
36
|
@transport = nil
|
36
37
|
@capabilities = DEFAULT_CAPABILITIES.dup
|
37
38
|
|
38
39
|
# Merge with provided capabilities
|
39
40
|
@capabilities.merge!(capabilities) if capabilities.is_a?(Hash)
|
40
41
|
end
|
42
|
+
attr_accessor :transport, :transport_klass, :logger
|
41
43
|
|
44
|
+
# Register multiple tools at once
|
45
|
+
# @param tools [Array<Tool>] Tools to register
|
42
46
|
def register_tools(*tools)
|
43
47
|
tools.each do |tool|
|
44
48
|
register_tool(tool)
|
@@ -49,6 +53,7 @@ module MCP
|
|
49
53
|
def register_tool(tool)
|
50
54
|
@tools[tool.tool_name] = tool
|
51
55
|
@logger.info("Registered tool: #{tool.tool_name}")
|
56
|
+
tool.server = self
|
52
57
|
end
|
53
58
|
|
54
59
|
# Register multiple resources at once
|
@@ -63,7 +68,7 @@ module MCP
|
|
63
68
|
def register_resource(resource)
|
64
69
|
@resources[resource.uri] = resource
|
65
70
|
@logger.info("Registered resource: #{resource.name} (#{resource.uri})")
|
66
|
-
|
71
|
+
resource.server = self
|
67
72
|
# Notify subscribers about the list change
|
68
73
|
notify_resource_list_changed if @transport
|
69
74
|
|
@@ -93,7 +98,8 @@ module MCP
|
|
93
98
|
@logger.info("Available resources: #{@resources.keys.join(', ')}")
|
94
99
|
|
95
100
|
# Use STDIO transport by default
|
96
|
-
@
|
101
|
+
@transport_klass = FastMcp::Transports::StdioTransport
|
102
|
+
@transport = @transport_klass.new(self, logger: @logger)
|
97
103
|
@transport.start
|
98
104
|
end
|
99
105
|
|
@@ -104,7 +110,8 @@ module MCP
|
|
104
110
|
@logger.info("Available resources: #{@resources.keys.join(', ')}")
|
105
111
|
|
106
112
|
# Use Rack transport
|
107
|
-
|
113
|
+
transport_klass = FastMcp::Transports::RackTransport
|
114
|
+
@transport = transport_klass.new(app, self, options.merge(logger: @logger))
|
108
115
|
@transport.start
|
109
116
|
|
110
117
|
# Return the transport as middleware
|
@@ -117,7 +124,8 @@ module MCP
|
|
117
124
|
@logger.info("Available resources: #{@resources.keys.join(', ')}")
|
118
125
|
|
119
126
|
# Use Rack transport
|
120
|
-
|
127
|
+
transport_klass = FastMcp::Transports::AuthenticatedRackTransport
|
128
|
+
@transport = transport_klass.new(app, self, options.merge(logger: @logger))
|
121
129
|
@transport.start
|
122
130
|
|
123
131
|
# Return the transport as middleware
|
@@ -180,47 +188,6 @@ module MCP
|
|
180
188
|
end
|
181
189
|
end
|
182
190
|
|
183
|
-
# Register a callback for resource updates
|
184
|
-
def on_resource_update(&block)
|
185
|
-
@resource_update_callbacks ||= []
|
186
|
-
callback_id = SecureRandom.uuid
|
187
|
-
@resource_update_callbacks << { id: callback_id, callback: block }
|
188
|
-
callback_id
|
189
|
-
end
|
190
|
-
|
191
|
-
# Remove a resource update callback
|
192
|
-
def remove_resource_update_callback(callback_id)
|
193
|
-
@resource_update_callbacks ||= []
|
194
|
-
@resource_update_callbacks.reject! { |cb| cb[:id] == callback_id }
|
195
|
-
end
|
196
|
-
|
197
|
-
# Update a resource and notify subscribers
|
198
|
-
def update_resource(uri, content)
|
199
|
-
return false unless @resources.key?(uri)
|
200
|
-
|
201
|
-
resource = @resources[uri]
|
202
|
-
resource.instance.content = content
|
203
|
-
|
204
|
-
# Notify subscribers
|
205
|
-
notify_resource_updated(uri) if @transport && @resource_subscriptions.key?(uri)
|
206
|
-
|
207
|
-
# Notify resource update callbacks
|
208
|
-
if @resource_update_callbacks && !@resource_update_callbacks.empty?
|
209
|
-
@resource_update_callbacks.each do |cb|
|
210
|
-
cb[:callback].call(
|
211
|
-
{
|
212
|
-
uri: uri,
|
213
|
-
name: resource.name,
|
214
|
-
mime_type: resource.mime_type,
|
215
|
-
content: content
|
216
|
-
}
|
217
|
-
)
|
218
|
-
end
|
219
|
-
end
|
220
|
-
|
221
|
-
true
|
222
|
-
end
|
223
|
-
|
224
191
|
# Read a resource directly
|
225
192
|
def read_resource(uri)
|
226
193
|
resource = @resources[uri]
|
@@ -229,13 +196,32 @@ module MCP
|
|
229
196
|
resource
|
230
197
|
end
|
231
198
|
|
199
|
+
# Notify subscribers about a resource update
|
200
|
+
def notify_resource_updated(uri)
|
201
|
+
@logger.warn("Notifying subscribers about resource update: #{uri}, #{@resource_subscriptions.inspect}")
|
202
|
+
return unless @client_initialized && @resource_subscriptions.key?(uri)
|
203
|
+
|
204
|
+
resource = @resources[uri]
|
205
|
+
notification = {
|
206
|
+
jsonrpc: '2.0',
|
207
|
+
method: 'notifications/resources/updated',
|
208
|
+
params: {
|
209
|
+
uri: uri,
|
210
|
+
name: resource.name,
|
211
|
+
mimeType: resource.mime_type
|
212
|
+
}
|
213
|
+
}
|
214
|
+
|
215
|
+
@transport.send_message(notification)
|
216
|
+
end
|
217
|
+
|
232
218
|
private
|
233
219
|
|
234
220
|
PROTOCOL_VERSION = '2024-11-05'
|
235
221
|
|
236
222
|
def handle_initialize(params, id)
|
237
|
-
|
238
|
-
client_capabilities = params['capabilities'] || {}
|
223
|
+
# Store client capabilities for later use
|
224
|
+
@client_capabilities = params['capabilities'] || {}
|
239
225
|
client_info = params['clientInfo'] || {}
|
240
226
|
|
241
227
|
# Log client information
|
@@ -254,9 +240,6 @@ module MCP
|
|
254
240
|
|
255
241
|
@logger.info("Server response: #{response.inspect}")
|
256
242
|
|
257
|
-
# Store client capabilities for later use
|
258
|
-
@client_capabilities = client_capabilities
|
259
|
-
|
260
243
|
send_result(response, id)
|
261
244
|
end
|
262
245
|
|
@@ -290,8 +273,9 @@ module MCP
|
|
290
273
|
# The client is now ready for normal operation
|
291
274
|
# No response needed for notifications
|
292
275
|
@client_initialized = true
|
293
|
-
@logger.set_client_initialized
|
294
276
|
@logger.info('Client initialized, beginning normal operation')
|
277
|
+
|
278
|
+
nil
|
295
279
|
end
|
296
280
|
|
297
281
|
# Handle tools/list request
|
@@ -324,7 +308,7 @@ module MCP
|
|
324
308
|
|
325
309
|
# Format and send the result
|
326
310
|
send_formatted_result(result, id)
|
327
|
-
rescue
|
311
|
+
rescue FastMcp::Tool::InvalidArgumentsError => e
|
328
312
|
@logger.error("Invalid arguments for tool #{tool_name}: #{e.message}")
|
329
313
|
send_error_result(e.message, id)
|
330
314
|
rescue StandardError => e
|
@@ -410,24 +394,6 @@ module MCP
|
|
410
394
|
send_result({ unsubscribed: true }, id)
|
411
395
|
end
|
412
396
|
|
413
|
-
# Notify subscribers about a resource update
|
414
|
-
def notify_resource_updated(uri)
|
415
|
-
return unless @client_initialized && @resource_subscriptions.key?(uri)
|
416
|
-
|
417
|
-
resource = @resources[uri]
|
418
|
-
notification = {
|
419
|
-
jsonrpc: '2.0',
|
420
|
-
method: 'notifications/resources/updated',
|
421
|
-
params: {
|
422
|
-
uri: uri,
|
423
|
-
name: resource.name,
|
424
|
-
mimeType: resource.mime_type
|
425
|
-
}
|
426
|
-
}
|
427
|
-
|
428
|
-
@transport.send_message(notification)
|
429
|
-
end
|
430
|
-
|
431
397
|
# Notify clients about resource list changes
|
432
398
|
def notify_resource_list_changed
|
433
399
|
return unless @client_initialized
|
@@ -470,10 +436,10 @@ module MCP
|
|
470
436
|
def send_response(response)
|
471
437
|
if @transport
|
472
438
|
@logger.info("Sending response: #{response.inspect}")
|
473
|
-
@logger.info("Transport: #{@transport.inspect}")
|
474
439
|
@transport.send_message(response)
|
475
440
|
else
|
476
441
|
@logger.warn("No transport available to send response: #{response.inspect}")
|
442
|
+
@logger.warn("Transport: #{@transport.inspect}, transport_klass: #{@transport_klass.inspect}")
|
477
443
|
end
|
478
444
|
end
|
479
445
|
|
data/lib/mcp/tool.rb
CHANGED
@@ -62,12 +62,14 @@ module Dry
|
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
65
|
-
module
|
65
|
+
module FastMcp
|
66
66
|
# Main Tool class that represents an MCP Tool
|
67
67
|
class Tool
|
68
68
|
class InvalidArgumentsError < StandardError; end
|
69
69
|
|
70
70
|
class << self
|
71
|
+
attr_accessor :server
|
72
|
+
|
71
73
|
def arguments(&block)
|
72
74
|
@input_schema = Dry::Schema.JSON(&block)
|
73
75
|
end
|
@@ -100,6 +102,10 @@ module MCP
|
|
100
102
|
end
|
101
103
|
end
|
102
104
|
|
105
|
+
def notify_resource_updated(uri)
|
106
|
+
self.class.server.notify_resource_updated(uri)
|
107
|
+
end
|
108
|
+
|
103
109
|
def call_with_schema_validation!(**args)
|
104
110
|
arg_validation = self.class.input_schema.call(args)
|
105
111
|
raise InvalidArgumentsError, arg_validation.errors.to_h.to_json if arg_validation.errors.any?
|
@@ -788,7 +794,7 @@ module MCP
|
|
788
794
|
end
|
789
795
|
|
790
796
|
# Example
|
791
|
-
# class ExampleTool <
|
797
|
+
# class ExampleTool < FastMcp::Tool
|
792
798
|
# description 'An example tool'
|
793
799
|
|
794
800
|
# arguments do
|
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
require_relative 'rack_transport'
|
4
4
|
|
5
|
-
module
|
5
|
+
module FastMcp
|
6
6
|
module Transports
|
7
7
|
class AuthenticatedRackTransport < RackTransport
|
8
|
-
def initialize(
|
8
|
+
def initialize(app, server, options = {})
|
9
9
|
super
|
10
10
|
|
11
11
|
@auth_token = options[:auth_token]
|
@@ -5,7 +5,7 @@ require 'securerandom'
|
|
5
5
|
require 'rack'
|
6
6
|
require_relative 'base_transport'
|
7
7
|
|
8
|
-
module
|
8
|
+
module FastMcp
|
9
9
|
module Transports
|
10
10
|
# Rack middleware transport for MCP
|
11
11
|
# This transport can be mounted in any Rack-compatible web framework
|
@@ -14,7 +14,7 @@ module MCP
|
|
14
14
|
|
15
15
|
attr_reader :app, :path_prefix, :sse_clients
|
16
16
|
|
17
|
-
def initialize(
|
17
|
+
def initialize(app, server, options = {}, &_block)
|
18
18
|
super(server, logger: options[:logger])
|
19
19
|
@app = app
|
20
20
|
@path_prefix = options[:path_prefix] || DEFAULT_PATH_PREFIX
|
@@ -24,13 +24,13 @@ module MCP
|
|
24
24
|
|
25
25
|
# Start the transport
|
26
26
|
def start
|
27
|
-
@logger.
|
27
|
+
@logger.debug("Starting Rack transport with path prefix: #{@path_prefix}")
|
28
28
|
@running = true
|
29
29
|
end
|
30
30
|
|
31
31
|
# Stop the transport
|
32
32
|
def stop
|
33
|
-
@logger.
|
33
|
+
@logger.debug('Stopping Rack transport')
|
34
34
|
@running = false
|
35
35
|
|
36
36
|
# Close all SSE connections
|
@@ -45,7 +45,7 @@ module MCP
|
|
45
45
|
# Send a message to all connected SSE clients
|
46
46
|
def send_message(message)
|
47
47
|
json_message = message.is_a?(String) ? message : JSON.generate(message)
|
48
|
-
@logger.
|
48
|
+
@logger.debug("Broadcasting message to #{@sse_clients.size} SSE clients: #{json_message}")
|
49
49
|
|
50
50
|
clients_to_remove = []
|
51
51
|
|
@@ -85,10 +85,12 @@ module MCP
|
|
85
85
|
def call(env)
|
86
86
|
request = Rack::Request.new(env)
|
87
87
|
path = request.path
|
88
|
-
@logger.
|
88
|
+
@logger.debug("Rack request path: #{path}")
|
89
89
|
|
90
90
|
# Check if the request is for our MCP endpoints
|
91
91
|
if path.start_with?(@path_prefix)
|
92
|
+
@logger.debug('Setting server transport to RackTransport')
|
93
|
+
@server.transport = self
|
92
94
|
handle_mcp_request(request, env)
|
93
95
|
else
|
94
96
|
# Pass through to the main application
|
@@ -107,7 +109,6 @@ module MCP
|
|
107
109
|
when '/sse'
|
108
110
|
handle_sse_request(request, env)
|
109
111
|
when '/messages'
|
110
|
-
@logger.info('Received message request')
|
111
112
|
handle_message_request(request)
|
112
113
|
else
|
113
114
|
@logger.info('Received unknown request')
|
@@ -270,11 +271,11 @@ module MCP
|
|
270
271
|
# Handle SSE with Rack hijacking (e.g., Puma)
|
271
272
|
def handle_rack_hijack_sse(env, headers)
|
272
273
|
client_id = extract_client_id(env)
|
273
|
-
@logger.
|
274
|
+
@logger.debug("Setting up Rack hijack SSE connection for client #{client_id}")
|
274
275
|
|
275
276
|
env['rack.hijack'].call
|
276
277
|
io = env['rack.hijack_io']
|
277
|
-
@logger.
|
278
|
+
@logger.debug("Obtained hijack IO for client #{client_id}")
|
278
279
|
|
279
280
|
setup_sse_connection(client_id, io, headers)
|
280
281
|
start_keep_alive_thread(client_id, io)
|
@@ -286,7 +287,7 @@ module MCP
|
|
286
287
|
# Set up the SSE connection
|
287
288
|
def setup_sse_connection(client_id, io, headers)
|
288
289
|
# Send headers
|
289
|
-
@logger.
|
290
|
+
@logger.debug("Sending HTTP headers for SSE connection #{client_id}")
|
290
291
|
io.write("HTTP/1.1 200 OK\r\n")
|
291
292
|
headers.each { |k, v| io.write("#{k}: #{v}\r\n") }
|
292
293
|
io.write("\r\n")
|
@@ -300,7 +301,7 @@ module MCP
|
|
300
301
|
|
301
302
|
# Send endpoint information as the first message
|
302
303
|
endpoint = "#{@path_prefix}/messages"
|
303
|
-
@logger.
|
304
|
+
@logger.debug("Sending endpoint information to client #{client_id}: #{endpoint}")
|
304
305
|
io.write("event: endpoint\ndata: #{endpoint}\n\n")
|
305
306
|
|
306
307
|
# Send a retry directive with a very short reconnect time
|
@@ -332,6 +333,7 @@ module MCP
|
|
332
333
|
ping_count = 0
|
333
334
|
ping_interval = 1 # Send a ping every 1 second
|
334
335
|
max_ping_count = 30 # Reset connection after 30 pings (about 30 seconds)
|
336
|
+
@running = true
|
335
337
|
|
336
338
|
while @running && !io.closed?
|
337
339
|
begin
|
@@ -357,13 +359,13 @@ module MCP
|
|
357
359
|
|
358
360
|
# Only send actual ping events every 5 counts to reduce overhead
|
359
361
|
if (ping_count % 5).zero?
|
360
|
-
@logger.
|
362
|
+
@logger.debug("Sending ping ##{ping_count} to SSE client #{client_id}")
|
361
363
|
send_ping_event(io)
|
362
364
|
end
|
363
365
|
|
364
366
|
# If we've reached the max ping count, force a reconnection
|
365
367
|
if ping_count >= max_ping_count
|
366
|
-
@logger.
|
368
|
+
@logger.debug("Reached max ping count (#{max_ping_count}) for client #{client_id}, forcing reconnection")
|
367
369
|
send_reconnect_event(io)
|
368
370
|
end
|
369
371
|
|
@@ -414,7 +416,7 @@ module MCP
|
|
414
416
|
|
415
417
|
# Handle message POST request
|
416
418
|
def handle_message_request(request)
|
417
|
-
@logger.
|
419
|
+
@logger.debug('Received message request')
|
418
420
|
return method_not_allowed_response unless request.post?
|
419
421
|
|
420
422
|
begin
|
@@ -431,9 +433,10 @@ module MCP
|
|
431
433
|
# Parse the request body
|
432
434
|
body = request.body.read
|
433
435
|
|
434
|
-
response = process_message(body)
|
436
|
+
response = process_message(body) || ''
|
435
437
|
@logger.info("Response: #{response}")
|
436
|
-
|
438
|
+
|
439
|
+
[200, { 'Content-Type' => 'application/json' }, response]
|
437
440
|
end
|
438
441
|
|
439
442
|
# Return a method not allowed error response
|
data/lib/mcp/version.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fast-mcp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yorick Jacquin
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-03-
|
11
|
+
date: 2025-03-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: base64
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: dry-schema
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -64,7 +78,14 @@ files:
|
|
64
78
|
- LICENSE
|
65
79
|
- README.md
|
66
80
|
- lib/fast_mcp.rb
|
81
|
+
- lib/generators/fast_mcp/install/install_generator.rb
|
82
|
+
- lib/generators/fast_mcp/install/templates/application_resource.rb
|
83
|
+
- lib/generators/fast_mcp/install/templates/application_tool.rb
|
84
|
+
- lib/generators/fast_mcp/install/templates/fast_mcp_initializer.rb
|
85
|
+
- lib/generators/fast_mcp/install/templates/sample_resource.rb
|
86
|
+
- lib/generators/fast_mcp/install/templates/sample_tool.rb
|
67
87
|
- lib/mcp/logger.rb
|
88
|
+
- lib/mcp/railtie.rb
|
68
89
|
- lib/mcp/resource.rb
|
69
90
|
- lib/mcp/server.rb
|
70
91
|
- lib/mcp/tool.rb
|
@@ -80,6 +101,7 @@ metadata:
|
|
80
101
|
homepage_uri: https://github.com/yjacquin/fast_mcp
|
81
102
|
source_code_uri: https://github.com/yjacquin/fast_mcp
|
82
103
|
changelog_uri: https://github.com/yjacquin/fast_mcp/blob/main/CHANGELOG.md
|
104
|
+
rubygems_mfa_required: 'true'
|
83
105
|
post_install_message:
|
84
106
|
rdoc_options: []
|
85
107
|
require_paths:
|