Class: ActionMCP::ResourceTemplate

Inherits:
Object
  • Object
show all
Includes:
CurrentHelpers, ResourceCallbacks, UriAmbiguityChecker, ActiveModel::Model, ActiveModel::Validations
Defined in:
lib/action_mcp/resource_template.rb

Direct Known Subclasses

ApplicationMCPResTemplate

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attributes = {}) ⇒ ResourceTemplate

Initialize with attribute values



438
439
440
441
442
# File 'lib/action_mcp/resource_template.rb', line 438

def initialize(attributes = {})
  super(attributes)
  @execution_context = {}
  validate!
end

Class Attribute Details

._metaObject (readonly)

Returns the value of attribute _meta.



24
25
26
# File 'lib/action_mcp/resource_template.rb', line 24

def _meta
  @_meta
end

.description(value = nil) ⇒ Object (readonly)

Returns the value of attribute description.



24
25
26
# File 'lib/action_mcp/resource_template.rb', line 24

def description
  @description
end

.mime_type(value = nil) ⇒ Object (readonly)

Returns the value of attribute mime_type.



24
25
26
# File 'lib/action_mcp/resource_template.rb', line 24

def mime_type
  @mime_type
end

.parametersObject (readonly)

Returns the value of attribute parameters.



24
25
26
# File 'lib/action_mcp/resource_template.rb', line 24

def parameters
  @parameters
end

.registered_templatesObject (readonly)

Returns the value of attribute registered_templates.



24
25
26
# File 'lib/action_mcp/resource_template.rb', line 24

def registered_templates
  @registered_templates
end

.template_name(value = nil) ⇒ Object (readonly)

Returns the value of attribute template_name.



24
25
26
# File 'lib/action_mcp/resource_template.rb', line 24

def template_name
  @template_name
end

.ui_metaObject (readonly)

Returns the value of attribute ui_meta.



24
25
26
# File 'lib/action_mcp/resource_template.rb', line 24

def ui_meta
  @ui_meta
end

.uri_template(value = nil) ⇒ Object (readonly)

Returns the value of attribute uri_template.



24
25
26
# File 'lib/action_mcp/resource_template.rb', line 24

def uri_template
  @uri_template
end

Instance Attribute Details

#execution_contextObject (readonly)

Returns the value of attribute execution_context.



18
19
20
# File 'lib/action_mcp/resource_template.rb', line 18

def execution_context
  @execution_context
end

#resolved_uriObject (readonly)

Returns the value of attribute resolved_uri.



18
19
20
# File 'lib/action_mcp/resource_template.rb', line 18

def resolved_uri
  @resolved_uri
end

Class Method Details

.abstract!Object



31
32
33
34
35
36
37
# File 'lib/action_mcp/resource_template.rb', line 31

def abstract!
  @abstract = true
  # Unregister from the appropriate registry if already registered
  return unless ActionMCP::ResourceTemplatesRegistry.items.values.include?(self)

  ActionMCP::ResourceTemplatesRegistry.unregister(self)
end

.abstract?Boolean

Returns:

  • (Boolean)


27
28
29
# File 'lib/action_mcp/resource_template.rb', line 27

def abstract?
  @abstract ||= false
end

.addressable_template_cacheObject



56
57
58
# File 'lib/action_mcp/resource_template.rb', line 56

def addressable_template_cache
  @addressable_template_cache ||= {}
end

.build_resource(uri:, name:, title: nil, description: nil, mime_type: nil, size: nil, annotations: nil, meta: nil) ⇒ ActionMCP::Resource

Factory helper that fills in template-level defaults.

