@@ -312,14 +312,57 @@ async def list_tools() -> list[Tool]:
312312 ),
313313 Tool (
314314 name = "run_script" ,
315- description = "Run a script by task_id without LLM (deterministic replay). Writes session to DB." ,
315+ description = "Run a script (start) or resume a paused session (deterministic replay). Writes session to DB." ,
316316 inputSchema = {
317317 "type" : "object" ,
318318 "properties" : {
319- "task_id" : {"type" : "string" , "description" : "Task ID of the script" },
320- "vars" : {"type" : "object" , "description" : "Optional map of {{varname}} replacements" }
319+ "task_id" : {"type" : "string" , "description" : "Start mode: task_id of the script (mutually exclusive with session_id)" },
320+ "vars" : {"type" : "object" , "description" : "Start mode: optional map of {{varname}} replacements" },
321+ "session_id" : {"type" : "string" , "description" : "Resume mode: session_id to resume (mutually exclusive with task_id)" },
322+ "resolution" : {"type" : "object" , "description" : "Resume mode: resolution object {type, note?, patch?}" }
323+ }
324+ }
325+ ),
326+ Tool (
327+ name = "dfu_save" ,
328+ description = "Save a DFU (Dynamic Fuzzy Unit) to the MCP database. Overwrites if dfu_id exists." ,
329+ inputSchema = {
330+ "type" : "object" ,
331+ "properties" : {
332+ "dfu_id" : {"type" : "string" , "description" : "DFU ID (key)" },
333+ "name" : {"type" : "string" , "description" : "DFU name" },
334+ "description" : {"type" : "string" , "description" : "Optional description" , "default" : "" },
335+ "triggers" : {"type" : "array" , "description" : "Trigger rules (declarative JSON), exact match only" },
336+ "prompt" : {"type" : "string" , "description" : "Prompt shown to orchestrator" , "default" : "" },
337+ "allowed_resolutions" : {"type" : "array" , "description" : "Allowed resolution types" , "items" : {"type" : "string" }}
321338 },
322- "required" : ["task_id" ]
339+ "required" : ["dfu_id" , "name" , "triggers" ]
340+ }
341+ ),
342+ Tool (
343+ name = "dfu_list" ,
344+ description = "List DFUs from the MCP database." ,
345+ inputSchema = {
346+ "type" : "object" ,
347+ "properties" : {"limit" : {"type" : "integer" , "default" : 100 }}
348+ }
349+ ),
350+ Tool (
351+ name = "dfu_load" ,
352+ description = "Load a DFU by dfu_id from the MCP database." ,
353+ inputSchema = {
354+ "type" : "object" ,
355+ "properties" : {"dfu_id" : {"type" : "string" }},
356+ "required" : ["dfu_id" ]
357+ }
358+ ),
359+ Tool (
360+ name = "dfu_delete" ,
361+ description = "Delete a DFU by dfu_id from the MCP database." ,
362+ inputSchema = {
363+ "type" : "object" ,
364+ "properties" : {"dfu_id" : {"type" : "string" }},
365+ "required" : ["dfu_id" ]
323366 }
324367 ),
325368 Tool (
@@ -683,16 +726,71 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
683726
684727 elif name == "run_script" :
685728 task_id = arguments .get ("task_id" )
686- vars_map = arguments .get ("vars" ) or {}
687- if not task_id :
688- return [TextContent (type = "text" , text = _error_response ("task_id is required" , code = "INVALID_PARAMS" , retryable = False ))]
689- script = get_storage ().script_load (task_id )
690- if script is None :
691- return [TextContent (type = "text" , text = _error_response (f"No script for task_id: { task_id } " , code = "SCRIPT_NOT_FOUND" , retryable = False ))]
692- engine = ScriptEngine (vars_map = vars_map )
693- result = await engine .run_script (script , controller , get_storage ())
729+ session_id = arguments .get ("session_id" )
730+ if bool (task_id ) == bool (session_id ):
731+ return [
732+ TextContent (
733+ type = "text" ,
734+ text = _error_response (
735+ "Provide exactly one of task_id or session_id" ,
736+ code = "INVALID_PARAMS" ,
737+ retryable = False ,
738+ ),
739+ )
740+ ]
741+ storage = get_storage ()
742+ if task_id :
743+ vars_map = arguments .get ("vars" ) or {}
744+ script = storage .script_load (task_id )
745+ if script is None :
746+ return [TextContent (type = "text" , text = _error_response (f"No script for task_id: { task_id } " , code = "SCRIPT_NOT_FOUND" , retryable = False ))]
747+ engine = ScriptEngine (vars_map = vars_map )
748+ result = await engine .run_script_start (script , controller , storage )
749+ return [TextContent (type = "text" , text = json .dumps (result , indent = 2 , ensure_ascii = False ))]
750+ resolution = arguments .get ("resolution" )
751+ if resolution is None :
752+ return [TextContent (type = "text" , text = _error_response ("resolution is required for resume" , code = "INVALID_PARAMS" , retryable = False ))]
753+ engine = ScriptEngine (vars_map = {})
754+ result = await engine .run_script_resume (session_id , resolution , controller , storage )
694755 return [TextContent (type = "text" , text = json .dumps (result , indent = 2 , ensure_ascii = False ))]
695756
757+ elif name == "dfu_save" :
758+ dfu_id = arguments .get ("dfu_id" )
759+ name_ = arguments .get ("name" )
760+ triggers = arguments .get ("triggers" )
761+ if not dfu_id or not name_ or triggers is None :
762+ return [TextContent (type = "text" , text = _error_response ("dfu_id, name, triggers are required" , code = "INVALID_PARAMS" , retryable = False ))]
763+ get_storage ().dfu_save (
764+ dfu_id ,
765+ name = name_ ,
766+ description = arguments .get ("description" , "" ) or "" ,
767+ triggers = triggers ,
768+ prompt = arguments .get ("prompt" , "" ) or "" ,
769+ allowed_resolutions = arguments .get ("allowed_resolutions" ) or [],
770+ )
771+ return [TextContent (type = "text" , text = json .dumps ({"success" : True , "dfu_id" : dfu_id }, indent = 2 , ensure_ascii = False ))]
772+
773+ elif name == "dfu_list" :
774+ limit = arguments .get ("limit" , 100 )
775+ items = get_storage ().dfu_list (limit = limit )
776+ return [TextContent (type = "text" , text = json .dumps ({"dfus" : items }, indent = 2 , ensure_ascii = False ))]
777+
778+ elif name == "dfu_load" :
779+ dfu_id = arguments .get ("dfu_id" )
780+ if not dfu_id :
781+ return [TextContent (type = "text" , text = _error_response ("dfu_id is required" , code = "INVALID_PARAMS" , retryable = False ))]
782+ dfu = get_storage ().dfu_load (dfu_id )
783+ if dfu is None :
784+ return [TextContent (type = "text" , text = _error_response (f"No dfu for dfu_id: { dfu_id } " , code = "DFU_NOT_FOUND" , retryable = False ))]
785+ return [TextContent (type = "text" , text = json .dumps ({"success" : True , "dfu" : dfu }, indent = 2 , ensure_ascii = False ))]
786+
787+ elif name == "dfu_delete" :
788+ dfu_id = arguments .get ("dfu_id" )
789+ if not dfu_id :
790+ return [TextContent (type = "text" , text = _error_response ("dfu_id is required" , code = "INVALID_PARAMS" , retryable = False ))]
791+ ok = get_storage ().dfu_delete (dfu_id )
792+ return [TextContent (type = "text" , text = json .dumps ({"success" : True , "deleted" : ok }, indent = 2 , ensure_ascii = False ))]
793+
696794 elif name == "session_list" :
697795 limit = arguments .get ("limit" , 100 )
698796 items = get_storage ().session_list (limit = limit )
0 commit comments