Skip to content

Commit bb8946c

Browse files
committed
cache normal Actor tools
1 parent b14ea2b commit bb8946c

File tree

6 files changed

+48
-24
lines changed

6 files changed

+48
-24
lines changed

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"model context protocol"
3131
],
3232
"dependencies": {
33+
"@apify/datastructures": "^2.0.3",
3334
"@apify/log": "^2.5.16",
3435
"@modelcontextprotocol/sdk": "^1.10.1",
3536
"ajv": "^8.17.1",

src/const.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,6 @@ export const defaults = {
4545
};
4646

4747
export const APIFY_USERNAME = 'apify';
48+
49+
export const TOOL_CACHE_MAX_SIZE = 500;
50+
export const TOOL_CACHE_TTL_SECS = 30 * 60;

src/mcp/actors.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ActorDefinition } from 'apify-client';
22

3-
import { ApifyClient, getApifyAPIBaseUrl } from '../apify-client.js';
3+
import { ApifyClient } from '../apify-client.js';
44

55
export async function isActorMCPServer(actorID: string, apifyToken: string): Promise<boolean> {
66
const mcpPath = await getActorsMCPServerPath(actorID, apifyToken);
@@ -56,26 +56,12 @@ export async function getActorStandbyURL(actorID: string, apifyToken: string, st
5656
return `https://${actorRealID}.${standbyBaseUrl}`;
5757
}
5858

59+
// TODO: optimize this to only use /builds/default endpoint with Actor name replacing / with ~
5960
export async function getActorDefinition(actorID: string, apifyToken: string): Promise<ActorDefinition> {
6061
const apifyClient = new ApifyClient({ token: apifyToken });
6162
const actor = apifyClient.actor(actorID);
62-
const info = await actor.get();
63-
if (!info) {
64-
throw new Error(`Actor ${actorID} not found`);
65-
}
66-
67-
const actorObjID = info.id;
68-
const res = await fetch(`${getApifyAPIBaseUrl()}/v2/acts/${actorObjID}/builds/default`, {
69-
headers: {
70-
// This is done so tests can pass with public Actors without token
71-
...(apifyToken ? { Authorization: `Bearer ${apifyToken}` } : {}),
72-
},
73-
});
74-
if (!res.ok) {
75-
throw new Error(`Failed to fetch default build for actor ${actorID}: ${res.statusText}`);
76-
}
77-
const json = await res.json() as any; // eslint-disable-line @typescript-eslint/no-explicit-any
78-
const buildInfo = json.data;
63+
const defaultBuildClient = await actor.defaultBuild();
64+
const buildInfo = await defaultBuildClient.get();
7965
if (!buildInfo) {
8066
throw new Error(`Default build for Actor ${actorID} not found`);
8167
}

src/tools/actor.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
22
import { Ajv } from 'ajv';
33
import type { ActorCallOptions } from 'apify-client';
44

5+
import { LruCache } from '@apify/datastructures';
56
import log from '@apify/log';
67

78
import { ApifyClient } from '../apify-client.js';
8-
import { ACTOR_ADDITIONAL_INSTRUCTIONS, ACTOR_MAX_MEMORY_MBYTES } from '../const.js';
9+
import { ACTOR_ADDITIONAL_INSTRUCTIONS, ACTOR_MAX_MEMORY_MBYTES, TOOL_CACHE_MAX_SIZE, TOOL_CACHE_TTL_SECS } from '../const.js';
910
import { getActorsMCPServerURL, isActorMCPServer } from '../mcp/actors.js';
1011
import { createMCPClient } from '../mcp/client.js';
1112
import { getMCPServerTools } from '../mcp/proxy.js';
12-
import type { ToolWrap } from '../types.js';
13+
import type { ToolCacheEntry, ToolWrap } from '../types.js';
1314
import { getActorDefinition } from './build.js';
1415
import {
1516
actorNameToToolName,
@@ -58,6 +59,10 @@ export async function callActorGetDataset(
5859
}
5960
}
6061

62+
// Cache for normal Actor tools
63+
const normalActorToolsCache = new LruCache<ToolCacheEntry>({
64+
maxLength: TOOL_CACHE_MAX_SIZE,
65+
});
6166
/**
6267
* This function is used to fetch normal non-MCP server Actors as a tool.
6368
*
@@ -83,13 +88,31 @@ export async function getNormalActorsAsTools(
8388
actors: string[],
8489
apifyToken: string,
8590
): Promise<ToolWrap[]> {
91+
const tools: ToolWrap[] = [];
92+
const actorsLoadedFromCache: string[] = [];
93+
for (const actorID of actors) {
94+
const cacheEntry = normalActorToolsCache.get(actorID);
95+
if (cacheEntry && cacheEntry.expiresAt > Date.now()) {
96+
tools.push(cacheEntry.tool);
97+
actorsLoadedFromCache.push(actorID);
98+
}
99+
}
100+
const actorsToLoad = actors.filter((actorID) => !actorsLoadedFromCache.includes(actorID));
101+
if (actorsToLoad.length === 0) {
102+
return tools;
103+
}
104+
86105
const ajv = new Ajv({ coerceTypes: 'array', strict: false });
87106
const getActorDefinitionWithToken = async (actorId: string) => {
88107
return await getActorDefinition(actorId, apifyToken);
89108
};
90-
const results = await Promise.all(actors.map(getActorDefinitionWithToken));
91-
const tools: ToolWrap[] = [];
92-
for (const result of results) {
109+
const results = await Promise.all(actorsToLoad.map(getActorDefinitionWithToken));
110+
111+
// Zip the results with their corresponding actorIDs
112+
for (let i = 0; i < results.length; i++) {
113+
const result = results[i];
114+
const actorID = actorsToLoad[i];
115+
93116
if (result) {
94117
if (result.input && 'properties' in result.input && result.input) {
95118
result.input.properties = markInputPropertiesAsRequired(result.input);
@@ -100,7 +123,7 @@ export async function getNormalActorsAsTools(
100123
}
101124
try {
102125
const memoryMbytes = result.defaultRunOptions?.memoryMbytes || ACTOR_MAX_MEMORY_MBYTES;
103-
tools.push({
126+
const tool: ToolWrap = {
104127
type: 'actor',
105128
tool: {
106129
name: actorNameToToolName(result.actorFullName),
@@ -110,6 +133,11 @@ export async function getNormalActorsAsTools(
110133
ajvValidate: ajv.compile(result.input || {}),
111134
memoryMbytes: memoryMbytes > ACTOR_MAX_MEMORY_MBYTES ? ACTOR_MAX_MEMORY_MBYTES : memoryMbytes,
112135
},
136+
};
137+
tools.push(tool);
138+
normalActorToolsCache.add(actorID, {
139+
tool,
140+
expiresAt: Date.now() + TOOL_CACHE_TTL_SECS * 1000,
113141
});
114142
} catch (validationError) {
115143
log.error(`Failed to compile AJV schema for Actor: ${result.actorFullName}. Error: ${validationError}`);

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,8 @@ export type Input = {
184184
debugActor?: string;
185185
debugActorInput?: unknown;
186186
};
187+
188+
export interface ToolCacheEntry {
189+
expiresAt: number;
190+
tool: ToolWrap;
191+
}

0 commit comments

Comments
 (0)