Skip to content

Commit efc6ade

Browse files
authored
feat(gpt-apps): add tools and widget descriptors (#375)
2 parents c1c415f + 878ada4 commit efc6ade

26 files changed

Lines changed: 1081 additions & 99 deletions

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,39 @@ As above, this exposes only the specified Actor (`apify/my-actor`) as a tool. No
243243
>
244244
> **For production use and stable interfaces, always explicitly specify the `tools` parameter** to ensure your configuration remains consistent across updates.
245245
246+
### UI mode configuration
247+
248+
The `uiMode` parameter enables OpenAI-specific widget rendering in tool responses. When enabled, tools like `search-actors` return interactive widget responses optimized for OpenAI clients.
249+
250+
**Configuring the hosted server:**
251+
252+
Enable UI mode using the `ui` query parameter:
253+
254+
```
255+
https://mcp.apify.com?ui=openai
256+
```
257+
258+
You can combine it with other parameters:
259+
260+
```
261+
https://mcp.apify.com?tools=actors,docs&ui=openai
262+
```
263+
264+
**Configuring the CLI:**
265+
266+
The CLI can be configured using command-line flags. For example, to enable UI mode:
267+
268+
```bash
269+
npx @apify/actors-mcp-server --uiMode openai
270+
```
271+
272+
You can also set it via the `UI_MODE` environment variable:
273+
274+
```bash
275+
export UI_MODE=openai
276+
npx @apify/actors-mcp-server
277+
```
278+
246279
### Backward compatibility
247280

248281
The v2 configuration preserves backward compatibility with v1 usage. Notes:

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export default [
2727
'evals/*.ts', // Top-level evaluation scripts
2828
'evals/*.md', // Documentation files
2929
'evals/*.json', // Test case data files
30+
'src/web/**', // Web directory has its own TypeScript project and build system
3031
],
3132
},
3233
// Apply the shared Apify TypeScript ESLint configuration

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,14 @@
8787
"lint:fix": "eslint . --fix",
8888
"check": "npm run type-check && npm run lint:fix",
8989
"build": "tsc -b src",
90+
"postbuild": "mkdir -p dist/web && (cp -r src/web/dist dist/web/ 2>/dev/null || :)",
9091
"build:watch": "tsc -b src -w",
92+
"check:widgets": "tsx scripts/check-widgets.ts",
9193
"type-check": "tsc --noEmit",
9294
"test": "npm run test:unit",
9395
"test:unit": "vitest run tests/unit",
9496
"test:integration": "npm run build && vitest run tests/integration",
95-
"clean": "tsc -b src --clean",
97+
"clean": "tsc -b src --clean && rm -rf dist/web",
9698
"evals:create-dataset": "tsx evals/create-dataset.ts",
9799
"evals:run": "tsx evals/run-evaluation.ts",
98100
"evals:workflow": "tsx evals/workflows/run-workflow-evals.ts"

scripts/check-widgets.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/* eslint-disable no-console */
2+
/**
3+
* Build-time check to ensure widget files exist
4+
* This script validates that all registered widgets have corresponding files
5+
*/
6+
7+
import { existsSync } from 'node:fs';
8+
import { dirname, resolve } from 'node:path';
9+
import { fileURLToPath } from 'node:url';
10+
11+
import { WIDGET_REGISTRY } from '../src/utils/widgets.js';
12+
13+
const filename = fileURLToPath(import.meta.url);
14+
const projectRoot = resolve(dirname(filename), '..');
15+
const webDistPath = resolve(projectRoot, 'src/web/dist');
16+
17+
const missingWidgets: string[] = [];
18+
const existingWidgets: string[] = [];
19+
20+
for (const config of Object.values(WIDGET_REGISTRY)) {
21+
const jsPath = resolve(webDistPath, config.jsFilename);
22+
if (existsSync(jsPath)) {
23+
existingWidgets.push(config.name);
24+
} else {
25+
missingWidgets.push(`${config.name} (${config.jsFilename})`);
26+
}
27+
}
28+
29+
if (missingWidgets.length > 0) {
30+
console.error('Missing widget files:');
31+
for (const widget of missingWidgets) {
32+
console.error(` - ${widget}`);
33+
}
34+
console.error(`\nExpected location: ${webDistPath}`);
35+
console.error('\nPlease build the widgets before building the server.');
36+
process.exit(1);
37+
}
38+
39+
if (existingWidgets.length > 0) {
40+
console.log('All widget files found:');
41+
for (const widget of existingWidgets) {
42+
console.log(` - ${widget}`);
43+
}
44+
}
45+
46+
process.exit(0);

