RecordHTML

Developers

RecordHTML MCP server

Connect RecordHTML to your AI assistant and ask it for videos. The hosted MCP server records web pages and HTML5 creatives to MP4, WebM or GIF with the same real-browser recorder, limits and storage as the web app and the API.

Coding agents with a shell can use the REST API too: its reference is plain Markdown at /docs/api.md, listed in /llms.txt. This page is also Markdown.

Connect

Add a remote MCP server with this URL. It speaks Streamable HTTP, and you sign in with OAuth: there is no key to paste and nothing to run on your machine.

Server URL
https://mcp.recordhtml.com
  • Use the root URL as shown. https://mcp.recordhtml.com/mcp works too; there is no /sse endpoint.
  • Your client opens a sign-in page: sign in or create an account, approve the access and go back to the client.
  • Check that the client lists five tools: record_html, get_recording, estimate_recording, list_recordings and delete_recording.
  • The client must support remote MCP servers with OAuth. Otherwise, use an API key.

Set up your client

Claude (claude.ai and Claude Desktop)

  1. Open Settings → Connectors and choose Add custom connector.
  2. Name it RecordHTML, enter https://mcp.recordhtml.com as the URL and add it.
  3. Click Connect and sign in. On Team and Enterprise plans an owner adds the connector for the organization first; each member then connects their own account.

Claude Code

  1. Add the server, then type /mcp in Claude Code, choose recordhtml and sign in in the browser.
Terminal
claude mcp add --transport http recordhtml https://mcp.recordhtml.com

ChatGPT

  1. In Settings → Apps & Connectors → Advanced settings, turn on Developer mode.
  2. Create a connector with https://mcp.recordhtml.com as the URL and OAuth as the authentication, and sign in.
  3. Pick the connector in a chat that has developer mode on.

Codex

  1. Codex sees that the server uses OAuth and opens the sign-in page. To sign in again later, run codex mcp login recordhtml.
Terminal
codex mcp add recordhtml --url https://mcp.recordhtml.com

Cursor

  1. Add the server to ~/.cursor/mcp.json (or .cursor/mcp.json in a project). Cursor asks you to sign in the first time it uses it.
mcp.json
{
  "mcpServers": {
    "recordhtml": {
      "url": "https://mcp.recordhtml.com"
    }
  }
}

VS Code

  1. Add the server to .vscode/mcp.json (or run MCP: Add Server), start it and sign in when VS Code asks. Turn its tools on in the chat tool picker.
.vscode/mcp.json
{
  "servers": {
    "recordhtml": {
      "type": "http",
      "url": "https://mcp.recordhtml.com"
    }
  }
}

Other clients, or an API key

  1. Any client that supports remote MCP servers with OAuth works with the URL alone.
  2. Headless agents, and clients that can set headers, can send an API key from the API settings page instead: Authorization: Bearer rh_live_…. Keys are refused from browser pages.
  3. Cursor ignores headers for a server that offers OAuth: sign in there instead.
Claude Code with an API key
claude mcp add --transport http recordhtml https://mcp.recordhtml.com \
  --header "Authorization: Bearer $RECORDHTML_API_KEY"

Your first recording

Ask for a video in plain words; the assistant fills in the arguments.

Prompt
Record https://example.com/banner/ at 300×600 for 15 seconds as an MP4.
Tool call
record_html { "url": "https://example.com/banner/", "width": 300, "height": 600, "durationSec": 15, "format": "mp4" }
What the assistant reads back
Recording 4c1f0e1e-8f0a-4a52-9d1b-2f5f3c6b9a10 is ready.
Video: https://…/banner.mp4?…
(signed link, valid until 2026-10-22T10:14:52.004Z; get_recording mints a fresh one)
300×600, 30 fps, 15.0 s, mp4, 1.8 MB
Started at the load event.
Page (for a person; they sign in): https://…/jobs/4c1f0e1e-8f0a-4a52-9d1b-2f5f3c6b9a10

