Skip to content

Latest commit

 

History

11 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

White Hat Thumbnails — MCP server

Lets an AI assistant run your Roblox thumbnails end to end from a chat window: read how they are performing, generate new ones, publish them, choose which are live, rotate them automatically, and talk to your Discord team about it.

"how are the thumbnails doing on +1 dumpling escape?"
"what am I actually allowed to do on magnet simulator?"
"generate three variants from this reference and put the best one live"
"drop the worst performer and promote the next one from the queue"
"go notify the thumbnail team on +1 dumpling escape walls to pay this link,
 with two options approve and disapprove"

Everything runs against your own account. A key only reaches the projects you already have access to on the site, and it can never read back a stored credential — only tell you whether one is there and whether it works.


Setup

1. Get a key

Sign in at https://whitehatthumbnails.vercel.app, open Keys, and create one. It is shown once — copy it then.

2. Install

git clone https://github.com/Haydebug/whitehat-thumbnails-mcp.git
cd whitehat-thumbnails-mcp
npm install
npm run build

3. Point your assistant at it

Claude Code
claude mcp add whitehat-thumbnails \
  --env WHT_API_KEY=wht_your_key_here \
  -- node /absolute/path/to/whitehat-thumbnails-mcp/dist/index.js
Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "whitehat-thumbnails": {
      "command": "node",
      "args": ["/absolute/path/to/whitehat-thumbnails-mcp/dist/index.js"],
      "env": { "WHT_API_KEY": "wht_your_key_here" }
    }
  }
}

The config file lives at:

  • macOS ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows %APPDATA%\Claude\claude_desktop_config.json

Restart Claude Desktop afterwards.

Use an absolute path — the assistant does not run from this folder.


Tools

Knowing what you can do

Tool What it does
check_permissions Probes one game and reports exactly which tools will work on it, and why the rest will not
set_project_credentials Store an Open Cloud key and/or a Roblox cookie on a game — the two things the report asks for

Projects

Tool What it does
list_projects Your games and their universe ids
get_game_info Look up any Roblox game by link, place id or universe id — it need not be yours
search_roblox_games Search Roblox by name or keyword
add_project Put a game on your account, pinned to whichever Roblox account can edit it
list_collaborators / invite_collaborator Who else reaches a project, and a single-use invite link

Thumbnails

Tool What it does
get_thumbnails Live and inactive thumbnails with QPTR, impressions, qualified plays, average playtime
set_active_thumbnails Choose which thumbnails Roblox actually shows — replace, add or remove
upload_thumbnail Publish a local image as a game thumbnail
delete_thumbnail Permanently remove a thumbnail from the game
get_qptr_trend Daily click-through and impressions over 7, 14 or 30 days
get_thumbnail_momentum Which individual thumbnails are rising, falling or flat
export_thumbnail_report The whole set, ordered by QPTR, as a PDF written to disk

Making thumbnails

Tool What it does
generate_thumbnail Generate an image from a prompt and references, write it to disk, optionally publish it
list_generations Everything generated for a game, with the prompts behind each one
upload_generation Publish something from that history without re-uploading it
list_style_profiles The named style guides generation can be pointed at

Rotating them

Tool What it does
get_auto_swap Whether a game rotates itself, on what rules, and what recent runs did
configure_auto_swap Turn rotation on or off and set its thresholds
run_auto_swap Fire one rotation now, without waiting for the schedule
get_queue What is lined up to go live next
queue_thumbnail / unqueue_thumbnail Add to or remove from that line

Discord

Tool What it does
list_discord_roles Roles that can be notified
notify_team DM a role, optionally with images and two answer buttons
get_notification_history What was already sent, with per-person answers and paid status
create_ticket Open a ticket — posts an embed with Approve/Deny in the game's channel
list_tickets Just the tickets on a game, with what is still unanswered or unpaid
list_artist_channels Text channels in the studio server, with ids
create_channel / link_channel Make a channel, and point a game's tickets at one
get_channel_messages Read a channel — author, time, text, attachment URLs
send_channel_message Post in a channel as the bot, as plain text or an embed, optionally with images

Game names are matched loosely, so "dumpling escape" finds "+1 Dumpling SMASH Walls". If a name is ambiguous the tool says so and lists the candidates rather than guessing.

Knowing what you are allowed to do

Authority over a Roblox game is not one permission. It is spread across a .ROBLOSECURITY cookie (uploads, deletes, swapping the live set), an Open Cloud key (analytics), a personalization config Roblox only creates once a game has several thumbnails (whether there is a swappable live set at all), and a Discord bot (tickets and notifications). Any of them can be missing on its own, and the failure you see is the same shrug either way.

check_permissions probes all of them at once and answers in two lists:

MAGNET SIMULATOR LEGENDS — universeId 7592711723
You own this project.
Open Cloud key: stored (••••YkpR), encrypted at rest
Roblox account: WH_AIbot (the shared default account)

