"""Hugging Face Space Application for Onshape MCP. Provides both: 1. An interactive Gradio web playground at `/` 2. A full Model Context Protocol (MCP) SSE endpoint at `/sse` for ChatGPT, Claude, and Cursor. """ from __future__ import annotations import asyncio import json import os import subprocess import sys from pathlib import Path # Add src to sys.path sys.path.insert(0, str(Path(__file__).parent / "src")) import gradio as gr from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware # Background task to ensure Playwright chromium is downloaded without blocking startup import threading def _background_install() -> None: try: subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"], check=False) except Exception: pass threading.Thread(target=_background_install, daemon=True).start() # ZeroGPU compatibility (no-op decorator so ZeroGPU hardware remains active without burning quota) try: import spaces @spaces.GPU def _gpu_keepalive() -> str: return "GPU active" except Exception: def _gpu_keepalive() -> str: return "CPU fallback" # Import Onshape MCP server from onshape_mcp.server import mcp from onshape_mcp.intent import parse as parse_intent from onshape_mcp.tools import ALL_TOOLS # 1. Build Gradio Interface def test_prompt(user_prompt: str) -> str: """Preview how Onshape MCP parses a prompt into CAD tools.""" if not user_prompt.strip(): return "Please enter a CAD prompt (e.g., 'Draw a 10cm by 5cm box on the Top plane')." try: plan = parse_intent(user_prompt) if plan: return json.dumps( { "status": "Recognized Deterministic CAD Actions", "action_count": len(plan), "actions": [ {"tool": tool, "args": args} for tool, args in plan ], }, indent=2, ) else: return json.dumps( { "status": "Complex Multi-Step Goal", "handler": "Autonomous Vision Loop (act)", "note": "Will be executed closed-loop in Onshape.", }, indent=2, ) except Exception as e: return f"Error parsing prompt: {e}" def list_tools() -> str: """Return available sketch tools catalog.""" tools_summary = [] for tool in ALL_TOOLS: tools_summary.append({ "name": tool.name, "purpose": tool.purpose, "requires": tool.requires, "produces": tool.produces, "status": tool.status, }) return json.dumps(tools_summary, indent=2) with gr.Blocks(title="Onshape MCP โ€” Autonomous CAD AI", theme=gr.themes.Soft()) as demo: gr.Markdown( """ # ๐Ÿ“ Onshape MCP โ€” Autonomous CAD Agent ### Control Onshape directly from ChatGPT, Claude, Cursor, and any MCP-compatible AI. """ ) with gr.Row(): with gr.Column(scale=2): gr.Markdown( """ ### ๐Ÿ”— Connect This Space to Your AI Client **MCP SSE Endpoint:** ```text https://x-r-1-8-onshape-mcp.hf.space/sse ``` * **ChatGPT Web**: In Custom GPTs or MCP Settings, add the SSE URL above. * **Claude Desktop**: Add this block to `claude_desktop_config.json`: ```json { "mcpServers": { "onshape": { "url": "https://x-r-1-8-onshape-mcp.hf.space/sse" } } } ``` * **Cursor / LibreChat**: Add as a remote SSE MCP server. """ ) with gr.Column(scale=1): gr.Markdown( """ ### โš™๏ธ Space Status * **Hardware**: ZeroGPU (RTX PRO 6000) * **Status**: ๐ŸŸข Server Live * **Sketch Tools**: 35+ Tools & 13 Constraints * **Protocol**: Model Context Protocol (2024-11-05) """ ) gr.Markdown("---") gr.Markdown("### ๐Ÿงช Test CAD Natural Language Parser") with gr.Row(): with gr.Column(): prompt_input = gr.Textbox( label="CAD Prompt", placeholder="e.g. Draw a 10cm by 5cm box on the Top plane and extrude 20mm", lines=2, value="Draw a 10cm by 5cm box on the Top plane", ) btn_run = gr.Button("Parse Intent Plan", variant="primary") with gr.Column(): output_json = gr.Code(label="Parsed Deterministic Plan", language="json") btn_run.click(fn=test_prompt, inputs=[prompt_input], outputs=[output_json]) with gr.Accordion("๐Ÿ“š View Full CAD Tools Catalog (35+ Tools)", open=False): tools_viewer = gr.Code(value=list_tools(), language="json", lines=15) # 2. Create Parent FastAPI App app = FastAPI() # Enable CORS for web clients app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 3. Mount FastMCP SSE App at / sse_app = mcp.sse_app() app.mount("/mcp", sse_app) # Also expose direct /sse and /messages routes at root for route in sse_app.routes: app.routes.append(route) # 4. Mount Gradio interface at root app = gr.mount_gradio_app(app, demo, path="/") if __name__ == "__main__": import uvicorn port = int(os.environ.get("PORT", "7860")) uvicorn.run(app, host="0.0.0.0", port=port)