@@ -34,6 +34,14 @@ import {
3434 WorkflowNotFoundError ,
3535 WorkflowStoredDataError ,
3636} from "./workflow-errors.js" ;
37+ import {
38+ loadWorkflowUiCallDetail ,
39+ loadWorkflowUiProject ,
40+ loadWorkflowUiRun ,
41+ } from "./workflow-ui.js" ;
42+
43+ const WORKSPACE_APP_URI = "ui://devspace/workspace-app.html" ;
44+ const WORKFLOW_UI_WAIT_MAX_MS = 30_000 ;
3745
3846const WORKFLOW_API_CHEATSHEET = `
3947Workflow scripts (JS only):
@@ -84,7 +92,7 @@ export function registerWorkflowTools(
8492 . describe ( `Ms to wait for early completion (default 2000, max ${ WORKFLOW_MCP_YIELD_MS } ).` ) ,
8593 } ,
8694 annotations : { readOnlyHint : false } ,
87- _meta : { } ,
95+ _meta : workflowWidgetMeta ( config ) ,
8896 } ,
8997 async ( { workspaceId, script, name, scriptPath, resumeFromRunId, args, yieldTimeMs } ) => {
9098 const workspace = workspaces . getWorkspace ( workspaceId ) ;
@@ -200,7 +208,7 @@ export function registerWorkflowTools(
200208
201209 const yieldMs = yieldTimeMs ?? 2_000 ;
202210 const page = await yieldEvents ( store , run . id , 0 , yieldMs ) ;
203- return toolResult ( page ) ;
211+ return toolResult ( page , "run_workflow" ) ;
204212 } catch ( error ) {
205213 if ( isWorkflowOperationError ( error ) ) return workflowToolError ( error ) ;
206214 throw error ;
@@ -228,14 +236,14 @@ export function registerWorkflowTools(
228236 . describe ( `Long-poll ms (default 0, max ${ WORKFLOW_MCP_YIELD_MS } ).` ) ,
229237 } ,
230238 annotations : { readOnlyHint : true } ,
231- _meta : { } ,
239+ _meta : workflowWidgetMeta ( config ) ,
232240 } ,
233241 async ( { runId, sinceSeq, yieldTimeMs } ) => {
234242 const store = createWorkflowStore ( config ) ;
235243 try {
236244 if ( ! store . getRun ( runId ) ) throw new WorkflowNotFoundError ( runId ) ;
237245 const page = await yieldEvents ( store , runId , sinceSeq ?? 0 , yieldTimeMs ?? 0 ) ;
238- return toolResult ( page ) ;
246+ return toolResult ( page , "workflow_status" ) ;
239247 } catch ( error ) {
240248 if ( isWorkflowOperationError ( error ) ) return workflowToolError ( error ) ;
241249 throw error ;
@@ -284,7 +292,104 @@ export function registerWorkflowTools(
284292 } ,
285293 ) ;
286294
295+ if ( config . widgets !== "off" ) {
296+ registerWorkflowUiTools ( server , config , workspaces ) ;
287297 }
298+ }
299+
300+ function registerWorkflowUiTools (
301+ server : McpServer ,
302+ config : ServerConfig ,
303+ workspaces : WorkspaceRegistry ,
304+ ) : void {
305+ registerAppTool (
306+ server ,
307+ "workspace_workflow_activity" ,
308+ {
309+ title : "Workspace workflow activity" ,
310+ description : "Read-only workflow activity for the DevSpace app." ,
311+ inputSchema : {
312+ workspaceId : z . string ( ) ,
313+ knownVersion : z . string ( ) . optional ( ) ,
314+ waitMs : z . number ( ) . int ( ) . min ( 0 ) . max ( WORKFLOW_UI_WAIT_MAX_MS ) . optional ( ) ,
315+ } ,
316+ annotations : { readOnlyHint : true } ,
317+ _meta : appOnlyToolMeta ( ) ,
318+ } ,
319+ async ( { workspaceId, knownVersion, waitMs } ) => {
320+ const workspace = workspaces . getWorkspace ( workspaceId ) ;
321+ const store = createWorkflowStore ( config ) ;
322+ try {
323+ const project = await waitForProjectSnapshot (
324+ store ,
325+ workspace . root ,
326+ knownVersion ,
327+ waitMs ?? 0 ,
328+ ) ;
329+ return appToolResult ( { workspaceId, project } ) ;
330+ } finally {
331+ store . close ( ) ;
332+ }
333+ } ,
334+ ) ;
335+
336+ registerAppTool (
337+ server ,
338+ "workflow_ui_snapshot" ,
339+ {
340+ title : "Workflow UI snapshot" ,
341+ description : "Read-only workflow snapshot for the DevSpace app." ,
342+ inputSchema : {
343+ runId : z . string ( ) ,
344+ knownVersion : z . string ( ) . optional ( ) ,
345+ waitMs : z . number ( ) . int ( ) . min ( 0 ) . max ( WORKFLOW_UI_WAIT_MAX_MS ) . optional ( ) ,
346+ } ,
347+ annotations : { readOnlyHint : true } ,
348+ _meta : appOnlyToolMeta ( ) ,
349+ } ,
350+ async ( { runId, knownVersion, waitMs } ) => {
351+ const store = createWorkflowStore ( config ) ;
352+ try {
353+ const run = await waitForRunSnapshot ( store , runId , knownVersion , waitMs ?? 0 ) ;
354+ if ( ! run ) throw new WorkflowNotFoundError ( runId ) ;
355+ return appToolResult ( { run } ) ;
356+ } finally {
357+ store . close ( ) ;
358+ }
359+ } ,
360+ ) ;
361+
362+ registerAppTool (
363+ server ,
364+ "workflow_ui_call_detail" ,
365+ {
366+ title : "Workflow call detail" ,
367+ description : "Read-only workflow call detail for the DevSpace app." ,
368+ inputSchema : {
369+ runId : z . string ( ) ,
370+ callIndex : z . number ( ) . int ( ) . min ( 0 ) ,
371+ } ,
372+ annotations : { readOnlyHint : true } ,
373+ _meta : appOnlyToolMeta ( ) ,
374+ } ,
375+ async ( { runId, callIndex } ) => {
376+ const store = createWorkflowStore ( config ) ;
377+ try {
378+ if ( ! store . getRun ( runId ) ) throw new WorkflowNotFoundError ( runId ) ;
379+ const call = loadWorkflowUiCallDetail ( store , runId , callIndex ) ;
380+ if ( ! call ) {
381+ throw new InvalidWorkflowInputError ( {
382+ code : "invalid_argument" ,
383+ message : `Unknown workflow agent call: ${ runId } #${ callIndex } ` ,
384+ } ) ;
385+ }
386+ return appToolResult ( { call } ) ;
387+ } finally {
388+ store . close ( ) ;
389+ }
390+ } ,
391+ ) ;
392+ }
288393
289394async function yieldEvents (
290395 store : ReturnType < typeof createWorkflowStore > ,
@@ -329,7 +434,7 @@ function toolResult(page: {
329434 nextSeq : number ;
330435 terminal : boolean ;
331436 callSummary : ReturnType < typeof summarizeCalls > ;
332- } ) {
437+ } , tool : "run_workflow" | "workflow_status" ) {
333438 const payload = {
334439 runId : page . run . id ,
335440 status : page . run . status ,
@@ -354,9 +459,74 @@ function toolResult(page: {
354459 return {
355460 content : [ { type : "text" as const , text : JSON . stringify ( payload , null , 2 ) } ] ,
356461 structuredContent : payload ,
462+ _meta : {
463+ tool,
464+ card : {
465+ runId : page . run . id ,
466+ status : page . run . status ,
467+ name : page . run . name ,
468+ } ,
469+ } ,
470+ } ;
471+ }
472+
473+ function workflowWidgetMeta ( config : ServerConfig ) {
474+ if ( config . widgets !== "full" ) return { } ;
475+ return {
476+ ui : {
477+ resourceUri : WORKSPACE_APP_URI ,
478+ visibility : [ "model" ] as const ,
479+ } ,
480+ } ;
481+ }
482+
483+ function appOnlyToolMeta ( ) {
484+ return {
485+ ui : {
486+ visibility : [ "app" ] as const ,
487+ } ,
357488 } ;
358489}
359490
491+ function appToolResult ( structuredContent : Record < string , unknown > ) {
492+ return {
493+ content : [ { type : "text" as const , text : JSON . stringify ( structuredContent ) } ] ,
494+ structuredContent,
495+ } ;
496+ }
497+
498+ async function waitForProjectSnapshot (
499+ store : ReturnType < typeof createWorkflowStore > ,
500+ workspaceRoot : string ,
501+ knownVersion : string | undefined ,
502+ waitMs : number ,
503+ ) {
504+ const deadline = Date . now ( ) + Math . min ( waitMs , WORKFLOW_UI_WAIT_MAX_MS ) ;
505+ for ( ; ; ) {
506+ const project = loadWorkflowUiProject ( store , workspaceRoot ) ;
507+ if ( ! knownVersion || project . version !== knownVersion || Date . now ( ) >= deadline ) {
508+ return project ;
509+ }
510+ await sleep ( 250 ) ;
511+ }
512+ }
513+
514+ async function waitForRunSnapshot (
515+ store : ReturnType < typeof createWorkflowStore > ,
516+ runId : string ,
517+ knownVersion : string | undefined ,
518+ waitMs : number ,
519+ ) {
520+ const deadline = Date . now ( ) + Math . min ( waitMs , WORKFLOW_UI_WAIT_MAX_MS ) ;
521+ for ( ; ; ) {
522+ const run = loadWorkflowUiRun ( store , runId ) ;
523+ if ( ! run || ! knownVersion || run . version !== knownVersion || Date . now ( ) >= deadline ) {
524+ return run ;
525+ }
526+ await sleep ( 250 ) ;
527+ }
528+ }
529+
360530function summarizeCalls ( calls : ReturnType < ReturnType < typeof createWorkflowStore > [ "listAgentCalls" ] > ) {
361531 return {
362532 reused : calls . filter ( ( call ) => call . fromCache ) . length ,
0 commit comments