-
Notifications
You must be signed in to change notification settings - Fork 400
Expand file tree
/
Copy pathpeekaboo-mcp.js
More file actions
executable file
·146 lines (122 loc) · 4.24 KB
/
Copy pathpeekaboo-mcp.js
File metadata and controls
executable file
·146 lines (122 loc) · 4.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#!/usr/bin/env node
// Peekaboo MCP wrapper that restarts the Swift server on crash
import { spawn } from 'child_process';
import { chmodSync, existsSync, realpathSync, statSync } from 'fs';
import { fileURLToPath, pathToFileURL } from 'url';
import { dirname, join } from 'path';
const modulePath = realpathSync(fileURLToPath(import.meta.url));
const __dirname = dirname(modulePath);
const defaultBinaryPath = join(__dirname, 'peekaboo');
const MAX_RESTARTS = 5;
const RESTART_WINDOW_MS = 60_000;
const INITIAL_DELAY_MS = 1000;
const MAX_DELAY_MS = 30_000;
export class PeekabooMCPWrapper {
constructor(options = {}) {
this.binaryPath = options.binaryPath || defaultBinaryPath;
this.initialDelayMs = options.initialDelayMs || INITIAL_DELAY_MS;
this.maxDelayMs = options.maxDelayMs || MAX_DELAY_MS;
this.restartTimestamps = [];
this.delay = this.initialDelayMs;
this.child = null;
this.restartTimer = null;
this.shuttingDown = false;
}
start() {
if (this.shuttingDown) return;
if (!existsSync(this.binaryPath)) {
console.error(`[Peekaboo MCP] Binary not found at ${this.binaryPath}`);
process.exit(1);
}
const now = Date.now();
this.restartTimestamps = this.restartTimestamps.filter(ts => now - ts < RESTART_WINDOW_MS);
if (this.restartTimestamps.length >= MAX_RESTARTS) {
console.error(`[Peekaboo MCP] Aborting: restarted ${MAX_RESTARTS} times within a minute.`);
process.exit(1);
}
console.error('[Peekaboo MCP] Starting Swift server...');
this.child = spawn(this.binaryPath, ['mcp', 'serve'], {
stdio: 'inherit',
env: {
...process.env,
PEEKABOO_MCP_WRAPPER: 'true'
}
});
this.child.on('exit', (code, signal) => {
this.child = null;
if (this.shuttingDown) return process.exit(code || 0);
if (code === 0 || signal === 'SIGINT' || signal === 'SIGTERM') {
console.error('[Peekaboo MCP] Server exited cleanly');
process.exit(code || 0);
}
this.handleCrash(code, signal);
});
this.child.on('error', (err) => {
this.child = null;
console.error('[Peekaboo MCP] Failed to launch:', err.message);
if (err.code === 'EACCES') {
try {
const mode = statSync(this.binaryPath).mode;
chmodSync(this.binaryPath, mode | 0o111);
console.error('[Peekaboo MCP] Fixed executable bit, retrying...');
this.handleCrash(1);
return;
} catch (_) {
console.error('[Peekaboo MCP] Could not make binary executable.');
}
}
this.handleCrash(err.code || 1);
});
}
handleCrash(code, signal) {
if (this.shuttingDown) return;
console.error(`[Peekaboo MCP] Server crashed (code ${code}${signal ? `, signal ${signal}` : ''}).`);
this.restartTimestamps.push(Date.now());
this.clearRestartTimer();
this.restartTimer = setTimeout(() => {
this.restartTimer = null;
if (this.shuttingDown) return;
this.delay = Math.min(this.delay * 2, this.maxDelayMs);
this.start();
}, this.delay);
}
shutdown() {
this.shuttingDown = true;
this.clearRestartTimer();
if (this.child && !this.child.killed) {
this.child.kill('SIGTERM');
}
}
clearRestartTimer() {
if (this.restartTimer) {
clearTimeout(this.restartTimer);
this.restartTimer = null;
}
}
}
if (isMainModule()) {
const wrapper = new PeekabooMCPWrapper();
wrapper.start();
process.on('SIGINT', () => {
console.error('\n[Peekaboo MCP] SIGINT received, shutting down...');
wrapper.shutdown();
});
process.on('SIGTERM', () => {
console.error('[Peekaboo MCP] SIGTERM received, shutting down...');
wrapper.shutdown();
});
process.on('uncaughtException', (err) => {
console.error('[Peekaboo MCP] Uncaught exception:', err);
wrapper.shutdown();
});
process.on('unhandledRejection', (reason) => {
console.error('[Peekaboo MCP] Unhandled rejection:', reason);
});
}
function isMainModule() {
const entry = process.argv[1];
if (entry === undefined) return false;
const entryUrl = pathToFileURL(entry).href;
if (import.meta.url === entryUrl) return true;
return pathToFileURL(realpathSync(modulePath)).href === pathToFileURL(realpathSync(entry)).href;
}