Skip to content

Commit 7b99e85

Browse files
authored
feat: Add Actor runs API, dataset API, KV-store (#122)
* fix: Use a new API to get Actor default build` * feat: add get-actor and get-user-runs-list * feat: add dataset, Actor runs, fix tests * fix: add key-value stores
1 parent 3bb7a58 commit 7b99e85

27 files changed

+1088
-620
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/actor/server.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@ import { parseInputParamsFromUrl, processParamsGetTools } from '../mcp/utils.js'
1616
import { getHelpMessage, HEADER_READINESS_PROBE, Routes } from './const.js';
1717
import { getActorRunData } from './utils.js';
1818

19+
/**
20+
* Helper function to load tools and actors based on input parameters
21+
* @param mcpServer The MCP server instance
22+
* @param url The request URL to parse parameters from
23+
* @param apifyToken The Apify token for authentication
24+
*/
25+
async function loadToolsAndActors(mcpServer: ActorsMcpServer, url: string, apifyToken: string): Promise<void> {
26+
const input = parseInputParamsFromUrl(url);
27+
if (input.actors || input.enableAddingActors) {
28+
await mcpServer.loadToolsFromUrl(url, apifyToken);
29+
}
30+
if (!input.actors) {
31+
await mcpServer.loadDefaultActors(apifyToken);
32+
}
33+
}
34+
1935
export function createExpressApp(
2036
host: string,
2137
mcpServer: ActorsMcpServer,
@@ -49,7 +65,7 @@ export function createExpressApp(
4965
// TODO: I think we should remove this logic, root should return only help message
5066
const tools = await processParamsGetTools(req.url, process.env.APIFY_TOKEN as string);
5167
if (tools) {
52-
mcpServer.updateTools(tools);
68+
mcpServer.upsertTools(tools);
5369
}
5470
res.setHeader('Content-Type', 'text/event-stream');
5571
res.setHeader('Cache-Control', 'no-cache');
@@ -67,14 +83,7 @@ export function createExpressApp(
6783
app.get(Routes.SSE, async (req: Request, res: Response) => {
6884
try {
6985
log.info(`Received GET message at: ${Routes.SSE}`);
70-
const input = parseInputParamsFromUrl(req.url);
71-
if (input.actors || input.enableAddingActors) {
72-
await mcpServer.loadToolsFromUrl(req.url, process.env.APIFY_TOKEN as string);
73-
}
74-
// Load default tools if no actors are specified
75-
if (!input.actors) {
76-
await mcpServer.loadDefaultTools(process.env.APIFY_TOKEN as string);
77-
}
86+
await loadToolsAndActors(mcpServer, req.url, process.env.APIFY_TOKEN as string);
7887
transportSSE = new SSEServerTransport(Routes.MESSAGE, res);
7988
await mcpServer.connect(transportSSE);
8089
} catch (error) {
@@ -124,16 +133,7 @@ export function createExpressApp(
124133
enableJsonResponse: true, // Enable JSON response mode
125134
});
126135
// Load MCP server tools
127-
// TODO using query parameters in POST request is not standard
128-
const input = parseInputParamsFromUrl(req.url);
129-
if (input.actors || input.enableAddingActors) {
130-
await mcpServer.loadToolsFromUrl(req.url, process.env.APIFY_TOKEN as string);
131-
}
132-
// Load default tools if no actors are specified
133-
if (!input.actors) {
134-
await mcpServer.loadDefaultTools(process.env.APIFY_TOKEN as string);
135-
}
136-
136+
await loadToolsAndActors(mcpServer, req.url, process.env.APIFY_TOKEN as string);
137137
// Connect the transport to the MCP server BEFORE handling the request
138138
await mcpServer.connect(transport);
139139

src/const.ts

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,7 @@ export const ACTOR_README_MAX_LENGTH = 5_000;
33
export const ACTOR_ENUM_MAX_LENGTH = 200;
44
export const ACTOR_MAX_DESCRIPTION_LENGTH = 500;
55

6-
// Actor output const
7-
export const ACTOR_OUTPUT_MAX_CHARS_PER_ITEM = 5_000;
8-
export const ACTOR_OUTPUT_TRUNCATED_MESSAGE = `Output was truncated because it will not fit into context.`
9-
+ `There is no reason to call this tool again!`;
10-
11-
export const ACTOR_ADDITIONAL_INSTRUCTIONS = 'Never call/execute tool/Actor unless confirmed by the user. '
12-
+ 'Always limit the number of results in the call arguments.';
6+
export const ACTOR_RUN_DATASET_OUTPUT_MAX_ITEMS = 5;
137

148
// Actor run const
159
export const ACTOR_MAX_MEMORY_MBYTES = 4_096; // If the Actor requires 8GB of memory, free users can't run actors-mcp-server and requested Actor
@@ -22,29 +16,43 @@ export const SERVER_VERSION = '1.0.0';
2216
export const USER_AGENT_ORIGIN = 'Origin/mcp-server';
2317

2418
export enum HelperTools {
25-
SEARCH_ACTORS = 'search-actors',
26-
ADD_ACTOR = 'add-actor',
27-
REMOVE_ACTOR = 'remove-actor',
28-
GET_ACTOR_DETAILS = 'get-actor-details',
29-
HELP_TOOL = 'help-tool',
19+
ACTOR_ADD = 'add-actor',
20+
ACTOR_GET = 'get-actor',
21+
ACTOR_GET_DETAILS = 'get-actor-details',
22+
ACTOR_REMOVE = 'remove-actor',
23+
ACTOR_RUNS_ABORT = 'abort-actor-run',
24+
ACTOR_RUNS_GET = 'get-actor-run',
25+
ACTOR_RUNS_LOG = 'get-actor-log',
26+
ACTOR_RUN_LIST_GET = 'get-actor-run-list',
27+
DATASET_GET = 'get-dataset',
28+
DATASET_LIST_GET = 'get-dataset-list',
29+
DATASET_GET_ITEMS = 'get-dataset-items',
30+
KEY_VALUE_STORE_LIST_GET = 'get-key-value-store-list',
31+
KEY_VALUE_STORE_GET = 'get-key-value-store',
32+
KEY_VALUE_STORE_KEYS_GET = 'get-key-value-store-keys',
33+
KEY_VALUE_STORE_RECORD_GET = 'get-key-value-store-record',
34+
APIFY_MCP_HELP_TOOL = 'apify-actor-help-tool',
35+
STORE_SEARCH = 'search-actors',
3036
}
3137

3238
export const defaults = {
3339
actors: [
3440
'apify/rag-web-browser',
3541
],
36-
helperTools: [
37-
HelperTools.SEARCH_ACTORS,
38-
HelperTools.GET_ACTOR_DETAILS,
39-
HelperTools.HELP_TOOL,
40-
],
41-
actorAddingTools: [
42-
HelperTools.ADD_ACTOR,
43-
HelperTools.REMOVE_ACTOR,
44-
],
4542
};
4643

47-
export const APIFY_USERNAME = 'apify';
44+
// Actor output const
45+
export const ACTOR_OUTPUT_MAX_CHARS_PER_ITEM = 5_000;
46+
export const ACTOR_OUTPUT_TRUNCATED_MESSAGE = `Output was truncated because it will not fit into context.`
47+
+ `There is no reason to call this tool again! You can use ${HelperTools.DATASET_GET_ITEMS} tool to get more items from the dataset.`;
48+
49+
export const ACTOR_ADDITIONAL_INSTRUCTIONS = `Never call/execute tool/Actor unless confirmed by the user.
50+
Workflow: When an Actor runs, it processes data and stores results in Apify storage,
51+
Datasets (for structured/tabular data) and Key-Value Store (for various data types like JSON, images, HTML).
52+
Each Actor run produces a dataset ID and key-value store ID for accessing the results.
53+
By default, the number of items returned from an Actor run is limited to ${ACTOR_RUN_DATASET_OUTPUT_MAX_ITEMS}.
54+
You can always use ${HelperTools.DATASET_GET_ITEMS} tool to get more items from the dataset.
55+
Actor run input is always stored in the key-value store, recordKey: INPUT.`;
4856

4957
export const TOOL_CACHE_MAX_SIZE = 500;
5058
export const TOOL_CACHE_TTL_SECS = 30 * 60;

src/examples/clientStreamableHttp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ async function callSearchTool(client: Client): Promise<void> {
6767
const searchRequest: CallToolRequest = {
6868
method: 'tools/call',
6969
params: {
70-
name: HelperTools.SEARCH_ACTORS,
70+
name: HelperTools.STORE_SEARCH,
7171
arguments: { search: 'rag web browser', limit: 1 },
7272
},
7373
};

src/main.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ if (STANDBY_MODE) {
4444
const { actors } = input;
4545
const actorsToLoad = Array.isArray(actors) ? actors : actors.split(',');
4646
const tools = await getActorsAsTools(actorsToLoad, process.env.APIFY_TOKEN as string);
47-
mcpServer.updateTools(tools);
47+
mcpServer.upsertTools(tools);
4848
}
4949
app.listen(PORT, () => {
5050
log.info(`The Actor web server is listening for user requests at ${HOST}`);
@@ -56,9 +56,9 @@ if (STANDBY_MODE) {
5656
await Actor.fail('If you need to debug a specific Actor, please provide the debugActor and debugActorInput fields in the input');
5757
}
5858
const options = { memory: input.maxActorMemoryBytes } as ActorCallOptions;
59-
const items = await callActorGetDataset(input.debugActor!, input.debugActorInput!, process.env.APIFY_TOKEN, options);
59+
const { datasetInfo, items } = await callActorGetDataset(input.debugActor!, input.debugActorInput!, process.env.APIFY_TOKEN, options);
6060

6161
await Actor.pushData(items);
62-
log.info(`Pushed ${items.length} items to the dataset`);
62+
log.info(`Pushed ${datasetInfo?.itemCount} items to the dataset`);
6363
await Actor.exit();
6464
}

src/mcp/proxy.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
22
import Ajv from 'ajv';
33

4-
import type { ActorMCPTool, ToolWrap } from '../types.js';
4+
import type { ActorMcpTool, ToolEntry } from '../types.js';
55
import { getMCPServerID, getProxyMCPServerToolName } from './utils.js';
66

77
export async function getMCPServerTools(
88
actorID: string,
99
client: Client,
1010
// Name of the MCP server
1111
serverUrl: string,
12-
): Promise<ToolWrap[]> {
12+
): Promise<ToolEntry[]> {
1313
const res = await client.listTools();
1414
const { tools } = res;
1515

1616
const ajv = new Ajv({ coerceTypes: 'array', strict: false });
1717

18-
const compiledTools: ToolWrap[] = [];
18+
const compiledTools: ToolEntry[] = [];
1919
for (const tool of tools) {
20-
const mcpTool: ActorMCPTool = {
21-
actorID,
20+
const mcpTool: ActorMcpTool = {
21+
actorId: actorID,
2222
serverId: getMCPServerID(serverUrl),
2323
serverUrl,
2424
originToolName: tool.name,
@@ -29,7 +29,7 @@ export async function getMCPServerTools(
2929
ajvValidate: ajv.compile(tool.inputSchema),
3030
};
3131

32-
const wrap: ToolWrap = {
32+
const wrap: ToolEntry = {
3333
type: 'actor-mcp',
3434
tool: mcpTool,
3535
};

0 commit comments

Comments
 (0)