@sveltium/mcp 0.1.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.
@@ -0,0 +1,630 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Tool Executor
5
+ *
6
+ * Executes MCP tools in the NW.js browser context
7
+ */
8
+
9
+ var SnapshotBuilder = require('./dom/snapshot-builder')
10
+ var ElementFinder = require('./dom/element-finder')
11
+ var EventDispatcher = require('./dom/event-dispatcher')
12
+ var ConsoleCapture = require('./dom/console-capture')
13
+
14
+ function ToolExecutor(win) {
15
+ this.win = win
16
+ this.doc = win.document
17
+
18
+ // Initialize DOM utilities
19
+ this.snapshot = new SnapshotBuilder(win)
20
+ this.finder = new ElementFinder(win)
21
+ this.events = new EventDispatcher(win)
22
+ this.console = new ConsoleCapture(win)
23
+
24
+ // Start console capture
25
+ this.console.start()
26
+ }
27
+
28
+ ToolExecutor.prototype.execute = function(tool, args, callback) {
29
+ var self = this
30
+
31
+ try {
32
+ switch (tool) {
33
+ case 'browser_snapshot':
34
+ this._snapshot(args, callback)
35
+ break
36
+
37
+ case 'browser_take_screenshot':
38
+ this._screenshot(args, callback)
39
+ break
40
+
41
+ case 'browser_click':
42
+ this._click(args, callback)
43
+ break
44
+
45
+ case 'browser_type':
46
+ this._type(args, callback)
47
+ break
48
+
49
+ case 'browser_evaluate':
50
+ this._evaluate(args, callback)
51
+ break
52
+
53
+ case 'browser_navigate':
54
+ this._navigate(args, callback)
55
+ break
56
+
57
+ case 'browser_console_messages':
58
+ this._consoleMessages(args, callback)
59
+ break
60
+
61
+ case 'browser_resize':
62
+ this._resize(args, callback)
63
+ break
64
+
65
+ case 'browser_wait_for':
66
+ this._waitFor(args, callback)
67
+ break
68
+
69
+ case 'browser_fill_form':
70
+ this._fillForm(args, callback)
71
+ break
72
+
73
+ case 'browser_press_key':
74
+ this._pressKey(args, callback)
75
+ break
76
+
77
+ case 'nwjs_reload':
78
+ this._reload(args, callback)
79
+ break
80
+
81
+ case 'nwjs_show_devtools':
82
+ this._showDevtools(args, callback)
83
+ break
84
+
85
+ case 'nwjs_close':
86
+ this._close(args, callback)
87
+ break
88
+
89
+ case 'nwjs_get_manifest':
90
+ this._getManifest(args, callback)
91
+ break
92
+
93
+ case 'nwjs_get_argv':
94
+ this._getArgv(args, callback)
95
+ break
96
+
97
+ case 'nwjs_minimize':
98
+ this._minimize(args, callback)
99
+ break
100
+
101
+ case 'nwjs_maximize':
102
+ this._maximize(args, callback)
103
+ break
104
+
105
+ case 'nwjs_restore':
106
+ this._restore(args, callback)
107
+ break
108
+
109
+ case 'nwjs_focus':
110
+ this._focus(args, callback)
111
+ break
112
+
113
+ case 'nwjs_get_bounds':
114
+ this._getBounds(args, callback)
115
+ break
116
+
117
+ case 'nwjs_set_bounds':
118
+ this._setBounds(args, callback)
119
+ break
120
+
121
+ case 'nwjs_zoom':
122
+ this._zoom(args, callback)
123
+ break
124
+
125
+ default:
126
+ callback(new Error('Unknown tool: ' + tool))
127
+ }
128
+ } catch (err) {
129
+ callback(err)
130
+ }
131
+ }
132
+
133
+ ToolExecutor.prototype._snapshot = function(args, callback) {
134
+ var tree = this.snapshot.build()
135
+ callback(null, {
136
+ content: [{ type: 'text', text: tree }]
137
+ })
138
+ }
139
+
140
+ ToolExecutor.prototype._screenshot = function(args, callback) {
141
+ var self = this
142
+
143
+ // Try to get nw.gui
144
+ var nwGui = null
145
+ try {
146
+ nwGui = this.win.require('nw.gui')
147
+ } catch (e) {
148
+ nwGui = this.win.nw
149
+ }
150
+
151
+ if (!nwGui || !nwGui.Window) {
152
+ callback(new Error('NW.js window API not available'))
153
+ return
154
+ }
155
+
156
+ var nwWin = nwGui.Window.get()
157
+
158
+ nwWin.capturePage(function(buffer) {
159
+ var base64 = buffer.toString('base64')
160
+ callback(null, {
161
+ content: [{ type: 'image', data: base64, mimeType: 'image/png' }]
162
+ })
163
+ }, { format: 'png', datatype: 'buffer' })
164
+ }
165
+
166
+ ToolExecutor.prototype._click = function(args, callback) {
167
+ var element = this.finder.findByRef(args.ref)
168
+ if (!element) {
169
+ callback(new Error('Element not found: ' + args.ref))
170
+ return
171
+ }
172
+
173
+ var button = args.button || 'left'
174
+ var doubleClick = args.doubleClick || false
175
+
176
+ this.events.click(element, button, doubleClick)
177
+
178
+ callback(null, {
179
+ content: [{ type: 'text', text: 'Clicked: ' + (args.element || args.ref) }]
180
+ })
181
+ }
182
+
183
+ ToolExecutor.prototype._type = function(args, callback) {
184
+ var element = this.finder.findByRef(args.ref)
185
+ if (!element) {
186
+ callback(new Error('Element not found: ' + args.ref))
187
+ return
188
+ }
189
+
190
+ var text = args.text || ''
191
+ var slowly = args.slowly || false
192
+ var submit = args.submit || false
193
+
194
+ this.events.type(element, text, slowly, submit)
195
+
196
+ callback(null, {
197
+ content: [{ type: 'text', text: 'Typed: ' + text }]
198
+ })
199
+ }
200
+
201
+ ToolExecutor.prototype._evaluate = function(args, callback) {
202
+ var code = args.function
203
+ var element = null
204
+
205
+ if (args.ref) {
206
+ element = this.finder.findByRef(args.ref)
207
+ if (!element) {
208
+ callback(new Error('Element not found: ' + args.ref))
209
+ return
210
+ }
211
+ }
212
+
213
+ var fn = null
214
+ try {
215
+ fn = this.win.eval('(' + code + ')')
216
+ } catch (e) {
217
+ callback(new Error('Failed to parse function: ' + e.message))
218
+ return
219
+ }
220
+
221
+ var result = null
222
+ try {
223
+ if (element) {
224
+ result = fn(element)
225
+ } else {
226
+ result = fn()
227
+ }
228
+ } catch (e) {
229
+ callback(new Error('Function execution failed: ' + e.message))
230
+ return
231
+ }
232
+
233
+ // Format result
234
+ var formatted = 'undefined'
235
+ if (result !== undefined) {
236
+ if (result === null) {
237
+ formatted = 'null'
238
+ } else if (typeof result === 'object') {
239
+ try {
240
+ formatted = JSON.stringify(result, null, 2)
241
+ } catch (e) {
242
+ formatted = String(result)
243
+ }
244
+ } else {
245
+ formatted = String(result)
246
+ }
247
+ }
248
+
249
+ callback(null, {
250
+ content: [{ type: 'text', text: formatted }]
251
+ })
252
+ }
253
+
254
+ ToolExecutor.prototype._navigate = function(args, callback) {
255
+ this.win.location.href = args.url
256
+ callback(null, {
257
+ content: [{ type: 'text', text: 'Navigating to: ' + args.url }]
258
+ })
259
+ }
260
+
261
+ ToolExecutor.prototype._consoleMessages = function(args, callback) {
262
+ var level = args.level || 'info'
263
+ var messages = this.console.getMessages(level)
264
+ callback(null, {
265
+ content: [{ type: 'text', text: messages.join('\n') || '(no messages)' }]
266
+ })
267
+ }
268
+
269
+ ToolExecutor.prototype._resize = function(args, callback) {
270
+ var nwGui = null
271
+ try {
272
+ nwGui = this.win.require('nw.gui')
273
+ } catch (e) {
274
+ nwGui = this.win.nw
275
+ }
276
+
277
+ if (!nwGui || !nwGui.Window) {
278
+ callback(new Error('NW.js window API not available'))
279
+ return
280
+ }
281
+
282
+ var nwWin = nwGui.Window.get()
283
+ nwWin.resizeTo(args.width, args.height)
284
+
285
+ callback(null, {
286
+ content: [{ type: 'text', text: 'Resized to ' + args.width + 'x' + args.height }]
287
+ })
288
+ }
289
+
290
+ ToolExecutor.prototype._waitFor = function(args, callback) {
291
+ var self = this
292
+
293
+ if (args.time) {
294
+ setTimeout(function() {
295
+ callback(null, {
296
+ content: [{ type: 'text', text: 'Waited ' + args.time + ' seconds' }]
297
+ })
298
+ }, args.time * 1000)
299
+ return
300
+ }
301
+
302
+ var text = args.text
303
+ var textGone = args.textGone
304
+ var timeout = 10000
305
+ var interval = 100
306
+ var elapsed = 0
307
+
308
+ var check = function() {
309
+ var bodyText = self.doc.body ? self.doc.body.innerText : ''
310
+
311
+ if (text) {
312
+ if (bodyText.indexOf(text) !== -1) {
313
+ callback(null, {
314
+ content: [{ type: 'text', text: 'Found: ' + text }]
315
+ })
316
+ return
317
+ }
318
+ }
319
+
320
+ if (textGone) {
321
+ if (bodyText.indexOf(textGone) === -1) {
322
+ callback(null, {
323
+ content: [{ type: 'text', text: 'Gone: ' + textGone }]
324
+ })
325
+ return
326
+ }
327
+ }
328
+
329
+ elapsed += interval
330
+ if (elapsed >= timeout) {
331
+ callback(new Error('Timeout waiting for ' + (text || textGone)))
332
+ return
333
+ }
334
+
335
+ setTimeout(check, interval)
336
+ }
337
+
338
+ check()
339
+ }
340
+
341
+ ToolExecutor.prototype._fillForm = function(args, callback) {
342
+ var self = this
343
+ var fields = args.fields || []
344
+ var filled = []
345
+
346
+ for (var i = 0; i < fields.length; i++) {
347
+ var field = fields[i]
348
+ var element = this.finder.findByRef(field.ref)
349
+
350
+ if (!element) {
351
+ callback(new Error('Field not found: ' + field.ref))
352
+ return
353
+ }
354
+
355
+ if (field.type === 'checkbox' || field.type === 'radio') {
356
+ element.checked = field.value === 'true'
357
+ } else if (field.type === 'combobox') {
358
+ element.value = field.value
359
+ this.events.dispatchInput(element)
360
+ } else {
361
+ element.value = field.value
362
+ this.events.dispatchInput(element)
363
+ }
364
+
365
+ filled.push(field.name)
366
+ }
367
+
368
+ callback(null, {
369
+ content: [{ type: 'text', text: 'Filled fields: ' + filled.join(', ') }]
370
+ })
371
+ }
372
+
373
+ ToolExecutor.prototype._pressKey = function(args, callback) {
374
+ this.events.pressKey(args.key)
375
+ callback(null, {
376
+ content: [{ type: 'text', text: 'Pressed: ' + args.key }]
377
+ })
378
+ }
379
+
380
+ ToolExecutor.prototype._reload = function(args, callback) {
381
+ var nwGui = this._getNwGui()
382
+ if (!nwGui) {
383
+ callback(new Error('NW.js API not available'))
384
+ return
385
+ }
386
+
387
+ var nwWin = nwGui.Window.get()
388
+
389
+ // Check if full relaunch is requested (restart the whole app)
390
+ if (args.relaunch) {
391
+ callback(null, {
392
+ content: [{ type: 'text', text: 'Relaunching app...' }]
393
+ })
394
+
395
+ // Relaunch using child_process.exec like XPCode does
396
+ setTimeout(function() {
397
+ var exec = require('child_process').exec
398
+ var execPath = process.execPath
399
+
400
+ // Quote the path and use "." for current app directory
401
+ var safeExecPath = '"' + execPath + '"'
402
+ var command = safeExecPath + ' .'
403
+
404
+ exec(command, { stdio: 'ignore', detached: true }, function() {})
405
+
406
+ // Close current window after short delay
407
+ setTimeout(function() {
408
+ nwWin.close()
409
+ }, 30)
410
+ }, 100)
411
+ return
412
+ }
413
+
414
+ // Just reload the window
415
+ if (args.ignoreCache) {
416
+ nwWin.reloadDev()
417
+ } else {
418
+ nwWin.reload()
419
+ }
420
+
421
+ callback(null, {
422
+ content: [{ type: 'text', text: 'Reloading app' + (args.ignoreCache ? ' (ignoring cache)' : '') }]
423
+ })
424
+ }
425
+
426
+ ToolExecutor.prototype._showDevtools = function(args, callback) {
427
+ var nwGui = this._getNwGui()
428
+ if (!nwGui) {
429
+ callback(new Error('NW.js API not available'))
430
+ return
431
+ }
432
+
433
+ var nwWin = nwGui.Window.get()
434
+ nwWin.showDevTools()
435
+
436
+ callback(null, {
437
+ content: [{ type: 'text', text: 'DevTools opened' }]
438
+ })
439
+ }
440
+
441
+ ToolExecutor.prototype._close = function(args, callback) {
442
+ var nwGui = this._getNwGui()
443
+ if (!nwGui) {
444
+ callback(new Error('NW.js API not available'))
445
+ return
446
+ }
447
+
448
+ callback(null, {
449
+ content: [{ type: 'text', text: 'Closing app' }]
450
+ })
451
+
452
+ // Close after sending response
453
+ var nwWin = nwGui.Window.get()
454
+ setTimeout(function() {
455
+ nwWin.close()
456
+ }, 100)
457
+ }
458
+
459
+ ToolExecutor.prototype._getNwGui = function() {
460
+ try {
461
+ return this.win.require('nw.gui')
462
+ } catch (e) {
463
+ return this.win.nw
464
+ }
465
+ }
466
+
467
+ ToolExecutor.prototype._getManifest = function(args, callback) {
468
+ var nwGui = this._getNwGui()
469
+ if (!nwGui || !nwGui.App) {
470
+ callback(new Error('NW.js API not available'))
471
+ return
472
+ }
473
+
474
+ var manifest = nwGui.App.manifest || {}
475
+ var formatted = JSON.stringify(manifest, null, 2)
476
+
477
+ callback(null, {
478
+ content: [{ type: 'text', text: formatted }]
479
+ })
480
+ }
481
+
482
+ ToolExecutor.prototype._getArgv = function(args, callback) {
483
+ var nwGui = this._getNwGui()
484
+ if (!nwGui || !nwGui.App) {
485
+ callback(new Error('NW.js API not available'))
486
+ return
487
+ }
488
+
489
+ var result = {
490
+ argv: nwGui.App.argv || [],
491
+ fullArgv: nwGui.App.fullArgv || [],
492
+ dataPath: nwGui.App.dataPath || ''
493
+ }
494
+
495
+ callback(null, {
496
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
497
+ })
498
+ }
499
+
500
+ ToolExecutor.prototype._minimize = function(args, callback) {
501
+ var nwGui = this._getNwGui()
502
+ if (!nwGui) {
503
+ callback(new Error('NW.js API not available'))
504
+ return
505
+ }
506
+
507
+ var nwWin = nwGui.Window.get()
508
+ nwWin.minimize()
509
+
510
+ callback(null, {
511
+ content: [{ type: 'text', text: 'Window minimized' }]
512
+ })
513
+ }
514
+
515
+ ToolExecutor.prototype._maximize = function(args, callback) {
516
+ var nwGui = this._getNwGui()
517
+ if (!nwGui) {
518
+ callback(new Error('NW.js API not available'))
519
+ return
520
+ }
521
+
522
+ var nwWin = nwGui.Window.get()
523
+ nwWin.maximize()
524
+
525
+ callback(null, {
526
+ content: [{ type: 'text', text: 'Window maximized' }]
527
+ })
528
+ }
529
+
530
+ ToolExecutor.prototype._restore = function(args, callback) {
531
+ var nwGui = this._getNwGui()
532
+ if (!nwGui) {
533
+ callback(new Error('NW.js API not available'))
534
+ return
535
+ }
536
+
537
+ var nwWin = nwGui.Window.get()
538
+ nwWin.restore()
539
+
540
+ callback(null, {
541
+ content: [{ type: 'text', text: 'Window restored' }]
542
+ })
543
+ }
544
+
545
+ ToolExecutor.prototype._focus = function(args, callback) {
546
+ var nwGui = this._getNwGui()
547
+ if (!nwGui) {
548
+ callback(new Error('NW.js API not available'))
549
+ return
550
+ }
551
+
552
+ var nwWin = nwGui.Window.get()
553
+ nwWin.focus()
554
+
555
+ callback(null, {
556
+ content: [{ type: 'text', text: 'Window focused' }]
557
+ })
558
+ }
559
+
560
+ ToolExecutor.prototype._getBounds = function(args, callback) {
561
+ var nwGui = this._getNwGui()
562
+ if (!nwGui) {
563
+ callback(new Error('NW.js API not available'))
564
+ return
565
+ }
566
+
567
+ var nwWin = nwGui.Window.get()
568
+ var bounds = {
569
+ x: nwWin.x,
570
+ y: nwWin.y,
571
+ width: nwWin.width,
572
+ height: nwWin.height
573
+ }
574
+
575
+ callback(null, {
576
+ content: [{ type: 'text', text: JSON.stringify(bounds, null, 2) }]
577
+ })
578
+ }
579
+
580
+ ToolExecutor.prototype._setBounds = function(args, callback) {
581
+ var nwGui = this._getNwGui()
582
+ if (!nwGui) {
583
+ callback(new Error('NW.js API not available'))
584
+ return
585
+ }
586
+
587
+ var nwWin = nwGui.Window.get()
588
+
589
+ if (args.x !== undefined && args.y !== undefined) {
590
+ nwWin.moveTo(args.x, args.y)
591
+ }
592
+
593
+ if (args.width !== undefined && args.height !== undefined) {
594
+ nwWin.resizeTo(args.width, args.height)
595
+ }
596
+
597
+ var newBounds = {
598
+ x: nwWin.x,
599
+ y: nwWin.y,
600
+ width: nwWin.width,
601
+ height: nwWin.height
602
+ }
603
+
604
+ callback(null, {
605
+ content: [{ type: 'text', text: 'Bounds updated: ' + JSON.stringify(newBounds) }]
606
+ })
607
+ }
608
+
609
+ ToolExecutor.prototype._zoom = function(args, callback) {
610
+ var nwGui = this._getNwGui()
611
+ if (!nwGui) {
612
+ callback(new Error('NW.js API not available'))
613
+ return
614
+ }
615
+
616
+ var nwWin = nwGui.Window.get()
617
+ var level = args.level || 1.0
618
+
619
+ // NW.js uses zoomLevel which is logarithmic (0 = 100%, 1 = 120%, -1 = ~83%)
620
+ // Convert linear scale to NW.js zoom level
621
+ // Formula: zoomLevel = log(scale) / log(1.2)
622
+ var zoomLevel = Math.log(level) / Math.log(1.2)
623
+ nwWin.zoomLevel = zoomLevel
624
+
625
+ callback(null, {
626
+ content: [{ type: 'text', text: 'Zoom set to ' + (level * 100) + '%' }]
627
+ })
628
+ }
629
+
630
+ module.exports = ToolExecutor