@imenam/simple-scraper 1.0.9 → 1.0.13
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/gui.js +68 -7
- package/dist/index.js +28 -7
- package/package.json +2 -2
package/dist/gui.js
CHANGED
|
@@ -20,6 +20,47 @@ if (!process.env.PROXY_URL) {
|
|
|
20
20
|
process.stderr.write('[GUI] PROXY_URL non défini — arrêt.\n');
|
|
21
21
|
process.exit(0);
|
|
22
22
|
}
|
|
23
|
+
const APP_PATH = process.env.PROXY_APP_PATH ?? '/simple-scraper-mcp';
|
|
24
|
+
const APP_NAME = process.env.PROXY_APP_NAME ?? 'Simple Scraper MCP';
|
|
25
|
+
// ─── Cycle de vie du process ─────────────────────────────────────────────────
|
|
26
|
+
let proxyClient = null;
|
|
27
|
+
/**
|
|
28
|
+
* Libère la route avant de mourir. Sans ça le proxy continue de router vers un
|
|
29
|
+
* port mort (502 opaques) jusqu'à ce que son health check finisse par nettoyer.
|
|
30
|
+
*/
|
|
31
|
+
async function cleanupProxy() {
|
|
32
|
+
if (proxyClient)
|
|
33
|
+
await proxyClient.unregister();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
process.on('SIGTERM', () => void cleanupProxy());
|
|
37
|
+
process.on('SIGINT', () => void cleanupProxy());
|
|
38
|
+
// Le master peut disparaître sans nous tuer (crash, kill -9) : sans ce watchdog
|
|
39
|
+
// la GUI reste orpheline et garde le port et la route du proxy.
|
|
40
|
+
const parentPid = process.ppid;
|
|
41
|
+
const parentCheckInterval = setInterval(() => {
|
|
42
|
+
try {
|
|
43
|
+
process.kill(parentPid, 0);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
console.error(`[GUI] Master (PID ${parentPid}) disparu — arrêt.`);
|
|
47
|
+
clearInterval(parentCheckInterval);
|
|
48
|
+
void cleanupProxy();
|
|
49
|
+
}
|
|
50
|
+
}, 2000);
|
|
51
|
+
process.on('disconnect', () => {
|
|
52
|
+
console.error('[GUI] Canal IPC fermé par le master — arrêt.');
|
|
53
|
+
void cleanupProxy();
|
|
54
|
+
});
|
|
55
|
+
process.on('uncaughtException', (err) => {
|
|
56
|
+
console.error(`[GUI] Uncaught Exception: ${err.message}`);
|
|
57
|
+
console.error(err.stack ?? 'No stack trace');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
});
|
|
60
|
+
process.on('unhandledRejection', (reason) => {
|
|
61
|
+
console.error(`[GUI] Unhandled Rejection: ${reason}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
23
64
|
const getCookiesDir = () => process.env.COOKIES_DIR ?? null;
|
|
24
65
|
const app = express();
|
|
25
66
|
app.use(bodyParser.json());
|
|
@@ -130,20 +171,40 @@ app.get('/api/sessions/:id/screenshot', async (req, res) => {
|
|
|
130
171
|
}
|
|
131
172
|
});
|
|
132
173
|
async function startServer() {
|
|
133
|
-
const
|
|
134
|
-
const result = await
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
174
|
+
const client = new ProxyClient(process.env.PROXY_URL);
|
|
175
|
+
const result = await client.register({ path: APP_PATH, name: APP_NAME });
|
|
176
|
+
// Une autre instance du serveur MCP sert déjà cette GUI. C'est le cas nominal
|
|
177
|
+
// quand le client redémarre le serveur : il lance le nouveau master avant de
|
|
178
|
+
// couper l'ancien, dont la GUI tient encore la route. Ce n'est pas un crash —
|
|
179
|
+
// on sort en 0 (sinon le launcher log "Giving up") sans toucher au proxy, dont
|
|
180
|
+
// la route appartient à l'autre instance.
|
|
181
|
+
if (result.error === 'HTTP 409') {
|
|
182
|
+
console.error('[GUI] Une autre instance sert déjà cette GUI — arrêt.');
|
|
183
|
+
ipc.send({
|
|
184
|
+
type: 'ALREADY_RUNNING',
|
|
185
|
+
data: { url: `${process.env.PROXY_URL}${APP_PATH}` },
|
|
186
|
+
timestamp: new Date().toISOString(),
|
|
187
|
+
});
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
138
190
|
if (!result.success) {
|
|
139
|
-
process.stderr.write(
|
|
191
|
+
process.stderr.write(`[GUI] Enregistrement proxy échoué (${result.error}) — arrêt.\n`);
|
|
140
192
|
process.exit(1);
|
|
141
193
|
}
|
|
194
|
+
// À partir d'ici la route nous appartient : cleanupProxy peut la libérer.
|
|
195
|
+
proxyClient = client;
|
|
142
196
|
const port = result.port;
|
|
197
|
+
// Signale au master que la GUI est enregistrée et joignable, pour que
|
|
198
|
+
// reconnect_gui se résolve immédiatement au lieu d'attendre le timeout.
|
|
199
|
+
ipc.send({
|
|
200
|
+
type: 'READY',
|
|
201
|
+
data: { url: result.url ?? `${process.env.PROXY_URL}${APP_PATH}` },
|
|
202
|
+
timestamp: new Date().toISOString(),
|
|
203
|
+
});
|
|
143
204
|
app.post('/api/stop', async (_req, res) => {
|
|
144
205
|
console.error('POST /api/stop called');
|
|
145
206
|
res.json({ success: true, message: 'Stopping...' });
|
|
146
|
-
await
|
|
207
|
+
await client.unregister();
|
|
147
208
|
setTimeout(() => {
|
|
148
209
|
if (process.send && process.connected) {
|
|
149
210
|
process.send({ type: 'STOP' });
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { z } from 'zod';
|
|
|
9
9
|
import { setupLogging } from './logger.js';
|
|
10
10
|
import { getBrowser, closeBrowser, getDefaultTimeout } from './browser.js';
|
|
11
11
|
import { loadAllCookies } from './cookies.js';
|
|
12
|
-
import { GuiLauncher } from '@imenam/mcp-gui-interface';
|
|
12
|
+
import { GuiLauncher, reconnectGuiToolDefinition } from '@imenam/mcp-gui-interface';
|
|
13
13
|
import { createSession, getSession, closeSession, closeAllSessions, listSessions, getSessionLogs, clearSessionLogs } from './sessions.js';
|
|
14
14
|
import { registerSessionRpc } from './session-rpc.js';
|
|
15
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -19,9 +19,14 @@ dotenv.config({ path: envPath, override: true });
|
|
|
19
19
|
setupLogging('MASTER');
|
|
20
20
|
const launcher = new GuiLauncher({
|
|
21
21
|
guiPath: path.join(__dirname, 'gui.js'),
|
|
22
|
+
reloadEnv: () => dotenv.config({ path: envPath, override: true }),
|
|
22
23
|
onMessage: (msg) => {
|
|
23
|
-
if (msg.type === 'STOP')
|
|
24
|
+
if (msg.type === 'STOP') {
|
|
24
25
|
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
else if (msg.type === 'ALREADY_RUNNING') {
|
|
28
|
+
console.error(`[MASTER] GUI déjà servie par une autre instance : ${msg.data?.url ?? 'URL inconnue'}`);
|
|
29
|
+
}
|
|
25
30
|
},
|
|
26
31
|
});
|
|
27
32
|
const server = new McpServer({
|
|
@@ -193,6 +198,18 @@ server.tool('scrape_page', 'Navigate to a URL using a headless browser and retur
|
|
|
193
198
|
await page.close();
|
|
194
199
|
}
|
|
195
200
|
});
|
|
201
|
+
// ─── reconnect_gui ──────────────────────────────────────────────────────────────
|
|
202
|
+
server.tool('reconnect_gui', reconnectGuiToolDefinition().description, {
|
|
203
|
+
force: z
|
|
204
|
+
.boolean()
|
|
205
|
+
.optional()
|
|
206
|
+
.describe('Restart the GUI worker even if it is already running and registered (default: false)'),
|
|
207
|
+
}, async ({ force }) => {
|
|
208
|
+
const res = await launcher.handleReconnectTool({ force });
|
|
209
|
+
return res.isError
|
|
210
|
+
? { content: res.content, isError: true }
|
|
211
|
+
: { content: res.content };
|
|
212
|
+
});
|
|
196
213
|
// ─── execute_js ───────────────────────────────────────────────────────────────
|
|
197
214
|
server.tool('execute_js', 'Navigate to a URL and execute custom JavaScript in the page context. The script is executed as a JavaScript function body with new Function(script)(), so it must use an explicit return statement to send data back to the tool. A bare expression such as document.title returns undefined. Return serializable values only; objects and arrays are returned as formatted JSON, primitives as text. For async work, return a Promise, for example: return (async () => { /* ... */ return data; })();', {
|
|
198
215
|
url: z.string().url().describe('URL of the page'),
|
|
@@ -609,17 +626,21 @@ server.tool('screenshot', 'Take a screenshot of a page. Provide either session_i
|
|
|
609
626
|
}
|
|
610
627
|
}
|
|
611
628
|
});
|
|
612
|
-
|
|
629
|
+
function launchGUI() {
|
|
613
630
|
if (!process.env.PROXY_URL) {
|
|
614
631
|
console.error('[MASTER] PROXY_URL non défini — GUI désactivée.');
|
|
632
|
+
return;
|
|
615
633
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
634
|
+
launcher.start();
|
|
635
|
+
registerSessionRpc(launcher);
|
|
636
|
+
}
|
|
637
|
+
async function main() {
|
|
620
638
|
const transport = new StdioServerTransport();
|
|
621
639
|
await server.connect(transport);
|
|
622
640
|
console.error('[MCP] Simple Scraper MCP server started');
|
|
641
|
+
// Après le connect, comme mcp-documentor : au redémarrage du serveur par le
|
|
642
|
+
// client, le master sortant a alors eu le temps de libérer la route.
|
|
643
|
+
launchGUI();
|
|
623
644
|
}
|
|
624
645
|
main().catch(async (err) => {
|
|
625
646
|
console.error('[MCP] Fatal error:', err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imenam/simple-scraper",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "MCP server for web scraping and JavaScript execution using Puppeteer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"node": ">=18.0.0"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@imenam/mcp-gui-interface": "^1.0.
|
|
38
|
+
"@imenam/mcp-gui-interface": "^1.0.10",
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
40
40
|
"body-parser": "^2.2.2",
|
|
41
41
|
"dotenv": "^17.3.1",
|