eienmojiki commited on
Commit
21bf97f
·
2 Parent(s): 702d53d 0c0cf30

Merge branch 'main' of https://huggingface.co/spaces/leafcat/leafcat-mcp

Browse files
Files changed (2) hide show
  1. README.md +11 -0
  2. app.py +63 -205
README.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Leafcat Mcp
3
+ emoji: 🏃
4
+ colorFrom: gray
5
+ colorTo: yellow
6
+ sdk: gradio
7
+ sdk_version: 5.29.0
8
+ app_file: app.py
9
+ pinned: false
10
+ short_description: LeafCat AIO MCP server with Gradio
11
+ ---
app.py CHANGED
@@ -9,30 +9,18 @@ import tools.time_tool
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)
@@ -42,141 +30,46 @@ def update_tool_info(
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:
@@ -199,6 +92,7 @@ with gr.Blocks(title="MCP Server Demo") as demo:
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)
@@ -228,7 +122,7 @@ with gr.Blocks(title="MCP Server Demo") as demo:
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
 
@@ -239,14 +133,11 @@ with gr.Blocks(title="MCP Server Demo") as demo:
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
@@ -257,64 +148,32 @@ with gr.Blocks(title="MCP Server Demo") as demo:
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 ---
@@ -322,8 +181,7 @@ 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.")
 
9
  # Import the mcp_tool registry instance
10
  from utils.mcp_decorator import mcp_tool
11
 
12
+ def update_tool_info(selected_api_name: str, *tool_group_visibility_states: gr.State) -> ty.List[gr.update]:
 
 
 
 
 
 
 
 
13
  """
14
  Updates the displayed docstring, visibility of tool UI groups, and
15
  visibility of execute/output areas based on the selected tool.
16
 
17
  Args:
18
+ selected_api_name: The api_name of the tool selected in the dropdown.
19
+ tool_group_visibility_states: The current state of visibility for each tool group component
20
+ (passed automatically by Gradio when they are outputs).
 
 
21
 
22
  Returns:
23
+ A list of gr.update objects for the docstring and each tool UI group.
 
 
24
  """
25
  updates = []
26
  tool_options = mcp_tool.get_tools_list() # List of (name, api_name)
 
30
  tool_ui_groups = tool_ui_groups_and_inputs[:len(tool_options)]
31
 
32
  # 1. Update Docstring
33
+ docstring_update = gr.Markdown.update(visible=False, value="") # Hide and clear by default
 
 
 
 
34
  if selected_api_name:
35
  tool_info = mcp_tool.get_tool_info(selected_api_name)
36
+ if tool_info and tool_info.get('tool_func') and tool_info['tool_func'].__doc__:
37
+ docstring_update = gr.Markdown.update(visible=True, value=f"### Tool Documentation\n---\n{tool_info['tool_func'].__doc__}\n---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ updates.append(docstring_update)
40
 
41
  # 2. Update Visibility of Tool UI Groups
42
+ # We need to return an update for *each* tool UI group defined in the layout.
43
+ # The order must match the order they appear in the 'outputs' list of the .change() event.
44
+ all_tools_api_names = [api_name for _, api_name in mcp_tool.get_tools_list()]
45
+
46
+ # Ensure the order of updates matches the order of tool_uis_list created below
47
+ # We'll assume the order from get_tools_list() is consistent.
48
+
49
+ for api_name_in_list in all_tools_api_names:
50
  is_selected_tool = (api_name_in_list == selected_api_name)
51
+ # Return gr.update(visible=...) for each tool group
52
+ updates.append(gr.Group.update(visible=is_selected_tool))
53
 
54
  # The total number of updates returned must match the total number of outputs
55
  # defined in the dropdown.change() call.
56
+ # Outputs are [doc_display, *list_of_tool_groups]
57
+ # Inputs are [dropdown, *list_of_tool_groups]
58
+ # This function signature with *tool_group_visibility_states receiving the
59
+ # current state of the output components is how Gradio handles inputs/outputs
60
+ # when the outputs themselves are also inputs for their own updates (like visibility).
61
+ # However, for simple visibility toggling based *only* on the dropdown value,
62
+ # we don't strictly *need* tool_group_visibility_states as input. We can just
63
+ # return the updates based on the selected_api_name.
64
+ # Let's simplify the function signature and inputs/outputs.
65
+ # The function just needs selected_api_name. It will return the docstring update
66
+ # and visibility updates for *all* groups.
67
+
68
+ # Revised update_tool_info:
69
+ # Input: selected_api_name (str)
70
+ # Outputs: doc_display (Markdown), Tool_UI_Group_1 (Group), Tool_UI_Group_2 (Group), ...
71
+
72
+ return updates # This list contains updates for docstring + all tool groups
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  # --- Gradio App Layout ---
75
  with gr.Blocks(title="MCP Server Demo") as demo:
 
92
  )
