eienmojiki commited on
Commit
702d53d
·
1 Parent(s): 49cb9f5
Files changed (3) hide show
  1. app.py +251 -75
  2. tools/time_tool.py +11 -16
  3. 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
- def update_tool_info(selected_api_name: str, *tool_group_visibility_states: gr.State) -> ty.List[gr.update]:
 
 
 
 
 
 
 
 
14
  """
15
- Updates the displayed docstring and the visibility of tool UI groups
16
- based on the selected tool from the dropdown.
17
 
18
  Args:
19
- selected_api_name: The api_name of the tool selected in the dropdown.
20
- tool_group_visibility_states: The current state of visibility for each tool group component
21
- (passed automatically by Gradio when they are outputs).
 
 
22
 
23
  Returns:
24
- A list of gr.update objects for the docstring and each tool UI group.
 
 
25
  """
26
  updates = []
 
 
 
 
 
27
 
28
  # 1. Update Docstring
29
- docstring_update = gr.Markdown.update(visible=False, value="") # Hide and clear by default
 
 
 
 
30
  if selected_api_name:
31
  tool_info = mcp_tool.get_tool_info(selected_api_name)
32
- if tool_info and tool_info.get('tool_func') and tool_info['tool_func'].__doc__:
33
- docstring_update = gr.Markdown.update(visible=True, value=f"### Tool Documentation\n---\n{tool_info['tool_func'].__doc__}\n---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- updates.append(docstring_update)
36
 
37
  # 2. Update Visibility of Tool UI Groups
38
- # We need to return an update for *each* tool UI group defined in the layout.
39
- # The order must match the order they appear in the 'outputs' list of the .change() event.
40
- all_tools_api_names = [api_name for _, api_name in mcp_tool.get_tools_list()]
41
-
42
- # Ensure the order of updates matches the order of tool_uis_list created below
43
- # We'll assume the order from get_tools_list() is consistent.
44
-
45
- for api_name_in_list in all_tools_api_names:
46
  is_selected_tool = (api_name_in_list == selected_api_name)
47
- # Return gr.update(visible=...) for each tool group
48
- updates.append(gr.Group.update(visible=is_selected_tool))
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, *list_of_tool_groups]
53
- # Inputs are [dropdown, *list_of_tool_groups]
54
- # This function signature with *tool_group_visibility_states receiving the
55
- # current state of the output components is how Gradio handles inputs/outputs
56
- # when the outputs themselves are also inputs for their own updates (like visibility).
57
- # However, for simple visibility toggling based *only* on the dropdown value,
58
- # we don't strictly *need* tool_group_visibility_states as input. We can just
59
- # return the updates based on the selected_api_name.
60
- # Let's simplify the function signature and inputs/outputs.
61
- # The function just needs selected_api_name. It will return the docstring update
62
- # and visibility updates for *all* groups.
63
-
64
- # Revised update_tool_info:
65
- # Input: selected_api_name (str)
66
- # Outputs: doc_display (Markdown), Tool_UI_Group_1 (Group), Tool_UI_Group_2 (Group), ...
67
-
68
- return updates # This list contains updates for docstring + all tool groups
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 UI controls.")
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
- # List to hold the UI groups for each tool and map outputs
97
  tool_ui_groups_list = []
 
98
 
99
  # Dynamically create UI components for each tool
100
  with tool_uis_container:
101
- for tool_name, api_name in tool_options:
102
- ui_builder = mcp_tool.get_tool_ui_builder(api_name)
 
 
103
  if ui_builder:
104
- # Call the UI builder function to get the components (should be a gr.Group/Column)
105
- tool_ui_group = ui_builder()
106
  # Ensure the group is initially hidden
107
  tool_ui_group.visible = False
108
- # Add the group to our list for output mapping
 
 
 
109
  tool_ui_groups_list.append(tool_ui_group)
 
 
 
 
 
 
 
110
  else:
111
  # Handle tools defined without a UI builder
112
- gr.Markdown(f"Tool '{tool_name}' ({api_name}) has no UI controls defined.", visible=False, elem_id=f"no_ui_{api_name}")
113
- # Add a placeholder to maintain output count consistency if necessary,
114
- # or ensure build_ui_control always returns a gr.Group (even empty).
115
- # Let's assume build_ui_control always returns a group/column.
116
- 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
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
- # When the dropdown selection changes, update the displayed info and controls
 
 
 
 
 
 
 
 
 
 
 
125
  dropdown.change(
126
  fn=update_tool_info,
127
- inputs=[dropdown], # Just the selected value from the dropdown
128
- outputs=[doc_display, *tool_ui_groups_list], # Docstring + all tool UI groups for visibility updates
129
- # Set queue=False for smoother UI updates if needed, depends on complexity
130
- # queue=False # might cause issues with visibility updates in complex layouts, test if needed
 
 
 
 
 
 
 
131
  )
132
-
133
- # Trigger an initial update if a default value is set or after loading
134
- # This ensures the UI is correct when the app first loads if a tool is pre-selected
135
- # or if you want to show the first tool's info by default.
136
- # Let's not trigger initially unless a tool is pre-selected in the dropdown.
137
- # If allow_clear=True and value is None, initial update won't show anything which is fine.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=True, # Default and recommended for Spaces
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 # You need to install pytz: pip install 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 the controls.
47
  """
48
- # Use a gr.Group to contain the controls for this tool
49
- # We will manage the visibility of this group in the main app
 
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
- # Note: We are only building the *display* components here.
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
- # When this file is imported, the decorators register the tool and its UI builder
 
 
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: {'api_name': {'name': str, 'tool_func': func, 'ui_builder': func}}
 
 
 
 
 
 
 
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
- """Decorator to associate a UI builder function with a tool."""
37
- def decorator(ui_builder_func: ty.Callable[..., ty.Union[gr.components.Component, tuple[gr.components.Component, ...]]]):
 
 
 
 
 
 
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
- # Return the original UI builder function
 
 
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['ui_builder'] if info else None
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['tool_func'] if info else None
65
 
66
- # Instantiate the registry. This instance will be used by decorators
67
- mcp_tool = MCPToolRegistry()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- # Note: Gradio's mcp_server=True inspects functions decorated with the
70
- # __mcp_tool__ attribute. Our @mcp_tool.define decorator handles this.
71
- # The registry (`mcp_tool` instance) is primarily for the Gradio UI
72
- # part to list tools, get docstrings, and build dynamic interfaces.
 
 
 
 
 
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()