@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,229 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * DOM Snapshot Builder
5
+ *
6
+ * Creates accessibility tree snapshot with element refs
7
+ */
8
+
9
+ var ElementFinder = require('./element-finder')
10
+
11
+ function SnapshotBuilder(win) {
12
+ this.win = win
13
+ this.doc = win.document
14
+ this.refCounter = 0
15
+ this.refMap = {}
16
+ }
17
+
18
+ SnapshotBuilder.prototype.build = function() {
19
+ this.refCounter = 0
20
+ this.refMap = {}
21
+
22
+ var lines = []
23
+ var title = this.doc.title || 'Untitled'
24
+ lines.push('- page "' + title + '" [ref=page]')
25
+
26
+ if (this.doc.body) {
27
+ this._buildNode(this.doc.body, 1, lines)
28
+ }
29
+
30
+ // Update global ref map for ElementFinder
31
+ ElementFinder.setRefMap(this.refMap)
32
+
33
+ return lines.join('\n')
34
+ }
35
+
36
+ SnapshotBuilder.prototype.getRefMap = function() {
37
+ return this.refMap
38
+ }
39
+
40
+ SnapshotBuilder.prototype._buildNode = function(node, depth, lines) {
41
+ if (node.nodeType !== 1) {
42
+ return
43
+ }
44
+
45
+ // Skip hidden elements
46
+ if (!this._isVisible(node)) {
47
+ return
48
+ }
49
+
50
+ // Skip script, style, etc.
51
+ var tagName = node.tagName.toLowerCase()
52
+ if (tagName === 'script' || tagName === 'style' || tagName === 'noscript') {
53
+ return
54
+ }
55
+
56
+ var role = this._getRole(node)
57
+ var name = this._getName(node)
58
+ var ref = 'e' + (++this.refCounter)
59
+
60
+ this.refMap[ref] = node
61
+
62
+ var indent = ''
63
+ for (var i = 0; i < depth; i++) {
64
+ indent += ' '
65
+ }
66
+
67
+ var line = indent + '- ' + role
68
+ if (name) {
69
+ line += ' "' + name + '"'
70
+ }
71
+
72
+ // Add attributes
73
+ var attrs = this._getAttributes(node)
74
+ if (attrs) {
75
+ line += ' ' + attrs
76
+ }
77
+
78
+ line += ' [ref=' + ref + ']'
79
+ lines.push(line)
80
+
81
+ // Process children
82
+ var children = node.children
83
+ for (var j = 0; j < children.length; j++) {
84
+ this._buildNode(children[j], depth + 1, lines)
85
+ }
86
+ }
87
+
88
+ SnapshotBuilder.prototype._isVisible = function(node) {
89
+ var style = this.win.getComputedStyle(node)
90
+ if (style.display === 'none' || style.visibility === 'hidden') {
91
+ return false
92
+ }
93
+ return true
94
+ }
95
+
96
+ SnapshotBuilder.prototype._getRole = function(node) {
97
+ var tagName = node.tagName.toLowerCase()
98
+ var role = node.getAttribute('role')
99
+
100
+ if (role) {
101
+ return role
102
+ }
103
+
104
+ var roleMap = {
105
+ 'button': 'button',
106
+ 'a': 'link',
107
+ 'input': this._getInputRole(node),
108
+ 'textarea': 'textbox',
109
+ 'select': 'combobox',
110
+ 'option': 'option',
111
+ 'h1': 'heading',
112
+ 'h2': 'heading',
113
+ 'h3': 'heading',
114
+ 'h4': 'heading',
115
+ 'h5': 'heading',
116
+ 'h6': 'heading',
117
+ 'ul': 'list',
118
+ 'ol': 'list',
119
+ 'li': 'listitem',
120
+ 'table': 'table',
121
+ 'tr': 'row',
122
+ 'td': 'cell',
123
+ 'th': 'columnheader',
124
+ 'img': 'image',
125
+ 'nav': 'navigation',
126
+ 'main': 'main',
127
+ 'header': 'banner',
128
+ 'footer': 'contentinfo',
129
+ 'aside': 'complementary',
130
+ 'form': 'form',
131
+ 'section': 'region',
132
+ 'article': 'article'
133
+ }
134
+
135
+ return roleMap[tagName] || 'generic'
136
+ }
137
+
138
+ SnapshotBuilder.prototype._getInputRole = function(node) {
139
+ var type = (node.getAttribute('type') || 'text').toLowerCase()
140
+
141
+ var typeRoleMap = {
142
+ 'text': 'textbox',
143
+ 'password': 'textbox',
144
+ 'email': 'textbox',
145
+ 'number': 'spinbutton',
146
+ 'checkbox': 'checkbox',
147
+ 'radio': 'radio',
148
+ 'button': 'button',
149
+ 'submit': 'button',
150
+ 'reset': 'button',
151
+ 'range': 'slider',
152
+ 'search': 'searchbox'
153
+ }
154
+
155
+ return typeRoleMap[type] || 'textbox'
156
+ }
157
+
158
+ SnapshotBuilder.prototype._getName = function(node) {
159
+ // aria-label
160
+ var ariaLabel = node.getAttribute('aria-label')
161
+ if (ariaLabel) {
162
+ return ariaLabel
163
+ }
164
+
165
+ // For inputs, use label or placeholder
166
+ var tagName = node.tagName.toLowerCase()
167
+ if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
168
+ var id = node.getAttribute('id')
169
+ if (id) {
170
+ var label = this.doc.querySelector('label[for="' + id + '"]')
171
+ if (label) {
172
+ return label.textContent.trim()
173
+ }
174
+ }
175
+ var placeholder = node.getAttribute('placeholder')
176
+ if (placeholder) {
177
+ return placeholder
178
+ }
179
+ }
180
+
181
+ // For buttons, links, headings - use text content
182
+ if (tagName === 'button' || tagName === 'a' || tagName.match(/^h[1-6]$/)) {
183
+ return node.textContent.trim().substring(0, 50)
184
+ }
185
+
186
+ // For images
187
+ if (tagName === 'img') {
188
+ return node.getAttribute('alt') || ''
189
+ }
190
+
191
+ // For list items with simple text
192
+ if (tagName === 'li') {
193
+ var text = node.textContent.trim()
194
+ if (text.length < 50 && node.children.length === 0) {
195
+ return text
196
+ }
197
+ }
198
+
199
+ return ''
200
+ }
201
+
202
+ SnapshotBuilder.prototype._getAttributes = function(node) {
203
+ var attrs = []
204
+ var tagName = node.tagName.toLowerCase()
205
+
206
+ if (tagName === 'input') {
207
+ var placeholder = node.getAttribute('placeholder')
208
+ if (placeholder) {
209
+ attrs.push('[placeholder="' + placeholder + '"]')
210
+ }
211
+ if (node.disabled) {
212
+ attrs.push('[disabled]')
213
+ }
214
+ if (node.checked) {
215
+ attrs.push('[checked]')
216
+ }
217
+ }
218
+
219
+ if (tagName === 'a') {
220
+ var href = node.getAttribute('href')
221
+ if (href && href.length < 50) {
222
+ attrs.push('[href="' + href + '"]')
223
+ }
224
+ }
225
+
226
+ return attrs.join(' ')
227
+ }
228
+
229
+ module.exports = SnapshotBuilder
@@ -0,0 +1,232 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * NW.js MCP Client
5
+ *
6
+ * Include this in your NW.js app to enable MCP testing.
7
+ * The MCP server is started by Claude Code via .mcp.json configuration.
8
+ *
9
+ * Usage:
10
+ * var mcp = require('@sveltium/nwjs-mcp')
11
+ * mcp.init({
12
+ * name: 'My App',
13
+ * port: 3940
14
+ * })
15
+ */
16
+
17
+ var ToolExecutor = require('./tool-executor')
18
+
19
+ var DEFAULT_PORT = 3940
20
+ var RECONNECT_DELAY = 2000
21
+
22
+ var _ws = null
23
+ var _appId = null
24
+ var _config = {}
25
+ var _executor = null
26
+ var _reconnectTimer = null
27
+ var _connected = false
28
+ var _win = null
29
+
30
+ /**
31
+ * Initialize MCP client - connects to the MCP server
32
+ * @param {Object} options
33
+ * @param {string} options.name - App name (for identification)
34
+ * @param {string} options.appId - Unique app ID (auto-generated if not provided)
35
+ * @param {number} options.port - WebSocket port (default: 3940)
36
+ * @param {Window} options.window - Browser window reference (auto-detected if not provided)
37
+ * @param {boolean} options.autoReconnect - Auto-reconnect on disconnect (default: true)
38
+ * @param {boolean} options.enabled - Enable MCP (default: true, use false or --no-mcp to disable)
39
+ */
40
+ function init(options) {
41
+ _config = options || {}
42
+ _config.port = _config.port || DEFAULT_PORT
43
+ _config.autoReconnect = _config.autoReconnect !== false
44
+ _config.enabled = _config.enabled !== false
45
+
46
+ // Check for --no-mcp flag to disable
47
+ var nwGui = _getNwGui()
48
+ if (nwGui && nwGui.App && nwGui.App.argv) {
49
+ var argv = nwGui.App.argv
50
+ for (var i = 0; i < argv.length; i++) {
51
+ if (argv[i] === '--no-mcp') {
52
+ _config.enabled = false
53
+ break
54
+ }
55
+ }
56
+ }
57
+
58
+ if (!_config.enabled) {
59
+ console.log('[nwjs-mcp] Disabled via config or --no-mcp flag')
60
+ return
61
+ }
62
+
63
+ // Get window reference
64
+ _win = _config.window || (typeof window !== 'undefined' ? window : null)
65
+ if (!_win) {
66
+ console.error('[nwjs-mcp] No window reference available. Pass { window: window } to init().')
67
+ return
68
+ }
69
+
70
+ // Initialize tool executor
71
+ _executor = new ToolExecutor(_win)
72
+
73
+ // Connect to server
74
+ _connect()
75
+ }
76
+
77
+ function _connect() {
78
+ var WS = _win.WebSocket
79
+ if (_ws && (_ws.readyState === WS.CONNECTING || _ws.readyState === WS.OPEN)) {
80
+ return
81
+ }
82
+
83
+ var url = 'ws://localhost:' + _config.port
84
+
85
+ try {
86
+ _ws = new WS(url)
87
+ } catch (e) {
88
+ console.error('[nwjs-mcp] Failed to create WebSocket:', e.message)
89
+ _scheduleReconnect()
90
+ return
91
+ }
92
+
93
+ _ws.onopen = function() {
94
+ console.log('[nwjs-mcp] Connected to MCP server')
95
+ _connected = true
96
+
97
+ // Register app
98
+ _ws.send(JSON.stringify({
99
+ type: 'register',
100
+ appId: _config.appId || _appId,
101
+ name: _config.name || ''
102
+ }))
103
+ }
104
+
105
+ _ws.onmessage = function(event) {
106
+ var message = null
107
+ try {
108
+ message = JSON.parse(event.data)
109
+ } catch (e) {
110
+ console.error('[nwjs-mcp] Invalid message:', e.message)
111
+ return
112
+ }
113
+
114
+ _handleMessage(message)
115
+ }
116
+
117
+ _ws.onclose = function() {
118
+ console.log('[nwjs-mcp] Disconnected from MCP server')
119
+ _connected = false
120
+ _ws = null
121
+ _scheduleReconnect()
122
+ }
123
+
124
+ _ws.onerror = function() {
125
+ // Error will be followed by close, reconnect handled there
126
+ }
127
+ }
128
+
129
+ function _scheduleReconnect() {
130
+ if (!_config.autoReconnect) {
131
+ return
132
+ }
133
+
134
+ if (_reconnectTimer) {
135
+ clearTimeout(_reconnectTimer)
136
+ }
137
+
138
+ _reconnectTimer = setTimeout(function() {
139
+ _reconnectTimer = null
140
+ console.log('[nwjs-mcp] Attempting to reconnect...')
141
+ _connect()
142
+ }, RECONNECT_DELAY)
143
+ }
144
+
145
+ function _handleMessage(message) {
146
+ if (message.type === 'registered') {
147
+ _appId = message.appId
148
+ console.log('[nwjs-mcp] Registered as:', _appId)
149
+ return
150
+ }
151
+
152
+ if (message.type === 'toolCall') {
153
+ _handleToolCall(message)
154
+ return
155
+ }
156
+ }
157
+
158
+ function _handleToolCall(message) {
159
+ var callId = message.callId
160
+ var tool = message.tool
161
+ var args = message.args
162
+
163
+ _executor.execute(tool, args, function(err, result) {
164
+ var WS = _win.WebSocket
165
+ if (_ws && _ws.readyState === WS.OPEN) {
166
+ if (err) {
167
+ _ws.send(JSON.stringify({
168
+ type: 'toolResult',
169
+ callId: callId,
170
+ error: err.message
171
+ }))
172
+ } else {
173
+ _ws.send(JSON.stringify({
174
+ type: 'toolResult',
175
+ callId: callId,
176
+ result: result
177
+ }))
178
+ }
179
+ }
180
+ })
181
+ }
182
+
183
+ function _getNwGui() {
184
+ try {
185
+ return require('nw.gui')
186
+ } catch (e) {
187
+ return typeof nw !== 'undefined' ? nw : null
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Disconnect from the MCP server
193
+ */
194
+ function disconnect() {
195
+ _config.autoReconnect = false
196
+
197
+ if (_reconnectTimer) {
198
+ clearTimeout(_reconnectTimer)
199
+ _reconnectTimer = null
200
+ }
201
+
202
+ if (_ws) {
203
+ _ws.close()
204
+ _ws = null
205
+ }
206
+
207
+ _connected = false
208
+ }
209
+
210
+ /**
211
+ * Check if connected
212
+ * @returns {boolean}
213
+ */
214
+ function isConnected() {
215
+ return _connected
216
+ }
217
+
218
+ /**
219
+ * Get the app ID
220
+ * @returns {string|null}
221
+ */
222
+ function getAppId() {
223
+ return _appId
224
+ }
225
+
226
+ module.exports = {
227
+ init: init,
228
+ connect: init, // Alias for backwards compatibility
229
+ disconnect: disconnect,
230
+ isConnected: isConnected,
231
+ getAppId: getAppId
232
+ }