AI Agents (MCP)
We speak the Model Context Protocol, so an AI assistant can work inside your workspace: list your connected accounts, check what a platform requires, publish a post, read back its status, and see how it performed.
These tools publish live posts. Anything an agent calls, your audience sees. Point it at a test account first, and keep your assistant configured to ask before it publishes.
Connect
POST https://api.multi-upload-tool.com/mcp
Step-by-step setup for each client (Claude, ChatGPT, Claude Code, Cursor, Codex) is on the install page . Directories can read the tool list without signing in from the server card ; the official MCP registry lists us as com.multi-upload-tool/mcp.
JSON-RPC 2.0 over plain HTTP POST — one request, one response, no session to keep alive. You connect by signing in; API keys on this endpoint are deprecated.
Sign in (OAuth) — Claude, ChatGPT and any spec-compliant client
Paste the URL above as a connector. The client discovers our authorization server by itself, sends you to a consent screen on Multi Upload Tool, and stores a token of its own. There is no key to copy, and nothing secret in a config file.
- Claude (web, desktop, mobile — every plan, one custom connector on Free): Customize → Connectors → + → Add custom connector, paste the URL, Add, then Connect.
- ChatGPT (web; Plus, Pro, Business, Enterprise, Edu): turn on Settings → Security and login → Developer mode, then in Plugins click +, paste the URL and choose OAuth.
- Claude Code:
claude mcp add --transport http multi-upload https://api.multi-upload-tool.com/mcp, then/mcp→ Authenticate.
On the consent screen you choose the workspace the assistant acts in, and its access:
| Scope | What the assistant can do |
|---|---|
mcp:read | List and search accounts and posts, read their status, limits, pools and analytics. The tools that publish or change something are not even listed. |
mcp:write | Everything above, plus create_media_upload, create_post, create_review_link, retry_post, retry_all_failed, reschedule_post, cancel_post and add_to_pool. |
A read-only connection that tries to publish gets an HTTP 403 with WWW-Authenticate: Bearer error="insufficient_scope" — the signal clients use to ask you for more, instead of failing silently. Every connection is listed under API → Connected apps, where one click revokes it.
The consent screen names the application by the domain it proved it controls (for Claude, ChatGPT and Claude Code: claude.ai, chatgpt.com), not by the name it declares. An application that registered dynamically cannot prove a domain; it is shown as unverified, with the address it will send you back to. When that address is your own computer (localhost), the screen says so — only approve a request you started yourself.
For implementers: protected resource metadata (RFC 9728 ) is at /.well-known/oauth-protected-resource/mcp, authorization server metadata (RFC 8414 ) at /.well-known/oauth-authorization-server. PKCE S256 is required; clients register with a Client ID Metadata Document or, for older clients, Dynamic Client Registration. Access tokens last one hour; refresh tokens are rotated on every use.
API keys and scopes
An API key used on the MCP endpoint only sees the tools its scopes allow — a key with analytics:read alone only gets get_analytics. A key with posts:write but not posts:publish can call create_post, and its posts wait for a human to approve them.
API key deprecation
Until now the MCP endpoint also took the API token of the rest of the API, in the x-api-key header. That still works, but it is deprecated, and it stops on 12 January 2027 at 09:00 UTC:
- until then, every response to a key carries
Deprecation(RFC 9745 ),Sunset(RFC 8594 ) and aLinkto this section, and the assistant is told oninitializeto have you reconnect; - after it, a key gets
401with the same OAuth challenge as an anonymous call, so a compliant client falls straight into the sign-in above.
To move: remove the server from your client and add it again with the URL alone, as above. The dashboard (API) flags every key that was used on MCP in the last 30 days.
The REST API (/api/v1) is not affected: it keeps working with API keys.
Rate limit: 60 calls per minute per user, whichever way you authenticate.
Tools
| Tool | What it does |
|---|---|
list_accounts | Connected accounts with their id, platform, name, status and tags — filter by platform, tag or search. Call it first — the ids feed every other tool. |
get_platform_capabilities | Character limits, daily caps, accepted media and the settings a platform requires, for one platform. |
create_media_upload | A one-time link where the user drops a video or images from their phone or computer, for clients that cannot send files (claude.ai, ChatGPT). See Files on your phone or computer. |
check_media_upload | Whether the user finished dropping files on that link; when they have, the mediaUrls to pass to create_post. |
validate_post | Dry run of create_post, same arguments: reports every account that would reject the post (inactive account, plan, daily cap, caption length, missing settings) without publishing anything. Read-only. |
create_post | Publishes or schedules to one or more accounts, mixing platforms in a single call. overrides gives a network or an account its own text / title (e.g. a short Bluesky version). status: "draft" or "pending_approval" creates it without publishing, for a human to approve. |
get_post_status | Where a post stands: queued, scheduled, published (with the platform’s own URL) or failed, with the error. |
list_recent_posts | Recent posts, filterable by account, platform or status — useful to avoid publishing the same thing twice. |
get_hashtag_history | The hashtags you actually use on a platform, plus the ones that would get the post rejected. |
get_analytics | Views, likes, comments and shares — for every account, one account, or one post. Account and global calls also return summary (last 30 days vs the 30 before: views gained, follower change, posts published), followerHistory, and bestTimes, the weekday and hours whose posts averaged the most views, ready to feed scheduledAt. Read-only. |
search | Keyword search over your posts (caption, title) and accounts. Returns { results: [{ id, title, url }] } — the shape ChatGPT deep research expects. |
fetch | One search result in full, by its id (post:<id> or account:<id>): caption, status, schedule or publication time, platform URL. |
reschedule_post | Moves a post that is still scheduled to another time, optionally with a new caption. Published posts are final. |
create_review_link | A no-login link where the user’s client approves the posts created with status: "pending_approval". See Drafts & Approval. |
retry_post | Sends one failed post again (one account of a post: the accounts that went out are not touched). Refused when retrying cannot help, unless force is set. |
retry_all_failed | Sends again every retryable post that failed in the last 24 hours, optionally for one account or platform. |
cancel_post | Cancels a scheduled post (or clears a failed one) so it never goes out. Does not delete anything already on the platform. |
get_upload_limits | How many more posts an account can publish today before the platform’s daily cap, and when it resets. |
list_pools | Your upload pools — queues drip-fed to accounts on weekly slots — with how many items are queued. |
add_to_pool | Queues 1 to 50 videos (public URLs) into a pool; each goes out at the next free slot. |
Pass timezone (an IANA zone such as Europe/Paris) to get_analytics. The weekday and
hour buckets behind bestTimes default to UTC, and an hour-of-day ranking read in the wrong
zone is off by the user’s whole offset — silently, because every bucket still holds a
plausible number. A slot with fewer than minSamples posts is returned but never ranked:
one lucky post is a coincidence, not a slot.
Each platform’s required settings are enforced, not guessed: TikTok needs a privacyLevel, YouTube a title, Pinterest a boardId. get_platform_capabilities returns the exact schema.
Media
create_post takes mediaUrls: up to 10 public http(s) URLs, which we download and attach. Each one must be under 200 MB and publicly reachable.
For anything larger — or for a file that has no public URL at all — presign it first:
POST /api/v1/upload/presignwith{"route": "video", "files": [{...}]}PUTthe file to the signed URL it returns (straight to storage, no size ceiling on the way)create_postwith the returnedbucketUrlinmediaUrls
A URL you presigned yourself is recognised and passed through untouched: the bytes are already in place, so nothing is downloaded again and the 200 MB limit does not apply. See Large File Uploads for the full flow.
An agent with a shell (Claude Code, Codex, Cursor…) can do all three steps on its own — presign, curl -T, publish. Nothing to install beyond the MCP connection above.
Files on your phone or computer
In claude.ai, ChatGPT or their mobile apps, the assistant has no shell and cannot run the local bridge below, and a file you attach to the chat cannot be forwarded to a tool. Ask it to post the video anyway: it calls create_media_upload and gives you a link.
- Open the link and drop your video or images. They go straight to storage, with no 100 MB edge limit.
- Go back to the conversation and say it is done.
- The assistant calls
check_media_upload, thencreate_postwith the files.
“Publie la vidéo que j’ai sur mon téléphone sur TikTok et YouTube.”
The link needs no login, so it is kept narrow: it works once, for 1 hour, only accepts video and images (up to 35 files, 5 GB in total), and can only add files — it cannot read or publish anything. Uploads count against your plan’s storage quota. Files not used in a post within 24 hours are deleted. At most 3 links can wait for files at the same time, and 20 can be created per day.
Files on your own machine
A tool call carries JSON, never bytes — so a remote server cannot reach the video sitting on your laptop. For that, run the small bridge on your machine instead: it reads the file off disk, sends it straight to storage, and publishes. The assistant only ever handles a file path, never a storage URL.
{
"mcpServers": {
"multi-upload-local": {
"command": "npx",
"args": ["-y", "multi-upload-tool-mcp"]
}
}
}That is the Claude Desktop / Cursor / Windsurf shape. Claude Code: claude mcp add multi-upload-local -- npx -y multi-upload-tool-mcp. Codex CLI: the same command under [mcp_servers.multi-upload-local] in ~/.codex/config.toml. Requires Node 20+; nothing else to install.
No key to paste: the first time a tool needs your account, the bridge opens your browser on the same consent screen as above, where it shows as verified by multi-upload-tool.com. If no browser can open (SSH, container), the sign-in link is in the tool result. Its tokens are kept in ~/.config/multi-upload-tool-mcp/oauth.json, readable by you only, and refreshed on their own; disconnecting it under API → Connected apps makes it ask again. MULTI_UPLOAD_API_KEY is still read, until the API key cut-off.
It adds two tools to list_accounts:
| Tool | What it does |
|---|---|
publish_carousel | Up to 35 local JPEG/PNG/WebP images (20 MB each) as a post or carousel. |
publish_video | One local MP4/MOV/WebM/M4V, up to 5 GB. |
“Publie
~/videos/clip.mp4sur ma chaîne YouTube, titre « Mon test », et sur TikTok en privé.”
Running many accounts
A post that failed carries failure: { code, retryable, nextAction } — act on the code, not on the platform’s wording. The codes and what each one calls for are listed in Why an Upload Failed.
Retry with retry_post (one post) or retry_all_failed (everything retryable from the last 24 hours). Never re-send a whole batch with create_post to fix part of it: the accounts that already published would publish twice. The media of a failed post is kept one day, after which only a new post can go out.
Publishing the same media to many accounts in the same minute is what platforms read as coordinated spam, and TikTok caps posts per creator over a rolling 24 hours. Either schedule each account a few minutes apart with scheduledAt, or queue the video into a pool with add_to_pool: a pool hands its slot’s accounts out at random moments inside the spread window set on it (staggerMinutes in list_pools). Pools are created in the app, not over MCP.
Errors
A tool that fails returns a normal MCP result with isError: true and a readable message, rather than a protocol error — the assistant reads it and can correct itself. A partially successful create_post reports per account: check every entry in posts before deciding anything was retried safely.
A failed post also carries failure: { code, retryable, nextAction } (see Why an Upload Failed). Read it before retrying: retryable: false means retrying would fail again.
Pass idempotencyKey (a UUID, new for each post) to create_post. If the call times out, retrying with the same key never publishes twice: within 24 hours it returns the first result with idempotentReplay: true.