Spaces:
Running
Running
| import gradio as gr | |
| from huggingface_hub import HfApi, list_spaces | |
| import time | |
| from typing import List, Dict, Optional | |
| from dataclasses import dataclass | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| class MCPSpace: | |
| id: str | |
| title: str | |
| author: str | |
| likes: int | |
| status: str | |
| url: str | |
| description: str | |
| sdk: str | |
| last_modified: str | |
| created_at: str | |
| class MCPSpaceFinder: | |
| def __init__(self): | |
| self.api = HfApi() | |
| self.all_mcp_spaces_cache = None # Cache for ALL MCP spaces | |
| self.running_spaces_cache = None # Separate cache for running/building spaces | |
| self.cache_timestamp = None | |
| self.cache_duration = 300 # 5 minutes cache | |
| def get_space_status(self, space_id: str) -> str: | |
| """Get the current runtime status of a space.""" | |
| try: | |
| runtime = self.api.get_space_runtime(space_id) | |
| if hasattr(runtime, 'stage'): | |
| return runtime.stage | |
| return "unknown" | |
| except Exception: | |
| return "error" | |
| def process_space_batch(self, spaces_batch) -> List[MCPSpace]: | |
| """Process a batch of spaces to get their runtime status.""" | |
| processed_spaces = [] | |
| for space in spaces_batch: | |
| try: | |
| space_id = space.id | |
| # Get space status -- an expensive operation (separate request per space) | |
| status = self.get_space_status(space_id) | |
| processed_space = MCPSpace( | |
| id=space_id, | |
| title=getattr(space, 'title', space_id.split('/')[-1]) or space_id.split('/')[-1], | |
| author=space.author, | |
| likes=getattr(space, 'likes', 0), | |
| status=status, | |
| url=f"https://huggingface.co/spaces/{space_id}", | |
| description=getattr(space, 'description', 'No description available') or 'No description available', | |
| sdk=getattr(space, 'sdk', 'unknown'), | |
| last_modified=str(getattr(space, 'last_modified', 'unknown')), | |
| created_at=str(getattr(space, 'created_at', 'unknown')) | |
| ) | |
| processed_spaces.append(processed_space) | |
| except Exception as e: | |
| print(f"Error processing space {getattr(space, 'id', 'unknown')}: {e}") | |
| continue | |
| return processed_spaces | |
| def find_all_mcp_spaces(self, check_status: bool = False) -> List[MCPSpace]: | |
| """ | |
| Find ALL MCP-enabled spaces, optionally checking their runtime status. | |
| Args: | |
| check_status: If True, fetch runtime status (slower). If False, return all spaces without status check. | |
| Returns: | |
| List of MCPSpace objects | |
| """ | |
| # Check cache first | |
| if not check_status and self.all_mcp_spaces_cache is not None: | |
| if self.cache_timestamp and time.time() - self.cache_timestamp < self.cache_duration: | |
| return self.all_mcp_spaces_cache | |
| if check_status and self.running_spaces_cache is not None: | |
| if self.cache_timestamp and time.time() - self.cache_timestamp < self.cache_duration: | |
| return self.running_spaces_cache | |
| print("Fetching ALL MCP spaces from HuggingFace Hub...") | |
| # Get ALL spaces with the mcp-server tag | |
| # Using limit=None or a very high limit to ensure we get everything | |
| try: | |
| # First try with no limit (gets all) | |
| spaces = list(list_spaces( | |
| filter="mcp-server", | |
| sort="likes", | |
| direction=-1, | |
| limit=None, # Get ALL spaces | |
| full=True | |
| )) | |
| except Exception as e: | |
| print(f"Failed with limit=None, trying with high limit: {e}") | |
| # Fallback to high limit if None doesn't work | |
| spaces = list(list_spaces( | |
| filter="mcp-server", | |
| sort="likes", | |
| direction=-1, | |
| limit=10000, # Very high limit to ensure we get all the relevant spaces | |
| full=True | |
| )) | |
| print(f"Found {len(spaces)} total spaces with mcp-server tag") | |
| if not check_status: | |
| # Quick mode: Don't check runtime status, just return basic info | |
| all_spaces = [] | |
| for space in spaces: | |
| try: | |
| processed_space = MCPSpace( | |
| id=space.id, | |
| title=getattr(space, 'title', space.id.split('/')[-1]) or space.id.split('/')[-1], | |
| author=space.author, | |
| likes=getattr(space, 'likes', 0), | |
| status="not_checked", # We're not checking status in quick mode | |
| url=f"https://huggingface.co/spaces/{space.id}", | |
| description=getattr(space, 'description', 'No description available') or 'No description available', | |
| sdk=getattr(space, 'sdk', 'unknown'), | |
| last_modified=str(getattr(space, 'last_modified', 'unknown')), | |
| created_at=str(getattr(space, 'created_at', 'unknown')) | |
| ) | |
| all_spaces.append(processed_space) | |
| except Exception as e: | |
| print(f"Error processing space: {e}") | |
| continue | |
| # Sort by likes | |
| all_spaces.sort(key=lambda x: x.likes, reverse=True) | |
| # Cache the results | |
| self.all_mcp_spaces_cache = all_spaces | |
| self.cache_timestamp = time.time() | |
| return all_spaces | |
| else: | |
| # Full mode: Check runtime status -- will be slow | |
| print("Checking runtime status for spaces...") | |
| # Process spaces in batches using ThreadPoolExecutor for status checking | |
| batch_size = 50 | |
| space_batches = [spaces[i:i + batch_size] for i in range(0, len(spaces), batch_size)] | |
| all_processed_spaces = [] | |
| with ThreadPoolExecutor(max_workers=5) as executor: | |
| future_to_batch = { | |
| executor.submit(self.process_space_batch, batch): batch | |
| for batch in space_batches | |
| } | |
| for future in as_completed(future_to_batch): | |
| try: | |
| batch_results = future.result() | |
| all_processed_spaces.extend(batch_results) | |
| except Exception as e: | |
| print(f"Error processing batch: {e}") | |
| # Filter to only include running or building spaces if desired | |
| running_building_spaces = [ | |
| space for space in all_processed_spaces | |
| if space.status in ["RUNNING", "RUNNING_BUILDING", "BUILDING"] | |
| ] | |
| # Sort by likes descending | |
| running_building_spaces.sort(key=lambda x: x.likes, reverse=True) | |
| # Debug: Count by status | |
| status_counts = {} | |
| for space in all_processed_spaces: | |
| status_counts[space.status] = status_counts.get(space.status, 0) + 1 | |
| print(f"Status breakdown: {status_counts}") | |
| print(f"Found {len(running_building_spaces)} running/building spaces out of {len(all_processed_spaces)} total") | |
| # Cache the results | |
| self.running_spaces_cache = running_building_spaces | |
| self.cache_timestamp = time.time() | |
| return running_building_spaces | |
| def find_mcp_spaces(self, limit: int = None, only_running: bool = False) -> List[MCPSpace]: | |
| """ | |
| Backward compatible method that can either: | |
| 1. Get ALL MCP spaces (default behavior, matching SA) | |
| 2. Get only running/building spaces (original FA behavior) | |
| Args: | |
| limit: Optional limit on number of spaces to return | |
| only_running: If True, only return running/building spaces (requires status check) | |
| """ | |
| if only_running: | |
| spaces = self.find_all_mcp_spaces(check_status=True) | |
| else: | |
| spaces = self.find_all_mcp_spaces(check_status=False) | |
| if limit and limit < len(spaces): | |
| return spaces[:limit] | |
| return spaces | |
| # Initialize the finder | |
| finder = MCPSpaceFinder() | |
| def get_mcp_spaces_list(): | |
| """Get the list of ALL MCP spaces for the dropdown.""" | |
| # Get ALL spaces, not just running ones (matching SA behavior) | |
| spaces = finder.find_mcp_spaces(only_running=False) | |
| # Create options for dropdown - format as username/spacename | |
| options = [] | |
| for space in spaces: | |
| label = space.id # This is already in format "username/spacename" | |
| options.append((label, space.id)) | |
| return options, len(spaces) | |
| def display_space_info(space_id): | |
| """Display detailed information about the selected space.""" | |
| if not space_id: | |
| return "Please select a space from the dropdown." | |
| # First check the all spaces cache | |
| spaces = finder.all_mcp_spaces_cache or finder.find_mcp_spaces(only_running=False) | |
| selected_space = next((s for s in spaces if s.id == space_id), None) | |
| if not selected_space: | |
| return f"Space {space_id} not found in cache." | |
| # Get fresh status if not already checked | |
| if selected_space.status == "not_checked": | |
| selected_space.status = finder.get_space_status(space_id) | |
| # Get status emoji and description | |
| status_emoji = "π’" if selected_space.status in ["RUNNING", "RUNNING_BUILDING"] else "π‘" if selected_space.status in ["BUILDING"] else "π΄" | |
| status_description = { | |
| "RUNNING": "Ready to use", | |
| "RUNNING_BUILDING": "Running (rebuilding)", | |
| "BUILDING": "Building - please wait", | |
| "STOPPED": "Stopped/Sleeping", | |
| "PAUSED": "Paused", | |
| "error": "Error getting status", | |
| "unknown": "Status unknown", | |
| "not_checked": "Status not checked" | |
| }.get(selected_space.status, selected_space.status) | |
| # Format the information | |
| info = f""" | |
| # {selected_space.title} | |
| **Author:** {selected_space.author} | |
| **Likes:** β€οΈ {selected_space.likes} | |
| **Status:** {status_emoji} {status_description} | |
| **SDK:** {selected_space.sdk} | |
| **Created:** {selected_space.created_at} | |
| **Last Modified:** {selected_space.last_modified} | |
| **URL:** [{selected_space.url}]({selected_space.url}) | |
| **Description:** | |
| {selected_space.description} | |
| --- | |
| ## π§ MCP Configuration | |
| ### For VSCode/Cursor/Claude Code (Recommended) | |
| ```json | |
| {{ | |
| "servers": {{ | |
| "{selected_space.id.replace('/', '-')}": {{ | |
| "url": "{selected_space.url}/gradio_api/mcp/sse" | |
| }} | |
| }} | |
| }} | |
| ``` | |
| ### For Claude Desktop | |
| ```json | |
| {{ | |
| "mcpServers": {{ | |
| "{selected_space.id.replace('/', '-')}": {{ | |
| "command": "npx", | |
| "args": [ | |
| "mcp-remote", | |
| "{selected_space.url}/gradio_api/mcp/sse" | |
| ] | |
| }} | |
| }} | |
| }} | |
| ``` | |
| ### Alternative: Use HF MCP Space Server | |
| ```json | |
| {{ | |
| "mcpServers": {{ | |
| "hf-spaces": {{ | |
| "command": "npx", | |
| "args": [ | |
| "-y", | |
| "@llmindset/mcp-hfspace", | |
| "{selected_space.id}" | |
| ] | |
| }} | |
| }} | |
| }} | |
| ``` | |
| --- | |
| **Note:** {status_description}{"" if selected_space.status in ["RUNNING", "RUNNING_BUILDING"] else " - The space may need to be started before use."} | |
| """.strip() | |
| return info | |
| def show_all_spaces(filter_running: bool = False): | |
| """ | |
| Display information about MCP spaces. | |
| Args: | |
| filter_running: If True, only show running/building spaces. If False, show all. | |
| """ | |
| spaces = finder.find_mcp_spaces(only_running=filter_running) | |
| total_spaces = len(spaces) | |
| # Create summary markdown | |
| filter_text = "Running/Building" if filter_running else "All" | |
| summary = f""" | |
| # π {filter_text} MCP Servers Summary | |
| **Available MCP Servers:** {total_spaces} {"(running or building)" if filter_running else "(all statuses)"} | |
| **Sorted by:** Popularity (likes descending) | |
| Browse all MCP servers: [https://huggingface.co/spaces?filter=mcp-server](https://huggingface.co/spaces?filter=mcp-server) | |
| --- | |
| """ | |
| # Create DataFrame data | |
| df_data = [] | |
| for i, space in enumerate(spaces, 1): | |
| if filter_running: | |
| status_emoji = "π’" if space.status == "RUNNING" else "π‘" if space.status == "RUNNING_BUILDING" else "πΆ" | |
| else: | |
| status_emoji = "β" # Unknown status for non-filtered view | |
| desc_short = (space.description[:80] + "...") if len(space.description) > 80 else space.description | |
| df_data.append([ | |
| i, # Rank | |
| space.id, # Show as username/spacename format | |
| space.author, | |
| space.likes, | |
| f"{status_emoji} {space.status if filter_running else 'Not checked'}", | |
| desc_short, | |
| f"[π Open]({space.url})" # Clickable link | |
| ]) | |
| return summary, df_data | |
| # Create the Gradio interface with toggle for showing all vs running spaces | |
| def create_interface(): | |
| with gr.Blocks(title="HuggingFace MCP Server Browser") as demo: | |
| gr.Markdown("# π€ HuggingFace MCP Server Browser") | |
| gr.Markdown("Discover **ALL Model Context Protocol (MCP)** servers on HuggingFace Spaces") | |
| with gr.Row(): | |
| with gr.Column(scale=3): | |
| # Get initial options and count | |
| initial_options, total_count = get_mcp_spaces_list() | |
| dropdown = gr.Dropdown( | |
| choices=initial_options, | |
| label=f"π€ MCP Servers ({total_count} total available)", | |
| info="All MCP servers on HuggingFace, sorted by popularity", | |
| value=None | |
| ) | |
| with gr.Column(scale=1): | |
| refresh_btn = gr.Button("π Refresh", variant="secondary") | |
| filter_toggle = gr.Checkbox(label="Show only running", value=False) | |
| summary_btn = gr.Button("π Show All", variant="primary") | |
| # Use Markdown for better formatting instead of Textbox | |
| output_md = gr.Markdown( | |
| value=f""" | |
| ## Welcome! | |
| Select an MCP server above to view configuration details. | |
| **Total MCP servers found:** {total_count} | |
| **Browse all:** [https://huggingface.co/spaces?filter=mcp-server](https://huggingface.co/spaces?filter=mcp-server) | |
| βΉοΈ **Note:** This browser now shows ALL MCP spaces by default. Enable "Show only running" to filter for active spaces (slower). | |
| """, | |
| visible=True | |
| ) | |
| # Add DataFrame for clean table display | |
| output_df = gr.DataFrame( | |
| headers=["Rank", "Space ID", "Author", "Likes", "Status", "Description", "Link"], | |
| datatype=["number", "str", "str", "number", "str", "str", "markdown"], | |
| visible=False, | |
| wrap=True, | |
| ) | |
| # Event handlers | |
| def handle_dropdown_change(space_id): | |
| if space_id: | |
| info = display_space_info(space_id) | |
| return gr.Markdown(value=info, visible=True), gr.DataFrame(visible=False) | |
| return gr.Markdown(visible=True), gr.DataFrame(visible=False) | |
| def handle_show_all(filter_running): | |
| summary, df_data = show_all_spaces(filter_running) | |
| return gr.Markdown(value=summary, visible=True), gr.DataFrame(value=df_data, visible=True) | |
| def handle_refresh(filter_running): | |
| # Clear cache to force refresh | |
| finder.all_mcp_spaces_cache = None | |
| finder.running_spaces_cache = None | |
| finder.cache_timestamp = None | |
| if filter_running: | |
| # This will be slower as it checks status | |
| options, total_count = [(s.id, s.id) for s in finder.find_mcp_spaces(only_running=True)], len(finder.find_mcp_spaces(only_running=True)) | |
| label_text = f"π€ Running MCP Servers ({total_count} available)" | |
| else: | |
| options, total_count = get_mcp_spaces_list() | |
| label_text = f"π€ MCP Servers ({total_count} total available)" | |
| return ( | |
| gr.Dropdown(choices=options, value=None, label=label_text), | |
| gr.Markdown(value=f""" | |
| ## Welcome! | |
| Select an MCP server above to view configuration details. | |
| **MCP servers found:** {total_count} | |
| **Filter:** {"Running/Building only" if filter_running else "All spaces"} | |
| **Browse all:** [https://huggingface.co/spaces?filter=mcp-server](https://huggingface.co/spaces?filter=mcp-server) | |
| """, visible=True), | |
| gr.DataFrame(visible=False) | |
| ) | |
| dropdown.change( | |
| fn=handle_dropdown_change, | |
| inputs=[dropdown], | |
| outputs=[output_md, output_df] | |
| ) | |
| summary_btn.click( | |
| fn=handle_show_all, | |
| inputs=[filter_toggle], | |
| outputs=[output_md, output_df] | |
| ) | |
| refresh_btn.click( | |
| fn=handle_refresh, | |
| inputs=[filter_toggle], | |
| outputs=[dropdown, output_md, output_df] | |
| ) | |
| filter_toggle.change( | |
| fn=handle_refresh, | |
| inputs=[filter_toggle], | |
| outputs=[dropdown, output_md, output_df] | |
| ) | |
| return demo | |
| # Create and launch the interface | |
| if __name__ == "__main__": | |
| demo = create_interface() | |
| demo.launch(mcp_server=True) |