rails-profiler 0.29.0 → 0.30.0

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/profiler-toolbar.js +18 -1
  3. data/app/assets/builds/profiler.js +93 -5
  4. data/app/controllers/profiler/api/cluster_controller.rb +35 -0
  5. data/app/controllers/profiler/api/profiles_controller.rb +8 -2
  6. data/app/controllers/profiler/api/slave_proxy_controller.rb +49 -0
  7. data/app/views/layouts/profiler/application.html.erb +3 -0
  8. data/config/routes.rb +10 -0
  9. data/lib/profiler/cluster/master_client.rb +79 -0
  10. data/lib/profiler/cluster/slave_proxy.rb +106 -0
  11. data/lib/profiler/cluster/slave_registry.rb +50 -0
  12. data/lib/profiler/configuration.rb +20 -1
  13. data/lib/profiler/mcp/server.rb +49 -20
  14. data/lib/profiler/mcp/slave_support.rb +23 -0
  15. data/lib/profiler/mcp/tools/analyze_queries.rb +5 -2
  16. data/lib/profiler/mcp/tools/clear_profiles.rb +11 -1
  17. data/lib/profiler/mcp/tools/delete_env_var.rb +7 -0
  18. data/lib/profiler/mcp/tools/explain_query.rb +8 -0
  19. data/lib/profiler/mcp/tools/get_profile_ajax.rb +5 -2
  20. data/lib/profiler/mcp/tools/get_profile_detail.rb +5 -2
  21. data/lib/profiler/mcp/tools/get_profile_dumps.rb +5 -2
  22. data/lib/profiler/mcp/tools/get_profile_http.rb +5 -2
  23. data/lib/profiler/mcp/tools/get_profile_mailers.rb +5 -2
  24. data/lib/profiler/mcp/tools/get_test_profile_detail.rb +5 -2
  25. data/lib/profiler/mcp/tools/list_env_vars.rb +38 -0
  26. data/lib/profiler/mcp/tools/list_slaves.rb +27 -0
  27. data/lib/profiler/mcp/tools/query_console_profiles.rb +4 -1
  28. data/lib/profiler/mcp/tools/query_jobs.rb +4 -1
  29. data/lib/profiler/mcp/tools/query_mailers.rb +4 -1
  30. data/lib/profiler/mcp/tools/query_profiles.rb +4 -1
  31. data/lib/profiler/mcp/tools/query_test_profiles.rb +4 -1
  32. data/lib/profiler/mcp/tools/reset_all_env_vars.rb +8 -1
  33. data/lib/profiler/mcp/tools/reset_env_var.rb +9 -0
  34. data/lib/profiler/mcp/tools/run_tests.rb +41 -0
  35. data/lib/profiler/mcp/tools/set_env_var.rb +7 -0
  36. data/lib/profiler/railtie.rb +11 -0
  37. data/lib/profiler/version.rb +1 -1
  38. data/lib/profiler.rb +7 -0
  39. metadata +8 -1
@@ -59,6 +59,7 @@ module Profiler
59
59
  private
60
60
 
61
61
  def build_tools
62
+ require_relative "slave_support"
62
63
  require_relative "file_cache"
63
64
  require_relative "path_extractor"
64
65
  require_relative "body_formatter"
@@ -82,6 +83,9 @@ module Profiler
82
83
  require_relative "tools/delete_env_var"
83
84
  require_relative "tools/reset_env_var"
84
85
  require_relative "tools/reset_all_env_vars"
86
+ require_relative "tools/list_slaves"
87
+
88
+ slave_param = { type: "string", description: "Name of a connected slave profiler to target. Omit to use this profiler's own data." }
85
89
 
86
90
  [
87
91
  define_tool(
@@ -95,7 +99,8 @@ module Profiler
95
99
  profile_type: { type: "string", description: "Filter by type: 'http' or 'job'" },
96
100
  limit: { type: "number", description: "Maximum number of results (default 20)" },
97
101
  fields: { type: "array", items: { type: "string" }, description: "Columns to include. Valid values: time, type, method, path, duration, queries, status, token. Omit for all." },
98
- cursor: { type: "string", description: "Pagination cursor: ISO8601 timestamp of the last item seen. Returns profiles older than this." }
102
+ cursor: { type: "string", description: "Pagination cursor: ISO8601 timestamp of the last item seen. Returns profiles older than this." },
103
+ slave: slave_param
99
104
  }
100
105
  },
