Skip to content

Commit aadbfa3

Browse files
committed
cache Actor definition and real Actor ID API calls
1 parent 7161444 commit aadbfa3

File tree

2 files changed

+48
-3
lines changed

2 files changed

+48
-3
lines changed

src/mcp/actors.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { log } from 'apify';
12
import type { ActorDefinition } from 'apify-client';
23

34
import { ApifyClient, getApifyAPIBaseUrl } from '../apify-client.js';
@@ -8,7 +9,7 @@ export async function isActorMCPServer(actorID: string, apifyToken: string): Pro
89
}
910

1011
export async function getActorsMCPServerPath(actorID: string, apifyToken: string): Promise<string | undefined> {
11-
const actorDefinition = await getActorDefinition(actorID, apifyToken);
12+
const actorDefinition = await getFullActorDefinition(actorID, apifyToken);
1213

1314
if ('webServerMcpPath' in actorDefinition && typeof actorDefinition.webServerMcpPath === 'string') {
1415
return actorDefinition.webServerMcpPath;
@@ -26,20 +27,32 @@ export async function getActorsMCPServerURL(actorID: string, apifyToken: string)
2627
return `${standbyUrl}${mcpPath}`;
2728
}
2829

30+
// ID does not change, so no TTL
31+
export const actorIDCache: Record<string, string> = {};
2932
/**
3033
* Gets Actor ID from the Actor object.
3134
*
3235
* @param actorID
3336
* @param apifyToken
3437
*/
3538
export async function getRealActorID(actorID: string, apifyToken: string): Promise<string> {
39+
if (actorIDCache[actorID]) {
40+
log.debug(`Actor ${actorID} ID cache hit`);
41+
return actorIDCache[actorID];
42+
}
43+
log.debug(`Actor ${actorID} ID cache miss`);
44+
3645
const apifyClient = new ApifyClient({ token: apifyToken });
3746

3847
const actor = apifyClient.actor(actorID);
3948
const info = await actor.get();
4049
if (!info) {
4150
throw new Error(`Actor ${actorID} not found`);
4251
}
52+
53+
if (!actorIDCache[actorID]) {
54+
actorIDCache[actorID] = info.id;
55+
}
4356
return info.id;
4457
}
4558

@@ -56,7 +69,29 @@ export async function getActorStandbyURL(actorID: string, apifyToken: string, st
5669
return `https://${actorRealID}.${standbyBaseUrl}`;
5770
}
5871

59-
export async function getActorDefinition(actorID: string, apifyToken: string): Promise<ActorDefinition> {
72+
export const actorDefinitionCache: Record<string, {
73+
timestamp: number;
74+
definition: ActorDefinition;
75+
}> = {};
76+
export const ACTOR_DEFINITION_CACHE_TTL_MS = 1000 * 60 * 60; // 1 hour
77+
/**
78+
* Gets full Actor definition from the Apify API.
79+
*/
80+
export async function getFullActorDefinition(actorID: string, apifyToken: string): Promise<ActorDefinition> {
81+
const cacheInTTL = Date.now() - (actorDefinitionCache[actorID]?.timestamp || 0) < ACTOR_DEFINITION_CACHE_TTL_MS;
82+
// Hit the cache
83+
if (actorDefinitionCache[actorID]
84+
&& cacheInTTL) {
85+
log.debug(`Actor ${actorID} definition cache hit`);
86+
return actorDefinitionCache[actorID].definition;
87+
}
88+
// Refresh the cache after TTL expired
89+
if (actorDefinitionCache[actorID] && !cacheInTTL) {
90+
log.debug(`Actor ${actorID} definition cache TTL expired, re-fetching`);
91+
} else {
92+
log.debug(`Actor ${actorID} definition cache miss`);
93+
}
94+
6095
const apifyClient = new ApifyClient({ token: apifyToken });
6196
const actor = apifyClient.actor(actorID);
6297
const info = await actor.get();
@@ -84,5 +119,14 @@ export async function getActorDefinition(actorID: string, apifyToken: string): P
84119
throw new Error(`Actor default build ${actorID} does not have Actor definition`);
85120
}
86121

122+
// If the Actor is public, we cache the definition
123+
// This code branch is executed only on cache miss, so we know the cache entry is empty
124+
if (info.isPublic) {
125+
actorDefinitionCache[actorID] = {
126+
timestamp: Date.now(),
127+
definition: actorDefinition,
128+
};
129+
}
130+
87131
return actorDefinition;
88132
}

src/tools/actor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ async function getMCPServersAsTools(
125125
): Promise<ToolWrap[]> {
126126
const actorsMCPServerTools: ToolWrap[] = [];
127127
for (const actorID of actors) {
128+
// getFullActorDefinition uses cache, so we can call it twice (this is the second time)
128129
const serverUrl = await getActorsMCPServerURL(actorID, apifyToken);
129130
log.info(`ActorID: ${actorID} MCP server URL: ${serverUrl}`);
130131

@@ -150,7 +151,7 @@ export async function getActorsAsTools(
150151
// Actorized MCP servers
151152
const actorsMCPServers: string[] = [];
152153
for (const actorID of actors) {
153-
// TODO: rework, we are fetching actor definition from API twice - in the getMCPServerTools
154+
// getFullActorDefinition uses cache, so we can call it twice (second time in the getMCPServerTools)
154155
if (await isActorMCPServer(actorID, apifyToken)) {
155156
actorsMCPServers.push(actorID);
156157
}

0 commit comments

Comments
 (0)