Class: McpToolkit::Resource

Inherits:
Object
  • Object
show all
Defined in:
lib/mcp_toolkit/resource.rb

Overview

Descriptor for a single read-only resource exposed via the MCP server. Built via McpToolkit.registry.register; consumed by the List/Get executors and the resource_schema tool.

The scope_block is the account-rooting relation: it receives the resolved local scope root (typically an Account) and MUST return a relation already scoped so that every row belongs to that root (directly via a foreign key, or transitively through an owning record). This is the single tenancy chokepoint — every get/list query roots on it.

The serializer is INJECTABLE per resource: it may be a subclass of the gem's McpToolkit::Serializer::Base, or any class satisfying the serializer contract (serialize_one / serialize_collection) — e.g. an app's existing serializer.

Defined Under Namespace

Classes: NotConfigured

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name) ⇒ Resource

Returns a new instance of Resource.



21
22
23
24
25
26
27
28
29
# File 'lib/mcp_toolkit/resource.rb', line 21

def initialize(name)
  @name = name.to_s
  @model = nil
  @serializer = nil
  @scope_block = nil
  @description = nil
  @filterable = {}
  @required_permissions_scope = nil
end

Instance Attribute Details

#nameObject (readonly)

Returns the value of attribute name.



19
20
21
# File 'lib/mcp_toolkit/resource.rb', line 19

def name
  @name
end

Instance Method Details

#association_descriptorsObject

Association descriptors (the links shape) read off the serializer.



124
125
126
127
128
# File 'lib/mcp_toolkit/resource.rb', line 124

def association_descriptors
  return [] unless serializer.respond_to?(:declared_associations)

  serializer.declared_associations
end

#attribute_namesObject

Serialized attribute names (the response shape), read off the serializer's declared attributes. Requires a serializer that exposes declared_attributes (the gem's base does); resource_schema degrades gracefully otherwise.



117
118
119
120
121
# File 'lib/mcp_toolkit/resource.rb', line 117

def attribute_names
  return [] unless serializer.respond_to?(:declared_attributes)

  serializer.declared_attributes.map(&:to_sym)
end

#description(text = nil) ⇒ Object



46
47
48
49
# File 'lib/mcp_toolkit/resource.rb', line 46

def description(text = nil)
  @description = text if text
  @description
end

#effective_required_permissions_scope(default = nil) ⇒ Object

The scope actually enforced for this resource: its own declared scope if set, otherwise the registry-level default passed in. nil = no scope required.



65
66
67
# File 'lib/mcp_toolkit/resource.rb', line 65

def effective_required_permissions_scope(default = nil)
  @required_permissions_scope || default
end

#filterable(mapping = nil) ⇒ Object

Declares the per-attribute filters this resource accepts on the list tool. Each entry maps a REQUEST-FACING filter key to the backing DATABASE COLUMN the WHERE is applied to. The mapping is what lets the consumer-facing key differ from the storage column (e.g. exposing a synced foreign key under its public name):

filterable booking_id: :synced_booking_id

A declared key accepts both a bare equality value AND operator-based conditions ({ op:, value: } or an array of them, ANDed) — see McpToolkit::Filtering for the supported operators per column type.

Unmapped/unknown keys are rejected by the list executor, never silently dropped, so a typo surfaces as actionable feedback.



83
84
85
86
87
88
89
90
# File 'lib/mcp_toolkit/resource.rb', line 83

def filterable(mapping = nil)
  return @filterable if mapping.nil?

  mapping.each do |request_key, column|
    @filterable[request_key.to_sym] = column.to_sym
  end
  @filterable
end

#filterable_columnsObject

Request-facing filter key (symbol) => backing column (symbol). Consumed by the list executor to build the WHERE clause.



100
101
102
# File 'lib/mcp_toolkit/resource.rb', line 100

def filterable_columns
  @filterable
end

#filterable_keysObject

Request-facing filter keys (symbols, sorted) this resource can be filtered by. Surfaced via the resource_schema tool.



94
95
96
# File 'lib/mcp_toolkit/resource.rb', line 94

def filterable_keys
  @filterable.keys.sort
end

#model(klass = nil) ⇒ Object



31
32
33
34
# File 'lib/mcp_toolkit/resource.rb', line 31

def model(klass = nil)
  @model = klass if klass
  @model
end

#required_permissions_scope(scope = nil) ⇒ Object

The OAuth-style scope a token MUST carry to reach this resource via the generic tools (e.g. "notifications__read"). Declared explicitly per resource:

required_permissions_scope "notifications__read"

Default nil = no scope required for this resource (unless the registry sets a default — see Registry#default_required_permissions_scope). Read with no arg.



58
59
60
61
# File 'lib/mcp_toolkit/resource.rb', line 58

def required_permissions_scope(scope = nil)
  @required_permissions_scope = scope if scope
  @required_permissions_scope
end

#resolve_relation(scope_root) ⇒ Object

The account-scoped relation for this resource. Raises if misconfigured so a registry mistake fails loudly rather than leaking an unscoped query.

Raises:



106
107
108
109
110
111
112
# File 'lib/mcp_toolkit/resource.rb', line 106

def resolve_relation(scope_root)
  raise NotConfigured, "resource #{name.inspect} has no scope block" unless scope
  raise NotConfigured, "resource #{name.inspect} has no model" unless model
  raise NotConfigured, "resource #{name.inspect} has no serializer" unless serializer

  scope.call(scope_root)
end

#scope(&block) ⇒ Object



41
42
43
44
# File 'lib/mcp_toolkit/resource.rb', line 41

def scope(&block)
  @scope_block = block if block
  @scope_block
end

#serializer(klass = nil) ⇒ Object



36
37
38
39
# File 'lib/mcp_toolkit/resource.rb', line 36

def serializer(klass = nil)
  @serializer = klass if klass
  @serializer
end