@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.
Files changed (49) hide show
  1. package/README.md +559 -0
  2. package/bin/cli.js +88 -0
  3. package/package.json +54 -0
  4. package/src/bridge/http-server.js +290 -0
  5. package/src/bridge/middleware.js +56 -0
  6. package/src/bridge/routes.js +1003 -0
  7. package/src/bridge-daemon.js +172 -0
  8. package/src/cli/auto-config.js +120 -0
  9. package/src/constants.js +13 -0
  10. package/src/index.js +279 -0
  11. package/src/mcp-bridge.js +136 -0
  12. package/src/metrics/error-codes.js +44 -0
  13. package/src/metrics/index.js +3 -0
  14. package/src/metrics/metrics-db.js +269 -0
  15. package/src/metrics/metrics-recorder.js +240 -0
  16. package/src/metrics/metrics-report.js +146 -0
  17. package/src/profiles/profile-db.js +159 -0
  18. package/src/profiles/profile-enricher.js +333 -0
  19. package/src/profiles/profile-manager.js +563 -0
  20. package/src/profiles/profile-repo.js +183 -0
  21. package/src/state/bridge-client.js +272 -0
  22. package/src/state/bridge-persistence.js +205 -0
  23. package/src/state/cache.js +38 -0
  24. package/src/state/extension-state.js +321 -0
  25. package/src/tools/action_tools.js +218 -0
  26. package/src/tools/analyze-page.js +247 -0
  27. package/src/tools/debug-mcp-state.js +172 -0
  28. package/src/tools/discover-apis.js +186 -0
  29. package/src/tools/execute-js.js +284 -0
  30. package/src/tools/export-session.js +171 -0
  31. package/src/tools/extract-data.js +395 -0
  32. package/src/tools/get-element.js +281 -0
  33. package/src/tools/get-network-trace.js +471 -0
  34. package/src/tools/index.js +110 -0
  35. package/src/tools/manage-site-profile.js +153 -0
  36. package/src/tools/paginate.js +444 -0
  37. package/src/tools/quick-scan.js +418 -0
  38. package/src/tools/screenshot_tools.js +117 -0
  39. package/src/utils/circuit-breaker.js +112 -0
  40. package/src/utils/extract-density.js +21 -0
  41. package/src/utils/logger.js +31 -0
  42. package/src/utils/paginate-detector.js +24 -0
  43. package/src/utils/rate-limiter.js +244 -0
  44. package/src/utils/run-script.js +37 -0
  45. package/src/utils/selector-validator.js +95 -0
  46. package/src/utils/state-validator.js +354 -0
  47. package/src/utils/tab-resolver.js +70 -0
  48. package/src/utils/workflow-helper.js +292 -0
  49. 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
+ }
@@ -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
+ });