Class: McpToolkit::Resource
- Inherits:
-
Object
- Object
- McpToolkit::Resource
- 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: CustomFilter, NotConfigured
Instance Attribute Summary collapse
-
#custom_filters ⇒ Object
readonly
Request-facing custom-filter key (symbol) => CustomFilter.
-
#extras ⇒ Object
readonly
The full host-extras bag (symbol/whatever key => value).
-
#name ⇒ Object
readonly
Returns the value of attribute name.
Instance Method Summary collapse
-
#association_descriptors ⇒ Object
Association descriptors (the
linksshape) read off the serializer. -
#attribute_names ⇒ Object
Serialized attribute names (the response shape), read off the serializer's declared attributes.
- #description(text = nil) ⇒ Object
-
#effective_required_permissions_scope(default = nil) ⇒ Object
The scope actually enforced for this resource: its own declared scope if set, otherwise the registry-level
defaultpassed in. -
#extra(key, value = UNSET) ⇒ Object
A generic, api-agnostic metadata bag for host-defined "extras" — declarations the gem does not model itself (e.g. an app's ORM dependency list used to build a serializer).
-
#filter(name, type:, description:, &applier) ⇒ Object
Declares a resource-specific ("custom") filter: a request-facing
namewhose value is applied to the already-scoped relation by the given block. -
#filterable(mapping = nil, &block) ⇒ Object
Declares the per-attribute filters this resource accepts on the
listtool. -
#filterable_columns ⇒ Object
Request-facing filter key (symbol) => backing column (symbol).
-
#filterable_keys ⇒ Object
Request-facing filter keys (symbols, sorted) this resource can be filtered by.
-
#initialize(name) ⇒ Resource
constructor
A new instance of Resource.
- #model(klass = nil) ⇒ Object
-
#note(text = nil) ⇒ Object
Free-form usage caveat surfaced by the
resources/resource_schematools, e.g. -
#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").
-
#resolve_relation(scope_root) ⇒ Object
The account-scoped relation for this resource.
- #scope(&block) ⇒ Object
- #serializer(klass = nil) ⇒ Object
-
#superusers_only! ⇒ Object
Restricts this resource to superuser (cross-tenant) callers on the AUTHORITY path: an authority tool refuses
get/list/resource_schemafor a non-superuser and HIDES the resource fromresourcesdiscovery. -
#superusers_only? ⇒ Boolean
Whether this resource is restricted to superuser callers (default false).
Constructor Details
#initialize(name) ⇒ Resource
Returns a new instance of Resource.
35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/mcp_toolkit/resource.rb', line 35 def initialize(name) @name = name.to_s @model = nil @serializer = nil @scope_block = nil @description = nil @note = nil @superusers_only = false @filterable = {} @filterable_source = nil @custom_filters = {} @required_permissions_scope = nil @extras = {} end |
Instance Attribute Details
#custom_filters ⇒ Object (readonly)
Request-facing custom-filter key (symbol) => CustomFilter. Consumed by the list executor (which applies each block whose key is present in the request params) and by resource_schema (which surfaces each filter's type/description).
138 139 140 |
# File 'lib/mcp_toolkit/resource.rb', line 138 def custom_filters @custom_filters end |
#extras ⇒ Object (readonly)
The full host-extras bag (symbol/whatever key => value). Read-only view for a resource_finalizer that wants to iterate every declared extra.
65 66 67 |
# File 'lib/mcp_toolkit/resource.rb', line 65 def extras @extras end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
33 34 35 |
# File 'lib/mcp_toolkit/resource.rb', line 33 def name @name end |
Instance Method Details
#association_descriptors ⇒ Object
Association descriptors (the links shape) read off the serializer.
224 225 226 227 228 |
# File 'lib/mcp_toolkit/resource.rb', line 224 def association_descriptors return [] unless serializer.respond_to?(:declared_associations) serializer.declared_associations end |
#attribute_names ⇒ Object
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.
217 218 219 220 221 |
# File 'lib/mcp_toolkit/resource.rb', line 217 def attribute_names return [] unless serializer.respond_to?(:declared_attributes) serializer.declared_attributes.map(&:to_sym) end |
#description(text = nil) ⇒ Object
82 83 84 85 |
# File 'lib/mcp_toolkit/resource.rb', line 82 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.
154 155 156 |
# File 'lib/mcp_toolkit/resource.rb', line 154 def (default = nil) @required_permissions_scope || default end |
#extra(key, value = UNSET) ⇒ Object
A generic, api-agnostic metadata bag for host-defined "extras" — declarations
the gem does not model itself (e.g. an app's ORM dependency list used to build a
serializer). It is the storage behind config.registry.resource_extension (the
host DSL that writes extras inside a registration block) and
config.registry.resource_finalizer (which reads them back to derive gem-native
fields such as serializer / filterable). Write with extra(:key, value);
read with extra(:key) (nil when unset). The gem never inspects the values.
57 58 59 60 61 |
# File 'lib/mcp_toolkit/resource.rb', line 57 def extra(key, value = UNSET) return @extras[key] if value.equal?(UNSET) @extras[key] = value end |
#filter(name, type:, description:, &applier) ⇒ Object
Declares a resource-specific ("custom") filter: a request-facing name whose
value is applied to the already-scoped relation by the given block. Unlike the
filterable allowlist (generic equality/operator filters on a declared
column), a custom filter runs ARBITRARY host logic, so a host can express a
relational filter the gem could not derive:
filter :rental_id, type: :integer, description: "Only rows for this rental" do |relation, value|
relation.joins(:booking).where(bookings: { rental_id: value })
end
The block receives (relation, value) and MUST return a relation (narrowing
only). type / description are metadata surfaced by resource_schema. The
value arrives from a TOP-LEVEL request param keyed by name (see ListExecutor),
applied BEFORE the allowlist filterable filters. api-agnostic: the gem stores
and calls the block without inspecting it.
131 132 133 |
# File 'lib/mcp_toolkit/resource.rb', line 131 def filter(name, type:, description:, &applier) @custom_filters[name.to_sym] = CustomFilter.new(name: name.to_sym, type:, description:, applier:) end |
#filterable(mapping = nil, &block) ⇒ 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.
Accepts a Hash (merged now) OR a callable returning a Hash (resolved LAZILY on
first read). The lazy form lets a host derive the map from something that must
NOT be touched at registration/boot time — e.g. a DB-backed column list
(Model.column_names): registration typically runs inside an initializer's
to_prepare, before the database may exist (e.g. CI's db:create), so hitting
the DB there aborts boot. A callable source is invoked at most once — on the
first read (a tool call, when the DB is present) — then memoized.
179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/mcp_toolkit/resource.rb', line 179 def filterable(mapping = nil, &block) source = block || mapping return filterable_columns if source.nil? if source.respond_to?(:call) @filterable_source = source else merge_filterable!(source) end self end |
#filterable_columns ⇒ Object
Request-facing filter key (symbol) => backing column (symbol). Consumed by the list executor to build the WHERE clause.
199 200 201 202 |
# File 'lib/mcp_toolkit/resource.rb', line 199 def filterable_columns resolve_filterable_source! @filterable end |
#filterable_keys ⇒ Object
Request-facing filter keys (symbols, sorted) this resource can be filtered
by. Surfaced via the resource_schema tool.
193 194 195 |
# File 'lib/mcp_toolkit/resource.rb', line 193 def filterable_keys filterable_columns.keys.sort end |
#model(klass = nil) ⇒ Object
67 68 69 70 |
# File 'lib/mcp_toolkit/resource.rb', line 67 def model(klass = nil) @model = klass if klass @model end |
#note(text = nil) ⇒ Object
Free-form usage caveat surfaced by the resources / resource_schema tools,
e.g. to flag a resource as internal-debugging-only and not to be interpreted
without domain knowledge. Read with no arg. api-agnostic passthrough string.
90 91 92 93 |
# File 'lib/mcp_toolkit/resource.rb', line 90 def note(text = nil) @note = text if text @note 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:
"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.
147 148 149 150 |
# File 'lib/mcp_toolkit/resource.rb', line 147 def (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.
206 207 208 209 210 211 212 |
# File 'lib/mcp_toolkit/resource.rb', line 206 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
77 78 79 80 |
# File 'lib/mcp_toolkit/resource.rb', line 77 def scope(&block) @scope_block = block if block @scope_block end |
#serializer(klass = nil) ⇒ Object
72 73 74 75 |
# File 'lib/mcp_toolkit/resource.rb', line 72 def serializer(klass = nil) @serializer = klass if klass @serializer end |
#superusers_only! ⇒ Object
Restricts this resource to superuser (cross-tenant) callers on the AUTHORITY
path: an authority tool refuses get / list / resource_schema for a
non-superuser and HIDES the resource from resources discovery. Declared in a
resource's registration block:
McpToolkit.registry.register(:audit_events) do
superusers_only!
...
end
Generic and api-agnostic — the gem never names an app concept; the caller's superuser-ness is derived by the Authority::Context off the principal.
107 108 109 |
# File 'lib/mcp_toolkit/resource.rb', line 107 def superusers_only! @superusers_only = true end |
#superusers_only? ⇒ Boolean
Whether this resource is restricted to superuser callers (default false).
112 113 114 |
# File 'lib/mcp_toolkit/resource.rb', line 112 def superusers_only? @superusers_only end |