interceptpilot-mcp 0.1.0

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/src/daemon.js ADDED
@@ -0,0 +1,312 @@
1
+ import { WebSocketServer } from 'ws'
2
+
3
+ import {
4
+ BRIDGE_HOST,
5
+ BRIDGE_PORT,
6
+ MESSAGE_TYPES,
7
+ createClientHelloAckMessage,
8
+ createCommandMessage,
9
+ createHelloAckMessage,
10
+ createPingMessage,
11
+ isAllowedCommand,
12
+ isCommandResponse,
13
+ parseBridgeMessage,
14
+ validateClientHelloMessage,
15
+ validateHelloMessage
16
+ } from './protocol.js'
17
+
18
+ const DEFAULT_REQUEST_TIMEOUT_MS = 5000
19
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 20000
20
+ const DEFAULT_MAX_PENDING_REQUESTS = 25
21
+ const NOT_CONNECTED_MESSAGE = 'InterceptPilot extension is not connected. Open InterceptPilot settings and start an AI Bridge session.'
22
+ const TIMEOUT_MESSAGE = 'InterceptPilot did not respond in time.'
23
+ const DISCONNECTED_MESSAGE = 'InterceptPilot extension disconnected.'
24
+ const COMMAND_NOT_ALLOWED_MESSAGE = 'Command is not allowed.'
25
+ const TOO_MANY_PENDING_MESSAGE = 'Too many pending InterceptPilot requests.'
26
+
27
+ function createDefaultLogger() {
28
+ return {
29
+ info: message => console.error(`[interceptpilot-mcp] ${message}`),
30
+ warn: message => console.error(`[interceptpilot-mcp] ${message}`),
31
+ error: message => console.error(`[interceptpilot-mcp] ${message}`)
32
+ }
33
+ }
34
+
35
+ function addSocketListener(socket, event, handler) {
36
+ if (typeof socket.on === 'function') {
37
+ socket.on(event, handler)
38
+ return
39
+ }
40
+ socket[`on${event}`] = handler
41
+ }
42
+
43
+ function sendJson(socket, message) {
44
+ if (!socket || socket.closed) return
45
+ socket.send(JSON.stringify(message))
46
+ }
47
+
48
+ function createClientError(id, code, message) {
49
+ return {
50
+ id,
51
+ type: MESSAGE_TYPES.clientError,
52
+ ok: false,
53
+ error: { code, message }
54
+ }
55
+ }
56
+
57
+ export function createInterceptPilotDaemon(options = {}) {
58
+ const {
59
+ WebSocketServerCtor = WebSocketServer,
60
+ port = BRIDGE_PORT,
61
+ expectedKey = '',
62
+ requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
63
+ heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS,
64
+ maxPendingRequests = DEFAULT_MAX_PENDING_REQUESTS,
65
+ idGenerator,
66
+ logger = createDefaultLogger(),
67
+ setTimeoutFn = setTimeout,
68
+ clearTimeoutFn = clearTimeout,
69
+ setIntervalFn = setInterval,
70
+ clearIntervalFn = clearInterval
71
+ } = options
72
+
73
+ const expectedConnectionKey = String(expectedKey || '')
74
+ const clients = new Set()
75
+ const pending = new Map()
76
+ const host = BRIDGE_HOST
77
+ let server = null
78
+ let extensionSocket = null
79
+ let extensionConnected = false
80
+ let requestSeq = 0
81
+ let heartbeatTimer = null
82
+
83
+ function nextRequestId() {
84
+ if (idGenerator) return idGenerator()
85
+ requestSeq += 1
86
+ return `daemon_req_${requestSeq}`
87
+ }
88
+
89
+ function isExtensionConnected() {
90
+ return Boolean(extensionSocket && extensionConnected)
91
+ }
92
+
93
+ function startHeartbeat() {
94
+ if (heartbeatTimer) return
95
+ heartbeatTimer = setIntervalFn(() => {
96
+ if (isExtensionConnected()) sendJson(extensionSocket, createPingMessage())
97
+ }, heartbeatIntervalMs)
98
+ heartbeatTimer?.unref?.()
99
+ }
100
+
101
+ function stopHeartbeat() {
102
+ if (!heartbeatTimer) return
103
+ clearIntervalFn(heartbeatTimer)
104
+ heartbeatTimer = null
105
+ }
106
+
107
+ function rejectPendingForClient(clientSocket) {
108
+ for (const [extensionRequestId, request] of pending) {
109
+ if (request.clientSocket !== clientSocket) continue
110
+ clearTimeoutFn(request.timer)
111
+ pending.delete(extensionRequestId)
112
+ }
113
+ }
114
+
115
+ function rejectAllPending(message) {
116
+ for (const [extensionRequestId, request] of pending) {
117
+ clearTimeoutFn(request.timer)
118
+ pending.delete(extensionRequestId)
119
+ sendJson(request.clientSocket, createClientError(
120
+ request.clientRequestId,
121
+ 'EXTENSION_DISCONNECTED',
122
+ message
123
+ ))
124
+ }
125
+ }
126
+
127
+ function detachExtension(socket) {
128
+ if (socket !== extensionSocket) return
129
+ extensionSocket = null
130
+ extensionConnected = false
131
+ stopHeartbeat()
132
+ rejectAllPending(DISCONNECTED_MESSAGE)
133
+ logger.info('Extension disconnected.')
134
+ }
135
+
136
+ function detachClient(socket) {
137
+ if (!clients.has(socket)) return
138
+ clients.delete(socket)
139
+ rejectPendingForClient(socket)
140
+ logger.info('MCP client disconnected.')
141
+ }
142
+
143
+ function acceptExtension(socket, message) {
144
+ const validation = validateHelloMessage(message, { expectedKey: expectedConnectionKey })
145
+ if (!validation.ok) {
146
+ logger.warn(validation.error)
147
+ socket.close()
148
+ return
149
+ }
150
+
151
+ if (extensionSocket && extensionSocket !== socket) {
152
+ const previousSocket = extensionSocket
153
+ detachExtension(previousSocket)
154
+ previousSocket.close()
155
+ }
156
+
157
+ extensionSocket = socket
158
+ extensionConnected = true
159
+ sendJson(socket, createHelloAckMessage())
160
+ startHeartbeat()
161
+ logger.info('Extension connected.')
162
+ }
163
+
164
+ function acceptClient(socket, message) {
165
+ const validation = validateClientHelloMessage(message, { expectedKey: expectedConnectionKey })
166
+ if (!validation.ok) {
167
+ logger.warn(validation.error)
168
+ socket.close()
169
+ return
170
+ }
171
+
172
+ clients.add(socket)
173
+ sendJson(socket, createClientHelloAckMessage())
174
+ logger.info('MCP client connected.')
175
+ }
176
+
177
+ function sendClientError(socket, id, code, message) {
178
+ sendJson(socket, createClientError(id, code, message))
179
+ }
180
+
181
+ function handleExtensionResponse(message) {
182
+ if (!isCommandResponse(message)) return
183
+ const request = pending.get(message.id)
184
+ if (!request) return
185
+ pending.delete(message.id)
186
+ clearTimeoutFn(request.timer)
187
+
188
+ if (message.type === MESSAGE_TYPES.commandError || message.ok === false) {
189
+ sendClientError(
190
+ request.clientSocket,
191
+ request.clientRequestId,
192
+ String(message.error?.code || 'COMMAND_FAILED'),
193
+ String(message.error?.message || 'InterceptPilot command failed.')
194
+ )
195
+ return
196
+ }
197
+
198
+ sendJson(request.clientSocket, {
199
+ id: request.clientRequestId,
200
+ type: MESSAGE_TYPES.clientResult,
201
+ ok: true,
202
+ result: message.result
203
+ })
204
+ }
205
+
206
+ function handleClientCommand(socket, message) {
207
+ const clientRequestId = String(message?.id || '')
208
+ const command = String(message?.command || '')
209
+ if (!clientRequestId) return
210
+
211
+ if (!isAllowedCommand(command)) {
212
+ sendClientError(socket, clientRequestId, 'COMMAND_NOT_ALLOWED', COMMAND_NOT_ALLOWED_MESSAGE)
213
+ return
214
+ }
215
+
216
+ if (!isExtensionConnected()) {
217
+ sendClientError(socket, clientRequestId, 'EXTENSION_NOT_CONNECTED', NOT_CONNECTED_MESSAGE)
218
+ return
219
+ }
220
+
221
+ if (pending.size >= maxPendingRequests) {
222
+ sendClientError(socket, clientRequestId, 'TOO_MANY_PENDING_REQUESTS', TOO_MANY_PENDING_MESSAGE)
223
+ return
224
+ }
225
+
226
+ const extensionRequestId = nextRequestId()
227
+ const timer = setTimeoutFn(() => {
228
+ pending.delete(extensionRequestId)
229
+ sendClientError(socket, clientRequestId, 'COMMAND_TIMEOUT', TIMEOUT_MESSAGE)
230
+ }, requestTimeoutMs)
231
+ timer?.unref?.()
232
+ pending.set(extensionRequestId, { clientSocket: socket, clientRequestId, timer, command })
233
+ sendJson(extensionSocket, createCommandMessage(
234
+ extensionRequestId,
235
+ command,
236
+ message.payload && typeof message.payload === 'object' ? message.payload : {}
237
+ ))
238
+ }
239
+
240
+ function handleSocketMessage(socket, raw) {
241
+ const message = parseBridgeMessage(raw)
242
+ if (!message) {
243
+ logger.warn('Ignoring invalid bridge message.')
244
+ return
245
+ }
246
+
247
+ if (message.type === MESSAGE_TYPES.hello) {
248
+ acceptExtension(socket, message)
249
+ return
250
+ }
251
+
252
+ if (message.type === MESSAGE_TYPES.clientHello) {
253
+ acceptClient(socket, message)
254
+ return
255
+ }
256
+
257
+ if (socket === extensionSocket && extensionConnected) {
258
+ if (message.type === MESSAGE_TYPES.pong) return
259
+ handleExtensionResponse(message)
260
+ return
261
+ }
262
+
263
+ if (clients.has(socket)) {
264
+ if (message.type === MESSAGE_TYPES.clientCommand) handleClientCommand(socket, message)
265
+ return
266
+ }
267
+ }
268
+
269
+ function handleSocket(socket) {
270
+ addSocketListener(socket, 'message', raw => handleSocketMessage(socket, raw))
271
+ addSocketListener(socket, 'close', () => {
272
+ detachExtension(socket)
273
+ detachClient(socket)
274
+ })
275
+ addSocketListener(socket, 'error', () => {
276
+ detachExtension(socket)
277
+ detachClient(socket)
278
+ })
279
+ }
280
+
281
+ function start() {
282
+ if (server) return server
283
+ server = new WebSocketServerCtor({ host, port })
284
+ server.on('connection', handleSocket)
285
+ server.on('listening', () => logger.info(`Shared daemon listening on ${host}:${port}.`))
286
+ server.on('error', () => logger.error('Shared daemon WebSocket error.'))
287
+ return server
288
+ }
289
+
290
+ async function close() {
291
+ stopHeartbeat()
292
+ rejectAllPending(DISCONNECTED_MESSAGE)
293
+ if (extensionSocket) extensionSocket.close()
294
+ for (const client of Array.from(clients)) client.close()
295
+ extensionSocket = null
296
+ extensionConnected = false
297
+ clients.clear()
298
+ if (server && typeof server.close === 'function') {
299
+ await new Promise(resolve => server.close(resolve))
300
+ }
301
+ server = null
302
+ }
303
+
304
+ return {
305
+ start,
306
+ close,
307
+ handleSocket,
308
+ isExtensionConnected,
309
+ getClientCount: () => clients.size,
310
+ getPendingCount: () => pending.size
311
+ }
312
+ }
@@ -0,0 +1,221 @@
1
+ export const IMPORT_BUNDLE_SCHEMA_VERSION = 1
2
+ export const IMPORT_BUNDLE_TYPES = ['interceptpilot.rule', 'interceptpilot.collection']
3
+
4
+ export const IMPORT_BUNDLE_ALLOWED_VALUES = {
5
+ matchType: ['contains', 'equals', 'regex', 'glob'],
6
+ method: ['ANY', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
7
+ resourceType: ['ANY', 'XHR', 'Fetch', 'Document', 'Script', 'Image', 'Stylesheet', 'Other'],
8
+ actionType: ['mockResponse', 'failRequest', 'delayOnly', 'passThrough'],
9
+ failReason: [
10
+ 'Failed',
11
+ 'Aborted',
12
+ 'TimedOut',
13
+ 'AccessDenied',
14
+ 'ConnectionClosed',
15
+ 'ConnectionRefused',
16
+ 'ConnectionReset',
17
+ 'InternetDisconnected',
18
+ 'NameNotResolved'
19
+ ]
20
+ }
21
+
22
+ const baseMatchShape = {
23
+ urlPattern: 'string',
24
+ matchType: 'contains | equals | regex | glob',
25
+ method: 'ANY | GET | POST | PUT | PATCH | DELETE | OPTIONS',
26
+ resourceType: 'ANY | XHR | Fetch | Document | Script | Image | Stylesheet | Other'
27
+ }
28
+
29
+ const baseActionShape = {
30
+ type: 'mockResponse | failRequest | delayOnly | passThrough',
31
+ statusCode: 'number optional, 100-599 for mockResponse',
32
+ statusText: 'string optional',
33
+ contentType: 'string optional',
34
+ responseBody: 'string optional; JSON responses must be stringified JSON',
35
+ responseHeaders: 'string optional; for redirects include a Location header such as "Location: /new-path"',
36
+ delayMs: 'number optional, milliseconds',
37
+ failReason: 'Failed | Aborted | TimedOut | AccessDenied | ConnectionClosed | ConnectionRefused | ConnectionReset | InternetDisconnected | NameNotResolved'
38
+ }
39
+
40
+ export function getImportBundleSchema() {
41
+ return {
42
+ schemaVersion: IMPORT_BUNDLE_SCHEMA_VERSION,
43
+ supportedTypes: [...IMPORT_BUNDLE_TYPES],
44
+ ruleBundleShape: {
45
+ schemaVersion: IMPORT_BUNDLE_SCHEMA_VERSION,
46
+ type: 'interceptpilot.rule',
47
+ exportedAt: 'ISO date string',
48
+ rule: {
49
+ id: 'string optional',
50
+ collectionId: 'string optional',
51
+ name: 'string',
52
+ enabled: 'boolean',
53
+ priority: 'number',
54
+ match: { ...baseMatchShape },
55
+ action: { ...baseActionShape }
56
+ }
57
+ },
58
+ collectionBundleShape: {
59
+ schemaVersion: IMPORT_BUNDLE_SCHEMA_VERSION,
60
+ type: 'interceptpilot.collection',
61
+ exportedAt: 'ISO date string',
62
+ collection: {
63
+ id: 'string optional',
64
+ name: 'string',
65
+ description: 'string optional',
66
+ enabled: 'boolean',
67
+ order: 'number optional'
68
+ },
69
+ rules: ['array of rule objects using the same rule shape as ruleBundleShape.rule']
70
+ },
71
+ allowedValues: {
72
+ matchType: [...IMPORT_BUNDLE_ALLOWED_VALUES.matchType],
73
+ method: [...IMPORT_BUNDLE_ALLOWED_VALUES.method],
74
+ resourceType: [...IMPORT_BUNDLE_ALLOWED_VALUES.resourceType],
75
+ actionType: [...IMPORT_BUNDLE_ALLOWED_VALUES.actionType],
76
+ failReason: [...IMPORT_BUNDLE_ALLOWED_VALUES.failReason]
77
+ },
78
+ guidance: [
79
+ 'Prefer resourceType "ANY" unless the user explicitly asks for a specific type.',
80
+ 'Use matchType "contains" for most endpoint path matching unless the user needs exact, regex or glob matching.',
81
+ 'Use mockResponse for HTTP status codes from 100 to 599, including 4xx and 5xx.',
82
+ 'Use failRequest only for network-level failures where there is no HTTP response.',
83
+ 'Use delayOnly only to add latency without changing the response.',
84
+ 'Use passThrough to allow the request to continue unchanged.',
85
+ 'responseBody must be a string. If it contains JSON, stringify and escape it correctly.',
86
+ 'For redirects such as 301 or 302, use mockResponse and include a Location header when appropriate.',
87
+ 'Importing a bundle only creates a pending proposal; the user must confirm it in the Full App before anything is applied.'
88
+ ]
89
+ }
90
+ }
91
+
92
+ function createRuleBundle({ name, urlPattern, action, method = 'GET', priority = 100 }) {
93
+ return {
94
+ schemaVersion: IMPORT_BUNDLE_SCHEMA_VERSION,
95
+ type: 'interceptpilot.rule',
96
+ exportedAt: '2026-01-01T00:00:00.000Z',
97
+ rule: {
98
+ name,
99
+ enabled: true,
100
+ priority,
101
+ match: {
102
+ urlPattern,
103
+ matchType: 'contains',
104
+ method,
105
+ resourceType: 'ANY'
106
+ },
107
+ action
108
+ }
109
+ }
110
+ }
111
+
112
+ export function getImportBundleExamples() {
113
+ return [
114
+ {
115
+ name: 'Single rule mock 200',
116
+ description: 'Example of a single rule bundle that mocks a JSON success response.',
117
+ bundle: createRuleBundle({
118
+ name: 'Mock example success',
119
+ urlPattern: '/api/example',
120
+ action: {
121
+ type: 'mockResponse',
122
+ statusCode: 200,
123
+ statusText: 'OK',
124
+ contentType: 'application/json',
125
+ responseBody: '{"ok":true,"source":"interceptpilot"}'
126
+ }
127
+ })
128
+ },
129
+ {
130
+ name: 'Single rule HTTP 500',
131
+ description: 'Example of an HTTP error response. Use mockResponse for HTTP 4xx and 5xx statuses.',
132
+ bundle: createRuleBundle({
133
+ name: 'Mock checkout server error',
134
+ urlPattern: '/api/checkout',
135
+ action: {
136
+ type: 'mockResponse',
137
+ statusCode: 500,
138
+ statusText: 'Internal Server Error',
139
+ contentType: 'application/json',
140
+ responseBody: '{"error":"temporary_failure"}'
141
+ }
142
+ })
143
+ },
144
+ {
145
+ name: 'Single rule network timeout',
146
+ description: 'Example of a network-level failure where no HTTP response should exist.',
147
+ bundle: createRuleBundle({
148
+ name: 'Simulate checkout timeout',
149
+ urlPattern: '/api/checkout',
150
+ action: {
151
+ type: 'failRequest',
152
+ failReason: 'TimedOut'
153
+ }
154
+ })
155
+ },
156
+ {
157
+ name: 'Single rule delay only',
158
+ description: 'Example of adding latency without changing the response.',
159
+ bundle: createRuleBundle({
160
+ name: 'Delay profile response',
161
+ urlPattern: '/api/user/profile',
162
+ action: {
163
+ type: 'delayOnly',
164
+ delayMs: 1200
165
+ }
166
+ })
167
+ },
168
+ {
169
+ name: 'Collection with multiple rules',
170
+ description: 'Example of a collection bundle grouping related checkout scenarios.',
171
+ bundle: {
172
+ schemaVersion: IMPORT_BUNDLE_SCHEMA_VERSION,
173
+ type: 'interceptpilot.collection',
174
+ exportedAt: '2026-01-01T00:00:00.000Z',
175
+ collection: {
176
+ name: 'Checkout examples',
177
+ description: 'Safe generic checkout scenarios',
178
+ enabled: true
179
+ },
180
+ rules: [
181
+ {
182
+ name: 'Checkout success',
183
+ enabled: true,
184
+ priority: 100,
185
+ match: {
186
+ urlPattern: '/api/checkout',
187
+ matchType: 'contains',
188
+ method: 'POST',
189
+ resourceType: 'ANY'
190
+ },
191
+ action: {
192
+ type: 'mockResponse',
193
+ statusCode: 200,
194
+ statusText: 'OK',
195
+ contentType: 'application/json',
196
+ responseBody: '{"ok":true,"orderId":"example-order"}'
197
+ }
198
+ },
199
+ {
200
+ name: 'Checkout unavailable',
201
+ enabled: true,
202
+ priority: 110,
203
+ match: {
204
+ urlPattern: '/api/checkout',
205
+ matchType: 'contains',
206
+ method: 'POST',
207
+ resourceType: 'ANY'
208
+ },
209
+ action: {
210
+ type: 'mockResponse',
211
+ statusCode: 503,
212
+ statusText: 'Service Unavailable',
213
+ contentType: 'application/json',
214
+ responseBody: '{"ok":false,"error":"service_unavailable"}'
215
+ }
216
+ }
217
+ ]
218
+ }
219
+ }
220
+ ]
221
+ }
package/src/index.js ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from 'node:url'
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
5
+
6
+ import { BRIDGE_PORT } from './protocol.js'
7
+ import { createDaemonClientBridge } from './daemon-client.js'
8
+ import { createInterceptPilotDaemon } from './daemon.js'
9
+ import { createMcpServer } from './mcp-server.js'
10
+
11
+ export function parseCliArgs(argv = []) {
12
+ const options = {
13
+ mode: 'adapter',
14
+ port: BRIDGE_PORT,
15
+ key: '',
16
+ daemonHost: '127.0.0.1',
17
+ daemonAutostart: true
18
+ }
19
+
20
+ const args = [...argv]
21
+ if (args[0] === 'daemon') {
22
+ options.mode = 'daemon'
23
+ args.shift()
24
+ }
25
+
26
+ for (let i = 0; i < args.length; i += 1) {
27
+ const arg = args[i]
28
+ if (arg === '--port' && args[i + 1]) {
29
+ options.port = Number(args[i + 1]) || BRIDGE_PORT
30
+ i += 1
31
+ } else if (arg === '--key' && args[i + 1]) {
32
+ options.key = String(args[i + 1] || '')
33
+ i += 1
34
+ } else if (arg === '--no-daemon-autostart') {
35
+ options.daemonAutostart = false
36
+ }
37
+ }
38
+
39
+ return options
40
+ }
41
+
42
+ function createStderrLogger() {
43
+ return {
44
+ info: message => console.error(`[interceptpilot-mcp] ${message}`),
45
+ warn: message => console.error(`[interceptpilot-mcp] ${message}`),
46
+ error: message => console.error(`[interceptpilot-mcp] ${message}`)
47
+ }
48
+ }
49
+
50
+ export async function createRuntime(options, factories = {}) {
51
+ const {
52
+ createDaemonClientBridge: createDaemonClientBridgeFn = createDaemonClientBridge,
53
+ createInterceptPilotDaemon: createInterceptPilotDaemonFn = createInterceptPilotDaemon,
54
+ createMcpServer: createMcpServerFn = createMcpServer,
55
+ StdioServerTransportCtor = StdioServerTransport,
56
+ logger = createStderrLogger()
57
+ } = factories
58
+
59
+ if (options.mode === 'daemon') {
60
+ const daemon = createInterceptPilotDaemonFn({
61
+ port: options.port,
62
+ expectedKey: options.key,
63
+ logger
64
+ })
65
+ daemon.start()
66
+ return {
67
+ mode: 'daemon',
68
+ daemon,
69
+ close: () => daemon.close()
70
+ }
71
+ }
72
+
73
+ const bridge = createDaemonClientBridgeFn({
74
+ port: options.port,
75
+ expectedKey: options.key,
76
+ daemonAutostart: options.daemonAutostart,
77
+ logger
78
+ })
79
+ await bridge.start()
80
+
81
+ const server = createMcpServerFn({ bridge })
82
+ const transport = new StdioServerTransportCtor()
83
+ await server.connect(transport)
84
+
85
+ return {
86
+ mode: 'adapter',
87
+ bridge,
88
+ server,
89
+ close: async () => {
90
+ await bridge.close?.()
91
+ await server.close?.()
92
+ }
93
+ }
94
+ }
95
+
96
+ export async function main(argv = process.argv.slice(2), factories = {}) {
97
+ const runtime = await createRuntime(parseCliArgs(argv), factories)
98
+
99
+ process.on('SIGINT', async () => {
100
+ await runtime.close()
101
+ process.exit(0)
102
+ })
103
+
104
+ return runtime
105
+ }
106
+
107
+ const currentFile = fileURLToPath(import.meta.url)
108
+ if (process.argv[1] === currentFile) {
109
+ main().catch(error => {
110
+ console.error(`[interceptpilot-mcp] ${String(error?.message || 'Failed to start InterceptPilot MCP server.')}`)
111
+ process.exit(1)
112
+ })
113
+ }