Commit
·
702d53d
1
Parent(s):
49cb9f5
- app.py +251 -75
- tools/time_tool.py +11 -16
- utils/mcp_decorator.py +52 -17
app.py
CHANGED
@@ -1,76 +1,187 @@
|
|
1 |
import gradio as gr
|
2 |
import typing as ty
|
|
|
|
|
3 |
|
4 |
# Importing the tool files automatically registers them with the mcp_tool registry
|
5 |
-
# This assumes all tool files are in the 'tools' directory and end with '_tool.py'
|
6 |
-
# A more robust approach might explicitly import each tool file.
|
7 |
-
# For this example, we explicitly import the one tool file.
|
8 |
import tools.time_tool
|
9 |
|
10 |
# Import the mcp_tool registry instance
|
11 |
from utils.mcp_decorator import mcp_tool
|
12 |
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
"""
|
15 |
-
Updates the displayed docstring
|
16 |
-
based on the selected tool
|
17 |
|
18 |
Args:
|
19 |
-
selected_api_name: The api_name of the tool selected
|
20 |
-
|
21 |
-
|
|
|
|
|
22 |
|
23 |
Returns:
|
24 |
-
A list of gr.update objects for the docstring
|
|
|
|
|
25 |
"""
|
26 |
updates = []
|
|
|
|
|
|
|
|
|
|
|
27 |
|
28 |
# 1. Update Docstring
|
29 |
-
|
|
|
|
|
|
|
|
|
30 |
if selected_api_name:
|
31 |
tool_info = mcp_tool.get_tool_info(selected_api_name)
|
32 |
-
if tool_info
|
33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
|
35 |
-
updates.append(docstring_update)
|
36 |
|
37 |
# 2. Update Visibility of Tool UI Groups
|
38 |
-
#
|
39 |
-
#
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
for api_name_in_list in all_tools_api_names:
|
46 |
is_selected_tool = (api_name_in_list == selected_api_name)
|
47 |
-
#
|
48 |
-
updates.append(gr.
|
49 |
|
50 |
# The total number of updates returned must match the total number of outputs
|
51 |
# defined in the dropdown.change() call.
|
52 |
-
# Outputs are [doc_display, *
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
#
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
|
70 |
# --- Gradio App Layout ---
|
71 |
with gr.Blocks(title="MCP Server Demo") as demo:
|
72 |
gr.Markdown("# Gradio MCP Server Demo")
|
73 |
-
gr.Markdown("Select a tool to view its documentation and
|
74 |
|
75 |
# Get defined tools
|
76 |
tool_options = mcp_tool.get_tools_list() # Returns list of (name, api_name)
|
@@ -84,70 +195,135 @@ with gr.Blocks(title="MCP Server Demo") as demo:
|
|
84 |
choices=[(name, api_name) for name, api_name in tool_options],
|
85 |
label="Select a Tool",
|
86 |
interactive=True,
|
|
|
87 |
)
|
88 |
|
89 |
# Markdown component to display tool documentation
|
90 |
doc_display = gr.Markdown(label="Tool Documentation", visible=False)
|
91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
# Container to hold dynamic UI controls.
|
93 |
-
# We will create a separate gr.Group for each tool's UI inside this container.
|
94 |
tool_uis_container = gr.Column()
|
95 |
|
96 |
-
#
|
97 |
tool_ui_groups_list = []
|
|
|
98 |
|
99 |
# Dynamically create UI components for each tool
|
100 |
with tool_uis_container:
|
101 |
-
|
102 |
-
|
|
|
|
|
103 |
if ui_builder:
|
104 |
-
# Call the UI builder function to get the components
|
105 |
-
tool_ui_group = ui_builder()
|
106 |
# Ensure the group is initially hidden
|
107 |
tool_ui_group.visible = False
|
108 |
-
#
|
|
|
|
|
|
|
109 |
tool_ui_groups_list.append(tool_ui_group)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
else:
|
111 |
# Handle tools defined without a UI builder
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
# Create a dummy group if builder was missing or returned None unexpectedly
|
118 |
-
with gr.Group(visible=False) as dummy_group:
|
119 |
-
gr.Markdown(f"No UI for {tool_name}")
|
120 |
-
tool_ui_groups_list.append(dummy_group)
|
121 |
-
|
122 |
|
123 |
# --- Event Handling ---
|
124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
dropdown.change(
|
126 |
fn=update_tool_info,
|
127 |
-
inputs=[
|
128 |
-
|
129 |
-
|
130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
)
|
132 |
-
|
133 |
-
#
|
134 |
-
#
|
135 |
-
|
136 |
-
|
137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
|
139 |
|
140 |
# --- Launch the Gradio App as an MCP Server ---
|
141 |
if __name__ == "__main__":
|
142 |
-
# To host on Hugging Face Spaces, use server_name="0.0.0.0" and server_port=7860 (default)
|
143 |
-
# The mcp_server=True flag enables the SSE endpoint for MCP clients.
|
144 |
-
# Gradio automatically discovers functions decorated with __mcp_tool__ attribute
|
145 |
-
# (which our @mcp_tool.define decorator adds) and exposes them via the MCP endpoint.
|
146 |
print("Launching Gradio app with MCP server enabled...")
|
147 |
demo.launch(
|
148 |
server_name="0.0.0.0", # Required for Spaces
|
149 |
# server_port=7860, # Default port for Spaces, can be omitted
|
150 |
mcp_server=True, # Enable the MCP server endpoint
|
151 |
-
# share=
|
152 |
)
|
153 |
print("Gradio app launched.")
|
|
|
1 |
import gradio as gr
|
2 |
import typing as ty
|
3 |
+
import inspect
|
4 |
+
import json # For potential JSON output formatting
|
5 |
|
6 |
# Importing the tool files automatically registers them with the mcp_tool registry
|
|
|
|
|
|
|
7 |
import tools.time_tool
|
8 |
|
9 |
# Import the mcp_tool registry instance
|
10 |
from utils.mcp_decorator import mcp_tool
|
11 |
|
12 |
+
# --- Event Handlers ---
|
13 |
+
|
14 |
+
def update_tool_info(
|
15 |
+
selected_api_name: str,
|
16 |
+
execute_button: gr.Button, # Pass component instances to update their visibility
|
17 |
+
api_call_display: ty.Union[gr.Code, gr.Textbox],
|
18 |
+
tool_output_display: ty.Union[gr.Textbox, gr.JSON],
|
19 |
+
*tool_ui_groups_and_inputs # This will receive all tool UI groups and their inputs
|
20 |
+
) -> ty.List[ty.Union[gr.update, dict]]:
|
21 |
"""
|
22 |
+
Updates the displayed docstring, visibility of tool UI groups, and
|
23 |
+
visibility of execute/output areas based on the selected tool.
|
24 |
|
25 |
Args:
|
26 |
+
selected_api_name: The api_name of the tool selected.
|
27 |
+
execute_button, api_call_display, tool_output_display: Component instances
|
28 |
+
*tool_ui_groups_and_inputs: Tuple containing all tool UI group instances
|
29 |
+
followed by all individual input component values.
|
30 |
+
Note: We only need the group instances here for visibility.
|
31 |
|
32 |
Returns:
|
33 |
+
A list of gr.update objects for the docstring, execute button,
|
34 |
+
API/output displays, and each tool UI group.
|
35 |
+
The order must match the order of outputs in the dropdown.change call.
|
36 |
"""
|
37 |
updates = []
|
38 |
+
tool_options = mcp_tool.get_tools_list() # List of (name, api_name)
|
39 |
+
|
40 |
+
# Separate tool groups from other inputs received by the handler
|
41 |
+
# The first len(tool_options) outputs are the tool UI groups
|
42 |
+
tool_ui_groups = tool_ui_groups_and_inputs[:len(tool_options)]
|
43 |
|
44 |
# 1. Update Docstring
|
45 |
+
docstring_value = ""
|
46 |
+
docstring_visible = False
|
47 |
+
execute_button_visible = False
|
48 |
+
output_area_visible = False # Hide API/Output displays by default
|
49 |
+
|
50 |
if selected_api_name:
|
51 |
tool_info = mcp_tool.get_tool_info(selected_api_name)
|
52 |
+
if tool_info:
|
53 |
+
# Show docstring if available
|
54 |
+
if tool_info.get('tool_func') and tool_info['tool_func'].__doc__:
|
55 |
+
docstring_value = f"### Tool Documentation\n---\n{tool_info['tool_func'].__doc__}\n---"
|
56 |
+
docstring_visible = True
|
57 |
+
|
58 |
+
# Show execute button and output area if a tool is selected
|
59 |
+
execute_button_visible = True
|
60 |
+
output_area_visible = True
|
61 |
+
|
62 |
+
|
63 |
+
# Update docstring component
|
64 |
+
updates.append(gr.update(visible=docstring_visible, value=docstring_value))
|
65 |
+
|
66 |
+
# Update visibility of Execute button and output areas
|
67 |
+
updates.append(gr.update(visible=execute_button_visible)) # Execute button
|
68 |
+
updates.append(gr.update(visible=output_area_visible)) # API Call display
|
69 |
+
updates.append(gr.update(visible=output_area_visible)) # Tool Output display
|
70 |
|
|
|
71 |
|
72 |
# 2. Update Visibility of Tool UI Groups
|
73 |
+
# Iterate through the tool groups *in the order they were passed as outputs*
|
74 |
+
# and update their visibility based on the selected_api_name.
|
75 |
+
# The order of tool_ui_groups list must match the order in tool_options / mcp_tool.get_tools_list()
|
76 |
+
all_tools_api_names = [api_name for _, api_name in tool_options]
|
77 |
+
|
78 |
+
for i, api_name_in_list in enumerate(all_tools_api_names):
|
|
|
|
|
79 |
is_selected_tool = (api_name_in_list == selected_api_name)
|
80 |
+
# updates.append(gr.update(component=tool_ui_groups[i], visible=is_selected_tool)) # This syntax also works but the simple value update is fine
|
81 |
+
updates.append(gr.update(visible=is_selected_tool)) # Update the i-th tool group output
|
82 |
|
83 |
# The total number of updates returned must match the total number of outputs
|
84 |
# defined in the dropdown.change() call.
|
85 |
+
# Outputs are: [doc_display, execute_button, api_call_display, tool_output_display, *tool_ui_groups_list]
|
86 |
+
|
87 |
+
return updates
|
88 |
+
|
89 |
+
def execute_tool(
|
90 |
+
selected_api_name: str,
|
91 |
+
*all_potential_input_values # This receives values from ALL input components across ALL tool UIs
|
92 |
+
) -> ty.Tuple[str, str]: # Returns (api_call_string, tool_output_string)
|
93 |
+
"""
|
94 |
+
Executes the selected tool using the values from the visible UI controls.
|
95 |
+
|
96 |
+
Args:
|
97 |
+
selected_api_name: The api_name of the tool selected.
|
98 |
+
*all_potential_input_values: A tuple containing the values of all input components
|
99 |
+
listed in the 'inputs' parameter of the click event.
|
100 |
+
|
101 |
+
Returns:
|
102 |
+
A tuple: (string representation of the API call, string representation of the tool's output).
|
103 |
+
"""
|
104 |
+
if not selected_api_name:
|
105 |
+
return "No tool selected.", "No tool output."
|
106 |
+
|
107 |
+
tool_info = mcp_tool.get_tool_info(selected_api_name)
|
108 |
+
if not tool_info:
|
109 |
+
return f"Error: Tool '{selected_api_name}' not found.", "Error."
|
110 |
+
|
111 |
+
tool_func = tool_info.get('tool_func')
|
112 |
+
arg_component_map = tool_info.get('arg_component_map')
|
113 |
+
|
114 |
+
if not tool_func or not arg_component_map:
|
115 |
+
return f"Error: Tool '{selected_api_name}' is not properly configured.", "Error."
|
116 |
+
|
117 |
+
# Get the complete ordered list of all input components to find value indices
|
118 |
+
all_input_components_list = mcp_tool.get_all_arg_components_list()
|
119 |
+
|
120 |
+
# Build the arguments dictionary for the tool function
|
121 |
+
tool_args = {}
|
122 |
+
try:
|
123 |
+
# Get the function signature to know expected arguments
|
124 |
+
sig = inspect.signature(tool_func)
|
125 |
+
param_names = list(sig.parameters.keys())
|
126 |
+
|
127 |
+
for arg_name in param_names:
|
128 |
+
if arg_name in arg_component_map:
|
129 |
+
component = arg_component_map[arg_name]
|
130 |
+
try:
|
131 |
+
# Find the index of this component in the master list of all inputs
|
132 |
+
# This index corresponds to the position of its value in all_potential_input_values
|
133 |
+
# Add 1 because the first input is the dropdown value (selected_api_name)
|
134 |
+
component_index_in_all_inputs = all_input_components_list.index(component) + 1
|
135 |
+
arg_value = all_potential_input_values[component_index_in_all_inputs]
|
136 |
+
tool_args[arg_name] = arg_value
|
137 |
+
except ValueError:
|
138 |
+
# Should not happen if setup is correct, but good for debugging
|
139 |
+
print(f"Warning: Component for arg '{arg_name}' not found in master input list.")
|
140 |
+
# You might want to skip this arg or handle missing input
|
141 |
+
else:
|
142 |
+
# Handle cases where a tool argument doesn't have a corresponding UI component
|
143 |
+
# Check if the argument has a default value
|
144 |
+
param = sig.parameters[arg_name]
|
145 |
+
if param.default is inspect.Parameter.empty:
|
146 |
+
# Required argument with no UI component and no default
|
147 |
+
return f"Error: Missing UI input for required argument '{arg_name}'.", "Error."
|
148 |
+
# If it has a default, it won't be in tool_args, which is correct for **kwargs
|
149 |
+
|
150 |
+
# --- Execute the tool function ---
|
151 |
+
print(f"Executing tool: {selected_api_name} with args: {tool_args}")
|
152 |
+
tool_output = tool_func(**tool_args)
|
153 |
+
|
154 |
+
# --- Format API Call Display ---
|
155 |
+
api_call_string = f"Tool: {selected_api_name}\n"
|
156 |
+
api_call_string += "Args:\n"
|
157 |
+
if tool_args:
|
158 |
+
# Format args nicely, especially for complex types if needed
|
159 |
+
try:
|
160 |
+
api_call_string += json.dumps(tool_args, indent=2)
|
161 |
+
except TypeError:
|
162 |
+
# Fallback for non-JSON serializable args
|
163 |
+
api_call_string += str(tool_args)
|
164 |
+
else:
|
165 |
+
api_call_string += "{}"
|
166 |
+
|
167 |
+
|
168 |
+
# --- Format Tool Output Display ---
|
169 |
+
# Convert output to string for display
|
170 |
+
tool_output_string = str(tool_output)
|
171 |
+
|
172 |
+
return api_call_string, tool_output_string
|
173 |
+
|
174 |
+
except Exception as e:
|
175 |
+
# Catch any errors during argument collection or function execution
|
176 |
+
error_message = f"An error occurred during tool execution: {e}"
|
177 |
+
print(error_message)
|
178 |
+
return f"API Call Error: {error_message}", f"Tool Output Error: {e}"
|
179 |
+
|
180 |
|
181 |
# --- Gradio App Layout ---
|
182 |
with gr.Blocks(title="MCP Server Demo") as demo:
|
183 |
gr.Markdown("# Gradio MCP Server Demo")
|
184 |
+
gr.Markdown("Select a tool to view its documentation, configure parameters, and execute.")
|
185 |
|
186 |
# Get defined tools
|
187 |
tool_options = mcp_tool.get_tools_list() # Returns list of (name, api_name)
|
|
|
195 |
choices=[(name, api_name) for name, api_name in tool_options],
|
196 |
label="Select a Tool",
|
197 |
interactive=True,
|
198 |
+
value=None # Start with no tool selected
|
199 |
)
|
200 |
|
201 |
# Markdown component to display tool documentation
|
202 |
doc_display = gr.Markdown(label="Tool Documentation", visible=False)
|
203 |
|
204 |
+
# Button to execute the tool (initially hidden)
|
205 |
+
execute_button = gr.Button("Execute Tool", visible=False)
|
206 |
+
|
207 |
+
# Area to display the API call signature (initially hidden)
|
208 |
+
# Using gr.Code for a code-like appearance
|
209 |
+
api_call_display = gr.Code(
|
210 |
+
label="Use Via API (Example Call)",
|
211 |
+
language="json", # Or "yaml" or "text"
|
212 |
+
interactive=False,
|
213 |
+
visible=False,
|
214 |
+
value=""
|
215 |
+
)
|
216 |
+
|
217 |
+
# Area to display the tool's output (initially hidden)
|
218 |
+
# Using gr.Textbox for simplicity, could use gr.JSON if outputs are dicts/lists
|
219 |
+
tool_output_display = gr.Textbox(
|
220 |
+
label="Tool Output",
|
221 |
+
interactive=False,
|
222 |
+
visible=False,
|
223 |
+
value="",
|
224 |
+
container=True # Wrap in a container for better spacing
|
225 |
+
)
|
226 |
+
|
227 |
+
|
228 |
# Container to hold dynamic UI controls.
|
|
|
229 |
tool_uis_container = gr.Column()
|
230 |
|
231 |
+
# Lists to hold the UI groups and ALL individual input components from ALL tools
|
232 |
tool_ui_groups_list = []
|
233 |
+
all_input_components_list = []
|
234 |
|
235 |
# Dynamically create UI components for each tool
|
236 |
with tool_uis_container:
|
237 |
+
# Process tools in a consistent order (matching mcp_tool.get_all_arg_components_list)
|
238 |
+
# This ensures the order of components in all_input_components_list is predictable.
|
239 |
+
for tool_name, api_name in sorted(mcp_tool.tools.items()): # Iterate registry items directly for order
|
240 |
+
ui_builder = api_name.get('ui_builder')
|
241 |
if ui_builder:
|
242 |
+
# Call the UI builder function to get the components and the arg mapping
|
243 |
+
tool_ui_group, arg_component_map = ui_builder()
|
244 |
# Ensure the group is initially hidden
|
245 |
tool_ui_group.visible = False
|
246 |
+
# Store the created group and component map in the registry instance
|
247 |
+
mcp_tool.set_tool_ui_components(api_name=tool_name, ui_group=tool_ui_group, arg_component_map=arg_component_map)
|
248 |
+
|
249 |
+
# Add the group to our list for output mapping (for visibility updates)
|
250 |
tool_ui_groups_list.append(tool_ui_group)
|
251 |
+
|
252 |
+
# Add the individual input components from this tool to the master list
|
253 |
+
# Ensure order within a tool is consistent (e.g., alphabetical by arg name)
|
254 |
+
for arg_name in sorted(arg_component_map.keys()):
|
255 |
+
component = arg_component_map[arg_name]
|
256 |
+
all_input_components_list.append(component)
|
257 |
+
|
258 |
else:
|
259 |
# Handle tools defined without a UI builder
|
260 |
+
# Add a placeholder group for consistency in outputs list
|
261 |
+
with gr.Group(visible=False) as empty_group_placeholder:
|
262 |
+
gr.Markdown(f"No UI defined for {tool_name} ({api_name})", visible=False)
|
263 |
+
tool_ui_groups_list.append(empty_group_placeholder)
|
264 |
+
# Note: No input components are added to all_input_components_list for this tool
|
|
|
|
|
|
|
|
|
|
|
265 |
|
266 |
# --- Event Handling ---
|
267 |
+
|
268 |
+
# Define the list of outputs for the dropdown change event
|
269 |
+
# The order must match the order the updates are returned by update_tool_info
|
270 |
+
dropdown_outputs = [
|
271 |
+
doc_display,
|
272 |
+
execute_button,
|
273 |
+
api_call_display,
|
274 |
+
tool_output_display,
|
275 |
+
*tool_ui_groups_list # Spread all the tool UI groups
|
276 |
+
]
|
277 |
+
|
278 |
+
# When the dropdown selection changes, update visibility of UI elements
|
279 |
dropdown.change(
|
280 |
fn=update_tool_info,
|
281 |
+
inputs=[
|
282 |
+
dropdown, # Input 0: Selected API Name
|
283 |
+
execute_button, # Input 1: Execute Button instance (needed for output mapping logic in handler)
|
284 |
+
api_call_display, # Input 2: API Display instance
|
285 |
+
tool_output_display, # Input 3: Output Display instance
|
286 |
+
*tool_ui_groups_list # Inputs 4 to 4 + len(tool_ui_groups_list) -1: Tool UI Group instances
|
287 |
+
# Although update_tool_info only uses their identity/order,
|
288 |
+
# Gradio requires them as inputs if they are also outputs.
|
289 |
+
],
|
290 |
+
outputs=dropdown_outputs,
|
291 |
+
# queue=False # Could potentially improve responsiveness if needed
|
292 |
)
|
293 |
+
|
294 |
+
# Define the list of inputs for the execute button click event
|
295 |
+
# The order must match the order expected by execute_tool function
|
296 |
+
execute_inputs = [
|
297 |
+
dropdown, # Input 0: Selected API Name
|
298 |
+
*all_input_components_list # Inputs 1 onwards: Values from all individual input components
|
299 |
+
]
|
300 |
+
|
301 |
+
# When the execute button is clicked, run the selected tool
|
302 |
+
execute_button.click(
|
303 |
+
fn=execute_tool,
|
304 |
+
inputs=execute_inputs,
|
305 |
+
outputs=[
|
306 |
+
api_call_display, # Output 0: Update the API Call display
|
307 |
+
tool_output_display # Output 1: Update the Tool Output display
|
308 |
+
],
|
309 |
+
# queue=True # Keep queue=True for potential long-running tool executions
|
310 |
+
)
|
311 |
+
|
312 |
+
# Optional: Trigger initial load to set visibility correctly on app startup
|
313 |
+
# This requires setting a default value in the dropdown or handling None correctly
|
314 |
+
# in the event handler if you don't want a default tool shown.
|
315 |
+
# If value=None in dropdown and allow_clear=True, the initial state handled by
|
316 |
+
# update_tool_info when selected_api_name is None will hide everything, which is fine.
|
317 |
+
# demo.load(fn=update_tool_info, inputs=[dropdown, execute_button, api_call_display, tool_output_display, *tool_ui_groups_list], outputs=dropdown_outputs)
|
318 |
|
319 |
|
320 |
# --- Launch the Gradio App as an MCP Server ---
|
321 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
322 |
print("Launching Gradio app with MCP server enabled...")
|
323 |
demo.launch(
|
324 |
server_name="0.0.0.0", # Required for Spaces
|
325 |
# server_port=7860, # Default port for Spaces, can be omitted
|
326 |
mcp_server=True, # Enable the MCP server endpoint
|
327 |
+
# share=False, # Default and recommended for Spaces
|
328 |
)
|
329 |
print("Gradio app launched.")
|
tools/time_tool.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
import datetime
|
2 |
-
import pytz
|
3 |
import typing as ty
|
4 |
import gradio as gr
|
5 |
|
@@ -26,12 +26,9 @@ def get_current_time(timezone: str = "UTC") -> str:
|
|
26 |
str: The current time in the specified timezone, or an error message.
|
27 |
"""
|
28 |
try:
|
29 |
-
# Get the timezone object
|
30 |
tz = pytz.timezone(timezone)
|
31 |
-
# Get the current UTC time and convert it to the target timezone
|
32 |
now_utc = datetime.datetime.utcnow()
|
33 |
now_in_tz = pytz.utc.localize(now_utc).astimezone(tz)
|
34 |
-
# Format the time
|
35 |
return now_in_tz.strftime('%Y-%m-%d %H:%M:%S %Z%z')
|
36 |
except pytz.UnknownTimeZoneError:
|
37 |
return f"Error: Unknown timezone '{timezone}'. Please provide a valid IANA timezone name."
|
@@ -40,28 +37,26 @@ def get_current_time(timezone: str = "UTC") -> str:
|
|
40 |
|
41 |
# --- UI Control Builder ---
|
42 |
@mcp_tool.build_ui_control(api_name="get_current_time")
|
43 |
-
def build_time_ui_control() -> gr.Group:
|
44 |
"""
|
45 |
Builds Gradio UI components for the Get current time tool.
|
46 |
-
Returns a gr.Group containing
|
47 |
"""
|
48 |
-
#
|
49 |
-
|
|
|
50 |
with gr.Group(visible=False) as time_tool_group:
|
51 |
gr.Markdown("Configure **Get current time** tool:")
|
52 |
# Create a textbox for the 'timezone' argument
|
|
|
53 |
timezone_input = gr.Textbox(
|
54 |
label="Timezone",
|
55 |
value="UTC",
|
56 |
placeholder="e.g., America/New_York, Asia/Ho_Chi_Minh",
|
57 |
interactive=True,
|
|
|
58 |
)
|
59 |
-
|
60 |
-
# If you wanted a "Run" button in the UI, you would add it here
|
61 |
-
# and define an event handler in the main app.
|
62 |
-
# For dynamic display based on tool selection, returning a container
|
63 |
-
# (like gr.Group or gr.Column) is a good pattern.
|
64 |
-
|
65 |
-
return time_tool_group # Return the group containing the controls
|
66 |
|
67 |
-
#
|
|
|
|
1 |
import datetime
|
2 |
+
import pytz
|
3 |
import typing as ty
|
4 |
import gradio as gr
|
5 |
|
|
|
26 |
str: The current time in the specified timezone, or an error message.
|
27 |
"""
|
28 |
try:
|
|
|
29 |
tz = pytz.timezone(timezone)
|
|
|
30 |
now_utc = datetime.datetime.utcnow()
|
31 |
now_in_tz = pytz.utc.localize(now_utc).astimezone(tz)
|
|
|
32 |
return now_in_tz.strftime('%Y-%m-%d %H:%M:%S %Z%z')
|
33 |
except pytz.UnknownTimeZoneError:
|
34 |
return f"Error: Unknown timezone '{timezone}'. Please provide a valid IANA timezone name."
|
|
|
37 |
|
38 |
# --- UI Control Builder ---
|
39 |
@mcp_tool.build_ui_control(api_name="get_current_time")
|
40 |
+
def build_time_ui_control() -> ty.Tuple[gr.Group, ty.Dict[str, gr.components.Component]]:
|
41 |
"""
|
42 |
Builds Gradio UI components for the Get current time tool.
|
43 |
+
Returns a tuple: (gr.Group containing controls, dict mapping arg name to component).
|
44 |
"""
|
45 |
+
# Dictionary to hold the mapping from argument name (string) to component instance
|
46 |
+
arg_components = {}
|
47 |
+
|
48 |
with gr.Group(visible=False) as time_tool_group:
|
49 |
gr.Markdown("Configure **Get current time** tool:")
|
50 |
# Create a textbox for the 'timezone' argument
|
51 |
+
# Store the component instance in the dictionary
|
52 |
timezone_input = gr.Textbox(
|
53 |
label="Timezone",
|
54 |
value="UTC",
|
55 |
placeholder="e.g., America/New_York, Asia/Ho_Chi_Minh",
|
56 |
interactive=True,
|
57 |
+
# elem_id=f"get_current_time_timezone_input" # Good practice for debugging, not strictly needed by logic
|
58 |
)
|
59 |
+
arg_components["timezone"] = timezone_input
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
|
61 |
+
# Return the group and the dictionary mapping arg names to components
|
62 |
+
return time_tool_group, arg_components
|
utils/mcp_decorator.py
CHANGED
@@ -6,7 +6,14 @@ import gradio as gr
|
|
6 |
# Define a registry to store tools and their metadata
|
7 |
class MCPToolRegistry:
|
8 |
def __init__(self):
|
9 |
-
# Stores tool data:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
self.tools = {}
|
11 |
|
12 |
def define(self, name: str, api_name: str):
|
@@ -20,28 +27,35 @@ class MCPToolRegistry:
|
|
20 |
'name': name,
|
21 |
'tool_func': tool_func,
|
22 |
'ui_builder': None, # Will be filled by @build_ui_control
|
|
|
|
|
23 |
}
|
24 |
-
|
25 |
# Gradio's MCP server needs the function itself to be decorated
|
26 |
# with an attribute carrying the MCP metadata.
|
27 |
-
# This is a Gradio-specific requirement for mcp_server=True
|
28 |
-
# The structure below is based on Gradio's internal handling.
|
29 |
setattr(tool_func, "__mcp_tool__", {"name": name, "api_name": api_name})
|
30 |
|
31 |
-
# Return the original function so it can be called if needed
|
32 |
return tool_func
|
33 |
return decorator
|
34 |
|
35 |
def build_ui_control(self, api_name: str):
|
36 |
-
"""
|
37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
if api_name not in self.tools:
|
39 |
raise ValueError(f"Tool with api_name '{api_name}' not defined. Define it using @mcp_tool.define first.")
|
40 |
|
41 |
-
# Store the UI builder function
|
42 |
self.tools[api_name]['ui_builder'] = ui_builder_func
|
43 |
|
44 |
-
#
|
|
|
|
|
45 |
return ui_builder_func
|
46 |
return decorator
|
47 |
|
@@ -56,17 +70,38 @@ class MCPToolRegistry:
|
|
56 |
def get_tool_ui_builder(self, api_name: str) -> ty.Optional[ty.Callable]:
|
57 |
"""Returns the UI builder function for a given api_name."""
|
58 |
info = self.get_tool_info(api_name)
|
59 |
-
return info
|
60 |
|
61 |
def get_tool_function(self, api_name: str) -> ty.Optional[ty.Callable]:
|
62 |
"""Returns the tool function for a given api_name."""
|
63 |
info = self.get_tool_info(api_name)
|
64 |
-
return info
|
65 |
|
66 |
-
|
67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
|
|
|
|
|
|
|
|
|
6 |
# Define a registry to store tools and their metadata
|
7 |
class MCPToolRegistry:
|
8 |
def __init__(self):
|
9 |
+
# Stores tool data:
|
10 |
+
# {'api_name': {
|
11 |
+
# 'name': str,
|
12 |
+
# 'tool_func': func,
|
13 |
+
# 'ui_builder': func,
|
14 |
+
# 'ui_group': gr.Group, # Store the created group instance
|
15 |
+
# 'arg_component_map': {str: gr.components.Component} # Map arg name to component instance
|
16 |
+
# }}
|
17 |
self.tools = {}
|
18 |
|
19 |
def define(self, name: str, api_name: str):
|
|
|
27 |
'name': name,
|
28 |
'tool_func': tool_func,
|
29 |
'ui_builder': None, # Will be filled by @build_ui_control
|
30 |
+
'ui_group': None, # Will be filled during UI construction
|
31 |
+
'arg_component_map': {}, # Will be filled during UI construction
|
32 |
}
|
33 |
+
|
34 |
# Gradio's MCP server needs the function itself to be decorated
|
35 |
# with an attribute carrying the MCP metadata.
|
|
|
|
|
36 |
setattr(tool_func, "__mcp_tool__", {"name": name, "api_name": api_name})
|
37 |
|
|
|
38 |
return tool_func
|
39 |
return decorator
|
40 |
|
41 |
def build_ui_control(self, api_name: str):
|
42 |
+
"""
|
43 |
+
Decorator to associate a UI builder function with a tool.
|
44 |
+
The decorated function should return a tuple:
|
45 |
+
(gr.Group or gr.Column component, dict[str, gr.components.Component])
|
46 |
+
where the dict maps argument names (matching tool_func signature)
|
47 |
+
to the corresponding input component instance.
|
48 |
+
"""
|
49 |
+
def decorator(ui_builder_func: ty.Callable[..., ty.Tuple[ty.Union[gr.components.Component, tuple[gr.components.Component, ...]], ty.Dict[str, gr.components.Component]]]):
|
50 |
if api_name not in self.tools:
|
51 |
raise ValueError(f"Tool with api_name '{api_name}' not defined. Define it using @mcp_tool.define first.")
|
52 |
|
53 |
+
# Store the UI builder function reference
|
54 |
self.tools[api_name]['ui_builder'] = ui_builder_func
|
55 |
|
56 |
+
# Note: The actual ui_group and arg_component_map will be populated
|
57 |
+
# when app.py calls the ui_builder_func during Blocks setup.
|
58 |
+
|
59 |
return ui_builder_func
|
60 |
return decorator
|
61 |
|
|
|
70 |
def get_tool_ui_builder(self, api_name: str) -> ty.Optional[ty.Callable]:
|
71 |
"""Returns the UI builder function for a given api_name."""
|
72 |
info = self.get_tool_info(api_name)
|
73 |
+
return info.get('ui_builder') if info else None
|
74 |
|
75 |
def get_tool_function(self, api_name: str) -> ty.Optional[ty.Callable]:
|
76 |
"""Returns the tool function for a given api_name."""
|
77 |
info = self.get_tool_info(api_name)
|
78 |
+
return info.get('tool_func') if info else None
|
79 |
|
80 |
+
def set_tool_ui_components(self, api_name: str, ui_group: gr.components.Component, arg_component_map: ty.Dict[str, gr.components.Component]):
|
81 |
+
"""Stores the actual UI group and component map after building."""
|
82 |
+
if api_name not in self.tools:
|
83 |
+
print(f"Warning: Tried to set UI components for undefined tool '{api_name}'")
|
84 |
+
return
|
85 |
+
self.tools[api_name]['ui_group'] = ui_group
|
86 |
+
self.tools[api_name]['arg_component_map'] = arg_component_map
|
87 |
+
|
88 |
+
def get_all_arg_components_list(self) -> ty.List[gr.components.Component]:
|
89 |
+
"""Returns a flat list of ALL input components from ALL tools, preserving order."""
|
90 |
+
all_components = []
|
91 |
+
# Iterate through tools in a consistent order (e.g., by api_name)
|
92 |
+
for api_name in sorted(self.tools.keys()):
|
93 |
+
tool_info = self.tools[api_name]
|
94 |
+
# Iterate through the arg_component_map in a consistent order (e.g., by arg name)
|
95 |
+
for arg_name in sorted(tool_info['arg_component_map'].keys()):
|
96 |
+
component = tool_info['arg_component_map'][arg_name]
|
97 |
+
all_components.append(component)
|
98 |
+
return all_components
|
99 |
|
100 |
+
def get_arg_component_map(self, api_name: str) -> ty.Optional[ty.Dict[str, gr.components.Component]]:
|
101 |
+
"""Returns the arg_component_map for a specific tool."""
|
102 |
+
info = self.get_tool_info(api_name)
|
103 |
+
return info.get('arg_component_map') if info else None
|
104 |
+
|
105 |
+
|
106 |
+
# Instantiate the registry. This instance will be used by decorators
|
107 |
+
mcp_tool = MCPToolRegistry()
|