The result also carries the same data as structured content (the job of the API, with pageUrl) and a link to the video file. To save the file from a shell: curl --fail --location --output banner.mp4 "<video url>".

Tools

record_htmlRecord HTML to video

What the model reads

Record a web page or HTML5 creative (banner, ad, animation, landing page) to a video: MP4 (default), WebM or GIF. Returns the finished recording with a video link. Input: either url (a public http(s) page; localhost and private networks are out of reach) or html (a complete single-file HTML document you wrote, published at an unlisted public URL and recorded from there). Set width and height to the creative's size (e.g. 300×250, 728×90, 1080×1920, 1920×1080) and durationSec to how long it runs. Waiting: the call waits up to waitSec (default 45 s) for the video. If it isn't done by then, you get the recording with status queued or running; it keeps going. Call get_recording with its id and waitSec to wait for it; don't call record_html again for it. Retries are safe: identical calls within 10 minutes return the same recording. After changing the creative behind the same url, pass a new revision to record it again. A failed recording is never reused: call again once you've fixed the cause. Trim markers: by default the video starts at the page's load event and lasts durationSec. To capture exactly the animation, the page can mark it. The recorder defines two global functions in every frame (iframes included): - trim_start(): call it once when the assets are loaded, just before the animation starts. The video starts there. - trim_end(): call it once from the animation's completion callback (e.g. GSAP onComplete, the last animationend). The video ends there and durationSec becomes the maximum. Guard both so the page still works in a normal browser: // assets loaded, just before the animation starts: if (typeof trim_start === 'function') trim_start(); // in the animation's completion callback: if (typeof trim_end === 'function') trim_end(); Never call the two right after each other: that marks an empty clip. With sync "markers" the recording fails instead of falling back to the load event when trim_start() isn't called within markerWaitSec (default 10 s). report.anchor says "trim_start" when the start marker was used. Links: video.url is signed and valid for 1 hour (get_recording mints a fresh one). pageUrl is the recording's page in the web app, for a person to open later (they sign in). Limits: the account's limits apply (length, size, fps, recordings at a time and per day); estimate_recording checks a request without recording. Errors say what to change: fix it and call again.

ParameterTypeDescription
urlstringPublic http(s) URL of the page or creative to record. The recorder runs on the internet: localhost and private networks are out of reach. Pass url or html, not both.
widthinteger, requiredViewport width in CSS pixels: even, 16–7680. Use the creative’s size, e.g. 300 for a 300×250 banner or 1920 for a full HD page.
heightinteger, requiredViewport height in CSS pixels: even, 16–7680.
durationSecnumber, requiredLength of the video in seconds (1–300; your account limit applies). With trim_end() it is the maximum.
fpsnumber | stringFrame rate: one of 15, 23.976, 24, 25, 29.97, 30, 59.94, 60 (default 30). For gif, a whole number from 1 to 30.
startAt"load" | integerWhere the video starts: "load" (default: the load event, or the first trim_start() call) or that many milliseconds after it (0–120000).
sync"auto" | "load" | "markers"How trim markers are used: auto (default: markers when the page calls trim_start(), else the load event), load (ignore markers) or markers (fail when trim_start() is not called within markerWaitSec).
markerWaitSecnumberHow long to wait for trim_start() after load, in seconds (0–60, default 10).
markerLatencyMsnumberOverrides the measured delay between a marker call and the frame it marks (0–1000 ms). Rarely needed.
format"mp4" | "webm" | "gif"mp4 (default, H.264), webm or gif.
crfintegerQuality for mp4 and webm, from 10 (best, largest) to 35 (smallest). Not together with bitrateMbps; not for gif.
bitrateMbpsnumberA target bitrate in Mbps (1–100) instead of crf, for platforms with a bitrate spec (e.g. DOOH). Not for gif.
bitrateMode"vbr" | "cbr"vbr (default) or cbr. Needs bitrateMbps.
pixelFormat"yuv420p" | "yuv422p" | "yuv444p"yuv420p (default; plays everywhere), yuv422p or yuv444p.
audiobooleanRecord the page’s audio too. Not for gif.
loopbooleangif only: whether the gif loops.
cropobjectRecord only this region of the viewport: { x, y, width, height } in pixels, even values, inside the viewport.
outputSizeobjectScale the video to { width, height } (even values); see fit.
fit"fill" | "contain" | "cover"How the frame fills outputSize: fill (stretch), contain (letterbox in background) or cover (crop to fill).
backgroundstringLetterbox colour for fit: contain, as #rrggbb.
renderer"auto" | "cpu" | "gpu"auto (default), cpu or gpu. Leave it on auto.
htmlstringA complete HTML document to record instead of a url, e.g. a banner you just wrote. One file: inline the CSS and JavaScript, and load images, fonts and libraries from public https URLs or data: URIs. It is published at an unlisted public URL and recorded from there.
revisionstringAny label that changes when the creative changes. Identical calls within 10 minutes return the same recording; after you change the creative behind the same url, pass a new revision to record it again.
waitSecintegerHow long to wait for the finished video, in seconds (0–45, default 45). The recording keeps going after the call returns.

