@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,722 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * MCP Protocol Handler (stdio transport)
5
+ *
6
+ * Handles JSON-RPC 2.0 messages from Claude Code via stdin/stdout
7
+ */
8
+
9
+ var PROTOCOL_VERSION = '2024-11-05'
10
+ var SERVER_NAME = 'nwjs-mcp'
11
+ var SERVER_VERSION = '0.1.0'
12
+
13
+ // Default NW.js executable path - can be overridden via setNwPath()
14
+ var DEFAULT_NW_PATH = null
15
+
16
+ function MCPServer(wsBridge) {
17
+ this.wsBridge = wsBridge
18
+ this.buffer = ''
19
+ this.running = false
20
+ this.nwPath = DEFAULT_NW_PATH
21
+ this.startedApps = [] // Track spawned app processes
22
+ }
23
+
24
+ /**
25
+ * Set the default NW.js executable path
26
+ * @param {string} path - Path to nw.exe
27
+ */
28
+ MCPServer.prototype.setNwPath = function(path) {
29
+ this.nwPath = path
30
+ }
31
+
32
+ MCPServer.prototype.start = function() {
33
+ var self = this
34
+ this.running = true
35
+
36
+ process.stdin.setEncoding('utf8')
37
+
38
+ process.stdin.on('data', function(chunk) {
39
+ self._handleData(chunk)
40
+ })
41
+
42
+ process.stdin.on('end', function() {
43
+ self.running = false
44
+ process.exit(0)
45
+ })
46
+
47
+ // Log to stderr (stdout is for MCP protocol)
48
+ process.stderr.write('[nwjs-mcp] Server started, waiting for MCP messages...\n')
49
+ }
50
+
51
+ MCPServer.prototype._handleData = function(chunk) {
52
+ var self = this
53
+ this.buffer += chunk
54
+
55
+ var lines = this.buffer.split('\n')
56
+ this.buffer = lines.pop()
57
+
58
+ for (var i = 0; i < lines.length; i++) {
59
+ var line = lines[i].trim()
60
+ if (line) {
61
+ self._processMessage(line)
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Handle a message and return response via callback (for HTTP mode)
68
+ */
69
+ MCPServer.prototype.handleMessage = function(message, callback) {
70
+ var self = this
71
+ var request = null
72
+
73
+ try {
74
+ request = JSON.parse(message)
75
+ } catch (e) {
76
+ callback(JSON.stringify({
77
+ jsonrpc: '2.0',
78
+ id: null,
79
+ error: { code: -32700, message: 'Parse error' }
80
+ }))
81
+ return
82
+ }
83
+
84
+ var id = request.id
85
+ var method = request.method
86
+ var params = request.params || {}
87
+
88
+ this._handleMethod(id, method, params, function(result, error) {
89
+ if (error) {
90
+ callback(JSON.stringify({
91
+ jsonrpc: '2.0',
92
+ id: id,
93
+ error: error
94
+ }))
95
+ } else {
96
+ callback(JSON.stringify({
97
+ jsonrpc: '2.0',
98
+ id: id,
99
+ result: result
100
+ }))
101
+ }
102
+ })
103
+ }
104
+
105
+ MCPServer.prototype._processMessage = function(message) {
106
+ var self = this
107
+ var request = null
108
+
109
+ try {
110
+ request = JSON.parse(message)
111
+ } catch (e) {
112
+ this._sendError(null, -32700, 'Parse error')
113
+ return
114
+ }
115
+
116
+ var id = request.id
117
+ var method = request.method
118
+ var params = request.params || {}
119
+
120
+ this._handleMethod(id, method, params, function(result, error) {
121
+ if (error) {
122
+ self._sendError(id, error.code, error.message, error.data)
123
+ } else {
124
+ self._sendResult(id, result)
125
+ }
126
+ })
127
+ }
128
+
129
+ MCPServer.prototype._handleMethod = function(id, method, params, callback) {
130
+ var self = this
131
+
132
+ switch (method) {
133
+ case 'initialize':
134
+ callback({
135
+ protocolVersion: PROTOCOL_VERSION,
136
+ capabilities: { tools: {} },
137
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
138
+ })
139
+ break
140
+
141
+ case 'initialized':
142
+ callback({})
143
+ break
144
+
145
+ case 'tools/list':
146
+ callback({ tools: this._getToolsList() })
147
+ break
148
+
149
+ case 'tools/call':
150
+ this._handleToolCall(params, callback)
151
+ break
152
+
153
+ case 'ping':
154
+ callback({})
155
+ break
156
+
157
+ default:
158
+ callback(null, { code: -32601, message: 'Method not found: ' + method })
159
+ }
160
+ }
161
+
162
+ MCPServer.prototype._getToolsList = function() {
163
+ return [
164
+ {
165
+ name: 'browser_snapshot',
166
+ description: 'Capture accessibility snapshot of the current page. Returns a tree structure with element refs for targeting interactions.',
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: {}
170
+ }
171
+ },
172
+ {
173
+ name: 'browser_take_screenshot',
174
+ description: 'Take a screenshot of the current page or a specific element.',
175
+ inputSchema: {
176
+ type: 'object',
177
+ properties: {
178
+ ref: { type: 'string', description: 'Element ref to screenshot (optional)' },
179
+ fullPage: { type: 'boolean', description: 'Capture full scrollable page' }
180
+ }
181
+ }
182
+ },
183
+ {
184
+ name: 'browser_click',
185
+ description: 'Click on an element specified by ref.',
186
+ inputSchema: {
187
+ type: 'object',
188
+ properties: {
189
+ ref: { type: 'string', description: 'Element ref from snapshot' },
190
+ element: { type: 'string', description: 'Human-readable element description' },
191
+ button: { type: 'string', enum: ['left', 'right', 'middle'], description: 'Mouse button' },
192
+ doubleClick: { type: 'boolean', description: 'Perform double click' }
193
+ },
194
+ required: ['ref', 'element']
195
+ }
196
+ },
197
+ {
198
+ name: 'browser_type',
199
+ description: 'Type text into an editable element.',
200
+ inputSchema: {
201
+ type: 'object',
202
+ properties: {
203
+ ref: { type: 'string', description: 'Element ref from snapshot' },
204
+ element: { type: 'string', description: 'Human-readable element description' },
205
+ text: { type: 'string', description: 'Text to type' },
206
+ slowly: { type: 'boolean', description: 'Type character by character' },
207
+ submit: { type: 'boolean', description: 'Press Enter after typing' }
208
+ },
209
+ required: ['ref', 'element', 'text']
210
+ }
211
+ },
212
+ {
213
+ name: 'browser_evaluate',
214
+ description: 'Evaluate JavaScript expression on the page.',
215
+ inputSchema: {
216
+ type: 'object',
217
+ properties: {
218
+ function: { type: 'string', description: 'JavaScript function to evaluate' },
219
+ ref: { type: 'string', description: 'Element ref to pass to function (optional)' },
220
+ element: { type: 'string', description: 'Human-readable element description' }
221
+ },
222
+ required: ['function']
223
+ }
224
+ },
225
+ {
226
+ name: 'browser_navigate',
227
+ description: 'Navigate to a URL.',
228
+ inputSchema: {
229
+ type: 'object',
230
+ properties: {
231
+ url: { type: 'string', description: 'URL to navigate to' }
232
+ },
233
+ required: ['url']
234
+ }
235
+ },
236
+ {
237
+ name: 'browser_console_messages',
238
+ description: 'Get console messages from the page.',
239
+ inputSchema: {
240
+ type: 'object',
241
+ properties: {
242
+ level: { type: 'string', enum: ['error', 'warning', 'info', 'debug'], description: 'Minimum log level' }
243
+ }
244
+ }
245
+ },
246
+ {
247
+ name: 'browser_resize',
248
+ description: 'Resize the browser window.',
249
+ inputSchema: {
250
+ type: 'object',
251
+ properties: {
252
+ width: { type: 'number', description: 'Window width in pixels' },
253
+ height: { type: 'number', description: 'Window height in pixels' }
254
+ },
255
+ required: ['width', 'height']
256
+ }
257
+ },
258
+ {
259
+ name: 'browser_wait_for',
260
+ description: 'Wait for text to appear or disappear, or wait for a specified time.',
261
+ inputSchema: {
262
+ type: 'object',
263
+ properties: {
264
+ text: { type: 'string', description: 'Text to wait for' },
265
+ textGone: { type: 'string', description: 'Text to wait for to disappear' },
266
+ time: { type: 'number', description: 'Time to wait in seconds' }
267
+ }
268
+ }
269
+ },
270
+ {
271
+ name: 'browser_fill_form',
272
+ description: 'Fill multiple form fields at once.',
273
+ inputSchema: {
274
+ type: 'object',
275
+ properties: {
276
+ fields: {
277
+ type: 'array',
278
+ description: 'Array of fields to fill',
279
+ items: {
280
+ type: 'object',
281
+ properties: {
282
+ ref: { type: 'string' },
283
+ name: { type: 'string' },
284
+ type: { type: 'string', enum: ['textbox', 'checkbox', 'radio', 'combobox', 'slider'] },
285
+ value: { type: 'string' }
286
+ },
287
+ required: ['ref', 'name', 'type', 'value']
288
+ }
289
+ }
290
+ },
291
+ required: ['fields']
292
+ }
293
+ },
294
+ {
295
+ name: 'browser_press_key',
296
+ description: 'Press a keyboard key.',
297
+ inputSchema: {
298
+ type: 'object',
299
+ properties: {
300
+ key: { type: 'string', description: 'Key to press (e.g., "Enter", "Tab", "a")' }
301
+ },
302
+ required: ['key']
303
+ }
304
+ },
305
+ {
306
+ name: 'nwjs_list_apps',
307
+ description: 'List all connected NW.js applications.',
308
+ inputSchema: {
309
+ type: 'object',
310
+ properties: {}
311
+ }
312
+ },
313
+ {
314
+ name: 'nwjs_select_app',
315
+ description: 'Select which NW.js app to target for browser commands.',
316
+ inputSchema: {
317
+ type: 'object',
318
+ properties: {
319
+ appId: { type: 'string', description: 'App ID to select' }
320
+ },
321
+ required: ['appId']
322
+ }
323
+ },
324
+ {
325
+ name: 'nwjs_reload',
326
+ description: 'Reload the NW.js app window.',
327
+ inputSchema: {
328
+ type: 'object',
329
+ properties: {
330
+ ignoreCache: { type: 'boolean', description: 'Ignore cache when reloading (like Ctrl+Shift+R)' },
331
+ relaunch: { type: 'boolean', description: 'Fully relaunch the app (restart the process) instead of just reloading the window' }
332
+ }
333
+ }
334
+ },
335
+ {
336
+ name: 'nwjs_show_devtools',
337
+ description: 'Show the developer tools for the NW.js app.',
338
+ inputSchema: {
339
+ type: 'object',
340
+ properties: {}
341
+ }
342
+ },
343
+ {
344
+ name: 'nwjs_close',
345
+ description: 'Close the NW.js app window.',
346
+ inputSchema: {
347
+ type: 'object',
348
+ properties: {}
349
+ }
350
+ },
351
+ {
352
+ name: 'nwjs_start_app',
353
+ description: 'Start an NW.js application. The app must include the MCP client library to connect.',
354
+ inputSchema: {
355
+ type: 'object',
356
+ properties: {
357
+ appPath: { type: 'string', description: 'Path to the NW.js app directory (containing package.json)' },
358
+ nwPath: { type: 'string', description: 'Path to nw.exe (optional, uses configured default if not specified)' },
359
+ args: { type: 'array', items: { type: 'string' }, description: 'Additional command line arguments' }
360
+ },
361
+ required: ['appPath']
362
+ }
363
+ },
364
+ {
365
+ name: 'nwjs_get_manifest',
366
+ description: 'Get the app manifest (package.json) information.',
367
+ inputSchema: {
368
+ type: 'object',
369
+ properties: {}
370
+ }
371
+ },
372
+ {
373
+ name: 'nwjs_get_argv',
374
+ description: 'Get command line arguments the app was started with.',
375
+ inputSchema: {
376
+ type: 'object',
377
+ properties: {}
378
+ }
379
+ },
380
+ {
381
+ name: 'nwjs_minimize',
382
+ description: 'Minimize the NW.js app window.',
383
+ inputSchema: {
384
+ type: 'object',
385
+ properties: {}
386
+ }
387
+ },
388
+ {
389
+ name: 'nwjs_maximize',
390
+ description: 'Maximize the NW.js app window.',
391
+ inputSchema: {
392
+ type: 'object',
393
+ properties: {}
394
+ }
395
+ },
396
+ {
397
+ name: 'nwjs_restore',
398
+ description: 'Restore the NW.js app window from minimized/maximized state.',
399
+ inputSchema: {
400
+ type: 'object',
401
+ properties: {}
402
+ }
403
+ },
404
+ {
405
+ name: 'nwjs_focus',
406
+ description: 'Bring the NW.js app window to the front.',
407
+ inputSchema: {
408
+ type: 'object',
409
+ properties: {}
410
+ }
411
+ },
412
+ {
413
+ name: 'nwjs_get_bounds',
414
+ description: 'Get the window position and size.',
415
+ inputSchema: {
416
+ type: 'object',
417
+ properties: {}
418
+ }
419
+ },
420
+ {
421
+ name: 'nwjs_set_bounds',
422
+ description: 'Set the window position and size.',
423
+ inputSchema: {
424
+ type: 'object',
425
+ properties: {
426
+ x: { type: 'number', description: 'Window X position' },
427
+ y: { type: 'number', description: 'Window Y position' },
428
+ width: { type: 'number', description: 'Window width' },
429
+ height: { type: 'number', description: 'Window height' }
430
+ }
431
+ }
432
+ },
433
+ {
434
+ name: 'nwjs_zoom',
435
+ description: 'Set the zoom level of the page.',
436
+ inputSchema: {
437
+ type: 'object',
438
+ properties: {
439
+ level: { type: 'number', description: 'Zoom level (1.0 = 100%, 1.5 = 150%, etc.)' }
440
+ },
441
+ required: ['level']
442
+ }
443
+ }
444
+ ]
445
+ }
446
+
447
+ MCPServer.prototype._handleToolCall = function(params, callback) {
448
+ var self = this
449
+ var toolName = params.name
450
+ var toolArgs = params.arguments || {}
451
+
452
+ // Handle server-side tools
453
+ if (toolName === 'nwjs_list_apps') {
454
+ var apps = this.wsBridge.getConnectedApps()
455
+ callback({
456
+ content: [{
457
+ type: 'text',
458
+ text: apps.length === 0
459
+ ? 'No NW.js apps connected. Start an NW.js app with the MCP client library.'
460
+ : 'Connected apps:\n' + apps.map(function(app) {
461
+ return '- ' + app.id + (app.name ? ' (' + app.name + ')' : '') + (app.active ? ' [active]' : '')
462
+ }).join('\n')
463
+ }]
464
+ })
465
+ return
466
+ }
467
+
468
+ if (toolName === 'nwjs_select_app') {
469
+ var success = this.wsBridge.selectApp(toolArgs.appId)
470
+ callback({
471
+ content: [{
472
+ type: 'text',
473
+ text: success
474
+ ? 'Selected app: ' + toolArgs.appId
475
+ : 'App not found: ' + toolArgs.appId
476
+ }],
477
+ isError: !success
478
+ })
479
+ return
480
+ }
481
+
482
+ if (toolName === 'nwjs_start_app') {
483
+ this._startApp(toolArgs, callback)
484
+ return
485
+ }
486
+
487
+ // Proxy to NW.js app
488
+ this.wsBridge.callTool(toolName, toolArgs, function(err, result) {
489
+ if (err) {
490
+ callback({
491
+ content: [{ type: 'text', text: 'Error: ' + err.message }],
492
+ isError: true
493
+ })
494
+ } else {
495
+ callback(result)
496
+ }
497
+ })
498
+ }
499
+
500
+ MCPServer.prototype._sendResult = function(id, result) {
501
+ var response = {
502
+ jsonrpc: '2.0',
503
+ id: id,
504
+ result: result
505
+ }
506
+ process.stdout.write(JSON.stringify(response) + '\n')
507
+ }
508
+
509
+ MCPServer.prototype._sendError = function(id, code, message, data) {
510
+ var response = {
511
+ jsonrpc: '2.0',
512
+ id: id,
513
+ error: {
514
+ code: code,
515
+ message: message
516
+ }
517
+ }
518
+ if (data !== undefined) {
519
+ response.error.data = data
520
+ }
521
+ process.stdout.write(JSON.stringify(response) + '\n')
522
+ }
523
+
524
+ MCPServer.prototype._startApp = function(args, callback) {
525
+ var self = this
526
+ var spawn = require('child_process').spawn
527
+ var path = require('path')
528
+ var fs = require('fs')
529
+
530
+ var appPath = args.appPath
531
+
532
+ // Validate app path exists
533
+ if (!fs.existsSync(appPath)) {
534
+ callback({
535
+ content: [{ type: 'text', text: 'App path does not exist: ' + appPath }],
536
+ isError: true
537
+ })
538
+ return
539
+ }
540
+
541
+ // Check for package.json
542
+ var packageJsonPath = path.join(appPath, 'package.json')
543
+ if (!fs.existsSync(packageJsonPath)) {
544
+ callback({
545
+ content: [{ type: 'text', text: 'No package.json found in: ' + appPath }],
546
+ isError: true
547
+ })
548
+ return
549
+ }
550
+
551
+ // Find nwPath from multiple sources
552
+ var nwPath = this._findNwPath(args.nwPath, appPath)
553
+
554
+ if (!nwPath) {
555
+ callback({
556
+ content: [{
557
+ type: 'text',
558
+ text: 'NW.js executable not found. Options:\n' +
559
+ '1. Pass nwPath argument to this tool\n' +
560
+ '2. Set NWJS_PATH in .env file in app directory\n' +
561
+ '3. Set NWJS_PATH environment variable\n' +
562
+ '4. Install "nw" package in your project: npm install nw'
563
+ }],
564
+ isError: true
565
+ })
566
+ return
567
+ }
568
+
569
+ // Check nw.exe exists
570
+ if (!fs.existsSync(nwPath)) {
571
+ callback({
572
+ content: [{ type: 'text', text: 'NW.js executable not found at: ' + nwPath }],
573
+ isError: true
574
+ })
575
+ return
576
+ }
577
+
578
+ // Build arguments
579
+ var spawnArgs = [appPath]
580
+ if (args.args && Array.isArray(args.args)) {
581
+ spawnArgs = spawnArgs.concat(args.args)
582
+ }
583
+
584
+ // Spawn the app
585
+ var child = spawn(nwPath, spawnArgs, {
586
+ detached: true,
587
+ stdio: 'ignore',
588
+ cwd: appPath
589
+ })
590
+
591
+ child.unref()
592
+
593
+ // Track spawned app
594
+ self.startedApps.push({
595
+ pid: child.pid,
596
+ appPath: appPath,
597
+ startedAt: Date.now()
598
+ })
599
+
600
+ callback({
601
+ content: [{
602
+ type: 'text',
603
+ text: 'Started NW.js app: ' + appPath + '\nPID: ' + child.pid + '\nWaiting for app to connect...'
604
+ }]
605
+ })
606
+ }
607
+
608
+ /**
609
+ * Parse a .env file and return key-value pairs
610
+ * @param {string} envFilePath - Path to .env file
611
+ * @returns {Object}
612
+ */
613
+ MCPServer.prototype._parseEnvFile = function(envFilePath) {
614
+ var fs = require('fs')
615
+ var result = {}
616
+
617
+ if (!fs.existsSync(envFilePath)) {
618
+ return result
619
+ }
620
+
621
+ try {
622
+ var content = fs.readFileSync(envFilePath, 'utf8')
623
+ var lines = content.split('\n')
624
+
625
+ for (var i = 0; i < lines.length; i++) {
626
+ var line = lines[i].trim()
627
+
628
+ // Skip empty lines and comments
629
+ if (!line || line.charAt(0) === '#') {
630
+ continue
631
+ }
632
+
633
+ var eqIndex = line.indexOf('=')
634
+ if (eqIndex === -1) {
635
+ continue
636
+ }
637
+
638
+ var key = line.substring(0, eqIndex).trim()
639
+ var value = line.substring(eqIndex + 1).trim()
640
+
641
+ // Remove surrounding quotes if present
642
+ if ((value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') ||
643
+ (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'")) {
644
+ value = value.substring(1, value.length - 1)
645
+ }
646
+
647
+ result[key] = value
648
+ }
649
+ } catch (e) {
650
+ // Failed to read/parse .env file
651
+ }
652
+
653
+ return result
654
+ }
655
+
656
+ /**
657
+ * Find NW.js executable path from multiple sources
658
+ * @param {string} explicitPath - Path passed as argument
659
+ * @param {string} appPath - App directory to check for nw package
660
+ * @returns {string|null}
661
+ */
662
+ MCPServer.prototype._findNwPath = function(explicitPath, appPath) {
663
+ var fs = require('fs')
664
+ var path = require('path')
665
+
666
+ // 1. Explicit path from argument
667
+ if (explicitPath && fs.existsSync(explicitPath)) {
668
+ return explicitPath
669
+ }
670
+
671
+ // 2. Server configured path
672
+ if (this.nwPath && fs.existsSync(this.nwPath)) {
673
+ return this.nwPath
674
+ }
675
+
676
+ // 3. Environment variable
677
+ var envPath = process.env.NWJS_PATH
678
+ if (envPath && fs.existsSync(envPath)) {
679
+ return envPath
680
+ }
681
+
682
+ // 4. Check .env file in app directory
683
+ var envFile = this._parseEnvFile(path.join(appPath, '.env'))
684
+ if (envFile.NWJS_PATH && fs.existsSync(envFile.NWJS_PATH)) {
685
+ return envFile.NWJS_PATH
686
+ }
687
+
688
+ // 5. Try to find from 'nw' package in app's node_modules
689
+ var nwPackagePath = path.join(appPath, 'node_modules', 'nw')
690
+ if (fs.existsSync(nwPackagePath)) {
691
+ try {
692
+ // The nw package has a findpath module
693
+ var findpath = require(path.join(nwPackagePath, 'lib', 'findpath'))
694
+ var foundPath = findpath()
695
+ if (foundPath && fs.existsSync(foundPath)) {
696
+ return foundPath
697
+ }
698
+ } catch (e) {
699
+ // findpath module not available or failed
700
+ }
701
+ }
702
+
703
+ // 6. Check common locations on Windows
704
+ if (process.platform === 'win32') {
705
+ var commonPaths = [
706
+ path.join(process.env.LOCALAPPDATA || '', 'nw'),
707
+ path.join(process.env.PROGRAMFILES || '', 'nw'),
708
+ 'C:\\nw'
709
+ ]
710
+
711
+ for (var i = 0; i < commonPaths.length; i++) {
712
+ var nwExe = path.join(commonPaths[i], 'nw.exe')
713
+ if (fs.existsSync(nwExe)) {
714
+ return nwExe
715
+ }
716
+ }
717
+ }
718
+
719
+ return null
720
+ }
721
+
722
+ module.exports = MCPServer