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/LICENSE +21 -0
- package/README.md +116 -0
- package/package.json +44 -0
- package/src/daemon-client.js +281 -0
- package/src/daemon.js +312 -0
- package/src/import-bundle-schema.js +221 -0
- package/src/index.js +113 -0
- package/src/mcp-server.js +674 -0
- package/src/process-manager.js +36 -0
- package/src/protocol.js +151 -0
- package/src/websocket-bridge.js +228 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { getImportBundleExamples, getImportBundleSchema } from './import-bundle-schema.js'
|
|
5
|
+
import { ALLOWED_COMMANDS, isAllowedCommand } from './protocol.js'
|
|
6
|
+
|
|
7
|
+
const IMPORT_BUNDLE_INPUT_DESCRIPTION = 'A JSON string containing an InterceptPilot import bundle. It can be either a single rule bundle with type "interceptpilot.rule" or a collection bundle with type "interceptpilot.collection". The extension will create a pending proposal and wait for user confirmation.'
|
|
8
|
+
const RULE_ID_INPUT_DESCRIPTION = 'The InterceptPilot rule id to compare against a captured request.'
|
|
9
|
+
const RULE_ACTION_RULE_ID_INPUT_DESCRIPTION = 'The existing InterceptPilot rule id to use for the pending rule action proposal.'
|
|
10
|
+
const COLLECTION_ACTION_COLLECTION_ID_INPUT_DESCRIPTION = 'The existing InterceptPilot collection id to use for the pending collection action proposal.'
|
|
11
|
+
const REQUEST_ID_INPUT_DESCRIPTION = 'The captured request/network entry id to compare against the rule.'
|
|
12
|
+
const EMPTY_JSON_INPUT_SCHEMA = {
|
|
13
|
+
type: 'object',
|
|
14
|
+
additionalProperties: false
|
|
15
|
+
}
|
|
16
|
+
const IMPORT_BUNDLE_JSON_INPUT_SCHEMA = {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
bundleJson: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
description: IMPORT_BUNDLE_INPUT_DESCRIPTION
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
required: ['bundleJson'],
|
|
25
|
+
additionalProperties: false
|
|
26
|
+
}
|
|
27
|
+
const EXPLAIN_RULE_MATCH_JSON_INPUT_SCHEMA = {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
ruleId: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: RULE_ID_INPUT_DESCRIPTION
|
|
33
|
+
},
|
|
34
|
+
requestId: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: REQUEST_ID_INPUT_DESCRIPTION
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
required: ['ruleId', 'requestId'],
|
|
40
|
+
additionalProperties: false
|
|
41
|
+
}
|
|
42
|
+
const RULE_ACTION_JSON_INPUT_SCHEMA = {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
ruleId: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: RULE_ACTION_RULE_ID_INPUT_DESCRIPTION
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
required: ['ruleId'],
|
|
51
|
+
additionalProperties: false
|
|
52
|
+
}
|
|
53
|
+
const COLLECTION_ACTION_JSON_INPUT_SCHEMA = {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
collectionId: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: COLLECTION_ACTION_COLLECTION_ID_INPUT_DESCRIPTION
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
required: ['collectionId'],
|
|
62
|
+
additionalProperties: false
|
|
63
|
+
}
|
|
64
|
+
const UPDATE_RULE_JSON_INPUT_SCHEMA = {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
ruleId: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: RULE_ACTION_RULE_ID_INPUT_DESCRIPTION
|
|
70
|
+
},
|
|
71
|
+
patch: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
description: 'Safe fields to update on the existing InterceptPilot rule. responseBody and responseHeaders are not allowed by this tool.'
|
|
74
|
+
},
|
|
75
|
+
reason: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description: 'Short reason shown to the user in the InterceptPilot confirmation modal.'
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
required: ['ruleId', 'patch'],
|
|
81
|
+
additionalProperties: false
|
|
82
|
+
}
|
|
83
|
+
const UPDATE_COLLECTION_JSON_INPUT_SCHEMA = {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
collectionId: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
description: 'The existing InterceptPilot collection id to update.'
|
|
89
|
+
},
|
|
90
|
+
patch: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
description: 'Safe fields to update on the existing InterceptPilot collection.'
|
|
93
|
+
},
|
|
94
|
+
reason: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
description: 'Short reason shown to the user in the InterceptPilot confirmation modal.'
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
required: ['collectionId', 'patch'],
|
|
100
|
+
additionalProperties: false
|
|
101
|
+
}
|
|
102
|
+
const DELETE_RULE_JSON_INPUT_SCHEMA = {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
ruleId: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: RULE_ACTION_RULE_ID_INPUT_DESCRIPTION
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
required: ['ruleId'],
|
|
111
|
+
additionalProperties: false
|
|
112
|
+
}
|
|
113
|
+
const DELETE_COLLECTION_JSON_INPUT_SCHEMA = {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
collectionId: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
description: 'The existing InterceptPilot collection id to delete after human confirmation.'
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
required: ['collectionId'],
|
|
122
|
+
additionalProperties: false
|
|
123
|
+
}
|
|
124
|
+
const PAGINATION_INPUT_PROPERTIES = {
|
|
125
|
+
limit: {
|
|
126
|
+
type: 'number',
|
|
127
|
+
minimum: 1,
|
|
128
|
+
maximum: 100
|
|
129
|
+
},
|
|
130
|
+
offset: {
|
|
131
|
+
type: 'number',
|
|
132
|
+
minimum: 0
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const REQUEST_SEARCH_INPUT_PROPERTIES = {
|
|
136
|
+
method: { type: 'string' },
|
|
137
|
+
status: { type: 'number' },
|
|
138
|
+
statusGroup: { type: 'string' },
|
|
139
|
+
resourceType: { type: 'string' },
|
|
140
|
+
urlContains: { type: 'string' },
|
|
141
|
+
pathContains: { type: 'string' },
|
|
142
|
+
origin: { type: 'string' },
|
|
143
|
+
matchedRuleId: { type: 'string' },
|
|
144
|
+
matchedRuleName: { type: 'string' },
|
|
145
|
+
intercepted: { type: 'boolean' },
|
|
146
|
+
appliedAction: { type: 'string' },
|
|
147
|
+
hasError: { type: 'boolean' },
|
|
148
|
+
since: { type: 'string' },
|
|
149
|
+
...PAGINATION_INPUT_PROPERTIES
|
|
150
|
+
}
|
|
151
|
+
const LOG_SEARCH_INPUT_PROPERTIES = {
|
|
152
|
+
level: { type: 'string' },
|
|
153
|
+
scope: { type: 'string' },
|
|
154
|
+
messageContains: { type: 'string' },
|
|
155
|
+
urlContains: { type: 'string' },
|
|
156
|
+
since: { type: 'string' },
|
|
157
|
+
...PAGINATION_INPUT_PROPERTIES
|
|
158
|
+
}
|
|
159
|
+
const RULE_RESULT_SEARCH_INPUT_PROPERTIES = {
|
|
160
|
+
ruleId: { type: 'string' },
|
|
161
|
+
ruleName: { type: 'string' },
|
|
162
|
+
method: { type: 'string' },
|
|
163
|
+
resourceType: { type: 'string' },
|
|
164
|
+
status: { type: 'number' },
|
|
165
|
+
appliedAction: { type: 'string' },
|
|
166
|
+
intercepted: { type: 'boolean' },
|
|
167
|
+
matched: { type: 'boolean' },
|
|
168
|
+
urlContains: { type: 'string' },
|
|
169
|
+
pathContains: { type: 'string' },
|
|
170
|
+
since: { type: 'string' },
|
|
171
|
+
...PAGINATION_INPUT_PROPERTIES
|
|
172
|
+
}
|
|
173
|
+
const RULE_SEARCH_INPUT_PROPERTIES = {
|
|
174
|
+
collectionId: { type: 'string' },
|
|
175
|
+
enabled: { type: 'boolean' },
|
|
176
|
+
method: { type: 'string' },
|
|
177
|
+
resourceType: { type: 'string' },
|
|
178
|
+
matchType: { type: 'string' },
|
|
179
|
+
urlPatternContains: { type: 'string' },
|
|
180
|
+
actionType: { type: 'string' },
|
|
181
|
+
nameContains: { type: 'string' },
|
|
182
|
+
hasConflict: { type: 'boolean' },
|
|
183
|
+
lastError: { type: 'boolean' },
|
|
184
|
+
...PAGINATION_INPUT_PROPERTIES
|
|
185
|
+
}
|
|
186
|
+
const REQUEST_SEARCH_JSON_INPUT_SCHEMA = {
|
|
187
|
+
type: 'object',
|
|
188
|
+
properties: REQUEST_SEARCH_INPUT_PROPERTIES,
|
|
189
|
+
additionalProperties: false
|
|
190
|
+
}
|
|
191
|
+
const LOG_SEARCH_JSON_INPUT_SCHEMA = {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: LOG_SEARCH_INPUT_PROPERTIES,
|
|
194
|
+
additionalProperties: false
|
|
195
|
+
}
|
|
196
|
+
const RULE_RESULT_SEARCH_JSON_INPUT_SCHEMA = {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: RULE_RESULT_SEARCH_INPUT_PROPERTIES,
|
|
199
|
+
additionalProperties: false
|
|
200
|
+
}
|
|
201
|
+
const RULE_SEARCH_JSON_INPUT_SCHEMA = {
|
|
202
|
+
type: 'object',
|
|
203
|
+
properties: RULE_SEARCH_INPUT_PROPERTIES,
|
|
204
|
+
additionalProperties: false
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function createEmptyPayload() {
|
|
208
|
+
return {}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function createTextResult(result) {
|
|
212
|
+
return {
|
|
213
|
+
content: [
|
|
214
|
+
{
|
|
215
|
+
type: 'text',
|
|
216
|
+
text: JSON.stringify(result, null, 2)
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function createSafeToolError(error) {
|
|
223
|
+
return {
|
|
224
|
+
isError: true,
|
|
225
|
+
content: [
|
|
226
|
+
{
|
|
227
|
+
type: 'text',
|
|
228
|
+
text: String(error?.message || 'InterceptPilot MCP tool failed.')
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildImportBundlePayload(args = {}) {
|
|
235
|
+
const bundleJson = args?.bundleJson
|
|
236
|
+
if (typeof bundleJson !== 'string' || !bundleJson.trim()) {
|
|
237
|
+
throw new Error('bundleJson must be a JSON string.')
|
|
238
|
+
}
|
|
239
|
+
return { bundleJson }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildExplainRuleMatchPayload(args = {}) {
|
|
243
|
+
const ruleId = args?.ruleId
|
|
244
|
+
const requestId = args?.requestId
|
|
245
|
+
if (typeof ruleId !== 'string' || !ruleId.trim() || typeof requestId !== 'string' || !requestId.trim()) {
|
|
246
|
+
throw new Error('ruleId and requestId are required strings.')
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
ruleId,
|
|
250
|
+
requestId
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function buildRuleActionPayload(args = {}) {
|
|
255
|
+
const ruleId = args?.ruleId
|
|
256
|
+
if (typeof ruleId !== 'string' || !ruleId.trim()) {
|
|
257
|
+
throw new Error('ruleId must be a non-empty string.')
|
|
258
|
+
}
|
|
259
|
+
return { ruleId }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function buildCollectionActionPayload(args = {}) {
|
|
263
|
+
const collectionId = args?.collectionId
|
|
264
|
+
if (typeof collectionId !== 'string' || !collectionId.trim()) {
|
|
265
|
+
throw new Error('collectionId must be a non-empty string.')
|
|
266
|
+
}
|
|
267
|
+
return { collectionId }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function buildUpdateRulePayload(args = {}) {
|
|
271
|
+
const ruleId = args?.ruleId
|
|
272
|
+
const patch = args?.patch
|
|
273
|
+
if (typeof ruleId !== 'string' || !ruleId.trim() || !patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
274
|
+
throw new Error('ruleId and patch are required.')
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
ruleId,
|
|
278
|
+
patch,
|
|
279
|
+
...(typeof args.reason === 'string' && args.reason.trim() ? { reason: args.reason } : {})
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildUpdateCollectionPayload(args = {}) {
|
|
284
|
+
const collectionId = args?.collectionId
|
|
285
|
+
const patch = args?.patch
|
|
286
|
+
if (typeof collectionId !== 'string' || !collectionId.trim() || !patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
287
|
+
throw new Error('collectionId and patch are required.')
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
collectionId,
|
|
291
|
+
patch,
|
|
292
|
+
...(typeof args.reason === 'string' && args.reason.trim() ? { reason: args.reason } : {})
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function buildDeleteRulePayload(args = {}) {
|
|
297
|
+
const ruleId = args?.ruleId
|
|
298
|
+
if (typeof ruleId !== 'string' || !ruleId.trim()) {
|
|
299
|
+
throw new Error('ruleId must be a non-empty string.')
|
|
300
|
+
}
|
|
301
|
+
return { ruleId }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function buildDeleteCollectionPayload(args = {}) {
|
|
305
|
+
const collectionId = args?.collectionId
|
|
306
|
+
if (typeof collectionId !== 'string' || !collectionId.trim()) {
|
|
307
|
+
throw new Error('collectionId must be a non-empty string.')
|
|
308
|
+
}
|
|
309
|
+
return { collectionId }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function buildAllowedPayload(args = {}, allowedKeys = []) {
|
|
313
|
+
const allowed = new Set(allowedKeys)
|
|
314
|
+
return Object.fromEntries(
|
|
315
|
+
Object.entries(args || {}).filter(([key, value]) => (
|
|
316
|
+
allowed.has(key) && value !== undefined && value !== null && value !== ''
|
|
317
|
+
))
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function createImportProposalResult(result = {}) {
|
|
322
|
+
if (result.status === 'applied') {
|
|
323
|
+
const summary = result.summary || {}
|
|
324
|
+
return {
|
|
325
|
+
content: [
|
|
326
|
+
{
|
|
327
|
+
type: 'text',
|
|
328
|
+
text: [
|
|
329
|
+
'Import bundle applied in InterceptPilot.',
|
|
330
|
+
'Approval: session_permission.',
|
|
331
|
+
summary.collectionName ? `Collection: ${summary.collectionName}` : '',
|
|
332
|
+
summary.ruleName ? `Rule: ${summary.ruleName}` : '',
|
|
333
|
+
Number.isFinite(Number(summary.ruleCount)) ? `Rules: ${summary.ruleCount}` : ''
|
|
334
|
+
].filter(Boolean).join('\n')
|
|
335
|
+
}
|
|
336
|
+
]
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const summary = result.summary || {}
|
|
340
|
+
const lines = [
|
|
341
|
+
'Import proposal sent to InterceptPilot.',
|
|
342
|
+
'The user must confirm it in the Full App before anything is applied.',
|
|
343
|
+
result.pendingImportId ? `Pending import: ${result.pendingImportId}` : '',
|
|
344
|
+
summary.collectionName ? `Collection: ${summary.collectionName}` : '',
|
|
345
|
+
summary.ruleName ? `Rule: ${summary.ruleName}` : '',
|
|
346
|
+
Number.isFinite(Number(summary.ruleCount)) ? `Rules: ${summary.ruleCount}` : ''
|
|
347
|
+
].filter(Boolean)
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
content: [
|
|
351
|
+
{
|
|
352
|
+
type: 'text',
|
|
353
|
+
text: lines.join('\n')
|
|
354
|
+
}
|
|
355
|
+
]
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function createRuleActionProposalResult(result = {}) {
|
|
360
|
+
if (result.status === 'applied') {
|
|
361
|
+
const summary = result.summary || {}
|
|
362
|
+
return {
|
|
363
|
+
content: [
|
|
364
|
+
{
|
|
365
|
+
type: 'text',
|
|
366
|
+
text: [
|
|
367
|
+
'Rule action applied in InterceptPilot.',
|
|
368
|
+
'Approval: session_permission.',
|
|
369
|
+
result.action ? `Action: ${result.action}` : '',
|
|
370
|
+
summary.ruleName ? `Rule: ${summary.ruleName}` : '',
|
|
371
|
+
summary.collectionName ? `Collection: ${summary.collectionName}` : ''
|
|
372
|
+
].filter(Boolean).join('\n')
|
|
373
|
+
}
|
|
374
|
+
]
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const summary = result.summary || {}
|
|
378
|
+
const lines = [
|
|
379
|
+
'Rule action proposal sent to InterceptPilot.',
|
|
380
|
+
'The user must confirm it in the Full App before anything is applied.',
|
|
381
|
+
result.pendingActionId ? `Pending action: ${result.pendingActionId}` : '',
|
|
382
|
+
result.action ? `Action: ${result.action}` : '',
|
|
383
|
+
summary.ruleName ? `Rule: ${summary.ruleName}` : '',
|
|
384
|
+
summary.collectionName ? `Collection: ${summary.collectionName}` : '',
|
|
385
|
+
Number.isFinite(Number(summary.affectedRuleCount)) ? `Affected rules: ${summary.affectedRuleCount}` : ''
|
|
386
|
+
].filter(Boolean)
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
content: [
|
|
390
|
+
{
|
|
391
|
+
type: 'text',
|
|
392
|
+
text: lines.join('\n')
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function createReadOnlyTool(description) {
|
|
399
|
+
return {
|
|
400
|
+
description,
|
|
401
|
+
jsonInputSchema: EMPTY_JSON_INPUT_SCHEMA,
|
|
402
|
+
registeredInputSchema: {},
|
|
403
|
+
buildPayload: createEmptyPayload,
|
|
404
|
+
formatResult: createTextResult
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function createSearchTool(description, jsonInputSchema) {
|
|
409
|
+
const registeredInputSchema = Object.fromEntries(Object.entries(jsonInputSchema.properties || {}).map(([key, schema]) => {
|
|
410
|
+
const base = schema.type === 'number'
|
|
411
|
+
? z.number()
|
|
412
|
+
: schema.type === 'boolean' ? z.boolean() : z.string()
|
|
413
|
+
return [key, base.optional()]
|
|
414
|
+
}))
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
description,
|
|
418
|
+
jsonInputSchema,
|
|
419
|
+
registeredInputSchema,
|
|
420
|
+
buildPayload: args => buildAllowedPayload(args, Object.keys(jsonInputSchema.properties || {})),
|
|
421
|
+
formatResult: createTextResult
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function createStaticReadOnlyTool(description, getResult) {
|
|
426
|
+
return {
|
|
427
|
+
description,
|
|
428
|
+
jsonInputSchema: EMPTY_JSON_INPUT_SCHEMA,
|
|
429
|
+
registeredInputSchema: {},
|
|
430
|
+
buildPayload: createEmptyPayload,
|
|
431
|
+
formatResult: createTextResult,
|
|
432
|
+
getResult
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function createReloadProposalResult(result = {}) {
|
|
437
|
+
if (result.status === 'applied') {
|
|
438
|
+
const summary = result.summary || {}
|
|
439
|
+
return {
|
|
440
|
+
content: [
|
|
441
|
+
{
|
|
442
|
+
type: 'text',
|
|
443
|
+
text: [
|
|
444
|
+
'Captured tab reload applied in InterceptPilot.',
|
|
445
|
+
'Approval: session_permission.',
|
|
446
|
+
summary.origin ? `Origin: ${summary.origin}` : '',
|
|
447
|
+
summary.path ? `Path: ${summary.path}` : ''
|
|
448
|
+
].filter(Boolean).join('\n')
|
|
449
|
+
}
|
|
450
|
+
]
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const summary = result.summary || {}
|
|
454
|
+
const lines = [
|
|
455
|
+
'Captured tab reload proposal sent to InterceptPilot.',
|
|
456
|
+
'The user must confirm it in the Full App before the tab is reloaded.',
|
|
457
|
+
result.pendingActionId ? `Pending action: ${result.pendingActionId}` : '',
|
|
458
|
+
summary.origin ? `Origin: ${summary.origin}` : '',
|
|
459
|
+
summary.path ? `Path: ${summary.path}` : ''
|
|
460
|
+
].filter(Boolean)
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
content: [
|
|
464
|
+
{
|
|
465
|
+
type: 'text',
|
|
466
|
+
text: lines.join('\n')
|
|
467
|
+
}
|
|
468
|
+
]
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function createCaptureActionResult(result = {}) {
|
|
473
|
+
const summary = result.summary || {}
|
|
474
|
+
const prefix = result.status === 'applied' ? 'Capture action applied in InterceptPilot.' : 'Capture action proposal sent to InterceptPilot.'
|
|
475
|
+
const lines = [
|
|
476
|
+
prefix,
|
|
477
|
+
result.status === 'applied' ? 'Approval: session_permission.' : 'The user must confirm it in the Full App before the capture state changes.',
|
|
478
|
+
result.pendingActionId ? `Pending action: ${result.pendingActionId}` : '',
|
|
479
|
+
result.action ? `Action: ${result.action}` : '',
|
|
480
|
+
summary.origin ? `Origin: ${summary.origin}` : '',
|
|
481
|
+
summary.path ? `Path: ${summary.path}` : '',
|
|
482
|
+
summary.nextMode ? `Mode: ${summary.nextMode}` : ''
|
|
483
|
+
].filter(Boolean)
|
|
484
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function createCaptureActionTool(description, inputSchema = EMPTY_JSON_INPUT_SCHEMA, registeredInputSchema = {}, buildPayload = createEmptyPayload) {
|
|
488
|
+
return { description, jsonInputSchema: inputSchema, registeredInputSchema, buildPayload, formatResult: createCaptureActionResult }
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function createRuleActionTool(description) {
|
|
492
|
+
return {
|
|
493
|
+
description,
|
|
494
|
+
jsonInputSchema: RULE_ACTION_JSON_INPUT_SCHEMA,
|
|
495
|
+
registeredInputSchema: {
|
|
496
|
+
ruleId: z.string().min(1).describe(RULE_ACTION_RULE_ID_INPUT_DESCRIPTION)
|
|
497
|
+
},
|
|
498
|
+
buildPayload: buildRuleActionPayload,
|
|
499
|
+
formatResult: createRuleActionProposalResult
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function createCollectionActionTool(description) {
|
|
504
|
+
return {
|
|
505
|
+
description,
|
|
506
|
+
jsonInputSchema: COLLECTION_ACTION_JSON_INPUT_SCHEMA,
|
|
507
|
+
registeredInputSchema: {
|
|
508
|
+
collectionId: z.string().min(1).describe(COLLECTION_ACTION_COLLECTION_ID_INPUT_DESCRIPTION)
|
|
509
|
+
},
|
|
510
|
+
buildPayload: buildCollectionActionPayload,
|
|
511
|
+
formatResult: createRuleActionProposalResult
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function createUpdateRuleTool(description) {
|
|
516
|
+
return {
|
|
517
|
+
description,
|
|
518
|
+
jsonInputSchema: UPDATE_RULE_JSON_INPUT_SCHEMA,
|
|
519
|
+
registeredInputSchema: {
|
|
520
|
+
ruleId: z.string().min(1).describe(RULE_ACTION_RULE_ID_INPUT_DESCRIPTION),
|
|
521
|
+
patch: z.object({}).passthrough().describe('Safe rule patch.'),
|
|
522
|
+
reason: z.string().optional().describe('Reason shown in the confirmation modal.')
|
|
523
|
+
},
|
|
524
|
+
buildPayload: buildUpdateRulePayload,
|
|
525
|
+
formatResult: createRuleActionProposalResult
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function createUpdateCollectionTool(description) {
|
|
530
|
+
return {
|
|
531
|
+
description,
|
|
532
|
+
jsonInputSchema: UPDATE_COLLECTION_JSON_INPUT_SCHEMA,
|
|
533
|
+
registeredInputSchema: {
|
|
534
|
+
collectionId: z.string().min(1).describe('The existing InterceptPilot collection id to update.'),
|
|
535
|
+
patch: z.object({}).passthrough().describe('Safe collection patch.'),
|
|
536
|
+
reason: z.string().optional().describe('Reason shown in the confirmation modal.')
|
|
537
|
+
},
|
|
538
|
+
buildPayload: buildUpdateCollectionPayload,
|
|
539
|
+
formatResult: createRuleActionProposalResult
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function createDeleteRuleTool(description) {
|
|
544
|
+
return {
|
|
545
|
+
description,
|
|
546
|
+
jsonInputSchema: DELETE_RULE_JSON_INPUT_SCHEMA,
|
|
547
|
+
registeredInputSchema: {
|
|
548
|
+
ruleId: z.string().min(1).describe(RULE_ACTION_RULE_ID_INPUT_DESCRIPTION)
|
|
549
|
+
},
|
|
550
|
+
buildPayload: buildDeleteRulePayload,
|
|
551
|
+
formatResult: createRuleActionProposalResult
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function createDeleteCollectionTool(description) {
|
|
556
|
+
return {
|
|
557
|
+
description,
|
|
558
|
+
jsonInputSchema: DELETE_COLLECTION_JSON_INPUT_SCHEMA,
|
|
559
|
+
registeredInputSchema: {
|
|
560
|
+
collectionId: z.string().min(1).describe('The existing InterceptPilot collection id to delete after human confirmation.')
|
|
561
|
+
},
|
|
562
|
+
buildPayload: buildDeleteCollectionPayload,
|
|
563
|
+
formatResult: createRuleActionProposalResult
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const MCP_TOOLS = {
|
|
568
|
+
list_captured_requests: createReadOnlyTool('List sanitized requests captured in the active InterceptPilot capture context. Useful for identifying endpoints to mock. Does not include headers, bodies, cookies, Authorization values, or query parameter values.'),
|
|
569
|
+
search_captured_requests: createSearchTool('Search sanitized requests captured in the active InterceptPilot capture context using safe filters and pagination. Does not include headers, bodies, cookies, Authorization values, or query parameter values unless the corresponding AI Bridge permission is enabled.', REQUEST_SEARCH_JSON_INPUT_SCHEMA),
|
|
570
|
+
list_collections: createReadOnlyTool('List InterceptPilot collections with safe summary fields and rule counts. Useful for understanding available scenario groups. Does not include headers, bodies, cookies, tokens, or query parameter values.'),
|
|
571
|
+
list_rules: createReadOnlyTool('List InterceptPilot rules with sanitized match/action summaries. Useful for understanding existing mocks and failures. Does not include response bodies, raw headers, cookies, Authorization values, or query parameter values.'),
|
|
572
|
+
search_rules: createSearchTool('Search InterceptPilot rules using safe metadata, match, action, and diagnostics filters. Does not include response bodies, raw headers, cookies, Authorization values, or query parameter values.', RULE_SEARCH_JSON_INPUT_SCHEMA),
|
|
573
|
+
list_sanitized_logs: createReadOnlyTool('List sanitized recent logs from the active InterceptPilot capture context. Useful for diagnosing page or capture issues. Does not include raw stack traces, headers, bodies, cookies, Authorization values, or query parameter values.'),
|
|
574
|
+
search_sanitized_logs: createSearchTool('Search sanitized recent logs from the active InterceptPilot capture context using safe filters and pagination. Does not include raw stack traces, headers, bodies, cookies, Authorization values, or query parameter values unless the corresponding AI Bridge permission is enabled.', LOG_SEARCH_JSON_INPUT_SCHEMA),
|
|
575
|
+
list_recent_rule_results: createReadOnlyTool('List sanitized recent rule result summaries from the active InterceptPilot capture context. Useful for seeing which rules matched, which action applied, and whether requests were intercepted. Does not include headers, bodies, cookies, Authorization values, or query parameter values.'),
|
|
576
|
+
search_rule_results: createSearchTool('Search sanitized recent rule result summaries using safe filters and pagination. Useful for finding which rule applied to a specific request or why a rule did not match. Does not include headers, bodies, cookies, Authorization values, or query parameter values.', RULE_RESULT_SEARCH_JSON_INPUT_SCHEMA),
|
|
577
|
+
get_active_collection: createReadOnlyTool('Get the active InterceptPilot collection summary and rule counts. Useful before proposing scenarios for the current collection. Does not include headers, bodies, cookies, tokens, or query parameter values.'),
|
|
578
|
+
get_current_test_context: createReadOnlyTool('Get the current sanitized InterceptPilot test context, including capture status, capture mode, active tab origin/path, active collection summary, rule counts, and AI Bridge status. Does not include full URLs with query values, headers, bodies, cookies, or tab title.'),
|
|
579
|
+
get_import_bundle_schema: createStaticReadOnlyTool('Return the static InterceptPilot import bundle schema, supported bundle types, allowed enum values, and guidance for creating rule or collection bundles. Does not require the extension to be connected and does not alter state.', getImportBundleSchema),
|
|
580
|
+
get_import_bundle_examples: createStaticReadOnlyTool('Return safe static examples of InterceptPilot rule and collection import bundles. Useful as references before calling import_bundle. Does not require the extension to be connected and does not alter state.', getImportBundleExamples),
|
|
581
|
+
explain_rule_match: {
|
|
582
|
+
description: 'Compare an InterceptPilot rule with a captured request and return a sanitized diagnostic explaining why the rule matches or why it likely did not apply. Does not expose headers, bodies, cookies, Authorization values, or query parameter values.',
|
|
583
|
+
jsonInputSchema: EXPLAIN_RULE_MATCH_JSON_INPUT_SCHEMA,
|
|
584
|
+
registeredInputSchema: {
|
|
585
|
+
ruleId: z.string().min(1).describe(RULE_ID_INPUT_DESCRIPTION),
|
|
586
|
+
requestId: z.string().min(1).describe(REQUEST_ID_INPUT_DESCRIPTION)
|
|
587
|
+
},
|
|
588
|
+
buildPayload: buildExplainRuleMatchPayload,
|
|
589
|
+
formatResult: createTextResult
|
|
590
|
+
},
|
|
591
|
+
import_bundle: {
|
|
592
|
+
description: 'Send an InterceptPilot rule or collection import bundle to the extension as a pending proposal. Supports bundles with type "interceptpilot.rule" or "interceptpilot.collection". The user must confirm it in the Full App before anything is applied.',
|
|
593
|
+
jsonInputSchema: IMPORT_BUNDLE_JSON_INPUT_SCHEMA,
|
|
594
|
+
registeredInputSchema: {
|
|
595
|
+
bundleJson: z.string().min(1).describe(IMPORT_BUNDLE_INPUT_DESCRIPTION)
|
|
596
|
+
},
|
|
597
|
+
buildPayload: buildImportBundlePayload,
|
|
598
|
+
formatResult: createImportProposalResult
|
|
599
|
+
},
|
|
600
|
+
request_enable_rule: createRuleActionTool('Request enabling an existing InterceptPilot rule. This creates a pending action proposal in the Full App and requires user confirmation before the rule is enabled.'),
|
|
601
|
+
request_disable_rule: createRuleActionTool('Request disabling an existing InterceptPilot rule. This creates a pending action proposal in the Full App and requires user confirmation before the rule is disabled.'),
|
|
602
|
+
request_set_only_active_rule: createRuleActionTool('Request making one rule the only active rule in its collection. This creates a pending action proposal in the Full App and requires user confirmation before enabling the selected rule and disabling the others in the same collection.'),
|
|
603
|
+
request_set_active_collection: createCollectionActionTool('Request making an existing InterceptPilot collection the active collection. This creates a pending action proposal in the Full App and requires user confirmation before the active collection changes, unless the session allows rule and collection actions without confirmation.'),
|
|
604
|
+
request_update_rule: createUpdateRuleTool('Request updating safe fields on an existing InterceptPilot rule. This is applied without confirmation when rule and collection actions are enabled for the session; otherwise it creates a pending proposal. It does not allow responseBody or responseHeaders edits.'),
|
|
605
|
+
request_update_collection: createUpdateCollectionTool('Request updating safe fields on an existing InterceptPilot collection. This is applied without confirmation when rule and collection actions are enabled for the session; otherwise it creates a pending proposal.'),
|
|
606
|
+
request_delete_rule: createDeleteRuleTool('Request deleting an existing InterceptPilot rule. It is applied without confirmation only when destructive rule and collection actions are enabled for the session; otherwise it always creates a pending proposal.'),
|
|
607
|
+
request_delete_collection: createDeleteCollectionTool('Request deleting an existing InterceptPilot collection. It is applied without confirmation only when destructive rule and collection actions are enabled for the session; otherwise it always creates a pending proposal.'),
|
|
608
|
+
request_reload_captured_tab: {
|
|
609
|
+
description: 'Request reloading only the InterceptPilot captured tab so a mock or rule change can be validated. Creates a pending action proposal in the Full App that the user must confirm, unless the captured-tab reload session permission is enabled. Never reloads an arbitrary or internal (chrome://, extension://) page and does not change capture mode or debugger state.',
|
|
610
|
+
jsonInputSchema: EMPTY_JSON_INPUT_SCHEMA,
|
|
611
|
+
registeredInputSchema: {},
|
|
612
|
+
buildPayload: createEmptyPayload,
|
|
613
|
+
formatResult: createReloadProposalResult
|
|
614
|
+
},
|
|
615
|
+
request_start_capture: createCaptureActionTool('Request starting capture on the eligible current web tab. It is applied without confirmation when capture control is enabled for the session; otherwise it creates a pending proposal. Never targets arbitrary URLs or restricted pages.'),
|
|
616
|
+
request_restart_capture: createCaptureActionTool('Request restarting capture on the eligible current web tab using the configured capture mode. It is applied without confirmation when capture control is enabled for the session; otherwise it creates a pending proposal.'),
|
|
617
|
+
request_stop_capture: createCaptureActionTool('Request stopping capture on the current captured web tab. It is applied without confirmation when capture control is enabled for the session; otherwise it creates a pending proposal.'),
|
|
618
|
+
request_capture_current_tab: createCaptureActionTool('Request capturing the eligible current web tab. It is applied without confirmation when capture control is enabled for the session; otherwise it creates a pending proposal. It does not accept arbitrary tab IDs or URLs.'),
|
|
619
|
+
request_set_capture_mode: createCaptureActionTool('Request changing capture mode. Allowed values are auto, full, and light. It is applied without confirmation when capture control is enabled for the session; otherwise it creates a pending proposal.', {
|
|
620
|
+
type: 'object', properties: { mode: { type: 'string', enum: ['auto', 'full', 'light'] } }, required: ['mode'], additionalProperties: false
|
|
621
|
+
}, { mode: z.enum(['auto', 'full', 'light']).describe('Capture mode: auto, full, or light.') }, args => ({ mode: args.mode }))
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function getMcpToolNames() {
|
|
625
|
+
return Object.keys(MCP_TOOLS)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export function getMcpToolDefinitions() {
|
|
629
|
+
return Object.entries(MCP_TOOLS).map(([name, tool]) => ({
|
|
630
|
+
name,
|
|
631
|
+
description: tool.description,
|
|
632
|
+
inputSchema: tool.jsonInputSchema
|
|
633
|
+
}))
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export function createToolCallHandler({ bridge }) {
|
|
637
|
+
return async function handleToolCall(name, args = {}) {
|
|
638
|
+
try {
|
|
639
|
+
const tool = MCP_TOOLS[name]
|
|
640
|
+
if (!tool || !isAllowedCommand(name)) {
|
|
641
|
+
throw new Error('Unknown InterceptPilot MCP tool.')
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const payload = tool.buildPayload(args)
|
|
645
|
+
if (tool.getResult) {
|
|
646
|
+
return tool.formatResult(tool.getResult(payload))
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const result = await bridge.callCommand(name, payload)
|
|
650
|
+
if (result?.error) throw new Error(result.error.message || 'InterceptPilot MCP tool failed.')
|
|
651
|
+
return tool.formatResult(result)
|
|
652
|
+
} catch (error) {
|
|
653
|
+
return createSafeToolError(error)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function createMcpServer({ bridge }) {
|
|
659
|
+
const server = new McpServer({
|
|
660
|
+
name: 'interceptpilot-mcp',
|
|
661
|
+
version: '0.1.0'
|
|
662
|
+
})
|
|
663
|
+
const handleToolCall = createToolCallHandler({ bridge })
|
|
664
|
+
|
|
665
|
+
for (const tool of getMcpToolDefinitions()) {
|
|
666
|
+
server.registerTool(tool.name, {
|
|
667
|
+
title: tool.name,
|
|
668
|
+
description: tool.description,
|
|
669
|
+
inputSchema: MCP_TOOLS[tool.name].registeredInputSchema
|
|
670
|
+
}, args => handleToolCall(tool.name, args))
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return server
|
|
674
|
+
}
|