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,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
+ }