@rendobar/mcp 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -2
- package/dist/bin.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,99 @@ get_job { "jobId": "job_9f2a", "wait": true }
|
|
|
69
69
|
The agent writes the filter. Rendobar runs it. Nothing gets installed on your
|
|
70
70
|
machine, and `-c:v copy` means the video stream is never re-encoded.
|
|
71
71
|
|
|
72
|
+
## Two more things to ask for
|
|
73
|
+
|
|
74
|
+
**Hit a size budget.**
|
|
75
|
+
|
|
76
|
+
> **You:** Get `demo.mov` under 25 MB so I can email it.
|
|
77
|
+
|
|
78
|
+
```jsonc
|
|
79
|
+
upload_file { "path": "~/recordings/demo.mov" }
|
|
80
|
+
// → { "downloadUrl": "https://cdn.rendobar.com/u/7c1e/demo.mov", "sizeBytes": 251658240 }
|
|
81
|
+
|
|
82
|
+
submit_job { "type": "compress.target",
|
|
83
|
+
"inputs": { "source": "https://cdn.rendobar.com/u/7c1e/demo.mov" },
|
|
84
|
+
"params": { "for": "web", "target": { "maxSize": "25MB" } } }
|
|
85
|
+
// → { "jobId": "job_4b8d", "status": "waiting" }
|
|
86
|
+
|
|
87
|
+
get_job { "jobId": "job_4b8d", "wait": true }
|
|
88
|
+
// → complete · https://cdn.rendobar.com/o/job_4b8d/out.mp4 · 23.8 MB
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
You give it the ceiling, not a bitrate. The encoder searches candidate encodes
|
|
92
|
+
and returns the smallest file that still clears the quality bar, so you are not
|
|
93
|
+
guessing at CRF values to land under a mail server's limit.
|
|
94
|
+
|
|
95
|
+
**Generate an image.**
|
|
96
|
+
|
|
97
|
+
> **You:** Make a 1920x1080 title card for a video about deep sea diving.
|
|
98
|
+
|
|
99
|
+
```jsonc
|
|
100
|
+
submit_job { "type": "image.generate",
|
|
101
|
+
"inputs": {},
|
|
102
|
+
"params": { "model": "standard",
|
|
103
|
+
"prompt": "Title card for a deep sea diving documentary. Shafts of light through deep blue water, small diver silhouette, empty space across the upper third for a title.",
|
|
104
|
+
"width": 1920, "height": 1080 } }
|
|
105
|
+
// → { "jobId": "job_2fa7", "status": "waiting" }
|
|
106
|
+
|
|
107
|
+
get_job { "jobId": "job_2fa7", "wait": true }
|
|
108
|
+
// → complete · https://cdn.rendobar.com/o/job_2fa7/out.png
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`inputs` is empty because nothing is being transformed. Ask for a tier
|
|
112
|
+
(`economy`, `standard`, `premium`) and the platform picks the model, or pin an
|
|
113
|
+
exact model id to reach its own controls. Requested dimensions are snapped to
|
|
114
|
+
what the chosen model can actually render.
|
|
115
|
+
|
|
116
|
+
## The rest of the surface
|
|
117
|
+
|
|
118
|
+
Four more tools, and the prompts that reach them.
|
|
119
|
+
|
|
120
|
+
> **You:** What can Rendobar actually do?
|
|
121
|
+
|
|
122
|
+
```jsonc
|
|
123
|
+
list_job_types {}
|
|
124
|
+
// → { "jobTypes": [ { "type": "compose", "tag": "Compose",
|
|
125
|
+
// "summary": "Render a video from a declarative JSON timeline",
|
|
126
|
+
// "acceptsMedia": ["video", "image", "audio"] }, ... ],
|
|
127
|
+
// "guidance": "..." }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Read live from the job registry on every call, which is why nothing in this
|
|
131
|
+
README enumerates job types. A new one appears here without a release.
|
|
132
|
+
|
|
133
|
+
> **You:** How much credit is left?
|
|
134
|
+
|
|
135
|
+
```jsonc
|
|
136
|
+
get_account {}
|
|
137
|
+
// → { "balance": "$4.86", "balanceUsd": 4.86, "plan": "free", "isPro": false,
|
|
138
|
+
// "limits": { "concurrentJobs": 1, "maxFileSize": "500 MB", "jobTimeoutMin": 5 } }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Worth a call before submitting something expensive.
|
|
142
|
+
|
|
143
|
+
> **You:** What did I run this morning?
|
|
144
|
+
|
|
145
|
+
```jsonc
|
|
146
|
+
list_jobs { "status": "complete", "limit": 5 }
|
|
147
|
+
// → { "jobs": [ { "id": "job_9f2a", "type": "ffmpeg", "status": "complete",
|
|
148
|
+
// "createdAt": "2026-08-04T09:12:00Z", "cost": "$0.01",
|
|
149
|
+
// "output": { "url": "https://cdn.rendobar.com/o/job_9f2a/out.mp4" } } ] }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The compact row is enough to find a result you lost. Call `get_job` when you
|
|
153
|
+
need the full output.
|
|
154
|
+
|
|
155
|
+
> **You:** Stop that one, I picked the wrong file.
|
|
156
|
+
|
|
157
|
+
```jsonc
|
|
158
|
+
cancel_job { "jobId": "job_9f2a" }
|
|
159
|
+
// → { "id": "job_9f2a", "status": "cancelled" }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Works on `waiting`, `dispatched` and `running` jobs. A running job's upstream
|
|
163
|
+
execution is stopped too, so you are not billed for work you cancelled.
|
|
164
|
+
|
|
72
165
|
## Install
|
|
73
166
|
|
|
74
167
|
Rendobar has two MCP servers. Pick by whether the agent needs your filesystem.
|
|
@@ -97,7 +190,9 @@ Already ran `rb login` with the Rendobar CLI? Drop `--env`. The server finds the
|
|
|
97
190
|
<details>
|
|
98
191
|
<summary><strong>Claude Desktop, Cursor, Cline, Windsurf</strong></summary>
|
|
99
192
|
|
|
100
|
-
Same block for all four. Claude Desktop: `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows). Cursor: `~/.cursor/mcp.json
|
|
193
|
+
Same block for all four. Claude Desktop: `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows). Cursor: `~/.cursor/mcp.json` on every OS. Windsurf: `~/.codeium/windsurf/mcp_config.json` on every OS. Cline: MCP panel → Configure.
|
|
194
|
+
|
|
195
|
+
On Linux, use Cursor, Windsurf, Cline, Zed, VS Code or Continue. Claude Desktop has no Linux build, so it is the one client on this list you cannot use there. The server itself runs fine on Linux.
|
|
101
196
|
|
|
102
197
|
```json
|
|
103
198
|
{
|
|
@@ -158,7 +253,7 @@ env:
|
|
|
158
253
|
```
|
|
159
254
|
</details>
|
|
160
255
|
|
|
161
|
-
Needs Node 20.10 or later
|
|
256
|
+
Runs on **macOS, Linux and Windows**. Every release is tested on all three in CI. There are no native dependencies, so architecture does not matter: x64 and arm64 both work. Needs Node 20.10 or later, and the server checks at startup and exits with a clear message on older versions.
|
|
162
257
|
|
|
163
258
|
## Tools
|
|
164
259
|
|
package/dist/bin.js
CHANGED
|
@@ -34,7 +34,7 @@ FFmpeg inputs accept a URL string, { url }, { content } (inline text staged verb
|
|
|
34
34
|
|
|
35
35
|
FFmpeg also accepts an optional params.compute ('auto' | 'cpu' | 'gpu'). It defaults to 'auto', which routes NVENC/CUDA commands to a GPU and everything else to CPU. Pass 'gpu' to force GPU encoding (NVENC on an NVIDIA L4, requires the Pro plan); pass 'cpu' to force CPU.
|
|
36
36
|
|
|
37
|
-
For local files, call upload_file first to get a downloadUrl, then use it as inputs.source. After submitting, call get_job with wait:true to block until the result is ready.`,ce=t.union([t.string(),t.object({url:t.string()}),t.object({content:t.string()}),t.object({job:t.string().regex(/^job_[A-Za-z0-9_-]+$/)})]),pe={type:t.string().describe("Job type from the registry. Call list_job_types for the current list. Use 'ffmpeg' for custom FFmpeg commands."),inputs:t.record(t.string(),ce).describe(`Map of input name to source. Each value is a URL string, { url }, { content } (inline text for subtitle files or ffmpeg concat lists), or { job: "job_..." } (a completed job's output, resolves only for ffmpeg inputs; for other job types pass the prior job's output URL from get_job instead). For FFmpeg: keys match filenames in the command.`),params:t.record(t.string(),t.unknown()).optional().describe("Type-specific parameters. For ffmpeg: { command: '...', compute?: 'auto' | 'cpu' | 'gpu' } \u2014 compute defaults to 'auto' and routes NVENC/CUDA commands to a GPU; 'gpu' forces GPU encoding (NVIDIA L4, Pro plan), 'cpu' forces CPU."),idempotencyKey:t.string().optional().describe("Prevents duplicate jobs on retry. Unique value per logical operation.")};async function de(e,n){if(!Q(e)||e.code!=="INVALID_JOB_TYPE")return e;let o=[e.message];try{let r=await d(n).jobs.types();o.push(`Live types right now: ${r.map(s=>s.type).join(", ")}.`)}catch{}return e.message.includes("list_job_types")||o.push("Call list_job_types for the current list."),new q(e.code,e.statusCode,o.join(" "),e.details,e.retryAfter)}var ue={name:"submit_job",title:"Submit Rendobar Job",description:le,inputSchema:pe,outputSchema:{jobId:t.string(),status:t.string().describe("Initial status, normally 'waiting'")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(e,n)=>{try{let o=await d(n).jobs.create({type:e.type,inputs:e.inputs,params:e.params,idempotencyKey:e.idempotencyKey});return{jobId:o.id,status:o.status}}catch(o){throw await de(o,n)}}},me={name:"cancel_job",title:"Cancel Rendobar Job",description:"Cancel a job. Jobs in status 'waiting', 'dispatched' or 'running' can be cancelled (a running job's upstream execution is stopped too). Use when the user changes their mind, or when you submitted the wrong job. Completed, failed, or already-cancelled jobs cannot be cancelled.",inputSchema:{jobId:t.string().describe("Job ID to cancel (e.g. 'job_abc123')")},outputSchema:{id:t.string(),status:t.string().describe("'cancelled' on success")},annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},execute:async(e,n)=>{let o=await d(n).jobs.cancel(e.jobId);return{id:o.id,status:o.status}}},fe=t.object({type:t.string(),tag:t.string(),summary:t.string(),acceptsMedia:t.array(t.string())}),ge=`Pick a type by its summary and acceptsMedia (the media kinds it takes), then call submit_job with that type. To chain jobs, pass a completed job's output as the next job's input: use { job: "job_..." } for ffmpeg; for every other job type, get the completed job's output URL from get_job and pass that URL instead.`,be={name:"list_job_types",title:"List Rendobar Job Types",description:"List every active Rendobar job type with its summary and the media kinds it accepts. Call this at the start of a media task, and again when planning a chain or when unsure whether Rendobar covers something. Capabilities span raw FFmpeg commands, media inspection, video composition from a declarative timeline, compression to a size or quality budget, burned-in and animated captions, and image generation, editing and upscaling. The type list is read live from the job registry on every call, so it is always current and is never cached in this description. Takes no arguments. Read-only: it never submits or changes a job.",inputSchema:{},outputSchema:{jobTypes:t.array(t.object({type:t.string().describe("Job type identifier, e.g. 'ffmpeg'"),tag:t.string().describe("Category tag"),summary:t.string().describe("Short description"),acceptsMedia:t.array(t.string()).describe("Media kinds this type accepts, e.g. video, image, audio")})),guidance:t.string()},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,n)=>({jobTypes:(await d(n).jobs.types()).map(r=>fe.parse(r)),guidance:ge})},b=e=>e;function P(){return[b(se),b(ae),b(ue),b(me),b(be)]}import{z as w}from"zod";import{promises as he}from"fs";import R from"path";import{promises as L}from"fs";import D from"path";async function F(e,n){let o=D.resolve(n.cwd,e),r=await L.realpath(o);if(n.roots!==void 0&&n.roots.length>0&&!(await Promise.all(n.roots.map(i=>L.realpath(i).catch(()=>null)))).some(i=>i!==null&&(r===i||r.startsWith(i+D.sep))))throw new Error(`Path is outside the allowed MCP roots: ${r}`);return r}async function ye(e){if(e.cachedMaxFileSize!==null)return e.cachedMaxFileSize;let n=await d(e).billing.state();return e.cachedMaxFileSize=n.plan.limits.maxInputFileSize,e.cachedMaxFileSize}var we={name:"upload_file",title:"Upload Local File to Rendobar",description:"Read a local file and upload it to Rendobar. Returns a downloadUrl to use as input in submit_job. If the file is already at a public HTTPS URL, skip this and pass the URL directly to submit_job.",inputSchema:{path:w.string().describe("Absolute or working-dir-relative path to the file"),filename:w.string().optional().describe("Filename hint sent to Rendobar (defaults to basename of path)")},outputSchema:{downloadUrl:w.string().url(),sizeBytes:w.number()},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(e,n,o)=>{let r=await F(e.path,{cwd:process.cwd()}),s=await he.open(r,"r");try{let a=await s.stat();if(!a.isFile())throw new Error(`Path is not a regular file: ${R.basename(r)}`);let i=a.size,l=await ye(n);if(i>l)throw new Error(`File size (${i} bytes) exceeds plan limit (${l} bytes). Upgrade your plan for larger uploads.`);n.logger.debug({msg:"upload_start",basename:R.basename(r),sizeBytes:i});let c=await s.readFile(),p=new Blob([c]),u=await d(n).uploads.create(p,{filename:e.filename??R.basename(r),signal:o.signal});return n.logger.info({msg:"upload_complete",basename:R.basename(r),sizeBytes:i}),{downloadUrl:u.url,sizeBytes:i}}finally{await s.close()}}},Re=e=>e;function N(){return[Re(we)]}function U(e,n){for(let o of k())h(e,n,o);for(let o of P())h(e,n,o);for(let o of N())h(e,n,o)}import{PostHog as _e}from"posthog-node";import{instrument as je}from"@posthog/mcp";var M=process.env.RENDOBAR_TELEMETRY_KEY??"phc_pf4JwZ5WGtDcWDG6kYEkDZtEAy7dbunkNHthLj8JRa9v",ve=process.env.RENDOBAR_TELEMETRY_HOST??"https://e.rendobar.com",Se=["$mcp_parameters","$mcp_response"];function Te(){if(process.env.DO_NOT_TRACK&&process.env.DO_NOT_TRACK!=="0")return!0;let e=(process.env.RENDOBAR_TELEMETRY??"").toLowerCase();return!!(e==="0"||e==="false"||e==="off"||e==="no"||process.env.RENDOBAR_NO_TELEMETRY||process.env.RENDOBAR_DISABLE_TELEMETRY||process.env.CI==="true")}function xe(){return M.length>0&&!Te()}function Ee(e){for(let n of Se)delete e[n];return e}function B(e,n){if(!xe())return null;let o=new _e(M,{host:ve,flushAt:1,flushInterval:0});return je(e,o,{beforeSend:r=>(r.properties=Ee(r.properties),r)}),n.info({msg:"Anonymous MCP usage analytics on (tool name, success, duration, intent \u2014 never your parameters, responses, or credentials). Disable with DO_NOT_TRACK=1 or RENDOBAR_TELEMETRY=0."}),async()=>{try{await o.shutdown()}catch{}}}var Ae="1.
|
|
37
|
+
For local files, call upload_file first to get a downloadUrl, then use it as inputs.source. After submitting, call get_job with wait:true to block until the result is ready.`,ce=t.union([t.string(),t.object({url:t.string()}),t.object({content:t.string()}),t.object({job:t.string().regex(/^job_[A-Za-z0-9_-]+$/)})]),pe={type:t.string().describe("Job type from the registry. Call list_job_types for the current list. Use 'ffmpeg' for custom FFmpeg commands."),inputs:t.record(t.string(),ce).describe(`Map of input name to source. Each value is a URL string, { url }, { content } (inline text for subtitle files or ffmpeg concat lists), or { job: "job_..." } (a completed job's output, resolves only for ffmpeg inputs; for other job types pass the prior job's output URL from get_job instead). For FFmpeg: keys match filenames in the command.`),params:t.record(t.string(),t.unknown()).optional().describe("Type-specific parameters. For ffmpeg: { command: '...', compute?: 'auto' | 'cpu' | 'gpu' } \u2014 compute defaults to 'auto' and routes NVENC/CUDA commands to a GPU; 'gpu' forces GPU encoding (NVIDIA L4, Pro plan), 'cpu' forces CPU."),idempotencyKey:t.string().optional().describe("Prevents duplicate jobs on retry. Unique value per logical operation.")};async function de(e,n){if(!Q(e)||e.code!=="INVALID_JOB_TYPE")return e;let o=[e.message];try{let r=await d(n).jobs.types();o.push(`Live types right now: ${r.map(s=>s.type).join(", ")}.`)}catch{}return e.message.includes("list_job_types")||o.push("Call list_job_types for the current list."),new q(e.code,e.statusCode,o.join(" "),e.details,e.retryAfter)}var ue={name:"submit_job",title:"Submit Rendobar Job",description:le,inputSchema:pe,outputSchema:{jobId:t.string(),status:t.string().describe("Initial status, normally 'waiting'")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(e,n)=>{try{let o=await d(n).jobs.create({type:e.type,inputs:e.inputs,params:e.params,idempotencyKey:e.idempotencyKey});return{jobId:o.id,status:o.status}}catch(o){throw await de(o,n)}}},me={name:"cancel_job",title:"Cancel Rendobar Job",description:"Cancel a job. Jobs in status 'waiting', 'dispatched' or 'running' can be cancelled (a running job's upstream execution is stopped too). Use when the user changes their mind, or when you submitted the wrong job. Completed, failed, or already-cancelled jobs cannot be cancelled.",inputSchema:{jobId:t.string().describe("Job ID to cancel (e.g. 'job_abc123')")},outputSchema:{id:t.string(),status:t.string().describe("'cancelled' on success")},annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},execute:async(e,n)=>{let o=await d(n).jobs.cancel(e.jobId);return{id:o.id,status:o.status}}},fe=t.object({type:t.string(),tag:t.string(),summary:t.string(),acceptsMedia:t.array(t.string())}),ge=`Pick a type by its summary and acceptsMedia (the media kinds it takes), then call submit_job with that type. To chain jobs, pass a completed job's output as the next job's input: use { job: "job_..." } for ffmpeg; for every other job type, get the completed job's output URL from get_job and pass that URL instead.`,be={name:"list_job_types",title:"List Rendobar Job Types",description:"List every active Rendobar job type with its summary and the media kinds it accepts. Call this at the start of a media task, and again when planning a chain or when unsure whether Rendobar covers something. Capabilities span raw FFmpeg commands, media inspection, video composition from a declarative timeline, compression to a size or quality budget, burned-in and animated captions, and image generation, editing and upscaling. The type list is read live from the job registry on every call, so it is always current and is never cached in this description. Takes no arguments. Read-only: it never submits or changes a job.",inputSchema:{},outputSchema:{jobTypes:t.array(t.object({type:t.string().describe("Job type identifier, e.g. 'ffmpeg'"),tag:t.string().describe("Category tag"),summary:t.string().describe("Short description"),acceptsMedia:t.array(t.string()).describe("Media kinds this type accepts, e.g. video, image, audio")})),guidance:t.string()},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,n)=>({jobTypes:(await d(n).jobs.types()).map(r=>fe.parse(r)),guidance:ge})},b=e=>e;function P(){return[b(se),b(ae),b(ue),b(me),b(be)]}import{z as w}from"zod";import{promises as he}from"fs";import R from"path";import{promises as L}from"fs";import D from"path";async function F(e,n){let o=D.resolve(n.cwd,e),r=await L.realpath(o);if(n.roots!==void 0&&n.roots.length>0&&!(await Promise.all(n.roots.map(i=>L.realpath(i).catch(()=>null)))).some(i=>i!==null&&(r===i||r.startsWith(i+D.sep))))throw new Error(`Path is outside the allowed MCP roots: ${r}`);return r}async function ye(e){if(e.cachedMaxFileSize!==null)return e.cachedMaxFileSize;let n=await d(e).billing.state();return e.cachedMaxFileSize=n.plan.limits.maxInputFileSize,e.cachedMaxFileSize}var we={name:"upload_file",title:"Upload Local File to Rendobar",description:"Read a local file and upload it to Rendobar. Returns a downloadUrl to use as input in submit_job. If the file is already at a public HTTPS URL, skip this and pass the URL directly to submit_job.",inputSchema:{path:w.string().describe("Absolute or working-dir-relative path to the file"),filename:w.string().optional().describe("Filename hint sent to Rendobar (defaults to basename of path)")},outputSchema:{downloadUrl:w.string().url(),sizeBytes:w.number()},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(e,n,o)=>{let r=await F(e.path,{cwd:process.cwd()}),s=await he.open(r,"r");try{let a=await s.stat();if(!a.isFile())throw new Error(`Path is not a regular file: ${R.basename(r)}`);let i=a.size,l=await ye(n);if(i>l)throw new Error(`File size (${i} bytes) exceeds plan limit (${l} bytes). Upgrade your plan for larger uploads.`);n.logger.debug({msg:"upload_start",basename:R.basename(r),sizeBytes:i});let c=await s.readFile(),p=new Blob([c]),u=await d(n).uploads.create(p,{filename:e.filename??R.basename(r),signal:o.signal});return n.logger.info({msg:"upload_complete",basename:R.basename(r),sizeBytes:i}),{downloadUrl:u.url,sizeBytes:i}}finally{await s.close()}}},Re=e=>e;function N(){return[Re(we)]}function U(e,n){for(let o of k())h(e,n,o);for(let o of P())h(e,n,o);for(let o of N())h(e,n,o)}import{PostHog as _e}from"posthog-node";import{instrument as je}from"@posthog/mcp";var M=process.env.RENDOBAR_TELEMETRY_KEY??"phc_pf4JwZ5WGtDcWDG6kYEkDZtEAy7dbunkNHthLj8JRa9v",ve=process.env.RENDOBAR_TELEMETRY_HOST??"https://e.rendobar.com",Se=["$mcp_parameters","$mcp_response"];function Te(){if(process.env.DO_NOT_TRACK&&process.env.DO_NOT_TRACK!=="0")return!0;let e=(process.env.RENDOBAR_TELEMETRY??"").toLowerCase();return!!(e==="0"||e==="false"||e==="off"||e==="no"||process.env.RENDOBAR_NO_TELEMETRY||process.env.RENDOBAR_DISABLE_TELEMETRY||process.env.CI==="true")}function xe(){return M.length>0&&!Te()}function Ee(e){for(let n of Se)delete e[n];return e}function B(e,n){if(!xe())return null;let o=new _e(M,{host:ve,flushAt:1,flushInterval:0});return je(e,o,{beforeSend:r=>(r.properties=Ee(r.properties),r)}),n.info({msg:"Anonymous MCP usage analytics on (tool name, success, duration, intent \u2014 never your parameters, responses, or credentials). Disable with DO_NOT_TRACK=1 or RENDOBAR_TELEMETRY=0."}),async()=>{try{await o.shutdown()}catch{}}}var Ae="1.10.0";async function H(e){let n=E(e.config,e.logger),o=new Ce({name:"rendobar",version:Ae},{capabilities:{tools:{},logging:{}},instructions:C});U(o,n);let r=B(o,e.logger);return{server:o,cleanup:async()=>{r&&await r()}}}var j="1.10.0",Oe=`@rendobar/mcp v${j}
|
|
38
38
|
Local stdio Model Context Protocol server for Rendobar.
|
|
39
39
|
|
|
40
40
|
Usage:
|
package/dist/index.js
CHANGED
|
@@ -33,4 +33,4 @@ FFmpeg inputs accept a URL string, { url }, { content } (inline text staged verb
|
|
|
33
33
|
|
|
34
34
|
FFmpeg also accepts an optional params.compute ('auto' | 'cpu' | 'gpu'). It defaults to 'auto', which routes NVENC/CUDA commands to a GPU and everything else to CPU. Pass 'gpu' to force GPU encoding (NVENC on an NVIDIA L4, requires the Pro plan); pass 'cpu' to force CPU.
|
|
35
35
|
|
|
36
|
-
For local files, call upload_file first to get a downloadUrl, then use it as inputs.source. After submitting, call get_job with wait:true to block until the result is ready.`,q=t.union([t.string(),t.object({url:t.string()}),t.object({content:t.string()}),t.object({job:t.string().regex(/^job_[A-Za-z0-9_-]+$/)})]),Q={type:t.string().describe("Job type from the registry. Call list_job_types for the current list. Use 'ffmpeg' for custom FFmpeg commands."),inputs:t.record(t.string(),q).describe(`Map of input name to source. Each value is a URL string, { url }, { content } (inline text for subtitle files or ffmpeg concat lists), or { job: "job_..." } (a completed job's output, resolves only for ffmpeg inputs; for other job types pass the prior job's output URL from get_job instead). For FFmpeg: keys match filenames in the command.`),params:t.record(t.string(),t.unknown()).optional().describe("Type-specific parameters. For ffmpeg: { command: '...', compute?: 'auto' | 'cpu' | 'gpu' } \u2014 compute defaults to 'auto' and routes NVENC/CUDA commands to a GPU; 'gpu' forces GPU encoding (NVIDIA L4, Pro plan), 'cpu' forces CPU."),idempotencyKey:t.string().optional().describe("Prevents duplicate jobs on retry. Unique value per logical operation.")};async function X(e,n){if(!H(e)||e.code!=="INVALID_JOB_TYPE")return e;let o=[e.message];try{let s=await c(n).jobs.types();o.push(`Live types right now: ${s.map(r=>r.type).join(", ")}.`)}catch{}return e.message.includes("list_job_types")||o.push("Call list_job_types for the current list."),new M(e.code,e.statusCode,o.join(" "),e.details,e.retryAfter)}var ee={name:"submit_job",title:"Submit Rendobar Job",description:W,inputSchema:Q,outputSchema:{jobId:t.string(),status:t.string().describe("Initial status, normally 'waiting'")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(e,n)=>{try{let o=await c(n).jobs.create({type:e.type,inputs:e.inputs,params:e.params,idempotencyKey:e.idempotencyKey});return{jobId:o.id,status:o.status}}catch(o){throw await X(o,n)}}},te={name:"cancel_job",title:"Cancel Rendobar Job",description:"Cancel a job. Jobs in status 'waiting', 'dispatched' or 'running' can be cancelled (a running job's upstream execution is stopped too). Use when the user changes their mind, or when you submitted the wrong job. Completed, failed, or already-cancelled jobs cannot be cancelled.",inputSchema:{jobId:t.string().describe("Job ID to cancel (e.g. 'job_abc123')")},outputSchema:{id:t.string(),status:t.string().describe("'cancelled' on success")},annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},execute:async(e,n)=>{let o=await c(n).jobs.cancel(e.jobId);return{id:o.id,status:o.status}}},oe=t.object({type:t.string(),tag:t.string(),summary:t.string(),acceptsMedia:t.array(t.string())}),ne=`Pick a type by its summary and acceptsMedia (the media kinds it takes), then call submit_job with that type. To chain jobs, pass a completed job's output as the next job's input: use { job: "job_..." } for ffmpeg; for every other job type, get the completed job's output URL from get_job and pass that URL instead.`,re={name:"list_job_types",title:"List Rendobar Job Types",description:"List every active Rendobar job type with its summary and the media kinds it accepts. Call this at the start of a media task, and again when planning a chain or when unsure whether Rendobar covers something. Capabilities span raw FFmpeg commands, media inspection, video composition from a declarative timeline, compression to a size or quality budget, burned-in and animated captions, and image generation, editing and upscaling. The type list is read live from the job registry on every call, so it is always current and is never cached in this description. Takes no arguments. Read-only: it never submits or changes a job.",inputSchema:{},outputSchema:{jobTypes:t.array(t.object({type:t.string().describe("Job type identifier, e.g. 'ffmpeg'"),tag:t.string().describe("Category tag"),summary:t.string().describe("Short description"),acceptsMedia:t.array(t.string()).describe("Media kinds this type accepts, e.g. video, image, audio")})),guidance:t.string()},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,n)=>({jobTypes:(await c(n).jobs.types()).map(s=>oe.parse(s)),guidance:ne})},m=e=>e;function x(){return[m(Y),m(V),m(ee),m(te),m(re)]}import{z as h}from"zod";import{promises as se}from"fs";import y from"path";import{promises as E}from"fs";import C from"path";async function I(e,n){let o=C.resolve(n.cwd,e),s=await E.realpath(o);if(n.roots!==void 0&&n.roots.length>0&&!(await Promise.all(n.roots.map(i=>E.realpath(i).catch(()=>null)))).some(i=>i!==null&&(s===i||s.startsWith(i+C.sep))))throw new Error(`Path is outside the allowed MCP roots: ${s}`);return s}async function ie(e){if(e.cachedMaxFileSize!==null)return e.cachedMaxFileSize;let n=await c(e).billing.state();return e.cachedMaxFileSize=n.plan.limits.maxInputFileSize,e.cachedMaxFileSize}var ae={name:"upload_file",title:"Upload Local File to Rendobar",description:"Read a local file and upload it to Rendobar. Returns a downloadUrl to use as input in submit_job. If the file is already at a public HTTPS URL, skip this and pass the URL directly to submit_job.",inputSchema:{path:h.string().describe("Absolute or working-dir-relative path to the file"),filename:h.string().optional().describe("Filename hint sent to Rendobar (defaults to basename of path)")},outputSchema:{downloadUrl:h.string().url(),sizeBytes:h.number()},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(e,n,o)=>{let s=await I(e.path,{cwd:process.cwd()}),r=await se.open(s,"r");try{let a=await r.stat();if(!a.isFile())throw new Error(`Path is not a regular file: ${y.basename(s)}`);let i=a.size,l=await ie(n);if(i>l)throw new Error(`File size (${i} bytes) exceeds plan limit (${l} bytes). Upgrade your plan for larger uploads.`);n.logger.debug({msg:"upload_start",basename:y.basename(s),sizeBytes:i});let p=await r.readFile(),u=new Blob([p]),w=await c(n).uploads.create(u,{filename:e.filename??y.basename(s),signal:o.signal});return n.logger.info({msg:"upload_complete",basename:y.basename(s),sizeBytes:i}),{downloadUrl:w.url,sizeBytes:i}}finally{await r.close()}}},le=e=>e;function A(){return[le(ae)]}function O(e,n){for(let o of T())b(e,n,o);for(let o of x())b(e,n,o);for(let o of A())b(e,n,o)}import{PostHog as ce}from"posthog-node";import{instrument as pe}from"@posthog/mcp";var k=process.env.RENDOBAR_TELEMETRY_KEY??"phc_pf4JwZ5WGtDcWDG6kYEkDZtEAy7dbunkNHthLj8JRa9v",ue=process.env.RENDOBAR_TELEMETRY_HOST??"https://e.rendobar.com",de=["$mcp_parameters","$mcp_response"];function me(){if(process.env.DO_NOT_TRACK&&process.env.DO_NOT_TRACK!=="0")return!0;let e=(process.env.RENDOBAR_TELEMETRY??"").toLowerCase();return!!(e==="0"||e==="false"||e==="off"||e==="no"||process.env.RENDOBAR_NO_TELEMETRY||process.env.RENDOBAR_DISABLE_TELEMETRY||process.env.CI==="true")}function fe(){return k.length>0&&!me()}function be(e){for(let n of de)delete e[n];return e}function P(e,n){if(!fe())return null;let o=new ce(k,{host:ue,flushAt:1,flushInterval:0});return pe(e,o,{beforeSend:s=>(s.properties=be(s.properties),s)}),n.info({msg:"Anonymous MCP usage analytics on (tool name, success, duration, intent \u2014 never your parameters, responses, or credentials). Disable with DO_NOT_TRACK=1 or RENDOBAR_TELEMETRY=0."}),async()=>{try{await o.shutdown()}catch{}}}var he="1.
|
|
36
|
+
For local files, call upload_file first to get a downloadUrl, then use it as inputs.source. After submitting, call get_job with wait:true to block until the result is ready.`,q=t.union([t.string(),t.object({url:t.string()}),t.object({content:t.string()}),t.object({job:t.string().regex(/^job_[A-Za-z0-9_-]+$/)})]),Q={type:t.string().describe("Job type from the registry. Call list_job_types for the current list. Use 'ffmpeg' for custom FFmpeg commands."),inputs:t.record(t.string(),q).describe(`Map of input name to source. Each value is a URL string, { url }, { content } (inline text for subtitle files or ffmpeg concat lists), or { job: "job_..." } (a completed job's output, resolves only for ffmpeg inputs; for other job types pass the prior job's output URL from get_job instead). For FFmpeg: keys match filenames in the command.`),params:t.record(t.string(),t.unknown()).optional().describe("Type-specific parameters. For ffmpeg: { command: '...', compute?: 'auto' | 'cpu' | 'gpu' } \u2014 compute defaults to 'auto' and routes NVENC/CUDA commands to a GPU; 'gpu' forces GPU encoding (NVIDIA L4, Pro plan), 'cpu' forces CPU."),idempotencyKey:t.string().optional().describe("Prevents duplicate jobs on retry. Unique value per logical operation.")};async function X(e,n){if(!H(e)||e.code!=="INVALID_JOB_TYPE")return e;let o=[e.message];try{let s=await c(n).jobs.types();o.push(`Live types right now: ${s.map(r=>r.type).join(", ")}.`)}catch{}return e.message.includes("list_job_types")||o.push("Call list_job_types for the current list."),new M(e.code,e.statusCode,o.join(" "),e.details,e.retryAfter)}var ee={name:"submit_job",title:"Submit Rendobar Job",description:W,inputSchema:Q,outputSchema:{jobId:t.string(),status:t.string().describe("Initial status, normally 'waiting'")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(e,n)=>{try{let o=await c(n).jobs.create({type:e.type,inputs:e.inputs,params:e.params,idempotencyKey:e.idempotencyKey});return{jobId:o.id,status:o.status}}catch(o){throw await X(o,n)}}},te={name:"cancel_job",title:"Cancel Rendobar Job",description:"Cancel a job. Jobs in status 'waiting', 'dispatched' or 'running' can be cancelled (a running job's upstream execution is stopped too). Use when the user changes their mind, or when you submitted the wrong job. Completed, failed, or already-cancelled jobs cannot be cancelled.",inputSchema:{jobId:t.string().describe("Job ID to cancel (e.g. 'job_abc123')")},outputSchema:{id:t.string(),status:t.string().describe("'cancelled' on success")},annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},execute:async(e,n)=>{let o=await c(n).jobs.cancel(e.jobId);return{id:o.id,status:o.status}}},oe=t.object({type:t.string(),tag:t.string(),summary:t.string(),acceptsMedia:t.array(t.string())}),ne=`Pick a type by its summary and acceptsMedia (the media kinds it takes), then call submit_job with that type. To chain jobs, pass a completed job's output as the next job's input: use { job: "job_..." } for ffmpeg; for every other job type, get the completed job's output URL from get_job and pass that URL instead.`,re={name:"list_job_types",title:"List Rendobar Job Types",description:"List every active Rendobar job type with its summary and the media kinds it accepts. Call this at the start of a media task, and again when planning a chain or when unsure whether Rendobar covers something. Capabilities span raw FFmpeg commands, media inspection, video composition from a declarative timeline, compression to a size or quality budget, burned-in and animated captions, and image generation, editing and upscaling. The type list is read live from the job registry on every call, so it is always current and is never cached in this description. Takes no arguments. Read-only: it never submits or changes a job.",inputSchema:{},outputSchema:{jobTypes:t.array(t.object({type:t.string().describe("Job type identifier, e.g. 'ffmpeg'"),tag:t.string().describe("Category tag"),summary:t.string().describe("Short description"),acceptsMedia:t.array(t.string()).describe("Media kinds this type accepts, e.g. video, image, audio")})),guidance:t.string()},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,n)=>({jobTypes:(await c(n).jobs.types()).map(s=>oe.parse(s)),guidance:ne})},m=e=>e;function x(){return[m(Y),m(V),m(ee),m(te),m(re)]}import{z as h}from"zod";import{promises as se}from"fs";import y from"path";import{promises as E}from"fs";import C from"path";async function I(e,n){let o=C.resolve(n.cwd,e),s=await E.realpath(o);if(n.roots!==void 0&&n.roots.length>0&&!(await Promise.all(n.roots.map(i=>E.realpath(i).catch(()=>null)))).some(i=>i!==null&&(s===i||s.startsWith(i+C.sep))))throw new Error(`Path is outside the allowed MCP roots: ${s}`);return s}async function ie(e){if(e.cachedMaxFileSize!==null)return e.cachedMaxFileSize;let n=await c(e).billing.state();return e.cachedMaxFileSize=n.plan.limits.maxInputFileSize,e.cachedMaxFileSize}var ae={name:"upload_file",title:"Upload Local File to Rendobar",description:"Read a local file and upload it to Rendobar. Returns a downloadUrl to use as input in submit_job. If the file is already at a public HTTPS URL, skip this and pass the URL directly to submit_job.",inputSchema:{path:h.string().describe("Absolute or working-dir-relative path to the file"),filename:h.string().optional().describe("Filename hint sent to Rendobar (defaults to basename of path)")},outputSchema:{downloadUrl:h.string().url(),sizeBytes:h.number()},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(e,n,o)=>{let s=await I(e.path,{cwd:process.cwd()}),r=await se.open(s,"r");try{let a=await r.stat();if(!a.isFile())throw new Error(`Path is not a regular file: ${y.basename(s)}`);let i=a.size,l=await ie(n);if(i>l)throw new Error(`File size (${i} bytes) exceeds plan limit (${l} bytes). Upgrade your plan for larger uploads.`);n.logger.debug({msg:"upload_start",basename:y.basename(s),sizeBytes:i});let p=await r.readFile(),u=new Blob([p]),w=await c(n).uploads.create(u,{filename:e.filename??y.basename(s),signal:o.signal});return n.logger.info({msg:"upload_complete",basename:y.basename(s),sizeBytes:i}),{downloadUrl:w.url,sizeBytes:i}}finally{await r.close()}}},le=e=>e;function A(){return[le(ae)]}function O(e,n){for(let o of T())b(e,n,o);for(let o of x())b(e,n,o);for(let o of A())b(e,n,o)}import{PostHog as ce}from"posthog-node";import{instrument as pe}from"@posthog/mcp";var k=process.env.RENDOBAR_TELEMETRY_KEY??"phc_pf4JwZ5WGtDcWDG6kYEkDZtEAy7dbunkNHthLj8JRa9v",ue=process.env.RENDOBAR_TELEMETRY_HOST??"https://e.rendobar.com",de=["$mcp_parameters","$mcp_response"];function me(){if(process.env.DO_NOT_TRACK&&process.env.DO_NOT_TRACK!=="0")return!0;let e=(process.env.RENDOBAR_TELEMETRY??"").toLowerCase();return!!(e==="0"||e==="false"||e==="off"||e==="no"||process.env.RENDOBAR_NO_TELEMETRY||process.env.RENDOBAR_DISABLE_TELEMETRY||process.env.CI==="true")}function fe(){return k.length>0&&!me()}function be(e){for(let n of de)delete e[n];return e}function P(e,n){if(!fe())return null;let o=new ce(k,{host:ue,flushAt:1,flushInterval:0});return pe(e,o,{beforeSend:s=>(s.properties=be(s.properties),s)}),n.info({msg:"Anonymous MCP usage analytics on (tool name, success, duration, intent \u2014 never your parameters, responses, or credentials). Disable with DO_NOT_TRACK=1 or RENDOBAR_TELEMETRY=0."}),async()=>{try{await o.shutdown()}catch{}}}var he="1.10.0";async function ye(e){let n=R(e.config,e.logger),o=new ge({name:"rendobar",version:he},{capabilities:{tools:{},logging:{}},instructions:j});O(o,n);let s=P(o,e.logger);return{server:o,cleanup:async()=>{s&&await s()}}}export{ye as createRendobarMcpServer};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rendobar/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Rendobar: serverless media processing and generation for AI agents. Transform video, audio and images with FFmpeg, compose from a timeline, burn captions and compress to a budget. Generate and edit images from a text prompt, and upscale on a diffusion model.",
|