Class: TalkToYourApp::Tool
- Inherits:
-
Object
- Object
- TalkToYourApp::Tool
- Defined in:
- lib/talk_to_your_app/tool.rb
Overview
Base class for MCP tools. Authors subclass it, declare arguments with the class-level DSL, and implement ‘#call(args, ctx)`.
class DbQueryTool < TalkToYourApp::Tool
name "db.query"
description "Run a read-only SQL query."
connection :replica_readonly
argument :sql, :string, required: true
argument :format, :string, enum: %w[json text html], default: "json"
def call(args, ctx)
ctx.connection { |conn| ... }
end
end
A tool compiles down to an ‘MCP::Tool` subclass via `.to_mcp_tool`; the argument DSL produces the JSON Schema the SDK validates against.
Direct Known Subclasses
Plugins::Db::Tools::Query, Plugins::Db::Tools::Schema, Plugins::Db::Tools::Tables, Plugins::Flipper::Tools::DisableFlag, Plugins::Flipper::Tools::EnableFlag, Plugins::Flipper::Tools::EnabledFlags, Plugins::Flipper::Tools::ListFlags, Plugins::Flipper::Tools::ReadFlag, Plugins::Jobs::Tools::FailedJobs, Plugins::Jobs::Tools::QueueSizes, Plugins::Jobs::Tools::RateMetrics, Plugins::Jobs::Tools::RecentJobs, Plugins::Rake::Tools::Run
Defined Under Namespace
Classes: Context
Class Attribute Summary collapse
-
.collecting_custom_tools ⇒ Object
While true, every newly defined Tool subclass adds itself to custom_registry.
Class Method Summary collapse
- .argument(arg_name, type, required: false, enum: nil, default: nil, description: nil, redact: false, minimum: nil, maximum: nil) ⇒ Object
- .arguments ⇒ Object
-
.clear_custom_registry! ⇒ Object
Test seam: forget all collected custom tools.
- .connection(value = NOT_SET) ⇒ Object
-
.custom_registry ⇒ Object
Tools collected while loading the custom_tools directory, in definition order.
-
.default_arguments ⇒ Object
Static metadata, computed once per tool class.
- .description(value = NOT_SET) ⇒ Object
-
.dispatch(args, plugin_name: nil, log_level: nil) ⇒ Object
Invocation entry point, wrapped by the audit logger: one log line per call.
-
.inherited(subclass) ⇒ Object
Records app-defined tools while collection is active (see above).
-
.input_schema_hash ⇒ Object
JSON Schema (object) for the declared arguments.
- .invoke(args) ⇒ Object
-
.name(value = NOT_SET) ⇒ Object
The MCP tool name (e.g. “db.query”).
- .normalize_response(result) ⇒ Object
-
.to_mcp_definition ⇒ Object
The shape used by tests and documentation.
-
.to_mcp_tool(plugin_name: nil, log_level: nil) ⇒ Object
Builds the MCP::Tool subclass the server registers.
- .tool_name ⇒ Object
Instance Method Summary collapse
-
#call(_args, _ctx) ⇒ Object
Tool authors override this.
Class Attribute Details
.collecting_custom_tools ⇒ Object
While true, every newly defined Tool subclass adds itself to custom_registry. The :custom_tools plugin flips this on only while it loads the host app’s app/talk_to_your_app/custom_tools/ directory, so the bundled tools (defined when the gem loads) are never collected.
112 113 114 |
# File 'lib/talk_to_your_app/tool.rb', line 112 def collecting_custom_tools @collecting_custom_tools end |
Class Method Details
.argument(arg_name, type, required: false, enum: nil, default: nil, description: nil, redact: false, minimum: nil, maximum: nil) ⇒ Object
89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/talk_to_your_app/tool.rb', line 89 def argument(arg_name, type, required: false, enum: nil, default: nil, description: nil, redact: false, minimum: nil, maximum: nil) arguments[arg_name.to_sym] = { type: type.to_s, required: required, enum: enum, default: default, description: description, redact: redact, minimum: minimum, maximum: maximum, }.compact end |
.arguments ⇒ Object
102 103 104 |
# File 'lib/talk_to_your_app/tool.rb', line 102 def arguments @arguments ||= {} end |
.clear_custom_registry! ⇒ Object
Test seam: forget all collected custom tools.
122 123 124 |
# File 'lib/talk_to_your_app/tool.rb', line 122 def clear_custom_registry! custom_registry.clear end |
.connection(value = NOT_SET) ⇒ Object
85 86 87 |
# File 'lib/talk_to_your_app/tool.rb', line 85 def connection(value = NOT_SET) value == NOT_SET ? @connection : (@connection = value) end |
.custom_registry ⇒ Object
Tools collected while loading the custom_tools directory, in definition order. Held on the base class; subclasses share the one list.
116 117 118 119 |
# File 'lib/talk_to_your_app/tool.rb', line 116 def custom_registry TalkToYourApp::Tool.instance_variable_get(:@custom_registry) || TalkToYourApp::Tool.instance_variable_set(:@custom_registry, []) end |
.default_arguments ⇒ Object
Static metadata, computed once per tool class.
199 200 201 202 203 |
# File 'lib/talk_to_your_app/tool.rb', line 199 def default_arguments @default_arguments ||= arguments.each_with_object({}) do |(arg_name, opts), acc| acc[arg_name] = opts[:default] unless opts[:default].nil? end end |
.description(value = NOT_SET) ⇒ Object
81 82 83 |
# File 'lib/talk_to_your_app/tool.rb', line 81 def description(value = NOT_SET) value == NOT_SET ? @description : (@description = value) end |
.dispatch(args, plugin_name: nil, log_level: nil) ⇒ Object
Invocation entry point, wrapped by the audit logger: one log line per call. Applies argument defaults, runs the tool, and normalizes the return into an MCP::Tool::Response.
179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/talk_to_your_app/tool.rb', line 179 def dispatch(args, plugin_name: nil, log_level: nil) AuditLogger.around(tool_class: self, plugin_name: plugin_name, log_level: log_level, params: args) do if TalkToYourApp.configuration.(TalkToYourApp::Current.principal, tool_name) invoke(args) else MCP::Tool::Response.new( [{ type: "text", text: "Not authorized: principal may not call #{tool_name}." }], error: true, ) end end end |
.inherited(subclass) ⇒ Object
Records app-defined tools while collection is active (see above). Fires at any subclass depth.
128 129 130 131 |
# File 'lib/talk_to_your_app/tool.rb', line 128 def inherited(subclass) super TalkToYourApp::Tool.custom_registry << subclass if TalkToYourApp::Tool.collecting_custom_tools end |
.input_schema_hash ⇒ Object
JSON Schema (object) for the declared arguments.
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/talk_to_your_app/tool.rb', line 136 def input_schema_hash properties = arguments.each_with_object({}) do |(arg_name, opts), acc| prop = { type: opts[:type] } prop[:enum] = opts[:enum] if opts[:enum] prop[:description] = opts[:description] if opts[:description] prop[:minimum] = opts[:minimum] if opts[:minimum] prop[:maximum] = opts[:maximum] if opts[:maximum] acc[arg_name] = prop end required = arguments.select { |_, o| o[:required] }.keys.map(&:to_s) schema = { properties: properties } # Draft-04 (the SDK's metaschema) rejects an empty `required` array. schema[:required] = required unless required.empty? schema end |
.invoke(args) ⇒ Object
192 193 194 195 196 |
# File 'lib/talk_to_your_app/tool.rb', line 192 def invoke(args) merged = default_arguments.merge(args) ctx = Context.new(tool_class: self, logger: TalkToYourApp.configuration.logger) normalize_response(new.call(merged, ctx)) end |
.name(value = NOT_SET) ⇒ Object
The MCP tool name (e.g. “db.query”). Overrides Class#name as a setter while preserving it as a reader before one is assigned.
69 70 71 72 73 74 75 |
# File 'lib/talk_to_your_app/tool.rb', line 69 def name(value = NOT_SET) if value == NOT_SET defined?(@tool_name) && @tool_name ? @tool_name : super() else @tool_name = value end end |
.normalize_response(result) ⇒ Object
205 206 207 208 209 210 211 212 213 214 |
# File 'lib/talk_to_your_app/tool.rb', line 205 def normalize_response(result) case result when MCP::Tool::Response result when String MCP::Tool::Response.new([{ type: "text", text: result }]) else MCP::Tool::Response.new([{ type: "text", text: result.to_json }]) end end |
.to_mcp_definition ⇒ Object
The shape used by tests and documentation.
153 154 155 |
# File 'lib/talk_to_your_app/tool.rb', line 153 def to_mcp_definition { name: tool_name, description: description, input_schema: input_schema_hash } end |
.to_mcp_tool(plugin_name: nil, log_level: nil) ⇒ Object
Builds the MCP::Tool subclass the server registers. Its class-level ‘call(**args, server_context:)` bridges to this tool’s ‘#call(args, ctx)`. plugin_name and log_level are threaded through for the audit logger.
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/talk_to_your_app/tool.rb', line 160 def to_mcp_tool(plugin_name: nil, log_level: nil) if tool_name.nil? || tool_name.to_s.empty? raise TalkToYourApp::ConfigurationError, "#{inspect} has no MCP tool name — call `name \"your.tool\"` in the tool class." end ttya_tool = self MCP::Tool.define( name: ttya_tool.tool_name, description: ttya_tool.description, input_schema: ttya_tool.input_schema_hash, ) do |server_context: nil, **args| ttya_tool.dispatch(args, plugin_name: plugin_name, log_level: log_level) end end |
.tool_name ⇒ Object
77 78 79 |
# File 'lib/talk_to_your_app/tool.rb', line 77 def tool_name @tool_name end |
Instance Method Details
#call(_args, _ctx) ⇒ Object
Tool authors override this.
218 219 220 |
# File 'lib/talk_to_your_app/tool.rb', line 218 def call(_args, _ctx) raise NotImplementedError, "#{self.class}#call must be implemented" end |