Skip to main content
Glama
index.js15 kB
#!/usr/bin/env node /** * Riddle MCP Server * * Provides screenshot and browser automation tools via MCP protocol. * Wraps the Riddle API (api.riddledc.com) for easy integration with Claude Code. * * Core tools: * - riddle_screenshot: Take screenshot of any URL * - riddle_batch_screenshot: Screenshot multiple URLs * - riddle_run_script: Run Playwright script (async) * - riddle_get_job: Check job status and get artifacts * - riddle_automate: Full sync automation with console logs and HAR * - riddle_click_and_screenshot: Simple click + screenshot */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { writeFileSync } from "fs"; const RIDDLE_API = "https://api.riddledc.com"; // Save image to /tmp and return path function saveToTmp(buffer, name) { const path = `/tmp/${name}.png`; writeFileSync(path, buffer); return path; } class RiddleClient { constructor() { this.apiKey = process.env.RIDDLE_API_KEY; if (!this.apiKey) { console.error("Warning: RIDDLE_API_KEY not set"); } } async screenshotSync(url, viewport) { const response = await fetch(`${RIDDLE_API}/v1/run`, { method: "POST", headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ url, viewport }), }); if (!response.ok) { throw new Error(`API error: ${response.status}`); } const buffer = await response.arrayBuffer(); return Buffer.from(buffer).toString("base64"); } async runScript(url, script, viewport) { const response = await fetch(`${RIDDLE_API}/v1/run`, { method: "POST", headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ url, script, viewport, sync: false }), }); if (!response.ok) { throw new Error(`API error: ${response.status}`); } return response.json(); } async getJob(jobId) { const response = await fetch(`${RIDDLE_API}/v1/jobs/${jobId}`, { headers: { Authorization: `Bearer ${this.apiKey}` }, }); return response.json(); } async getArtifacts(jobId) { const response = await fetch(`${RIDDLE_API}/v1/jobs/${jobId}/artifacts`, { headers: { Authorization: `Bearer ${this.apiKey}` }, }); return response.json(); } // Wait for job to complete async waitForJob(jobId, maxAttempts = 30, intervalMs = 2000) { for (let i = 0; i < maxAttempts; i++) { const job = await this.getJob(jobId); if (job.status === "completed" || job.status === "complete") { return job; } if (job.status === "failed") { throw new Error(`Job failed: ${job.error || "Unknown error"}`); } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } throw new Error("Job timed out"); } // Download artifact from URL async downloadArtifact(url) { const response = await fetch(url); if (!response.ok) throw new Error(`Download failed: ${response.status}`); return Buffer.from(await response.arrayBuffer()); } // Run script and wait for all artifacts (full automation) async runScriptSync(url, script, viewport, timeoutSec = 60) { const response = await fetch(`${RIDDLE_API}/v1/run`, { method: "POST", headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ url, script, viewport, sync: false, timeout_sec: timeoutSec, }), }); if (!response.ok) { const text = await response.text(); throw new Error(`API error ${response.status}: ${text}`); } const { job_id } = await response.json(); if (!job_id) throw new Error("No job_id returned"); // Wait for completion await this.waitForJob(job_id); // Get artifacts const artifacts = await this.getArtifacts(job_id); const results = []; let consoleLogs = null; let networkHar = null; for (const artifact of artifacts.artifacts || []) { if (artifact.url) { const buffer = await this.downloadArtifact(artifact.url); // Parse console.json if (artifact.name === "console.json") { try { consoleLogs = JSON.parse(buffer.toString("utf8")); } catch (e) {} } // Parse network.har else if (artifact.name === "network.har") { try { networkHar = JSON.parse(buffer.toString("utf8")); } catch (e) {} } results.push({ name: artifact.name, type: artifact.name?.endsWith(".png") ? "image" : "file", buffer, }); } } return { job_id, artifacts: results, consoleLogs, networkHar }; } } // Create server const server = new Server( { name: "riddle-mcp-server", version: "1.0.0" }, { capabilities: { tools: {} } } ); const client = new RiddleClient(); // Device presets const devices = { desktop: { width: 1280, height: 720 }, ipad: { width: 820, height: 1180 }, iphone: { width: 390, height: 844 }, }; // Define available tools server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "riddle_screenshot", description: "Take a screenshot of a URL using the Riddle API. Returns base64-encoded PNG image.", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL to screenshot" }, width: { type: "number", description: "Viewport width (default: 1280)", }, height: { type: "number", description: "Viewport height (default: 720)", }, device: { type: "string", enum: ["desktop", "ipad", "iphone"], description: "Device preset (overrides width/height)", }, }, required: ["url"], }, }, { name: "riddle_batch_screenshot", description: "Screenshot multiple URLs. Returns array of base64 images.", inputSchema: { type: "object", properties: { urls: { type: "array", items: { type: "string" }, description: "URLs to screenshot", }, device: { type: "string", enum: ["desktop", "ipad", "iphone"], }, }, required: ["urls"], }, }, { name: "riddle_run_script", description: "Run a Playwright script on a page (async). Returns job_id to check status later.", inputSchema: { type: "object", properties: { url: { type: "string", description: "Starting URL" }, script: { type: "string", description: "Playwright script (page object available)", }, width: { type: "number" }, height: { type: "number" }, }, required: ["url", "script"], }, }, { name: "riddle_get_job", description: "Get status and artifacts of a Riddle job", inputSchema: { type: "object", properties: { job_id: { type: "string", description: "Job ID to check" }, }, required: ["job_id"], }, }, { name: "riddle_automate", description: "Run a Playwright script, wait for completion, and return all artifacts. Includes console logs and network HAR. Full sync automation - one call does everything.", inputSchema: { type: "object", properties: { url: { type: "string", description: "Starting URL" }, script: { type: "string", description: "Playwright script. Use 'page' object. Example: await page.click('button'); await page.screenshot({path: 'result.png'});", }, device: { type: "string", enum: ["desktop", "ipad", "iphone"], description: "Device preset", }, timeout_sec: { type: "number", description: "Max execution time in seconds (default: 60)", }, }, required: ["url", "script"], }, }, { name: "riddle_click_and_screenshot", description: "Simple automation: load URL, click a selector, take screenshot. Good for testing button clicks, game starts, etc.", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL to load" }, click: { type: "string", description: "CSS selector to click (e.g., 'button.start', '.play-btn')", }, wait_ms: { type: "number", description: "Wait time after click before screenshot (default: 1000)", }, device: { type: "string", enum: ["desktop", "ipad", "iphone"], }, }, required: ["url", "click"], }, }, ], })); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { if (name === "riddle_screenshot") { const viewport = args.device ? devices[args.device] : { width: args.width || 1280, height: args.height || 720 }; const base64 = await client.screenshotSync(args.url, viewport); return { content: [ { type: "image", data: base64, mimeType: "image/png", }, ], }; } if (name === "riddle_batch_screenshot") { const viewport = args.device ? devices[args.device] : devices.desktop; const results = []; for (const url of args.urls) { try { const base64 = await client.screenshotSync(url, viewport); results.push({ url, success: true, image: base64 }); } catch (e) { results.push({ url, success: false, error: e.message }); } } return { content: [ { type: "text", text: JSON.stringify( results.map((r) => ({ url: r.url, success: r.success, error: r.error, })), null, 2 ), }, ...results .filter((r) => r.success) .map((r) => ({ type: "image", data: r.image, mimeType: "image/png", })), ], }; } if (name === "riddle_run_script") { const viewport = { width: args.width || 1280, height: args.height || 720 }; const result = await client.runScript(args.url, args.script, viewport); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } if (name === "riddle_get_job") { const job = await client.getJob(args.job_id); const artifacts = await client.getArtifacts(args.job_id); return { content: [ { type: "text", text: JSON.stringify({ job, artifacts }, null, 2), }, ], }; } if (name === "riddle_automate") { const viewport = args.device ? devices[args.device] : devices.desktop; const { job_id, artifacts, consoleLogs, networkHar } = await client.runScriptSync( args.url, args.script, viewport, args.timeout_sec || 60 ); const images = []; const savedPaths = []; for (const artifact of artifacts) { if (artifact.type === "image") { const base64 = artifact.buffer.toString("base64"); images.push({ type: "image", data: base64, mimeType: "image/png" }); const path = saveToTmp(artifact.buffer, artifact.name.replace(".png", "")); savedPaths.push(path); } } // Format console logs for readability let consoleOutput = null; if (consoleLogs?.entries) { consoleOutput = { summary: consoleLogs.summary, logs: consoleLogs.entries.log?.slice(-20) || [], errors: consoleLogs.entries.error || [], warns: consoleLogs.entries.warn || [], }; } // Format network HAR summary let networkSummary = null; if (networkHar?.log?.entries) { const entries = networkHar.log.entries; networkSummary = { total_requests: entries.length, failed: entries.filter((e) => e.response?.status >= 400).length, requests: entries.slice(-10).map((e) => ({ url: e.request?.url?.substring(0, 80), status: e.response?.status, time: e.time, })), }; } return { content: [ { type: "text", text: JSON.stringify( { job_id, saved_to: savedPaths, console: consoleOutput, network: networkSummary, }, null, 2 ), }, ...images, ], }; } if (name === "riddle_click_and_screenshot") { const viewport = args.device ? devices[args.device] : devices.desktop; const waitMs = args.wait_ms || 1000; const script = ` await page.waitForLoadState('networkidle'); await page.click('${args.click.replace(/'/g, "\\'")}'); await page.waitForTimeout(${waitMs}); await page.screenshot({ path: 'after-click.png', fullPage: false }); `; const { job_id, artifacts } = await client.runScriptSync( args.url, script, viewport, 30 ); const images = []; for (const artifact of artifacts) { if (artifact.type === "image") { const base64 = artifact.buffer.toString("base64"); images.push({ type: "image", data: base64, mimeType: "image/png" }); saveToTmp(artifact.buffer, "click-result"); } } return { content: [ { type: "text", text: JSON.stringify({ job_id, clicked: args.click }, null, 2), }, ...images, ], }; } throw new Error(`Unknown tool: ${name}`); } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true, }; } }); // Start server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Riddle MCP server running"); } main().catch(console.error);

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/davisdiehl/riddle-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server