93
 
94
  # Markdown component to display tool documentation
95
+ # Needs a specific elem_id or be directly referenced as an output
96
  doc_display = gr.Markdown(label="Tool Documentation", visible=False)
97
 
98
  # Button to execute the tool (initially hidden)
 
122
  # Container to hold dynamic UI controls.
123
  tool_uis_container = gr.Column()
124
 
125
+ # List to hold the UI groups for each tool and map outputs
126
  tool_ui_groups_list = []
127
  all_input_components_list = []
128
 
 
133
  for tool_name, api_name in sorted(mcp_tool.tools.items()): # Iterate registry items directly for order
134
  ui_builder = api_name.get('ui_builder')
135
  if ui_builder:
136
+ # Call the UI builder function to get the components (should be a gr.Group/Column)
137
+ tool_ui_group = ui_builder()
138
  # Ensure the group is initially hidden
139
  tool_ui_group.visible = False
140
+ # Add the group to our list for output mapping
 
 
 
141
  tool_ui_groups_list.append(tool_ui_group)
142
 
143
  # Add the individual input components from this tool to the master list
 
148
 
149
  else:
150
  # Handle tools defined without a UI builder
151
+ gr.Markdown(f"Tool '{tool_name}' ({api_name}) has no UI controls defined.", visible=False, elem_id=f"no_ui_{api_name}")
152
+ # Add a placeholder to maintain output count consistency if necessary,
153
+ # or ensure build_ui_control always returns a gr.Group (even empty).
154
+ # Let's assume build_ui_control always returns a group/column.
155
+ 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
156
+ # Create a dummy group if builder was missing or returned None unexpectedly
157
+ with gr.Group(visible=False) as dummy_group:
158
+ gr.Markdown(f"No UI for {tool_name}")
159
+ tool_ui_groups_list.append(dummy_group)
160
 
 
161
 
162
+ # --- Event Handling ---
163
+ # When the dropdown selection changes, update the displayed info and controls
 
 
 
 
 
 
 
 
 
164
  dropdown.change(
165
  fn=update_tool_info,
166
+ inputs=[dropdown], # Just the selected value from the dropdown
167
+ outputs=[doc_display, *tool_ui_groups_list], # Docstring + all tool UI groups for visibility updates
168
+ # Set queue=False for smoother UI updates if needed, depends on complexity
169
+ # queue=False # might cause issues with visibility updates in complex layouts, test if needed
 
 
 
 
 
 
 
170
  )
171
+
172
+ # Trigger an initial update if a default value is set or after loading
173
+ # This ensures the UI is correct when the app first loads if a tool is pre-selected
174
+ # or if you want to show the first tool's info by default.
175
+ # Let's not trigger initially unless a tool is pre-selected in the dropdown.
176
+ # If allow_clear=True and value is None, initial update won't show anything which is fine.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
 
179
  # --- Launch the Gradio App as an MCP Server ---
 
181
  print("Launching Gradio app with MCP server enabled...")
182
  demo.launch(
183
  server_name="0.0.0.0", # Required for Spaces
 
184
  mcp_server=True, # Enable the MCP server endpoint
185
+ # share=True, # Default and recommended for Spaces
186
  )
187
  print("Gradio app launched.")