fast-mcp-annotations 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 +7 -0
- data/CHANGELOG.md +115 -0
- data/LICENSE +21 -0
- data/README.md +440 -0
- data/lib/fast_mcp.rb +189 -0
- 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 +42 -0
- data/lib/generators/fast_mcp/install/templates/sample_resource.rb +12 -0
- data/lib/generators/fast_mcp/install/templates/sample_tool.rb +23 -0
- data/lib/mcp/logger.rb +32 -0
- data/lib/mcp/railtie.rb +45 -0
- data/lib/mcp/resource.rb +210 -0
- data/lib/mcp/server.rb +499 -0
- data/lib/mcp/server_filtering.rb +80 -0
- data/lib/mcp/tool.rb +867 -0
- data/lib/mcp/transports/authenticated_rack_transport.rb +71 -0
- data/lib/mcp/transports/base_transport.rb +40 -0
- data/lib/mcp/transports/rack_transport.rb +627 -0
- data/lib/mcp/transports/stdio_transport.rb +62 -0
- data/lib/mcp/version.rb +5 -0
- metadata +151 -0
data/lib/mcp/server.rb
ADDED
@@ -0,0 +1,499 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'logger'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'base64'
|
7
|
+
require_relative 'transports/stdio_transport'
|
8
|
+
require_relative 'transports/rack_transport'
|
9
|
+
require_relative 'transports/authenticated_rack_transport'
|
10
|
+
require_relative 'logger'
|
11
|
+
require_relative 'server_filtering'
|
12
|
+
|
13
|
+
module FastMcp
|
14
|
+
class Server
|
15
|
+
include ServerFiltering
|
16
|
+
|
17
|
+
attr_reader :name, :version, :tools, :resources, :capabilities
|
18
|
+
|
19
|
+
DEFAULT_CAPABILITIES = {
|
20
|
+
resources: {
|
21
|
+
subscribe: true,
|
22
|
+
listChanged: true
|
23
|
+
},
|
24
|
+
tools: {
|
25
|
+
listChanged: true
|
26
|
+
}
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
def initialize(name:, version:, logger: FastMcp::Logger.new, capabilities: {})
|
30
|
+
@name = name
|
31
|
+
@version = version
|
32
|
+
@tools = {}
|
33
|
+
@resources = []
|
34
|
+
@resource_subscriptions = {}
|
35
|
+
@logger = logger
|
36
|
+
@request_id = 0
|
37
|
+
@transport_klass = nil
|
38
|
+
@transport = nil
|
39
|
+
@capabilities = DEFAULT_CAPABILITIES.dup
|
40
|
+
@tool_filters = []
|
41
|
+
@resource_filters = []
|
42
|
+
|
43
|
+
# Merge with provided capabilities
|
44
|
+
@capabilities.merge!(capabilities) if capabilities.is_a?(Hash)
|
45
|
+
end
|
46
|
+
attr_accessor :transport, :transport_klass, :logger
|
47
|
+
|
48
|
+
# Register multiple tools at once
|
49
|
+
# @param tools [Array<Tool>] Tools to register
|
50
|
+
def register_tools(*tools)
|
51
|
+
tools.each do |tool|
|
52
|
+
register_tool(tool)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Register a tool with the server
|
57
|
+
def register_tool(tool)
|
58
|
+
@tools[tool.tool_name] = tool
|
59
|
+
@logger.debug("Registered tool: #{tool.tool_name}")
|
60
|
+
tool.server = self
|
61
|
+
end
|
62
|
+
|
63
|
+
# Register multiple resources at once
|
64
|
+
# @param resources [Array<Resource>] Resources to register
|
65
|
+
def register_resources(*resources)
|
66
|
+
resources.each do |resource|
|
67
|
+
register_resource(resource)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Register a resource with the server
|
72
|
+
def register_resource(resource)
|
73
|
+
@resources << resource
|
74
|
+
|
75
|
+
@logger.debug("Registered resource: #{resource.resource_name} (#{resource.uri})")
|
76
|
+
resource.server = self
|
77
|
+
# Notify subscribers about the list change
|
78
|
+
notify_resource_list_changed if @transport
|
79
|
+
|
80
|
+
resource
|
81
|
+
end
|
82
|
+
|
83
|
+
# Remove a resource from the server
|
84
|
+
def remove_resource(uri)
|
85
|
+
resource = @resources.find { |r| r.uri == uri }
|
86
|
+
|
87
|
+
if resource
|
88
|
+
@resources.delete(resource)
|
89
|
+
@logger.debug("Removed resource: #{resource.name} (#{uri})")
|
90
|
+
|
91
|
+
# Notify subscribers about the list change
|
92
|
+
notify_resource_list_changed if @transport
|
93
|
+
|
94
|
+
true
|
95
|
+
else
|
96
|
+
false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Start the server using stdio transport
|
101
|
+
def start
|
102
|
+
@logger.transport = :stdio
|
103
|
+
@logger.info("Starting MCP server: #{@name} v#{@version}")
|
104
|
+
@logger.info("Available tools: #{@tools.keys.join(', ')}")
|
105
|
+
@logger.info("Available resources: #{@resources.map(&:resource_name).join(', ')}")
|
106
|
+
|
107
|
+
# Use STDIO transport by default
|
108
|
+
@transport_klass = FastMcp::Transports::StdioTransport
|
109
|
+
@transport = @transport_klass.new(self, logger: @logger)
|
110
|
+
@transport.start
|
111
|
+
end
|
112
|
+
|
113
|
+
# Start the server as a Rack middleware
|
114
|
+
def start_rack(app, options = {})
|
115
|
+
@logger.info("Starting MCP server as Rack middleware: #{@name} v#{@version}")
|
116
|
+
@logger.info("Available tools: #{@tools.keys.join(', ')}")
|
117
|
+
@logger.info("Available resources: #{@resources.map(&:resource_name).join(', ')}")
|
118
|
+
|
119
|
+
# Use Rack transport
|
120
|
+
transport_klass = FastMcp::Transports::RackTransport
|
121
|
+
@transport = transport_klass.new(app, self, options.merge(logger: @logger))
|
122
|
+
@transport.start
|
123
|
+
|
124
|
+
# Return the transport as middleware
|
125
|
+
@transport
|
126
|
+
end
|
127
|
+
|
128
|
+
def start_authenticated_rack(app, options = {})
|
129
|
+
@logger.info("Starting MCP server as Authenticated Rack middleware: #{@name} v#{@version}")
|
130
|
+
@logger.info("Available tools: #{@tools.keys.join(', ')}")
|
131
|
+
@logger.info("Available resources: #{@resources.map(&:resource_name).join(', ')}")
|
132
|
+
|
133
|
+
# Use Rack transport
|
134
|
+
transport_klass = FastMcp::Transports::AuthenticatedRackTransport
|
135
|
+
@transport = transport_klass.new(app, self, options.merge(logger: @logger))
|
136
|
+
@transport.start
|
137
|
+
|
138
|
+
# Return the transport as middleware
|
139
|
+
@transport
|
140
|
+
end
|
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
|
+
|
149
|
+
# Handle incoming JSON-RPC request
|
150
|
+
def handle_request(json_str, headers: {}) # rubocop:disable Metrics/MethodLength
|
151
|
+
begin
|
152
|
+
request = JSON.parse(json_str)
|
153
|
+
rescue JSON::ParserError, TypeError
|
154
|
+
return send_error(-32_600, 'Invalid Request', nil)
|
155
|
+
end
|
156
|
+
|
157
|
+
@logger.debug("Received request: #{request.inspect}")
|
158
|
+
|
159
|
+
method = request['method']
|
160
|
+
params = request['params'] || {}
|
161
|
+
id = request['id']
|
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
|
+
|
166
|
+
case method
|
167
|
+
when 'ping'
|
168
|
+
send_result({}, id)
|
169
|
+
when 'initialize'
|
170
|
+
handle_initialize(params, id)
|
171
|
+
when 'notifications/initialized'
|
172
|
+
handle_initialized_notification
|
173
|
+
when 'tools/list'
|
174
|
+
handle_tools_list(id)
|
175
|
+
when 'tools/call'
|
176
|
+
handle_tools_call(params, headers, id)
|
177
|
+
when 'resources/list'
|
178
|
+
handle_resources_list(id)
|
179
|
+
when 'resources/templates/list'
|
180
|
+
handle_resources_templates_list(id)
|
181
|
+
when 'resources/read'
|
182
|
+
handle_resources_read(params, id)
|
183
|
+
when 'resources/subscribe'
|
184
|
+
handle_resources_subscribe(params, id)
|
185
|
+
when 'resources/unsubscribe'
|
186
|
+
handle_resources_unsubscribe(params, id)
|
187
|
+
when nil
|
188
|
+
# This is a notification response, we don't need to handle it
|
189
|
+
nil
|
190
|
+
else
|
191
|
+
send_error(-32_601, "Method not found: #{method}", id)
|
192
|
+
end
|
193
|
+
rescue StandardError => e
|
194
|
+
@logger.error("Error handling request: #{e.message}, #{e.backtrace.join("\n")}")
|
195
|
+
send_error(-32_600, "Internal error: #{e.message}, #{e.backtrace.join("\n")}", id)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Notify subscribers about a resource update
|
199
|
+
def notify_resource_updated(uri)
|
200
|
+
@logger.warn("Notifying subscribers about resource update: #{uri}, #{@resource_subscriptions.inspect}")
|
201
|
+
return unless @client_initialized && @resource_subscriptions.key?(uri)
|
202
|
+
|
203
|
+
resource = @resources[uri]
|
204
|
+
notification = {
|
205
|
+
jsonrpc: '2.0',
|
206
|
+
method: 'notifications/resources/updated',
|
207
|
+
params: {
|
208
|
+
uri: uri,
|
209
|
+
name: resource.name,
|
210
|
+
mimeType: resource.mime_type
|
211
|
+
}
|
212
|
+
}
|
213
|
+
|
214
|
+
@transport.send_message(notification)
|
215
|
+
end
|
216
|
+
|
217
|
+
def read_resource(uri)
|
218
|
+
@resources.find { |r| r.match(uri) }
|
219
|
+
end
|
220
|
+
|
221
|
+
private
|
222
|
+
|
223
|
+
PROTOCOL_VERSION = '2024-11-05'
|
224
|
+
|
225
|
+
def handle_initialize(params, id)
|
226
|
+
# Store client capabilities for later use
|
227
|
+
@client_capabilities = params['capabilities'] || {}
|
228
|
+
client_info = params['clientInfo'] || {}
|
229
|
+
|
230
|
+
# Log client information
|
231
|
+
@logger.info("Client connected: #{client_info['name']} v#{client_info['version']}")
|
232
|
+
# @logger.debug("Client capabilities: #{client_capabilities.inspect}")
|
233
|
+
|
234
|
+
# Prepare server response
|
235
|
+
response = {
|
236
|
+
protocolVersion: PROTOCOL_VERSION, # For now, only version 2024-11-05 is supported.
|
237
|
+
capabilities: @capabilities,
|
238
|
+
serverInfo: {
|
239
|
+
name: @name,
|
240
|
+
version: @version
|
241
|
+
}
|
242
|
+
}
|
243
|
+
|
244
|
+
@logger.info("Server response: #{response.inspect}")
|
245
|
+
|
246
|
+
send_result(response, id)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Handle a resource read
|
250
|
+
def handle_resources_read(params, id)
|
251
|
+
uri = params['uri']
|
252
|
+
|
253
|
+
return send_error(-32_602, 'Invalid params: missing resource URI', id) unless uri
|
254
|
+
|
255
|
+
@logger.debug("Looking for resource with URI: #{uri}")
|
256
|
+
|
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
|
283
|
+
end
|
284
|
+
|
285
|
+
def handle_initialized_notification
|
286
|
+
# The client is now ready for normal operation
|
287
|
+
# No response needed for notifications
|
288
|
+
@client_initialized = true
|
289
|
+
@logger.info('Client initialized, beginning normal operation')
|
290
|
+
|
291
|
+
nil
|
292
|
+
end
|
293
|
+
|
294
|
+
# Handle tools/list request
|
295
|
+
def handle_tools_list(id)
|
296
|
+
tools_list = @tools.values.map do |tool|
|
297
|
+
tool_info = {
|
298
|
+
name: tool.tool_name,
|
299
|
+
description: tool.description || '',
|
300
|
+
inputSchema: tool.input_schema_to_json || { type: 'object', properties: {}, required: [] }
|
301
|
+
}
|
302
|
+
|
303
|
+
# Add annotations if they exist
|
304
|
+
annotations = tool.annotations
|
305
|
+
unless annotations.empty?
|
306
|
+
# Convert snake_case keys to camelCase for MCP protocol
|
307
|
+
camel_case_annotations = {}
|
308
|
+
annotations.each do |key, value|
|
309
|
+
camel_key = key.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }.to_sym
|
310
|
+
camel_case_annotations[camel_key] = value
|
311
|
+
end
|
312
|
+
tool_info[:annotations] = camel_case_annotations
|
313
|
+
end
|
314
|
+
|
315
|
+
tool_info
|
316
|
+
end
|
317
|
+
|
318
|
+
send_result({ tools: tools_list }, id)
|
319
|
+
end
|
320
|
+
|
321
|
+
# Handle tools/call request
|
322
|
+
def handle_tools_call(params, headers, id)
|
323
|
+
tool_name = params['name']
|
324
|
+
arguments = params['arguments'] || {}
|
325
|
+
|
326
|
+
return send_error(-32_602, 'Invalid params: missing tool name', id) unless tool_name
|
327
|
+
|
328
|
+
tool = @tools[tool_name]
|
329
|
+
return send_error(-32_602, "Tool not found: #{tool_name}", id) unless tool
|
330
|
+
|
331
|
+
begin
|
332
|
+
# Convert string keys to symbols for Ruby
|
333
|
+
symbolized_args = symbolize_keys(arguments)
|
334
|
+
|
335
|
+
tool_instance = tool.new(headers: headers)
|
336
|
+
authorized = tool_instance.authorized?(**symbolized_args)
|
337
|
+
|
338
|
+
return send_error(-32_602, 'Unauthorized', id) unless authorized
|
339
|
+
|
340
|
+
result, metadata = tool_instance.call_with_schema_validation!(**symbolized_args)
|
341
|
+
|
342
|
+
# Format and send the result
|
343
|
+
send_formatted_result(result, id, metadata)
|
344
|
+
rescue FastMcp::Tool::InvalidArgumentsError => e
|
345
|
+
@logger.error("Invalid arguments for tool #{tool_name}: #{e.message}")
|
346
|
+
send_error_result(e.message, id)
|
347
|
+
rescue StandardError => e
|
348
|
+
@logger.error("Error calling tool #{tool_name}: #{e.message}")
|
349
|
+
send_error_result("#{e.message}, #{e.backtrace.join("\n")}", id)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# Format and send successful result
|
354
|
+
def send_formatted_result(result, id, metadata)
|
355
|
+
# Check if the result is already in the expected format
|
356
|
+
if result.is_a?(Hash) && result.key?(:content)
|
357
|
+
send_result(result, id, metadata: metadata)
|
358
|
+
else
|
359
|
+
# Format the result according to the MCP specification
|
360
|
+
formatted_result = {
|
361
|
+
content: [{ type: 'text', text: result.to_s }],
|
362
|
+
isError: false
|
363
|
+
}
|
364
|
+
|
365
|
+
send_result(formatted_result, id, metadata: metadata)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Format and send error result
|
370
|
+
def send_error_result(message, id)
|
371
|
+
# Format error according to the MCP specification
|
372
|
+
error_result = {
|
373
|
+
content: [{ type: 'text', text: "Error: #{message}" }],
|
374
|
+
isError: true
|
375
|
+
}
|
376
|
+
|
377
|
+
send_result(error_result, id)
|
378
|
+
end
|
379
|
+
|
380
|
+
# Handle resources/list request
|
381
|
+
def handle_resources_list(id)
|
382
|
+
resources_list = @resources.select(&:non_templated?).map(&:metadata)
|
383
|
+
|
384
|
+
send_result({ resources: resources_list }, id)
|
385
|
+
end
|
386
|
+
|
387
|
+
# Handle resources/templates/list request
|
388
|
+
def handle_resources_templates_list(id)
|
389
|
+
# Collect templated resources
|
390
|
+
templated_resources_list = @resources.select(&:templated?).map(&:metadata)
|
391
|
+
|
392
|
+
send_result({ resourceTemplates: templated_resources_list }, id)
|
393
|
+
end
|
394
|
+
|
395
|
+
# Handle resources/subscribe request
|
396
|
+
def handle_resources_subscribe(params, id)
|
397
|
+
return unless @client_initialized
|
398
|
+
|
399
|
+
uri = params['uri']
|
400
|
+
|
401
|
+
unless uri
|
402
|
+
send_error(-32_602, 'Invalid params: missing resource URI', id)
|
403
|
+
return
|
404
|
+
end
|
405
|
+
|
406
|
+
resource = @resources.find { |r| r.match(uri) }
|
407
|
+
return send_error(-32_602, "Resource not found: #{uri}", id) unless resource
|
408
|
+
|
409
|
+
# Add to subscriptions
|
410
|
+
@resource_subscriptions[uri] ||= []
|
411
|
+
@resource_subscriptions[uri] << id
|
412
|
+
|
413
|
+
send_result({ subscribed: true }, id)
|
414
|
+
end
|
415
|
+
|
416
|
+
# Handle resources/unsubscribe request
|
417
|
+
def handle_resources_unsubscribe(params, id)
|
418
|
+
return unless @client_initialized
|
419
|
+
|
420
|
+
uri = params['uri']
|
421
|
+
|
422
|
+
unless uri
|
423
|
+
send_error(-32_602, 'Invalid params: missing resource URI', id)
|
424
|
+
return
|
425
|
+
end
|
426
|
+
|
427
|
+
# Remove from subscriptions
|
428
|
+
if @resource_subscriptions.key?(uri)
|
429
|
+
@resource_subscriptions[uri].delete(id)
|
430
|
+
@resource_subscriptions.delete(uri) if @resource_subscriptions[uri].empty?
|
431
|
+
end
|
432
|
+
|
433
|
+
send_result({ unsubscribed: true }, id)
|
434
|
+
end
|
435
|
+
|
436
|
+
# Notify clients about resource list changes
|
437
|
+
def notify_resource_list_changed
|
438
|
+
return unless @client_initialized
|
439
|
+
|
440
|
+
notification = {
|
441
|
+
jsonrpc: '2.0',
|
442
|
+
method: 'notifications/resources/listChanged',
|
443
|
+
params: {}
|
444
|
+
}
|
445
|
+
|
446
|
+
@transport.send_message(notification)
|
447
|
+
end
|
448
|
+
|
449
|
+
# Send a JSON-RPC result response
|
450
|
+
def send_result(result, id, metadata: {})
|
451
|
+
result[:_meta] = metadata if metadata.is_a?(Hash) && !metadata.empty?
|
452
|
+
|
453
|
+
response = {
|
454
|
+
jsonrpc: '2.0',
|
455
|
+
id: id,
|
456
|
+
result: result
|
457
|
+
}
|
458
|
+
|
459
|
+
@logger.info("Sending result: #{response.inspect}")
|
460
|
+
send_response(response)
|
461
|
+
end
|
462
|
+
|
463
|
+
# Send a JSON-RPC error response
|
464
|
+
def send_error(code, message, id = nil)
|
465
|
+
response = {
|
466
|
+
jsonrpc: '2.0',
|
467
|
+
error: {
|
468
|
+
code: code,
|
469
|
+
message: message
|
470
|
+
},
|
471
|
+
id: id
|
472
|
+
}
|
473
|
+
|
474
|
+
send_response(response)
|
475
|
+
end
|
476
|
+
|
477
|
+
# Send a JSON-RPC response
|
478
|
+
def send_response(response)
|
479
|
+
if @transport
|
480
|
+
@logger.debug("Sending response: #{response.inspect}")
|
481
|
+
@transport.send_message(response)
|
482
|
+
else
|
483
|
+
@logger.warn("No transport available to send response: #{response.inspect}")
|
484
|
+
@logger.warn("Transport: #{@transport.inspect}, transport_klass: #{@transport_klass.inspect}")
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
# Helper method to convert string keys to symbols
|
489
|
+
def symbolize_keys(hash)
|
490
|
+
return hash unless hash.is_a?(Hash)
|
491
|
+
|
492
|
+
hash.each_with_object({}) do |(key, value), result|
|
493
|
+
new_key = key.is_a?(String) ? key.to_sym : key
|
494
|
+
new_value = value.is_a?(Hash) ? symbolize_keys(value) : value
|
495
|
+
result[new_key] = new_value
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FastMcp
|
4
|
+
# Module for handling server filtering functionality
|
5
|
+
module ServerFiltering
|
6
|
+
# Add filter for tools
|
7
|
+
def filter_tools(&block)
|
8
|
+
@tool_filters << block if block_given?
|
9
|
+
end
|
10
|
+
|
11
|
+
# Add filter for resources
|
12
|
+
def filter_resources(&block)
|
13
|
+
@resource_filters << block if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
# Check if filters are configured
|
17
|
+
def contains_filters?
|
18
|
+
@tool_filters.any? || @resource_filters.any?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Create a filtered copy for a specific request
|
22
|
+
def create_filtered_copy(request)
|
23
|
+
filtered_server = self.class.new(
|
24
|
+
name: @name,
|
25
|
+
version: @version,
|
26
|
+
logger: @logger,
|
27
|
+
capabilities: @capabilities
|
28
|
+
)
|
29
|
+
|
30
|
+
# Copy transport settings
|
31
|
+
filtered_server.transport_klass = @transport_klass
|
32
|
+
|
33
|
+
# Apply filters and register items
|
34
|
+
register_filtered_tools(filtered_server, request)
|
35
|
+
register_filtered_resources(filtered_server, request)
|
36
|
+
|
37
|
+
filtered_server
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Apply tool filters and register filtered tools
|
43
|
+
def register_filtered_tools(filtered_server, request)
|
44
|
+
filtered_tools = apply_tool_filters(request)
|
45
|
+
|
46
|
+
# Register filtered tools
|
47
|
+
filtered_tools.each do |tool|
|
48
|
+
filtered_server.register_tool(tool)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Apply resource filters and register filtered resources
|
53
|
+
def register_filtered_resources(filtered_server, request)
|
54
|
+
filtered_resources = apply_resource_filters(request)
|
55
|
+
|
56
|
+
# Register filtered resources
|
57
|
+
filtered_resources.each do |resource|
|
58
|
+
filtered_server.register_resource(resource)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Apply all tool filters to the tools collection
|
63
|
+
def apply_tool_filters(request)
|
64
|
+
filtered_tools = @tools.values
|
65
|
+
@tool_filters.each do |filter|
|
66
|
+
filtered_tools = filter.call(request, filtered_tools)
|
67
|
+
end
|
68
|
+
filtered_tools
|
69
|
+
end
|
70
|
+
|
71
|
+
# Apply all resource filters to the resources collection
|
72
|
+
def apply_resource_filters(request)
|
73
|
+
filtered_resources = @resources
|
74
|
+
@resource_filters.each do |filter|
|
75
|
+
filtered_resources = filter.call(request, filtered_resources)
|
76
|
+
end
|
77
|
+
filtered_resources
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|