mcpuploadclient / app.py
cpuai's picture
Update app.py
a0076fe verified
import base64
import json
import mimetypes
import os
from dataclasses import dataclass
from typing import Any, Dict, Optional, Tuple
from urllib.parse import urlparse
import gradio as gr
import requests
CONFIG_FILE = "mcpuploadclient.config.json"
@dataclass(frozen=True)
class ClientConfig:
endpoint: str
access_token: str
request_timeout_seconds: int = 300
def load_config() -> ClientConfig:
config_path = os.path.join(os.getcwd(), CONFIG_FILE)
if not os.path.exists(config_path):
raise FileNotFoundError(
f"Config file not found: {config_path}. "
f"Create {CONFIG_FILE} next to app.py (working directory)."
)
with open(config_path, "r", encoding="utf-8") as f:
raw = json.load(f)
endpoint = raw.get("Endpoint") or raw.get("endpoint")
access_token = raw.get("AccessToken") or raw.get("access_token") or raw.get("Access_Token")
timeout = raw.get("RequestTimeoutSeconds") or raw.get("request_timeout_seconds") or 300
if not endpoint or not isinstance(endpoint, str):
raise ValueError("Config must include 'Endpoint' (string).")
if not access_token or not isinstance(access_token, str):
raise ValueError("Config must include 'AccessToken' (string).")
try:
timeout_int = int(timeout)
except Exception:
timeout_int = 300
return ClientConfig(endpoint=endpoint, access_token=access_token, request_timeout_seconds=timeout_int)
ALLOWED_EXTS = {".png", ".jpg", ".jpeg", ".webp"}
def _filename_from_url(image_url: str) -> str:
parsed = urlparse(image_url)
name = os.path.basename(parsed.path)
if not name:
raise ValueError(
"Image URL must end with a filename (e.g. https://.../foo.png). "
"URLs without a path filename are not supported."
)
return name
def _mime_from_filename(filename: str) -> str:
ext = os.path.splitext(filename)[1].lower()
if ext not in ALLOWED_EXTS:
raise ValueError(f"Unsupported image extension '{ext}'. Allowed: {sorted(ALLOWED_EXTS)}")
if ext == ".png":
return "image/png"
if ext in (".jpg", ".jpeg"):
return "image/jpeg"
if ext == ".webp":
return "image/webp"
guessed, _ = mimetypes.guess_type(filename)
return guessed or "application/octet-stream"
def _parse_first_sse_data_line(sse_text: str) -> Dict[str, Any]:
for line in sse_text.splitlines():
line = line.strip()
if not line:
continue
if line.startswith("data:"):
payload = line[5:].strip()
if not payload:
continue
return json.loads(payload)
raise ValueError("No SSE data lines found in server response.")
def mcp_upload(image_url: str, json_text: str) -> str:
image_url = (image_url or "").strip()
json_text = (json_text or "").strip()
if not image_url:
return "ERROR: image_url is required."
if not json_text:
return "ERROR: json_text is required."
try:
cfg = load_config()
except Exception as ex:
return f"ERROR: failed to load config: {ex}"
try:
image_file_name = _filename_from_url(image_url)
image_mime_type = _mime_from_filename(image_file_name)
base = os.path.splitext(image_file_name)[0]
json_file_name = base + ".json"
# Download image bytes
r = requests.get(image_url, timeout=cfg.request_timeout_seconds)
r.raise_for_status()
image_bytes = r.content
image_b64 = base64.b64encode(image_bytes).decode("ascii")
# Build JSON-RPC tool call
body = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "upload_image_and_json",
"arguments": {
"imageFileName": image_file_name,
"imageMimeType": image_mime_type,
"imageBase64": image_b64,
"jsonFileName": json_file_name,
"jsonText": json_text,
},
},
}
headers = {
"ProtocolVersion": "2025-06-18",
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json",
"Authorization": f"Bearer {cfg.access_token}",
}
resp = requests.post(
cfg.endpoint,
headers=headers,
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
timeout=cfg.request_timeout_seconds,
)
resp.raise_for_status()
rpc = _parse_first_sse_data_line(resp.text)
# Extract tool-returned text
tool_text = (
rpc.get("result", {})
.get("content", [{}])[0]
.get("text")
)
if not tool_text:
return f"ERROR: server response did not include result.content[0].text. Raw first data: {json.dumps(rpc, ensure_ascii=False)}"
return tool_text
except requests.RequestException as ex:
return f"ERROR: HTTP request failed: {ex}"
except Exception as ex:
return f"ERROR: {ex}"
with gr.Blocks(title="McpUploadServer Client") as demo:
gr.Markdown("# McpUploadServer Client (Gradio)\n\nEnter an **image URL** and a **JSON text**. Click Upload to send them to McpUploadServer via MCP.")
image_url = gr.Textbox(label="Image URL", placeholder="https://example.com/image.png")
json_text = gr.Textbox(label="JSON text", lines=12, placeholder='{"Category":"portrait", ... }')
upload_btn = gr.Button("Upload")
result = gr.Textbox(label="Result", lines=12)
upload_btn.click(fn=mcp_upload, inputs=[image_url, json_text], outputs=[result])
if __name__ == "__main__":
launch_kwargs = {
"server_port": int(os.environ.get("PORT", "7860")),
}
# NOTE: Setting server_name="0.0.0.0" can cause Gradio's internal startup check
# (GET /gradio_api/startup-events) to hang locally because it tries to connect
# to http://0.0.0.0:PORT, which isn't a routable address.
# HuggingFace Spaces typically sets GRADIO_SERVER_NAME=0.0.0.0 for you.
server_name = os.environ.get("GRADIO_SERVER_NAME")
if not server_name:
# Common HuggingFace Spaces environment variables. If we detect Spaces and the
# host isn't explicitly set, bind to 0.0.0.0 so the reverse proxy can reach us.
is_spaces = any(
os.environ.get(k)
for k in (
"SPACE_ID",
"SPACE_REPO_NAME",
"SPACE_AUTHOR_NAME",
"HF_SPACE_ID",
)
)
if is_spaces:
server_name = "0.0.0.0"
if server_name:
launch_kwargs["server_name"] = server_name
demo.launch(**launch_kwargs,mcp_server=True)