Class: McpToolkit::Tools::Base

Inherits:
MCP::Tool
  • Object
show all
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

Get, List, ResourceSchema, Resources

Class Method Summary collapse

Class Method Details

.config_from(server_context) ⇒ Object



83
84
85
# File 'lib/mcp_toolkit/tools/base.rb', line 83

def self.config_from(server_context)
  server_context[:mcp_config] || McpToolkit.config
end

.error_response(message) ⇒ Object



74
75
76
# File 'lib/mcp_toolkit/tools/base.rb', line 74

def self.error_response(message)
  MCP::Tool::Response.new([{ type: "text", text: message }], error: true)
end

.lookup_resource(name, config) ⇒ Object



87
88
89
90
91
# File 'lib/mcp_toolkit/tools/base.rb', line 87

def self.lookup_resource(name, config)
  config.registry.fetch(name)
rescue McpToolkit::Registry::UnknownResource => e
  raise McpToolkit::Errors::InvalidParams, e.message
end

.meta_from(server_context) ⇒ Object

The gem nests the request _meta under server_context.



79
80
81
# File 'lib/mcp_toolkit/tools/base.rb', line 79

def self.meta_from(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.



95
96
97
98
99
# File 'lib/mcp_toolkit/tools/base.rb', line 95

def self.resolve_descriptor(name, config)
  raise McpToolkit::Errors::InvalidParams, "resource is required" if name.to_s.strip.empty?

  lookup_resource(name, config)
end

.text_response(payload) ⇒ Object



69
70
71
72
# File 'lib/mcp_toolkit/tools/base.rb', line 69

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) ⇒ 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
# File 'lib/mcp_toolkit/tools/base.rb', line 29

def self.(server_context, account_id: nil, required_scope: nil)
  config = config_from(server_context)
  context = McpToolkit::Auth::Authenticator.call(
    token: server_context[:bearer_token],
    meta: meta_from(server_context),
    arguments: { "account_id" =>  }.compact,
    header_account_id: server_context[:header_account_id],
    config:
  )

  unless context.introspection.authorized_for_scope?(required_scope)
    return error_response("Unauthorized: token lacks the #{required_scope.inspect} scope")
  end

  text_response(yield(context.scope_root))
rescue McpToolkit::Errors::Unauthorized => e
  error_response("Unauthorized: #{e.message}")
rescue McpToolkit::Errors::InvalidParams => e
  error_response("Invalid request: #{e.message}")
end

.with_authentication(server_context, required_scope: 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.



55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/mcp_toolkit/tools/base.rb', line 55

def self.with_authentication(server_context, required_scope: 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.authorized_for_scope?(required_scope)
    return error_response("Unauthorized: token lacks the #{required_scope.inspect} scope")
  end

  text_response(yield)
rescue McpToolkit::Errors::InvalidParams => e
  error_response("Invalid request: #{e.message}")
end