Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """ | |
| MCP Client Server that connects to Hugging Face Spaces API | |
| This acts as a bridge between Cursor and your Hugging Face Spaces server | |
| """ | |
| import json | |
| import asyncio | |
| import logging | |
| import aiohttp | |
| from typing import Any, Dict, List, Optional | |
| from mcp.server import Server | |
| from mcp.server.models import InitializationOptions | |
| from mcp.server.stdio import stdio_server | |
| from mcp.types import ( | |
| Resource, | |
| Tool, | |
| TextContent, | |
| LoggingLevel | |
| ) | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # Hugging Face Spaces URL - replace with your actual space URL | |
| HF_SPACE_URL = "https://galcan-mcp-docs-server.hf.space" | |
| # Initialize the MCP server | |
| server = Server("mcp-docs-client") | |
| async def make_request(endpoint: str, method: str = "GET", data: dict = None) -> dict: | |
| """Make HTTP request to Hugging Face Spaces API""" | |
| url = f"{HF_SPACE_URL}{endpoint}" | |
| try: | |
| async with aiohttp.ClientSession() as session: | |
| if method == "GET": | |
| async with session.get(url) as response: | |
| return await response.json() | |
| elif method == "POST": | |
| async with session.post(url, json=data) as response: | |
| return await response.json() | |
| except Exception as e: | |
| logger.error(f"Request failed: {e}") | |
| return {"error": str(e)} | |
| async def list_resources() -> List[Resource]: | |
| """List available documentation resources""" | |
| try: | |
| # Get docs from HF Spaces | |
| response = await make_request("/docs") | |
| if "error" in response: | |
| return [] | |
| resources = [] | |
| for doc in response.get("documents", []): | |
| resources.append(Resource( | |
| uri=f"mcp://docs/{doc.get('id', 'unknown')}", | |
| name=doc.get('title', 'Untitled'), | |
| description=doc.get('content', '')[:200] + "..." if len(doc.get('content', '')) > 200 else doc.get('content', ''), | |
| mimeType="text/plain" | |
| )) | |
| return resources | |
| except Exception as e: | |
| logger.error(f"Error listing resources: {e}") | |
| return [] | |
| async def read_resource(uri: str) -> str: | |
| """Read a specific documentation resource""" | |
| try: | |
| # Extract document ID from URI | |
| if uri.startswith("mcp://docs/"): | |
| doc_id = uri.replace("mcp://docs/", "") | |
| # Search for chunks related to this document | |
| search_response = await make_request("/search", "POST", { | |
| "query": doc_id, | |
| "limit": 10 | |
| }) | |
| if "error" in search_response: | |
| return f"Error: {search_response['error']}" | |
| results = search_response.get("results", []) | |
| if results: | |
| content = "\n\n".join([result.get("text", "") for result in results]) | |
| return content | |
| else: | |
| return f"Document {doc_id} not found" | |
| return "Invalid URI" | |
| except Exception as e: | |
| return f"Error reading resource: {e}" | |
| async def list_tools() -> List[Tool]: | |
| """List available tools""" | |
| return [ | |
| Tool( | |
| name="search_docs", | |
| description="Search through MCP documentation chunks on Hugging Face Spaces", | |
| inputSchema={ | |
| "type": "object", | |
| "properties": { | |
| "query": { | |
| "type": "string", | |
| "description": "Search query for MCP documentation" | |
| }, | |
| "limit": { | |
| "type": "integer", | |
| "description": "Maximum number of results", | |
| "default": 5 | |
| } | |
| }, | |
| "required": ["query"] | |
| } | |
| ), | |
| Tool( | |
| name="get_chunk", | |
| description="Get a specific documentation chunk by ID from Hugging Face Spaces", | |
| inputSchema={ | |
| "type": "object", | |
| "properties": { | |
| "chunk_id": { | |
| "type": "string", | |
| "description": "Chunk ID to retrieve" | |
| } | |
| }, | |
| "required": ["chunk_id"] | |
| } | |
| ), | |
| Tool( | |
| name="list_docs", | |
| description="List all available documents from Hugging Face Spaces", | |
| inputSchema={ | |
| "type": "object", | |
| "properties": {} | |
| } | |
| ), | |
| Tool( | |
| name="health_check", | |
| description="Check if the Hugging Face Spaces server is running", | |
| inputSchema={ | |
| "type": "object", | |
| "properties": {} | |
| } | |
| ) | |
| ] | |
| async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]: | |
| """Handle tool calls by forwarding to Hugging Face Spaces""" | |
| if name == "search_docs": | |
| query = arguments.get("query", "") | |
| limit = arguments.get("limit", 5) | |
| try: | |
| response = await make_request("/search", "POST", { | |
| "query": query, | |
| "limit": limit | |
| }) | |
| if "error" in response: | |
| return [TextContent(type="text", text=f"Error: {response['error']}")] | |
| results = response.get("results", []) | |
| total = response.get("total", 0) | |
| if results: | |
| response_text = f"Found {total} results for '{query}':\n\n" | |
| for i, result in enumerate(results, 1): | |
| response_text += f"{i}. **{result.get('title', 'Untitled')}**\n" | |
| response_text += f" {result.get('text', '')[:200]}...\n" | |
| response_text += f" Source: {result.get('filename', 'Unknown')}\n" | |
| if result.get('score'): | |
| response_text += f" Score: {result['score']}\n" | |
| response_text += "\n" | |
| else: | |
| response_text = f"No results found for '{query}'" | |
| return [TextContent(type="text", text=response_text)] | |
| except Exception as e: | |
| return [TextContent(type="text", text=f"Error searching: {e}")] | |
| elif name == "get_chunk": | |
| chunk_id = arguments.get("chunk_id", "") | |
| try: | |
| response = await make_request(f"/chunks/{chunk_id}") | |
| if "error" in response: | |
| return [TextContent(type="text", text=f"Error: {response['error']}")] | |
| if response: | |
| result_text = f"**{response.get('title', 'Untitled')}**\n\n" | |
| result_text += f"{response.get('text', '')}\n\n" | |
| result_text += f"Source: {response.get('filename', 'Unknown')}\n" | |
| result_text += f"URL: {response.get('url', 'N/A')}" | |
| return [TextContent(type="text", text=result_text)] | |
| else: | |
| return [TextContent(type="text", text=f"Chunk {chunk_id} not found")] | |
| except Exception as e: | |
| return [TextContent(type="text", text=f"Error getting chunk: {e}")] | |
| elif name == "list_docs": | |
| try: | |
| response = await make_request("/docs") | |
| if "error" in response: | |
| return [TextContent(type="text", text=f"Error: {response['error']}")] | |
| docs = response.get("documents", []) | |
| if docs: | |
| response_text = "Available documents:\n\n" | |
| for doc in docs: | |
| response_text += f"- **{doc.get('title', 'Untitled')}**\n" | |
| response_text += f" ID: {doc.get('id', 'Unknown')}\n" | |
| response_text += f" URL: {doc.get('url', 'N/A')}\n\n" | |
| else: | |
| response_text = "No documents available" | |
| return [TextContent(type="text", text=response_text)] | |
| except Exception as e: | |
| return [TextContent(type="text", text=f"Error listing docs: {e}")] | |
| elif name == "health_check": | |
| try: | |
| response = await make_request("/") | |
| if "error" in response: | |
| return [TextContent(type="text", text=f"Server error: {response['error']}")] | |
| status = response.get("status", "unknown") | |
| chunks_loaded = response.get("chunks_loaded", 0) | |
| docs_loaded = response.get("docs_loaded", 0) | |
| health_text = f"**Hugging Face Spaces Server Status**\n\n" | |
| health_text += f"Status: {status}\n" | |
| health_text += f"Chunks loaded: {chunks_loaded}\n" | |
| health_text += f"Documents loaded: {docs_loaded}\n" | |
| health_text += f"Server URL: {HF_SPACE_URL}" | |
| return [TextContent(type="text", text=health_text)] | |
| except Exception as e: | |
| return [TextContent(type="text", text=f"Health check failed: {e}")] | |
| else: | |
| return [TextContent(type="text", text=f"Unknown tool: {name}")] | |
| async def main(): | |
| """Main entry point""" | |
| logger.info(f"Starting MCP client server for {HF_SPACE_URL}") | |
| # Run the server | |
| async with stdio_server() as (read_stream, write_stream): | |
| await server.run( | |
| read_stream, | |
| write_stream, | |
| InitializationOptions( | |
| server_name="mcp-docs-client", | |
| server_version="1.0.0", | |
| capabilities=server.get_capabilities( | |
| notification_options=None, | |
| experimental_capabilities=None | |
| ) | |
| ) | |
| ) | |
| if __name__ == "__main__": | |
| asyncio.run(main()) | |