101
106
  handler: Tools::QueryProfiles
@@ -112,7 +117,8 @@ module Profiler
112
117
  json_path: { type: "string", description: "JSONPath expression to extract from response body (e.g. '$.data.items[0]'). Only applied when save_bodies is true." },
113
118
  xml_path: { type: "string", description: "XPath expression to extract from response body (e.g. '//items/item[1]/name'). Only applied when save_bodies is true." },
114
119
  log_min_level: { type: "string", description: "Minimum log level to include in the logs section: DEBUG, INFO, WARN, ERROR, FATAL. Only applied when 'logs' section is requested." },
115
- env_filter: { type: "string", description: "Required when requesting the env section. Case-insensitive substring filter on ENV key name (e.g. 'RAILS', 'DATABASE')." }
120
+ env_filter: { type: "string", description: "Required when requesting the env section. Case-insensitive substring filter on ENV key name (e.g. 'RAILS', 'DATABASE')." },
121
+ slave: slave_param
116
122
  },
117
123
  required: ["token"]
118
124
  },
@@ -124,7 +130,8 @@ module Profiler
124
130
  input_schema: {
125
131
  properties: {
126
132
  token: { type: "string", description: "Profile token, or 'latest' for the most recent profile (required)" },
127
- summary_only: { type: "boolean", description: "Return only the summary statistics section, skipping slow query and N+1 details." }
133
+ summary_only: { type: "boolean", description: "Return only the summary statistics section, skipping slow query and N+1 details." },
134
+ slave: slave_param
128
135
  },
129
136
  required: ["token"]
130
137
  },
@@ -136,7 +143,8 @@ module Profiler
136
143
  input_schema: {
137
144
  properties: {
138
145
  token: { type: "string", description: "Profile token, or 'latest' for the most recent profile (required)" },
139
- query_index: { type: "integer", description: "Zero-based index of the query within the profile's database queries list (required)" }
146
+ query_index: { type: "integer", description: "Zero-based index of the query within the profile's database queries list (required)" },
147
+ slave: slave_param
140
148
  },
141
149
  required: ["token", "query_index"]
142
150
  },
@@ -147,7 +155,8 @@ module Profiler
147
155
  description: "Get detailed AJAX sub-request breakdown for a profile. Use 'latest' as token to get the most recent profile.",
148
156
  input_schema: {
149
157
  properties: {
150
- token: { type: "string", description: "Profile token, or 'latest' for the most recent profile (required)" }
158
+ token: { type: "string", description: "Profile token, or 'latest' for the most recent profile (required)" },
159
+ slave: slave_param
151
160
  },
152
161
  required: ["token"]
153
162
  },
@@ -158,7 +167,8 @@ module Profiler
158
167
  description: "Get variable dumps captured during a profile. Use 'latest' as token to get the most recent profile.",
159
168
  input_schema: {
160
169
  properties: {
161
- token: { type: "string", description: "Profile token, or 'latest' for the most recent profile (required)" }
170
+ token: { type: "string", description: "Profile token, or 'latest' for the most recent profile (required)" },
171
+ slave: slave_param
162
172
  },
163
173
  required: ["token"]
164
174
  },
@@ -174,7 +184,8 @@ module Profiler
174
184
  save_bodies: { type: "boolean", description: "Save request/response bodies to temp files and return paths instead of inlining content." },
175
185
  max_body_size: { type: "number", description: "Truncate inlined body content at N characters. Ignored when save_bodies is true." },
176
186
  json_path: { type: "string", description: "JSONPath expression to extract from response bodies (e.g. '$.data.items[0]'). Only applied when save_bodies is true." },
177
- xml_path: { type: "string", description: "XPath expression to extract from response bodies (e.g. '//items/item[1]/name'). Only applied when save_bodies is true." }
187
+ xml_path: { type: "string", description: "XPath expression to extract from response bodies (e.g. '//items/item[1]/name'). Only applied when save_bodies is true." },
188
+ slave: slave_param
178
189
  },
179
190
  required: ["token"]
180
191
  },
