Commit
·
49cb9f5
1
Parent(s):
3c30c34
++
Browse files- .gitattributes +0 -35
- README.md +0 -13
- app.py +146 -302
- src/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- src/tools/__pycache__/echo_tool.cpython-312.pyc +0 -0
- src/tools/__pycache__/time_tool.cpython-312.pyc +0 -0
- src/tools/echo_tool.py +0 -49
- src/tools/time_tool.py +0 -49
- src/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- src/utils/__pycache__/decorators.cpython-312.pyc +0 -0
- src/utils/decorators.py +0 -66
- tools/__pycache__/time_tool.cpython-312.pyc +0 -0
- tools/time_tool.py +67 -0
- utils/__pycache__/mcp_decorator.cpython-312.pyc +0 -0
- utils/mcp_decorator.py +72 -0
.gitattributes
DELETED
@@ -1,35 +0,0 @@
|
|
1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
DELETED
@@ -1,13 +0,0 @@
|
|
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 |
-
---
|
12 |
-
|
13 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
@@ -1,309 +1,153 @@
|
|
1 |
import gradio as gr
|
2 |
-
import
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
input_components = []
|
88 |
-
output_components = []
|
89 |
-
children_for_ui_area = []
|
90 |
-
|
91 |
-
if ui_builder:
|
92 |
-
try:
|
93 |
-
# The ui_builder should return two lists: input_components and output_components
|
94 |
-
# These components are already instantiated Gradio components.
|
95 |
-
inputs, outputs = ui_builder()
|
96 |
-
input_components.extend(inputs)
|
97 |
-
output_components.extend(outputs)
|
98 |
-
|
99 |
-
# Add all returned components to be displayed
|
100 |
-
children_for_ui_area.extend(input_components)
|
101 |
-
children_for_ui_area.extend(output_components)
|
102 |
-
|
103 |
-
# Store references for the run button
|
104 |
-
tools_registry[tool_name]['input_components'] = input_components
|
105 |
-
tools_registry[tool_name]['output_components'] = output_components
|
106 |
-
|
107 |
-
except Exception as e:
|
108 |
-
print(f"Error building UI for tool '{tool_name}': {e}")
|
109 |
-
children_for_ui_area = [gr.Markdown(f"**Error building UI for {tool_name}:** {e}")]
|
110 |
-
# Fallback or error display
|
111 |
-
else:
|
112 |
-
children_for_ui_area = [gr.Markdown(f"**Note:** No custom UI defined for tool '{tool_name}'. Parameters (if any) will be handled generically if possible, or this tool might not be runnable via UI directly without a UI builder.")]
|
113 |
-
# Potentially fall back to generic inputs if no ui_builder, or disable run button
|
114 |
-
# For now, we assume ui_builder is necessary for runnable tools with this new setup.
|
115 |
-
return {
|
116 |
-
tool_docstring_display: gr.update(visible=True, value=docstring_md),
|
117 |
-
tool_specific_ui_area: gr.update(visible=True, children=children_for_ui_area),
|
118 |
-
run_button: gr.update(visible=False), # No UI, no run
|
119 |
-
current_tool_inputs_state: [],
|
120 |
-
current_tool_outputs_state: []
|
121 |
-
}
|
122 |
-
|
123 |
-
return {
|
124 |
-
tool_docstring_display: gr.update(visible=True, value=docstring_md),
|
125 |
-
# Update the group with the new children components
|
126 |
-
tool_specific_ui_area: gr.update(visible=True, children=children_for_ui_area),
|
127 |
-
run_button: gr.update(visible=True if selected_tool.get("function") else False),
|
128 |
-
current_tool_inputs_state: input_components, # Pass actual components
|
129 |
-
current_tool_outputs_state: output_components # Pass actual components
|
130 |
-
}
|
131 |
-
|
132 |
-
# The `run_selected_tool` function now needs to accept `*args` for dynamic inputs.
|
133 |
-
# The order of `*args` will match the order of `input_components` returned by the `ui_builder`.
|
134 |
-
def run_selected_tool(tool_name, *input_values):
|
135 |
-
if not tool_name or tool_name not in tools_registry:
|
136 |
-
return [gr.update(value="Error: Tool not selected or not found.")] * len(tools_registry.get(tool_name, {}).get('output_components', [1])) # Match number of outputs
|
137 |
-
|
138 |
-
selected_tool_info = tools_registry[tool_name]
|
139 |
-
tool_function = selected_tool_info.get("function")
|
140 |
-
output_ui_components = selected_tool_info.get("output_components", [])
|
141 |
-
num_outputs = len(output_ui_components) if output_ui_components else 1
|
142 |
-
|
143 |
-
if not tool_function:
|
144 |
-
return [gr.update(value=f"Error: No function defined for tool '{tool_name}'.")] * num_outputs
|
145 |
-
|
146 |
-
sig = inspect.signature(tool_function)
|
147 |
-
param_names = list(sig.parameters.keys())
|
148 |
-
|
149 |
-
kwargs = {}
|
150 |
-
# Map input_values to parameter names based on order
|
151 |
-
# This assumes the ui_builder returns input components in the same order as function parameters.
|
152 |
-
# More robust mapping might use component labels or ids if available and reliable.
|
153 |
-
if len(input_values) != len(param_names):
|
154 |
-
# This can happen if ui_builder provides different number of inputs than function expects
|
155 |
-
# Or if not all inputs are passed correctly by Gradio (e.g. if some are gr.State)
|
156 |
-
# For now, we'll proceed if input_values are fewer, assuming defaults or optional args.
|
157 |
-
# A more robust solution would involve inspecting component properties or explicit mapping.
|
158 |
-
print(f"Warning: Number of input values ({len(input_values)}) from UI does not match number of function parameters ({len(param_names)}) for tool '{tool_name}'. Attempting to call with available values.")
|
159 |
-
|
160 |
-
for i, param_name in enumerate(param_names):
|
161 |
-
if i < len(input_values):
|
162 |
-
# Type conversion based on annotation - similar to previous logic but simplified for this example
|
163 |
-
param_annotation = sig.parameters[param_name].annotation
|
164 |
-
raw_value = input_values[i]
|
165 |
-
try:
|
166 |
-
if param_annotation is inspect.Parameter.empty or param_annotation == str:
|
167 |
-
kwargs[param_name] = str(raw_value)
|
168 |
-
elif param_annotation == int:
|
169 |
-
kwargs[param_name] = int(raw_value)
|
170 |
-
elif param_annotation == float:
|
171 |
-
kwargs[param_name] = float(raw_value)
|
172 |
-
elif param_annotation == bool:
|
173 |
-
kwargs[param_name] = bool(raw_value) # Gradio components like Checkbox pass bool directly
|
174 |
-
else:
|
175 |
-
kwargs[param_name] = raw_value # Pass as is for other types or rely on tool validation
|
176 |
-
except ValueError as ve:
|
177 |
-
error_msg = f"Error converting parameter '{param_name}' for tool '{tool_name}': {ve}"
|
178 |
-
return [gr.update(value=error_msg)] * num_outputs
|
179 |
-
# If fewer input_values than params, rely on function defaults for remaining params
|
180 |
-
|
181 |
-
try:
|
182 |
-
result = tool_function(**kwargs)
|
183 |
-
# If the tool function returns multiple values, it should be a tuple/list
|
184 |
-
# matching the number of output_ui_components.
|
185 |
-
if num_outputs == 1:
|
186 |
-
return [gr.update(value=str(result))]
|
187 |
-
elif isinstance(result, (list, tuple)) and len(result) == num_outputs:
|
188 |
-
return [gr.update(value=str(r)) for r in result]
|
189 |
-
else:
|
190 |
-
# Fallback if result format doesn't match output components
|
191 |
-
print(f"Warning: Tool '{tool_name}' result format mismatch. Expected {num_outputs} outputs, got {type(result)}.")
|
192 |
-
return [gr.update(value=str(result))] + [gr.update(value="-")] * (num_outputs - 1)
|
193 |
-
|
194 |
-
except Exception as e:
|
195 |
-
error_msg = f"Error executing tool '{tool_name}': {str(e)}"
|
196 |
-
return [gr.update(value=error_msg)] * num_outputs
|
197 |
-
|
198 |
-
tool_dropdown.change(
|
199 |
-
fn=display_tool_interface,
|
200 |
-
inputs=[tool_dropdown],
|
201 |
-
outputs=[
|
202 |
-
tool_docstring_display,
|
203 |
-
tool_specific_ui_area,
|
204 |
-
run_button,
|
205 |
-
current_tool_inputs_state, # To store the dynamic input components
|
206 |
-
current_tool_outputs_state # To store the dynamic output components
|
207 |
-
],
|
208 |
-
# Add request=True if you need access to gr.Request object in display_tool_interface
|
209 |
-
# api_name=False # if you don't want this to be an API endpoint
|
210 |
)
|
211 |
|
212 |
-
#
|
213 |
-
|
214 |
-
|
215 |
-
#
|
216 |
-
#
|
217 |
-
|
218 |
-
|
219 |
-
#
|
220 |
-
|
221 |
-
|
222 |
-
#
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
#
|
247 |
-
#
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
# This is not directly possible without re-creating the button or using JS.
|
255 |
-
|
256 |
-
# Alternative: Use a fixed number of placeholder inputs/outputs for the click event,
|
257 |
-
# and map them inside run_selected_tool based on the actual active tool's UI components.
|
258 |
-
# This is a common workaround.
|
259 |
-
# Let's assume a maximum of, say, 5 input components and 5 output components for any tool for now.
|
260 |
-
# These would be placeholder components in the main UI, and the tool's UI builder would update them.
|
261 |
-
# This brings us back to a semi-generic approach but with tool-specific UI builders.
|
262 |
-
|
263 |
-
# For a truly dynamic approach as intended by the new decorator:
|
264 |
-
# The `run_button.click` needs to be able to gather values from dynamically added components.
|
265 |
-
# The `*args` in `run_selected_tool` is the most Gradio-idiomatic way.
|
266 |
-
# The `inputs` to `click` must be a list of components.
|
267 |
-
# `current_tool_inputs_state` (which is `gr.State`) will pass its *value* (the list of components)
|
268 |
-
# to `run_selected_tool`. Gradio doesn't automatically unpack this list of components
|
269 |
-
# and pass their values as `*args`.
|
270 |
-
|
271 |
-
# The most straightforward way with the current Gradio version for this dynamicism is to
|
272 |
-
# have `run_selected_tool` accept the list of input values directly.
|
273 |
-
# The `inputs` to `run_button.click` will be `[tool_dropdown, current_tool_inputs_state]`
|
274 |
-
# Then, inside `run_selected_tool`, `current_tool_inputs_state` will be the *list of values* from those components.
|
275 |
-
|
276 |
-
# Let's refine `run_selected_tool` to expect `input_values_list` from `current_tool_inputs_state`.
|
277 |
-
|
278 |
-
# The `outputs` for `tool_dropdown.change` needs to include the `current_tool_inputs_state` and `current_tool_outputs_state`
|
279 |
-
# so they are correctly populated with the component instances.
|
280 |
-
|
281 |
-
# The `inputs` for `run_button.click` will be `[tool_dropdown, current_tool_inputs_state]`
|
282 |
-
# The `outputs` for `run_button.click` will be `current_tool_outputs_state`
|
283 |
-
# This means `run_selected_tool` will receive the *values* from the components in `current_tool_inputs_state` as its second argument.
|
284 |
-
|
285 |
-
# Revisit `run_selected_tool` signature and logic:
|
286 |
-
# def run_selected_tool(tool_name, input_values_from_state_components):
|
287 |
-
# ... where input_values_from_state_components is a list of values.
|
288 |
-
|
289 |
-
# This seems to be the most viable path with Gradio's Python API for dynamic inputs/outputs to an event.
|
290 |
-
|
291 |
-
# The `outputs` of `tool_dropdown.change` are correct in setting the states.
|
292 |
-
# Now, wire `run_button.click`:
|
293 |
-
run_button.click(
|
294 |
-
fn=run_selected_tool,
|
295 |
-
inputs=[tool_dropdown, current_tool_inputs_state], # Pass tool_name and the list of input component values
|
296 |
-
outputs=current_tool_outputs_state # The list of output components to update
|
297 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
298 |
|
299 |
-
return demo
|
300 |
|
|
|
301 |
if __name__ == "__main__":
|
302 |
-
|
303 |
-
# The mcp_server=True enables
|
304 |
-
#
|
305 |
-
#
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
|
|
|
|
|
|
|
|
|
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)
|
77 |
+
|
78 |
+
if not tool_options:
|
79 |
+
gr.Warning("No tools defined. Please check the 'tools' directory.")
|
80 |
+
gr.Markdown("No tools available.")
|
81 |
+
else:
|
82 |
+
# Dropdown to select tool. Using api_name as value for easier lookup.
|
83 |
+
dropdown = gr.Dropdown(
|
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.")
|
src/tools/__pycache__/__init__.cpython-312.pyc
DELETED
Binary file (149 Bytes)
|
|
src/tools/__pycache__/echo_tool.cpython-312.pyc
DELETED
Binary file (811 Bytes)
|
|
src/tools/__pycache__/time_tool.cpython-312.pyc
DELETED
Binary file (1.34 kB)
|
|
src/tools/echo_tool.py
DELETED
@@ -1,49 +0,0 @@
|
|
1 |
-
import gradio as gr
|
2 |
-
from src.utils.decorators import mcp_tool_instance # Updated import
|
3 |
-
|
4 |
-
TOOL_NAME = "Echo Message"
|
5 |
-
|
6 |
-
@mcp_tool_instance.define(name=TOOL_NAME, description="Echoes a message a specified number of times.")
|
7 |
-
def echo_message(message: str, count: int = 1) -> str:
|
8 |
-
"""Repeats the given message a specified number of times.
|
9 |
-
|
10 |
-
Args:
|
11 |
-
message (str): The message to echo.
|
12 |
-
count (int, optional): The number of times to repeat the message. Defaults to 1.
|
13 |
-
|
14 |
-
Returns:
|
15 |
-
str: The echoed message, or an error message if inputs are invalid.
|
16 |
-
"""
|
17 |
-
if not isinstance(message, str):
|
18 |
-
return f"Error: Message must be a string. Received type: {type(message)}"
|
19 |
-
if not isinstance(count, int):
|
20 |
-
try:
|
21 |
-
# Attempt to convert count to int if it's a string representation of an int
|
22 |
-
count = int(count)
|
23 |
-
except ValueError:
|
24 |
-
return f"Error: Count must be an integer. Received type: {type(count)}, value: {count}"
|
25 |
-
except TypeError: # Handles cases where count might be None or other non-convertible types
|
26 |
-
return f"Error: Count must be an integer. Received invalid type: {type(count)}"
|
27 |
-
|
28 |
-
if count < 0:
|
29 |
-
return "Error: Count cannot be negative."
|
30 |
-
if count > 1000: # Adding a reasonable upper limit
|
31 |
-
return "Error: Count is too large (max 1000)."
|
32 |
-
|
33 |
-
# Ensure message is not excessively long to prevent performance issues
|
34 |
-
if len(message) * count > 10000: # Arbitrary limit for total output length
|
35 |
-
return "Error: Resulting message is too long."
|
36 |
-
|
37 |
-
return (message + "\n") * count
|
38 |
-
|
39 |
-
@mcp_tool_instance.ui()
|
40 |
-
def echo_tool_ui():
|
41 |
-
"""Creates the Gradio UI components for the Echo Message tool."""
|
42 |
-
# Input components
|
43 |
-
message_input = gr.Textbox(label="Message", info="The message to repeat.")
|
44 |
-
count_input = gr.Number(label="Count", value=1, minimum=0, maximum=1000, step=1, info="Number of times to repeat the message.")
|
45 |
-
|
46 |
-
# Output component
|
47 |
-
output_display = gr.Textbox(label="Echoed Message", interactive=False, lines=3)
|
48 |
-
|
49 |
-
return [message_input, count_input], [output_display]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/tools/time_tool.py
DELETED
@@ -1,49 +0,0 @@
|
|
1 |
-
from datetime import datetime
|
2 |
-
import pytz
|
3 |
-
import gradio as gr
|
4 |
-
from src.utils.decorators import mcp_tool_instance # Updated import
|
5 |
-
|
6 |
-
TOOL_NAME = "Get Current Time"
|
7 |
-
|
8 |
-
@mcp_tool_instance.define(name=TOOL_NAME, description="Gets the current time in a specified IANA timezone.")
|
9 |
-
def get_time(timezone: str) -> str:
|
10 |
-
"""Fetches the current time for a given IANA timezone.
|
11 |
-
|
12 |
-
Args:
|
13 |
-
timezone (str): The IANA timezone name (e.g., 'America/New_York', 'Europe/London').
|
14 |
-
|
15 |
-
Returns:
|
16 |
-
str: The current time in 'YYYY-MM-DD HH:MM:SS TZ' format, or an error message.
|
17 |
-
"""
|
18 |
-
try:
|
19 |
-
if not isinstance(timezone, str):
|
20 |
-
return f"Error: Timezone must be a string. Received type: {type(timezone)}"
|
21 |
-
if not timezone.strip():
|
22 |
-
return f"Error: Timezone cannot be empty."
|
23 |
-
# Validate if the timezone is known
|
24 |
-
if timezone not in pytz.all_timezones:
|
25 |
-
# Provide a hint for common mistakes
|
26 |
-
suggestion = ""
|
27 |
-
if timezone.lower() == "utc" or timezone.lower() == "gmt":
|
28 |
-
suggestion = " Did you mean 'UTC' or 'Etc/GMT'?"
|
29 |
-
return f"Error: Unknown timezone '{timezone}'. Please use a valid IANA timezone name.{suggestion}"
|
30 |
-
|
31 |
-
tz = pytz.timezone(timezone)
|
32 |
-
current_time = datetime.now(tz)
|
33 |
-
return current_time.strftime("%Y-%m-%d %H:%M:%S %Z%z")
|
34 |
-
except pytz.exceptions.UnknownTimeZoneError:
|
35 |
-
return f"Error: Unknown timezone '{timezone}'."
|
36 |
-
except Exception as e:
|
37 |
-
return f"An unexpected error occurred while fetching time for '{timezone}': {str(e)}"
|
38 |
-
|
39 |
-
@mcp_tool_instance.ui()
|
40 |
-
def time_tool_ui():
|
41 |
-
"""Creates the Gradio UI components for the Get Current Time tool."""
|
42 |
-
# Input component: Textbox for the timezone
|
43 |
-
timezone_input = gr.Textbox(label="Timezone", info="Enter IANA timezone (e.g., America/New_York, UTC, Europe/London)", value="UTC")
|
44 |
-
|
45 |
-
# Output component: Textbox to display the result
|
46 |
-
output_display = gr.Textbox(label="Current Time", interactive=False)
|
47 |
-
|
48 |
-
# Return a list of input components and a list of output components
|
49 |
-
return [timezone_input], [output_display]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/utils/__pycache__/__init__.cpython-312.pyc
DELETED
Binary file (223 Bytes)
|
|
src/utils/__pycache__/decorators.cpython-312.pyc
DELETED
Binary file (620 Bytes)
|
|
src/utils/decorators.py
DELETED
@@ -1,66 +0,0 @@
|
|
1 |
-
import inspect
|
2 |
-
import gradio as gr
|
3 |
-
|
4 |
-
tools_registry = {}
|
5 |
-
|
6 |
-
class MCPTool:
|
7 |
-
def __init__(self):
|
8 |
-
self._last_defined_tool_name = None # To store the name of the tool most recently defined
|
9 |
-
|
10 |
-
def define(self, name, description):
|
11 |
-
"""Decorator factory to define a tool's metadata and core function."""
|
12 |
-
def decorator(func):
|
13 |
-
if name in tools_registry and "function" in tools_registry[name]:
|
14 |
-
# If UI was defined first, merge, otherwise overwrite/update.
|
15 |
-
tools_registry[name].update({
|
16 |
-
"function": func,
|
17 |
-
"description": description,
|
18 |
-
"parameters": func.__annotations__,
|
19 |
-
"docstring": inspect.getdoc(func)
|
20 |
-
})
|
21 |
-
else:
|
22 |
-
tools_registry[name] = {
|
23 |
-
"function": func,
|
24 |
-
"description": description,
|
25 |
-
"parameters": func.__annotations__,
|
26 |
-
"docstring": inspect.getdoc(func),
|
27 |
-
"ui_builder": None, # Placeholder for UI builder function
|
28 |
-
"input_components": [], # Placeholder for actual input Gradio components
|
29 |
-
"output_components": [] # Placeholder for actual output Gradio components
|
30 |
-
}
|
31 |
-
self._last_defined_tool_name = name # Store the name for the ui decorator
|
32 |
-
# print(f"Defined tool: {name}")
|
33 |
-
return func
|
34 |
-
return decorator
|
35 |
-
|
36 |
-
def ui(self): # Removed tool_name parameter
|
37 |
-
"""Decorator factory to define a tool's UI builder function."""
|
38 |
-
def decorator(ui_func):
|
39 |
-
tool_name_to_use = self._last_defined_tool_name
|
40 |
-
if not tool_name_to_use:
|
41 |
-
raise ValueError("MCPTool.define() must be called before MCPTool.ui() for the same tool instance.")
|
42 |
-
|
43 |
-
if tool_name_to_use not in tools_registry:
|
44 |
-
# Define tool structure if UI is defined before the main function
|
45 |
-
# This case should ideally not happen if define is always called first for a given name.
|
46 |
-
# However, if ui() is called after define() for a *different* tool name without an intervening define(),
|
47 |
-
# this logic might still be hit if _last_defined_tool_name was from a previous, unrelated tool.
|
48 |
-
# The ValueError above should prevent misuse for the *same* tool instance flow.
|
49 |
-
tools_registry[tool_name_to_use] = {
|
50 |
-
"function": None,
|
51 |
-
"description": "(Description to be defined)",
|
52 |
-
"parameters": {},
|
53 |
-
"docstring": "(Docstring to be defined)",
|
54 |
-
"ui_builder": ui_func,
|
55 |
-
"input_components": [],
|
56 |
-
"output_components": []
|
57 |
-
}
|
58 |
-
else:
|
59 |
-
tools_registry[tool_name_to_use]["ui_builder"] = ui_func
|
60 |
-
|
61 |
-
# print(f"Registered UI for tool: {tool_name_to_use}")
|
62 |
-
return ui_func
|
63 |
-
return decorator
|
64 |
-
|
65 |
-
# Default instance for convenience, or users can create their own MCPTool instances
|
66 |
-
mcp_tool_instance = MCPTool()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tools/__pycache__/time_tool.cpython-312.pyc
ADDED
Binary file (2.64 kB). View file
|
|
tools/time_tool.py
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
|
6 |
+
# Import the decorator instance
|
7 |
+
from utils.mcp_decorator import mcp_tool
|
8 |
+
|
9 |
+
# --- Tool Definition ---
|
10 |
+
@mcp_tool.define(
|
11 |
+
name="Get current time",
|
12 |
+
api_name="get_current_time",
|
13 |
+
)
|
14 |
+
def get_current_time(timezone: str = "UTC") -> str:
|
15 |
+
"""
|
16 |
+
Gets the current time for the given timezone string.
|
17 |
+
|
18 |
+
This tool takes an IANA timezone name (like "UTC", "America/New_York",
|
19 |
+
"Asia/Ho_Chi_Minh") and returns the current datetime in that zone.
|
20 |
+
Defaults to "UTC" if no timezone is provided.
|
21 |
+
|
22 |
+
Args:
|
23 |
+
timezone (str): The IANA timezone string (e.g., "UTC").
|
24 |
+
|
25 |
+
Returns:
|
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."
|
38 |
+
except Exception as e:
|
39 |
+
return f"An unexpected error occurred: {e}"
|
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
|
utils/__pycache__/mcp_decorator.cpython-312.pyc
ADDED
Binary file (3.93 kB). View file
|
|
utils/mcp_decorator.py
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import functools
|
2 |
+
import inspect
|
3 |
+
import typing as ty
|
4 |
+
import gradio as gr
|
5 |
+
|
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):
|
13 |
+
"""Decorator to define an MCP tool."""
|
14 |
+
def decorator(tool_func: ty.Callable):
|
15 |
+
if api_name in self.tools:
|
16 |
+
raise ValueError(f"Tool with api_name '{api_name}' already defined.")
|
17 |
+
|
18 |
+
# Store the tool function and metadata
|
19 |
+
self.tools[api_name] = {
|
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 |
+
|
48 |
+
def get_tools_list(self) -> list[tuple[str, str]]:
|
49 |
+
"""Returns a list of (tool_name, api_name) for all defined tools."""
|
50 |
+
return [(data['name'], api_name) for api_name, data in self.tools.items()]
|
51 |
+
|
52 |
+
def get_tool_info(self, api_name: str) -> ty.Optional[dict]:
|
53 |
+
"""Returns the full info dict for a given api_name."""
|
54 |
+
return self.tools.get(api_name)
|
55 |
+
|
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.
|