WORKS (6)
  get_thumbnails
      2 active, 124 inactive
  analytics (QPTR, impressions, playtime)
      figures came back for 16 of 126 thumbnails via the Open Cloud key ••••YkpR
  set_active_thumbnails
      a personalization config was resolved, so the live set can be changed
  …

DOES NOT WORK (2)
  upload_thumbnail, delete_thumbnail
      WH_AIbot (the shared default account) is signed in but Roblox refuses it
      edit access to this game
      Fix: give that account permission on the experience, or store a cookie for
      one that already has it with set_project_credentials.
  ticket posting to a channel
      this game is not linked to a Discord channel, so tickets only go out as DMs
      Fix: call create_channel with this game, or link_channel with an id.

Every "no" carries the reason and the fix, because "no" on its own sends you round the same loop again. Run it before a write you care about, or after one fails and it is not obvious which credential was missing.

The two credentials it asks for go in with set_project_credentials. They are stored encrypted and never read back — the report can say a key is present and whether it works, and nothing more.

Choosing what is live

Uploading a thumbnail does not show it to anyone. Roblox keeps every image a game has ever had and serves a chosen subset; set_active_thumbnails is what changes that subset, and it takes effect immediately.

set_active_thumbnails(game: "magnet simulator", ids: ["108286870859415"], mode: "add")
set_active_thumbnails(game: "magnet simulator", ids: ["112922133257215"], mode: "remove")

add and remove adjust what is already live and are what you usually want. replace makes exactly the listed set live and everything else inactive — easy to fire by accident when you meant add. Ids can be the asset ids the reports lead with or the internal thumbnail ids; both are accepted.

Nothing here deletes anything. A deactivated thumbnail keeps its history and can be brought back. delete_thumbnail is the one that cannot be undone, and it refuses to remove a game's last live image.

Generating

generate_thumbnail runs the same pipeline the site's generator page does: your prompt is rewritten against a style guide, then rendered against one or more reference images. The result is written to a file on your machine so it can actually be looked at before it goes anywhere near a game.

generate_thumbnail(
  game: "magnet simulator",
  prompt: "character yanking a giant magnet out of a pile of loot",
  referencePaths: ["ref.png"],
  profile: "Simulator"
)

References can be local paths or public URLs; at least one is required, since the model works from an image rather than from nothing. profile picks one of the style guides list_style_profiles lists. Pass publish: true only when the image has already been agreed on — otherwise generate, look at the file, then upload_thumbnail.

Rotating automatically

Auto-swap drops the worst-performing live thumbnails by QPTR and promotes replacements from a queue, on a schedule.

queue_thumbnail(game: "magnet simulator", id: "108286870859415")
configure_auto_swap(game: "magnet simulator", enabled: true, dropCount: 1,
                    frequencyMinutes: 1440, qptrThreshold: 2)
run_auto_swap(game: "magnet simulator")

qptrThreshold is a percentage, not a fraction: 2 means 2%. A game with an empty queue will run and do nothing, so check get_queue before turning it on. get_auto_swap shows the rules, the schedule and what recent runs actually moved — and if rotation turned itself off, the reason it gives is the thing to fix.

Answer buttons

notify_team can attach two buttons and let people answer:

notify_team(
  game: "+1 dumpling escape",
  role: "Thumbnail Team",
  message: "Pay here please: https://…",
  askForAnswer: true,
  approveLabel: "Paid",
  declineLabel: "Cancel"
)

Whoever clicks has their decision DM'd to everyone with that role, including themselves. Each person can answer once.

get_notification_history reports each answer on its own line with the person's Discord id and the moment they clicked, so an automation can gate on a specific pair of people having both approved rather than on a count:

2026-08-20T03:42:31.704Z — Hayden S → "Thumbnail Team" (2/2 delivered) · ticket clx…
  Dumpling concept art from lamy
  payment: $10 · https://paypal.me/asianlamy · PAID by Bug at 2026-08-20T03:45:02.118Z
  answers:
    Bug (4207…) = Paid at 2026-08-20T03:45:02.118Z
    ang (9931…) = Deny at 2026-08-20T03:46:44.500Z

Paying for commissioned work

Pass paymentAmount and the ticket becomes an invoice — the DM shows the amount and a pay button, the buttons default to Paid / Cancel, and the first click on Paid marks the ticket paid:

notify_team(
  game: "+1 dumpling escape",
  role: "Thumbnail Team",
  message: "Dumpling concept art from lamy",
  paymentAmount: 10,
  paypalLink: "paypal.me/asianlamy"
)

paid latches. A decline after someone has already marked it paid is recorded as an answer but does not un-pay the ticket — money that went out cannot be recalled by a button.

Tickets

