Plugins
A plugin is an MCP server you run. Vidmoat never executes your code — it forwards a tools/call to your endpoint and hands the result back to whoever asked. That means you keep your runtime, your dependencies and your deploy cycle, and we keep the ability to cut a bad integration off without touching anything else.
Submitting: paste the URL
You do not write a manifest. Give us your endpoint and Vidmoat calls tools/list and initialize and fills in the rest from what your server reports — so what is registered can never disagree with what you actually serve. That mismatch is otherwise the most common reason a submission is held.
| WE READ | FROM |
|---|---|
| Tool names, descriptions, input schemas | tools/list |
| Display name | initialize → serverInfo.title (or .name) |
| Description | initialize → instructions |
| Suggested slug | derived from the name — you can change it |
The one thing MCP cannot express is producesMedia, because it is a Vidmoat concept with no place in the spec. Declare it per tool and your plugin imports with nothing to fill in at all:
{
"name": "make_thumbnail",
"description": "Takes a title and an optional face crop and returns a 1280x720 thumbnail.",
"inputSchema": { "type": "object", "properties": { "title": { "type": "string" } }, "required": ["title"] },
"_meta": { "vidmoat": { "producesMedia": "image" } }
}Otherwise you tick it in the form. Everything else — slug, name, description — is editable there too; what you cannot override is the tool list, which is re-read from your server at the moment you submit.
The shape of a call
Once a user installs your plugin, your tools appear in their tools/list alongside ours, and are reachable over REST. Both go through the same code path, so they behave identically.
# MCP — for agents
{"method":"tools/call","params":{"name":"my_plugin__do_the_thing","arguments":{...}}}
# REST — for everything else
POST https://api.vidmoat.com/v1/plugins/my-plugin/do_the_thing
Authorization: Bearer vm_live_...
Content-Type: application/json
{ "arguments": { "subject": "..." }, "projectId": "optional" }Namespacing, and why your tool names are safe
Your tools are exposed as slug__tool. Agents pick tools by name, so if a plugin could register render_project, an agent asked to render a video would reach a stranger's server holding a real project id. Slugs are unique across Vidmoat and a manifest claiming one of our tool names is refused at submission — which also means nobody else can shadow yours.
What we send you
POST /mcp
X-Vidmoat-Request-Id: 0f0d… # unique per call
X-Vidmoat-Timestamp: 1756... # unix seconds
X-Vidmoat-Signature: <hex> # HMAC-SHA256 of "timestamp.body"
X-Your-Header: <your secret> # only if you configured one
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "do_the_thing", # YOUR name, not the namespaced one
"arguments": { ... },
"_meta": { "vidmoat": {
"requestId": "0f0d…",
"user": "a1b2…", # opaque, stable, per-plugin
"projectId": "cm…" | null
}}
}
}The user field is an HMAC of the real id with your plugin id. It is stable, so you can cache per-user work against it — and it is useless to any other plugin, so nobody can correlate one person across the catalogue.
Limits, and what happens when you hit them
| LIMIT | VALUE | WHAT THE CALLER SEES |
|---|---|---|
| Response time | 20s | Timeout, counted as a failure |
| Response body | 1 MB | body_too_large — return a URL, not inline data |
| Returned media | 200 MB | The call fails; nothing is added to the project |
| Redirects | Not followed | redirect — respond directly |
| Consecutive failures | 8 | Plugin auto-disabled until you fix and re-enable it |
Returning media
Declare producesMediaon a tool and return a URL. Vidmoat downloads it, checks the content type against what you declared, and copies it into the user's own library before handing back our URL.
This is not a formality. A plugin's URL written into a saved project is a clip that goes black the day you change hosts, and neither the render worker nor the user would be able to tell that from a deliberately black shot. Copying it is what makes plugin output a real asset — garbage-collected, proxied and rendered exactly like an upload.
{
"name": "rig_character",
"description": "Rigs a front-facing character illustration and returns a looping idle animation.",
"inputSchema": { "type": "object", "properties": { "imageUrl": { "type": "string" } }, "required": ["imageUrl"] },
"producesMedia": "video"
}
// Your response — any of these shapes is understood:
{ "content": [{ "type": "text", "text": "{\"url\":\"https://you.dev/out.webm\"}" }] }
{ "content": { "url": "https://you.dev/out.webm" } }Writing tool descriptions
The description is the whole interface. An agent reads it and nothing else when deciding whether to call your tool — it never sees your README, your site, or your intent. Say what the tool does, what it needs, and what it returns, in a sentence someone could act on. Submissions where the descriptions say nothing are held for review for exactly this reason.
Private plugins
A plugin does not have to be for anybody else. Register it as private and it is yours alone: it never appears in the marketplace, nobody else can install it, and it skips review entirely — there is no third party for a reviewer to be protecting. Its tools show up in your agents' tools/list and are callable over REST with your own key, exactly like a published one.
| VISIBILITY | WHO CAN INSTALL IT | REVIEWED | IN THE MARKETPLACE |
|---|---|---|---|
private | Nobody — only you can call it | No | No |
unlisted | Anyone you give the slug to | Yes | No |
public | Anyone | Yes | Yes |
Unlisted is reviewed like public on purpose. Being hard to find is not a safety property — a link gets forwarded — so the moment anybody other than you can install it, somebody has looked at it.
Your slug is taken globally either way, including against plugins you cannot see. It is the namespace your tools are exposed under and that namespace is shared by everything on the platform, so my_thing__run has to mean one thing whoever is asking.
Changing private → public or unlisted re-runs the whole submission triage at that moment — we re-probe your endpoint and re-check your tools — so registering privately and flipping the switch later is not a way around review, it is just review at the point it starts mattering. Going back to private is free and immediate; anyone who had installed it keeps the install but loses the tools, and gets them back if you publish again.
Review
You can call your own plugin the moment it is registered — building before talking to anyone is the point. Review gates other people installing it. Most clean submissions publish automatically; a hold always comes with the reason, and the ones that hold are mechanical: an endpoint that does not answer, one that is not an MCP server, or a manifest declaring tools the endpoint does not advertise.
A useful consequence: a plugin behind a development tunnel is refused for publication (it stops answering the moment you close your laptop) but registers privately without argument. The objection to a tunnel is that it disappears on other people, and a private plugin has no other people.
v1 — new fields, new endpoints, new enum values. Ignore unknown fields; a client that rejects them will break on a routine release. Breaking changes get a /v2, and v1 is supported for at least 12 months after one exists. Send Vidmoat-Version: 2026-08-01 to pin date-based behaviour within v1.