eienmojiki commited on
Commit
49cb9f5
·
1 Parent(s): 3c30c34
.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 inspect
3
- import importlib
4
- import os
5
- import sys
6
-
7
- from src.utils.decorators import tools_registry, mcp_tool_instance
8
-
9
- # --- Dynamic Tool Loading ---
10
- def load_tools(tools_dir_name="tools"):
11
- """Dynamically loads tools from the specified directory (src/tools)."""
12
- # Construct the absolute path to the 'src/tools' directory
13
- # __file__ is app.py in the project root
14
- # tools are in src/tools
15
- base_dir = os.path.dirname(os.path.abspath(__file__))
16
- tools_path = os.path.join(base_dir, "src", tools_dir_name)
17
-
18
- if not os.path.exists(tools_path):
19
- print(f"Tools directory not found: {tools_path}")
20
- return
21
-
22
- for filename in os.listdir(tools_path):
23
- if filename.endswith(".py") and not filename.startswith("__"):
24
- # Module name should be relative to a path in sys.path
25
- # e.g., src.tools.echo_tool
26
- module_name = f"src.{tools_dir_name}.{filename[:-3]}"
27
- try:
28
- importlib.import_module(module_name)
29
- except ImportError as e:
30
- print(f"Error importing tool module {module_name}: {e}")
31
- except Exception as e:
32
- print(f"An unexpected error occurred while loading {module_name}: {e}")
33
-
34
- # --- Gradio UI ---
35
- def build_ui():
36
- load_tools()
37
-
38
- with gr.Blocks(title="MCP Server") as demo:
39
- gr.Markdown("# MCP Server with Custom Tool UIs")
40
-
41
- with gr.Row():
42
- tool_dropdown = gr.Dropdown(choices=sorted(list(tools_registry.keys())), label="Select Tool", allow_custom_value=False)
43
-
44
- tool_docstring_display = gr.Markdown(visible=False)
45
-
46
- # This area will hold the dynamically generated UI for the selected tool
47
- # We use a Group to contain potentially multiple rows/columns from the tool's UI builder
48
- tool_specific_ui_area = gr.Group(visible=False)
49
-
50
- # Placeholder for where the tool's input components will be rendered
51
- # This needs to be a component that can be updated with other components (e.g., gr.Column or gr.Group)
52
- # We will manage the actual components within the display_tool_interface function
53
- # and pass them to the run_button.click event.
54
- # For simplicity, we'll create a list of placeholders for inputs and outputs that can be updated.
55
- # The actual components will be created by the tool's ui_builder.
56
-
57
- # We need a way to pass the dynamically created input components to the run_selected_tool function.
58
- # Gradio's `inputs` for `click` events expects a list of components.
59
- # We'll use a gr.State to hold references to the current tool's input and output components.
60
- current_tool_inputs_state = gr.State([])
61
- current_tool_outputs_state = gr.State([])
62
-
63
- run_button = gr.Button("Run Tool", visible=False)
64
- # Output display will now be part of the tool_specific_ui_area, managed by the tool's UI builder.
65
- # We remove the generic tool_output here.
66
-
67
- def display_tool_interface(tool_name, request: gr.Request):
68
- # Clear previous tool's UI if any
69
- # This is tricky because we can't directly remove components.
70
- # We hide the main container and will repopulate it.
71
- # A better approach might involve JavaScript or more complex Gradio state management.
72
- # For now, we'll rely on returning gr.update for the container and its contents.
73
-
74
- if not tool_name or tool_name not in tools_registry:
75
- return {
76
- tool_docstring_display: gr.update(visible=False, value=""),
77
- tool_specific_ui_area: gr.update(visible=False, children=[]),
78
- run_button: gr.update(visible=False),
79
- current_tool_inputs_state: [],
80
- current_tool_outputs_state: []
81
- }
82
-
83
- selected_tool = tools_registry[tool_name]
84
- docstring_md = f"### {tool_name}\n\n**Description:** {selected_tool.get('description', 'N/A')}\n\n**Docstring:**\n```\n{selected_tool.get('docstring', 'No docstring provided.')}\n```"
85
-
86
- ui_builder = selected_tool.get("ui_builder")
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
- # The run_button.click event needs to be set up *after* display_tool_interface
213
- # has potentially populated current_tool_inputs_state and current_tool_outputs_state.
214
- # However, Gradio's event system defines inputs/outputs statically when the UI is built.
215
- # This means we can't directly use the components from current_tool_inputs_state in the inputs list here
216
- # in a way that Gradio understands for dynamic component lists.
217
- # The *args approach in run_selected_tool is the way to handle dynamic inputs.
218
- # We need to ensure that the `inputs` list for `run_button.click` correctly refers to the components
219
- # that are *currently visible and active* for the selected tool.
220
- # This is the most complex part of dynamic UI in Gradio.
221
-
222
- # A common pattern is to have a fixed maximum number of input slots and update them,
223
- # or to use gr.update to change the `inputs` of the click event dynamically (more advanced, often needs JS).
224
-
225
- # For now, the `run_selected_tool` will receive `tool_name` and `*input_values`.
226
- # The `*input_values` will be sourced from the components held in `current_tool_inputs_state`.
227
- # Gradio needs a list of components for the `inputs` argument of `click`.
228
- # We will pass the `current_tool_inputs_state` itself, and unpack it inside `run_selected_tool` if needed,
229
- # or rely on Gradio to pass the *values* from these components if they are correctly registered.
230
-
231
- # Let's adjust `run_button.click`:
232
- # The `inputs` should be `tool_dropdown` and then the components from `current_tool_inputs_state`.
233
- # The `outputs` should be the components from `current_tool_outputs_state`.
234
- # This requires `display_tool_interface` to correctly populate these states with component instances.
235
-
236
- # This is a simplified approach. A fully dynamic setup where the number and type of inputs/outputs
237
- # to .click() change based on dropdown selection is hard in pure Python Gradio.
238
- # The `*args` in `run_selected_tool` is key.
239
- # We will pass the state objects that *contain* the lists of components.
240
- # Gradio will pass the *values* from the components referenced by the state at the time of the click.
241
-
242
- # The `run_button.click` will be connected when a tool is selected and its UI is built.
243
- # We can't define it here with dynamic inputs/outputs directly in the initial build.
244
- # Instead, we will update the click event or handle it via the `current_tool_inputs_state`.
245
-
246
- # Let's try to make the `run_button.click` inputs dynamic by passing the state
247
- # and then accessing the components from the state inside the handler or relying on Gradio's behavior.
248
- # The `inputs` to `run_button.click` will be `tool_dropdown` and then the components themselves.
249
- # This means `display_tool_interface` must return the components to be wired up.
250
- # This is getting complex. Let's simplify: `run_selected_tool` will take `tool_name` and `*args`.
251
- # The `inputs` to `run_button.click` will be `tool_dropdown` plus the *actual input components*.
252
- # The `outputs` will be the *actual output components*.
253
- # This means `display_tool_interface` must update the `run_button.click` event or its `inputs`/`outputs`.
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
- app_ui = build_ui()
303
- # The mcp_server=True enables Server-Sent Events for MCP communication if Gradio supports it directly.
304
- # For Gradio versions that don't directly use mcp_server for custom SSE,
305
- # this flag might be for internal Gradio features or future compatibility.
306
- # The core MCP functionality here is through the tool registration and execution via UI.
307
- # Using share=False for local development by default, can be changed to True if ngrok tunneling is desired.
308
- app_ui.launch(server_name="0.0.0.0", server_port=7860, mcp_server=True, share=False)
309
- # print("Gradio App Launched. Registered tools:", list(tools_registry.keys()))
 
 
 
 
 
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.