Merge branch 'main' of https://huggingface.co/spaces/leafcat/leafcat-mcp
Browse files
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 |
-
|
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 |
-
|
28 |
-
|
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
|
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 |
-
|
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 |
-
|
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 |
-
#
|
74 |
-
#
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
|
|
|
|
79 |
is_selected_tool = (api_name_in_list == selected_api_name)
|
80 |
-
#
|
81 |
-
updates.append(gr.update(visible=is_selected_tool))
|
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
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
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 |
-
#
|
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
|
243 |
-
tool_ui_group
|
244 |
# Ensure the group is initially hidden
|
245 |
tool_ui_group.visible = False
|
246 |
-
#
|
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 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
|
|
|
|
|
|
|
|
265 |
|
266 |
-
# --- Event Handling ---
|
267 |
|
268 |
-
#
|
269 |
-
#
|
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 |
-
|
283 |
-
|
284 |
-
|
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 |
-
#
|
295 |
-
#
|
296 |
-
|
297 |
-
|
298 |
-
|
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=
|
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.")
|