src/actor/server.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import log from '@apify/log';
1515

1616
import { ApifyClient } from '../apify-client.js';
1717
import { ActorsMcpServer } from '../mcp/server.js';
18-
import type { ApifyRequestParams } from '../types.js';
18+
import type { ApifyRequestParams, UiMode } from '../types.js';
1919
import { parseBooleanFromString } from '../utils/generic.js';
2020
import { getHelpMessage, HEADER_READINESS_PROBE, Routes, TransportType } from './const.js';
2121
import { getActorRunData } from './utils.js';
@@ -90,13 +90,17 @@ export function createExpressApp(
9090
?? parseBooleanFromString(process.env.TELEMETRY_ENABLED)
9191
?? true;
9292

93+
const uiModeParam = urlParams.get('ui') as UiMode | undefined;
94+
const uiMode = uiModeParam ?? process.env.UI_MODE as UiMode | undefined;
95+
9396
const mcpServer = new ActorsMcpServer({
9497
taskStore,
9598
setupSigintHandler: false,
9699
transportType: 'sse',
97100
telemetry: {
98101
enabled: telemetryEnabled,
99102
},
103+
uiMode,
100104
});
101105
const transport = new SSEServerTransport(Routes.MESSAGE, res);
102106

@@ -214,6 +218,9 @@ export function createExpressApp(
214218
?? parseBooleanFromString(process.env.TELEMETRY_ENABLED)
215219
?? true;
216220

221+
const uiModeParam = urlParams.get('ui') as UiMode | undefined;
222+
const uiMode = uiModeParam ?? process.env.UI_MODE as UiMode | undefined;
223+
217224
const mcpServer = new ActorsMcpServer({
218225
taskStore,
219226
setupSigintHandler: false,
@@ -222,6 +229,7 @@ export function createExpressApp(
222229
telemetry: {
223230
enabled: telemetryEnabled,
224231
},
232+
uiMode,
225233
});
226234

227235
// Load MCP server tools

src/const.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,11 @@ These tools are called **Actors**. They enable you to extract structured data fr
215215
216216
### Tool dependencies
217217
- \`${HelperTools.ACTOR_CALL}\`:
218-
- First call with \`step="info"\` or use \`${HelperTools.ACTOR_GET_DETAILS}\` to obtain the Actors schema.
218+
- First call with \`step="info"\` or use \`${HelperTools.ACTOR_GET_DETAILS}\` to obtain the Actor's schema.
219219
- Then call with \`step="call"\` to execute the Actor.
220+
- When \`step="call"\`, supports async execution via the \`async\` parameter:
221+
- When \`async: false\` or not provided (default when UI mode is disabled): Waits for completion and returns results immediately.
222+
- When \`async: true\` (default when UI mode is enabled): Starts the run and returns immediately with runId. Use \`${HelperTools.ACTOR_RUNS_GET}\` to check status and retrieve results.
220223
221224
### Tool disambiguation
222225
- **${HelperTools.ACTOR_OUTPUT_GET} vs ${HelperTools.DATASET_GET_ITEMS}:**
@@ -227,4 +230,6 @@ These tools are called **Actors**. They enable you to extract structured data fr
227230
\`${HelperTools.STORE_SEARCH}\` finds robust and reliable Actors for specific websites; ${RAG_WEB_BROWSER} is a general and versatile web scraping tool.
228231
- **Dedicated Actor tools (e.g. ${RAG_WEB_BROWSER}) vs ${HelperTools.ACTOR_CALL}:**
229232
Prefer dedicated tools when available; use \`${HelperTools.ACTOR_CALL}\` only when no specialized tool exists in Apify store.
233+
- **Async parameter for ${HelperTools.ACTOR_CALL} (when step="call"):**
234+
\`${HelperTools.ACTOR_CALL}\` supports async execution via the \`async\` boolean parameter when \`step="call"\`. When \`async: false\` or not provided, waits for completion and returns results (default when UI mode is disabled). When \`async: true\`, starts the run and returns immediately with runId (default when UI mode is enabled). Use \`async: true\` when the user wants background/progress/UI. After starting an async run and obtaining runId, do NOT start another run—only poll with \`${HelperTools.ACTOR_RUNS_GET}\` using that runId.
230235
`;

0 commit comments

Comments
 (0)