Class: McpToolkit::Tools::Base
- Inherits:
-
MCP::Tool
- Object
- MCP::Tool
- McpToolkit::Tools::Base
- Defined in:
- lib/mcp_toolkit/tools/base.rb
Overview
Base class for the generic MCP tools. Subclasses an official-SDK MCP::Tool,
so name/description/input_schema and the call contract are the gem's.
This base adds the shared concern every tool needs: authenticating +
scope-resolving the caller (via McpToolkit::Auth::Authenticator) before
running, and turning tool-level errors into isError: true MCP results
(rather than letting them become JSON-RPC protocol errors).
The bearer token, JSON-RPC _meta, and the account-id header are threaded in
through server_context (set per-request by the controller). The active
McpToolkit config is also threaded in as server_context[:mcp_config] so a
process can, in principle, host more than one configured server; it falls back
to McpToolkit.config.
Direct Known Subclasses
Class Method Summary collapse
- .config_from(server_context) ⇒ Object
- .error_response(message) ⇒ Object
- .lookup_resource(name, config) ⇒ Object
-
.meta_from(server_context) ⇒ Object
The gem nests the request
_metaunder server_context. -
.resolve_descriptor(name, config) ⇒ Object
Validates the
resourceargument is present and resolves its descriptor, raising InvalidParams (=> clean tool error) for a blank or unknown resource. -
.superuser_only_refusal(resource, introspection) ⇒ Object
Refuses a superuser-only resource for a non-superuser caller (get / list / resource_schema); nil = allowed.
- .text_response(payload) ⇒ Object
-
.with_account(server_context, account_id: nil, required_scope: nil, resource: nil) ⇒ Object
Runs
blockwith an authenticated, scoped context, serializing any McpToolkit::Errors into a clean text tool error. -
.with_authentication(server_context, required_scope: nil, resource: nil) ⇒ Object
Authenticates the token (valid + the explicitly-declared
required_scope) WITHOUT requiring an account selection.
Class Method Details
.config_from(server_context) ⇒ Object
103 104 105 |
# File 'lib/mcp_toolkit/tools/base.rb', line 103 def self.config_from(server_context) server_context[:mcp_config] || McpToolkit.config end |
.error_response(message) ⇒ Object
94 95 96 |
# File 'lib/mcp_toolkit/tools/base.rb', line 94 def self.error_response() MCP::Tool::Response.new([{ type: "text", text: }], error: true) end |
.lookup_resource(name, config) ⇒ Object
107 108 109 110 111 |
# File 'lib/mcp_toolkit/tools/base.rb', line 107 def self.lookup_resource(name, config) config.registry.fetch(name) rescue McpToolkit::Registry::UnknownResource => e raise McpToolkit::Errors::InvalidParams, e. end |
.meta_from(server_context) ⇒ Object
The gem nests the request _meta under server_context.
99 100 101 |
# File 'lib/mcp_toolkit/tools/base.rb', line 99 def self.(server_context) server_context[:_meta] || {} end |
.resolve_descriptor(name, config) ⇒ Object
Validates the resource argument is present and resolves its descriptor,
raising InvalidParams (=> clean tool error) for a blank or unknown resource.
115 116 117 118 119 |
# File 'lib/mcp_toolkit/tools/base.rb', line 115 def self.resolve_descriptor(name, config) raise McpToolkit::Errors::InvalidParams, "resource is required" if name.to_s.strip.empty? lookup_resource(name, config) end |
.superuser_only_refusal(resource, introspection) ⇒ Object
Refuses a superuser-only resource for a non-superuser caller (get / list /
resource_schema); nil = allowed. Mirrors the authority path's
ensure_resource_accessible!. resources HIDES such resources instead of
refusing — it filters on introspection.superuser? directly.
82 83 84 85 86 87 |
# File 'lib/mcp_toolkit/tools/base.rb', line 82 def self.superuser_only_refusal(resource, introspection) return nil unless resource&.superusers_only? return nil if introspection.superuser? error_response("Unauthorized: #{resource.name} is restricted to superuser (user-scoped) MCP tokens") end |
.text_response(payload) ⇒ Object
89 90 91 92 |
# File 'lib/mcp_toolkit/tools/base.rb', line 89 def self.text_response(payload) text = payload.is_a?(String) ? payload : JSON.generate(payload) MCP::Tool::Response.new([{ type: "text", text: }]) end |
.with_account(server_context, account_id: nil, required_scope: nil, resource: nil) ⇒ Object
Runs block with an authenticated, scoped context, serializing any
McpToolkit::Errors into a clean text tool error.
The resolved scope_root is yielded — it is the tools' serializer scope
AND the root every query is scoped through.
account_id is the superuser account selector arriving as a tool
argument (the gem passes tool args as kwargs, not via server_context),
threaded here so it joins _meta / the header in the resolution order.
required_scope is the explicitly-declared scope a token must carry (the
caller resolves it from the resource — see Registry#required_scope_for).
Empty/nil => no scope check (authorized_for_scope? treats "" as a pass).
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/mcp_toolkit/tools/base.rb', line 29 def self.with_account(server_context, account_id: nil, required_scope: nil, resource: nil) config = config_from(server_context) context = McpToolkit::Auth::Authenticator.call( token: server_context[:bearer_token], meta: (server_context), arguments: { "account_id" => account_id }.compact, header_account_id: server_context[:header_account_id], config: ) unless context.introspection.(required_scope) return error_response("Unauthorized: token lacks the #{required_scope.inspect} scope") end superuser_refusal = superuser_only_refusal(resource, context.introspection) return superuser_refusal if superuser_refusal text_response(yield(context.scope_root)) rescue McpToolkit::Errors::Unauthorized => e error_response("Unauthorized: #{e.}") rescue McpToolkit::Errors::InvalidParams => e error_response("Invalid request: #{e.}") end |
.with_authentication(server_context, required_scope: nil, resource: nil) ⇒ Object
Authenticates the token (valid + the explicitly-declared required_scope)
WITHOUT requiring an account selection. Used by the schema-discovery tools,
which reveal shape, not tenant data, so a superuser shouldn't have to pin an
account just to discover what exists. Empty/nil required_scope => no scope
check.
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/mcp_toolkit/tools/base.rb', line 58 def self.with_authentication(server_context, required_scope: nil, resource: nil) config = config_from(server_context) introspection = McpToolkit::Auth::Introspection.call(server_context[:bearer_token], config:) return error_response("Unauthorized: invalid or expired token") unless introspection.valid? unless introspection.(required_scope) return error_response("Unauthorized: token lacks the #{required_scope.inspect} scope") end superuser_refusal = superuser_only_refusal(resource, introspection) return superuser_refusal if superuser_refusal # Yields the introspection so a discovery tool (`resources`) can HIDE # superuser-only resources from a non-superuser caller (get/list/resource_schema # instead pass `resource:` above to REFUSE a specific one). text_response(yield(introspection)) rescue McpToolkit::Errors::InvalidParams => e error_response("Invalid request: #{e.}") end |