get_recordingGet a recording

read-only

What the model reads

Get a recording's status, report and a fresh video link (signed, valid for 1 hour). With waitSec, waits up to that many seconds (at most 45) for a queued or running recording to finish. Use it to continue after record_html returned before the video was ready, or to refresh an expired link.

ParameterTypeDescription
idstring, requiredThe recording’s id, from record_html or list_recordings.
waitSecintegerHow long to wait for the finished video, in seconds (0–45, default 0). The recording keeps going after the call returns.

estimate_recordingCheck a recording request

read-only

What the model reads

Check a recording request without recording or using up any quota. Takes the same arguments as record_html. Returns what to fix (invalidIssues, limitIssues, capacity.reasons), the resource estimate, your limits and your usage: recordings in progress and recordings started in the last 24 hours.

ParameterTypeDescription
urlstringPublic http(s) URL of the page or creative to record. The recorder runs on the internet: localhost and private networks are out of reach. Pass url or html, not both.
widthinteger, requiredViewport width in CSS pixels: even, 16–7680. Use the creative’s size, e.g. 300 for a 300×250 banner or 1920 for a full HD page.
heightinteger, requiredViewport height in CSS pixels: even, 16–7680.
durationSecnumber, requiredLength of the video in seconds (1–300; your account limit applies). With trim_end() it is the maximum.
fpsnumber | stringFrame rate: one of 15, 23.976, 24, 25, 29.97, 30, 59.94, 60 (default 30). For gif, a whole number from 1 to 30.
startAt"load" | integerWhere the video starts: "load" (default: the load event, or the first trim_start() call) or that many milliseconds after it (0–120000).
sync"auto" | "load" | "markers"How trim markers are used: auto (default: markers when the page calls trim_start(), else the load event), load (ignore markers) or markers (fail when trim_start() is not called within markerWaitSec).
markerWaitSecnumberHow long to wait for trim_start() after load, in seconds (0–60, default 10).
markerLatencyMsnumberOverrides the measured delay between a marker call and the frame it marks (0–1000 ms). Rarely needed.
format"mp4" | "webm" | "gif"mp4 (default, H.264), webm or gif.
crfintegerQuality for mp4 and webm, from 10 (best, largest) to 35 (smallest). Not together with bitrateMbps; not for gif.
bitrateMbpsnumberA target bitrate in Mbps (1–100) instead of crf, for platforms with a bitrate spec (e.g. DOOH). Not for gif.
bitrateMode"vbr" | "cbr"vbr (default) or cbr. Needs bitrateMbps.
pixelFormat"yuv420p" | "yuv422p" | "yuv444p"yuv420p (default; plays everywhere), yuv422p or yuv444p.
audiobooleanRecord the page’s audio too. Not for gif.
loopbooleangif only: whether the gif loops.
cropobjectRecord only this region of the viewport: { x, y, width, height } in pixels, even values, inside the viewport.
outputSizeobjectScale the video to { width, height } (even values); see fit.
fit"fill" | "contain" | "cover"How the frame fills outputSize: fill (stretch), contain (letterbox in background) or cover (crop to fill).
backgroundstringLetterbox colour for fit: contain, as #rrggbb.
renderer"auto" | "cpu" | "gpu"auto (default), cpu or gpu. Leave it on auto.
htmlstringA complete HTML document to record instead of a url, e.g. a banner you just wrote. One file: inline the CSS and JavaScript, and load images, fonts and libraries from public https URLs or data: URIs. It is published at an unlisted public URL and recorded from there.

list_recordingsList recordings

read-only

What the model reads

List your recordings, newest first: id, status, request, report and pageUrl (10 per page by default). Video links are left out; call get_recording for one. Continue with nextCursor.

ParameterTypeDescription
statusstring"active" (queued, running or storing) or a comma list of queued, running, storing, ready, failed, deleted, expired. Default: everything except deleted.
limitintegerHow many to return (1–50, default 10).
cursorstringnextCursor from the previous page.

delete_recordingDelete a recording

destructiveidempotent

What the model reads

Delete a recording’s video files for good: the original and any crop or trim. The recording stays in the history with status deleted. This cannot be undone; deleting it again changes nothing.

ParameterTypeDescription
idstring, requiredThe recording’s id, from record_html or list_recordings.

Waiting and retries

  • record_html waits up to 45 seconds (waitSec) for the video, and reports progress to clients that show it. A longer recording comes back with its id and status queued or running, and keeps going: get_recording with waitSec picks it up.
  • Identical record_html calls within 10 minutes return the same recording, so a retried call never records twice. After changing the creative behind the same URL, pass a new revision. A failed recording is never reused.
  • Stopping a call (or a client timeout) never stops the recording.

Trim markers

An assistant that writes the creative can mark exactly what to record. The recorder defines two global functions in every frame, iframes included:

In the creative
// assets loaded, just before the animation starts:
if (typeof trim_start === 'function') trim_start();
// in the animation's completion callback:
if (typeof trim_end === 'function') trim_end();
  • trim_start(): once, when the assets are loaded and just before the animation starts. The video starts there.
  • trim_end(): once, from the animation's completion callback. The video ends there, and durationSec becomes the maximum.
  • Never call the two right after each other: that marks an empty clip.
  • With sync: "markers" the recording fails when trim_start() isn't called within markerWaitSec, instead of starting at the load event.

Every option is in the API reference.

Limits and errors

MCP, the API and the web app share your account's limits: recordings in progress at a time, recordings per 24 hours, and the maximum duration, size and frame rate. estimate_recording shows them with your usage. Each account can also call record_html 20 times a minute and estimate_recording 60 times a minute.

Errors come back as tool errors the assistant can act on: the code, the message, one line per problem and what to do next.

A tool error
limit_exceeded: this job is beyond your limits
- durationSec: your limit is 60 s
Lower the values listed (estimate_recording shows the account limits) and call again.

The codes are those of the API.

Manage access

  • To disconnect a client, remove the server or connector in it; that deletes its tokens.
  • An API key used with MCP can be revoked at once on the API settings page.

Troubleshooting

ProblemWhat to do
The client can’t connectUse https://mcp.recordhtml.com with Streamable HTTP. The client must support OAuth for remote servers, or send an API key.
The page can’t be reachedThe recorder runs on the internet: localhost and private addresses are out of reach. Publish the page, or let the assistant pass the HTML as html.
uploads_disabledThis server can’t publish inline HTML: pass a public url.
too_many_active_jobsYour recordings in progress are at the limit: wait for one (get_recording with waitSec).
The client stopped waitingSome clients give up on a call after about a minute. The recording carries on: ask the assistant to call get_recording.
The link expiredSigned links last an hour: get_recording mints a fresh one.