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
CustomTool, 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::Health::Tools::ListChecks, Plugins::Health::Tools::RunCheck, Plugins::Jobs::Tools::FailedJobs, Plugins::Jobs::Tools::Health, Plugins::Jobs::Tools::QueueSizes, Plugins::Jobs::Tools::RateMetrics, Plugins::Jobs::Tools::RecentJobs, Plugins::Rake::Tools::Run
Defined Under Namespace
Classes: Context
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
- .connection(value = NOT_SET) ⇒ Object
-
.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.
-
.input_schema_hash ⇒ Object
JSON Schema (object) for the declared arguments.
- .invoke(args) ⇒ Object
-
.name(value = NOT_SET) ⇒ Object
(also: tool_name)
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.
Instance Method Summary collapse
-
#call(_args, _ctx) ⇒ Object
Tool authors override this.
Class Method Details
.argument(arg_name, type, required: false, enum: nil, default: nil, description: nil, redact: false, minimum: nil, maximum: nil) ⇒ Object
82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/talk_to_your_app/tool.rb', line 82 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
95 96 97 |
# File 'lib/talk_to_your_app/tool.rb', line 95 def arguments @arguments ||= {} end |
.connection(value = NOT_SET) ⇒ Object
78 79 80 |
# File 'lib/talk_to_your_app/tool.rb', line 78 def connection(value = NOT_SET) value == NOT_SET ? @connection : (@connection = value) end |
.default_arguments ⇒ Object
Static metadata, computed once per tool class.
165 166 167 168 169 |
# File 'lib/talk_to_your_app/tool.rb', line 165 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
74 75 76 |
# File 'lib/talk_to_your_app/tool.rb', line 74 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.
145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/talk_to_your_app/tool.rb', line 145 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 |
.input_schema_hash ⇒ Object
JSON Schema (object) for the declared arguments.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/talk_to_your_app/tool.rb', line 102 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
158 159 160 161 162 |
# File 'lib/talk_to_your_app/tool.rb', line 158 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 Also known as: tool_name
The MCP tool name (e.g. “db.query”). Overrides Class#name as a setter while preserving it as a reader before one is assigned.
65 66 67 68 69 70 71 |
# File 'lib/talk_to_your_app/tool.rb', line 65 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
171 172 173 174 175 176 177 178 179 180 |
# File 'lib/talk_to_your_app/tool.rb', line 171 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.
119 120 121 |
# File 'lib/talk_to_your_app/tool.rb', line 119 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.
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/talk_to_your_app/tool.rb', line 126 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 |
Instance Method Details
#call(_args, _ctx) ⇒ Object
Tool authors override this.
184 185 186 |
# File 'lib/talk_to_your_app/tool.rb', line 184 def call(_args, _ctx) raise NotImplementedError, "#{self.class}#call must be implemented" end |