@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.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # nwjs-mcp
2
+
3
+ MCP (Model Context Protocol) server for NW.js application testing and automation.
4
+
5
+ This package enables AI assistants (like Claude Code) to interact with and test NW.js applications, addressing the limitation that browser automation tools like Playwright don't work with NW.js.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install nwjs-mcp
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ Add to your NW.js app's `node-main` script:
16
+
17
+ ```javascript
18
+ // main.js (node-main in package.json)
19
+ if (process.argv.includes('--mcp')) {
20
+ require('nwjs-mcp').startServer({
21
+ mode: 'stdio',
22
+ native: true
23
+ })
24
+ }
25
+ ```
26
+
27
+ Or in your `package.json`:
28
+
29
+ ```json
30
+ {
31
+ "main": "index.html",
32
+ "node-main": "mcp-bootstrap.js"
33
+ }
34
+ ```
35
+
36
+ ## Claude Code Integration
37
+
38
+ Add to your `.claude.json` or Claude Code settings:
39
+
40
+ ```json
41
+ {
42
+ "mcpServers": {
43
+ "nwjs": {
44
+ "command": "path/to/nw.exe",
45
+ "args": ["path/to/your-app", "--mcp"],
46
+ "env": {
47
+ "NWJS_MCP_MODE": "stdio"
48
+ }
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Available Tools
55
+
56
+ | Tool | Description |
57
+ |------|-------------|
58
+ | `browser_snapshot` | Get accessibility tree of the page with element refs |
59
+ | `browser_take_screenshot` | Capture page screenshot |
60
+ | `browser_click` | Click an element by ref |
61
+ | `browser_type` | Type text into an element |
62
+ | `browser_press_key` | Press a keyboard key |
63
+ | `browser_evaluate` | Execute JavaScript on the page |
64
+ | `browser_navigate` | Navigate to a URL |
65
+ | `browser_console_messages` | Get console log history |
66
+ | `browser_resize` | Resize the window |
67
+ | `browser_wait_for` | Wait for text or time |
68
+ | `browser_fill_form` | Fill multiple form fields |
69
+
70
+ ## API
71
+
72
+ ### startServer(options)
73
+
74
+ Start the MCP server.
75
+
76
+ ```javascript
77
+ var server = require('nwjs-mcp').startServer({
78
+ mode: 'stdio', // 'stdio' or 'http'
79
+ port: 3000, // HTTP port (only for http mode)
80
+ native: true // Try to load native addon
81
+ })
82
+ ```
83
+
84
+ ### createServer(options)
85
+
86
+ Create server instance without starting.
87
+
88
+ ```javascript
89
+ var server = require('nwjs-mcp').createServer({ mode: 'http' })
90
+ // Start later
91
+ server.start()
92
+ // Stop when done
93
+ server.stop()
94
+ ```
95
+
96
+ ## Server Modes
97
+
98
+ ### stdio Mode (Recommended)
99
+
100
+ Uses stdin/stdout for communication. Best for integration with Claude Code.
101
+
102
+ ```javascript
103
+ startServer({ mode: 'stdio' })
104
+ ```
105
+
106
+ ### HTTP Mode
107
+
108
+ Runs an HTTP server for network access.
109
+
110
+ ```javascript
111
+ startServer({ mode: 'http', port: 3000 })
112
+ ```
113
+
114
+ ## Native Addon (Optional)
115
+
116
+ For enhanced features like system-level input (useful for testing native dialogs), install the native addon:
117
+
118
+ ```bash
119
+ npm install @aspect/nwjs-addons
120
+ ```
121
+
122
+ Native features:
123
+ - System-level mouse clicks (for native dialogs)
124
+ - System-level keyboard input
125
+ - Window management (focus, minimize)
126
+ - Screen capture
127
+
128
+ The server gracefully falls back to JS-only mode if native addon is unavailable.
129
+
130
+ ## Example Usage with Claude Code
131
+
132
+ Once configured, you can ask Claude Code to:
133
+
134
+ ```
135
+ "Test my NW.js app - click the login button and verify the form appears"
136
+ ```
137
+
138
+ Claude Code will:
139
+ 1. Use `browser_snapshot` to see the page structure
140
+ 2. Use `browser_click` to click the login button
141
+ 3. Use `browser_snapshot` again to verify the form
142
+ 4. Report the results
143
+
144
+ ## License
145
+
146
+ MIT
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@sveltium/mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for NW.js application testing and automation",
5
+ "main": "src/client/index.js",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "nwjs-mcp": "./src/server/index.js"
9
+ },
10
+ "exports": {
11
+ ".": "./src/client/index.js",
12
+ "./client": "./src/client/index.js",
13
+ "./server": "./src/server/index.js"
14
+ },
15
+ "files": [
16
+ "src/**/*.js",
17
+ "scripts/**/*.js",
18
+ "types/**/*.d.ts"
19
+ ],
20
+ "scripts": {
21
+ "start": "node src/server/index.js",
22
+ "postinstall": "node scripts/postinstall.js",
23
+ "test": "echo \"No tests yet\""
24
+ },
25
+ "keywords": [
26
+ "nwjs",
27
+ "mcp",
28
+ "model-context-protocol",
29
+ "testing",
30
+ "automation",
31
+ "claude"
32
+ ],
33
+ "author": {
34
+ "name": "Oosuke Ren",
35
+ "email": "oosukeren@gmail.com",
36
+ "url": "https://github.com/OosukeRen"
37
+ },
38
+ "license": "MIT",
39
+ "dependencies": {
40
+ "ws": "^8.0.0"
41
+ },
42
+ "peerDependencies": {
43
+ "nw": "*"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "nw": {
47
+ "optional": true
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ /**
5
+ * Postinstall script that configures .mcp.json for Claude Code integration
6
+ */
7
+
8
+ var fs = require('fs')
9
+ var path = require('path')
10
+
11
+ // Find the project root - use npm's INIT_CWD which is where npm was run from
12
+ function findProjectRoot() {
13
+ // INIT_CWD is set by npm to the directory where `npm install` was run
14
+ if (process.env.INIT_CWD) {
15
+ return process.env.INIT_CWD
16
+ }
17
+
18
+ // Fallback: use current working directory
19
+ return process.cwd()
20
+ }
21
+
22
+ // Get the path to the main server index.js in node_modules
23
+ function getServerPath(projectRoot) {
24
+ // Check if we're installed in node_modules of the project (with scope)
25
+ var nodeModulesPath = path.join(projectRoot, 'node_modules', '@sveltium', 'nwjs-mcp', 'src', 'server', 'index.js')
26
+
27
+ // Also check without scope
28
+ var nodeModulesPathNoScope = path.join(projectRoot, 'node_modules', 'nwjs-mcp', 'src', 'server', 'index.js')
29
+
30
+ if (fs.existsSync(nodeModulesPath)) {
31
+ return nodeModulesPath
32
+ }
33
+
34
+ if (fs.existsSync(nodeModulesPathNoScope)) {
35
+ return nodeModulesPathNoScope
36
+ }
37
+
38
+ // Fallback: use relative to this script (for dev mode)
39
+ var localPath = path.join(__dirname, '..', 'src', 'server', 'index.js')
40
+ return path.resolve(localPath)
41
+ }
42
+
43
+ function main() {
44
+ // Skip for global installs
45
+ if (process.env.npm_config_global === 'true') {
46
+ console.log('[nwjs-mcp] Global install detected. Run "nwjs-mcp init" in your project to configure .mcp.json')
47
+ return
48
+ }
49
+
50
+ var projectRoot = findProjectRoot()
51
+ if (!projectRoot) {
52
+ console.log('[nwjs-mcp] Could not find project root, skipping .mcp.json configuration')
53
+ return
54
+ }
55
+
56
+ var mcpJsonPath = path.join(projectRoot, '.mcp.json')
57
+ var serverPath = getServerPath(projectRoot)
58
+
59
+ var mcpConfig = {}
60
+
61
+ // Read existing .mcp.json if it exists
62
+ if (fs.existsSync(mcpJsonPath)) {
63
+ try {
64
+ mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'))
65
+ } catch (e) {
66
+ console.log('[nwjs-mcp] Could not parse existing .mcp.json, creating new one')
67
+ mcpConfig = {}
68
+ }
69
+ }
70
+
71
+ // Ensure mcpServers object exists
72
+ if (!mcpConfig.mcpServers) {
73
+ mcpConfig.mcpServers = {}
74
+ }
75
+
76
+ // Add or update nwjs entry
77
+ mcpConfig.mcpServers.nwjs = {
78
+ command: 'node',
79
+ args: [serverPath]
80
+ }
81
+
82
+ // Write .mcp.json
83
+ try {
84
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n')
85
+ console.log('[nwjs-mcp] Configured .mcp.json at:', mcpJsonPath)
86
+ console.log('[nwjs-mcp] Server path:', serverPath)
87
+ console.log('[nwjs-mcp] To use nwjs_start_app, either:')
88
+ console.log(' - Install "nw" package: npm install nw')
89
+ console.log(' - Or set NWJS_PATH in .env file')
90
+ console.log(' - Or add NWJS_PATH env to .mcp.json')
91
+ } catch (e) {
92
+ console.log('[nwjs-mcp] Could not write .mcp.json:', e.message)
93
+ }
94
+ }
95
+
96
+ main()
@@ -0,0 +1,143 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Console Capture
5
+ *
6
+ * Captures console messages for retrieval
7
+ */
8
+
9
+ function ConsoleCapture(win) {
10
+ this.win = win
11
+ this.messages = []
12
+ this.maxMessages = 1000
13
+ this.originalConsole = {}
14
+ this.started = false
15
+ }
16
+
17
+ /**
18
+ * Start capturing console messages
19
+ */
20
+ ConsoleCapture.prototype.start = function() {
21
+ if (this.started) {
22
+ return
23
+ }
24
+
25
+ var self = this
26
+ var console = this.win.console
27
+
28
+ // Store original methods
29
+ this.originalConsole.log = console.log
30
+ this.originalConsole.info = console.info
31
+ this.originalConsole.warn = console.warn
32
+ this.originalConsole.error = console.error
33
+ this.originalConsole.debug = console.debug
34
+
35
+ // Override console methods
36
+ console.log = function() {
37
+ self._capture('info', arguments)
38
+ self.originalConsole.log.apply(console, arguments)
39
+ }
40
+
41
+ console.info = function() {
42
+ self._capture('info', arguments)
43
+ self.originalConsole.info.apply(console, arguments)
44
+ }
45
+
46
+ console.warn = function() {
47
+ self._capture('warning', arguments)
48
+ self.originalConsole.warn.apply(console, arguments)
49
+ }
50
+
51
+ console.error = function() {
52
+ self._capture('error', arguments)
53
+ self.originalConsole.error.apply(console, arguments)
54
+ }
55
+
56
+ console.debug = function() {
57
+ self._capture('debug', arguments)
58
+ self.originalConsole.debug.apply(console, arguments)
59
+ }
60
+
61
+ this.started = true
62
+ }
63
+
64
+ /**
65
+ * Stop capturing console messages
66
+ */
67
+ ConsoleCapture.prototype.stop = function() {
68
+ if (!this.started) {
69
+ return
70
+ }
71
+
72
+ var console = this.win.console
73
+
74
+ // Restore original methods
75
+ console.log = this.originalConsole.log
76
+ console.info = this.originalConsole.info
77
+ console.warn = this.originalConsole.warn
78
+ console.error = this.originalConsole.error
79
+ console.debug = this.originalConsole.debug
80
+
81
+ this.started = false
82
+ }
83
+
84
+ /**
85
+ * Capture a console message
86
+ */
87
+ ConsoleCapture.prototype._capture = function(level, args) {
88
+ var message = ''
89
+
90
+ for (var i = 0; i < args.length; i++) {
91
+ if (i > 0) message += ' '
92
+
93
+ var arg = args[i]
94
+ if (typeof arg === 'object') {
95
+ try {
96
+ message += JSON.stringify(arg)
97
+ } catch (e) {
98
+ message += String(arg)
99
+ }
100
+ } else {
101
+ message += String(arg)
102
+ }
103
+ }
104
+
105
+ this.messages.push({
106
+ level: level,
107
+ message: message,
108
+ timestamp: Date.now()
109
+ })
110
+
111
+ // Trim old messages
112
+ if (this.messages.length > this.maxMessages) {
113
+ this.messages = this.messages.slice(-this.maxMessages)
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Get messages at or above a given level
119
+ */
120
+ ConsoleCapture.prototype.getMessages = function(minLevel) {
121
+ var levels = ['debug', 'info', 'warning', 'error']
122
+ var minIndex = levels.indexOf(minLevel || 'info')
123
+
124
+ var result = []
125
+ for (var i = 0; i < this.messages.length; i++) {
126
+ var msg = this.messages[i]
127
+ var msgIndex = levels.indexOf(msg.level)
128
+ if (msgIndex >= minIndex) {
129
+ result.push('[' + msg.level.toUpperCase() + '] ' + msg.message)
130
+ }
131
+ }
132
+
133
+ return result
134
+ }
135
+
136
+ /**
137
+ * Clear all captured messages
138
+ */
139
+ ConsoleCapture.prototype.clear = function() {
140
+ this.messages = []
141
+ }
142
+
143
+ module.exports = ConsoleCapture
@@ -0,0 +1,59 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Element Finder
5
+ *
6
+ * Finds elements by ref (populated by SnapshotBuilder)
7
+ */
8
+
9
+ // Global ref map (shared with SnapshotBuilder instances)
10
+ var _globalRefMap = {}
11
+
12
+ function ElementFinder(win) {
13
+ this.win = win
14
+ this.doc = win.document
15
+ }
16
+
17
+ /**
18
+ * Set the global ref map (called by SnapshotBuilder after building)
19
+ */
20
+ ElementFinder.setRefMap = function(refMap) {
21
+ _globalRefMap = refMap
22
+ }
23
+
24
+ /**
25
+ * Find element by ref
26
+ */
27
+ ElementFinder.prototype.findByRef = function(ref) {
28
+ if (ref === 'page') {
29
+ return this.doc.body
30
+ }
31
+ return _globalRefMap[ref] || null
32
+ }
33
+
34
+ /**
35
+ * Get bounding box of element
36
+ */
37
+ ElementFinder.prototype.getBounds = function(element) {
38
+ var rect = element.getBoundingClientRect()
39
+ return {
40
+ x: rect.left,
41
+ y: rect.top,
42
+ width: rect.width,
43
+ height: rect.height
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Check if element is visible
49
+ */
50
+ ElementFinder.prototype.isVisible = function(element) {
51
+ var style = this.win.getComputedStyle(element)
52
+ if (style.display === 'none' || style.visibility === 'hidden') {
53
+ return false
54
+ }
55
+ var rect = element.getBoundingClientRect()
56
+ return rect.width > 0 && rect.height > 0
57
+ }
58
+
59
+ module.exports = ElementFinder
@@ -0,0 +1,180 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Event Dispatcher
5
+ *
6
+ * Dispatches DOM events to elements
7
+ */
8
+
9
+ function EventDispatcher(win) {
10
+ this.win = win
11
+ this.doc = win.document
12
+ }
13
+
14
+ /**
15
+ * Click an element
16
+ */
17
+ EventDispatcher.prototype.click = function(element, button, doubleClick) {
18
+ var buttonCode = 0
19
+ if (button === 'right') buttonCode = 2
20
+ if (button === 'middle') buttonCode = 1
21
+
22
+ // Focus the element if focusable
23
+ if (element.focus) {
24
+ element.focus()
25
+ }
26
+
27
+ // Get element center
28
+ var rect = element.getBoundingClientRect()
29
+ var x = rect.left + rect.width / 2
30
+ var y = rect.top + rect.height / 2
31
+
32
+ // Dispatch mouse events
33
+ this._dispatchMouse(element, 'mousedown', x, y, buttonCode)
34
+ this._dispatchMouse(element, 'mouseup', x, y, buttonCode)
35
+ this._dispatchMouse(element, 'click', x, y, buttonCode)
36
+
37
+ if (doubleClick) {
38
+ this._dispatchMouse(element, 'mousedown', x, y, buttonCode)
39
+ this._dispatchMouse(element, 'mouseup', x, y, buttonCode)
40
+ this._dispatchMouse(element, 'click', x, y, buttonCode)
41
+ this._dispatchMouse(element, 'dblclick', x, y, buttonCode)
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Type text into an element
47
+ */
48
+ EventDispatcher.prototype.type = function(element, text, slowly, submit) {
49
+ // Focus the element
50
+ if (element.focus) {
51
+ element.focus()
52
+ }
53
+
54
+ if (slowly) {
55
+ // Type character by character
56
+ for (var i = 0; i < text.length; i++) {
57
+ var char = text[i]
58
+ this._dispatchKey(element, 'keydown', char)
59
+ this._dispatchKey(element, 'keypress', char)
60
+
61
+ // Update value
62
+ if (element.value !== undefined) {
63
+ element.value += char
64
+ }
65
+
66
+ this._dispatchKey(element, 'keyup', char)
67
+ this.dispatchInput(element)
68
+ }
69
+ } else {
70
+ // Set value directly
71
+ if (element.value !== undefined) {
72
+ element.value = text
73
+ } else if (element.contentEditable === 'true') {
74
+ element.textContent = text
75
+ }
76
+ this.dispatchInput(element)
77
+ }
78
+
79
+ if (submit) {
80
+ this.pressKey('Enter', element)
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Press a key
86
+ */
87
+ EventDispatcher.prototype.pressKey = function(key, target) {
88
+ target = target || this.doc.activeElement || this.doc.body
89
+
90
+ this._dispatchKey(target, 'keydown', key)
91
+ this._dispatchKey(target, 'keypress', key)
92
+ this._dispatchKey(target, 'keyup', key)
93
+
94
+ // Handle Enter key submitting forms
95
+ if (key === 'Enter') {
96
+ var form = target.form || target.closest('form')
97
+ if (form) {
98
+ var submitEvent = new this.win.Event('submit', {
99
+ bubbles: true,
100
+ cancelable: true
101
+ })
102
+ form.dispatchEvent(submitEvent)
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Dispatch input event
109
+ */
110
+ EventDispatcher.prototype.dispatchInput = function(element) {
111
+ var inputEvent = new this.win.Event('input', {
112
+ bubbles: true,
113
+ cancelable: true
114
+ })
115
+ element.dispatchEvent(inputEvent)
116
+
117
+ var changeEvent = new this.win.Event('change', {
118
+ bubbles: true,
119
+ cancelable: true
120
+ })
121
+ element.dispatchEvent(changeEvent)
122
+ }
123
+
124
+ /**
125
+ * Dispatch mouse event
126
+ */
127
+ EventDispatcher.prototype._dispatchMouse = function(element, type, x, y, button) {
128
+ var event = new this.win.MouseEvent(type, {
129
+ view: this.win,
130
+ bubbles: true,
131
+ cancelable: true,
132
+ clientX: x,
133
+ clientY: y,
134
+ button: button
135
+ })
136
+ element.dispatchEvent(event)
137
+ }
138
+
139
+ /**
140
+ * Dispatch keyboard event
141
+ */
142
+ EventDispatcher.prototype._dispatchKey = function(element, type, key) {
143
+ var keyCode = key.length === 1 ? key.charCodeAt(0) : this._getKeyCode(key)
144
+
145
+ var event = new this.win.KeyboardEvent(type, {
146
+ view: this.win,
147
+ bubbles: true,
148
+ cancelable: true,
149
+ key: key,
150
+ code: key.length === 1 ? 'Key' + key.toUpperCase() : key,
151
+ keyCode: keyCode,
152
+ which: keyCode
153
+ })
154
+ element.dispatchEvent(event)
155
+ }
156
+
157
+ /**
158
+ * Get key code for special keys
159
+ */
160
+ EventDispatcher.prototype._getKeyCode = function(key) {
161
+ var codes = {
162
+ 'Enter': 13,
163
+ 'Tab': 9,
164
+ 'Escape': 27,
165
+ 'Backspace': 8,
166
+ 'Delete': 46,
167
+ 'ArrowUp': 38,
168
+ 'ArrowDown': 40,
169
+ 'ArrowLeft': 37,
170
+ 'ArrowRight': 39,
171
+ 'Home': 36,
172
+ 'End': 35,
173
+ 'PageUp': 33,
174
+ 'PageDown': 34,
175
+ 'Space': 32
176
+ }
177
+ return codes[key] || 0
178
+ }
179
+
180
+ module.exports = EventDispatcher