@@ -17,6 +17,7 @@ import {
1717 SERVER_NAME ,
1818 SERVER_VERSION ,
1919} from '../const.js' ;
20+ import { internalToolsMap } from '../toolmap.js' ;
2021import { helpTool } from '../tools/helpers.js' ;
2122import {
2223 actorDefinitionTool ,
@@ -37,13 +38,16 @@ type ActorsMcpServerOptions = {
3738 enableDefaultActors ?: boolean ;
3839} ;
3940
41+ type ToolsChangedHandler = ( toolNames : string [ ] ) => void ;
42+
4043/**
4144 * Create Apify MCP server
4245 */
4346export class ActorsMcpServer {
4447 public readonly server : Server ;
4548 public readonly tools : Map < string , ToolWrap > ;
4649 private readonly options : ActorsMcpServerOptions ;
50+ private toolsChangedHandler : ToolsChangedHandler | undefined ;
4751
4852 constructor ( options : ActorsMcpServerOptions = { } , setupSIGINTHandler = true ) {
4953 this . options = {
@@ -80,13 +84,128 @@ export class ActorsMcpServer {
8084 } ) ;
8185 }
8286
87+ /**
88+ * Returns a list of Actor IDs that are registered as MCP servers.
89+ * @returns {string[] } - An array of Actor MCP server Actor IDs (e.g., 'apify/actors-mcp-server').
90+ */
91+ public getToolMCPServerActors ( ) : string [ ] {
92+ const mcpServerActors : Set < string > = new Set ( ) ;
93+ for ( const tool of this . tools . values ( ) ) {
94+ if ( tool . type === 'actor-mcp' ) {
95+ mcpServerActors . add ( ( tool . tool as ActorMCPTool ) . actorID ) ;
96+ }
97+ }
98+
99+ return Array . from ( mcpServerActors ) ;
100+ }
101+
102+ /**
103+ * Register handler to get notified when tools change.
104+ * The handler receives an array of tool names that the server has after the change.
105+ * This is primarily used to store the tools in shared state (e.g., Redis) for recovery
106+ * when the server loses local state.
107+ * @throws {Error } - If a handler is already registered.
108+ * @param handler - The handler function to be called when tools change.
109+ */
110+ public registerToolsChangedHandler ( handler : ( toolNames : string [ ] ) => void ) {
111+ if ( this . toolsChangedHandler ) {
112+ throw new Error ( 'Tools changed handler is already registered.' ) ;
113+ }
114+ this . toolsChangedHandler = handler ;
115+ }
116+
117+ /**
118+ * Unregister the handler for tools changed event.
119+ * @throws {Error } - If no handler is currently registered.
120+ */
121+ public unregisterToolsChangedHandler ( ) {
122+ if ( ! this . toolsChangedHandler ) {
123+ throw new Error ( 'Tools changed handler is not registered.' ) ;
124+ }
125+ this . toolsChangedHandler = undefined ;
126+ }
127+
128+ /**
129+ * Loads missing tools from a provided list of tool names.
130+ * Skips tools that are already loaded and loads only the missing ones.
131+ * @param tools - Array of tool names to ensure are loaded
132+ * @param apifyToken - Apify API token for authentication
133+ */
134+ public async loadToolsFromToolsList ( tools : string [ ] , apifyToken : string ) {
135+ const loadedTools = this . getLoadedActorToolsList ( ) ;
136+ const actorsToLoad : string [ ] = [ ] ;
137+
138+ for ( const tool of tools ) {
139+ // Skip if the tool is already loaded
140+ if ( loadedTools . includes ( tool ) ) {
141+ continue ;
142+ }
143+
144+ // Load internal tool
145+ if ( internalToolsMap . has ( tool ) ) {
146+ const toolWrap = internalToolsMap . get ( tool ) as ToolWrap ;
147+ this . tools . set ( tool , toolWrap ) ;
148+ log . info ( `Added internal tool: ${ tool } ` ) ;
149+ // Handler Actor tool
150+ } else {
151+ actorsToLoad . push ( tool ) ;
152+ }
153+ }
154+
155+ if ( actorsToLoad . length > 0 ) {
156+ const actorTools = await getActorsAsTools ( actorsToLoad , apifyToken ) ;
157+ if ( actorTools . length > 0 ) {
158+ this . updateTools ( actorTools ) ;
159+ }
160+ log . info ( `Loaded tools: ${ actorTools . map ( ( t ) => t . tool . name ) . join ( ', ' ) } ` ) ;
161+ }
162+ }
163+
164+ /**
165+ * Returns the list of all currently loaded Actor tool IDs.
166+ * @returns {string[] } - Array of loaded Actor tool IDs (e.g., 'apify/rag-web-browser')
167+ */
168+ public getLoadedActorToolsList ( ) : string [ ] {
169+ // Get the list of tool names
170+ const tools : string [ ] = [ ] ;
171+ for ( const tool of this . tools . values ( ) ) {
172+ if ( tool . type === 'actor' ) {
173+ tools . push ( ( tool . tool as ActorTool ) . actorFullName ) ;
174+ // Skip Actorized MCP servers since there may be multiple tools from the same Actor MCP server
175+ // so we skip and then get unique list of Actor MCP servers separately
176+ } else if ( tool . type === 'actor-mcp' ) {
177+ continue ;
178+ } else {
179+ tools . push ( tool . tool . name ) ;
180+ }
181+ }
182+ // Add unique list Actorized MCP servers original Actor IDs - for example: apify/actors-mcp-server
183+ tools . push ( ...this . getToolMCPServerActors ( ) ) ;
184+
185+ return tools ;
186+ }
187+
188+ private notifyToolsChangedHandler ( ) {
189+ // If no handler is registered, do nothing
190+ if ( ! this . toolsChangedHandler ) return ;
191+
192+ // Get the list of tool names
193+ const tools : string [ ] = this . getLoadedActorToolsList ( ) ;
194+
195+ this . toolsChangedHandler ( tools ) ;
196+ }
197+
83198 /**
84199 * Resets the server to the default state.
85200 * This method clears all tools and loads the default tools.
86201 * Used primarily for testing purposes.
87202 */
88203 public async reset ( ) : Promise < void > {
89204 this . tools . clear ( ) ;
205+ // Unregister the tools changed handler
206+ if ( this . toolsChangedHandler ) {
207+ this . unregisterToolsChangedHandler ( ) ;
208+ }
90209 this . updateTools ( [ searchTool , actorDefinitionTool , helpTool ] ) ;
91210 if ( this . options . enableAddingActors ) {
92211 this . loadToolsToAddActors ( ) ;
@@ -128,30 +247,57 @@ export class ActorsMcpServer {
128247 const tools = await processParamsGetTools ( url , apifyToken ) ;
129248 if ( tools . length > 0 ) {
130249 log . info ( 'Loading tools from query parameters...' ) ;
131- this . updateTools ( tools ) ;
250+ this . updateTools ( tools , false ) ;
132251 }
133252 }
134253
135254 /**
136255 * Add Actors to server dynamically
137256 */
138257 public loadToolsToAddActors ( ) {
139- this . updateTools ( [ addTool , removeTool ] ) ;
258+ this . updateTools ( [ addTool , removeTool ] , false ) ;
140259 }
141260
142261 /**
143262 * Upsert new tools.
144- * @param tools - Array of tool wrappers.
145- * @returns Array of tool wrappers.
263+ * @param tools - Array of tool wrappers to add or update
264+ * @param shouldNotifyToolsChangedHandler - Whether to notify the tools changed handler
265+ * @returns Array of added/updated tool wrappers
146266 */
147- public updateTools ( tools : ToolWrap [ ] ) {
267+ public updateTools ( tools : ToolWrap [ ] , shouldNotifyToolsChangedHandler = false ) {
148268 for ( const wrap of tools ) {
149269 this . tools . set ( wrap . tool . name , wrap ) ;
150270 log . info ( `Added/updated tool: ${ wrap . tool . name } ` ) ;
151271 }
272+ if ( shouldNotifyToolsChangedHandler ) this . notifyToolsChangedHandler ( ) ;
152273 return tools ;
153274 }
154275
276+ /**
277+ * Delete tools by name.
278+ * Notifies the tools changed handler if any tools were deleted.
279+ * @param toolNames - Array of tool names to delete
280+ * @returns Array of tool names that were successfully deleted
281+ */
282+ public deleteTools ( toolNames : string [ ] ) : string [ ] {
283+ const notFoundTools : string [ ] = [ ] ;
284+ // Delete the tools
285+ for ( const toolName of toolNames ) {
286+ if ( this . tools . has ( toolName ) ) {
287+ this . tools . delete ( toolName ) ;
288+ log . info ( `Deleted tool: ${ toolName } ` ) ;
289+ } else {
290+ notFoundTools . push ( toolName ) ;
291+ }
292+ }
293+
294+ if ( toolNames . length > notFoundTools . length ) {
295+ this . notifyToolsChangedHandler ( ) ;
296+ }
297+ // Return the list of tools that were removed
298+ return toolNames . filter ( ( toolName ) => ! notFoundTools . includes ( toolName ) ) ;
299+ }
300+
155301 /**
156302 * Returns an array of tool names.
157303 * @returns {string[] } - An array of tool names.
0 commit comments