Skip to content

Commit 0ade868

Browse files
authored
feat: add hot reload to widgets [internal] (#411)
1 parent 37d5b0e commit 0ade868

4 files changed

Lines changed: 72 additions & 12 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ We use **4 spaces** for indentation (configured in `.editorconfig`).
343343
### Development commands
344344

345345
- `npm run start` - Start standby server (uses `tsx` for direct TypeScript execution)
346-
- `npm run dev` - Alias for `npm run start`
346+
- `npm run dev` - Run standby server with hot-reload
347347
- `npm run build` - Build TypeScript and UI widgets
348348
- `npm run build:web` - Build UI widgets only
349349
- `npm run lint` - Run ESLint

DEVELOPMENT.md

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,32 +50,42 @@ npm install
5050

5151
### Working on the MCP Apps (ChatGPT Apps) UI widgets
5252

53-
The MCP server uses UI widgets from the `src/web/` directory that need to be built before running the server. To build everything, including the UI widgets, run:
53+
The MCP server uses UI widgets from the `src/web/` directory.
54+
5455
See the [OpenAI Apps SDK documentation](https://developers.openai.com/apps-sdk) for background on MCP Apps and widgets.
5556

57+
### Production build
58+
59+
If you need the compiled assets copied into the top-level `dist/web` for packaging or integration tests, build everything:
60+
5661
```bash
5762
npm run build
5863
```
5964

6065
This command builds the core project and the `src/web/` widgets, then copies the widgets into the `dist/` directory.
6166

62-
If you only want to work on the React UI widgets, all widget code lives in the self-contained `src/web/` React project. The widgets (MCP Apps) are rendered based on the structured output returned by MCP tools. If you need to add specific data to a widget, you will need to modify the corresponding MCP tool's output since widgets can only render data returned by the MCP tool call result.
67+
All widget code lives in the self-contained `src/web/` React project. The widgets (MCP Apps) are rendered based on the structured output returned by MCP tools. If you need to add specific data to a widget, modify the corresponding MCP tool's output, since widgets can only render data returned by the MCP tool call result.
6368

6469
> **Important (UI mode):** Widget rendering is enabled only when the server runs in UI mode. Use the `ui=openai` query parameter (e.g., `/mcp?ui=openai`) or set `UI_MODE=openai`. Currently, `openai` is the only supported `ui` value.
6570
66-
> **Important:** After changing widgets, you must rebuild the project with `npm run build` to refresh the React widgets in the `dist/` directory.
71+
### Hot-reload development
6772

68-
### Running the MCP server locally
69-
70-
Start the MCP server locally using:
73+
Run the orchestrator, which starts the web widgets builder in watch mode and the MCP server in standby mode:
7174

7275
```bash
73-
APIFY_TOKEN='your-apify-token' npm run start
76+
APIFY_TOKEN='your-apify-token' npm run dev
7477
```
7578

76-
This will spawn the MCP server at port `3001`.
77-
The HTTP server implementation used here is the standby Actor server in `src/actor/server.ts` (used by `src/main.ts` in STANDBY mode).
78-
The hosted production server behind [mcp.apify.com](https://mcp.apify.com) is located in the internal Apify repository.
79+
What happens:
80+
- The `src/web` project runs `npm run dev` and continuously writes compiled files to `src/web/dist`.
81+
- The MCP server reads widget assets directly from `src/web/dist` (compiled JS/HTML only; no TypeScript or JSX at runtime).
82+
- Editing files under `src/web/src/widgets/*.tsx` triggers a rebuild; the next widget render will use the updated code without restarting the server.
83+
84+
Notes:
85+
- Widget discovery happens when the server connects. Changing widget code is hot-reloaded; adding brand-new widget filenames typically requires reconnecting the MCP client (or restarting the server) to expose the new resource.
86+
- You can preview widgets quickly via the local esbuild dev server at `http://localhost:3000/index.html`.
87+
88+
The MCP server listens on port `3001`. The HTTP server implementation used here is the standby Actor server in `src/actor/server.ts` (used by `src/main.ts` in STANDBY mode). The hosted production server behind [mcp.apify.com](https://mcp.apify.com) is located in the internal Apify repository.
7989

8090
### Testing with MCPJam (optional)
8191

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
},
8282
"scripts": {
8383
"start": "npm run start:standby",
84-
"dev": "npm run start:standby",
84+
"dev": "node scripts/dev-standby.js",
8585
"start:standby": "APIFY_META_ORIGIN=\"STANDBY\" tsx src/main.ts",
8686
"build": "npm run build:core && npm run build:web",
8787
"build:core": "tsc -b src",

scripts/dev-standby.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env node
2+
/*
3+
Dev orchestrator: runs web widget builder in watch mode and the MCP server in standby dev mode.
4+
Ensures the server always reads compiled assets from src/web/dist while enabling hot-reload.
5+
*/
6+
7+
import { spawn } from 'node:child_process';
8+
import { dirname, resolve } from 'node:path';
9+
import { fileURLToPath } from 'node:url';
10+
11+
const filename = fileURLToPath(import.meta.url);
12+
const currentDir = dirname(filename);
13+
const repoRoot = resolve(currentDir, '..');
14+
15+
function run(cmd, args, opts = {}) {
16+
const child = spawn(cmd, args, {
17+
stdio: 'inherit',
18+
env: { ...process.env, ...opts.env },
19+
cwd: opts.cwd || repoRoot,
20+
shell: process.platform === 'win32',
21+
});
22+
child.on('exit', (code) => {
23+
// If one process exits, exit the orchestrator with the same code.
24+
process.exitCode = code ?? 1;
25+
});
26+
return child;
27+
}
28+
29+
// 1) Start web build in watch mode (produces src/web/dist)
30+
const webDir = resolve(repoRoot, 'src/web');
31+
const web = run('npm', ['run', 'dev', '--silent'], { cwd: webDir });
32+
33+
// 2) Start server in STANDBY dev mode (reads src/web/dist via resolveAvailableWidgets)
34+
const server = run('npm', ['run', 'start:standby'], { env: { APIFY_META_ORIGIN: 'STANDBY' } });
35+
36+
// Forward signals so both children terminate cleanly
37+
function shutdown() {
38+
try {
39+
web.kill('SIGINT');
40+
} catch {
41+
// ignore
42+
}
43+
try {
44+
server.kill('SIGINT');
45+
} catch {
46+
// ignore
47+
}
48+
}
49+
process.on('SIGINT', shutdown);
50+
process.on('SIGTERM', shutdown);

0 commit comments

Comments
 (0)