@majkapp/plugin-kit 1.3.1 → 1.3.4
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/dist/mcp-dom-agent.js +404 -0
- package/dist/plugin-kit.d.ts.map +1 -1
- package/dist/plugin-kit.js +111 -5
- package/package.json +2 -2
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// mcp-dom-agent.js
|
|
2
|
+
// Automatically injected into all MAJK plugin screens for remote control and screenshots
|
|
3
|
+
|
|
4
|
+
(function () {
|
|
5
|
+
if (window.__mcpDomAgent) return;
|
|
6
|
+
window.__mcpDomAgent = true;
|
|
7
|
+
|
|
8
|
+
// ---- Configuration ----
|
|
9
|
+
const SCREENSHOT_ENDPOINT = '/api/mcp/screenshot';
|
|
10
|
+
let replyId = 0;
|
|
11
|
+
|
|
12
|
+
// ---- Helpers ----
|
|
13
|
+
|
|
14
|
+
function getById(id) {
|
|
15
|
+
return document.querySelector('[data-mcp-id="' + CSS.escape(id) + '"]');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function centerScroll(el) {
|
|
19
|
+
el.scrollIntoView({
|
|
20
|
+
behavior: 'instant',
|
|
21
|
+
block: 'center',
|
|
22
|
+
inline: 'center'
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function elementCenter(el) {
|
|
27
|
+
const r = el.getBoundingClientRect();
|
|
28
|
+
return {
|
|
29
|
+
x: r.left + r.width / 2,
|
|
30
|
+
y: r.top + r.height / 2,
|
|
31
|
+
rect: { x: r.left, y: r.top, width: r.width, height: r.height }
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function dispatchClick(el) {
|
|
36
|
+
const { x, y } = elementCenter(el);
|
|
37
|
+
['mousedown', 'mouseup', 'click'].forEach((type) => {
|
|
38
|
+
el.dispatchEvent(new MouseEvent(type, {
|
|
39
|
+
bubbles: true,
|
|
40
|
+
cancelable: true,
|
|
41
|
+
composed: true,
|
|
42
|
+
clientX: x,
|
|
43
|
+
clientY: y,
|
|
44
|
+
button: 0
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function dispatchHover(el) {
|
|
50
|
+
const { x, y } = elementCenter(el);
|
|
51
|
+
el.dispatchEvent(new MouseEvent('mousemove', {
|
|
52
|
+
bubbles: true,
|
|
53
|
+
cancelable: true,
|
|
54
|
+
composed: true,
|
|
55
|
+
clientX: x,
|
|
56
|
+
clientY: y
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function reply(ev, id, result, error) {
|
|
61
|
+
if (ev.source) {
|
|
62
|
+
ev.source.postMessage(
|
|
63
|
+
{
|
|
64
|
+
__mcpReplyId: id,
|
|
65
|
+
result: result,
|
|
66
|
+
error: error ? String(error) : undefined
|
|
67
|
+
},
|
|
68
|
+
'*'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- Screenshot Capture ----
|
|
74
|
+
|
|
75
|
+
async function captureScreenshot(options = {}) {
|
|
76
|
+
try {
|
|
77
|
+
// Use html2canvas if available, otherwise use basic canvas approach
|
|
78
|
+
if (typeof html2canvas !== 'undefined') {
|
|
79
|
+
const canvas = await html2canvas(document.body, {
|
|
80
|
+
allowTaint: true,
|
|
81
|
+
useCORS: true,
|
|
82
|
+
logging: false,
|
|
83
|
+
...options
|
|
84
|
+
});
|
|
85
|
+
return canvas.toDataURL('image/png');
|
|
86
|
+
} else {
|
|
87
|
+
// Fallback: Use basic canvas API to capture visible viewport
|
|
88
|
+
return captureViewportScreenshot(options);
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('[MCP DOM Agent] Screenshot capture failed:', error);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function captureViewportScreenshot(options = {}) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
try {
|
|
99
|
+
const canvas = document.createElement('canvas');
|
|
100
|
+
const ctx = canvas.getContext('2d');
|
|
101
|
+
|
|
102
|
+
canvas.width = window.innerWidth;
|
|
103
|
+
canvas.height = window.innerHeight;
|
|
104
|
+
|
|
105
|
+
// This is a basic fallback - it won't capture actual content
|
|
106
|
+
// but provides a placeholder implementation
|
|
107
|
+
ctx.fillStyle = '#ffffff';
|
|
108
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
109
|
+
|
|
110
|
+
ctx.fillStyle = '#333333';
|
|
111
|
+
ctx.font = '20px Arial';
|
|
112
|
+
ctx.fillText('Screenshot captured at ' + new Date().toISOString(), 50, 50);
|
|
113
|
+
ctx.fillText('(Install html2canvas for full screenshot support)', 50, 80);
|
|
114
|
+
|
|
115
|
+
resolve(canvas.toDataURL('image/png'));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
reject(error);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function sendScreenshotToBackend(dataUrl, metadata = {}) {
|
|
123
|
+
try {
|
|
124
|
+
const baseUrl = window.__MAJK_BASE_URL__ || '';
|
|
125
|
+
const response = await fetch(baseUrl + SCREENSHOT_ENDPOINT, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
'Content-Type': 'application/json'
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
data: dataUrl,
|
|
132
|
+
metadata: {
|
|
133
|
+
url: window.location.href,
|
|
134
|
+
timestamp: new Date().toISOString(),
|
|
135
|
+
userAgent: navigator.userAgent,
|
|
136
|
+
viewport: {
|
|
137
|
+
width: window.innerWidth,
|
|
138
|
+
height: window.innerHeight
|
|
139
|
+
},
|
|
140
|
+
...metadata
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw new Error(`Screenshot upload failed: ${response.status} ${response.statusText}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const result = await response.json();
|
|
150
|
+
return result;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('[MCP DOM Agent] Failed to send screenshot to backend:', error);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---- Main Listener ----
|
|
158
|
+
|
|
159
|
+
window.addEventListener('message', async function (ev) {
|
|
160
|
+
const msg = ev.data;
|
|
161
|
+
if (!msg || !msg.__mcpCmdId) return;
|
|
162
|
+
|
|
163
|
+
const cmdId = msg.__mcpCmdId;
|
|
164
|
+
const type = msg.type;
|
|
165
|
+
const id = msg.id;
|
|
166
|
+
|
|
167
|
+
let el = null;
|
|
168
|
+
if (typeof id === 'string') {
|
|
169
|
+
el = getById(id);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
switch (type) {
|
|
174
|
+
|
|
175
|
+
case 'find': {
|
|
176
|
+
reply(ev, cmdId, { exists: !!el });
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case 'scrollIntoView': {
|
|
181
|
+
if (!el) throw new Error('not found: ' + id);
|
|
182
|
+
centerScroll(el);
|
|
183
|
+
requestAnimationFrame(function () {
|
|
184
|
+
reply(ev, cmdId, { ok: true });
|
|
185
|
+
});
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case 'click': {
|
|
190
|
+
if (!el) throw new Error('not found: ' + id);
|
|
191
|
+
centerScroll(el);
|
|
192
|
+
requestAnimationFrame(function () {
|
|
193
|
+
dispatchClick(el);
|
|
194
|
+
reply(ev, cmdId, { ok: true });
|
|
195
|
+
});
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case 'hover': {
|
|
200
|
+
if (!el) throw new Error('not found: ' + id);
|
|
201
|
+
centerScroll(el);
|
|
202
|
+
requestAnimationFrame(function () {
|
|
203
|
+
dispatchHover(el);
|
|
204
|
+
reply(ev, cmdId, { ok: true });
|
|
205
|
+
});
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
case 'getRect': {
|
|
210
|
+
if (!el) throw new Error('not found: ' + id);
|
|
211
|
+
const r = el.getBoundingClientRect();
|
|
212
|
+
reply(ev, cmdId, {
|
|
213
|
+
x: r.left,
|
|
214
|
+
y: r.top,
|
|
215
|
+
width: r.width,
|
|
216
|
+
height: r.height
|
|
217
|
+
});
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
case 'captureScreenshot': {
|
|
222
|
+
const options = msg.options || {};
|
|
223
|
+
const metadata = msg.metadata || {};
|
|
224
|
+
const sendToBackend = msg.sendToBackend !== false; // Default true
|
|
225
|
+
|
|
226
|
+
// Capture screenshot
|
|
227
|
+
const dataUrl = await captureScreenshot(options);
|
|
228
|
+
|
|
229
|
+
// Send to backend if requested
|
|
230
|
+
let backendResult = null;
|
|
231
|
+
if (sendToBackend) {
|
|
232
|
+
backendResult = await sendScreenshotToBackend(dataUrl, metadata);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
reply(ev, cmdId, {
|
|
236
|
+
success: true,
|
|
237
|
+
dataUrl: sendToBackend ? undefined : dataUrl, // Don't send back if already sent to backend
|
|
238
|
+
backend: backendResult
|
|
239
|
+
});
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
default: {
|
|
244
|
+
reply(ev, cmdId, null, 'unknown type: ' + type);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch (err) {
|
|
248
|
+
reply(ev, cmdId, null, err && err.message ? err.message : String(err));
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ---- File Picker Bridge ----
|
|
253
|
+
// Since plugins run in cross-origin iframes, they can't directly use File System Access APIs
|
|
254
|
+
// This bridge sends requests to the parent window which executes the APIs and returns results
|
|
255
|
+
|
|
256
|
+
let filePickerRequestId = 0;
|
|
257
|
+
const pendingFilePickerRequests = new Map();
|
|
258
|
+
|
|
259
|
+
// Listen for file picker results from parent window
|
|
260
|
+
window.addEventListener('message', function (ev) {
|
|
261
|
+
const msg = ev.data;
|
|
262
|
+
if (!msg) return;
|
|
263
|
+
|
|
264
|
+
// Handle directory picker result
|
|
265
|
+
if (msg.type === 'majk:file-picker:directory-picker-result') {
|
|
266
|
+
const pending = pendingFilePickerRequests.get(msg.requestId);
|
|
267
|
+
if (pending) {
|
|
268
|
+
pendingFilePickerRequests.delete(msg.requestId);
|
|
269
|
+
if (msg.success) {
|
|
270
|
+
pending.resolve(msg.result);
|
|
271
|
+
} else {
|
|
272
|
+
pending.reject(new Error(msg.error?.message || 'Directory picker failed'));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Handle open file picker result
|
|
278
|
+
if (msg.type === 'majk:file-picker:open-file-picker-result') {
|
|
279
|
+
const pending = pendingFilePickerRequests.get(msg.requestId);
|
|
280
|
+
if (pending) {
|
|
281
|
+
pendingFilePickerRequests.delete(msg.requestId);
|
|
282
|
+
if (msg.success) {
|
|
283
|
+
pending.resolve(msg.results);
|
|
284
|
+
} else {
|
|
285
|
+
pending.reject(new Error(msg.error?.message || 'File picker failed'));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Handle save file picker result
|
|
291
|
+
if (msg.type === 'majk:file-picker:save-file-picker-result') {
|
|
292
|
+
const pending = pendingFilePickerRequests.get(msg.requestId);
|
|
293
|
+
if (pending) {
|
|
294
|
+
pendingFilePickerRequests.delete(msg.requestId);
|
|
295
|
+
if (msg.success) {
|
|
296
|
+
pending.resolve(msg.result);
|
|
297
|
+
} else {
|
|
298
|
+
pending.reject(new Error(msg.error?.message || 'Save file picker failed'));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Polyfill File System Access APIs to use the bridge
|
|
305
|
+
async function showDirectoryPickerBridge(options) {
|
|
306
|
+
const requestId = 'file-picker-' + (filePickerRequestId++);
|
|
307
|
+
|
|
308
|
+
return new Promise((resolve, reject) => {
|
|
309
|
+
pendingFilePickerRequests.set(requestId, { resolve, reject });
|
|
310
|
+
|
|
311
|
+
// Send request to parent window
|
|
312
|
+
window.parent.postMessage({
|
|
313
|
+
type: 'majk:file-picker:show-directory-picker',
|
|
314
|
+
requestId,
|
|
315
|
+
options
|
|
316
|
+
}, '*');
|
|
317
|
+
|
|
318
|
+
// Timeout after 5 minutes
|
|
319
|
+
setTimeout(() => {
|
|
320
|
+
if (pendingFilePickerRequests.has(requestId)) {
|
|
321
|
+
pendingFilePickerRequests.delete(requestId);
|
|
322
|
+
reject(new Error('Directory picker request timed out'));
|
|
323
|
+
}
|
|
324
|
+
}, 300000);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function showOpenFilePickerBridge(options) {
|
|
329
|
+
const requestId = 'file-picker-' + (filePickerRequestId++);
|
|
330
|
+
|
|
331
|
+
return new Promise((resolve, reject) => {
|
|
332
|
+
pendingFilePickerRequests.set(requestId, { resolve, reject });
|
|
333
|
+
|
|
334
|
+
// Send request to parent window
|
|
335
|
+
window.parent.postMessage({
|
|
336
|
+
type: 'majk:file-picker:show-open-file-picker',
|
|
337
|
+
requestId,
|
|
338
|
+
options
|
|
339
|
+
}, '*');
|
|
340
|
+
|
|
341
|
+
// Timeout after 5 minutes
|
|
342
|
+
setTimeout(() => {
|
|
343
|
+
if (pendingFilePickerRequests.has(requestId)) {
|
|
344
|
+
pendingFilePickerRequests.delete(requestId);
|
|
345
|
+
reject(new Error('File picker request timed out'));
|
|
346
|
+
}
|
|
347
|
+
}, 300000);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function showSaveFilePickerBridge(options) {
|
|
352
|
+
const requestId = 'file-picker-' + (filePickerRequestId++);
|
|
353
|
+
|
|
354
|
+
return new Promise((resolve, reject) => {
|
|
355
|
+
pendingFilePickerRequests.set(requestId, { resolve, reject });
|
|
356
|
+
|
|
357
|
+
// Send request to parent window
|
|
358
|
+
window.parent.postMessage({
|
|
359
|
+
type: 'majk:file-picker:show-save-file-picker',
|
|
360
|
+
requestId,
|
|
361
|
+
options
|
|
362
|
+
}, '*');
|
|
363
|
+
|
|
364
|
+
// Timeout after 5 minutes
|
|
365
|
+
setTimeout(() => {
|
|
366
|
+
if (pendingFilePickerRequests.has(requestId)) {
|
|
367
|
+
pendingFilePickerRequests.delete(requestId);
|
|
368
|
+
reject(new Error('Save file picker request timed out'));
|
|
369
|
+
}
|
|
370
|
+
}, 300000);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ---- Global API for direct access ----
|
|
375
|
+
window.mcpDomAgent = {
|
|
376
|
+
version: '1.1.0',
|
|
377
|
+
captureScreenshot: async (options) => {
|
|
378
|
+
const dataUrl = await captureScreenshot(options);
|
|
379
|
+
return dataUrl;
|
|
380
|
+
},
|
|
381
|
+
captureAndSend: async (metadata) => {
|
|
382
|
+
const dataUrl = await captureScreenshot();
|
|
383
|
+
const result = await sendScreenshotToBackend(dataUrl, metadata);
|
|
384
|
+
return result;
|
|
385
|
+
},
|
|
386
|
+
// File picker APIs - use these instead of native File System Access APIs in plugin iframes
|
|
387
|
+
showDirectoryPicker: showDirectoryPickerBridge,
|
|
388
|
+
showOpenFilePicker: showOpenFilePickerBridge,
|
|
389
|
+
showSaveFilePicker: showSaveFilePickerBridge
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// Also expose file pickers as global functions for convenience (matching native API)
|
|
393
|
+
if (!window.showDirectoryPicker) {
|
|
394
|
+
window.showDirectoryPicker = showDirectoryPickerBridge;
|
|
395
|
+
}
|
|
396
|
+
if (!window.showOpenFilePicker) {
|
|
397
|
+
window.showOpenFilePicker = showOpenFilePickerBridge;
|
|
398
|
+
}
|
|
399
|
+
if (!window.showSaveFilePicker) {
|
|
400
|
+
window.showSaveFilePicker = showSaveFilePickerBridge;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
console.log('[MCP DOM Agent] Initialized v1.1.0 with File Picker Bridge');
|
|
404
|
+
})();
|
package/dist/plugin-kit.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-kit.d.ts","sourceRoot":"","sources":["../src/plugin-kit.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,aAAa,EAGb,eAAe,EACf,QAAQ,EACR,WAAW,EACX,WAAW,EACX,UAAU,EACV,QAAQ,EACR,WAAW,EACX,UAAU,EACV,eAAe,EACf,WAAW,EACX,KAAK,EACL,UAAU,EACV,SAAS,EACT,aAAa,EAIb,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,eAAe,EACf,mBAAmB,EACnB,SAAS,EACT,iBAAiB,EAGjB,sBAAsB,EACvB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin-kit.d.ts","sourceRoot":"","sources":["../src/plugin-kit.ts"],"names":[],"mappings":"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"}
|
package/dist/plugin-kit.js
CHANGED
|
@@ -389,20 +389,44 @@ function serveSpa(ui, req, res, ctx) {
|
|
|
389
389
|
return;
|
|
390
390
|
}
|
|
391
391
|
let content = fs_1.default.readFileSync(targetFile);
|
|
392
|
-
// Inject base URL for React apps
|
|
392
|
+
// Inject base URL for React apps and MCP DOM Agent
|
|
393
393
|
if (path_1.default.basename(targetFile) === 'index.html') {
|
|
394
394
|
const html = content.toString('utf-8');
|
|
395
|
-
|
|
395
|
+
// Inject configuration
|
|
396
|
+
const configInject = `<script>` +
|
|
396
397
|
`window.__MAJK_BASE_URL__=${JSON.stringify(ctx.http.baseUrl)};` +
|
|
397
398
|
`window.__MAJK_IFRAME_BASE__=${JSON.stringify(ui.base)};` +
|
|
398
399
|
`window.__MAJK_PLUGIN_ID__=${JSON.stringify(ctx.pluginId)};` +
|
|
399
400
|
`</script>`;
|
|
400
|
-
|
|
401
|
+
// Inject MCP DOM Agent script
|
|
402
|
+
const mcpScript = BuiltPlugin.mcpDomAgentScript
|
|
403
|
+
? `<script>${BuiltPlugin.mcpDomAgentScript}</script>`
|
|
404
|
+
: '';
|
|
405
|
+
const allInjections = configInject + mcpScript;
|
|
406
|
+
const injected = html.replace('</head>', `${allInjections}</head>`);
|
|
401
407
|
content = Buffer.from(injected, 'utf-8');
|
|
402
408
|
}
|
|
403
409
|
res.writeHead(200, corsHeaders({ 'Content-Type': getContentType(targetFile) }));
|
|
404
410
|
res.end(content);
|
|
405
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Load MCP DOM Agent script
|
|
414
|
+
*/
|
|
415
|
+
function loadMcpDomAgentScript() {
|
|
416
|
+
try {
|
|
417
|
+
const scriptPath = path_1.default.join(__dirname, 'mcp-dom-agent.js');
|
|
418
|
+
if (fs_1.default.existsSync(scriptPath)) {
|
|
419
|
+
return fs_1.default.readFileSync(scriptPath, 'utf-8');
|
|
420
|
+
}
|
|
421
|
+
// Fallback: return minimal inline version if file not found
|
|
422
|
+
log('[MCP DOM Agent] Script file not found, using minimal fallback');
|
|
423
|
+
return `(function(){if(window.__mcpDomAgent)return;window.__mcpDomAgent=true;console.log('[MCP DOM Agent] Minimal fallback loaded');})();`;
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
log(`[MCP DOM Agent] Failed to load script: ${error.message}`);
|
|
427
|
+
return '';
|
|
428
|
+
}
|
|
429
|
+
}
|
|
406
430
|
/**
|
|
407
431
|
* Main plugin class built by the fluent API
|
|
408
432
|
*/
|
|
@@ -560,6 +584,13 @@ class BuiltPlugin {
|
|
|
560
584
|
context.logger.info(`🌐 HTTP Port: ${context.http.port}`);
|
|
561
585
|
context.logger.info(`🔗 Base URL: ${context.http.baseUrl}`);
|
|
562
586
|
context.logger.info('═══════════════════════════════════════════════════════');
|
|
587
|
+
// Load MCP DOM Agent script once for all plugins
|
|
588
|
+
if (!BuiltPlugin.mcpDomAgentScript) {
|
|
589
|
+
BuiltPlugin.mcpDomAgentScript = loadMcpDomAgentScript();
|
|
590
|
+
if (BuiltPlugin.mcpDomAgentScript) {
|
|
591
|
+
context.logger.info('✅ MCP DOM Agent script loaded - will be injected into all screens');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
563
594
|
// Validate file paths now that we have pluginRoot context
|
|
564
595
|
if (this.reactScreens.length > 0 && this.uiConfig) {
|
|
565
596
|
const indexPath = path_1.default.join(this.context.pluginRoot, this.uiConfig.appDir || '', 'index.html');
|
|
@@ -814,6 +845,63 @@ class BuiltPlugin {
|
|
|
814
845
|
this.context.logger.debug(`← ${method} ${url} 200 (${duration}ms)`);
|
|
815
846
|
return;
|
|
816
847
|
}
|
|
848
|
+
// MCP Screenshot endpoint
|
|
849
|
+
if (req.method === 'POST' && req.url === '/api/mcp/screenshot') {
|
|
850
|
+
try {
|
|
851
|
+
const body = await readBody(req);
|
|
852
|
+
if (!body || !body.data) {
|
|
853
|
+
res.writeHead(400, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
854
|
+
res.end(JSON.stringify({ error: 'Missing screenshot data' }));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
// Create screenshots directory if it doesn't exist
|
|
858
|
+
const screenshotsDir = path_1.default.join(this.context.pluginRoot, 'screenshots');
|
|
859
|
+
if (!fs_1.default.existsSync(screenshotsDir)) {
|
|
860
|
+
fs_1.default.mkdirSync(screenshotsDir, { recursive: true });
|
|
861
|
+
}
|
|
862
|
+
// Generate filename
|
|
863
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
864
|
+
const metadata = body.metadata || {};
|
|
865
|
+
const customName = metadata.filename || `screenshot-${timestamp}`;
|
|
866
|
+
const filename = `${customName}.png`;
|
|
867
|
+
const filepath = path_1.default.join(screenshotsDir, filename);
|
|
868
|
+
// Extract base64 data (remove data:image/png;base64, prefix if present)
|
|
869
|
+
const base64Data = body.data.replace(/^data:image\/\w+;base64,/, '');
|
|
870
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
871
|
+
// Write file
|
|
872
|
+
fs_1.default.writeFileSync(filepath, buffer);
|
|
873
|
+
// Also save metadata
|
|
874
|
+
const metadataPath = path_1.default.join(screenshotsDir, `${customName}.json`);
|
|
875
|
+
fs_1.default.writeFileSync(metadataPath, JSON.stringify({
|
|
876
|
+
...metadata,
|
|
877
|
+
filename,
|
|
878
|
+
filepath,
|
|
879
|
+
timestamp: new Date().toISOString(),
|
|
880
|
+
size: buffer.length
|
|
881
|
+
}, null, 2));
|
|
882
|
+
this.context.logger.info(`📸 Screenshot saved: ${filepath}`);
|
|
883
|
+
res.writeHead(200, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
884
|
+
res.end(JSON.stringify({
|
|
885
|
+
success: true,
|
|
886
|
+
filename,
|
|
887
|
+
filepath,
|
|
888
|
+
size: buffer.length,
|
|
889
|
+
message: 'Screenshot saved successfully'
|
|
890
|
+
}));
|
|
891
|
+
const duration = Date.now() - startTime;
|
|
892
|
+
this.context.logger.debug(`← ${method} ${url} 200 (${duration}ms)`);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
this.context.logger.error(`Failed to save screenshot: ${error.message}`);
|
|
897
|
+
res.writeHead(500, corsHeaders({ 'Content-Type': 'application/json' }));
|
|
898
|
+
res.end(JSON.stringify({
|
|
899
|
+
success: false,
|
|
900
|
+
error: error.message || 'Failed to save screenshot'
|
|
901
|
+
}));
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
817
905
|
// HTML screens virtual route
|
|
818
906
|
if (req.method === 'GET' && req.url?.startsWith('/__html/')) {
|
|
819
907
|
const id = decodeURIComponent(req.url.substring('/__html/'.length)).split('?')[0];
|
|
@@ -826,8 +914,9 @@ class BuiltPlugin {
|
|
|
826
914
|
return;
|
|
827
915
|
}
|
|
828
916
|
res.writeHead(200, corsHeaders({ 'Content-Type': 'text/html; charset=utf-8' }));
|
|
917
|
+
let htmlContent;
|
|
829
918
|
if ('html' in screen) {
|
|
830
|
-
|
|
919
|
+
htmlContent = screen.html;
|
|
831
920
|
}
|
|
832
921
|
else {
|
|
833
922
|
const filePath = path_1.default.join(this.context.pluginRoot, screen.htmlFile);
|
|
@@ -843,7 +932,7 @@ class BuiltPlugin {
|
|
|
843
932
|
return;
|
|
844
933
|
}
|
|
845
934
|
try {
|
|
846
|
-
|
|
935
|
+
htmlContent = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
847
936
|
}
|
|
848
937
|
catch (error) {
|
|
849
938
|
this.context.logger.error(`Failed to read HTML file: ${error.message}`);
|
|
@@ -854,6 +943,21 @@ class BuiltPlugin {
|
|
|
854
943
|
return;
|
|
855
944
|
}
|
|
856
945
|
}
|
|
946
|
+
// Inject MCP DOM Agent script into HTML screens
|
|
947
|
+
if (BuiltPlugin.mcpDomAgentScript) {
|
|
948
|
+
const mcpScript = `<script>${BuiltPlugin.mcpDomAgentScript}</script>`;
|
|
949
|
+
// Try to inject before </body>, fallback to </head>, fallback to end of HTML
|
|
950
|
+
if (htmlContent.includes('</body>')) {
|
|
951
|
+
htmlContent = htmlContent.replace('</body>', `${mcpScript}</body>`);
|
|
952
|
+
}
|
|
953
|
+
else if (htmlContent.includes('</head>')) {
|
|
954
|
+
htmlContent = htmlContent.replace('</head>', `${mcpScript}</head>`);
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
htmlContent += mcpScript;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
res.end(htmlContent);
|
|
857
961
|
const duration = Date.now() - startTime;
|
|
858
962
|
this.context.logger.debug(`← ${method} ${url} 200 (${duration}ms)`);
|
|
859
963
|
return;
|
|
@@ -1002,6 +1106,8 @@ class BuiltPlugin {
|
|
|
1002
1106
|
});
|
|
1003
1107
|
}
|
|
1004
1108
|
}
|
|
1109
|
+
// MCP DOM Agent script (loaded once at startup, accessible to serveSpa function)
|
|
1110
|
+
BuiltPlugin.mcpDomAgentScript = null;
|
|
1005
1111
|
/**
|
|
1006
1112
|
* Group tools by scope
|
|
1007
1113
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@majkapp/plugin-kit",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "Fluent builder framework for creating robust MAJK plugins",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
|
-
"build": "tsc",
|
|
24
|
+
"build": "tsc && cp src/mcp-dom-agent.js dist/mcp-dom-agent.js",
|
|
25
25
|
"watch": "tsc --watch",
|
|
26
26
|
"clean": "rm -rf dist"
|
|
27
27
|
},
|