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.
@@ -0,0 +1,36 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { fileURLToPath } from 'node:url'
3
+
4
+ function createDefaultLogger() {
5
+ return {
6
+ info: message => console.error(`[interceptpilot-mcp] ${message}`),
7
+ warn: message => console.error(`[interceptpilot-mcp] ${message}`),
8
+ error: message => console.error(`[interceptpilot-mcp] ${message}`)
9
+ }
10
+ }
11
+
12
+ export function getDefaultDaemonEntryPath() {
13
+ return fileURLToPath(new URL('./index.js', import.meta.url))
14
+ }
15
+
16
+ export function startDaemonProcess(options = {}) {
17
+ const {
18
+ port,
19
+ key = '',
20
+ nodeExecPath = process.execPath,
21
+ entryPath = getDefaultDaemonEntryPath(),
22
+ spawnFn = spawn,
23
+ logger = createDefaultLogger()
24
+ } = options
25
+
26
+ const args = [entryPath, 'daemon', '--port', String(port), '--key', String(key || '')]
27
+ const child = spawnFn(nodeExecPath, args, {
28
+ detached: true,
29
+ stdio: 'ignore',
30
+ windowsHide: true
31
+ })
32
+
33
+ if (typeof child?.unref === 'function') child.unref()
34
+ logger.info('Started InterceptPilot daemon process.')
35
+ return child
36
+ }
@@ -0,0 +1,151 @@
1
+ export const BRIDGE_HOST = '127.0.0.1'
2
+ export const BRIDGE_PORT = 37177
3
+ export const BRIDGE_PROTOCOL_VERSION = 1
4
+ export const BRIDGE_SOURCE = 'interceptpilot-extension'
5
+ export const MCP_CLIENT_SOURCE = 'interceptpilot-mcp-client'
6
+
7
+ export const MESSAGE_TYPES = {
8
+ hello: 'hello',
9
+ helloAck: 'hello:ack',
10
+ command: 'command',
11
+ commandResult: 'command:result',
12
+ commandError: 'command:error',
13
+ clientHello: 'client:hello',
14
+ clientHelloAck: 'client:hello:ack',
15
+ clientCommand: 'client:command',
16
+ clientResult: 'client:result',
17
+ clientError: 'client:error',
18
+ ping: 'ping',
19
+ pong: 'pong'
20
+ }
21
+
22
+ export const ALLOWED_COMMANDS = Object.freeze([
23
+ 'list_captured_requests',
24
+ 'search_captured_requests',
25
+ 'list_collections',
26
+ 'list_rules',
27
+ 'search_rules',
28
+ 'list_sanitized_logs',
29
+ 'search_sanitized_logs',
30
+ 'list_recent_rule_results',
31
+ 'search_rule_results',
32
+ 'get_active_collection',
33
+ 'get_current_test_context',
34
+ 'get_import_bundle_schema',
35
+ 'get_import_bundle_examples',
36
+ 'explain_rule_match',
37
+ 'import_bundle',
38
+ 'request_enable_rule',
39
+ 'request_disable_rule',
40
+ 'request_set_only_active_rule',
41
+ 'request_set_active_collection',
42
+ 'request_update_rule',
43
+ 'request_update_collection',
44
+ 'request_delete_rule',
45
+ 'request_delete_collection',
46
+ 'request_reload_captured_tab',
47
+ 'request_start_capture',
48
+ 'request_restart_capture',
49
+ 'request_stop_capture',
50
+ 'request_capture_current_tab',
51
+ 'request_set_capture_mode'
52
+ ])
53
+
54
+ const ALLOWED_COMMAND_SET = new Set(ALLOWED_COMMANDS)
55
+
56
+ export function isAllowedCommand(command) {
57
+ return ALLOWED_COMMAND_SET.has(String(command || ''))
58
+ }
59
+
60
+ export function createHelloAckMessage() {
61
+ return {
62
+ type: MESSAGE_TYPES.helloAck,
63
+ ok: true,
64
+ version: BRIDGE_PROTOCOL_VERSION
65
+ }
66
+ }
67
+
68
+ export function createClientHelloAckMessage() {
69
+ return {
70
+ type: MESSAGE_TYPES.clientHelloAck,
71
+ ok: true,
72
+ version: BRIDGE_PROTOCOL_VERSION
73
+ }
74
+ }
75
+
76
+ export function createCommandMessage(id, command, payload = {}) {
77
+ return {
78
+ id,
79
+ type: MESSAGE_TYPES.command,
80
+ command,
81
+ payload
82
+ }
83
+ }
84
+
85
+ export function createPingMessage() {
86
+ return { type: MESSAGE_TYPES.ping }
87
+ }
88
+
89
+ export function parseBridgeMessage(raw) {
90
+ try {
91
+ const text = Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw ?? '')
92
+ const message = JSON.parse(text)
93
+ return message && typeof message === 'object' ? message : null
94
+ } catch {
95
+ return null
96
+ }
97
+ }
98
+
99
+ export function validateHelloMessage(message = {}, options = {}) {
100
+ const key = String(message.key || '')
101
+ const expectedKey = String(options.expectedKey || '')
102
+ const ok = message.type === MESSAGE_TYPES.hello
103
+ && message.source === BRIDGE_SOURCE
104
+ && Number(message.version) === BRIDGE_PROTOCOL_VERSION
105
+ && Boolean(key)
106
+ && (!expectedKey || key === expectedKey)
107
+
108
+ if (!ok) {
109
+ return {
110
+ ok: false,
111
+ error: 'Invalid InterceptPilot hello message.'
112
+ }
113
+ }
114
+
115
+ return { ok: true }
116
+ }
117
+
118
+ export function validateClientHelloMessage(message = {}, options = {}) {
119
+ const key = String(message.key || '')
120
+ const expectedKey = String(options.expectedKey || '')
121
+ const ok = message.type === MESSAGE_TYPES.clientHello
122
+ && message.source === MCP_CLIENT_SOURCE
123
+ && Number(message.version) === BRIDGE_PROTOCOL_VERSION
124
+ && Boolean(key)
125
+ && (!expectedKey || key === expectedKey)
126
+
127
+ if (!ok) {
128
+ return {
129
+ ok: false,
130
+ error: 'Invalid InterceptPilot client hello message.'
131
+ }
132
+ }
133
+
134
+ return { ok: true }
135
+ }
136
+
137
+ export function isCommandResponse(message = {}) {
138
+ return Boolean(
139
+ message
140
+ && typeof message.id === 'string'
141
+ && [MESSAGE_TYPES.commandResult, MESSAGE_TYPES.commandError].includes(message.type)
142
+ )
143
+ }
144
+
145
+ export function isClientResponse(message = {}) {
146
+ return Boolean(
147
+ message
148
+ && typeof message.id === 'string'
149
+ && [MESSAGE_TYPES.clientResult, MESSAGE_TYPES.clientError].includes(message.type)
150
+ )
151
+ }
@@ -0,0 +1,228 @@
1
+ import { WebSocketServer } from 'ws'
2
+
3
+ import {
4
+ BRIDGE_HOST,
5
+ BRIDGE_PORT,
6
+ MESSAGE_TYPES,
7
+ createCommandMessage,
8
+ createHelloAckMessage,
9
+ createPingMessage,
10
+ isAllowedCommand,
11
+ isCommandResponse,
12
+ parseBridgeMessage,
13
+ validateHelloMessage
14
+ } from './protocol.js'
15
+
16
+ const DEFAULT_REQUEST_TIMEOUT_MS = 5000
17
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 20000
18
+ const DEFAULT_MAX_PENDING_REQUESTS = 25
19
+ const NOT_CONNECTED_MESSAGE = 'InterceptPilot extension is not connected. Open InterceptPilot settings and start an AI Bridge session.'
20
+ const TIMEOUT_MESSAGE = 'InterceptPilot did not respond in time.'
21
+ const DISCONNECTED_MESSAGE = 'InterceptPilot extension disconnected.'
22
+ const COMMAND_NOT_ALLOWED_MESSAGE = 'Command is not allowed.'
23
+ const TOO_MANY_PENDING_MESSAGE = 'Too many pending InterceptPilot requests.'
24
+
25
+ function createDefaultLogger() {
26
+ return {
27
+ info: message => console.error(`[interceptpilot-mcp] ${message}`),
28
+ warn: message => console.error(`[interceptpilot-mcp] ${message}`),
29
+ error: message => console.error(`[interceptpilot-mcp] ${message}`)
30
+ }
31
+ }
32
+
33
+ function addSocketListener(socket, event, handler) {
34
+ if (typeof socket.on === 'function') {
35
+ socket.on(event, handler)
36
+ return
37
+ }
38
+ socket[`on${event}`] = handler
39
+ }
40
+
41
+ function sendJson(socket, message) {
42
+ socket.send(JSON.stringify(message))
43
+ }
44
+
45
+ function createSafeError(message) {
46
+ return new Error(message)
47
+ }
48
+
49
+ export function createExtensionBridge(options = {}) {
50
+ const {
51
+ WebSocketServerCtor = WebSocketServer,
52
+ port = BRIDGE_PORT,
53
+ expectedKey = '',
54
+ requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
55
+ maxPendingRequests = DEFAULT_MAX_PENDING_REQUESTS,
56
+ heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS,
57
+ idGenerator,
58
+ logger = createDefaultLogger(),
59
+ setTimeoutFn = setTimeout,
60
+ clearTimeoutFn = clearTimeout,
61
+ setIntervalFn = setInterval,
62
+ clearIntervalFn = clearInterval
63
+ } = options
64
+
65
+ const expectedConnectionKey = String(expectedKey || '')
66
+
67
+ let server = null
68
+ let activeSocket = null
69
+ let connected = false
70
+ let requestSeq = 0
71
+ let heartbeatTimer = null
72
+ const pending = new Map()
73
+ const host = BRIDGE_HOST
74
+
75
+ function nextRequestId() {
76
+ if (idGenerator) return idGenerator()
77
+ requestSeq += 1
78
+ return `req_${requestSeq}`
79
+ }
80
+
81
+ function isConnected() {
82
+ return Boolean(activeSocket && connected)
83
+ }
84
+
85
+ function rejectPending(message) {
86
+ for (const [, request] of pending) {
87
+ clearTimeoutFn(request.timer)
88
+ request.reject(createSafeError(message))
89
+ }
90
+ pending.clear()
91
+ }
92
+
93
+ function startHeartbeat() {
94
+ if (heartbeatTimer) return
95
+ heartbeatTimer = setIntervalFn(() => {
96
+ if (activeSocket && connected) sendJson(activeSocket, 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 detachSocket(socket) {
108
+ if (socket !== activeSocket) return
109
+ activeSocket = null
110
+ connected = false
111
+ stopHeartbeat()
112
+ rejectPending(DISCONNECTED_MESSAGE)
113
+ logger.info('Extension disconnected.')
114
+ }
115
+
116
+ function handleResponse(message) {
117
+ if (!isCommandResponse(message)) return
118
+ const request = pending.get(message.id)
119
+ if (!request) return
120
+ pending.delete(message.id)
121
+ clearTimeoutFn(request.timer)
122
+
123
+ if (message.type === 'command:error' || message.ok === false) {
124
+ request.reject(createSafeError(String(message.error?.message || 'InterceptPilot command failed.')))
125
+ return
126
+ }
127
+
128
+ request.resolve(message.result)
129
+ }
130
+
131
+ function acceptHello(socket, message) {
132
+ const validation = validateHelloMessage(message, { expectedKey: expectedConnectionKey })
133
+ if (!validation.ok) {
134
+ logger.warn(validation.error)
135
+ socket.close()
136
+ return
137
+ }
138
+
139
+ if (activeSocket && activeSocket !== socket) {
140
+ const previousSocket = activeSocket
141
+ rejectPending(DISCONNECTED_MESSAGE)
142
+ previousSocket.close()
143
+ }
144
+
145
+ activeSocket = socket
146
+ connected = true
147
+ sendJson(socket, createHelloAckMessage())
148
+ startHeartbeat()
149
+ logger.info('Extension connected.')
150
+ }
151
+
152
+ function handleSocketMessage(socket, raw) {
153
+ const message = parseBridgeMessage(raw)
154
+ if (!message) {
155
+ logger.warn('Ignoring invalid message from extension.')
156
+ return
157
+ }
158
+
159
+ if (!connected || socket !== activeSocket) {
160
+ if (message.type === 'hello') acceptHello(socket, message)
161
+ return
162
+ }
163
+
164
+ if (message.type === MESSAGE_TYPES.pong) return
165
+
166
+ handleResponse(message)
167
+ }
168
+
169
+ function handleSocket(socket) {
170
+ addSocketListener(socket, 'message', raw => handleSocketMessage(socket, raw))
171
+ addSocketListener(socket, 'close', () => detachSocket(socket))
172
+ addSocketListener(socket, 'error', () => detachSocket(socket))
173
+ }
174
+
175
+ function start() {
176
+ if (server) return server
177
+ server = new WebSocketServerCtor({ host, port })
178
+ server.on('connection', handleSocket)
179
+ server.on('listening', () => logger.info(`WebSocket bridge listening on ${host}:${port}.`))
180
+ server.on('error', () => logger.error('WebSocket bridge error.'))
181
+ return server
182
+ }
183
+
184
+ async function close() {
185
+ stopHeartbeat()
186
+ rejectPending(DISCONNECTED_MESSAGE)
187
+ if (activeSocket) activeSocket.close()
188
+ activeSocket = null
189
+ connected = false
190
+ if (server && typeof server.close === 'function') {
191
+ await new Promise(resolve => server.close(resolve))
192
+ }
193
+ server = null
194
+ }
195
+
196
+ function callCommand(command, payload = {}) {
197
+ if (!isAllowedCommand(command)) {
198
+ return Promise.reject(createSafeError(COMMAND_NOT_ALLOWED_MESSAGE))
199
+ }
200
+ if (!isConnected()) {
201
+ return Promise.reject(createSafeError(NOT_CONNECTED_MESSAGE))
202
+ }
203
+ if (pending.size >= maxPendingRequests) {
204
+ return Promise.reject(createSafeError(TOO_MANY_PENDING_MESSAGE))
205
+ }
206
+
207
+ const id = nextRequestId()
208
+ const message = createCommandMessage(id, command, payload && typeof payload === 'object' ? payload : {})
209
+
210
+ return new Promise((resolve, reject) => {
211
+ const timer = setTimeoutFn(() => {
212
+ pending.delete(id)
213
+ reject(createSafeError(TIMEOUT_MESSAGE))
214
+ }, requestTimeoutMs)
215
+ pending.set(id, { resolve, reject, timer })
216
+ sendJson(activeSocket, message)
217
+ })
218
+ }
219
+
220
+ return {
221
+ start,
222
+ close,
223
+ handleSocket,
224
+ callCommand,
225
+ isConnected,
226
+ getPendingCount: () => pending.size
227
+ }
228
+ }