import gradio as gr | |
import typing as ty | |
# Importing the tool files automatically registers them with the mcp_tool registry | |
# This assumes all tool files are in the 'tools' directory and end with '_tool.py' | |
# A more robust approach might explicitly import each tool file. | |
# For this example, we explicitly import the one tool file. | |
import tools.time_tool | |
# Import the mcp_tool registry instance | |
from utils.mcp_decorator import mcp_tool | |
def update_tool_info(selected_api_name: str, *tool_group_visibility_states: gr.State) -> ty.List[gr.update]: | |
""" | |
Updates the displayed docstring and the visibility of tool UI groups | |
based on the selected tool from the dropdown. | |
Args: | |
selected_api_name: The api_name of the tool selected in the dropdown. | |
tool_group_visibility_states: The current state of visibility for each tool group component | |
(passed automatically by Gradio when they are outputs). | |
Returns: | |
A list of gr.update objects for the docstring and each tool UI group. | |
""" | |
updates = [] | |
# 1. Update Docstring | |
docstring_update = gr.Markdown.update(visible=False, value="") # Hide and clear by default | |
if selected_api_name: | |
tool_info = mcp_tool.get_tool_info(selected_api_name) | |
if tool_info and tool_info.get('tool_func') and tool_info['tool_func'].__doc__: | |
docstring_update = gr.Markdown.update(visible=True, value=f"### Tool Documentation\n---\n{tool_info['tool_func'].__doc__}\n---") | |
updates.append(docstring_update) | |
# 2. Update Visibility of Tool UI Groups | |
# We need to return an update for *each* tool UI group defined in the layout. | |
# The order must match the order they appear in the 'outputs' list of the .change() event. | |
all_tools_api_names = [api_name for _, api_name in mcp_tool.get_tools_list()] | |
# Ensure the order of updates matches the order of tool_uis_list created below | |
# We'll assume the order from get_tools_list() is consistent. | |
for api_name_in_list in all_tools_api_names: | |
is_selected_tool = (api_name_in_list == selected_api_name) | |
# Return gr.update(visible=...) for each tool group | |
updates.append(gr.Group.update(visible=is_selected_tool)) | |
# The total number of updates returned must match the total number of outputs | |
# defined in the dropdown.change() call. | |
# Outputs are [doc_display, *list_of_tool_groups] | |
# Inputs are [dropdown, *list_of_tool_groups] | |
# This function signature with *tool_group_visibility_states receiving the | |
# current state of the output components is how Gradio handles inputs/outputs | |
# when the outputs themselves are also inputs for their own updates (like visibility). | |
# However, for simple visibility toggling based *only* on the dropdown value, | |
# we don't strictly *need* tool_group_visibility_states as input. We can just | |
# return the updates based on the selected_api_name. | |
# Let's simplify the function signature and inputs/outputs. | |
# The function just needs selected_api_name. It will return the docstring update | |
# and visibility updates for *all* groups. | |
# Revised update_tool_info: | |
# Input: selected_api_name (str) | |
# Outputs: doc_display (Markdown), Tool_UI_Group_1 (Group), Tool_UI_Group_2 (Group), ... | |
return updates # This list contains updates for docstring + all tool groups | |
# --- Gradio App Layout --- | |
with gr.Blocks(title="MCP Server Demo") as demo: | |
gr.Markdown("# Gradio MCP Server Demo") | |
gr.Markdown("Select a tool to view its documentation and UI controls.") | |
# Get defined tools | |
tool_options = mcp_tool.get_tools_list() # Returns list of (name, api_name) | |
if not tool_options: | |
gr.Warning("No tools defined. Please check the 'tools' directory.") | |
gr.Markdown("No tools available.") | |
else: | |
# Dropdown to select tool. Using api_name as value for easier lookup. | |
dropdown = gr.Dropdown( | |
choices=[(name, api_name) for name, api_name in tool_options], | |
label="Select a Tool", | |
interactive=True, | |
) | |
# Markdown component to display tool documentation | |
doc_display = gr.Markdown(label="Tool Documentation", visible=False) | |
# Container to hold dynamic UI controls. | |
# We will create a separate gr.Group for each tool's UI inside this container. | |
tool_uis_container = gr.Column() | |
# List to hold the UI groups for each tool and map outputs | |
tool_ui_groups_list = [] | |
# Dynamically create UI components for each tool | |
with tool_uis_container: | |
for tool_name, api_name in tool_options: | |
ui_builder = mcp_tool.get_tool_ui_builder(api_name) | |
if ui_builder: | |
# Call the UI builder function to get the components (should be a gr.Group/Column) | |
tool_ui_group = ui_builder() | |
# Ensure the group is initially hidden | |
tool_ui_group.visible = False | |
# Add the group to our list for output mapping | |
tool_ui_groups_list.append(tool_ui_group) | |
else: | |
# Handle tools defined without a UI builder | |
gr.Markdown(f"Tool '{tool_name}' ({api_name}) has no UI controls defined.", visible=False, elem_id=f"no_ui_{api_name}") | |
# Add a placeholder to maintain output count consistency if necessary, | |
# or ensure build_ui_control always returns a gr.Group (even empty). | |
# Let's assume build_ui_control always returns a group/column. | |
if api_name not in [g.elem_id for g in tool_ui_groups_list if hasattr(g, 'elem_id')]: # Avoid duplicates if builder called earlier | |
# Create a dummy group if builder was missing or returned None unexpectedly | |
with gr.Group(visible=False) as dummy_group: | |
gr.Markdown(f"No UI for {tool_name}") | |
tool_ui_groups_list.append(dummy_group) | |
# --- Event Handling --- | |
# When the dropdown selection changes, update the displayed info and controls | |
dropdown.change( | |
fn=update_tool_info, | |
inputs=[dropdown], # Just the selected value from the dropdown | |
outputs=[doc_display, *tool_ui_groups_list], # Docstring + all tool UI groups for visibility updates | |
# Set queue=False for smoother UI updates if needed, depends on complexity | |
# queue=False # might cause issues with visibility updates in complex layouts, test if needed | |
) | |
# Trigger an initial update if a default value is set or after loading | |
# This ensures the UI is correct when the app first loads if a tool is pre-selected | |
# or if you want to show the first tool's info by default. | |
# Let's not trigger initially unless a tool is pre-selected in the dropdown. | |
# If allow_clear=True and value is None, initial update won't show anything which is fine. | |
# --- Launch the Gradio App as an MCP Server --- | |
if __name__ == "__main__": | |
# To host on Hugging Face Spaces, use server_name="0.0.0.0" and server_port=7860 (default) | |
# The mcp_server=True flag enables the SSE endpoint for MCP clients. | |
# Gradio automatically discovers functions decorated with __mcp_tool__ attribute | |
# (which our @mcp_tool.define decorator adds) and exposes them via the MCP endpoint. | |
print("Launching Gradio app with MCP server enabled...") | |
demo.launch( | |
server_name="0.0.0.0", # Required for Spaces | |
# server_port=7860, # Default port for Spaces, can be omitted | |
mcp_server=True, # Enable the MCP server endpoint | |
# share=True, # Default and recommended for Spaces | |
) | |
print("Gradio app launched.") |