KarianaUMCP / app.py
barlowski's picture
Upload folder using huggingface_hub
8aeaf22 verified
#!/usr/bin/env python3
"""
Kariana UMCP - Unified Gradio Dashboard for Unreal Engine MCP
=============================================================
HuggingFace Spaces Entry Point
MCP 1st Birthday Hackathon Submission
Track: MCP in Action | Category: Multimodal + Productivity
"""
import gradio as gr
import json
import socket
import logging
from typing import Dict, List, Optional, Any
from datetime import datetime
from dataclasses import dataclass
import os
import re
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# =============================================================================
# CONNECTION & DISCOVERY
# =============================================================================
@dataclass
class UnrealInstance:
"""Represents a discovered Unreal Engine instance"""
host: str
port: int
instance_id: Optional[str] = None
project_name: Optional[str] = None
version: Optional[str] = None
authenticated: bool = False
# Global state
_connected_instance: Optional[UnrealInstance] = None
_session_token: Optional[str] = None
def parse_connection_string(conn_str: str) -> tuple:
"""Parse connection string like 'localhost:9877' or 'tcp://0.tcp.ngrok.io:12345'"""
conn_str = conn_str.strip()
# Remove protocol prefix if present
if conn_str.startswith("tcp://"):
conn_str = conn_str[6:]
elif conn_str.startswith("http://"):
conn_str = conn_str[7:]
elif conn_str.startswith("https://"):
conn_str = conn_str[8:]
# Default values
host = "localhost"
port = 9877
# Parse host:port
if ":" in conn_str:
parts = conn_str.rsplit(":", 1)
host = parts[0]
try:
port = int(parts[1])
except ValueError:
pass
elif conn_str:
host = conn_str
return host, port
def send_socket_command(host: str, port: int, command: dict, timeout: float = 10.0) -> Optional[dict]:
"""Send command to KarianaUMCP socket server"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
sock.connect((host, port))
message = json.dumps(command) + "\n"
sock.send(message.encode('utf-8'))
response = sock.recv(16384).decode('utf-8')
sock.close()
return json.loads(response.strip())
except socket.timeout:
logger.warning(f"Timeout connecting to {host}:{port}")
return {"success": False, "error": f"Connection timeout to {host}:{port}"}
except ConnectionRefusedError:
logger.warning(f"Connection refused at {host}:{port}")
return {"success": False, "error": f"Connection refused at {host}:{port}. Is Unreal running?"}
except socket.gaierror as e:
logger.warning(f"DNS resolution failed for {host}: {e}")
return {"success": False, "error": f"Cannot resolve host: {host}"}
except Exception as e:
logger.error(f"Socket error: {e}")
return {"success": False, "error": str(e)}
def test_connection(host: str, port: int) -> tuple:
"""Test connection to a KarianaUMCP instance"""
response = send_socket_command(host, port, {"type": "get_server_info"}, timeout=5.0)
if response and (response.get("success") or response.get("server") == "KarianaUMCP"):
return True, response
return False, response
def validate_pin(pin: str, host: str, port: int) -> tuple:
"""Validate PIN against the instance"""
global _connected_instance, _session_token
if not pin or len(pin) != 4 or not pin.isdigit():
return False, "Invalid PIN format (must be 4 digits)"
response = send_socket_command(host, port, {"type": "validate_pin", "pin": pin})
if response and response.get("success"):
_connected_instance = UnrealInstance(
host=host,
port=port,
instance_id=response.get("instance_id"),
project_name=response.get("project"),
authenticated=True
)
_session_token = response.get("session_token")
return True, f"Connected to {response.get('project', 'Unreal')} at {host}:{port}"
error = response.get("error", "Invalid PIN") if response else "Connection failed"
return False, error
def get_connection_status() -> tuple:
"""Get current connection status"""
if _connected_instance and _connected_instance.authenticated:
return True, _connected_instance
return False, None
def send_command(command_type: str, **params) -> Optional[dict]:
"""Send command to connected instance"""
if not _connected_instance:
return {"success": False, "error": "Not connected"}
command = {"type": command_type, **params}
return send_socket_command(_connected_instance.host, _connected_instance.port, command)
# =============================================================================
# TAB: SETUP
# =============================================================================
def create_setup_tab():
"""Create Setup & Connection tab"""
# Plugin version and download URLs
PLUGIN_VERSION = "1.0.1"
# Dropbox download links (GitHub repo is private)
DOWNLOAD_URLS = {
"windows": "https://www.dropbox.com/scl/fi/y4zfyfkadmik2sma5jkwp/KarianaUMCP-v1.0.1-windows.zip?rlkey=u0wk39o1av7mnflcnhfu7cij1&st=nvuj5kaj&dl=0",
"macos": "https://www.dropbox.com/scl/fi/gydzrcjujtf14c743s07m/KarianaUMCP-v1.0.1-macos.zip?rlkey=239u0ltp17s665nzg08pkatzs&st=5hs5wokz&dl=0",
"linux": "https://www.dropbox.com/scl/fi/za196pymwr8l0wgamdact/KarianaUMCP-v1.0.1-linux.zip?rlkey=eq37irxfif6hlahkxtdygjiqo&st=j5q2p9yh&dl=0"
}
def get_download_url(platform: str) -> str:
return DOWNLOAD_URLS.get(platform, "#")
with gr.Column():
# ===== STEP 1: PLUGIN DOWNLOAD =====
gr.Markdown("## Step 1: Install the Plugin")
gr.Markdown("*Download and install the KarianaUMCP plugin into your Unreal project*")
with gr.Accordion("Download Plugin", open=True):
gr.Markdown(f"""
### Download for Your Platform
| Platform | Download Link |
|----------|---------------|
| **Windows** | [KarianaUMCP-v{PLUGIN_VERSION}-windows.zip]({get_download_url('windows')}) |
| **macOS** | [KarianaUMCP-v{PLUGIN_VERSION}-macos.zip]({get_download_url('macos')}) |
| **Linux** | [KarianaUMCP-v{PLUGIN_VERSION}-linux.zip]({get_download_url('linux')}) |
### Installation Steps
1. **Download** the ZIP file for your platform
2. **Extract** the contents to: `YourProject/Plugins/KarianaUMCP/`
3. **Open** your Unreal project
4. **Enable** Python Script Plugin (Edit > Plugins > Python)
5. **Restart** Unreal Editor
6. **Look for the PIN** in the Output Log (Window > Output Log)
The PIN will appear as a prominent banner like:
```
============================================================
KARIANA UMCP - CONNECTION PIN: 1234
PORT: 9877
============================================================
```
""")
gr.Markdown("---")
# ===== STEP 2: CONNECTION =====
gr.Markdown("## Step 2: Connect to Unreal Engine")
with gr.Accordion("Connection Options", open=True):
gr.Markdown("""
### How to Connect
**Option A: Local Network (Same Computer/LAN)**
- If Unreal is on the same network, use your computer's IP address
- Find your IP: `ipconfig` (Windows) or `ifconfig` (Mac/Linux)
- Example: `192.168.1.100:9877`
**Option B: Remote Access via ngrok (Recommended)**
1. Install ngrok: https://ngrok.com/download
2. In terminal, run: `ngrok tcp 9877`
3. Copy the forwarding URL (e.g., `0.tcp.ngrok.io:12345`)
4. Paste it below
""")
with gr.Group():
gr.Markdown("### Enter Connection Details")
with gr.Row():
connection_url = gr.Textbox(
label="Unreal Engine Address",
placeholder="e.g., 192.168.1.100:9877 or 0.tcp.ngrok.io:12345",
info="Enter your computer's IP:port or ngrok URL",
scale=3
)
test_btn = gr.Button("Test Connection", variant="secondary", scale=1)
connection_test_result = gr.Markdown("")
with gr.Row():
pin_input = gr.Textbox(
label="Connection PIN",
placeholder="Enter 4-digit PIN from Unreal",
max_lines=1,
scale=2
)
connect_btn = gr.Button("Connect", variant="primary", size="lg", scale=1)
connection_result = gr.Markdown("")
gr.Markdown("---")
# ===== CONNECTION STATUS =====
gr.Markdown("### Connection Status")
with gr.Row():
status_indicator = gr.Markdown("**Status:** Not connected")
refresh_status_btn = gr.Button("Refresh", size="sm")
server_info_display = gr.JSON(label="Server Info", value={})
# Event handlers
def handle_test_connection(url):
if not url or not url.strip():
return "**Please enter a connection URL**"
# Detect localhost and warn user - this won't work from cloud!
url_lower = url.lower().strip()
if "localhost" in url_lower or "127.0.0.1" in url_lower:
return """**'localhost' won't work from this dashboard!**
This dashboard runs on HuggingFace servers, not your computer.
'localhost' refers to the HuggingFace server, not your Mac/PC.
**You need ngrok to expose your local Unreal port:**
1. Open Terminal and run: `ngrok tcp 9877`
2. Copy the URL (e.g., `0.tcp.ngrok.io:12345`)
3. Paste that ngrok URL here instead of localhost
Your Unreal PIN stays the same - just change the URL!"""
host, port = parse_connection_string(url)
success, response = test_connection(host, port)
if success:
project = response.get("project", "Unknown")
version = response.get("version", "1.0.0")
return f"**Connection successful!** Found KarianaUMCP server.\n\nProject: {project} | Version: {version}\n\n*Now enter your PIN and click Connect*"
else:
error = response.get("error", "Unknown error") if isinstance(response, dict) else str(response)
return f"**Connection failed:** {error}\n\n*Make sure Unreal is running with the plugin enabled*"
def handle_connect(url, pin):
if not url or not url.strip():
return "**Please enter a connection URL first**"
if not pin or not pin.strip():
return "**Please enter the PIN from Unreal's Output Log**"
host, port = parse_connection_string(url)
success, message = validate_pin(pin.strip(), host, port)
if success:
return f"**Connected!** {message}"
return f"**Connection failed:** {message}"
def handle_refresh_status():
connected, instance = get_connection_status()
if connected:
info = send_command("get_server_info") or {}
project = instance.project_name or "Unknown"
return f"**Status:** Connected to {project} ({instance.host}:{instance.port})", info
return "**Status:** Not connected", {}
test_btn.click(handle_test_connection, inputs=[connection_url], outputs=[connection_test_result])
connect_btn.click(handle_connect, inputs=[connection_url, pin_input], outputs=[connection_result])
refresh_status_btn.click(handle_refresh_status, outputs=[status_indicator, server_info_display])
return {"pin_input": pin_input, "connection_url": connection_url}
# =============================================================================
# TAB: MONITORING
# =============================================================================
def create_monitoring_tab():
"""Create Monitoring tab"""
with gr.Column():
gr.Markdown("## Real-Time Monitoring")
with gr.Row():
status_display = gr.Markdown("**Status:** Not connected")
refresh_btn = gr.Button("Refresh", size="sm")
with gr.Row():
with gr.Column():
ping_btn = gr.Button("Ping", size="sm")
ping_result = gr.Textbox(label="Ping Result", interactive=False)
with gr.Column():
server_info = gr.JSON(label="Server Info", value={})
gr.Markdown("---")
gr.Markdown("### Unreal Engine Logs")
with gr.Row():
log_limit = gr.Slider(minimum=10, maximum=200, value=50, step=10, label="Max Lines")
fetch_logs_btn = gr.Button("Fetch Logs", size="sm")
logs_display = gr.TextArea(label="Log Output", lines=12, interactive=False)
# Event handlers
def handle_ping():
start = datetime.now()
response = send_command("ping")
elapsed = (datetime.now() - start).total_seconds() * 1000
if response and response.get("success"):
return f"Pong! Response time: {elapsed:.1f}ms"
error = response.get("error", "Unknown") if response else "Not connected"
return f"Ping failed: {error}"
def handle_refresh():
connected, instance = get_connection_status()
if connected:
info = send_command("get_server_info") or {}
return f"**Status:** Connected to {instance.project_name} (Port {instance.port})", info
return "**Status:** Not connected", {}
def handle_fetch_logs(limit):
response = send_command("get_ue_logs", limit=int(limit))
if response and response.get("success"):
logs = response.get("logs", [])
return "\n".join(logs) if logs else "No logs available"
error = response.get("error", "Unknown") if response else "Not connected"
return f"Failed to fetch logs: {error}"
ping_btn.click(handle_ping, outputs=[ping_result])
refresh_btn.click(handle_refresh, outputs=[status_display, server_info])
fetch_logs_btn.click(handle_fetch_logs, inputs=[log_limit], outputs=[logs_display])
return {}
# =============================================================================
# TAB: SCENE CONTROL
# =============================================================================
def create_scene_tab():
"""Create Scene Control tab"""
with gr.Column():
gr.Markdown("## Scene Control")
with gr.Row():
# Actor list
with gr.Column(scale=1):
gr.Markdown("### Actors")
refresh_actors_btn = gr.Button("Refresh Actor List", size="sm")
actor_list = gr.Dataframe(
headers=["Name", "Type"],
datatype=["str", "str"],
interactive=False,
row_count=(15, "dynamic")
)
# Quick actions
with gr.Column(scale=1):
gr.Markdown("### Quick Actions")
with gr.Group():
actor_type = gr.Dropdown(
label="Actor Type",
choices=["Cube", "Sphere", "Cylinder", "PointLight", "SpotLight", "Camera"],
value="Cube"
)
actor_name = gr.Textbox(label="Name", placeholder="MyActor")
with gr.Row():
loc_x = gr.Number(label="X", value=0)
loc_y = gr.Number(label="Y", value=0)
loc_z = gr.Number(label="Z", value=0)
spawn_btn = gr.Button("Spawn Actor", variant="primary")
spawn_result = gr.Markdown("")
gr.Markdown("---")
gr.Markdown("### Screenshot")
with gr.Row():
capture_btn = gr.Button("Capture Screenshot", size="sm")
screenshot_result = gr.Markdown("")
# Event handlers
def handle_refresh_actors():
response = send_command("list_actors")
if response and response.get("success"):
actors = response.get("actors", [])
if isinstance(actors, list) and actors and isinstance(actors[0], dict):
return [[a.get("name", "?"), a.get("type", a.get("class", "?"))] for a in actors]
elif isinstance(actors, list):
return [[str(a), "Actor"] for a in actors]
error = response.get("error", "Unknown") if response else "Not connected"
return [[error, "—"]]
def handle_spawn(atype, name, x, y, z):
if not name:
return "**Enter an actor name**"
response = send_command("spawn_actor", actor_type=atype, name=name, location=[x, y, z])
if response and response.get("success"):
return f"**Spawned {atype}: {name}**"
error = response.get("error", "Unknown") if response else "Not connected"
return f"**Failed:** {error}"
def handle_capture():
response = send_command("capture_screenshot")
if response and response.get("success"):
return f"**Screenshot saved:** {response.get('path', 'Check Unreal project folder')}"
error = response.get("error", "Unknown") if response else "Not connected"
return f"**Failed to capture screenshot:** {error}"
refresh_actors_btn.click(handle_refresh_actors, outputs=[actor_list])
spawn_btn.click(handle_spawn, inputs=[actor_type, actor_name, loc_x, loc_y, loc_z], outputs=[spawn_result])
capture_btn.click(handle_capture, outputs=[screenshot_result])
return {}
# =============================================================================
# TAB: SKILLS & AGENTS
# =============================================================================
AGENTS = [
("Scene Analyst", "Transform natural language into scene specifications"),
("Asset Scout", "Discover and catalog project assets"),
("Pattern Strategist", "Select spatial generation patterns"),
("Executor", "Execute generation strategies via MCP"),
("World Organizer", "Automate World Outliner organization"),
("Orchestrator", "Coordinate multiple agents"),
("MCP Log Monitor", "Diagnose MCP connection issues"),
("Project Organizer", "Organize project structure"),
]
def create_skills_tab():
"""Create Skills & Agents tab"""
with gr.Column():
gr.Markdown("## Skills & Claude Agents")
with gr.Row():
# Skills
with gr.Column(scale=1):
gr.Markdown("### Available Skills")
refresh_skills_btn = gr.Button("Refresh Skills", size="sm")
skills_list = gr.Dataframe(
headers=["Name", "Description"],
datatype=["str", "str"],
interactive=False
)
gr.Markdown("### Execute Skill")
skill_name = gr.Textbox(label="Skill Name")
skill_params = gr.Code(label="Parameters (JSON)", language="json", value="{}")
execute_skill_btn = gr.Button("Execute", variant="primary")
skill_result = gr.TextArea(label="Result", lines=6, interactive=False)
# Agents
with gr.Column(scale=1):
gr.Markdown("### Claude Agents")
gr.Markdown("*Specialized AI agents for complex tasks*")
agents_table = gr.Dataframe(
headers=["Agent", "Purpose"],
datatype=["str", "str"],
value=[[a[0], a[1]] for a in AGENTS],
interactive=False
)
gr.Markdown("""
**How to use agents:**
1. Connect to Unreal via PIN
2. Use Claude Code or Claude Desktop
3. Ask Claude to "activate the [Agent Name] agent"
4. Describe your task
""")
# Event handlers
def handle_refresh_skills():
response = send_command("list_skills")
if response and response.get("success"):
skills = response.get("skills", [])
return [[s.get("name", "?"), s.get("description", "")[:50]] for s in skills]
return [["Not connected", "—"]]
def handle_execute_skill(name, params_json):
if not name:
return "Enter a skill name"
try:
params = json.loads(params_json) if params_json.strip() else {}
except json.JSONDecodeError as e:
return f"Invalid JSON: {e}"
response = send_command("execute_skill", skill_name=name, params=params)
if response:
return json.dumps(response, indent=2)
return "Failed - not connected"
refresh_skills_btn.click(handle_refresh_skills, outputs=[skills_list])
execute_skill_btn.click(handle_execute_skill, inputs=[skill_name, skill_params], outputs=[skill_result])
return {}
# =============================================================================
# TAB: CLAUDE DESKTOP
# =============================================================================
def create_claude_desktop_tab():
"""Create Claude Desktop configuration tab"""
with gr.Column():
gr.Markdown("## Claude Desktop Integration")
gr.Markdown("*Connect Claude Desktop to control Unreal Engine via MCP*")
with gr.Accordion("How It Works", open=True):
gr.Markdown("""
### Architecture
```
Claude Desktop → umcp-mcp-server.js → ngrok tunnel → Your Unreal Engine
```
Claude Desktop uses our custom MCP server (`umcp-mcp-server.js`) which connects
to your Unreal Engine via ngrok. This gives Claude full control over the scene!
""")
gr.Markdown("---")
gr.Markdown("### Generate Configuration")
with gr.Row():
ngrok_host = gr.Textbox(
label="ngrok Host",
placeholder="e.g., 6.tcp.eu.ngrok.io",
info="The host from your ngrok tunnel URL"
)
ngrok_port = gr.Textbox(
label="ngrok Port",
placeholder="e.g., 19404",
info="The port from your ngrok tunnel URL"
)
with gr.Row():
mcp_server_path = gr.Textbox(
label="Path to umcp-mcp-server.js",
placeholder="/path/to/Kariana-UMCP/umcp-mcp-server.js",
info="Full path to the MCP server script on your machine"
)
generate_btn = gr.Button("Generate Config", variant="primary")
config_output = gr.Code(
label="claude_desktop_config.json",
language="json",
lines=20
)
gr.Markdown("""
### Installation Steps
1. **Copy the configuration** above
2. **Open your Claude Desktop config file:**
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows:** `%APPDATA%\\Claude\\claude_desktop_config.json`
3. **Paste the configuration** (merge with existing if needed)
4. **Restart Claude Desktop** completely (quit and reopen)
5. **Look for the MCP icon** in Claude Desktop (bottom right)
6. **Test the connection:** Ask Claude "Can you use handshake to test the Unreal connection?"
""")
# Event handler
def generate_config(host, port, server_path):
if not host or not port:
return '{"error": "Please enter ngrok host and port"}'
if not server_path:
server_path = "/path/to/Kariana-UMCP/umcp-mcp-server.js"
config = {
"mcpServers": {
"umcp-unreal": {
"command": "node",
"args": [server_path],
"env": {
"UMCP_HOST": host.strip(),
"UMCP_PORT": port.strip()
}
}
}
}
return json.dumps(config, indent=2)
generate_btn.click(
generate_config,
inputs=[ngrok_host, ngrok_port, mcp_server_path],
outputs=[config_output]
)
return {}
# =============================================================================
# TAB: TEST TOOLS
# =============================================================================
def create_test_tools_tab():
"""Create Test Tools tab for MCP tool testing"""
with gr.Column():
gr.Markdown("## MCP Tool Testing")
gr.Markdown("*Test each MCP tool to verify Unreal Engine connection*")
with gr.Row():
run_all_btn = gr.Button("Run All Tests", variant="primary", size="lg")
test_results = gr.Dataframe(
headers=["Tool", "Status", "Response Time", "Details"],
datatype=["str", "str", "str", "str"],
value=[
["ping", "—", "—", "Not tested"],
["get_server_info", "—", "—", "Not tested"],
["list_functions", "—", "—", "Not tested"],
["list_actors", "—", "—", "Not tested"],
["spawn_actor", "—", "—", "Not tested"],
["delete_actor", "—", "—", "Not tested"],
],
interactive=False
)
gr.Markdown("---")
gr.Markdown("### Individual Tool Test")
with gr.Row():
with gr.Column():
tool_select = gr.Dropdown(
label="Select Tool",
choices=[
"ping", "get_server_info", "list_functions",
"list_actors", "spawn_actor", "delete_actor",
"capture_screenshot", "list_skills"
],
value="ping"
)
tool_params = gr.Code(
label="Parameters (JSON)",
language="json",
value="{}",
lines=5
)
test_single_btn = gr.Button("Test Tool")
with gr.Column():
single_result = gr.JSON(label="Result", value={})
# Event handlers
def run_all_tests():
results = []
tests = [
("ping", {}),
("get_server_info", {}),
("list_functions", {}),
("list_actors", {}),
]
for tool_name, params in tests:
start = datetime.now()
try:
response = send_command(tool_name, **params)
elapsed = (datetime.now() - start).total_seconds() * 1000
if response and (response.get("success") or response.get("server")):
status = "PASS"
details = str(response)[:50] + "..." if len(str(response)) > 50 else str(response)
else:
status = "FAIL"
details = response.get("error", "Unknown error") if response else "No response"
except Exception as e:
elapsed = 0
status = "ERROR"
details = str(e)[:50]
results.append([tool_name, status, f"{elapsed:.1f}ms", details])
# Add untested items
results.append(["spawn_actor", "SKIP", "—", "Manual test only"])
results.append(["delete_actor", "SKIP", "—", "Manual test only"])
return results
def test_single_tool(tool_name, params_json):
try:
params = json.loads(params_json) if params_json.strip() else {}
except json.JSONDecodeError as e:
return {"error": f"Invalid JSON: {e}"}
response = send_command(tool_name, **params)
return response or {"error": "No response - not connected"}
run_all_btn.click(run_all_tests, outputs=[test_results])
test_single_btn.click(
test_single_tool,
inputs=[tool_select, tool_params],
outputs=[single_result]
)
return {}
# =============================================================================
# TAB: WORKFLOW BUILDER
# =============================================================================
WORKFLOW_ACTIONS = ["spawn_actor", "delete_actor", "execute_python", "console_command", "capture_screenshot", "wait"]
def create_workflow_tab():
"""Create Workflow Builder tab"""
with gr.Column():
gr.Markdown("## Workflow Builder")
gr.Markdown("*Create reusable multi-step automation workflows*")
with gr.Row():
with gr.Column(scale=2):
workflow_name = gr.Textbox(label="Workflow Name", placeholder="my_workflow")
gr.Markdown("### Add Step")
with gr.Group():
action_select = gr.Dropdown(label="Action", choices=WORKFLOW_ACTIONS, value="spawn_actor")
param1 = gr.Textbox(label="Parameter 1")
param2 = gr.Textbox(label="Parameter 2")
param3 = gr.Textbox(label="Parameter 3")
add_step_btn = gr.Button("Add Step")
steps_json = gr.Code(label="Workflow Steps", language="json", value="[]", lines=10)
with gr.Row():
clear_steps_btn = gr.Button("Clear All", size="sm")
with gr.Column(scale=1):
gr.Markdown("### Execute")
execute_workflow_btn = gr.Button("Run Workflow", variant="primary", size="lg")
execution_log = gr.TextArea(label="Execution Log", lines=15, interactive=False)
gr.Markdown("### Templates")
template_lighting = gr.Button("3-Point Lighting", size="sm")
template_scatter = gr.Button("Scatter Objects", size="sm")
# Event handlers
def add_step(action, p1, p2, p3, current):
try:
steps = json.loads(current) if current else []
except (json.JSONDecodeError, ValueError):
steps = []
steps.append({"action": action, "params": {"p1": p1, "p2": p2, "p3": p3}})
return json.dumps(steps, indent=2)
def execute_workflow(name, steps_json):
try:
steps = json.loads(steps_json) if steps_json else []
except (json.JSONDecodeError, ValueError):
return "Invalid workflow JSON"
if not steps:
return "No steps to execute"
log = [f"Executing: {name or 'Unnamed'}", f"Steps: {len(steps)}", "---"]
for i, step in enumerate(steps):
action = step.get("action", "unknown")
log.append(f"[{i+1}] {action}")
params = step.get("params", {})
if action == "spawn_actor":
response = send_command("spawn_actor", actor_type="Cube", name=params.get("p1", f"WF_{i}"))
log.append(f" -> {'OK' if response and response.get('success') else 'FAIL'}")
elif action == "wait":
import time
time.sleep(float(params.get("p1", 1)))
log.append(" -> Waited")
else:
log.append(" -> Skipped (demo mode)")
log.append("---")
log.append("Done!")
return "\n".join(log)
def apply_template(template):
if template == "lighting":
return json.dumps([
{"action": "spawn_actor", "params": {"p1": "KeyLight", "p2": "PointLight", "p3": ""}},
{"action": "spawn_actor", "params": {"p1": "FillLight", "p2": "PointLight", "p3": ""}},
{"action": "spawn_actor", "params": {"p1": "BackLight", "p2": "PointLight", "p3": ""}},
], indent=2)
else:
return json.dumps([
{"action": "spawn_actor", "params": {"p1": "Scatter_1", "p2": "Cube", "p3": ""}},
{"action": "spawn_actor", "params": {"p1": "Scatter_2", "p2": "Cube", "p3": ""}},
{"action": "spawn_actor", "params": {"p1": "Scatter_3", "p2": "Cube", "p3": ""}},
], indent=2)
add_step_btn.click(add_step, inputs=[action_select, param1, param2, param3, steps_json], outputs=[steps_json])
clear_steps_btn.click(lambda: "[]", outputs=[steps_json])
execute_workflow_btn.click(execute_workflow, inputs=[workflow_name, steps_json], outputs=[execution_log])
template_lighting.click(lambda: apply_template("lighting"), outputs=[steps_json])
template_scatter.click(lambda: apply_template("scatter"), outputs=[steps_json])
return {}
# =============================================================================
# TAB: INTRO
# =============================================================================
def create_intro_tab():
"""Create Intro/Landing tab"""
with gr.Column():
# Hero + feature card section
gr.Markdown(
"""
<section class="kariana-container kariana-hero-wrapper">
<div class="kariana-hero-inner">
<div>
<h1 class="kariana-hero-title">
AI Assistant for <span class="em">Unreal Engine</span> that powers<br/>
<span class="highlight">Training</span>
</h1>
<p class="kariana-hero-subtitle">
Your AI copilot for Unreal Engine development in non-gaming industries—complete with scene generation, Blueprint automation, and asset optimization.
</p>
<div class="kariana-hero-actions">
<button class="kariana-btn-primary">Book a demo</button>
<button class="kariana-btn-secondary">View capabilities</button>
</div>
<p class="kariana-hero-note">MCP 1st Birthday Hackathon · Track: MCP in Action</p>
</div>
<div class="kariana-hero-card">
<div class="kariana-hero-card-header">
<span>Share your project with KARIANA...</span>
<span class="kariana-pill-success">Blueprint ready</span>
</div>
<p style="font-size: 0.82rem; color: #9ca3af; margin-bottom: 0.6rem;">Upload your level, materials, and scene assets—then describe the walkthrough you want to build.</p>
<p style="font-size: 0.8rem; color: #d1d5db; opacity: 0.9;">This live demo connects to your Unreal Engine via the KarianaUMCP plugin, exposing 280+ MCP tools for scene control and automation.</p>
</div>
</div>
</section>
"""
)
# Tealy-style 3-step explainer
gr.Markdown(
"""
<section class="kariana-container kariana-steps-wrapper">
<div class="kariana-steps-inner">
<div class="kariana-steps-canvas">
<div class="kariana-steps-canvas-icons">
<div class="kariana-steps-icon">✶</div>
<div class="kariana-steps-icon">◎</div>
<div class="kariana-steps-icon">DB</div>
</div>
<div class="kariana-steps-unreal-card">
<div class="kariana-steps-unreal-logo">U</div>
<div class="kariana-steps-unreal-text">
Unreal Engine becomes the execution engine for your AI assistants.
KARIANA connects MCP tools directly into your scene.
</div>
</div>
</div>
<div class="kariana-steps-list">
<article class="kariana-step-card">
<div class="kariana-step-badge">1</div>
<div>
<h4>Connect Your AI Model</h4>
<p>Choose your preferred AI: Claude, OpenAI (ChatGPT), or a local LLM. Online for power, offline for security—your choice, your control.</p>
</div>
</article>
<article class="kariana-step-card">
<div class="kariana-step-badge">2</div>
<div>
<h4>Prompt What You Want</h4>
<p>Natural language commands like "Create 50 vehicle variants with different materials" or "Generate a Blueprint for our ADAS camera system". Just describe it.</p>
</div>
</article>
<article class="kariana-step-card">
<div class="kariana-step-badge">3</div>
<div>
<h4>KARIANA Executes Your Vision</h4>
<p>280+ specialized tools automatically execute workflows inside Unreal Engine. Three days of manual work becomes ten minutes. Professional results, instantly.</p>
</div>
</article>
</div>
</div>
</section>
"""
)
# Use Cases & Features Sections
gr.Markdown(
"""
<!-- USE CASES -->
<section class="kariana-container kariana-usecases-wrapper">
<div class="kariana-usecases-grid">
<div class="kariana-usecase-card">
<div class="kariana-usecase-icon icon-pink">🏢</div>
<h3 class="kariana-usecase-title">Architecture & Real Estate</h3>
<p class="kariana-usecase-desc">Create photorealistic walkthroughs, interactive floor plans, and client presentations in minutes.</p>
<ul class="kariana-usecase-list">
<li><span class="check-pink">✓</span> Virtual property tours</li>
<li><span class="check-pink">✓</span> Real-time material swaps</li>
<li><span class="check-pink">✓</span> Lighting simulation</li>
</ul>
</div>
<div class="kariana-usecase-card">
<div class="kariana-usecase-icon icon-blue">🚗</div>
<h3 class="kariana-usecase-title">Automotive Design</h3>
<p class="kariana-usecase-desc">Visualize vehicle designs, configure interiors, and create marketing assets with speed.</p>
<ul class="kariana-usecase-list">
<li><span class="check-pink">✓</span> 360° configurators</li>
<li><span class="check-pink">✓</span> Assembly animations</li>
<li><span class="check-pink">✓</span> Showroom experiences</li>
</ul>
</div>
<div class="kariana-usecase-card">
<div class="kariana-usecase-icon icon-pink">🏥</div>
<h3 class="kariana-usecase-title">Medical & Healthcare</h3>
<p class="kariana-usecase-desc">Build surgical simulations, anatomical visualizations, and patient education experiences.</p>
<ul class="kariana-usecase-list">
<li><span class="check-pink">✓</span> Surgical training</li>
<li><span class="check-pink">✓</span> Anatomical models</li>
<li><span class="check-pink">✓</span> Procedure walkthroughs</li>
</ul>
</div>
<div class="kariana-usecase-card">
<div class="kariana-usecase-icon icon-blue">🎓</div>
<h3 class="kariana-usecase-title">Training & Simulation</h3>
<p class="kariana-usecase-desc">Develop immersive training scenarios for industrial, military, and educational applications.</p>
<ul class="kariana-usecase-list">
<li><span class="check-pink">✓</span> Safety training</li>
<li><span class="check-pink">✓</span> Equipment operation</li>
<li><span class="check-pink">✓</span> Scenario simulation</li>
</ul>
</div>
</div>
</section>
<!-- FEATURES GRID -->
<section class="kariana-container kariana-features-wrapper">
<div class="kariana-features-grid">
<div class="kariana-feature-card feat-darkblue">
<h3>Actor Management</h3>
<p>Spawn, organize, and manipulate actors at scale. Bulk operations, naming conventions, and hierarchical structures.</p>
<div class="feature-img-placeholder">📦</div>
</div>
<div class="kariana-feature-card feat-teal">
<h3>Blueprint System</h3>
<p>Generate and modify Blueprints through natural language. Event graphs, functions, variables, and interfaces.</p>
<div class="feature-img-placeholder">⚡️</div>
</div>
<div class="kariana-feature-card feat-orange">
<h3>PCG Automation</h3>
<p>Procedural Content Generation workflows. Landscapes, foliage, and parametric architecture.</p>
<div class="feature-img-placeholder">🌲</div>
</div>
<div class="kariana-feature-card feat-yellow">
<h3>Material Pipeline</h3>
<p>Create and modify master materials, material instances. PBR workflows and shader optimization.</p>
<div class="feature-img-placeholder">🎨</div>
</div>
<div class="kariana-feature-card feat-purple">
<h3>Lighting & Rendering</h3>
<p>Automated lighting setup, bake optimization, post-process configuration, and render settings management.</p>
<div class="feature-img-placeholder">💡</div>
</div>
<div class="kariana-feature-card feat-lightteal">
<h3>Physics Configuration</h3>
<p>Set up collision, physics bodies, constraints, and simulation parameters for accurate behavior.</p>
<div class="feature-img-placeholder">⚛️</div>
</div>
<div class="kariana-feature-card feat-darkorange">
<h3>Project Organization</h3>
<p>Intelligent folder structures, asset naming conventions, and project-wide refactoring tools.</p>
<div class="feature-img-placeholder">📂</div>
</div>
<div class="kariana-feature-card feat-darkgrey">
<h3>Blender Integration</h3>
<p>60+ Blender tools with seamless UE bridge. Model preparation, LOD generation, and asset automation.</p>
<div class="feature-img-placeholder">🧊</div>
</div>
<div class="kariana-feature-card feat-lightyellow">
<h3>Compliance Support</h3>
<p>Documentation generation, audit trails, and configuration management for ISO 26262 and AS9100.</p>
<div class="feature-img-placeholder">📋</div>
</div>
</div>
</section>
"""
)
return {}
# =============================================================================
# MAIN APP
# =============================================================================
def create_app():
"""Create the main Gradio application"""
# Kariana brand colors from kariana.base44.app
kariana_theme = gr.themes.Soft(
primary_hue=gr.themes.Color(
c50="#e6f2f2", c100="#b3d9d9", c200="#80bfbf",
c300="#4da6a6", c400="#1a8c8c", c500="#1a5a5a",
c600="#155050", c700="#104545", c800="#0b3b3b",
c900="#063030", c950="#031818"
),
secondary_hue="slate",
neutral_hue="slate"
).set(
button_primary_background_fill="#1a5a5a",
button_primary_background_fill_hover="#155050",
block_title_text_color="#1e3a5f",
body_background_fill="#f8f9fa"
)
# Kariana CSS for custom components
kariana_css = """
/* Kariana Brand Colors */
:root {
--kariana-teal: #1a5a5a;
--kariana-navy: #1e3a5f;
--kariana-orange: #d4a574;
--kariana-cyan: #4a9a9a;
--kariana-pink: #e91e8c;
}
/* Hero Section */
.kariana-hero-wrapper { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); padding: 2rem; border-radius: 12px; margin-bottom: 1.5rem; }
.kariana-hero-title { color: #1e3a5f; font-size: 2rem; font-weight: 700; line-height: 1.2; }
.kariana-hero-title .em { color: #1a5a5a; }
.kariana-hero-title .highlight { background: linear-gradient(90deg, #d4a574, #e91e8c); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.kariana-hero-subtitle { color: #4a5568; font-size: 1.1rem; margin: 1rem 0; }
.kariana-hero-note { color: #718096; font-size: 0.85rem; margin-top: 1rem; }
/* Buttons */
.kariana-btn-primary { background: #1a5a5a !important; color: white !important; padding: 0.75rem 1.5rem; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
.kariana-btn-primary:hover { background: #155050 !important; }
.kariana-btn-secondary { background: transparent !important; color: #1a5a5a !important; border: 2px solid #1a5a5a !important; padding: 0.75rem 1.5rem; border-radius: 8px; font-weight: 600; cursor: pointer; }
/* Pills/Badges */
.kariana-pill-success { background: #4a9a9a; color: white; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.75rem; font-weight: 500; }
/* Hero Card */
.kariana-hero-card { background: #1e3a5f; color: white; padding: 1.5rem; border-radius: 12px; }
.kariana-hero-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; }
/* Steps Section */
.kariana-steps-wrapper { padding: 2rem 0; }
.kariana-step-card { background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 1.25rem; margin-bottom: 1rem; }
.kariana-step-badge { background: #1a5a5a; color: white; width: 2rem; height: 2rem; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; margin-bottom: 0.75rem; }
/* Use Case Cards */
.kariana-usecases-wrapper { padding: 2rem 0; }
.kariana-usecases-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; }
.kariana-usecase-card { background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 1.5rem; }
.kariana-usecase-icon { font-size: 2rem; margin-bottom: 0.75rem; }
.kariana-usecase-icon.icon-pink { background: linear-gradient(135deg, #fce7f3, #fbcfe8); padding: 0.5rem; border-radius: 8px; display: inline-block; }
.kariana-usecase-icon.icon-blue { background: linear-gradient(135deg, #dbeafe, #bfdbfe); padding: 0.5rem; border-radius: 8px; display: inline-block; }
.kariana-usecase-title { color: #1e3a5f; font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem; }
.kariana-usecase-desc { color: #4a5568; font-size: 0.9rem; margin-bottom: 0.75rem; }
.kariana-usecase-list { color: #718096; font-size: 0.85rem; padding-left: 1.25rem; }
/* Feature Cards - matching kariana.base44.app colors */
.kariana-features-wrapper { padding: 2rem 0; }
.kariana-features-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; }
.kariana-feature-card { padding: 1.25rem; border-radius: 12px; color: white; }
.kariana-feature-card h4 { font-size: 1rem; font-weight: 600; margin-bottom: 0.5rem; }
.kariana-feature-card p { font-size: 0.85rem; opacity: 0.9; }
/* Feature card colors from kariana.base44.app */
.feat-darkblue, .feat-teal { background: #1a5a5a; }
.feat-orange { background: #d4a574; color: #1e3a5f; }
.feat-yellow, .feat-lightyellow { background: #d4a574; color: #1e3a5f; }
.feat-purple { background: #1e3a5f; }
.feat-lightteal { background: #4a9a9a; }
.feat-darkorange { background: #c49464; color: #1e3a5f; }
.feat-darkgrey { background: #4a5568; }
/* Unreal Card in Steps */
.kariana-steps-unreal-card { background: #1e3a5f; color: white; padding: 1rem; border-radius: 8px; display: inline-flex; align-items: center; gap: 0.75rem; }
.kariana-steps-unreal-logo { background: white; color: #1e3a5f; width: 2rem; height: 2rem; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-weight: 700; }
/* Container */
.kariana-container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; }
"""
with gr.Blocks(title="Kariana UMCP", theme=kariana_theme, css=kariana_css) as demo:
gr.Markdown("""
# Kariana UMCP
**AI-Powered Virtual Production Control for Unreal Engine**
*MCP 1st Birthday Hackathon | Track: MCP in Action*
[🔗 Learn more about this project →](https://kariana.base44.app)
""")
with gr.Row():
connection_badge = gr.Markdown("**Not Connected** - Go to Setup tab to connect")
with gr.Tabs():
with gr.Tab("Intro"):
create_intro_tab()
with gr.Tab("Setup"):
create_setup_tab()
with gr.Tab("Monitor"):
create_monitoring_tab()
with gr.Tab("Scene"):
create_scene_tab()
with gr.Tab("Skills"):
create_skills_tab()
with gr.Tab("Workflow"):
create_workflow_tab()
with gr.Tab("Claude Desktop"):
create_claude_desktop_tab()
with gr.Tab("Test Tools"):
create_test_tools_tab()
gr.Markdown("""
---
**Links:** [GitHub](https://github.com/kerinzeebart/MCPTest-with-UMCP.it) |
[MCP Hackathon](https://huggingface.co/MCP-1st-Birthday)
*Built with Gradio, FastMCP, and Unreal Engine 5.6*
""")
return demo
# Launch
demo = create_app()
if __name__ == "__main__":
demo.launch()