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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 247f36763da6fd88d9c9e454d5d63eaeb351d08a72de8c2e5dc65919d9e2a9f1
4
- data.tar.gz: 846d3427bb33357cfdc6c5387d153d393449792455beab413e36eeb8d0f1bf1d
3
+ metadata.gz: 1a99f230c0ff73568287d0812e53c266dffca1bd46de3c0308000d5658b5b9a4
4
+ data.tar.gz: 38fb4275c6cf123691ed02145687c70f5251be28e784f71b42a00c85b5fbee3d
5
5
  SHA512:
6
- metadata.gz: 8917675ff31b47cd67ddc5171a0c1e853cca9ba8a5853a7a9314ab5b794c9868c7c48b2db17dbd2edb580e81e9b12de8a0318151239babc2a5971cc2d4b10d72
7
- data.tar.gz: 926a58cda786415804f88bb7d2cbabe91fe90b6ae84964be9452070f0ac4de7a29705eedd69af79c641c4e4086865b5dda3438108f1cb068457b1b9c2930472e
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 "file://popular_users.json"
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.register_resource(PopularUsers)
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.uri)
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 'singleton'
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
- uri: uri,
52
- name: resource_name,
53
- description: description,
54
- mimeType: mime_type
55
- }.compact
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
- include Singleton
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.name
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[resource.uri] = resource
70
- @logger.debug("Registered resource: #{resource.name} (#{resource.uri})")
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
- if @resources.key?(uri)
81
- resource = @resources.delete(uri)
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.keys.join(', ')}")
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.keys.join(', ')}")
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.keys.join(', ')}")
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 = @resources[uri]
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
- base_content = { uri: resource.uri }
256
- base_content[:mimeType] = resource.mime_type if resource.mime_type
257
- resource_instance = resource.instance
258
- # Format the response according to the MCP specification
259
- result = if resource_instance.binary?
260
- {
261
- contents: [base_content.merge(blob: Base64.strict_encode64(resource_instance.content))]
262
- }
263
- else
264
- {
265
- contents: [base_content.merge(text: resource_instance.content)]
266
- }
267
- end
268
-
269
- send_result(result, id)
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
- result = tool.new.call_with_schema_validation!(**symbolized_args)
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
- # Result is already in the correct format
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
- send_result(formatted_result, id)
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.values.map(&:metadata)
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[uri]
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