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



369
370
371
372
373
# File 'lib/action_mcp/resource_template.rb', line 369

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

Class Attribute Details

._metaObject (readonly)

Returns the value of attribute _meta.



22
23
24
# File 'lib/action_mcp/resource_template.rb', line 22

def _meta
  @_meta
end

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

Returns the value of attribute description.



22
23
24
# File 'lib/action_mcp/resource_template.rb', line 22

def description
  @description
end

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

Returns the value of attribute mime_type.



22
23
24
# File 'lib/action_mcp/resource_template.rb', line 22

def mime_type
  @mime_type
end

.parametersObject (readonly)

Returns the value of attribute parameters.



22
23
24
# File 'lib/action_mcp/resource_template.rb', line 22

def parameters
  @parameters
end

.registered_templatesObject (readonly)

Returns the value of attribute registered_templates.



22
23
24
# File 'lib/action_mcp/resource_template.rb', line 22

def registered_templates
  @registered_templates
end

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

Returns the value of attribute template_name.



22
23
24
# File 'lib/action_mcp/resource_template.rb', line 22

def template_name
  @template_name
end

.ui_metaObject (readonly)

Returns the value of attribute ui_meta.



22
23
24
# File 'lib/action_mcp/resource_template.rb', line 22

def ui_meta
  @ui_meta
end

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

Returns the value of attribute uri_template.



22
23
24
# File 'lib/action_mcp/resource_template.rb', line 22

def uri_template
  @uri_template
end

Instance Attribute Details

#execution_contextObject (readonly)

Returns the value of attribute execution_context.



16
17
18
# File 'lib/action_mcp/resource_template.rb', line 16

def execution_context
  @execution_context
end

Class Method Details

.abstract!Object



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

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)


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

def abstract?
  @abstract ||= false
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:



195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/action_mcp/resource_template.rb', line 195

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
  )
end

.capability_nameObject



162
163
164
165
166
# File 'lib/action_mcp/resource_template.rb', line 162

def capability_name
  return "" if name.nil?

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

.inherited(subclass) ⇒ Object



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

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 Array<ActionMCP::Resource>.

Parameters:

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

    The current MCP session

Returns:



175
176
177
# File 'lib/action_mcp/resource_template.rb', line 175

def list(session: nil)
  []
end

.lists_resources?Boolean

Returns true if this template subclass overrides list.

Returns:

  • (Boolean)


180
181
182
# File 'lib/action_mcp/resource_template.rb', line 180

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

.meta(data = nil) ⇒ Object

Sets or retrieves the _meta field



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

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

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



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

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



225
226
227
228
229
230
231
232
233
234
# File 'lib/action_mcp/resource_template.rb', line 225

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

  # Create new instance with the extracted parameters
  new(params)
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)


213
214
215
216
217
218
219
220
221
222
# File 'lib/action_mcp/resource_template.rb', line 213

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



50
51
52
# File 'lib/action_mcp/resource_template.rb', line 50

def required_parameters
  @required_parameters ||= []
end

.to_hObject



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/action_mcp/resource_template.rb', line 146

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

  # Add _meta if present
  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)


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

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

  validate_ui_csp_origins!(data[:csp])
  @ui_meta ||= {}
  @ui_meta = @ui_meta.deep_merge(data)
end

.uri_regex_cacheObject

Cache compiled regex patterns for URI matching to avoid recompilation



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

def uri_regex_cache
  @uri_regex_cache ||= {}
end

Instance Method Details

#callObject



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
# File 'lib/action_mcp/resource_template.rb', line 396

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: {}) ⇒ 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")


341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/action_mcp/resource_template.rb', line 341

def render_ui(text: nil, template: nil, layout: false, locals: {})
  resolved =
    if text
      text
    elsif template
      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

  ui = self.class.ui_meta
  ActionMCP::Content::Resource.new(
    self.class.uri_template,
    self.class.mime_type || ActionMCP::Apps::MIME_TYPE,
    text: resolved,
    meta: (ui&.any? ? { ui: ui } : nil)
  )
end

#sessionObject



385
386
387
# File 'lib/action_mcp/resource_template.rb', line 385

def session
  execution_context[:session]
end

#validate!Object

Override validate! to not raise exceptions



376
377
378
# File 'lib/action_mcp/resource_template.rb', line 376

def validate!
  valid?
end

#with_context(context) ⇒ Object



380
381
382
383
# File 'lib/action_mcp/resource_template.rb', line 380

def with_context(context)
  @execution_context = context
  self
end