Parameters:

  • uri (String)

    The concrete resource URI

  • name (String)

    Display name

  • title (String, nil) (defaults to: nil)

    Human-readable title

  • description (String, nil) (defaults to: nil)

    Falls back to template description

  • mime_type (String, nil) (defaults to: nil)

    Falls back to template mime_type

  • size (Integer, nil) (defaults to: nil)

    Size in bytes

  • annotations (Hash, nil) (defaults to: nil)

    Optional annotations

  • meta (Hash, #to_hash, #to_h, nil) (defaults to: nil)

    Optional extension metadata passed through to the Resource (emitted as _meta)

Returns:



289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/action_mcp/resource_template.rb', line 289

def build_resource(uri:, name:, title: nil, description: nil, mime_type: nil, size: nil, annotations: nil, meta: nil)
  ActionMCP::Resource.new(
    uri: uri,
    name: name,
    title: title,
    description: description || @description,
    mime_type: mime_type || @mime_type,
    size: size,
    annotations: annotations,
    meta: meta_with_ui(meta)
  )
end

.capability_nameObject



256
257
258
259
260
# File 'lib/action_mcp/resource_template.rb', line 256

def capability_name
  return "" if name.nil?

  @capability_name ||= name.demodulize.underscore.sub(/_template$/, "")
end

.inherited(subclass) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
# File 'lib/action_mcp/resource_template.rb', line 39

def inherited(subclass)
  super
  subclass.instance_variable_set(:@abstract, false)
  # Create a copy of validation requirements for subclasses
  subclass.instance_variable_set(:@required_parameters, [])

  # Run the ActiveSupport load hook when a resource template is defined
  subclass.class_eval do
    ActiveSupport.run_load_hooks(:action_mcp_resource_template, subclass)
  end
end

.list(session: nil) ⇒ Array<ActionMCP::Resource>

Override in subclasses to enumerate concrete resources. Returns ArrayActionMCP::Resource.

Parameters:

  • session (Object, nil) (defaults to: nil)

    The current MCP session

Returns:



269
270
271
# File 'lib/action_mcp/resource_template.rb', line 269

def list(session: nil)
  []
end

.lists_resources?Boolean

Returns true if this template subclass overrides list.

Returns:

  • (Boolean)


274
275
276
# File 'lib/action_mcp/resource_template.rb', line 274

def lists_resources?
  method(:list).owner != ActionMCP::ResourceTemplate.singleton_class
end

.meta(data = nil) ⇒ Object

Sets or retrieves the _meta field



104
105
106
107
108
109
110
111
112
113
# File 'lib/action_mcp/resource_template.rb', line 104

def meta(data = nil)
  if data
    raise ArgumentError, "_meta must be a hash" unless data.is_a?(Hash)

    @_meta ||= {}
    @_meta = @_meta.merge(data)
  else
    @_meta || {}
  end
end

.meta_with_ui(meta = nil) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
# File 'lib/action_mcp/resource_template.rb', line 130

def meta_with_ui(meta = nil)
  supplied_meta = coerce_meta(meta)
  ui_meta = @ui_meta&.any? ? { ui: @ui_meta } : {}
  combined = ui_meta.deep_merge(supplied_meta)

  return nil if combined.empty?

  ui_present, combined_ui = (combined, :ui)
  validate_ui_metadata!(combined_ui) if ui_present
  combined
end

.parameter(name, description:, required: false, **options) ⇒ Object Also known as: attribute



60
61
62
63
64
65
66
67
68
69
# File 'lib/action_mcp/resource_template.rb', line 60

def parameter(name, description:, required: false, **options)
  @parameters ||= {}
  @parameters[name] = { description: description, required: required, **options }

  # Define attribute accessor if not already defined
  attr_accessor name unless method_defined?(name) && method_defined?("#{name}=")

  # Track required parameters for validation
  required_parameters << name if required
end

.process(uri_string) ⇒ Object

Process a URI string to create a template instance



319
320
321
322
323
324
325
326
327
328
329
# File 'lib/action_mcp/resource_template.rb', line 319

def process(uri_string)
  return nil unless @uri_template

  # Extract parameters from URI using pattern matching
  params = extract_params_from_uri(uri_string)
  return new if params.nil? # Return invalid template for bad URI

  # Preserve the concrete URI requested by the client. The template URI
  # may contain placeholders and must not leak into resource contents.
  new(params).tap { |record| record.instance_variable_set(:@resolved_uri, uri_string) }
end

.readable_uri?(uri) ⇒ Boolean

Check if a concrete URI is readable by this template. Returns false if the URI doesn't match the template pattern.

Parameters:

  • uri (String)

    A concrete URI to check

Returns:

  • (Boolean)


307
308
309
310
311
312
313
314
315
316
# File 'lib/action_mcp/resource_template.rb', line 307

def readable_uri?(uri)
  return false unless @uri_template

  params = extract_params_from_uri(uri)
  return false if params.nil?

  new(params).valid?
rescue StandardError
  false
end

.required_parametersObject

Track required parameters for validation



52
53
54
# File 'lib/action_mcp/resource_template.rb', line 52

def required_parameters
  @required_parameters ||= []
end

.to_hObject



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/action_mcp/resource_template.rb', line 240

def to_h
  name_value = defined?(@template_name) ? @template_name : name.demodulize.underscore.gsub(/_template$/, "")

  result = {
    uriTemplate: @uri_template,
    name: name_value,
    description: @description,
    mimeType: @mime_type
  }.compact

  meta = meta_with_ui(@_meta)
  result[:_meta] = meta if meta&.any?

  result
end

.ui(**data) ⇒ Object

Declares MCP Apps UI metadata for this resource template. Stored verbatim (camelCase keys per the ext-apps spec). Used both for the resources/list entry's _meta.ui and the default content _meta.ui produced by render_ui.

Examples:

ui csp: { connectDomains: %w[api.openweathermap.org] }, prefersBorder: true

Raises:

  • (ArgumentError)


122
123
124
125
126
127
128
# File 'lib/action_mcp/resource_template.rb', line 122

def ui(**data)
  raise ArgumentError, "ui metadata must not be empty" if data.empty?

  validate_ui_metadata!(data)
  @ui_meta ||= {}
  @ui_meta = @ui_meta.deep_merge(data)
end

Instance Method Details

#callObject



465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
# File 'lib/action_mcp/resource_template.rb', line 465

def call
  @response = ResourceResponse.new

  # Validate parameters first
  unless valid?
    missing_params = errors.full_messages
    @response.mark_as_parameter_validation_failed!(missing_params, "template://#{self.class.name}")
    return @response
  end

  begin
    run_callbacks :resolve do
      result = resolve
      if result.nil?
        @response.mark_as_not_found!("template://#{self.class.name}")
      else
        @response.add_content(result)
      end
      @response
    end
  rescue StandardError => e
    @response.mark_as_resolution_failed!("template://#{self.class.name}", e.message)
  end

  @response
end

#render_ui(text: nil, template: nil, layout: false, locals: {}, meta: nil) ⇒ Object

Build a Content::Resource for an MCP Apps UI view. Accepts either a raw :text string or a Rails :template path; the class-level ui macro supplies the content-level _meta.ui automatically.

Examples:

render_ui(template: "mcp/ui/weather_dashboard")

Raises:

  • (ArgumentError)


392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/action_mcp/resource_template.rb', line 392

def render_ui(text: nil, template: nil, layout: false, locals: {}, meta: nil)
  raise ArgumentError, "render_ui accepts either :text or :template, not both" if !text.nil? && !template.nil?

  resolved =
    if !text.nil?
      text
    elsif !template.nil?
      rendered = ActionMCP::MCPAppRenderer.render(template: template, layout: layout, locals: locals)
      if rendered.to_s.strip.empty?
        ActionMCP.logger.warn(
          "[ActionMCP] render_ui produced empty output for #{self.class.name} " \
          "(uri_template=#{self.class.uri_template.inspect}, template=#{template.inspect}). " \
          "Check the template path and host view configuration."
        )
      end
      rendered
    else
      raise ArgumentError, "render_ui requires :text or :template"
    end

  resource_uri = resolved_uri || self.class.uri_template
  unless resource_uri.is_a?(String) && ActionMCP::Apps::URI_SCHEME.match?(resource_uri)
    raise ArgumentError, "render_ui requires a concrete ui:// resource URI, got: #{resource_uri.inspect}"
  end
  if resolved_uri.nil? && resource_uri.match?(/\{[^}]+\}/)
    raise ArgumentError, "render_ui cannot emit a parameterized URI template without a concrete request URI"
  end

  resource_mime_type = self.class.mime_type || ActionMCP::Apps::MIME_TYPE
  unless resource_mime_type == ActionMCP::Apps::MIME_TYPE
    raise ArgumentError,
          "render_ui requires mime type #{ActionMCP::Apps::MIME_TYPE.inspect}, got: #{resource_mime_type.inspect}"
  end
  unless resolved.is_a?(String)
    raise ArgumentError, "render_ui content must be a string, got: #{resolved.class}"
  end

  ActionMCP::Content::Resource.new(
    resource_uri,
    resource_mime_type,
    text: resolved,
    meta: self.class.meta_with_ui(meta)
  )
end

#sessionObject



454
455
456
# File 'lib/action_mcp/resource_template.rb', line 454

def session
  execution_context[:session]
end

#validate!Object

Override validate! to not raise exceptions



445
446
447
# File 'lib/action_mcp/resource_template.rb', line 445

def validate!
  valid?
end

#with_context(context) ⇒ Object



449
450
451
452
# File 'lib/action_mcp/resource_template.rb', line 449

def with_context(context)
  @execution_context = context
  self
end