import { NextRequest, NextResponse } from "next/server"; import { RepoDesignation, createRepo, uploadFiles, spaceInfo, listCommits } from "@huggingface/hub"; import { COLORS } from "@/lib/utils"; import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge"; import { Commit, Page } from "@/types"; // Timeout configuration (in milliseconds) const OPERATION_TIMEOUT = 120000; // 2 minutes for HF operations // Extend the maximum execution time for this route export const maxDuration = 180; // 3 minutes // Utility function to wrap promises with timeout async function withTimeout( promise: Promise, timeoutMs: number, errorMessage: string = "Operation timed out" ): Promise { let timeoutId: NodeJS.Timeout; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(errorMessage)); }, timeoutMs); }); try { const result = await Promise.race([promise, timeoutPromise]); clearTimeout(timeoutId!); return result; } catch (error) { clearTimeout(timeoutId!); throw error; } } interface MCPRequest { jsonrpc: "2.0"; id: number | string; method: string; params?: any; } interface MCPResponse { jsonrpc: "2.0"; id: number | string; result?: any; error?: { code: number; message: string; data?: any; }; } interface CreateProjectParams { title?: string; pages: Page[]; prompt?: string; hf_token?: string; // Optional - can come from header instead } // MCP Server over HTTP export async function POST(req: NextRequest) { try { const body: MCPRequest = await req.json(); const { jsonrpc, id, method, params } = body; // Validate JSON-RPC 2.0 format if (jsonrpc !== "2.0") { return NextResponse.json({ jsonrpc: "2.0", id: id || null, error: { code: -32600, message: "Invalid Request: jsonrpc must be '2.0'", }, }); } let response: MCPResponse; switch (method) { case "initialize": response = { jsonrpc: "2.0", id, result: { protocolVersion: "2024-11-05", capabilities: { tools: {}, }, serverInfo: { name: "deepsite-mcp-server", version: "1.0.0", }, }, }; break; case "tools/list": response = { jsonrpc: "2.0", id, result: { tools: [ { name: "create_project", description: `Create a new DeepSite project. This will create a new Hugging Face Space with your HTML/CSS/JS files. Example usage: - Create a simple website with HTML, CSS, and JavaScript files - Each page needs a 'path' (filename like "index.html", "styles.css", "script.js") and 'html' (the actual content) - The title will be formatted to a valid repository name - Returns the project URL and metadata`, inputSchema: { type: "object", properties: { title: { type: "string", description: "Project title (optional, defaults to 'DeepSite Project'). Will be formatted to a valid repo name.", }, pages: { type: "array", description: "Array of files to include in the project", items: { type: "object", properties: { path: { type: "string", description: "File path (e.g., 'index.html', 'styles.css', 'script.js')", }, html: { type: "string", description: "File content", }, }, required: ["path", "html"], }, }, prompt: { type: "string", description: "Optional prompt/description for the commit message", }, hf_token: { type: "string", description: "Hugging Face API token (optional if provided via Authorization header)", }, }, required: ["pages"], }, }, ], }, }; break; case "tools/call": const { name, arguments: toolArgs } = params; if (name === "create_project") { try { // Extract token from Authorization header if present const authHeader = req.headers.get("authorization"); let hf_token = toolArgs.hf_token; if (authHeader && authHeader.startsWith("Bearer ")) { hf_token = authHeader.substring(7); // Remove "Bearer " prefix } const result = await handleCreateProject({ ...toolArgs, hf_token, } as CreateProjectParams); response = { jsonrpc: "2.0", id, result, }; } catch (error: any) { response = { jsonrpc: "2.0", id, error: { code: -32000, message: error.message || "Failed to create project", data: error.data, }, }; } } else { response = { jsonrpc: "2.0", id, error: { code: -32601, message: `Unknown tool: ${name}`, }, }; } break; default: response = { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}`, }, }; } return NextResponse.json(response); } catch (error: any) { return NextResponse.json({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error", data: error.message, }, }); } } // Handle OPTIONS for CORS export async function OPTIONS() { return new NextResponse(null, { status: 200, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", }, }); } async function handleCreateProject(params: CreateProjectParams) { const { title: titleFromRequest, pages, prompt, hf_token } = params; // Validate required parameters if (!hf_token || typeof hf_token !== "string") { throw new Error("hf_token is required and must be a string"); } if (!pages || !Array.isArray(pages) || pages.length === 0) { throw new Error("At least one page is required"); } // Validate that each page has required fields for (const page of pages) { if (!page.path || !page.html) { throw new Error("Each page must have 'path' and 'html' properties"); } } // Get user info from HF token let username: string; try { const userResponse = await withTimeout( fetch("https://huggingface.co/api/whoami-v2", { headers: { Authorization: `Bearer ${hf_token}`, }, }), 30000, // 30 seconds for authentication "Authentication timeout: Unable to verify Hugging Face token" ); if (!userResponse.ok) { throw new Error("Invalid Hugging Face token"); } const userData = await userResponse.json(); username = userData.name; } catch (error: any) { if (error.message?.includes('timeout')) { throw new Error(`Authentication timeout: ${error.message}`); } throw new Error(`Authentication failed: ${error.message}`); } const title = titleFromRequest ?? "DeepSite Project"; const formattedTitle = title .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .split("-") .filter(Boolean) .join("-") .slice(0, 96); const repo: RepoDesignation = { type: "space", name: `${username}/${formattedTitle}`, }; const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)]; const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)]; const README = `--- title: ${title} colorFrom: ${colorFrom} colorTo: ${colorTo} emoji: 🐳 sdk: static pinned: false tags: - deepsite-v3 --- # Welcome to your new DeepSite project! This project was created with [DeepSite](https://huggingface.co/deepsite). `; const files: File[] = []; const readmeFile = new File([README], "README.md", { type: "text/markdown" }); files.push(readmeFile); pages.forEach((page: Page) => { // Determine MIME type based on file extension let mimeType = "text/html"; if (page.path.endsWith(".css")) { mimeType = "text/css"; } else if (page.path.endsWith(".js")) { mimeType = "text/javascript"; } else if (page.path.endsWith(".json")) { mimeType = "application/json"; } // Inject the DeepSite badge script into index pages only const content = mimeType === "text/html" && isIndexPage(page.path) ? injectDeepSiteBadge(page.html) : page.html; const file = new File([content], page.path, { type: mimeType }); files.push(file); }); try { const { repoUrl } = await withTimeout( createRepo({ repo, accessToken: hf_token, }), 60000, // 1 minute for repo creation "Timeout creating repository. Please try again." ); const commitTitle = !prompt || prompt.trim() === "" ? "Initial project creation via MCP" : prompt; await withTimeout( uploadFiles({ repo, files, accessToken: hf_token, commitTitle, }), OPERATION_TIMEOUT, "Timeout uploading files. The repository was created but files may not have been uploaded." ); const path = repoUrl.split("/").slice(-2).join("/"); const commits: Commit[] = []; const commitIterator = listCommits({ repo, accessToken: hf_token }); // Wrap the commit listing with a timeout const commitTimeout = new Promise((_, reject) => { setTimeout(() => reject(new Error("Timeout listing commits")), 30000); }); try { await Promise.race([ (async () => { for await (const commit of commitIterator) { if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Promote version")) { continue; } commits.push({ title: commit.title, oid: commit.oid, date: commit.date, }); } })(), commitTimeout ]); } catch (error: any) { // If listing commits times out, continue with empty commits array console.error("Failed to list commits:", error.message); } const space = await withTimeout( spaceInfo({ name: repo.name, accessToken: hf_token, }), 30000, // 30 seconds for space info "Timeout fetching space information" ); const projectUrl = `https://huggingface.co/deepsite/${path}`; const spaceUrl = `https://huggingface.co/spaces/${path}`; const liveUrl = `https://${username}-${formattedTitle}.hf.space`; return { content: [ { type: "text", text: JSON.stringify( { success: true, message: "Project created successfully!", projectUrl, spaceUrl, liveUrl, spaceId: space.name, projectId: space.id, files: pages.map((p) => p.path), updatedAt: space.updatedAt, }, null, 2 ), }, ], }; } catch (err: any) { if (err.message?.includes('timeout') || err.message?.includes('Timeout')) { throw new Error(err.message || "Operation timed out. Please try again."); } throw new Error(err.message || "Failed to create project"); } }