@@ -190,7 +201,8 @@ module Profiler
190
201
  action: { type: "string", description: "Filter by mailer action (partial match, e.g. 'welcome_email')" },
191
202
  delivery_mode: { type: "string", description: "Filter by delivery mode: 'deliver_now', 'deliver_later', or 'queued'" },
192
203
  save_bodies: { type: "boolean", description: "Save email bodies to temp files and return paths instead of inlining content." },
193
- max_body_size: { type: "number", description: "Truncate inlined body content at N characters." }
204
+ max_body_size: { type: "number", description: "Truncate inlined body content at N characters." },
205
+ slave: slave_param
194
206
  },
195
207
  required: ["token"]
196
208
  },
@@ -205,7 +217,8 @@ module Profiler
205
217
  status: { type: "string", description: "Filter by status (completed, failed)" },
206
218
  limit: { type: "number", description: "Maximum number of results (default 20)" },
207
219
  fields: { type: "array", items: { type: "string" }, description: "Columns to include. Valid values: time, job_class, queue, status, duration, token. Omit for all." },
208
- cursor: { type: "string", description: "Pagination cursor: ISO8601 timestamp of the last item seen. Returns jobs older than this." }
220
+ cursor: { type: "string", description: "Pagination cursor: ISO8601 timestamp of the last item seen. Returns jobs older than this." },
221
+ slave: slave_param
209
222
  }
210
223
  },
211
224
  handler: Tools::QueryJobs
@@ -221,7 +234,8 @@ module Profiler
221
234
  has_error: { type: "boolean", description: "Filter to only emails with delivery errors" },
222
235
  limit: { type: "number", description: "Maximum number of results (default 20)" },
223
236
  fields: { type: "array", items: { type: "string" }, description: "Columns to include. Valid values: time, profile, mailer, action, subject, to, mode, duration, status, token. Omit for all." },
224
- cursor: { type: "string", description: "Pagination cursor: ISO8601 timestamp of the last item seen." }
237
+ cursor: { type: "string", description: "Pagination cursor: ISO8601 timestamp of the last item seen." },
238
+ slave: slave_param
225
239
  }
226
240
  },
227
241
  handler: Tools::QueryMailers
@@ -236,7 +250,8 @@ module Profiler
236
250
  min_duration: { type: "number", description: "Minimum duration in milliseconds" },
237
251
  limit: { type: "number", description: "Maximum number of results (default 20)" },
238
252
  fields: { type: "array", items: { type: "string" }, description: "Columns to include. Valid values: time, test_name, status, duration, queries, n1, token. Omit for all." },
239
- cursor: { type: "string", description: "Pagination cursor: ISO8601 timestamp of the last item seen." }
253
+ cursor: { type: "string", description: "Pagination cursor: ISO8601 timestamp of the last item seen." },
254
+ slave: slave_param
240
255
  }
241
256
  },
242
257
  handler: Tools::QueryTestProfiles
@@ -246,7 +261,8 @@ module Profiler
246
261
  description: "Get detailed data for a test profile: metadata, SQL queries, N+1 patterns, cache, exception. Use 'latest' as token for the most recent test.",
247
262
  input_schema: {
248
263
  properties: {
249
- token: { type: "string", description: "Test profile token, or 'latest' for the most recent test profile (required)" }
264
+ token: { type: "string", description: "Test profile token, or 'latest' for the most recent test profile (required)" },
265
+ slave: slave_param
250
266
  },
251
267
  required: ["token"]
252
268
  },
@@ -273,7 +289,8 @@ module Profiler
273
289
  max_output: {
274
290
  type: "number",
275
291
  description: "Maximum characters of output to return (tail). Default: 4000."
276
- }
292
+ },
293
+ slave: slave_param
277
294
  }
278
295
  },
279
296
  handler: Tools::RunTests
@@ -288,7 +305,8 @@ module Profiler
288
305
  min_duration: { type: "number", description: "Minimum duration in milliseconds" },
289
306
  limit: { type: "number", description: "Maximum number of results (default 20)" },
290
307
  fields: { type: "array", items: { type: "string" }, description: "Columns to include. Valid values: time, expression, return_value, status, duration, queries, token. Omit for all." },
