@@ -73,6 +73,8 @@ import { getToolStatusFromError } from '../utils/tool-status.js';
7373import { cloneToolEntry , getToolPublicFieldOnly } from '../utils/tools.js' ;
7474import { getUserIdFromTokenCached } from '../utils/userid-cache.js' ;
7575import { getPackageVersion } from '../utils/version.js' ;
76+ import type { AvailableWidget } from '../utils/widgets.js' ;
77+ import { resolveAvailableWidgets } from '../utils/widgets.js' ;
7678import { connectMCPClient } from './client.js' ;
7779import { EXTERNAL_TOOL_CALL_TIMEOUT_MSEC , LOG_LEVEL_MAP } from './const.js' ;
7880import { isTaskCancelled , processParamsGetTools } from './utils.js' ;
@@ -95,6 +97,9 @@ export class ActorsMcpServer {
9597 private telemetryEnabled : boolean | null = null ;
9698 private telemetryEnv : TelemetryEnv = DEFAULT_TELEMETRY_ENV ;
9799
100+ // List of widgets that are ready to be served
101+ private availableWidgets : Map < string , AvailableWidget > = new Map ( ) ;
102+
98103 constructor ( options : ActorsMcpServerOptions = { } ) {
99104 this . options = options ;
100105
@@ -446,51 +451,18 @@ export class ActorsMcpServer {
446451 }
447452
448453 if ( this . options . uiMode === 'openai' ) {
449- resources . push ( {
450- uri : 'ui://widget/search-actors.html' ,
451- name : 'search-actors-widget' ,
452- description : 'Interactive Actor search results widget' ,
453- mimeType : 'text/html+skybridge' ,
454- _meta : {
455- 'openai/outputTemplate' : 'ui://widget/search-actors.html' ,
456- 'openai/toolInvocation/invoking' : 'Searching Apify Store...' ,
457- 'openai/toolInvocation/invoked' : 'Found Actors matching your criteria' ,
458- 'openai/widgetAccessible' : true ,
459- 'openai/resultCanProduceWidget' : true ,
460- 'openai/widgetDomain' : 'https://apify.com' ,
461- 'openai/widgetCSP' : {
462- connect_domains : [
463- 'https://api.apify.com' ,
464- ] ,
465- resource_domains : [
466- 'https://mcp.apify.com' ,
467- 'https://images.apifyusercontent.com' ,
468- ] ,
469- } ,
470- } ,
471- } ) ;
472-
473- resources . push ( {
474- uri : 'ui://widget/actor-run.html' ,
475- name : 'actor-run-widget' ,
476- description : 'Interactive Actor run widget' ,
477- mimeType : 'text/html+skybridge' ,
478- _meta : {
479- 'openai/outputTemplate' : 'ui://widget/actor-run.html' ,
480- 'openai/widgetAccessible' : true ,
481- 'openai/resultCanProduceWidget' : true ,
482- 'openai/widgetDomain' : 'https://apify.com' ,
483- 'openai/widgetCSP' : {
484- connect_domains : [
485- 'https://api.apify.com' ,
486- ] ,
487- resource_domains : [
488- 'https://mcp.apify.com' ,
489- 'https://images.apifyusercontent.com' ,
490- ] ,
491- } ,
492- } ,
493- } ) ;
454+ // Only register widgets that are available
455+ for ( const widget of this . availableWidgets . values ( ) ) {
456+ if ( widget . exists ) {
457+ resources . push ( {
458+ uri : widget . uri ,
459+ name : widget . name ,
460+ description : widget . description ,
461+ mimeType : 'text/html+skybridge' ,
462+ _meta : widget . meta ,
463+ } ) ;
464+ }
465+ }
494466 }
495467
496468 return { resources } ;
@@ -509,45 +481,30 @@ export class ActorsMcpServer {
509481 }
510482
511483 if ( this . options . uiMode === 'openai' && uri . startsWith ( 'ui://widget/' ) ) {
512- try {
513- log . debug ( 'Reading widget files' , { uri } ) ;
514- const fs = await import ( 'node:fs' ) ;
515- const path = await import ( 'node:path' ) ;
516- const { fileURLToPath } = await import ( 'node:url' ) ;
517-
518- // Get the directory of this file
519- const filename = fileURLToPath ( import . meta. url ) ;
520- const dirName = path . dirname ( filename ) ;
521-
522- let widgetJsFilename = '' ;
523- let widgetTitle = '' ;
524-
525- if ( uri === 'ui://widget/search-actors.html' ) {
526- widgetJsFilename = 'search-actors-widget.js' ;
527- widgetTitle = 'Apify Actor Search' ;
528- } else if ( uri === 'ui://widget/actor-run.html' ) {
529- widgetJsFilename = 'actor-run-widget.js' ;
530- widgetTitle = 'Apify Actor Run' ;
531- } else {
532- return {
533- contents : [ {
534- uri, mimeType : 'text/plain' , text : `Widget resource ${ uri } not found` ,
535- } ] ,
536- } ;
537- }
484+ const widget = this . availableWidgets . get ( uri ) ;
538485
539- const widgetJsPath = path . resolve ( dirName , `../web/dist/${ widgetJsFilename } ` ) ;
486+ if ( ! widget || ! widget . exists ) {
487+ return {
488+ contents : [ {
489+ uri,
490+ mimeType : 'text/plain' ,
491+ text : `Widget ${ uri } is not available. ${ ! widget ? 'Not found in registry.' : `File not found at ${ widget . jsPath } ` } ` ,
492+ } ] ,
493+ } ;
494+ }
540495
541- log . debug ( 'Reading widget file' , { widgetJsPath } ) ;
496+ try {
497+ log . debug ( 'Reading widget file' , { uri, jsPath : widget . jsPath } ) ;
498+ const fs = await import ( 'node:fs' ) ;
542499
543- const widgetJs = fs . readFileSync ( widgetJsPath , 'utf-8' ) ;
500+ const widgetJs = fs . readFileSync ( widget . jsPath , 'utf-8' ) ;
544501
545502 const widgetHtml = `<!DOCTYPE html>
546503<html lang="en">
547504 <head>
548505 <meta charset="UTF-8" />
549506 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
550- <title>${ widgetTitle } </title>
507+ <title>${ widget . title } </title>
551508 </head>
552509 <body>
553510 <div id="root"></div>
@@ -561,22 +518,7 @@ export class ActorsMcpServer {
561518 mimeType : 'text/html+skybridge' ,
562519 text : widgetHtml ,
563520 html : widgetHtml ,
564- _meta : {
565- 'openai/widgetPrefersBorder' : true ,
566- 'openai/outputTemplate' : uri ,
567- 'openai/widgetAccessible' : true ,
568- 'openai/resultCanProduceWidget' : true ,
569- 'openai/widgetDomain' : 'https://apify.com' ,
570- 'openai/widgetCSP' : {
571- connect_domains : [
572- 'https://api.apify.com' ,
573- ] ,
574- resource_domains : [
575- 'https://mcp.apify.com' ,
576- 'https://images.apifyusercontent.com' ,
577- ] ,
578- } ,
579- } ,
521+ _meta : widget . meta ,
580522 } ] ,
581523 } ;
582524 } catch ( error ) {
@@ -1281,7 +1223,55 @@ Please verify the tool name and ensure the tool is properly registered.`;
12811223 return { telemetryData, userId } ;
12821224 }
12831225
1226+ /**
1227+ * Resolves widgets and determines which ones are ready to be served.
1228+ */
1229+ private async resolveWidgets ( ) : Promise < void > {
1230+ if ( this . options . uiMode !== 'openai' ) {
1231+ return ;
1232+ }
1233+
1234+ try {
1235+ const { fileURLToPath } = await import ( 'node:url' ) ;
1236+ const path = await import ( 'node:path' ) ;
1237+
1238+ const filename = fileURLToPath ( import . meta. url ) ;
1239+ const dirName = path . dirname ( filename ) ;
1240+
1241+ const resolved = await resolveAvailableWidgets ( dirName ) ;
1242+ this . availableWidgets = resolved ;
1243+
1244+ const readyWidgets : string [ ] = [ ] ;
1245+ const missingWidgets : string [ ] = [ ] ;
1246+
1247+ for ( const [ uri , widget ] of resolved . entries ( ) ) {
1248+ if ( widget . exists ) {
1249+ readyWidgets . push ( widget . name ) ;
1250+ } else {
1251+ missingWidgets . push ( widget . name ) ;
1252+ log . softFail ( `Widget file not found: ${ widget . jsPath } (widget: ${ uri } )` ) ;
1253+ }
1254+ }
1255+
1256+ if ( readyWidgets . length > 0 ) {
1257+ log . debug ( 'Ready widgets' , { widgets : readyWidgets } ) ;
1258+ }
1259+
1260+ if ( missingWidgets . length > 0 ) {
1261+ log . softFail ( 'Some widgets are not ready' , {
1262+ widgets : missingWidgets ,
1263+ note : 'These widgets will not be available. Ensure web/dist files are built and included in deployment.' ,
1264+ } ) ;
1265+ }
1266+ } catch ( error ) {
1267+ const errorMessage = error instanceof Error ? error . message : String ( error ) ;
1268+ log . softFail ( `Failed to resolve widgets: ${ errorMessage } ` ) ;
1269+ // Continue without widgets
1270+ }
1271+ }
1272+
12841273 async connect ( transport : Transport ) : Promise < void > {
1274+ await this . resolveWidgets ( ) ;
12851275 await this . server . connect ( transport ) ;
12861276 }
12871277
0 commit comments