talk_to_your_app 0.1.0.pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +402 -0
  4. data/lib/generators/talk_to_your_app/custom_tool/custom_tool_generator.rb +39 -0
  5. data/lib/generators/talk_to_your_app/custom_tool/templates/tool.rb.tt +18 -0
  6. data/lib/generators/talk_to_your_app/health_check/health_check_generator.rb +31 -0
  7. data/lib/generators/talk_to_your_app/health_check/templates/check.rb.tt +12 -0
  8. data/lib/generators/talk_to_your_app/install/install_generator.rb +27 -0
  9. data/lib/generators/talk_to_your_app/install/templates/initializer.rb.tt +78 -0
  10. data/lib/talk_to_your_app/audit_logger.rb +115 -0
  11. data/lib/talk_to_your_app/auth/api_key.rb +29 -0
  12. data/lib/talk_to_your_app/auth/basic.rb +24 -0
  13. data/lib/talk_to_your_app/auth/middleware.rb +74 -0
  14. data/lib/talk_to_your_app/configuration.rb +129 -0
  15. data/lib/talk_to_your_app/connection_registry.rb +131 -0
  16. data/lib/talk_to_your_app/current.rb +14 -0
  17. data/lib/talk_to_your_app/custom_tool.rb +40 -0
  18. data/lib/talk_to_your_app/plugin.rb +59 -0
  19. data/lib/talk_to_your_app/plugin_registry.rb +48 -0
  20. data/lib/talk_to_your_app/plugins/custom_tools/plugin.rb +26 -0
  21. data/lib/talk_to_your_app/plugins/db/plugin.rb +57 -0
  22. data/lib/talk_to_your_app/plugins/db/tools/query.rb +126 -0
  23. data/lib/talk_to_your_app/plugins/db/tools/schema.rb +60 -0
  24. data/lib/talk_to_your_app/plugins/db/tools/tables.rb +28 -0
  25. data/lib/talk_to_your_app/plugins/flipper/plugin.rb +132 -0
  26. data/lib/talk_to_your_app/plugins/flipper/tools/disable_flag.rb +41 -0
  27. data/lib/talk_to_your_app/plugins/flipper/tools/enable_flag.rb +42 -0
  28. data/lib/talk_to_your_app/plugins/flipper/tools/enabled_flags.rb +41 -0
  29. data/lib/talk_to_your_app/plugins/flipper/tools/list_flags.rb +23 -0
  30. data/lib/talk_to_your_app/plugins/flipper/tools/read_flag.rb +33 -0
  31. data/lib/talk_to_your_app/plugins/health/plugin.rb +31 -0
  32. data/lib/talk_to_your_app/plugins/health/registry.rb +68 -0
  33. data/lib/talk_to_your_app/plugins/health/tools/list_checks.rb +24 -0
  34. data/lib/talk_to_your_app/plugins/health/tools/run_check.rb +27 -0
  35. data/lib/talk_to_your_app/plugins/jobs/adapters/sidekiq.rb +122 -0
  36. data/lib/talk_to_your_app/plugins/jobs/adapters/solid_queue.rb +90 -0
  37. data/lib/talk_to_your_app/plugins/jobs/interface.rb +38 -0
  38. data/lib/talk_to_your_app/plugins/jobs/plugin.rb +87 -0
  39. data/lib/talk_to_your_app/plugins/jobs/tools/failed_jobs.rb +28 -0
  40. data/lib/talk_to_your_app/plugins/jobs/tools/health.rb +25 -0
  41. data/lib/talk_to_your_app/plugins/jobs/tools/queue_sizes.rb +23 -0
  42. data/lib/talk_to_your_app/plugins/jobs/tools/rate_metrics.rb +30 -0
  43. data/lib/talk_to_your_app/plugins/jobs/tools/recent_jobs.rb +28 -0
  44. data/lib/talk_to_your_app/plugins/rake/plugin.rb +42 -0
  45. data/lib/talk_to_your_app/plugins/rake/tools/run.rb +56 -0
  46. data/lib/talk_to_your_app/railtie.rb +56 -0
  47. data/lib/talk_to_your_app/renderers/html_table.rb +27 -0
  48. data/lib/talk_to_your_app/tool.rb +204 -0
  49. data/lib/talk_to_your_app/transport/rails_mount.rb +46 -0
  50. data/lib/talk_to_your_app/version.rb +5 -0
  51. data/lib/talk_to_your_app.rb +124 -0
  52. metadata +140 -0
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require_relative "../../../tool"
5
+
6
+ module TalkToYourApp
7
+ module Plugins
8
+ module Rake
9
+ module Tools
10
+ # Runs a single allow-listed rake task and returns its status and output
11
+ # as JSON. The task must be on the plugin's `allowed:` list; anything
12
+ # else is refused without executing. The task runs in a subprocess
13
+ # (`bundle exec rake`), so it cannot inject shell commands and its real
14
+ # exit status is captured.
15
+ class Run < TalkToYourApp::Tool
16
+ Result = Struct.new(:stdout, :stderr, :exit_code, keyword_init: true)
17
+
18
+ name "rake.run"
19
+ description "Run an allow-listed rake task and return its status and output."
20
+ argument :task, :string, required: true, description: "Rake task name (must be allow-listed), e.g. \"stats\"."
21
+ argument :args, :array, description: "Positional task arguments, e.g. [\"42\"] for task[42]."
22
+
23
+ def call(args, _ctx)
24
+ task = args[:task].to_s
25
+ unless TalkToYourApp::Plugins::Rake.allowed?(task)
26
+ return error("Rake task #{task.inspect} is not allowed. Allowed tasks: #{TalkToYourApp::Plugins::Rake.allowed_tasks.inspect}.")
27
+ end
28
+
29
+ result = self.class.run_task(task, Array(args[:args]).map(&:to_s))
30
+ json(
31
+ task: task,
32
+ status: result.exit_code.zero? ? "success" : "error",
33
+ exit_code: result.exit_code,
34
+ output: result.stdout.strip,
35
+ error: (result.stderr.strip.empty? ? nil : result.stderr.strip),
36
+ )
37
+ rescue StandardError => e
38
+ error("Failed to run rake #{args[:task]}: #{e.class}: #{e.message}")
39
+ end
40
+
41
+ # Executes `bundle exec rake <task>[args]` in the app root. Array argv
42
+ # (no shell) so task/args cannot inject shell commands.
43
+ def self.run_task(task, rake_args)
44
+ invocation = rake_args.empty? ? task : "#{task}[#{rake_args.join(",")}]"
45
+ stdout, stderr, status = Open3.capture3("bundle", "exec", "rake", invocation, chdir: app_root)
46
+ Result.new(stdout: stdout, stderr: stderr, exit_code: status.exitstatus || 1)
47
+ end
48
+
49
+ def self.app_root
50
+ defined?(Rails) && Rails.respond_to?(:root) ? Rails.root.to_s : Dir.pwd
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module TalkToYourApp
6
+ # Wires the gem into the Rails boot sequence. The headline behavior is the
7
+ # fail-closed validation initializer: every required piece of configuration is
8
+ # checked once, at the end of boot, so misconfiguration surfaces at deploy
9
+ # time rather than on the first MCP request.
10
+ class Railtie < ::Rails::Railtie
11
+ # The gem owns two subdirs of app/talk_to_your_app/ and loads them itself
12
+ # (custom_tools/ files it requires; health/ files define no constant — they
13
+ # call Health.register). Tell every autoloader to ignore just those two
14
+ # subdirs before it is set up, so eager loading (production, zeitwerk:check)
15
+ # doesn't expect constants that aren't there. Anything else a host places
16
+ # under app/talk_to_your_app/ is left to Zeitwerk to autoload normally.
17
+ initializer "talk_to_your_app.ignore_app_dir", before: :set_autoload_paths do |app|
18
+ next unless ::Rails.respond_to?(:autoloaders)
19
+
20
+ %w[custom_tools health].each do |subdir|
21
+ dir = app.root.join(TalkToYourApp::APP_DIR, subdir)
22
+ ::Rails.autoloaders.each { |loader| loader.ignore(dir) }
23
+ end
24
+ end
25
+
26
+ # Runs after the host app's config/initializers (where the operator calls
27
+ # TalkToYourApp.configure) have loaded.
28
+ initializer "talk_to_your_app.validate", after: :load_config_initializers do
29
+ config.after_initialize do
30
+ TalkToYourApp.configuration.logger ||= Rails.logger
31
+ TalkToYourApp::Railtie.validate_boot!
32
+ end
33
+ end
34
+
35
+ # Aggregated fail-closed validation. With nothing enabled (the default),
36
+ # validation is a no-op and the gem boots silently. Later units add auth and
37
+ # plugin checks.
38
+ def self.validate_boot!
39
+ TalkToYourApp::PluginRegistry.validate_enabled!
40
+ TalkToYourApp::ConnectionRegistry.validate!(TalkToYourApp.required_connections)
41
+ validate_auth!
42
+ end
43
+
44
+ # At least one auth mechanism must be configured once the endpoint serves
45
+ # tools. With no plugins enabled (the default) there is nothing to protect,
46
+ # so the gem still boots cleanly with no configuration at all.
47
+ def self.validate_auth!
48
+ return if TalkToYourApp.configuration.enabled_plugins.empty?
49
+ return if TalkToYourApp.configuration.auth_configured?
50
+
51
+ raise ConfigurationError,
52
+ "talk_to_your_app: no authentication configured. Set `config.api_keys` or `config.basic_auth` " \
53
+ "in config/initializers/talk_to_your_app.rb (the MCP endpoint must not be exposed unauthenticated)."
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module TalkToYourApp
6
+ module Renderers
7
+ # Minimal HTML table renderer — no ActionView dependency. Every value is
8
+ # HTML-escaped via CGI.escape_html; the output is returned as a plain string
9
+ # for an MCP text content block and is never marked html_safe. NULL cells
10
+ # render as empty.
11
+ module HtmlTable
12
+ module_function
13
+
14
+ def render(columns, rows)
15
+ head = "<thead><tr>#{columns.map { |c| "<th>#{esc(c)}</th>" }.join}</tr></thead>"
16
+ body = rows.map do |row|
17
+ "<tr>#{row.map { |cell| "<td>#{cell.nil? ? "" : esc(cell)}</td>" }.join}</tr>"
18
+ end.join
19
+ "<table>#{head}<tbody>#{body}</tbody></table>"
20
+ end
21
+
22
+ def esc(value)
23
+ CGI.escape_html(value.to_s)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "mcp"
5
+ require_relative "current"
6
+ require_relative "audit_logger"
7
+
8
+ module TalkToYourApp
9
+ # Base class for MCP tools. Authors subclass it, declare arguments with the
10
+ # class-level DSL, and implement `#call(args, ctx)`.
11
+ #
12
+ # class DbQueryTool < TalkToYourApp::Tool
13
+ # name "db.query"
14
+ # description "Run a read-only SQL query."
15
+ # connection :replica_readonly
16
+ # argument :sql, :string, required: true
17
+ # argument :format, :string, enum: %w[json text html], default: "json"
18
+ #
19
+ # def call(args, ctx)
20
+ # ctx.connection { |conn| ... }
21
+ # end
22
+ # end
23
+ #
24
+ # A tool compiles down to an `MCP::Tool` subclass via `.to_mcp_tool`; the
25
+ # argument DSL produces the JSON Schema the SDK validates against.
26
+ class Tool
27
+ NOT_SET = Object.new
28
+ private_constant :NOT_SET
29
+
30
+ # The context handed to `#call`. Exposes the request principal and session,
31
+ # the audit logger, and role-switched database connections.
32
+ class Context
33
+ attr_reader :logger
34
+
35
+ def initialize(tool_class:, logger: nil)
36
+ @tool_class = tool_class
37
+ @logger = logger
38
+ end
39
+
40
+ def principal
41
+ TalkToYourApp::Current.principal
42
+ end
43
+
44
+ def session_id
45
+ TalkToYourApp::Current.session_id
46
+ end
47
+
48
+ # Runs the block against a registry connection, switched to its declared
49
+ # role. With no name, uses the tool's declared `connection`.
50
+ def connection(name = nil, &block)
51
+ name ||= @tool_class.connection
52
+ if name.nil?
53
+ raise TalkToYourApp::ConfigurationError,
54
+ "tool #{@tool_class.tool_name.inspect} called ctx.connection without a name and declares no `connection`."
55
+ end
56
+ TalkToYourApp::ConnectionRegistry.with(name, &block)
57
+ end
58
+ end
59
+
60
+ class << self
61
+ # --- DSL ---------------------------------------------------------------
62
+
63
+ # The MCP tool name (e.g. "db.query"). Overrides Class#name as a setter
64
+ # while preserving it as a reader before one is assigned.
65
+ def name(value = NOT_SET)
66
+ if value == NOT_SET
67
+ defined?(@tool_name) && @tool_name ? @tool_name : super()
68
+ else
69
+ @tool_name = value
70
+ end
71
+ end
72
+ alias_method :tool_name, :name
73
+
74
+ def description(value = NOT_SET)
75
+ value == NOT_SET ? @description : (@description = value)
76
+ end
77
+
78
+ def connection(value = NOT_SET)
79
+ value == NOT_SET ? @connection : (@connection = value)
80
+ end
81
+
82
+ def argument(arg_name, type, required: false, enum: nil, default: nil, description: nil, redact: false, minimum: nil, maximum: nil)
83
+ arguments[arg_name.to_sym] = {
84
+ type: type.to_s,
85
+ required: required,
86
+ enum: enum,
87
+ default: default,
88
+ description: description,
89
+ redact: redact,
90
+ minimum: minimum,
91
+ maximum: maximum,
92
+ }.compact
93
+ end
94
+
95
+ def arguments
96
+ @arguments ||= {}
97
+ end
98
+
99
+ # --- Compilation -------------------------------------------------------
100
+
101
+ # JSON Schema (object) for the declared arguments.
102
+ def input_schema_hash
103
+ properties = arguments.each_with_object({}) do |(arg_name, opts), acc|
104
+ prop = { type: opts[:type] }
105
+ prop[:enum] = opts[:enum] if opts[:enum]
106
+ prop[:description] = opts[:description] if opts[:description]
107
+ prop[:minimum] = opts[:minimum] if opts[:minimum]
108
+ prop[:maximum] = opts[:maximum] if opts[:maximum]
109
+ acc[arg_name] = prop
110
+ end
111
+ required = arguments.select { |_, o| o[:required] }.keys.map(&:to_s)
112
+ schema = { properties: properties }
113
+ # Draft-04 (the SDK's metaschema) rejects an empty `required` array.
114
+ schema[:required] = required unless required.empty?
115
+ schema
116
+ end
117
+
118
+ # The shape used by tests and documentation.
119
+ def to_mcp_definition
120
+ { name: tool_name, description: description, input_schema: input_schema_hash }
121
+ end
122
+
123
+ # Builds the MCP::Tool subclass the server registers. Its class-level
124
+ # `call(**args, server_context:)` bridges to this tool's `#call(args, ctx)`.
125
+ # plugin_name and log_level are threaded through for the audit logger.
126
+ def to_mcp_tool(plugin_name: nil, log_level: nil)
127
+ if tool_name.nil? || tool_name.to_s.empty?
128
+ raise TalkToYourApp::ConfigurationError,
129
+ "#{inspect} has no MCP tool name — call `name \"your.tool\"` in the tool class."
130
+ end
131
+
132
+ ttya_tool = self
133
+ MCP::Tool.define(
134
+ name: ttya_tool.tool_name,
135
+ description: ttya_tool.description,
136
+ input_schema: ttya_tool.input_schema_hash,
137
+ ) do |server_context: nil, **args|
138
+ ttya_tool.dispatch(args, plugin_name: plugin_name, log_level: log_level)
139
+ end
140
+ end
141
+
142
+ # Invocation entry point, wrapped by the audit logger: one log line per
143
+ # call. Applies argument defaults, runs the tool, and normalizes the return
144
+ # into an MCP::Tool::Response.
145
+ def dispatch(args, plugin_name: nil, log_level: nil)
146
+ AuditLogger.around(tool_class: self, plugin_name: plugin_name, log_level: log_level, params: args) do
147
+ if TalkToYourApp.configuration.authorized?(TalkToYourApp::Current.principal, tool_name)
148
+ invoke(args)
149
+ else
150
+ MCP::Tool::Response.new(
151
+ [{ type: "text", text: "Not authorized: principal may not call #{tool_name}." }],
152
+ error: true,
153
+ )
154
+ end
155
+ end
156
+ end
157
+
158
+ def invoke(args)
159
+ merged = default_arguments.merge(args)
160
+ ctx = Context.new(tool_class: self, logger: TalkToYourApp.configuration.logger)
161
+ normalize_response(new.call(merged, ctx))
162
+ end
163
+
164
+ # Static metadata, computed once per tool class.
165
+ def default_arguments
166
+ @default_arguments ||= arguments.each_with_object({}) do |(arg_name, opts), acc|
167
+ acc[arg_name] = opts[:default] unless opts[:default].nil?
168
+ end
169
+ end
170
+
171
+ def normalize_response(result)
172
+ case result
173
+ when MCP::Tool::Response
174
+ result
175
+ when String
176
+ MCP::Tool::Response.new([{ type: "text", text: result }])
177
+ else
178
+ MCP::Tool::Response.new([{ type: "text", text: result.to_json }])
179
+ end
180
+ end
181
+ end
182
+
183
+ # Tool authors override this.
184
+ def call(_args, _ctx)
185
+ raise NotImplementedError, "#{self.class}#call must be implemented"
186
+ end
187
+
188
+ # --- Response helpers available to subclasses ---------------------------
189
+
190
+ private
191
+
192
+ def text(string)
193
+ MCP::Tool::Response.new([{ type: "text", text: string.to_s }])
194
+ end
195
+
196
+ def json(object)
197
+ MCP::Tool::Response.new([{ type: "text", text: JSON.pretty_generate(object) }])
198
+ end
199
+
200
+ def error(message)
201
+ MCP::Tool::Response.new([{ type: "text", text: message.to_s }], error: true)
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mcp"
4
+ require_relative "../auth/middleware"
5
+
6
+ module TalkToYourApp
7
+ module Transport
8
+ # Builds the Rack application the host app mounts. It assembles an
9
+ # MCP::Server from the enabled plugins' tools, wraps it in the SDK's
10
+ # Streamable HTTP transport, and fronts the whole thing with the auth
11
+ # middleware. The session layer lives entirely in the SDK transport, so a
12
+ # future stateless flip (2026-07-28 RC) is a one-line change here.
13
+ module RailsMount
14
+ module_function
15
+
16
+ def build
17
+ config = TalkToYourApp.configuration
18
+ server = MCP::Server.new(
19
+ name: config.server_name,
20
+ title: config.server_title,
21
+ version: config.server_version,
22
+ description: config.server_description,
23
+ instructions: config.instructions,
24
+ tools: collect_tools,
25
+ )
26
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(
27
+ server,
28
+ enable_json_response: true,
29
+ )
30
+ Auth::Middleware.new(transport)
31
+ end
32
+
33
+ # Compiles every enabled plugin's tools into MCP::Tool subclasses, each
34
+ # audit-wrapped with its plugin name and (optional) per-plugin log level.
35
+ def collect_tools
36
+ TalkToYourApp.enabled_plugins.flat_map do |name, plugin_class, _opts|
37
+ next [] unless plugin_class
38
+
39
+ plugin_class.tools.map do |tool_class|
40
+ tool_class.to_mcp_tool(plugin_name: name, log_level: plugin_class.log_level)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TalkToYourApp
4
+ VERSION = "0.1.0.pre.1"
5
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "talk_to_your_app/version"
4
+ require_relative "talk_to_your_app/configuration"
5
+ require_relative "talk_to_your_app/connection_registry"
6
+ require_relative "talk_to_your_app/current"
7
+ require_relative "talk_to_your_app/audit_logger"
8
+ require_relative "talk_to_your_app/tool"
9
+ require_relative "talk_to_your_app/plugin"
10
+ require_relative "talk_to_your_app/plugin_registry"
11
+ require_relative "talk_to_your_app/transport/rails_mount"
12
+
13
+ # Rails-native MCP server. See README for the configuration reference.
14
+ module TalkToYourApp
15
+ # Convention directory for host-app extensions: custom_tools/ holds CustomTool
16
+ # subclasses, health/ holds health-check files. The railtie tells Zeitwerk to
17
+ # ignore this tree (health files define no constant; tools we require here), so
18
+ # the gem loads its files itself via require_app_dir.
19
+ APP_DIR = "app/talk_to_your_app"
20
+
21
+ class << self
22
+ # Yields the configuration singleton for the host app's initializer.
23
+ #
24
+ # TalkToYourApp.configure do |config|
25
+ # config.mount_at = "/mcp"
26
+ # end
27
+ #
28
+ # Calling this more than once merges into the same object rather than
29
+ # replacing it.
30
+ def configure
31
+ yield(configuration) if block_given?
32
+ configuration
33
+ end
34
+
35
+ def configuration
36
+ @configuration ||= Configuration.new
37
+ end
38
+
39
+ # Test seam: drop all configuration back to defaults.
40
+ def reset_configuration!
41
+ @configuration = Configuration.new
42
+ @rack_app = nil
43
+ ConnectionRegistry.reset!
44
+ end
45
+
46
+ # The Rack application to mount in the host app's routes:
47
+ #
48
+ # mount TalkToYourApp.rack_app, at: TalkToYourApp.configuration.mount_at
49
+ #
50
+ # Built once from the enabled plugins. reset_configuration! clears it.
51
+ def rack_app
52
+ @rack_app ||= Transport::RailsMount.build
53
+ end
54
+
55
+ # Registers a plugin class under a name so it can be enabled in the
56
+ # initializer. Bundled plugins register themselves on load; plugin authors
57
+ # call this from their own code.
58
+ def register_plugin(name, plugin_class)
59
+ PluginRegistry.register(name, plugin_class)
60
+ end
61
+
62
+ # The enabled plugins as [name, plugin_class, options] triples, in the order
63
+ # they were enabled. plugin_class is nil if the name was never registered
64
+ # (validation surfaces that at boot).
65
+ def enabled_plugins
66
+ configuration.enabled_plugins.map do |name, opts|
67
+ [name, PluginRegistry[name], opts]
68
+ end
69
+ end
70
+
71
+ # Connections required by the enabled plugins and their tools, as
72
+ # [connection_name, requester_label] pairs, for ConnectionRegistry.validate!.
73
+ def required_connections
74
+ requirements = []
75
+ enabled_plugins.each do |name, plugin_class, _opts|
76
+ next unless plugin_class
77
+
78
+ Array(plugin_class.required_connections).each do |conn_name|
79
+ requirements << [conn_name, "Plugin #{name.inspect}"]
80
+ end
81
+ plugin_class.tools.each do |tool_class|
82
+ requirements << [tool_class.connection, "Tool #{tool_class.tool_name.inspect}"] if tool_class.connection
83
+ end
84
+ end
85
+ requirements
86
+ end
87
+
88
+ # Requires every .rb under app/talk_to_your_app/<subdir> so the files'
89
+ # side effects run (CustomTool subclasses register; health checks call
90
+ # Health.register). require is idempotent, so calling this repeatedly loads
91
+ # each file once per process (edits need a restart). No-op when the directory
92
+ # or Rails is absent. Each file is loaded in isolation: one file that fails
93
+ # to load is logged and skipped so it can't suppress the others. ScriptError
94
+ # is rescued alongside StandardError so a syntax/load error is reported the
95
+ # same way as a runtime error rather than escaping uncaught.
96
+ def require_app_dir(subdir)
97
+ return unless defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
98
+
99
+ dir = ::Rails.root.join(APP_DIR, subdir)
100
+ return unless File.directory?(dir)
101
+
102
+ Dir[File.join(dir, "**/*.rb")].sort.each do |file|
103
+ require file
104
+ rescue StandardError, ScriptError => e
105
+ message = "talk_to_your_app: failed to load #{file}: #{e.class}: #{e.message}"
106
+ logger = configuration.logger
107
+ logger ? logger.error(message) : $stderr.puts(message)
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ require_relative "talk_to_your_app/railtie" if defined?(Rails::Railtie)
114
+
115
+ # Bundled plugins self-register on load (after register_plugin is defined). They
116
+ # are off by default; the operator enables them in the initializer. Soft-dep
117
+ # constants are referenced only inside tool calls, so loading these files never
118
+ # requires the backing gem.
119
+ require_relative "talk_to_your_app/plugins/db/plugin"
120
+ require_relative "talk_to_your_app/plugins/jobs/plugin"
121
+ require_relative "talk_to_your_app/plugins/flipper/plugin"
122
+ require_relative "talk_to_your_app/plugins/health/plugin"
123
+ require_relative "talk_to_your_app/plugins/rake/plugin"
124
+ require_relative "talk_to_your_app/plugins/custom_tools/plugin"
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: talk_to_your_app
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre.1
5
+ platform: ruby
6
+ authors:
7
+ - Igor
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: mcp
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.18'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.18'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activerecord
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: railties
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '7.1'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '7.1'
54
+ description: |
55
+ A thin Rails layer over the official MCP Ruby SDK. Mounts a Streamable HTTP
56
+ MCP endpoint, gates access behind API-key or HTTP Basic auth, enforces
57
+ fail-closed per-tool database roles, and ships six bundled plugins
58
+ (DB, Jobs, Flipper, Health, Rake, Custom Tools) plus a Ruby DSL for
59
+ writing your own.
60
+ email:
61
+ - igor@everai.ai
62
+ executables: []
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - LICENSE
67
+ - README.md
68
+ - lib/generators/talk_to_your_app/custom_tool/custom_tool_generator.rb
69
+ - lib/generators/talk_to_your_app/custom_tool/templates/tool.rb.tt
70
+ - lib/generators/talk_to_your_app/health_check/health_check_generator.rb
71
+ - lib/generators/talk_to_your_app/health_check/templates/check.rb.tt
72
+ - lib/generators/talk_to_your_app/install/install_generator.rb
73
+ - lib/generators/talk_to_your_app/install/templates/initializer.rb.tt
74
+ - lib/talk_to_your_app.rb
75
+ - lib/talk_to_your_app/audit_logger.rb
76
+ - lib/talk_to_your_app/auth/api_key.rb
77
+ - lib/talk_to_your_app/auth/basic.rb
78
+ - lib/talk_to_your_app/auth/middleware.rb
79
+ - lib/talk_to_your_app/configuration.rb
80
+ - lib/talk_to_your_app/connection_registry.rb
81
+ - lib/talk_to_your_app/current.rb
82
+ - lib/talk_to_your_app/custom_tool.rb
83
+ - lib/talk_to_your_app/plugin.rb
84
+ - lib/talk_to_your_app/plugin_registry.rb
85
+ - lib/talk_to_your_app/plugins/custom_tools/plugin.rb
86
+ - lib/talk_to_your_app/plugins/db/plugin.rb
87
+ - lib/talk_to_your_app/plugins/db/tools/query.rb
88
+ - lib/talk_to_your_app/plugins/db/tools/schema.rb
89
+ - lib/talk_to_your_app/plugins/db/tools/tables.rb
90
+ - lib/talk_to_your_app/plugins/flipper/plugin.rb
91
+ - lib/talk_to_your_app/plugins/flipper/tools/disable_flag.rb
92
+ - lib/talk_to_your_app/plugins/flipper/tools/enable_flag.rb
93
+ - lib/talk_to_your_app/plugins/flipper/tools/enabled_flags.rb
94
+ - lib/talk_to_your_app/plugins/flipper/tools/list_flags.rb
95
+ - lib/talk_to_your_app/plugins/flipper/tools/read_flag.rb
96
+ - lib/talk_to_your_app/plugins/health/plugin.rb
97
+ - lib/talk_to_your_app/plugins/health/registry.rb
98
+ - lib/talk_to_your_app/plugins/health/tools/list_checks.rb
99
+ - lib/talk_to_your_app/plugins/health/tools/run_check.rb
100
+ - lib/talk_to_your_app/plugins/jobs/adapters/sidekiq.rb
101
+ - lib/talk_to_your_app/plugins/jobs/adapters/solid_queue.rb
102
+ - lib/talk_to_your_app/plugins/jobs/interface.rb
103
+ - lib/talk_to_your_app/plugins/jobs/plugin.rb
104
+ - lib/talk_to_your_app/plugins/jobs/tools/failed_jobs.rb
105
+ - lib/talk_to_your_app/plugins/jobs/tools/health.rb
106
+ - lib/talk_to_your_app/plugins/jobs/tools/queue_sizes.rb
107
+ - lib/talk_to_your_app/plugins/jobs/tools/rate_metrics.rb
108
+ - lib/talk_to_your_app/plugins/jobs/tools/recent_jobs.rb
109
+ - lib/talk_to_your_app/plugins/rake/plugin.rb
110
+ - lib/talk_to_your_app/plugins/rake/tools/run.rb
111
+ - lib/talk_to_your_app/railtie.rb
112
+ - lib/talk_to_your_app/renderers/html_table.rb
113
+ - lib/talk_to_your_app/tool.rb
114
+ - lib/talk_to_your_app/transport/rails_mount.rb
115
+ - lib/talk_to_your_app/version.rb
116
+ homepage: https://github.com/everai/talk_to_your_app
117
+ licenses:
118
+ - MIT
119
+ metadata:
120
+ homepage_uri: https://github.com/everai/talk_to_your_app
121
+ source_code_uri: https://github.com/everai/talk_to_your_app
122
+ rubygems_mfa_required: 'true'
123
+ rdoc_options: []
124
+ require_paths:
125
+ - lib
126
+ required_ruby_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '3.2'
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ requirements: []
137
+ rubygems_version: 3.7.2
138
+ specification_version: 4
139
+ summary: 'Rails-native MCP server: ask your running app real questions over MCP.'
140
+ test_files: []