Skip to content

Commit 13f013d

Browse files
committed
Implement the renderer-side of MCP
1 parent b709807 commit 13f013d

11 files changed

Lines changed: 1034 additions & 0 deletions

File tree

src/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import { App } from './components/app';
4343
import { StyleProvider } from './components/style-provider';
4444
import { ErrorBoundary } from './components/error-boundary';
4545

46+
import { initializeUiApi } from './services/ui-api/api-interface';
47+
4648
console.log(`Initialising UI (version ${UI_VERSION})`);
4749

4850
const APP_ELEMENT_SELECTOR = '#app';
@@ -100,6 +102,10 @@ const appStartupPromise = Promise.all(
100102
);
101103
initMetrics();
102104

105+
// The UI exposes an API itself, allowing external components (the desktop shell) to access
106+
// UI state for the MCP server etc.
107+
initializeUiApi({ accountStore, eventsStore });
108+
103109
// Once the app is loaded, show the app
104110
appStartupPromise.then(() => {
105111
// We now know that the server is running - tell it to check for updates

src/services/desktop-api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { desktopVersion } from "./service-versions";
2+
import { OperationDefinition } from "./ui-api/api-types";
23

34
type DesktopInjectedKey =
45
| 'httpToolkitDesktopVersion'
@@ -54,6 +55,11 @@ interface DesktopApi {
5455
* File at all, it throws.
5556
*/
5657
getPathForFile?: (file: File) => string | null;
58+
59+
setApiOperations?: (operations: OperationDefinition[]) => void;
60+
onOperationRequest?: (
61+
handler: (operation: string, params: Record<string, unknown>) => Promise<unknown>
62+
) => void;
5763
}
5864

5965
interface NativeContextMenuDefinition {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { OperationRegistry } from './api-registry';
2+
import { registerAllOperations } from './operations';
3+
4+
import { DesktopApi } from '../desktop-api';
5+
import { AccountStore } from '../../model/account/account-store';
6+
import { EventsStore } from '../../model/events/events-store';
7+
8+
export function initializeUiApi(stores: {
9+
accountStore: AccountStore;
10+
eventsStore: EventsStore;
11+
}) {
12+
if (!DesktopApi.setApiOperations || !DesktopApi.onOperationRequest) {
13+
console.log("UI API not available");
14+
return;
15+
}
16+
17+
const { accountStore, eventsStore } = stores;
18+
19+
const registry = new OperationRegistry(
20+
() => accountStore.isPaidUser
21+
);
22+
23+
registerAllOperations(
24+
registry,
25+
() => eventsStore.events
26+
);
27+
28+
DesktopApi.setApiOperations(registry.getDefinitions());
29+
DesktopApi.onOperationRequest(async (operation, params) => {
30+
return await registry.execute(operation, params || {});
31+
});
32+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Operation, OperationResult } from './api-types';
2+
3+
const PRO_REQUIRED_ERROR: OperationResult = {
4+
success: false,
5+
error: {
6+
code: 'PRO_REQUIRED',
7+
message: 'This feature requires an HTTP Toolkit Pro subscription. ' +
8+
'Get Pro at https://httptoolkit.com/pricing/ to unlock ' +
9+
'programmatic access to HTTP Toolkit via MCP, CLI, and more.'
10+
}
11+
};
12+
13+
export class OperationRegistry {
14+
15+
private operations = new Map<string, Operation>();
16+
17+
constructor(private isPaidUser: () => boolean) {}
18+
19+
register(op: Operation): void {
20+
this.operations.set(op.definition.name, op);
21+
}
22+
23+
getDefinitions() {
24+
return Array.from(this.operations.values()).map(op => op.definition);
25+
}
26+
27+
async execute(name: string, params: Record<string, unknown>): Promise<OperationResult> {
28+
if (!this.isPaidUser()) {
29+
return PRO_REQUIRED_ERROR;
30+
}
31+
32+
const op = this.operations.get(name);
33+
if (!op) {
34+
return {
35+
success: false,
36+
error: {
37+
code: 'UNKNOWN_OPERATION',
38+
message: `Unknown operation: ${name}`
39+
}
40+
};
41+
}
42+
43+
try {
44+
return await op.handler(params);
45+
} catch (e: any) {
46+
return {
47+
success: false,
48+
error: {
49+
code: 'EXECUTION_ERROR',
50+
message: e.message || String(e)
51+
}
52+
};
53+
}
54+
}
55+
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { JSONSchema7 } from 'json-schema';
2+
3+
export interface OperationDefinition {
4+
name: string;
5+
description: string;
6+
inputSchema: JSONSchema7;
7+
outputSchema: JSONSchema7;
8+
category: string;
9+
}
10+
11+
export interface OperationResult<T = unknown> {
12+
success: boolean;
13+
data?: T;
14+
error?: { code: string; message: string };
15+
}
16+
17+
export type OperationHandler = (
18+
params: Record<string, unknown>
19+
) => Promise<OperationResult>;
20+
21+
export interface Operation {
22+
definition: OperationDefinition;
23+
handler: OperationHandler;
24+
}

0 commit comments

Comments
 (0)