@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 +146 -0
- package/package.json +50 -0
- package/scripts/postinstall.js +96 -0
- package/src/client/dom/console-capture.js +143 -0
- package/src/client/dom/element-finder.js +59 -0
- package/src/client/dom/event-dispatcher.js +180 -0
- package/src/client/dom/snapshot-builder.js +229 -0
- package/src/client/index.js +232 -0
- package/src/client/tool-executor.js +630 -0
- package/src/server/index.js +115 -0
- package/src/server/mcp-handler.js +722 -0
- package/src/server/ws-bridge.js +212 -0
- package/types/index.d.ts +58 -0
|
@@ -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
|
+
}
|