fast-mcp 1.3.2 → 1.5.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 +20 -0
- data/README.md +52 -4
- data/lib/mcp/resource.rb +86 -29
- data/lib/mcp/server.rb +94 -66
- data/lib/mcp/server_filtering.rb +80 -0
- data/lib/mcp/tool.rb +125 -79
- data/lib/mcp/transports/base_transport.rb +2 -2
- data/lib/mcp/transports/rack_transport.rb +146 -60
- data/lib/mcp/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a99f230c0ff73568287d0812e53c266dffca1bd46de3c0308000d5658b5b9a4
|
4
|
+
data.tar.gz: 38fb4275c6cf123691ed02145687c70f5251be28e784f71b42a00c85b5fbee3d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f3863d28ed426ef2274475d0ba39630dcc673ef91a416a3ae4b76833cdb87dac3d40d6f01806dddf32572e0d0fa7ab5f5b0a5dd0c5619c6c73202dcfbfa4ccf5
|
7
|
+
data.tar.gz: 246ee983dc56d22708261681f5df1f0150e1376f19a18e373024ed5ecd12c1a54f592998a9738fb8d0aabb7194fe937ce98304a145f7bbf12efc1555d0b7d794
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,26 @@ 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.5.0] - 2025-06-01
|
9
|
+
### Added
|
10
|
+
- Handle filtering tools and resources [#85 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/85)
|
11
|
+
- Support for resource templates 🥳 Big thanks to @danielcooper for the contribution [#84 co-authored by @danielcooper and @yjacquin](https://github.com/yjacquin/fast-mcp/pull/84)
|
12
|
+
- Possibility to authorize requests before tool calls [#79 @EuanEdgar](https://github.com/yjacquin/fast-mcp/pull/79)
|
13
|
+
- Possibility to read request headers in tool calls [#78 @EuanEdgar](https://github.com/yjacquin/fast-mcp/pull/78)
|
14
|
+
### Changed
|
15
|
+
- Bump Dependencies [#86 @aothelal](https://github.com/yjacquin/fast-mcp/pull/86)
|
16
|
+
- ⚠️ Resources are now stateless, meaning that in-memory resources won't work anymore, they require an external data source such as database, file to read and write too, etc. This was needed for a refactoring of the resource class for the [resource template PR](https://github.com/yjacquin/fast-mcp/pull/84)
|
17
|
+
### Fixed
|
18
|
+
- Stop overriding log level to info [#91 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/91)
|
19
|
+
- Properly handle ping request responses from clients [#89 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/89)
|
20
|
+
- Add Thread Safety to RackTransport sse_clients [#82 @Kevin-K](https://github.com/yjacquin/fast-mcp/pull/82)
|
21
|
+
|
22
|
+
## [1.4.0] - 2025-05-10
|
23
|
+
### Added
|
24
|
+
- Conditionnally hidden properties for tool calls (#70) [#70 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/70)
|
25
|
+
- Metadata to tool call results (#69) [#69 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/69)
|
26
|
+
- Link to official Discord Server in README.md
|
27
|
+
|
8
28
|
## [1.3.2] - 2025-05-08
|
9
29
|
### Changed
|
10
30
|
- Logs are now less verbose by default [#64 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/64)
|
data/README.md
CHANGED
@@ -10,6 +10,7 @@
|
|
10
10
|
<a href="https://github.com/yjacquin/fast-mcp/workflows/CI/badge.svg"><img src="https://github.com/yjacquin/fast-mcp/workflows/CI/badge.svg" alt="CI Status" /></a>
|
11
11
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
|
12
12
|
<a href="code_of_conduct.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg" alt="Contributor Covenant" /></a>
|
13
|
+
<a href="https://discord.gg/9HHfAtY3HF"><img src = "https://dcbadge.limes.pink/api/server/https://discord.gg/9HHfAtY3HF?style=flat" alt="Discord invite link" /></a>
|
13
14
|
</p>
|
14
15
|
|
15
16
|
## 🌟 Interface your Servers with LLMs in minutes !
|
@@ -31,6 +32,7 @@ Fast MCP solves all these problems by providing a clean, Ruby-focused implementa
|
|
31
32
|
- 🧩 **Framework Integration** - Works seamlessly with Rails, Sinatra or any Rack app.
|
32
33
|
- 🔒 **Authentication Support** - Secure your AI-powered endpoints with ease
|
33
34
|
- 🚀 **Real-time Updates** - Subscribe to changes for interactive applications
|
35
|
+
- 🎯 **Dynamic Filtering** - Control tool/resource access based on request context (permissions, API versions, etc.)
|
34
36
|
|
35
37
|
|
36
38
|
## 💎 What Makes FastMCP Great
|
@@ -64,7 +66,7 @@ server.register_tool(CreateUserTool)
|
|
64
66
|
|
65
67
|
# Share data resources with AI models by inheriting from FastMcp::Resource
|
66
68
|
class PopularUsers < FastMcp::Resource
|
67
|
-
uri "
|
69
|
+
uri "myapp:///users/popular"
|
68
70
|
resource_name "Popular Users"
|
69
71
|
mime_type "application/json"
|
70
72
|
|
@@ -73,14 +75,59 @@ class PopularUsers < FastMcp::Resource
|
|
73
75
|
end
|
74
76
|
end
|
75
77
|
|
78
|
+
class User < FastMcp::Resource
|
79
|
+
uri "myapp:///users/{id}" # This is a resource template
|
80
|
+
resource_name "user"
|
81
|
+
mime_type "application/json"
|
82
|
+
|
83
|
+
def content
|
84
|
+
id = params[:id] # params are computed from the uri pattern
|
85
|
+
|
86
|
+
JSON.generate(User.find(id).as_json)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
76
90
|
# Register the resource with the server
|
77
|
-
server.
|
91
|
+
server.register_resources(PopularUsers, User)
|
78
92
|
|
79
93
|
# Accessing the resource through the server
|
80
94
|
server.read_resource(PopularUsers.uri)
|
81
95
|
|
82
96
|
# Notify the resource content has been updated to clients
|
83
|
-
server.notify_resource_updated(PopularUsers.
|
97
|
+
server.notify_resource_updated(PopularUsers.variabilized_uri)
|
98
|
+
|
99
|
+
# Notifiy the content of a resource from a template has been updated to clients
|
100
|
+
server.notify_resource_updated(User.variabilized_uri(id: 1))
|
101
|
+
```
|
102
|
+
|
103
|
+
### 🎯 Dynamic Tool Filtering
|
104
|
+
|
105
|
+
Control which tools and resources are available based on request context:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
# Tag your tools for easy filtering
|
109
|
+
class AdminTool < FastMcp::Tool
|
110
|
+
tags :admin, :dangerous
|
111
|
+
description "Perform admin operations"
|
112
|
+
|
113
|
+
def call
|
114
|
+
# Admin only functionality
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Filter tools based on user permissions
|
119
|
+
server.filter_tools do |request, tools|
|
120
|
+
user_role = request.params['role']
|
121
|
+
|
122
|
+
case user_role
|
123
|
+
when 'admin'
|
124
|
+
tools # Admins see all tools
|
125
|
+
when 'user'
|
126
|
+
tools.reject { |t| t.tags.include?(:admin) }
|
127
|
+
else
|
128
|
+
tools.select { |t| t.tags.include?(:public) }
|
129
|
+
end
|
130
|
+
end
|
84
131
|
```
|
85
132
|
|
86
133
|
### 🚂 Fast Ruby on Rails implementation
|
@@ -299,7 +346,7 @@ Please refer to [configuring_mcp_clients](docs/configuring_mcp_clients.md)
|
|
299
346
|
|---------|--------|
|
300
347
|
| ✅ **JSON-RPC 2.0** | Full implementation for communication |
|
301
348
|
| ✅ **Tool Definition & Calling** | Define and call tools with rich argument types |
|
302
|
-
| ✅ **Resource Management** | Create, read, update, and subscribe to resources |
|
349
|
+
| ✅ **Resource & Resource Templates Management** | Create, read, update, and subscribe to resources |
|
303
350
|
| ✅ **Transport Options** | STDIO, HTTP, and SSE for flexible integration |
|
304
351
|
| ✅ **Framework Integration** | Rails, Sinatra, Hanami, and any Rack-compatible framework |
|
305
352
|
| ✅ **Authentication** | Secure your AI endpoints with token authentication |
|
@@ -352,6 +399,7 @@ FastMcp.authenticated_rack_middleware(app,
|
|
352
399
|
- [📚 Resources](docs/resources.md)
|
353
400
|
- [🛠️ Tools](docs/tools.md)
|
354
401
|
- [🔒 Security](docs/security.md)
|
402
|
+
- [🎯 Dynamic Filtering](docs/filtering.md)
|
355
403
|
|
356
404
|
## 💻 Examples
|
357
405
|
|
data/lib/mcp/resource.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
require 'json'
|
4
4
|
require 'base64'
|
5
5
|
require 'mime/types'
|
6
|
-
require '
|
6
|
+
require 'addressable/template'
|
7
7
|
|
8
8
|
module FastMcp
|
9
9
|
# Resource class for MCP Resources feature
|
@@ -17,9 +17,62 @@ module FastMcp
|
|
17
17
|
# @return [String] The URI for this resource
|
18
18
|
def uri(value = nil)
|
19
19
|
@uri = value if value
|
20
|
+
|
20
21
|
@uri || (superclass.respond_to?(:uri) ? superclass.uri : nil)
|
21
22
|
end
|
22
23
|
|
24
|
+
# Variabilize the URI with the given params
|
25
|
+
# @param params [Hash] The parameters to variabilize the URI with
|
26
|
+
# @return [String] The variabilized URI
|
27
|
+
def variabilized_uri(params = {})
|
28
|
+
addressable_template.partial_expand(params).pattern
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get the Addressable::Template for this resource
|
32
|
+
# @return [Addressable::Template] The Addressable::Template for this resource
|
33
|
+
def addressable_template
|
34
|
+
@addressable_template ||= Addressable::Template.new(uri)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get the template variables for this resource
|
38
|
+
# @return [Array] The template variables for this resource
|
39
|
+
def template_variables
|
40
|
+
addressable_template.variables
|
41
|
+
end
|
42
|
+
|
43
|
+
# Check if this resource has a templated URI
|
44
|
+
# @return [Boolean] true if the URI contains template parameters
|
45
|
+
def templated?
|
46
|
+
template_variables.any?
|
47
|
+
end
|
48
|
+
|
49
|
+
# Check if this resource has a non-templated URI
|
50
|
+
# @return [Boolean] true if the URI does not contain template parameters
|
51
|
+
def non_templated?
|
52
|
+
!templated?
|
53
|
+
end
|
54
|
+
|
55
|
+
# Match the given URI against the resource's addressable template
|
56
|
+
# @param uri [String] The URI to match
|
57
|
+
# @return [Addressable::Template::MatchData, nil] The match data if the URI matches, nil otherwise
|
58
|
+
def match(uri)
|
59
|
+
addressable_template.match(uri)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Initialize a new instance from the given URI
|
63
|
+
# @param uri [String] The URI to initialize from
|
64
|
+
# @return [Resource] A new resource instance
|
65
|
+
def initialize_from_uri(uri)
|
66
|
+
new(params_from_uri(uri))
|
67
|
+
end
|
68
|
+
|
69
|
+
# Get the parameters from the given URI
|
70
|
+
# @param uri [String] The URI to get the parameters from
|
71
|
+
# @return [Hash] The parameters from the URI
|
72
|
+
def params_from_uri(uri)
|
73
|
+
match(uri).mapping.transform_keys(&:to_sym)
|
74
|
+
end
|
75
|
+
|
23
76
|
# Define name for this resource
|
24
77
|
# @param value [String, nil] The name for this resource
|
25
78
|
# @return [String] The name for this resource
|
@@ -28,6 +81,13 @@ module FastMcp
|
|
28
81
|
@name || (superclass.respond_to?(:resource_name) ? superclass.resource_name : nil)
|
29
82
|
end
|
30
83
|
|
84
|
+
alias original_name name
|
85
|
+
def name
|
86
|
+
return resource_name if resource_name
|
87
|
+
|
88
|
+
original_name
|
89
|
+
end
|
90
|
+
|
31
91
|
# Define description for this resource
|
32
92
|
# @param value [String, nil] The description for this resource
|
33
93
|
# @return [String] The description for this resource
|
@@ -47,12 +107,21 @@ module FastMcp
|
|
47
107
|
# Get the resource metadata (without content)
|
48
108
|
# @return [Hash] Resource metadata
|
49
109
|
def metadata
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
110
|
+
if templated?
|
111
|
+
{
|
112
|
+
uriTemplate: uri,
|
113
|
+
name: resource_name,
|
114
|
+
description: description,
|
115
|
+
mimeType: mime_type
|
116
|
+
}.compact
|
117
|
+
else
|
118
|
+
{
|
119
|
+
uri: uri,
|
120
|
+
name: resource_name,
|
121
|
+
description: description,
|
122
|
+
mimeType: mime_type
|
123
|
+
}.compact
|
124
|
+
end
|
56
125
|
end
|
57
126
|
|
58
127
|
# Load content from a file (class method)
|
@@ -87,7 +156,11 @@ module FastMcp
|
|
87
156
|
end
|
88
157
|
end
|
89
158
|
|
90
|
-
|
159
|
+
# Initialize with instance variables
|
160
|
+
# @param params [Hash] The parameters for this resource instance
|
161
|
+
def initialize(params = {})
|
162
|
+
@params = params
|
163
|
+
end
|
91
164
|
|
92
165
|
# URI of the resource - delegates to class method
|
93
166
|
# @return [String, nil] The URI for this resource
|
@@ -98,7 +171,7 @@ module FastMcp
|
|
98
171
|
# Name of the resource - delegates to class method
|
99
172
|
# @return [String, nil] The name for this resource
|
100
173
|
def name
|
101
|
-
self.class.
|
174
|
+
self.class.resource_name
|
102
175
|
end
|
103
176
|
|
104
177
|
# Description of the resource - delegates to class method
|
@@ -113,6 +186,10 @@ module FastMcp
|
|
113
186
|
self.class.mime_type
|
114
187
|
end
|
115
188
|
|
189
|
+
# Get parameters from the URI template
|
190
|
+
# @return [Hash] The parameters extracted from the URI
|
191
|
+
attr_reader :params
|
192
|
+
|
116
193
|
# Method to be overridden by subclasses to dynamically generate content
|
117
194
|
# @return [String, nil] Generated content for this resource
|
118
195
|
def content
|
@@ -129,25 +206,5 @@ module FastMcp
|
|
129
206
|
mime_type == 'application/xml' ||
|
130
207
|
mime_type == 'application/javascript')
|
131
208
|
end
|
132
|
-
|
133
|
-
# Get the resource contents
|
134
|
-
# @return [Hash] Resource contents
|
135
|
-
def contents
|
136
|
-
result = {
|
137
|
-
uri: uri,
|
138
|
-
mimeType: mime_type
|
139
|
-
}
|
140
|
-
|
141
|
-
content_value = content
|
142
|
-
if content_value
|
143
|
-
if binary?
|
144
|
-
result[:blob] = Base64.strict_encode64(content_value)
|
145
|
-
else
|
146
|
-
result[:text] = content_value
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
result
|
151
|
-
end
|
152
209
|
end
|
153
210
|
end
|
data/lib/mcp/server.rb
CHANGED
@@ -8,9 +8,12 @@ require_relative 'transports/stdio_transport'
|
|
8
8
|
require_relative 'transports/rack_transport'
|
9
9
|
require_relative 'transports/authenticated_rack_transport'
|
10
10
|
require_relative 'logger'
|
11
|
+
require_relative 'server_filtering'
|
11
12
|
|
12
13
|
module FastMcp
|
13
14
|
class Server
|
15
|
+
include ServerFiltering
|
16
|
+
|
14
17
|
attr_reader :name, :version, :tools, :resources, :capabilities
|
15
18
|
|
16
19
|
DEFAULT_CAPABILITIES = {
|
@@ -27,14 +30,15 @@ module FastMcp
|
|
27
30
|
@name = name
|
28
31
|
@version = version
|
29
32
|
@tools = {}
|
30
|
-
@resources =
|
33
|
+
@resources = []
|
31
34
|
@resource_subscriptions = {}
|
32
35
|
@logger = logger
|
33
|
-
@logger.level = Logger::INFO
|
34
36
|
@request_id = 0
|
35
37
|
@transport_klass = nil
|
36
38
|
@transport = nil
|
37
39
|
@capabilities = DEFAULT_CAPABILITIES.dup
|
40
|
+
@tool_filters = []
|
41
|
+
@resource_filters = []
|
38
42
|
|
39
43
|
# Merge with provided capabilities
|
40
44
|
@capabilities.merge!(capabilities) if capabilities.is_a?(Hash)
|
@@ -66,8 +70,9 @@ module FastMcp
|
|
66
70
|
|
67
71
|
# Register a resource with the server
|
68
72
|
def register_resource(resource)
|
69
|
-
@resources
|
70
|
-
|
73
|
+
@resources << resource
|
74
|
+
|
75
|
+
@logger.debug("Registered resource: #{resource.resource_name} (#{resource.uri})")
|
71
76
|
resource.server = self
|
72
77
|
# Notify subscribers about the list change
|
73
78
|
notify_resource_list_changed if @transport
|
@@ -77,8 +82,10 @@ module FastMcp
|
|
77
82
|
|
78
83
|
# Remove a resource from the server
|
79
84
|
def remove_resource(uri)
|
80
|
-
|
81
|
-
|
85
|
+
resource = @resources.find { |r| r.uri == uri }
|
86
|
+
|
87
|
+
if resource
|
88
|
+
@resources.delete(resource)
|
82
89
|
@logger.debug("Removed resource: #{resource.name} (#{uri})")
|
83
90
|
|
84
91
|
# Notify subscribers about the list change
|
@@ -95,7 +102,7 @@ module FastMcp
|
|
95
102
|
@logger.transport = :stdio
|
96
103
|
@logger.info("Starting MCP server: #{@name} v#{@version}")
|
97
104
|
@logger.info("Available tools: #{@tools.keys.join(', ')}")
|
98
|
-
@logger.info("Available resources: #{@resources.
|
105
|
+
@logger.info("Available resources: #{@resources.map(&:resource_name).join(', ')}")
|
99
106
|
|
100
107
|
# Use STDIO transport by default
|
101
108
|
@transport_klass = FastMcp::Transports::StdioTransport
|
@@ -107,7 +114,7 @@ module FastMcp
|
|
107
114
|
def start_rack(app, options = {})
|
108
115
|
@logger.info("Starting MCP server as Rack middleware: #{@name} v#{@version}")
|
109
116
|
@logger.info("Available tools: #{@tools.keys.join(', ')}")
|
110
|
-
@logger.info("Available resources: #{@resources.
|
117
|
+
@logger.info("Available resources: #{@resources.map(&:resource_name).join(', ')}")
|
111
118
|
|
112
119
|
# Use Rack transport
|
113
120
|
transport_klass = FastMcp::Transports::RackTransport
|
@@ -121,7 +128,7 @@ module FastMcp
|
|
121
128
|
def start_authenticated_rack(app, options = {})
|
122
129
|
@logger.info("Starting MCP server as Authenticated Rack middleware: #{@name} v#{@version}")
|
123
130
|
@logger.info("Available tools: #{@tools.keys.join(', ')}")
|
124
|
-
@logger.info("Available resources: #{@resources.
|
131
|
+
@logger.info("Available resources: #{@resources.map(&:resource_name).join(', ')}")
|
125
132
|
|
126
133
|
# Use Rack transport
|
127
134
|
transport_klass = FastMcp::Transports::AuthenticatedRackTransport
|
@@ -132,8 +139,15 @@ module FastMcp
|
|
132
139
|
@transport
|
133
140
|
end
|
134
141
|
|
142
|
+
# Handle a JSON-RPC request and return the response as a JSON string
|
143
|
+
def handle_json_request(request, headers: {})
|
144
|
+
request_str = request.is_a?(String) ? request : JSON.generate(request)
|
145
|
+
|
146
|
+
handle_request(request_str, headers: headers)
|
147
|
+
end
|
148
|
+
|
135
149
|
# Handle incoming JSON-RPC request
|
136
|
-
def handle_request(json_str) # rubocop:disable Metrics/MethodLength
|
150
|
+
def handle_request(json_str, headers: {}) # rubocop:disable Metrics/MethodLength
|
137
151
|
begin
|
138
152
|
request = JSON.parse(json_str)
|
139
153
|
rescue JSON::ParserError, TypeError
|
@@ -142,15 +156,13 @@ module FastMcp
|
|
142
156
|
|
143
157
|
@logger.debug("Received request: #{request.inspect}")
|
144
158
|
|
145
|
-
# Check if it's a valid JSON-RPC 2.0 request
|
146
|
-
unless request['jsonrpc'] == '2.0' && request['method']
|
147
|
-
return send_error(-32_600, 'Invalid Request', request['id'])
|
148
|
-
end
|
149
|
-
|
150
159
|
method = request['method']
|
151
160
|
params = request['params'] || {}
|
152
161
|
id = request['id']
|
153
162
|
|
163
|
+
# Check if it's a valid JSON-RPC 2.0 request
|
164
|
+
return send_error(-32_600, 'Invalid Request', id) unless request['jsonrpc'] == '2.0'
|
165
|
+
|
154
166
|
case method
|
155
167
|
when 'ping'
|
156
168
|
send_result({}, id)
|
@@ -161,39 +173,26 @@ module FastMcp
|
|
161
173
|
when 'tools/list'
|
162
174
|
handle_tools_list(id)
|
163
175
|
when 'tools/call'
|
164
|
-
handle_tools_call(params, id)
|
176
|
+
handle_tools_call(params, headers, id)
|
165
177
|
when 'resources/list'
|
166
178
|
handle_resources_list(id)
|
179
|
+
when 'resources/templates/list'
|
180
|
+
handle_resources_templates_list(id)
|
167
181
|
when 'resources/read'
|
168
182
|
handle_resources_read(params, id)
|
169
183
|
when 'resources/subscribe'
|
170
184
|
handle_resources_subscribe(params, id)
|
171
185
|
when 'resources/unsubscribe'
|
172
186
|
handle_resources_unsubscribe(params, id)
|
187
|
+
when nil
|
188
|
+
# This is a notification response, we don't need to handle it
|
189
|
+
nil
|
173
190
|
else
|
174
191
|
send_error(-32_601, "Method not found: #{method}", id)
|
175
192
|
end
|
176
193
|
rescue StandardError => e
|
177
194
|
@logger.error("Error handling request: #{e.message}, #{e.backtrace.join("\n")}")
|
178
|
-
send_error(-32_600, "Internal error: #{e.message}", id)
|
179
|
-
end
|
180
|
-
|
181
|
-
# Handle a JSON-RPC request and return the response as a JSON string
|
182
|
-
def handle_json_request(request)
|
183
|
-
# Process the request
|
184
|
-
if request.is_a?(String)
|
185
|
-
handle_request(request)
|
186
|
-
else
|
187
|
-
handle_request(JSON.generate(request))
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
# Read a resource directly
|
192
|
-
def read_resource(uri)
|
193
|
-
resource = @resources[uri]
|
194
|
-
raise "Resource not found: #{uri}" unless resource
|
195
|
-
|
196
|
-
resource
|
195
|
+
send_error(-32_600, "Internal error: #{e.message}, #{e.backtrace.join("\n")}", id)
|
197
196
|
end
|
198
197
|
|
199
198
|
# Notify subscribers about a resource update
|
@@ -215,6 +214,10 @@ module FastMcp
|
|
215
214
|
@transport.send_message(notification)
|
216
215
|
end
|
217
216
|
|
217
|
+
def read_resource(uri)
|
218
|
+
@resources.find { |r| r.match(uri) }
|
219
|
+
end
|
220
|
+
|
218
221
|
private
|
219
222
|
|
220
223
|
PROTOCOL_VERSION = '2024-11-05'
|
@@ -249,24 +252,34 @@ module FastMcp
|
|
249
252
|
|
250
253
|
return send_error(-32_602, 'Invalid params: missing resource URI', id) unless uri
|
251
254
|
|
252
|
-
resource
|
253
|
-
return send_error(-32_602, "Resource not found: #{uri}", id) unless resource
|
255
|
+
@logger.debug("Looking for resource with URI: #{uri}")
|
254
256
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
257
|
+
begin
|
258
|
+
resource = read_resource(uri)
|
259
|
+
return send_error(-32_602, "Resource not found: #{uri}", id) unless resource
|
260
|
+
|
261
|
+
@logger.debug("Found resource: #{resource.resource_name}, templated: #{resource.templated?}")
|
262
|
+
|
263
|
+
base_content = { uri: uri }
|
264
|
+
base_content[:mimeType] = resource.mime_type if resource.mime_type
|
265
|
+
resource_instance = resource.initialize_from_uri(uri)
|
266
|
+
@logger.debug("Resource instance params: #{resource_instance.params.inspect}")
|
267
|
+
|
268
|
+
result = if resource_instance.binary?
|
269
|
+
{
|
270
|
+
contents: [base_content.merge(blob: Base64.strict_encode64(resource_instance.content))]
|
271
|
+
}
|
272
|
+
else
|
273
|
+
{
|
274
|
+
contents: [base_content.merge(text: resource_instance.content)]
|
275
|
+
}
|
276
|
+
end
|
277
|
+
|
278
|
+
# # rescue StandardError => e
|
279
|
+
# @logger.error("Error reading resource: #{e.message}")
|
280
|
+
# @logger.error(e.backtrace.join("\n"))
|
281
|
+
send_result(result, id)
|
282
|
+
end
|
270
283
|
end
|
271
284
|
|
272
285
|
def handle_initialized_notification
|
@@ -292,7 +305,7 @@ module FastMcp
|
|
292
305
|
end
|
293
306
|
|
294
307
|
# Handle tools/call request
|
295
|
-
def handle_tools_call(params, id)
|
308
|
+
def handle_tools_call(params, headers, id)
|
296
309
|
tool_name = params['name']
|
297
310
|
arguments = params['arguments'] || {}
|
298
311
|
|
@@ -304,10 +317,16 @@ module FastMcp
|
|
304
317
|
begin
|
305
318
|
# Convert string keys to symbols for Ruby
|
306
319
|
symbolized_args = symbolize_keys(arguments)
|
307
|
-
|
320
|
+
|
321
|
+
tool_instance = tool.new(headers: headers)
|
322
|
+
authorized = tool_instance.authorized?(**symbolized_args)
|
323
|
+
|
324
|
+
return send_error(-32_602, 'Unauthorized', id) unless authorized
|
325
|
+
|
326
|
+
result, metadata = tool_instance.call_with_schema_validation!(**symbolized_args)
|
308
327
|
|
309
328
|
# Format and send the result
|
310
|
-
send_formatted_result(result, id)
|
329
|
+
send_formatted_result(result, id, metadata)
|
311
330
|
rescue FastMcp::Tool::InvalidArgumentsError => e
|
312
331
|
@logger.error("Invalid arguments for tool #{tool_name}: #{e.message}")
|
313
332
|
send_error_result(e.message, id)
|
@@ -318,18 +337,18 @@ module FastMcp
|
|
318
337
|
end
|
319
338
|
|
320
339
|
# Format and send successful result
|
321
|
-
def send_formatted_result(result, id)
|
340
|
+
def send_formatted_result(result, id, metadata)
|
322
341
|
# Check if the result is already in the expected format
|
323
342
|
if result.is_a?(Hash) && result.key?(:content)
|
324
|
-
|
325
|
-
send_result(result, id)
|
343
|
+
send_result(result, id, metadata: metadata)
|
326
344
|
else
|
327
345
|
# Format the result according to the MCP specification
|
328
346
|
formatted_result = {
|
329
347
|
content: [{ type: 'text', text: result.to_s }],
|
330
348
|
isError: false
|
331
349
|
}
|
332
|
-
|
350
|
+
|
351
|
+
send_result(formatted_result, id, metadata: metadata)
|
333
352
|
end
|
334
353
|
end
|
335
354
|
|
@@ -340,16 +359,25 @@ module FastMcp
|
|
340
359
|
content: [{ type: 'text', text: "Error: #{message}" }],
|
341
360
|
isError: true
|
342
361
|
}
|
362
|
+
|
343
363
|
send_result(error_result, id)
|
344
364
|
end
|
345
365
|
|
346
366
|
# Handle resources/list request
|
347
367
|
def handle_resources_list(id)
|
348
|
-
resources_list = @resources.
|
368
|
+
resources_list = @resources.select(&:non_templated?).map(&:metadata)
|
349
369
|
|
350
370
|
send_result({ resources: resources_list }, id)
|
351
371
|
end
|
352
372
|
|
373
|
+
# Handle resources/templates/list request
|
374
|
+
def handle_resources_templates_list(id)
|
375
|
+
# Collect templated resources
|
376
|
+
templated_resources_list = @resources.select(&:templated?).map(&:metadata)
|
377
|
+
|
378
|
+
send_result({ resourceTemplates: templated_resources_list }, id)
|
379
|
+
end
|
380
|
+
|
353
381
|
# Handle resources/subscribe request
|
354
382
|
def handle_resources_subscribe(params, id)
|
355
383
|
return unless @client_initialized
|
@@ -361,11 +389,8 @@ module FastMcp
|
|
361
389
|
return
|
362
390
|
end
|
363
391
|
|
364
|
-
resource = @resources
|
365
|
-
unless resource
|
366
|
-
send_error(-32_602, "Resource not found: #{uri}", id)
|
367
|
-
return
|
368
|
-
end
|
392
|
+
resource = @resources.find { |r| r.match(uri) }
|
393
|
+
return send_error(-32_602, "Resource not found: #{uri}", id) unless resource
|
369
394
|
|
370
395
|
# Add to subscriptions
|
371
396
|
@resource_subscriptions[uri] ||= []
|
@@ -408,7 +433,9 @@ module FastMcp
|
|
408
433
|
end
|
409
434
|
|
410
435
|
# Send a JSON-RPC result response
|
411
|
-
def send_result(result, id)
|
436
|
+
def send_result(result, id, metadata: {})
|
437
|
+
result[:_meta] = metadata if metadata.is_a?(Hash) && !metadata.empty?
|
438
|
+
|
412
439
|
response = {
|
413
440
|
jsonrpc: '2.0',
|
414
441
|
id: id,
|
@@ -429,6 +456,7 @@ module FastMcp
|
|
429
456
|
},
|
430
457
|
id: id
|
431
458
|
}
|
459
|
+
|
432
460
|
send_response(response)
|
433
461
|
end
|
434
462
|
|