Skip to content

Commit 4b12210

Browse files
committed
Add event clearing & non-interactive proxy/interceptor to MCP API
1 parent 13f013d commit 4b12210

7 files changed

Lines changed: 262 additions & 4 deletions

File tree

src/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ initMetrics();
104104

105105
// The UI exposes an API itself, allowing external components (the desktop shell) to access
106106
// UI state for the MCP server etc.
107-
initializeUiApi({ accountStore, eventsStore });
107+
initializeUiApi({ accountStore, eventsStore, proxyStore, interceptorStore });
108108

109109
// Once the app is loaded, show the app
110110
appStartupPromise.then(() => {

src/services/ui-api/api-interface.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,29 @@ import { registerAllOperations } from './operations';
44
import { DesktopApi } from '../desktop-api';
55
import { AccountStore } from '../../model/account/account-store';
66
import { EventsStore } from '../../model/events/events-store';
7+
import { ProxyStore } from '../../model/proxy-store';
8+
import { InterceptorStore } from '../../model/interception/interceptor-store';
79

810
export function initializeUiApi(stores: {
911
accountStore: AccountStore;
1012
eventsStore: EventsStore;
13+
proxyStore: ProxyStore;
14+
interceptorStore: InterceptorStore;
1115
}) {
1216
if (!DesktopApi.setApiOperations || !DesktopApi.onOperationRequest) {
1317
console.log("UI API not available");
1418
return;
1519
}
1620

17-
const { accountStore, eventsStore } = stores;
21+
const { accountStore, eventsStore, proxyStore, interceptorStore } = stores;
1822

1923
const registry = new OperationRegistry(
2024
() => accountStore.isPaidUser
2125
);
2226

2327
registerAllOperations(
2428
registry,
29+
{ eventsStore, proxyStore, interceptorStore },
2530
() => eventsStore.events
2631
);
2732

src/services/ui-api/operations/event-operations.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@ import { serializeExchangeSummary, serializeExchangeOutline, serializeBody } fro
44
import { HttpExchange, CollectedEvent } from '../../../types';
55
import { matchFilters } from '../../../model/filters/filter-matching';
66
import { SelectableSearchFilterClasses } from '../../../model/filters/search-filters';
7+
import { EventsStore } from '../../../model/events/events-store';
78

89
export function registerEventOperations(
910
registry: OperationRegistry,
11+
eventsStore: EventsStore,
1012
getEvents: () => ReadonlyArray<CollectedEvent>
1113
): void {
1214
registry.register(eventsListOperation(getEvents));
1315
registry.register(eventsGetOutlineOperation(getEvents));
1416
registry.register(eventsGetRequestBodyOperation(getEvents));
1517
registry.register(eventsGetResponseBodyOperation(getEvents));
18+
registry.register(eventsClearOperation(eventsStore));
1619
}
1720

1821
function findHttpExchange(
@@ -251,3 +254,33 @@ function eventsGetResponseBodyOperation(
251254
}
252255
};
253256
}
257+
258+
function eventsClearOperation(eventsStore: EventsStore): Operation {
259+
return {
260+
definition: {
261+
name: 'events.clear',
262+
description: 'Clear all captured events. By default, pinned events are preserved.',
263+
category: 'events',
264+
inputSchema: {
265+
type: 'object',
266+
properties: {
267+
clearPinned: {
268+
type: 'boolean',
269+
description: 'Whether to also clear pinned events (default false)'
270+
}
271+
}
272+
},
273+
outputSchema: {
274+
type: 'object',
275+
properties: {
276+
success: { type: 'boolean' }
277+
}
278+
}
279+
},
280+
handler: async (params) => {
281+
const clearPinned = (params.clearPinned as boolean) || false;
282+
eventsStore.clearInterceptedData(clearPinned);
283+
return { success: true, data: {} };
284+
}
285+
};
286+
}
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
import { OperationRegistry } from '../api-registry';
22
import { registerEventOperations } from './event-operations';
3+
import { registerProxyOperations } from './proxy-operations';
4+
import { registerInterceptorOperations } from './interceptor-operations';
35
import { CollectedEvent } from '../../../types';
6+
import { EventsStore } from '../../../model/events/events-store';
7+
import { ProxyStore } from '../../../model/proxy-store';
8+
import { InterceptorStore } from '../../../model/interception/interceptor-store';
49

510
export function registerAllOperations(
611
registry: OperationRegistry,
12+
stores: {
13+
eventsStore: EventsStore;
14+
proxyStore: ProxyStore;
15+
interceptorStore: InterceptorStore;
16+
},
717
getEvents: () => ReadonlyArray<CollectedEvent>
818
): void {
9-
registerEventOperations(registry, getEvents);
19+
registerEventOperations(registry, stores.eventsStore, getEvents);
20+
registerProxyOperations(registry, stores.proxyStore);
21+
registerInterceptorOperations(registry, stores.interceptorStore);
1022
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { Operation } from '../api-types';
2+
import { InterceptorStore } from '../../../model/interception/interceptor-store';
3+
import { Interceptor } from '../../../model/interception/interceptors';
4+
5+
function isNonInteractive(interceptor: Interceptor): boolean {
6+
return !interceptor.uiConfig && !interceptor.customActivation && !interceptor.clientOnly;
7+
}
8+
9+
export function registerInterceptorOperations(
10+
registry: { register(op: Operation): void },
11+
interceptorStore: InterceptorStore
12+
): void {
13+
registry.register(interceptorsListOperation(interceptorStore));
14+
registry.register(interceptorsActivateOperation(interceptorStore));
15+
}
16+
17+
function serializeInterceptor(interceptor: Interceptor) {
18+
return {
19+
id: interceptor.id,
20+
name: interceptor.name,
21+
description: interceptor.description.join(' '),
22+
isActive: interceptor.isActive,
23+
isActivable: interceptor.isActivable,
24+
isSupported: interceptor.isSupported,
25+
inProgress: !!interceptor.inProgress
26+
};
27+
}
28+
29+
function interceptorsListOperation(interceptorStore: InterceptorStore): Operation {
30+
return {
31+
definition: {
32+
name: 'interceptors.list',
33+
description: 'List available interceptors and their current status. ' +
34+
'Shows which interceptors are supported, activable, and currently active.',
35+
category: 'interceptors',
36+
inputSchema: {
37+
type: 'object',
38+
properties: {}
39+
},
40+
outputSchema: {
41+
type: 'object',
42+
properties: {
43+
interceptors: {
44+
type: 'array',
45+
items: {
46+
type: 'object',
47+
properties: {
48+
id: { type: 'string' },
49+
name: { type: 'string' },
50+
description: { type: 'string' },
51+
isActive: { type: 'boolean' },
52+
isActivable: { type: 'boolean' },
53+
isSupported: { type: 'boolean' },
54+
inProgress: { type: 'boolean' }
55+
}
56+
}
57+
}
58+
}
59+
}
60+
},
61+
handler: async () => {
62+
const interceptors = Object.values(interceptorStore.interceptors)
63+
.filter((i): i is Interceptor => !!i)
64+
.map(serializeInterceptor);
65+
66+
return {
67+
success: true,
68+
data: { interceptors }
69+
};
70+
}
71+
};
72+
}
73+
74+
function interceptorsActivateOperation(interceptorStore: InterceptorStore): Operation {
75+
return {
76+
definition: {
77+
name: 'interceptors.activate',
78+
description: 'Activate a non-interactive interceptor. Only simple interceptors that ' +
79+
'require no UI interaction or confirmation are supported (e.g. fresh browser windows, ' +
80+
'fresh-terminal, system-proxy). Interactive interceptors like docker-attach, ' +
81+
'android-adb, electron etc. require the full UI.',
82+
category: 'interceptors',
83+
inputSchema: {
84+
type: 'object',
85+
properties: {
86+
id: {
87+
type: 'string',
88+
description: 'The interceptor ID to activate (e.g. "fresh-chrome", "fresh-terminal", "system-proxy")'
89+
}
90+
},
91+
required: ['id']
92+
},
93+
outputSchema: {
94+
type: 'object',
95+
properties: {
96+
success: { type: 'boolean' }
97+
}
98+
}
99+
},
100+
handler: async (params) => {
101+
const id = params.id as string;
102+
103+
if (!id) {
104+
return {
105+
success: false,
106+
error: { code: 'INVALID_PARAMS', message: 'Missing required parameter: id' }
107+
};
108+
}
109+
110+
const interceptor = interceptorStore.interceptors[id];
111+
if (!interceptor) {
112+
return {
113+
success: false,
114+
error: { code: 'NOT_FOUND', message: `Unknown interceptor: ${id}` }
115+
};
116+
}
117+
118+
if (!isNonInteractive(interceptor)) {
119+
return {
120+
success: false,
121+
error: {
122+
code: 'INTERACTIVE_REQUIRED',
123+
message: `Interceptor '${id}' (${interceptor.name}) requires interactive setup ` +
124+
`through the UI. Only non-interactive interceptors can be activated via this API.`
125+
}
126+
};
127+
}
128+
129+
if (!interceptor.isActivable) {
130+
return {
131+
success: false,
132+
error: {
133+
code: 'NOT_ACTIVABLE',
134+
message: `Interceptor '${id}' (${interceptor.name}) is not currently activable. ` +
135+
`It may not be installed or supported on this system.`
136+
}
137+
};
138+
}
139+
140+
try {
141+
await interceptorStore.activateInterceptor(
142+
id,
143+
interceptor.activationOptions
144+
);
145+
return { success: true, data: {} };
146+
} catch (e) {
147+
return {
148+
success: false,
149+
error: {
150+
code: 'ACTIVATION_FAILED',
151+
message: `Failed to activate interceptor '${id}': ${
152+
e instanceof Error ? e.message : String(e)
153+
}`
154+
}
155+
};
156+
}
157+
}
158+
};
159+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Operation } from '../api-types';
2+
import { ProxyStore } from '../../../model/proxy-store';
3+
4+
export function registerProxyOperations(
5+
registry: { register(op: Operation): void },
6+
proxyStore: ProxyStore
7+
): void {
8+
registry.register(proxyGetConfigOperation(proxyStore));
9+
}
10+
11+
function proxyGetConfigOperation(proxyStore: ProxyStore): Operation {
12+
return {
13+
definition: {
14+
name: 'proxy.get-config',
15+
description: 'Get the current proxy configuration: port, certificate path, ' +
16+
'certificate fingerprint, and external network addresses.',
17+
category: 'proxy',
18+
inputSchema: {
19+
type: 'object',
20+
properties: {}
21+
},
22+
outputSchema: {
23+
type: 'object',
24+
properties: {
25+
httpProxyPort: { type: 'number', description: 'The HTTP proxy port' },
26+
certPath: { type: 'string', description: 'Path to the CA certificate file' },
27+
certFingerprint: { type: 'string', description: 'SHA256 fingerprint of the CA certificate' },
28+
externalNetworkAddresses: {
29+
type: 'array',
30+
items: { type: 'string' },
31+
description: 'External IPv4 addresses of this machine'
32+
}
33+
}
34+
}
35+
},
36+
handler: async () => {
37+
return {
38+
success: true,
39+
data: {
40+
httpProxyPort: proxyStore.httpProxyPort,
41+
certPath: proxyStore.certPath,
42+
certFingerprint: proxyStore.certFingerprint,
43+
externalNetworkAddresses: proxyStore.externalNetworkAddresses
44+
}
45+
};
46+
}
47+
};
48+
}

test/unit/services/api/event-operations.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ describe('Event operations', () => {
1313
beforeEach(() => {
1414
events = [];
1515
registry = new OperationRegistry(() => true);
16-
registerEventOperations(registry, () => events);
16+
const mockEventsStore = { clearInterceptedData: () => {} } as any;
17+
registerEventOperations(registry, mockEventsStore, () => events);
1718
});
1819

1920
describe('events.list', () => {

0 commit comments

Comments
 (0)