291
- cursor: { type: "string", description: "Pagination cursor: ISO8601 timestamp of the last item seen. Returns profiles older than this." }
308
+ cursor: { type: "string", description: "Pagination cursor: ISO8601 timestamp of the last item seen. Returns profiles older than this." },
309
+ slave: slave_param
292
310
  }
293
311
  },
294
312
  handler: Tools::QueryConsoleProfiles
@@ -298,7 +316,8 @@ module Profiler
298
316
  description: "Clear profiler history. Omit type to clear everything, or pass 'http', 'job', 'test', or 'console' to clear only that type.",
299
317
  input_schema: {
300
318
  properties: {
301
- type: { type: "string", description: "Optional: 'http' to clear only requests, 'job' to clear only jobs, 'test' to clear only test profiles, 'console' to clear only console sessions" }
319
+ type: { type: "string", description: "Optional: 'http' to clear only requests, 'job' to clear only jobs, 'test' to clear only test profiles, 'console' to clear only console sessions" },
320
+ slave: slave_param
302
321
  }
303
322
  },
304
323
  handler: Tools::ClearProfiles
@@ -309,7 +328,8 @@ module Profiler
309
328
  input_schema: {
310
329
  properties: {
311
330
  include_all: { type: "boolean", description: "If true, return all ENV variables (not just overrides). Default: false." },
312
- filter: { type: "string", description: "Case-insensitive substring filter on key name." }
331
+ filter: { type: "string", description: "Case-insensitive substring filter on key name." },
332
+ slave: slave_param
313
333
  }
314
334
  },
315
335
  handler: Tools::ListEnvVars
@@ -320,7 +340,8 @@ module Profiler
320
340
  input_schema: {
321
341
  properties: {
322
342
  key: { type: "string", description: "Environment variable name (required)" },
323
- value: { type: "string", description: "New value (required)" }
343
+ value: { type: "string", description: "New value (required)" },
344
+ slave: slave_param
324
345
  },
325
346
  required: ["key", "value"]
326
347
  },
@@ -331,7 +352,8 @@ module Profiler
331
352
  description: "Delete an environment variable for this session (persisted across restarts until reset).",
332
353
  input_schema: {
333
354
  properties: {
334
- key: { type: "string", description: "Environment variable name (required)" }
355
+ key: { type: "string", description: "Environment variable name (required)" },
356
+ slave: slave_param
335
357
  },
336
358
  required: ["key"]
337
359
  },
@@ -342,7 +364,8 @@ module Profiler
342
364
  description: "Restore an overridden environment variable to its original value.",
343
365
  input_schema: {
344
366
  properties: {
345
- key: { type: "string", description: "Environment variable name to restore (required)" }
367
+ key: { type: "string", description: "Environment variable name to restore (required)" },
368
+ slave: slave_param
346
369
  },
347
370
  required: ["key"]
348
371
  },
@@ -351,8 +374,14 @@ module Profiler
351
374
  define_tool(
352
375
  name: "reset_all_env_vars",
353
376
  description: "Restore all overridden environment variables to their original values.",
354
- input_schema: { properties: {} },
377
+ input_schema: { properties: { slave: slave_param } },
355
378
  handler: Tools::ResetAllEnvVars
379
+ ),
380
+ define_tool(
381
+ name: "list_slaves",
382
+ description: "List all slave profilers connected to this master profiler, with their connection status.",
383
+ input_schema: { properties: {} },
384
+ handler: Tools::ListSlaves
356
385
  )
357
386
  ]
358
387
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Profiler
4
+ module MCP
5
+ module SlaveSupport
6
+ def self.resolve_storage(params)
7
+ if (slave_name = params["slave"])
8
+ require_relative "../cluster/slave_proxy"
9
+ Cluster::SlaveProxy.new(slave_name)
10
+ else
11
+ Profiler.storage
12
+ end
13
+ end
14
+
15
+ def self.with_slave_proxy(params)
16
+ return nil unless params["slave"]
17
+
18
+ require_relative "../cluster/slave_proxy"
19
+ Cluster::SlaveProxy.new(params["slave"])
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  module Profiler
4
6
  module MCP
5
7
  module Tools
@@ -15,10 +17,11 @@ module Profiler
15
17
  ]
16
18
  end
17
19
 
20
+ storage = MCP::SlaveSupport.resolve_storage(params)
18
21
  profile = if token == "latest"
19
- Profiler.storage.list(limit: 1).first
22
+ storage.list(limit: 1).first
20
23
  else
21
- Profiler.storage.load(token)
24
+ storage.load(token)
22
25
  end
23
26
  unless profile
24
27
  return [
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
5
+ require "uri"
6
+
3
7
  module Profiler
4
8
  module MCP
5
9
  module Tools
@@ -11,7 +15,13 @@ module Profiler
11
15
  return [{ type: "text", text: "Error: type must be 'http', 'job', 'test', or 'console'" }]
12
16
  end
13
17
 
14
- Profiler.storage.clear(type: type)
18
+ if (proxy = MCP::SlaveSupport.with_slave_proxy(params))
19
+ path = "/_profiler/api/profiles/clear"
20
+ path += "?type=#{URI.encode_www_form_component(type)}" if type
21
+ proxy.delete_json(path)
22
+ else
23
+ Profiler.storage.clear(type: type)
24
+ end
15
25
 
16
26
  label = type ? "#{type} profiles" : "all profiles"
17
27
  [{ type: "text", text: "Cleared #{label}." }]
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  module Profiler
4
6
  module MCP
5
7
  module Tools
@@ -9,6 +11,11 @@ module Profiler
9
11
 
10
12
  return [{ type: "text", text: "Error: key cannot be blank." }] if key.empty?
11
13
 
14
+ if (proxy = MCP::SlaveSupport.with_slave_proxy(params))
15
+ proxy.patch_json("/_profiler/api/env_vars", { key: key, value: "" })
16
+ return [{ type: "text", text: "Deleted #{key} on slave '#{params["slave"]}'. Override persisted across restarts until reset." }]
17
+ end
18
+
12
19
  Profiler.env_override_store.delete(key)
13
20
  ENV.delete(key)
14
21
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  require "profiler/explain_runner"
4
6
 
5
7
  module Profiler
@@ -18,6 +20,12 @@ module Profiler
18
20
  return [{ type: "text", text: "Error: query_index parameter is required" }]
19
21
  end
20
22
 
23
+ if (proxy = MCP::SlaveSupport.with_slave_proxy(params))
24
+ result = proxy.post_json("/_profiler/api/explain", { token: token, query_index: query_index.to_i })
25
+ text = result["error"] ? "Error: #{result["error"]}" : result.inspect
26
+ return [{ type: "text", text: text }]
27
+ end
28
+
21
29
  unless Profiler.configuration.enabled
22
30
  return [{ type: "text", text: "Error: EXPLAIN is only available when the profiler is enabled" }]
23
31
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  module Profiler
4
6
  module MCP
5
7
  module Tools
@@ -10,10 +12,11 @@ module Profiler
10
12
  return [{ type: "text", text: "Error: token parameter is required" }]
11
13
  end
12
14
 
15
+ storage = MCP::SlaveSupport.resolve_storage(params)
13
16
  profile = if token == "latest"
14
- Profiler.storage.list(limit: 1).first
17
+ storage.list(limit: 1).first
15
18
  else
16
- Profiler.storage.load(token)
19
+ storage.load(token)
17
20
  end
18
21
  unless profile
19
22
  return [{ type: "text", text: "Profile not found: #{token}" }]
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  require "shellwords"
4
6
  require "cgi"
5
7
 
@@ -18,10 +20,11 @@ module Profiler
18
20
  ]
19
21
  end
20
22
 
23
+ storage = MCP::SlaveSupport.resolve_storage(params)
21
24
  profile = if token == "latest"
22
- Profiler.storage.list(limit: 1).first
25
+ storage.list(limit: 1).first
23
26
  else
24
- Profiler.storage.load(token)
27
+ storage.load(token)
25
28
  end
26
29
  unless profile
27
30
  return [
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  module Profiler
4
6
  module MCP
5
7
  module Tools
@@ -10,10 +12,11 @@ module Profiler
10
12
  return [{ type: "text", text: "Error: token parameter is required" }]
11
13
  end
12
14
 
15
+ storage = MCP::SlaveSupport.resolve_storage(params)
13
16
  profile = if token == "latest"
14
- Profiler.storage.list(limit: 1).first
17
+ storage.list(limit: 1).first
15
18
  else
16
- Profiler.storage.load(token)
19
+ storage.load(token)
17
20
  end
18
21
  unless profile
19
22
  return [{ type: "text", text: "Profile not found: #{token}" }]
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  require "shellwords"
4
6
  require "uri"
5
7
 
@@ -13,10 +15,11 @@ module Profiler
13
15
  return [{ type: "text", text: "Error: token parameter is required" }]
14
16
  end
15
17
 
18
+ storage = MCP::SlaveSupport.resolve_storage(params)
16
19
  profile = if token == "latest"
17
- Profiler.storage.list(limit: 1).first
20
+ storage.list(limit: 1).first
18
21
  else
19
- Profiler.storage.load(token)
22
+ storage.load(token)
20
23
  end
21
24
  unless profile
22
25
  return [{ type: "text", text: "Profile not found: #{token}" }]
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  module Profiler
4
6
  module MCP
5
7
  module Tools
@@ -12,10 +14,11 @@ module Profiler
12
14
  return [{ type: "text", text: "Error: token parameter is required" }]
13
15
  end
14
16
 
17
+ storage = MCP::SlaveSupport.resolve_storage(params)
15
18
  profile = if token == "latest"
16
- Profiler.storage.list(limit: 1).first
19
+ storage.list(limit: 1).first
17
20
  else
18
- Profiler.storage.load(token)
21
+ storage.load(token)
19
22
  end
20
23
  unless profile
21
24
  return [{ type: "text", text: "Profile not found: #{token}" }]
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  module Profiler
4
6
  module MCP
5
7
  module Tools
@@ -10,11 +12,12 @@ module Profiler
10
12
  return [{ type: "text", text: "Error: token parameter is required" }]
11
13
  end
12
14
 
15
+ storage = MCP::SlaveSupport.resolve_storage(params)
13
16
  profile = if token == "latest"
14
- profiles = Profiler.storage.list(limit: 200)
17
+ profiles = storage.list(limit: 200)
15
18
  profiles.find { |p| p.profile_type == "test" }
16
19
  else
17
- Profiler.storage.load(token)
20
+ storage.load(token)
18
21
  end
19
22
 
20
23
  unless profile && profile.profile_type == "test"
@@ -1,10 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  module Profiler
4
6
  module MCP
5
7
  module Tools
6
8
  class ListEnvVars
7
9
  def self.call(params)
10
+ if (proxy = MCP::SlaveSupport.with_slave_proxy(params))
11
+ data = proxy.get_json("/_profiler/api/env_vars",
12
+ include_all: params["include_all"], filter: params["filter"])
13
+ text = format_slave_env_vars(data, params)
14
+ return [{ type: "text", text: text }]
15
+ end
16
+
8
17
  include_all = params["include_all"]
9
18
  filter = params["filter"]&.downcase
10
19
 
@@ -51,6 +60,35 @@ module Profiler
51
60
 
52
61
  [{ type: "text", text: text }]
53
62
  end
63
+
64
+ private
65
+
66
+ def self.format_slave_env_vars(data, params)
67
+ overrides = data["overrides"] || {}
68
+ filter = params["filter"]&.downcase
69
+
70
+ if params["include_all"]
71
+ vars = (data["variables"] || {}).sort.to_h
72
+ vars = vars.select { |k, _| k.downcase.include?(filter) } if filter
73
+ return "No environment variables match filter '#{params["filter"]}'." if vars.empty?
74
+
75
+ rows = vars.map do |key, value|
76
+ override = overrides[key]
77
+ overridden = override ? "✓ (was: #{override["original"] || "(unset)"})" : ""
78
+ "| #{key} | #{value} | #{overridden} |"
79
+ end
80
+ "**ENV variables (#{vars.size})**\n\n| Key | Current Value | Overridden |\n|-----|--------------|------------|\n" + rows.join("\n")
81
+ else
82
+ overrides = overrides.select { |k, _| k.downcase.include?(filter) } if filter
83
+ return "No overrides active." if overrides.empty?
84
+
85
+ rows = overrides.map do |key, entry|
86
+ current = entry["value"] == "__PROFILER_DELETED__" ? "(deleted)" : entry["value"]
87
+ "| #{key} | #{current} | #{entry["original"] || "(unset)"} |"
88
+ end
89
+ "**Active ENV overrides (#{overrides.size})**\n\n| Key | Current Value | Original Value |\n|-----|--------------|----------------|\n" + rows.join("\n")
90
+ end
91
+ end
54
92
  end
55
93
  end
56
94
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Profiler
4
+ module MCP
5
+ module Tools
6
+ class ListSlaves
7
+ def self.call(_params)
8
+ slaves = Profiler.slave_registry.all
9
+
10
+ if slaves.empty?
11
+ return [{ type: "text", text: "No slave profilers connected." }]
12
+ end
13
+
14
+ lines = ["# Connected Slave Profilers\n"]
15
+ lines << "| Name | URL | Status | Registered At | Last Heartbeat |"
16
+ lines << "|------|-----|--------|--------------|----------------|"
17
+
18
+ slaves.each do |s|
19
+ lines << "| #{s[:name]} | #{s[:url]} | #{s[:status]} | #{s[:registered_at]} | #{s[:last_heartbeat_at]} |"
20
+ end
21
+
22
+ [{ type: "text", text: lines.join("\n") }]
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  module Profiler
4
6
  module MCP
5
7
  module Tools
@@ -9,7 +11,8 @@ module Profiler
9
11
  def self.call(params)
10
12
  limit = params["limit"]&.to_i || 20
11
13
  fetch_size = [limit * 5, 500].min
12
- profiles = Profiler.storage.list(limit: fetch_size)
14
+ storage = MCP::SlaveSupport.resolve_storage(params)
15
+ profiles = storage.list(limit: fetch_size)
13
16
 
14
17
  consoles = profiles.select { |p| p.profile_type == "console" }
15
18
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  module Profiler
4
6
  module MCP
5
7
  module Tools
@@ -9,7 +11,8 @@ module Profiler
9
11
  def self.call(params)
10
12
  limit = params["limit"]&.to_i || 20
11
13
  fetch_size = [limit * 5, 500].min
12
- profiles = Profiler.storage.list(limit: fetch_size)
14
+ storage = MCP::SlaveSupport.resolve_storage(params)
15
+ profiles = storage.list(limit: fetch_size)
13
16
 
14
17
  jobs = profiles.select { |p| p.profile_type == "job" }
15
18
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  module Profiler
4
6
  module MCP
5
7
  module Tools
@@ -10,7 +12,8 @@ module Profiler
10
12
  def call(params)
11
13
  limit = params["limit"]&.to_i || 20
12
14
  fetch_size = [limit * 10, 1000].min
13
- profiles = Profiler.storage.list(limit: fetch_size)
15
+ storage = MCP::SlaveSupport.resolve_storage(params)
16
+ profiles = storage.list(limit: fetch_size)
14
17
 
15
18
  emails = []
16
19
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  module Profiler
4
6
  module MCP
5
7
  module Tools
@@ -7,9 +9,10 @@ module Profiler
7
9
  ALL_FIELDS = %w[time type method path duration queries status token].freeze
8
10
 
9
11
  def self.call(params)
12
+ storage = MCP::SlaveSupport.resolve_storage(params)
10
13
  limit = params["limit"]&.to_i || 20
11
14
  fetch_size = [limit * 5, 500].min
12
- profiles = Profiler.storage.list(limit: fetch_size)
15
+ profiles = storage.list(limit: fetch_size)
13
16
 
14
17
  profiles = profiles.select { |p| p.path&.include?(params["path"]) } if params["path"]
15
18
  profiles = profiles.select { |p| p.method == params["method"]&.upcase } if params["method"]
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../slave_support"
4
+
3
5
  module Profiler
4
6
  module MCP
5
7
  module Tools
@@ -9,7 +11,8 @@ module Profiler
9
11
  def self.call(params)
10
12
  limit = params["limit"]&.to_i || 20
11
13
  fetch_size = [limit * 5, 500].min
12
- profiles = Profiler.storage.list(limit: fetch_size)
14
+ storage = MCP::SlaveSupport.resolve_storage(params)
15
+ profiles = storage.list(limit: fetch_size)
13
16
 
14
17
  tests = profiles.select { |p| p.profile_type == "test" }
15
18