onshape-mcp / app.py
X-R-1-8's picture
Fix: launch uvicorn ASGI server on port 7860 with non-blocking browser install
e4badc2 verified
Raw History Blame Contribute Delete
5.78 kB
"""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)