crucible 0.1.2
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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +102 -0
- data/Gemfile +10 -0
- data/LICENSE +21 -0
- data/README.md +366 -0
- data/Rakefile +23 -0
- data/TESTING.md +319 -0
- data/config.sample.yml +48 -0
- data/crucible.gemspec +48 -0
- data/exe/crucible +122 -0
- data/lib/crucible/configuration.rb +212 -0
- data/lib/crucible/server.rb +123 -0
- data/lib/crucible/session_manager.rb +209 -0
- data/lib/crucible/stealth/evasions/chrome_app.js +75 -0
- data/lib/crucible/stealth/evasions/chrome_csi.js +33 -0
- data/lib/crucible/stealth/evasions/chrome_load_times.js +44 -0
- data/lib/crucible/stealth/evasions/chrome_runtime.js +190 -0
- data/lib/crucible/stealth/evasions/iframe_content_window.js +101 -0
- data/lib/crucible/stealth/evasions/media_codecs.js +65 -0
- data/lib/crucible/stealth/evasions/navigator_hardware_concurrency.js +18 -0
- data/lib/crucible/stealth/evasions/navigator_languages.js +18 -0
- data/lib/crucible/stealth/evasions/navigator_permissions.js +53 -0
- data/lib/crucible/stealth/evasions/navigator_plugins.js +261 -0
- data/lib/crucible/stealth/evasions/navigator_vendor.js +18 -0
- data/lib/crucible/stealth/evasions/navigator_webdriver.js +16 -0
- data/lib/crucible/stealth/evasions/webgl_vendor.js +43 -0
- data/lib/crucible/stealth/evasions/window_outerdimensions.js +18 -0
- data/lib/crucible/stealth/utils.js +266 -0
- data/lib/crucible/stealth.rb +213 -0
- data/lib/crucible/tools/cookies.rb +206 -0
- data/lib/crucible/tools/downloads.rb +273 -0
- data/lib/crucible/tools/extraction.rb +335 -0
- data/lib/crucible/tools/helpers.rb +46 -0
- data/lib/crucible/tools/interaction.rb +355 -0
- data/lib/crucible/tools/navigation.rb +181 -0
- data/lib/crucible/tools/sessions.rb +85 -0
- data/lib/crucible/tools/stealth.rb +167 -0
- data/lib/crucible/tools.rb +42 -0
- data/lib/crucible/version.rb +5 -0
- data/lib/crucible.rb +60 -0
- metadata +201 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mcp'
|
|
4
|
+
|
|
5
|
+
module Crucible
|
|
6
|
+
module Tools
|
|
7
|
+
# Interaction tools: click, type, fill_form, select_option, scroll, hover
|
|
8
|
+
module Interaction
|
|
9
|
+
class << self
|
|
10
|
+
def tools(sessions, _config)
|
|
11
|
+
[
|
|
12
|
+
click_tool(sessions),
|
|
13
|
+
type_tool(sessions),
|
|
14
|
+
fill_form_tool(sessions),
|
|
15
|
+
select_option_tool(sessions),
|
|
16
|
+
scroll_tool(sessions),
|
|
17
|
+
hover_tool(sessions)
|
|
18
|
+
]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def click_tool(sessions)
|
|
24
|
+
MCP::Tool.define(
|
|
25
|
+
name: 'click',
|
|
26
|
+
description: 'Click an element on the page',
|
|
27
|
+
input_schema: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
session: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'Session name',
|
|
33
|
+
default: 'default'
|
|
34
|
+
},
|
|
35
|
+
selector: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'CSS selector for the element to click'
|
|
38
|
+
},
|
|
39
|
+
button: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'Mouse button to use',
|
|
42
|
+
enum: %w[left right middle],
|
|
43
|
+
default: 'left'
|
|
44
|
+
},
|
|
45
|
+
count: {
|
|
46
|
+
type: 'integer',
|
|
47
|
+
description: 'Number of clicks (1 for single, 2 for double)',
|
|
48
|
+
default: 1
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
required: ['selector']
|
|
52
|
+
}
|
|
53
|
+
) do |selector:, session: 'default', button: 'left', count: 1, **|
|
|
54
|
+
page = sessions.page(session)
|
|
55
|
+
element = page.at_css(selector)
|
|
56
|
+
|
|
57
|
+
raise ElementNotFoundError, "Element not found: #{selector}" unless element
|
|
58
|
+
|
|
59
|
+
# Ferrum uses mode: for click type - :left, :right, or :double
|
|
60
|
+
mode = if count == 2
|
|
61
|
+
:double
|
|
62
|
+
else
|
|
63
|
+
button.to_sym
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
element.click(mode: mode)
|
|
67
|
+
|
|
68
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Clicked: #{selector}" }])
|
|
69
|
+
rescue ElementNotFoundError => e
|
|
70
|
+
MCP::Tool::Response.new([{ type: 'text', text: e.message }], error: true)
|
|
71
|
+
rescue Ferrum::Error => e
|
|
72
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Click failed: #{e.message}" }], error: true)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def type_tool(sessions)
|
|
77
|
+
MCP::Tool.define(
|
|
78
|
+
name: 'type',
|
|
79
|
+
description: 'Type text into an input element',
|
|
80
|
+
input_schema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
session: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'Session name',
|
|
86
|
+
default: 'default'
|
|
87
|
+
},
|
|
88
|
+
selector: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'CSS selector for the input element'
|
|
91
|
+
},
|
|
92
|
+
text: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description: 'Text to type'
|
|
95
|
+
},
|
|
96
|
+
clear: {
|
|
97
|
+
type: 'boolean',
|
|
98
|
+
description: 'Clear the field before typing',
|
|
99
|
+
default: false
|
|
100
|
+
},
|
|
101
|
+
submit: {
|
|
102
|
+
type: 'boolean',
|
|
103
|
+
description: 'Press Enter after typing',
|
|
104
|
+
default: false
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
required: %w[selector text]
|
|
108
|
+
}
|
|
109
|
+
) do |selector:, text:, session: 'default', clear: false, submit: false, **|
|
|
110
|
+
page = sessions.page(session)
|
|
111
|
+
element = page.at_css(selector)
|
|
112
|
+
|
|
113
|
+
raise ElementNotFoundError, "Element not found: #{selector}" unless element
|
|
114
|
+
|
|
115
|
+
element.focus
|
|
116
|
+
|
|
117
|
+
if clear
|
|
118
|
+
# Select all and delete (use meta on macOS, control elsewhere)
|
|
119
|
+
modifier = RUBY_PLATFORM.include?('darwin') ? :meta : :control
|
|
120
|
+
element.type([modifier, 'a'], [:backspace])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if submit
|
|
124
|
+
element.type(text, :Enter)
|
|
125
|
+
else
|
|
126
|
+
element.type(text)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Typed into: #{selector}" }])
|
|
130
|
+
rescue ElementNotFoundError => e
|
|
131
|
+
MCP::Tool::Response.new([{ type: 'text', text: e.message }], error: true)
|
|
132
|
+
rescue Ferrum::Error => e
|
|
133
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Type failed: #{e.message}" }], error: true)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def fill_form_tool(sessions)
|
|
138
|
+
MCP::Tool.define(
|
|
139
|
+
name: 'fill_form',
|
|
140
|
+
description: 'Fill multiple form fields at once',
|
|
141
|
+
input_schema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
session: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
description: 'Session name',
|
|
147
|
+
default: 'default'
|
|
148
|
+
},
|
|
149
|
+
fields: {
|
|
150
|
+
type: 'array',
|
|
151
|
+
description: 'Array of fields to fill',
|
|
152
|
+
items: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
selector: {
|
|
156
|
+
type: 'string',
|
|
157
|
+
description: 'CSS selector for the field'
|
|
158
|
+
},
|
|
159
|
+
value: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
description: 'Value to enter'
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
required: %w[selector value]
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
required: ['fields']
|
|
169
|
+
}
|
|
170
|
+
) do |fields:, session: 'default', **|
|
|
171
|
+
page = sessions.page(session)
|
|
172
|
+
filled = []
|
|
173
|
+
errors = []
|
|
174
|
+
|
|
175
|
+
fields.each do |field|
|
|
176
|
+
field_selector = field[:selector] || field['selector']
|
|
177
|
+
value = field[:value] || field['value']
|
|
178
|
+
|
|
179
|
+
element = page.at_css(field_selector)
|
|
180
|
+
if element
|
|
181
|
+
element.focus
|
|
182
|
+
# Clear first (use meta on macOS, control elsewhere)
|
|
183
|
+
modifier = RUBY_PLATFORM.include?('darwin') ? :meta : :control
|
|
184
|
+
element.type([modifier, 'a'], [:backspace])
|
|
185
|
+
element.type(value)
|
|
186
|
+
filled << field_selector
|
|
187
|
+
else
|
|
188
|
+
errors << field_selector
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
message = "Filled #{filled.size} field(s)"
|
|
193
|
+
message += ". Not found: #{errors.join(', ')}" if errors.any?
|
|
194
|
+
|
|
195
|
+
if errors.any? && filled.empty?
|
|
196
|
+
MCP::Tool::Response.new([{ type: 'text', text: message }], error: true)
|
|
197
|
+
else
|
|
198
|
+
MCP::Tool::Response.new([{ type: 'text', text: message }])
|
|
199
|
+
end
|
|
200
|
+
rescue Ferrum::Error => e
|
|
201
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Fill form failed: #{e.message}" }], error: true)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def select_option_tool(sessions)
|
|
206
|
+
MCP::Tool.define(
|
|
207
|
+
name: 'select_option',
|
|
208
|
+
description: 'Select an option from a dropdown/select element',
|
|
209
|
+
input_schema: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
session: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
description: 'Session name',
|
|
215
|
+
default: 'default'
|
|
216
|
+
},
|
|
217
|
+
selector: {
|
|
218
|
+
type: 'string',
|
|
219
|
+
description: 'CSS selector for the select element'
|
|
220
|
+
},
|
|
221
|
+
value: {
|
|
222
|
+
type: 'string',
|
|
223
|
+
description: 'Value or text of the option to select'
|
|
224
|
+
},
|
|
225
|
+
by: {
|
|
226
|
+
type: 'string',
|
|
227
|
+
description: 'How to match the option',
|
|
228
|
+
enum: %w[value text],
|
|
229
|
+
default: 'value'
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
required: %w[selector value]
|
|
233
|
+
}
|
|
234
|
+
) do |selector:, value:, session: 'default', by: 'value', **|
|
|
235
|
+
_ = by # TODO: Implement selection by text/index
|
|
236
|
+
|
|
237
|
+
page = sessions.page(session)
|
|
238
|
+
select = page.at_css(selector)
|
|
239
|
+
|
|
240
|
+
raise ElementNotFoundError, "Select element not found: #{selector}" unless select
|
|
241
|
+
|
|
242
|
+
select.select(value)
|
|
243
|
+
|
|
244
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Selected '#{value}' in: #{selector}" }])
|
|
245
|
+
rescue ElementNotFoundError => e
|
|
246
|
+
MCP::Tool::Response.new([{ type: 'text', text: e.message }], error: true)
|
|
247
|
+
rescue Ferrum::Error => e
|
|
248
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Select failed: #{e.message}" }], error: true)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def scroll_tool(sessions)
|
|
253
|
+
MCP::Tool.define(
|
|
254
|
+
name: 'scroll',
|
|
255
|
+
description: 'Scroll the page or scroll an element into view',
|
|
256
|
+
input_schema: {
|
|
257
|
+
type: 'object',
|
|
258
|
+
properties: {
|
|
259
|
+
session: {
|
|
260
|
+
type: 'string',
|
|
261
|
+
description: 'Session name',
|
|
262
|
+
default: 'default'
|
|
263
|
+
},
|
|
264
|
+
selector: {
|
|
265
|
+
type: 'string',
|
|
266
|
+
description: 'CSS selector to scroll into view (optional)'
|
|
267
|
+
},
|
|
268
|
+
x: {
|
|
269
|
+
type: 'integer',
|
|
270
|
+
description: 'Horizontal scroll amount in pixels',
|
|
271
|
+
default: 0
|
|
272
|
+
},
|
|
273
|
+
y: {
|
|
274
|
+
type: 'integer',
|
|
275
|
+
description: 'Vertical scroll amount in pixels',
|
|
276
|
+
default: 0
|
|
277
|
+
},
|
|
278
|
+
direction: {
|
|
279
|
+
type: 'string',
|
|
280
|
+
description: 'Scroll direction shortcut',
|
|
281
|
+
enum: %w[up down left right top bottom]
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
required: []
|
|
285
|
+
}
|
|
286
|
+
) do |session: 'default', selector: nil, x: 0, y: 0, direction: nil, **|
|
|
287
|
+
page = sessions.page(session)
|
|
288
|
+
|
|
289
|
+
if selector
|
|
290
|
+
# Scroll element into view
|
|
291
|
+
element = page.at_css(selector)
|
|
292
|
+
raise ElementNotFoundError, "Element not found: #{selector}" unless element
|
|
293
|
+
|
|
294
|
+
element.scroll_into_view
|
|
295
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Scrolled into view: #{selector}" }])
|
|
296
|
+
else
|
|
297
|
+
# Scroll by coordinates or direction
|
|
298
|
+
scroll_x, scroll_y = case direction
|
|
299
|
+
when 'up' then [0, -500]
|
|
300
|
+
when 'down' then [0, 500]
|
|
301
|
+
when 'left' then [-500, 0]
|
|
302
|
+
when 'right' then [500, 0]
|
|
303
|
+
when 'top' then [0, -100_000]
|
|
304
|
+
when 'bottom' then [0, 100_000]
|
|
305
|
+
else [x, y]
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
page.execute("window.scrollBy(#{scroll_x}, #{scroll_y})")
|
|
309
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Scrolled by (#{scroll_x}, #{scroll_y})" }])
|
|
310
|
+
end
|
|
311
|
+
rescue ElementNotFoundError => e
|
|
312
|
+
MCP::Tool::Response.new([{ type: 'text', text: e.message }], error: true)
|
|
313
|
+
rescue Ferrum::Error => e
|
|
314
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Scroll failed: #{e.message}" }], error: true)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def hover_tool(sessions)
|
|
319
|
+
MCP::Tool.define(
|
|
320
|
+
name: 'hover',
|
|
321
|
+
description: 'Hover over an element',
|
|
322
|
+
input_schema: {
|
|
323
|
+
type: 'object',
|
|
324
|
+
properties: {
|
|
325
|
+
session: {
|
|
326
|
+
type: 'string',
|
|
327
|
+
description: 'Session name',
|
|
328
|
+
default: 'default'
|
|
329
|
+
},
|
|
330
|
+
selector: {
|
|
331
|
+
type: 'string',
|
|
332
|
+
description: 'CSS selector for the element to hover'
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
required: ['selector']
|
|
336
|
+
}
|
|
337
|
+
) do |selector:, session: 'default', **|
|
|
338
|
+
page = sessions.page(session)
|
|
339
|
+
element = page.at_css(selector)
|
|
340
|
+
|
|
341
|
+
raise ElementNotFoundError, "Element not found: #{selector}" unless element
|
|
342
|
+
|
|
343
|
+
element.hover
|
|
344
|
+
|
|
345
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Hovering over: #{selector}" }])
|
|
346
|
+
rescue ElementNotFoundError => e
|
|
347
|
+
MCP::Tool::Response.new([{ type: 'text', text: e.message }], error: true)
|
|
348
|
+
rescue Ferrum::Error => e
|
|
349
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Hover failed: #{e.message}" }], error: true)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mcp'
|
|
4
|
+
|
|
5
|
+
module Crucible
|
|
6
|
+
module Tools
|
|
7
|
+
# Navigation tools: navigate, wait_for, back, forward, refresh
|
|
8
|
+
module Navigation
|
|
9
|
+
class << self
|
|
10
|
+
def tools(sessions, _config)
|
|
11
|
+
[
|
|
12
|
+
navigate_tool(sessions),
|
|
13
|
+
wait_for_tool(sessions),
|
|
14
|
+
back_tool(sessions),
|
|
15
|
+
forward_tool(sessions),
|
|
16
|
+
refresh_tool(sessions)
|
|
17
|
+
]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def navigate_tool(sessions)
|
|
23
|
+
MCP::Tool.define(
|
|
24
|
+
name: 'navigate',
|
|
25
|
+
description: "Navigate browser to a URL. Creates a new session if it doesn't exist.",
|
|
26
|
+
input_schema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
session: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Session name for managing multiple browsers',
|
|
32
|
+
default: 'default'
|
|
33
|
+
},
|
|
34
|
+
url: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'URL to navigate to'
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
required: ['url']
|
|
40
|
+
}
|
|
41
|
+
) do |url:, session: 'default', **|
|
|
42
|
+
page = sessions.page(session)
|
|
43
|
+
page.go_to(url)
|
|
44
|
+
|
|
45
|
+
MCP::Tool::Response.new(
|
|
46
|
+
[{
|
|
47
|
+
type: 'text',
|
|
48
|
+
text: "Navigated to #{url} (session: #{session})"
|
|
49
|
+
}]
|
|
50
|
+
)
|
|
51
|
+
rescue Ferrum::Error => e
|
|
52
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Navigation failed: #{e.message}" }], error: true)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def wait_for_tool(sessions)
|
|
57
|
+
MCP::Tool.define(
|
|
58
|
+
name: 'wait_for',
|
|
59
|
+
description: 'Wait for an element to appear on the page',
|
|
60
|
+
input_schema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
session: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'Session name',
|
|
66
|
+
default: 'default'
|
|
67
|
+
},
|
|
68
|
+
selector: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'CSS selector to wait for'
|
|
71
|
+
},
|
|
72
|
+
timeout: {
|
|
73
|
+
type: 'number',
|
|
74
|
+
description: 'Maximum wait time in seconds',
|
|
75
|
+
default: 30
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
required: ['selector']
|
|
79
|
+
}
|
|
80
|
+
) do |selector:, session: 'default', timeout: 30, **|
|
|
81
|
+
page = sessions.page(session)
|
|
82
|
+
|
|
83
|
+
# Poll for element with timeout
|
|
84
|
+
start_time = Time.now
|
|
85
|
+
element = nil
|
|
86
|
+
loop do
|
|
87
|
+
element = page.at_css(selector)
|
|
88
|
+
break if element
|
|
89
|
+
|
|
90
|
+
if Time.now - start_time > timeout
|
|
91
|
+
return MCP::Tool::Response.new([{ type: 'text', text: "Timeout waiting for: #{selector}" }],
|
|
92
|
+
error: true)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
sleep 0.1
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Found element: #{selector}" }])
|
|
99
|
+
rescue Ferrum::Error => e
|
|
100
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Wait failed: #{e.message}" }], error: true)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def back_tool(sessions)
|
|
105
|
+
MCP::Tool.define(
|
|
106
|
+
name: 'back',
|
|
107
|
+
description: 'Navigate back in browser history',
|
|
108
|
+
input_schema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
session: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: 'Session name',
|
|
114
|
+
default: 'default'
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
required: []
|
|
118
|
+
}
|
|
119
|
+
) do |session: 'default', **|
|
|
120
|
+
page = sessions.page(session)
|
|
121
|
+
page.back
|
|
122
|
+
|
|
123
|
+
MCP::Tool::Response.new([{ type: 'text', text: 'Navigated back' }])
|
|
124
|
+
rescue Ferrum::Error => e
|
|
125
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Back navigation failed: #{e.message}" }], error: true)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def forward_tool(sessions)
|
|
130
|
+
MCP::Tool.define(
|
|
131
|
+
name: 'forward',
|
|
132
|
+
description: 'Navigate forward in browser history',
|
|
133
|
+
input_schema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
session: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: 'Session name',
|
|
139
|
+
default: 'default'
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
required: []
|
|
143
|
+
}
|
|
144
|
+
) do |session: 'default', **|
|
|
145
|
+
page = sessions.page(session)
|
|
146
|
+
page.forward
|
|
147
|
+
|
|
148
|
+
MCP::Tool::Response.new([{ type: 'text', text: 'Navigated forward' }])
|
|
149
|
+
rescue Ferrum::Error => e
|
|
150
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Forward navigation failed: #{e.message}" }], error: true)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def refresh_tool(sessions)
|
|
155
|
+
MCP::Tool.define(
|
|
156
|
+
name: 'refresh',
|
|
157
|
+
description: 'Refresh the current page',
|
|
158
|
+
input_schema: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
session: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
description: 'Session name',
|
|
164
|
+
default: 'default'
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
required: []
|
|
168
|
+
}
|
|
169
|
+
) do |session: 'default', **|
|
|
170
|
+
page = sessions.page(session)
|
|
171
|
+
page.refresh
|
|
172
|
+
|
|
173
|
+
MCP::Tool::Response.new([{ type: 'text', text: 'Page refreshed' }])
|
|
174
|
+
rescue Ferrum::Error => e
|
|
175
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Refresh failed: #{e.message}" }], error: true)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mcp'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Crucible
|
|
7
|
+
module Tools
|
|
8
|
+
# Session management tools: list_sessions, close_session
|
|
9
|
+
module Sessions
|
|
10
|
+
class << self
|
|
11
|
+
def tools(sessions, _config)
|
|
12
|
+
[
|
|
13
|
+
list_sessions_tool(sessions),
|
|
14
|
+
close_session_tool(sessions)
|
|
15
|
+
]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def list_sessions_tool(sessions)
|
|
21
|
+
MCP::Tool.define(
|
|
22
|
+
name: 'list_sessions',
|
|
23
|
+
description: 'List all active browser sessions',
|
|
24
|
+
input_schema: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {},
|
|
27
|
+
required: []
|
|
28
|
+
}
|
|
29
|
+
) do |**|
|
|
30
|
+
session_list = sessions.list
|
|
31
|
+
|
|
32
|
+
if session_list.empty?
|
|
33
|
+
MCP::Tool::Response.new([{ type: 'text', text: 'No active sessions' }])
|
|
34
|
+
else
|
|
35
|
+
result = {
|
|
36
|
+
count: session_list.size,
|
|
37
|
+
sessions: session_list
|
|
38
|
+
}
|
|
39
|
+
MCP::Tool::Response.new([{ type: 'text', text: JSON.pretty_generate(result) }])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def close_session_tool(sessions)
|
|
45
|
+
MCP::Tool.define(
|
|
46
|
+
name: 'close_session',
|
|
47
|
+
description: 'Close a browser session and free resources',
|
|
48
|
+
input_schema: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: {
|
|
51
|
+
session: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
description: 'Session name to close'
|
|
54
|
+
},
|
|
55
|
+
all: {
|
|
56
|
+
type: 'boolean',
|
|
57
|
+
description: 'Close all sessions',
|
|
58
|
+
default: false
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
required: []
|
|
62
|
+
}
|
|
63
|
+
) do |session: nil, all: false, **|
|
|
64
|
+
if all
|
|
65
|
+
count = sessions.count
|
|
66
|
+
sessions.close_all
|
|
67
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Closed #{count} session(s)" }])
|
|
68
|
+
elsif session
|
|
69
|
+
if sessions.close(session)
|
|
70
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Closed session: #{session}" }])
|
|
71
|
+
else
|
|
72
|
+
MCP::Tool::Response.new([{ type: 'text', text: "Session not found: #{session}" }], error: true)
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
MCP::Tool::Response.new([{
|
|
76
|
+
type: 'text',
|
|
77
|
+
text: 'Please specify a session name or use all: true to close all sessions'
|
|
78
|
+
}], error: true)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|