Image Upload (MCP)

Get a screenshot from an AI chat into your media library without dumping the bytes into the conversation. The assistant prepares the metadata, hands you a one-time link, and you finish the upload in the browser — the raw bytes never ride through the model or an MCP tool call.

Quick upload is how an MCP-connected assistant gets a pasted screenshot into a BlackOps site's media library without routing the image bytes through the conversation or an MCP tool call. The assistant prepares the metadata, BlackOps hands you a one-time, token-scoped link, you finish the upload in the browser, and the assistant claims the finished asset. The model only ever handles lightweight metadata — the bytes go straight from your browser to storage.

Why this exists

The only naive way to upload a clipboard screenshot through an MCP tool is to base64-encode it and pass it as a parameter. A single 1–2 MB image becomes roughly half a million tokens of text in one call — it consumes the context window, slows every turn after it, and bills you for noise. In practice it froze the conversation entirely. This was logged and fixed as the "MCP Media Upload Context Freeze". The byte-bearing params (file_data, base64, file_path) are now rejected outright.

Pasting an image into chat is fine

A common worry: doesn't pasting the image into the chat recreate the same problem? It does not. When you paste an image, the model ingests it as a vision input — it's tokenized as a bounded number of image tokens (the image is downscaled to a max dimension first), typically around 1,000–1,600 tokens. That's cheap and it doesn't balloon.

What froze the context was the other thing: taking the same pixels and encoding them as base64 text inside an MCP tool call. Same image, two wildly different costs:

| Path | What the model stores | Rough cost | | --- | --- | --- | | Paste into chat (vision) | Tokenized image | ~1–1.6k tokens | | Base64 image as text via MCP | Raw text characters | ~500k+ tokens |

So pasting the image so the assistant can see it — to write the description, alt text, and run the safety scan — is the cheap copy. The handoff exists so the full-resolution bytes reach storage without re-pasting them or base64-ing them through a tool call. Two copies, two jobs: a small vision copy for understanding, the original bytes for the library.

The flow

  1. Paste the image(s) into your AI chat. Drop one or more screenshots into the conversation with an MCP-connected assistant. It views them as vision input — it never needs the raw bytes or a file path.
  2. The assistant prepares metadata and runs a safety scan. For each image it writes an SEO-friendly kebab-case filename and descriptive alt text. With the BlackOps media-upload skill installed, it also scans the image for content that should not be shared before anything leaves the machine (see below).
  3. The assistant calls upload_media and returns a link. With the metadata ready, it calls upload_media in interactive mode. BlackOps returns a one-time, token-scoped URL pointing at the quick-upload page.
  4. You open the link and upload. Sign in if asked, then drop or paste images. They upload as they land. Click Done when finished. The bytes go from your browser to your media library — the assistant never touches them.
  5. The assistant claims the finished assets. It calls upload_media({ token }) to retrieve the finalized asset(s), each with an asset_id, hosted URL, and dimensions, and can attach them to a draft post, tweet, thread, or LinkedIn post.

The pre-upload safety scan

The safety scan is not enforced by the server. BlackOps never inspects the bytes. The scan runs only because the BlackOps media-upload skill instructs the assistant to check the image before it calls upload_media. If that skill is not installed, the assistant has no scan instruction and the upload goes through unscanned. Installing the skill is what turns the scan on.

When the skill is active, the assistant also has to be able to actually see the image, so paste it into the session. It then flags:

  • Secrets — API keys, tokens, session cookies, .env values, anything in an open devtools panel
  • Cross-tenant or other-customer data visible in the frame
  • Internal or localhost URLs in the address bar
  • PII — emails, real names, account IDs not meant to be public
  • Stale or draft state, debug overlays, error toasts

If the scan flags anything, the assistant stops and asks you to confirm or supply a cleaned image before uploading.

The three upload modes

upload_media is one tool with three modes:

  • Interactive — pass no file_url and no token. Returns { url, token, expires_at }. The default for pasted screenshots. One link accepts multiple images.
  • Claim — pass token. Returns all finalized assets once you click Done (status: "completed" with an assets[] array). Returns status: "pending" while you're still uploading — poll again.
  • URL — pass file_url + filename + content_type. The server fetches an already-hosted asset and stores it. The only mode that accepts video.

Metadata carry-through: the filename, alt text, and title prepared in chat ride along on the handoff and are applied to the first uploaded image. Additional images in a bulk upload auto-generate their own.

What never happens

Two byte-bearing parameters are rejected outright by upload_media:

  • file_data / base64 — this was the context-freeze path and is now refused. Use a hosted URL or the interactive link instead.
  • file_path — the MCP server runs remotely and cannot read your local upload directory.

How it works

Handoff creation. upload_media (interactive) calls POST /api/v2/upload-handoffs, which inserts a row in upload_handoffs with a random token, the resolved site_id, the AI-prepared desired_filename / desired_alt_text / desired_title, and an expiry. It returns /quick-upload?token=….

Browser upload. The quick-upload page loads the handoff via GET /api/admin/quick-upload/[token] (cookie session — sign-in required). Each image is POSTed as multipart/form-data to …/[token]/complete, which validates ownership, sniffs the real file type from magic bytes, stores to the site-assets bucket, and writes the image_assets row. A …/[token]/finish call marks the handoff complete so a claim returns the assets.

Byte path. Browser → BlackOps REST route → Supabase storage. The bytes never pass through the model or the MCP server.

API reference

| Endpoint | Purpose | | --- | --- | | POST /api/v2/upload-handoffs | Creates a token-scoped upload handoff (V2 token auth). Returns { token, url, expires_at, site_id }. Drives upload_media interactive mode. | | GET /api/admin/quick-upload/[token] | Loads handoff state + finished assets for the quick-upload page. Cookie session; 401 when signed out. | | POST /api/admin/quick-upload/[token]/complete | Accepts multipart/form-data image bytes from the browser, validates the handoff, stores to site-assets, creates the asset. This is where the bytes land — never near the model. | | POST /api/admin/quick-upload/[token]/finish | Marks the handoff complete when you click Done, so a claim returns status: "completed". |

MCP tools

  • upload_media — one tool, three modes (interactive / claim / URL).
  • attach_media_to_post / _tweet / _thread / _linkedin_post — attach a claimed asset_id to a draft. One asset per call; attach several in sequence.
  • list_media / get_media — find and reuse an asset already in the library instead of re-uploading it.

Things to know

  • Links expire. TTL defaults to 15 minutes and caps at 1 hour (ttl_ms). An expired link needs a fresh upload_media call.
  • Quick upload is image-only (jpeg, png, webp, gif), max 50 MB per file. For video, host it and use URL mode.
  • The link is bound to the user who created it — you must sign in as that user to complete the upload.
  • The safety scan only runs when the BlackOps media-upload skill is installed. It is the assistant's responsibility, not the server's, and it only works if the assistant can see the image, so paste it into the chat. Without the skill, uploads are not scanned.
  • In a bulk upload, your prepared filename/alt/title apply to the first image only; the rest auto-generate.
Want this page as machine-readable markdown? GET /docs/features/image-upload.md