A ticket is the same record as a team notification — same table, same per-person answers, same paid flag — with a title, a type and a channel to live in. It can be opened three ways and they are indistinguishable afterwards:

  • /ticket create in Discord, from any channel or a DM with the bot
  • create_ticket here, for automations
  • the notification tooling on the site
create_ticket(
  game: "+1 dumpling escape",
  type: "concept_review",
  title: "Dumpling wall smash — first pass",
  description: "Three angles from lamy. Picking one for the A/B.",
  imagePaths: ["pass1.png", "pass2.png"]
)

Types are concept_review, final_review, payment and status. Everything but status gets Approve / Deny buttons; a payment ticket gets Paid / Cancel plus the amount and a tappable PayPal link, and the first Paid click sets the paid flag. Answers land in get_notification_history exactly as button answers on a DM do, and the embed in the channel is rewritten as people answer so the message everyone is looking at shows the current state.

Each game posts its tickets to one channel, set once from Discord:

/ticket config game:<game> channel:#lammyta

Without that, a ticket is still created and still readable — it just has nowhere to appear, and the tool says so rather than pretending it was delivered.

Talking to artists

Each artist has their own text channel in the studio server. The three channel tools are a poll loop, not a live feed: read with since set to the lastMessageId you were given last time, and you see only what arrived since.

list_artist_channels()                              → #lammyta — id 1539…
get_channel_messages(channelId: "1539…", since: "…") → replies + art
send_channel_message(channelId: "1539…", text: "…", imagePaths: ["brief.png"])

Attachment URLs from Discord are signed and expire in roughly a day, so download art when you first see it rather than storing the link for later.

Embeds

A channel post can be an embed rather than a line of text — the card with a coloured bar down the side, the same one the notification DMs use. Pass embed instead of, or alongside, text:

send_channel_message(
  channelId: "1539…",
  embed: {
    author: "White Hat Thumbnails · MCP",
    title: "+1 Dumpling SMASH Walls",
    url: "https://whitehatthumbnails.vercel.app/projects/10307670587",
    color: "#facc15",
    description: "Five thumbnails live. **0.41% QPTR** on 1.95M impressions.",
    fields: [
      { name: "Top QPTR", value: "0.41%", inline: true },
      { name: "Impressions", value: "1,947,283", inline: true }
    ],
    thumbnailUrl: "https://tr.rbxcdn.com/…",
    imageUrl: "https://tr.rbxcdn.com/…",
    footer: "Sent by the MCP",
    buttons: [{ label: "View project", url: "https://whitehatthumbnails.vercel.app/…" }]
  }
)

The shape is flat on purpose — Discord nests the footer, the image and every button inside wrappers, and none of that is worth knowing to send a message. Fields are cut to the lengths Discord enforces rather than sent long, because it rejects the whole message over one oversized field.

Buttons here only open links. A button that asks a question and records the answer belongs to notify_team or create_ticket, which have somewhere to write the answer down.

For the bot to see a channel it needs View Channel, Read Message History, Send Messages and Attach Files there. Invite it with:

https://discord.com/oauth2/authorize?client_id=<APP_ID>&scope=bot%20applications.commands&permissions=117760

Both scopes matter. bot gets it into the server; applications.commands is what makes /ticket visible there. A bot invited without the second one works fine for DMs and channel posts while its slash commands silently never appear.


Configuration

Variable Required Meaning
WHT_API_KEY yes Your key from the Keys page
WHT_BASE_URL no Only if you run your own copy of the site

Notes

Discord DMs can be refused, and that is not a bug. Discord will not let a bot message someone who has blocked it, or who has DMs from server members turned off, or who shares no server with the bot. notify_team reports per person which of those happened rather than a single success count.

Roblox moderates uploads asynchronously. upload_thumbnail returning Pending means it was accepted and is being reviewed — usually a few minutes.

Some games are read-only. Uploading and swapping need a Roblox account with edit access to that experience; an Open Cloud key cannot do either. The site shows each project as Full Edit, View Only or Public View, and the tools say when something is unavailable rather than failing silently. check_permissions is the quickest way to find out which case a game is in.

Three tools change what players actually see, and do so the moment they return: set_active_thumbnails, run_auto_swap, and configure_auto_swap with enabled: true. Uploading and generating do not — an uploaded thumbnail sits inactive until something puts it live.

One tool cannot be undone. delete_thumbnail removes the image from Roblox along with its analytics history. Deactivating with set_active_thumbnails(mode: "remove") is the reversible version and is almost always what was meant.

check_permissions does not probe generation. The only honest check would be generating an image, which costs money and leaves a record behind. If generate_thumbnail is going to fail it fails on the first call, with the reason.

Licence

MIT

About

MCP server for White Hat Thumbnails — read Roblox thumbnail performance, upload thumbnails, and notify your Discord team from an AI assistant.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages