@@ -429,6 +429,140 @@ def get_user(api_key: str, ctx: MCPContext) -> dict[str, Any]:
429429 ops_manager = get_operations_manager (api_key , ctx )
430430 return ops_manager .get_user ()
431431
432+ # ==================== User-Defined Tools (UDT) ====================
433+
434+ @mcp .tool ()
435+ def list_user_tools (api_key : str , ctx : MCPContext , active : bool = True ) -> dict [str , Any ]:
436+ """List user-defined tools belonging to the current user.
437+
438+ Args:
439+ active: If True (default), only show active tools. Set False to
440+ include deactivated tools.
441+
442+ Returns:
443+ Dict with 'tools' (list of user tools, each with id, uuid, tool_id,
444+ name, and active status) and 'count'.
445+ """
446+ with _mcp_error_handler ("list_user_tools" ):
447+ ops_manager = get_operations_manager (api_key , ctx )
448+ return ops_manager .list_user_tools (active )
449+
450+ @mcp .tool ()
451+ def create_user_tool (representation : dict [str , Any ], api_key : str , ctx : MCPContext ) -> dict [str , Any ]:
452+ """Create a user-defined tool in Galaxy from a YAML tool definition.
453+
454+ User-defined tools are lightweight, containerized tools that can be
455+ created without admin privileges. They are stored in the database,
456+ scoped to the creating user, and can be embedded in workflows
457+ (importing the workflow automatically creates the tool for the
458+ importing user).
459+
460+ Requires the USER_TOOL_EXECUTE role on the calling user and
461+ enable_beta_tool_formats=true in the Galaxy config; both are enforced
462+ by the underlying manager and surface as permission/config errors here.
463+
464+ Args:
465+ representation: The tool definition as a dictionary matching the
466+ GalaxyUserTool schema. Required fields:
467+ - class: "GalaxyUserTool" (exactly this string)
468+ - id: tool identifier (lowercase, no spaces, 3-255 chars)
469+ - version: version string (e.g. "0.1.0")
470+ - name: display name shown in Galaxy tool menu
471+ - container: container image as a STRING (e.g. "python:3.12-slim"),
472+ NOT a dict -- this is a common mistake
473+ - shell_command: the command to execute, with $(inputs.name.path)
474+ for data inputs and $(inputs.name) for parameter inputs
475+ - inputs: list of input dicts, each with "name" and "type"
476+ (type can be: "data", "integer", "float", "text", "boolean")
477+ - outputs: list of output dicts, each with "name", "type": "data",
478+ "format" (e.g. "tabular", "vcf", "bed"), and "from_work_dir"
479+
480+ Returns:
481+ Dict with the created tool's id, uuid, tool_id, active status, and
482+ the validated representation.
483+
484+ Example:
485+ create_user_tool({
486+ "class": "GalaxyUserTool",
487+ "id": "my_filter",
488+ "version": "0.1.0",
489+ "name": "My Filter",
490+ "container": "python:3.12-slim",
491+ "shell_command": "python3 -c 'import sys; ...'",
492+ "inputs": [{"name": "input1", "type": "data", "format": "tabular"}],
493+ "outputs": [
494+ {"name": "output1", "type": "data",
495+ "format": "tabular", "from_work_dir": "out.tsv"}
496+ ]
497+ })
498+
499+ NEXT STEPS:
500+ - Run the tool: run_user_tool(history_id, tool_uuid, inputs)
501+ - List your tools: list_user_tools()
502+ - Delete a tool: delete_user_tool(uuid)
503+ """
504+ with _mcp_error_handler ("create_user_tool" ):
505+ ops_manager = get_operations_manager (api_key , ctx )
506+ return ops_manager .create_user_tool (representation )
507+
508+ @mcp .tool ()
509+ def delete_user_tool (uuid : str , api_key : str , ctx : MCPContext ) -> dict [str , Any ]:
510+ """Deactivate a user-defined tool. Deactivated tools are not loaded into the toolbox.
511+
512+ Existing job history that referenced the tool is preserved; only
513+ future runs are blocked.
514+
515+ Args:
516+ uuid: The UUID of the tool to deactivate. Get this from list_user_tools().
517+
518+ Returns:
519+ Dict confirming deactivation: {"uuid": ..., "deactivated": True}.
520+ """
521+ with _mcp_error_handler ("delete_user_tool" ):
522+ ops_manager = get_operations_manager (api_key , ctx )
523+ return ops_manager .delete_user_tool (uuid )
524+
525+ @mcp .tool ()
526+ def run_user_tool (
527+ history_id : str ,
528+ tool_uuid : str ,
529+ inputs : dict [str , Any ],
530+ api_key : str ,
531+ ctx : MCPContext ,
532+ ) -> dict [str , Any ]:
533+ """Run a user-defined tool by UUID, producing outputs in the given history.
534+
535+ Resolution happens through the tool service's standard run path,
536+ which accepts tool_uuid in the payload and dispatches via the
537+ toolbox's unprivileged-tool resolver -- so this is functionally a
538+ UUID-keyed counterpart to run_tool().
539+
540+ Args:
541+ history_id: Galaxy history ID where outputs will be placed.
542+ tool_uuid: The UUID of the user-defined tool (from create_user_tool
543+ or list_user_tools).
544+ inputs: Tool input parameters keyed by input name.
545+ - Dataset inputs: {"input_name": {"src": "hda", "id": "<dataset_id>"}}
546+ - Collection inputs: {"input_name": {"src": "hdca", "id": "<collection_id>"}}
547+ - Scalar parameters: {"param_name": value}
548+
549+ Returns:
550+ Dict with job info (job_id, history_id, state) and output dataset IDs.
551+
552+ Example:
553+ run_user_tool(
554+ history_id="abc123",
555+ tool_uuid="61d15277-a911-45ef-aa66-5385146578cc",
556+ inputs={
557+ "scorer_output": {"src": "hda", "id": "59ace41fc068d3ad"},
558+ "top_tracks_per_variant": 5,
559+ },
560+ )
561+ """
562+ with _mcp_error_handler ("run_user_tool" ):
563+ ops_manager = get_operations_manager (api_key , ctx )
564+ return ops_manager .run_user_tool (history_id , tool_uuid , inputs )
565+
432566 mcp_app = mcp .http_app (path = "/" )
433567 mcp_app .state .mcp_server = mcp
434568
0 commit comments