@yusufffararatt/dombridge-mcp 2.7.5
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 +559 -0
- package/bin/cli.js +88 -0
- package/package.json +54 -0
- package/src/bridge/http-server.js +290 -0
- package/src/bridge/middleware.js +56 -0
- package/src/bridge/routes.js +1003 -0
- package/src/bridge-daemon.js +172 -0
- package/src/cli/auto-config.js +120 -0
- package/src/constants.js +13 -0
- package/src/index.js +279 -0
- package/src/mcp-bridge.js +136 -0
- package/src/metrics/error-codes.js +44 -0
- package/src/metrics/index.js +3 -0
- package/src/metrics/metrics-db.js +269 -0
- package/src/metrics/metrics-recorder.js +240 -0
- package/src/metrics/metrics-report.js +146 -0
- package/src/profiles/profile-db.js +159 -0
- package/src/profiles/profile-enricher.js +333 -0
- package/src/profiles/profile-manager.js +563 -0
- package/src/profiles/profile-repo.js +183 -0
- package/src/state/bridge-client.js +272 -0
- package/src/state/bridge-persistence.js +205 -0
- package/src/state/cache.js +38 -0
- package/src/state/extension-state.js +321 -0
- package/src/tools/action_tools.js +218 -0
- package/src/tools/analyze-page.js +247 -0
- package/src/tools/debug-mcp-state.js +172 -0
- package/src/tools/discover-apis.js +186 -0
- package/src/tools/execute-js.js +284 -0
- package/src/tools/export-session.js +171 -0
- package/src/tools/extract-data.js +395 -0
- package/src/tools/get-element.js +281 -0
- package/src/tools/get-network-trace.js +471 -0
- package/src/tools/index.js +110 -0
- package/src/tools/manage-site-profile.js +153 -0
- package/src/tools/paginate.js +444 -0
- package/src/tools/quick-scan.js +418 -0
- package/src/tools/screenshot_tools.js +117 -0
- package/src/utils/circuit-breaker.js +112 -0
- package/src/utils/extract-density.js +21 -0
- package/src/utils/logger.js +31 -0
- package/src/utils/paginate-detector.js +24 -0
- package/src/utils/rate-limiter.js +244 -0
- package/src/utils/run-script.js +37 -0
- package/src/utils/selector-validator.js +95 -0
- package/src/utils/state-validator.js +354 -0
- package/src/utils/tab-resolver.js +70 -0
- package/src/utils/workflow-helper.js +292 -0
- package/src/utils/workflow-state.js +177 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bridge Daemon — Standalone HTTP Bridge Process
|
|
5
|
+
*
|
|
6
|
+
* Phase 2.1: Runs the HTTP bridge (port 3101) independently from the MCP client.
|
|
7
|
+
* The MCP client connects to this daemon via HTTP instead of managing its own bridge.
|
|
8
|
+
*
|
|
9
|
+
* Lifecycle:
|
|
10
|
+
* - Started by MCP client (auto-spawn) or manually via `npm run bridge`
|
|
11
|
+
* - Survives MCP client restarts — extension connections are NOT lost
|
|
12
|
+
* - Graceful shutdown on SIGTERM/SIGINT with state persistence
|
|
13
|
+
* - PID file at mcp-server/state/bridge.pid for tracking
|
|
14
|
+
*
|
|
15
|
+
* Architecture:
|
|
16
|
+
* - Manages extensionData (in-memory + disk persistence)
|
|
17
|
+
* - Serves all /api/* routes for extension communication
|
|
18
|
+
* - NO MCP stdio transport — that's the client's responsibility
|
|
19
|
+
* - NO /api/die kill logic — this daemon decides when to exit
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import { dirname, join } from 'path';
|
|
25
|
+
|
|
26
|
+
import { extensionData } from './state/extension-state.js';
|
|
27
|
+
import { loadPersistedState, persistStateNow } from './state/bridge-persistence.js';
|
|
28
|
+
import { createHttpServer, startHttpServer, connectionHealth } from './bridge/http-server.js';
|
|
29
|
+
import { MetricsDB } from './metrics/metrics-db.js';
|
|
30
|
+
|
|
31
|
+
// Metrics DB for lifecycle events
|
|
32
|
+
const metricsDB = new MetricsDB();
|
|
33
|
+
|
|
34
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const { version: _mcpVersion } = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
|
|
36
|
+
|
|
37
|
+
const HTTP_PORT = parseInt(process.env.MCP_PORT || process.env.PORT || '3101', 10);
|
|
38
|
+
const PID_FILE = join(__dirname, '..', 'state', 'bridge.pid');
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// PID FILE MANAGEMENT
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
function writePidFile() {
|
|
45
|
+
try {
|
|
46
|
+
writeFileSync(PID_FILE, String(process.pid), 'utf8');
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(`[Bridge Daemon] Failed to write PID file: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removePidFile() {
|
|
53
|
+
try {
|
|
54
|
+
unlinkSync(PID_FILE);
|
|
55
|
+
} catch {
|
|
56
|
+
// File may not exist, that's fine
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// STARTUP
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
async function start() {
|
|
65
|
+
console.error(`[Bridge Daemon] v${_mcpVersion} starting on port ${HTTP_PORT} (standalone mode)`);
|
|
66
|
+
console.error(`[Bridge Daemon] PID: ${process.pid}`);
|
|
67
|
+
|
|
68
|
+
// Load persisted state from disk
|
|
69
|
+
const stateLoaded = loadPersistedState(extensionData);
|
|
70
|
+
if (stateLoaded) {
|
|
71
|
+
console.error('[Bridge Daemon] Restored previous state from disk');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Bind connection health
|
|
75
|
+
extensionData._connectionHealth = connectionHealth;
|
|
76
|
+
|
|
77
|
+
// Create and start HTTP server
|
|
78
|
+
const app = createHttpServer(HTTP_PORT);
|
|
79
|
+
const { httpServer } = await startHttpServer(app, HTTP_PORT, extensionData);
|
|
80
|
+
|
|
81
|
+
if (!httpServer) {
|
|
82
|
+
console.error('[Bridge Daemon] Failed to start HTTP server (port in use?). Exiting.');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Write PID file for tracking
|
|
87
|
+
writePidFile();
|
|
88
|
+
|
|
89
|
+
// Record bridge_spawn lifecycle event
|
|
90
|
+
metricsDB.recordConnectionEvent({
|
|
91
|
+
event_type: 'bridge_spawn',
|
|
92
|
+
failure_reason: `v${_mcpVersion}`,
|
|
93
|
+
metadata: JSON.stringify({ port: HTTP_PORT, pid: process.pid }),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.error(`[Bridge Daemon] HTTP bridge running on port ${HTTP_PORT}`);
|
|
97
|
+
console.error('[Bridge Daemon] Press Ctrl+C to stop gracefully');
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// GRACEFUL SHUTDOWN
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
let isShuttingDown = false;
|
|
104
|
+
|
|
105
|
+
const shutdown = (signal) => {
|
|
106
|
+
if (isShuttingDown) return;
|
|
107
|
+
isShuttingDown = true;
|
|
108
|
+
|
|
109
|
+
console.error(`[Bridge Daemon] ${signal} received — shutting down gracefully...`);
|
|
110
|
+
|
|
111
|
+
// Persist state before exit
|
|
112
|
+
try {
|
|
113
|
+
persistStateNow(extensionData);
|
|
114
|
+
console.error('[Bridge Daemon] State persisted before shutdown');
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error(`[Bridge Daemon] Failed to persist state: ${err.message}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Remove PID file
|
|
120
|
+
removePidFile();
|
|
121
|
+
|
|
122
|
+
// Close HTTP server
|
|
123
|
+
if (httpServer) {
|
|
124
|
+
httpServer.close(() => {
|
|
125
|
+
console.error('[Bridge Daemon] HTTP server closed');
|
|
126
|
+
process.exit(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Force exit after 5 seconds
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
console.error('[Bridge Daemon] Forced exit after 5s timeout');
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}, 5000);
|
|
134
|
+
} else {
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
140
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
141
|
+
|
|
142
|
+
// Windows compatibility
|
|
143
|
+
if (process.platform === 'win32') {
|
|
144
|
+
process.on('SIGHUP', () => shutdown('SIGHUP'));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Handle uncaught errors
|
|
148
|
+
process.on('uncaughtException', (err) => {
|
|
149
|
+
console.error(`[Bridge Daemon] Uncaught exception: ${err.message}`);
|
|
150
|
+
console.error(err.stack);
|
|
151
|
+
// Record bridge_crash lifecycle event
|
|
152
|
+
metricsDB.recordConnectionEvent({
|
|
153
|
+
event_type: 'bridge_crash',
|
|
154
|
+
failure_reason: err.message.slice(0, 200),
|
|
155
|
+
metadata: JSON.stringify({ exit_code: 1 }),
|
|
156
|
+
});
|
|
157
|
+
// Try to persist state before crashing
|
|
158
|
+
try { persistStateNow(extensionData); } catch { /* ignore */ }
|
|
159
|
+
removePidFile();
|
|
160
|
+
process.exit(1);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
process.on('unhandledRejection', (reason) => {
|
|
164
|
+
console.error(`[Bridge Daemon] Unhandled rejection: ${reason}`);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
start().catch((err) => {
|
|
169
|
+
console.error(`[Bridge Daemon] Fatal error: ${err.message}`);
|
|
170
|
+
removePidFile();
|
|
171
|
+
process.exit(1);
|
|
172
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/**
|
|
3
|
+
* Auto-Config Module
|
|
4
|
+
* Automatically adds this MCP server to AI agent config files
|
|
5
|
+
* Supports: Claude Desktop, Cursor, Windsurf, generic MCP configs
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
9
|
+
import { join, dirname } from 'node:path';
|
|
10
|
+
import { homedir, platform } from 'node:os';
|
|
11
|
+
|
|
12
|
+
const SUPPORTED_CONFIGS = [
|
|
13
|
+
{
|
|
14
|
+
name: 'Claude Desktop',
|
|
15
|
+
getPaths() {
|
|
16
|
+
const home = homedir();
|
|
17
|
+
if (platform() === 'win32') {
|
|
18
|
+
return [join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json')];
|
|
19
|
+
} else if (platform() === 'darwin') {
|
|
20
|
+
return [join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')];
|
|
21
|
+
}
|
|
22
|
+
return [join(home, '.config', 'claude', 'claude_desktop_config.json')];
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'Cursor',
|
|
27
|
+
getPaths() {
|
|
28
|
+
const home = homedir();
|
|
29
|
+
if (platform() === 'win32') {
|
|
30
|
+
return [join(home, '.cursor', 'mcp.json')];
|
|
31
|
+
}
|
|
32
|
+
return [join(home, '.cursor', 'mcp.json')];
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'Windsurf (Codeium)',
|
|
37
|
+
getPaths() {
|
|
38
|
+
const home = homedir();
|
|
39
|
+
return [join(home, '.codeium', 'windsurf', 'mcp_config.json')];
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Google Antigravity',
|
|
44
|
+
getPaths() {
|
|
45
|
+
return [join(homedir(), '.gemini', 'antigravity', 'mcp_config.json')];
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const MCP_ENTRY_KEY = 'dombridge';
|
|
51
|
+
|
|
52
|
+
function getMcpServerEntry() {
|
|
53
|
+
return {
|
|
54
|
+
command: 'npx',
|
|
55
|
+
args: ['-y', '@yusufffararatt/dombridge-mcp'],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mergeConfig(existingContent) {
|
|
60
|
+
let config;
|
|
61
|
+
try {
|
|
62
|
+
config = JSON.parse(existingContent);
|
|
63
|
+
} catch {
|
|
64
|
+
config = {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!config.mcpServers) {
|
|
68
|
+
config.mcpServers = {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const alreadyExists = !!config.mcpServers[MCP_ENTRY_KEY];
|
|
72
|
+
config.mcpServers[MCP_ENTRY_KEY] = getMcpServerEntry();
|
|
73
|
+
|
|
74
|
+
return { config, alreadyExists };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function runAutoConfig(pkg) {
|
|
78
|
+
console.log(`\n 🔧 ${pkg.name} v${pkg.version} — Auto-Config\n`);
|
|
79
|
+
|
|
80
|
+
let configured = 0;
|
|
81
|
+
let skipped = 0;
|
|
82
|
+
|
|
83
|
+
for (const target of SUPPORTED_CONFIGS) {
|
|
84
|
+
const paths = target.getPaths();
|
|
85
|
+
|
|
86
|
+
for (const configPath of paths) {
|
|
87
|
+
const dirPath = dirname(configPath);
|
|
88
|
+
|
|
89
|
+
if (!existsSync(dirPath)) {
|
|
90
|
+
console.log(` ⏭ ${target.name}: config directory not found, skipping`);
|
|
91
|
+
skipped++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let existingContent = '{}';
|
|
96
|
+
if (existsSync(configPath)) {
|
|
97
|
+
existingContent = readFileSync(configPath, 'utf-8');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { config, alreadyExists } = mergeConfig(existingContent);
|
|
101
|
+
|
|
102
|
+
if (alreadyExists) {
|
|
103
|
+
console.log(` ✅ ${target.name}: already configured (updated)`);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(` ✅ ${target.name}: added successfully`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
109
|
+
configured++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(`\n 📊 Result: ${configured} configured, ${skipped} skipped`);
|
|
114
|
+
|
|
115
|
+
if (configured > 0) {
|
|
116
|
+
console.log(`\n ⚠️ Restart your AI agent (Claude/Cursor/Windsurf) to apply changes.`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log('');
|
|
120
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server Constants
|
|
3
|
+
* Single source of truth for server-side configurable values.
|
|
4
|
+
*
|
|
5
|
+
* Extension mirror: modules/constants.js
|
|
6
|
+
* When changing MCP_PORT: update both files + manifest.json host_permissions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const MCP_PORT = 3101;
|
|
10
|
+
export const CONNECTION_STALE_TIMEOUT_MS = 60_000;
|
|
11
|
+
export const STALE_MONITOR_CHECK_INTERVAL_MS = 20_000;
|
|
12
|
+
export const HEALTH_CHECK_FETCH_TIMEOUT_MS = 2_000;
|
|
13
|
+
export const CONNECTION_HEALTH_MAX_EVENTS = 20;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DOMBridge - MCP Thin Client
|
|
5
|
+
*
|
|
6
|
+
* Phase 2.2: MCP process is now a thin client that connects to the bridge daemon
|
|
7
|
+
* via HTTP. No direct extensionData access — all state goes through BridgeClient.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* AI Assistant ←stdio→ MCP Client (this file)
|
|
11
|
+
* │ HTTP (localhost:3101)
|
|
12
|
+
* ↓
|
|
13
|
+
* Bridge Daemon (bridge-daemon.js)
|
|
14
|
+
* ├── extensionData (IN-MEMORY + DISK)
|
|
15
|
+
* ├── HTTP Bridge ←→ Extension
|
|
16
|
+
* └── State persistence (JSON)
|
|
17
|
+
*
|
|
18
|
+
* Auto-spawn: If bridge daemon is not running, MCP client spawns it as a child process.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
22
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
23
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
24
|
+
import { readFileSync, unlinkSync } from 'fs';
|
|
25
|
+
import { fileURLToPath } from 'url';
|
|
26
|
+
import { dirname, join } from 'path';
|
|
27
|
+
import { spawn } from 'child_process';
|
|
28
|
+
import { logger } from './utils/logger.js';
|
|
29
|
+
import { BridgeClient } from './state/bridge-client.js';
|
|
30
|
+
import { getToolsList, getToolHandler } from './tools/index.js';
|
|
31
|
+
import { MetricsDB } from './metrics/metrics-db.js';
|
|
32
|
+
import { setMetricsDB } from './metrics/metrics-recorder.js';
|
|
33
|
+
|
|
34
|
+
const __mcpDirname = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const MCP_ROOT = join(__mcpDirname, '..');
|
|
36
|
+
const { version: _mcpVersion } = JSON.parse(readFileSync(join(MCP_ROOT, 'package.json'), 'utf8'));
|
|
37
|
+
|
|
38
|
+
const HTTP_PORT = parseInt(process.env.MCP_PORT || process.env.PORT || '3101', 10);
|
|
39
|
+
const PID_FILE = join(MCP_ROOT, 'state', 'bridge.pid');
|
|
40
|
+
|
|
41
|
+
logger.info('Server', `v${_mcpVersion} starting (bridge port ${HTTP_PORT})`);
|
|
42
|
+
if (_mcpVersion !== '2.7.5') {
|
|
43
|
+
logger.warn('Server', `Expected v2.7.5, running v${_mcpVersion}. Update: cd mcp-server && npm link`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// BRIDGE CONNECTION + AUTO-SPAWN
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if bridge daemon is already running and healthy.
|
|
52
|
+
*/
|
|
53
|
+
async function checkBridgeHealth(port) {
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch(`http://localhost:${port}/health`, {
|
|
56
|
+
signal: AbortSignal.timeout(3000)
|
|
57
|
+
});
|
|
58
|
+
if (response.ok) {
|
|
59
|
+
return await response.json();
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// Not running or not reachable
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Read PID file and check if process is alive.
|
|
69
|
+
*/
|
|
70
|
+
function checkStalePid() {
|
|
71
|
+
try {
|
|
72
|
+
const pid = parseInt(readFileSync(PID_FILE, 'utf8').trim(), 10);
|
|
73
|
+
if (isNaN(pid)) return false;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
process.kill(pid, 0); // Check if process exists (doesn't actually kill)
|
|
77
|
+
return true; // Process is alive
|
|
78
|
+
} catch {
|
|
79
|
+
// Process doesn't exist, stale PID
|
|
80
|
+
try { unlinkSync(PID_FILE); } catch { /* ignore */ }
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
return false; // No PID file
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Kill stale bridge process.
|
|
90
|
+
*/
|
|
91
|
+
function killStaleBridge(port) {
|
|
92
|
+
try {
|
|
93
|
+
fetch(`http://localhost:${port}/api/die`, { method: 'POST' }).catch(() => {});
|
|
94
|
+
} catch { /* ignore */ }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Spawn bridge daemon as a child process.
|
|
99
|
+
*/
|
|
100
|
+
function spawnBridge(port) {
|
|
101
|
+
logger.info('Server', `Spawning bridge daemon on port ${port}...`);
|
|
102
|
+
|
|
103
|
+
const bridgeProcess = spawn('node', ['src/bridge-daemon.js'], {
|
|
104
|
+
cwd: MCP_ROOT,
|
|
105
|
+
detached: true,
|
|
106
|
+
stdio: 'ignore',
|
|
107
|
+
env: { ...process.env, MCP_PORT: String(port) }
|
|
108
|
+
});
|
|
109
|
+
bridgeProcess.unref();
|
|
110
|
+
|
|
111
|
+
logger.info('Server', `Bridge daemon spawned (PID: ${bridgeProcess.pid})`);
|
|
112
|
+
return bridgeProcess;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Wait for bridge daemon to become healthy.
|
|
117
|
+
*/
|
|
118
|
+
async function waitForBridge(port, maxWaitMs = 5000) {
|
|
119
|
+
const startTime = Date.now();
|
|
120
|
+
const interval = 500;
|
|
121
|
+
|
|
122
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
123
|
+
await new Promise(resolve => setTimeout(resolve, interval));
|
|
124
|
+
const health = await checkBridgeHealth(port);
|
|
125
|
+
if (health) {
|
|
126
|
+
return health;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Ensure bridge daemon is running. If not, spawn it.
|
|
135
|
+
* Returns the BridgeClient instance.
|
|
136
|
+
*/
|
|
137
|
+
async function ensureBridgeRunning() {
|
|
138
|
+
// 1. Check if bridge is already running
|
|
139
|
+
const existingHealth = await checkBridgeHealth(HTTP_PORT);
|
|
140
|
+
if (existingHealth) {
|
|
141
|
+
logger.info('Server', `Bridge daemon already running on port ${HTTP_PORT}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 2. Check for stale PID
|
|
146
|
+
const isAlive = checkStalePid();
|
|
147
|
+
if (isAlive) {
|
|
148
|
+
logger.warn('Server', `Stale bridge process detected, killing...`);
|
|
149
|
+
killStaleBridge(HTTP_PORT);
|
|
150
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 3. Spawn bridge daemon
|
|
154
|
+
spawnBridge(HTTP_PORT);
|
|
155
|
+
|
|
156
|
+
// 4. Wait for it to be ready
|
|
157
|
+
const health = await waitForBridge(HTTP_PORT, 5000);
|
|
158
|
+
if (!health) {
|
|
159
|
+
throw new Error(`Bridge daemon failed to start on port ${HTTP_PORT} within 5s`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
logger.info('Server', `Bridge daemon ready on port ${HTTP_PORT}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// BRIDGE CLIENT INSTANCE
|
|
167
|
+
// ============================================================================
|
|
168
|
+
|
|
169
|
+
const bridgeClient = new BridgeClient(HTTP_PORT);
|
|
170
|
+
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// MCP SERVER
|
|
173
|
+
// ============================================================================
|
|
174
|
+
|
|
175
|
+
const server = new Server(
|
|
176
|
+
{
|
|
177
|
+
name: 'dombridge',
|
|
178
|
+
version: _mcpVersion,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
capabilities: {
|
|
182
|
+
tools: {},
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* List Tools Handler
|
|
189
|
+
*/
|
|
190
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
191
|
+
return {
|
|
192
|
+
tools: getToolsList()
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Call Tool Handler
|
|
198
|
+
*/
|
|
199
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
200
|
+
const { name, arguments: args } = request.params;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const handler = getToolHandler(name);
|
|
204
|
+
|
|
205
|
+
if (!handler) {
|
|
206
|
+
return {
|
|
207
|
+
content: [
|
|
208
|
+
{
|
|
209
|
+
type: 'text',
|
|
210
|
+
text: `❌ Unknown tool: ${name}`
|
|
211
|
+
}
|
|
212
|
+
],
|
|
213
|
+
isError: true
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Refresh bridge client state before each tool call
|
|
218
|
+
// so tool handlers can access fresh state synchronously
|
|
219
|
+
try {
|
|
220
|
+
await bridgeClient.refreshState();
|
|
221
|
+
} catch (err) {
|
|
222
|
+
logger.warn('Server', `Failed to refresh state from bridge: ${err.message}`);
|
|
223
|
+
// Continue with stale state — tool handlers should check isConnected
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check if bridge has signaled a restart request — exit so Claude Code restarts us
|
|
227
|
+
if (bridgeClient.restartRequestedAt) {
|
|
228
|
+
logger.info('Server', `Bridge restart detected (signaled at ${new Date(bridgeClient.restartRequestedAt).toISOString()}). Exiting for restart.`);
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Call tool handler with bridgeClient instead of extensionData + httpPort
|
|
233
|
+
return await handler(args, bridgeClient);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return {
|
|
236
|
+
content: [
|
|
237
|
+
{
|
|
238
|
+
type: 'text',
|
|
239
|
+
text: `❌ Error: ${error.message}`
|
|
240
|
+
}
|
|
241
|
+
],
|
|
242
|
+
isError: true
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ============================================================================
|
|
248
|
+
// START
|
|
249
|
+
// ============================================================================
|
|
250
|
+
|
|
251
|
+
async function main() {
|
|
252
|
+
// Ensure bridge daemon is running
|
|
253
|
+
await ensureBridgeRunning();
|
|
254
|
+
|
|
255
|
+
// Clear any pending restart signal (we are the fresh instance)
|
|
256
|
+
try {
|
|
257
|
+
await bridgeClient.refreshState();
|
|
258
|
+
if (bridgeClient.restartRequestedAt) {
|
|
259
|
+
logger.info('Server', 'Clearing restart signal from previous bridge restart');
|
|
260
|
+
await bridgeClient.post('clear-restart-signal');
|
|
261
|
+
}
|
|
262
|
+
} catch (err) {
|
|
263
|
+
logger.warn('Server', `Could not clear restart signal: ${err.message}`);
|
|
264
|
+
// Not critical — will be cleared on next tool call cycle
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Initialize metrics database
|
|
268
|
+
const metricsDB = new MetricsDB();
|
|
269
|
+
setMetricsDB(metricsDB);
|
|
270
|
+
|
|
271
|
+
const transport = new StdioServerTransport();
|
|
272
|
+
await server.connect(transport);
|
|
273
|
+
logger.info('Server', 'MCP client connected via stdio');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
main().catch((error) => {
|
|
277
|
+
logger.error('Server', 'Fatal error:', error);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
});
|