@rendobar/mcp 1.9.1 → 1.11.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 +54 -1
- package/dist/bin.js +10 -11
- package/dist/index.js +6 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,6 +113,55 @@ get_job { "jobId": "job_2fa7", "wait": true }
|
|
|
113
113
|
exact model id to reach its own controls. Requested dimensions are snapped to
|
|
114
114
|
what the chosen model can actually render.
|
|
115
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
|
+
|
|
116
165
|
## Install
|
|
117
166
|
|
|
118
167
|
Rendobar has two MCP servers. Pick by whether the agent needs your filesystem.
|
|
@@ -252,7 +301,11 @@ Three sources, first match wins:
|
|
|
252
301
|
2. `RENDOBAR_API_KEY` environment variable
|
|
253
302
|
3. `~/.config/rendobar/credentials.json` on Unix, `%APPDATA%\rendobar\credentials.json` on Windows, written by `rb login` (Rendobar CLI 1.1+)
|
|
254
303
|
|
|
255
|
-
|
|
304
|
+
Installed as a `.mcpb` extension, the key goes in the extension's own settings field and none of the three above apply.
|
|
305
|
+
|
|
306
|
+
The server starts without a key so clients and directories can list its tools, and it makes no network call at startup. Nothing it advertises depends on the registry, so the job type list can never be baked into a build. `list_job_types` reads it live instead, and because `GET /jobs/types` is public it answers without a key at all. Every other tool returns a clear error until a key is set.
|
|
307
|
+
|
|
308
|
+
If you do not need Rendobar to read files off your machine, the hosted server at `https://api.rendobar.com/mcp` signs you in through the browser and there is no key to manage. The local server exists for disk access, and the key is the price of it.
|
|
256
309
|
|
|
257
310
|
## Telemetry
|
|
258
311
|
|
package/dist/bin.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{StdioServerTransport as
|
|
3
|
-
`)};if(e.patchConsole===!0&&f===null){f={log:console.log.bind(console),info:console.info.bind(console),warn:console.warn.bind(console),debug:console.debug.bind(console)};let r=s=>(...a)=>{let i=a.map(l=>typeof l=="string"?l:JSON.stringify(l)).join(" ");o(s,{msg:i,source:"console"})};console.log=r("info"),console.info=r("info"),console.warn=r("warn"),console.debug=r("debug")}return{debug:r=>o("debug",r),info:r=>o("info",r),warn:r=>o("warn",r),error:r=>o("error",r),restoreConsole:()=>{f!==null&&(console.log=f.log,console.info=f.info,console.warn=f.warn,console.debug=f.debug,f=null)}}}import{promises as
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
3. credentials file (written by 'rb login' from the Rendobar CLI)
|
|
2
|
+
import{StdioServerTransport as Oe}from"@modelcontextprotocol/sdk/server/stdio.js";import{homedir as J,platform as Pe}from"os";import _ from"path";var S={debug:10,info:20,warn:30,error:40},f=null;function v(e){let n=S[e.level],o=(r,s)=>{if(S[r]<n)return;let a={level:r,time:Date.now(),...s};e.name!==void 0&&(a.name=e.name),process.stderr.write(JSON.stringify(a)+`
|
|
3
|
+
`)};if(e.patchConsole===!0&&f===null){f={log:console.log.bind(console),info:console.info.bind(console),warn:console.warn.bind(console),debug:console.debug.bind(console)};let r=s=>(...a)=>{let i=a.map(l=>typeof l=="string"?l:JSON.stringify(l)).join(" ");o(s,{msg:i,source:"console"})};console.log=r("info"),console.info=r("info"),console.warn=r("warn"),console.debug=r("debug")}return{debug:r=>o("debug",r),info:r=>o("info",r),warn:r=>o("warn",r),error:r=>o("error",r),restoreConsole:()=>{f!==null&&(console.log=f.log,console.info=f.info,console.warn=f.warn,console.debug=f.debug,f=null)}}}import{promises as V}from"fs";var g=class extends Error{constructor(n){super(n),this.name="ConfigError"}},$="https://api.rendobar.com",Y=new Set(["debug","info","warn","error"]);async function x(e){let n=T(e.argv,"--api-key"),o=T(e.argv,"--api-base"),r={};try{let c=await V.readFile(e.credsPath,"utf8"),d=JSON.parse(c);d!==null&&typeof d=="object"&&(r=d)}catch{}let s=n??e.env.RENDOBAR_API_KEY??r.apiKey,a=o??r.apiBase??$,i=s===void 0||s===""?null:s;if(i!==null&&!i.startsWith("rb_"))throw new g(`Invalid Rendobar API key: must start with 'rb_' (got '${i.slice(0,4)}...').`);let l=e.env.RENDOBAR_LOG_LEVEL??"info";if(!Y.has(l))throw new g(`Invalid RENDOBAR_LOG_LEVEL='${l}'. Use one of: debug, info, warn, error.`);return{apiKey:i,apiBase:a,logLevel:l}}function T(e,n){let o=`${n}=`;for(let r=0;r<e.length;r++){let s=e[r];if(s!==void 0){if(s.startsWith(o))return s.slice(o.length);if(s===n){let a=e[r+1];if(a!==void 0&&!a.startsWith("--"))return a}}}}import{McpServer as Ie}from"@modelcontextprotocol/sdk/server/mcp.js";import{createClient as E}from"@rendobar/sdk";function C(e,n){let o=e.apiKey===null?null:E({apiKey:e.apiKey,baseUrl:e.apiBase});return{logger:n,sdk:o,config:e,cachedMaxFileSize:null}}function p(e){if(e.sdk===null)throw new g(Z);return e.sdk}function A(e){return e.sdk??E({baseUrl:e.config.apiBase})}var Z=`No Rendobar API key configured. If you installed the Rendobar extension, paste your key in its settings. Otherwise set RENDOBAR_API_KEY, or run 'rb login' with the Rendobar CLI.
|
|
4
|
+
|
|
5
|
+
Get a key at https://app.rendobar.com/settings/api-keys. New accounts start with $5 of free credit and no card.
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
If Rendobar does not need to read files off this machine, add https://api.rendobar.com/mcp as a connector instead and sign in through your browser. There is no key to manage.`;var I=`Rendobar runs media jobs in the cloud and returns hosted output URLs.
|
|
9
8
|
|
|
10
9
|
Call list_job_types first. It reads the job registry live, so it is the only
|
|
11
10
|
current answer to "can Rendobar do this". The job types are not listed here on
|
|
@@ -24,7 +23,7 @@ What Rendobar cannot do:
|
|
|
24
23
|
- Stream live media
|
|
25
24
|
- Run arbitrary local binaries (sharp, imagemagick, yt-dlp) \u2014 submit a job instead
|
|
26
25
|
|
|
27
|
-
For anything list_job_types does not cover, tell the user instead of improvising locally.`;import{isApiError as
|
|
26
|
+
For anything list_job_types does not cover, tell the user instead of improvising locally.`;import{isApiError as W}from"@rendobar/sdk";function k(e,n,o){return async()=>{let r=Date.now();try{let s=await o();return e.logger.info({tool:n,durationMs:Date.now()-r,ok:!0}),{content:[{type:"text",text:JSON.stringify(s,null,2)}],structuredContent:s}}catch(s){let a=Date.now()-r;if(W(s))return e.logger.error({tool:n,durationMs:a,ok:!1,errCode:s.code,errMsg:s.message}),{isError:!0,content:[{type:"text",text:JSON.stringify({error:{code:s.code,message:s.message,retryable:s.statusCode===429}})}]};throw e.logger.error({tool:n,durationMs:a,ok:!1,err:String(s)}),s}}}function h(e,n,o){let r={title:o.title,description:o.description,inputSchema:o.inputSchema,annotations:o.annotations};o.outputSchema!==void 0&&(r.outputSchema=o.outputSchema);let s=async(a,i)=>k(n,o.name,()=>o.execute(a,n,i))();e.registerTool(o.name,r,s)}import{z as m}from"zod";function q(e){return e>=1073741824?`${(e/1073741824).toFixed(1)} GB`:e>=1048576?`${(e/1048576).toFixed(0)} MB`:e>=1024?`${(e/1024).toFixed(0)} KB`:`${e} B`}var X={name:"get_account",title:"Get Rendobar Account",description:"Get the authenticated account's credit balance, plan, and limits. Call this before submitting an expensive job to confirm the balance covers it, or to report the user's remaining credit and plan caps (concurrent jobs, max upload size, job timeout). Takes no arguments. Read-only and idempotent \u2014 it never spends credit or changes anything. Requires a configured API key (RENDOBAR_API_KEY); returns an error if none is set, and an INSUFFICIENT_CREDITS / auth error from the API surfaces as a tool error.",inputSchema:{},outputSchema:{balance:m.string(),balanceUsd:m.number(),plan:m.string(),isPro:m.boolean(),limits:m.object({concurrentJobs:m.number(),maxFileSize:m.string(),maxFileSizeBytes:m.number(),jobTimeoutMin:m.number()})},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,n)=>{let o=await p(n).billing.state();return n.cachedMaxFileSize=o.plan.limits.maxInputFileSize,{balance:`$${o.balance.amount.toFixed(2)}`,balanceUsd:o.balance.amount,plan:o.plan.slug,isPro:o.isPro,limits:{concurrentJobs:o.plan.limits.concurrentJobs,maxFileSize:q(o.plan.limits.maxInputFileSize),maxFileSizeBytes:o.plan.limits.maxInputFileSize,jobTimeoutMin:Math.floor(o.plan.limits.maxJobTimeout/60)}}}};function P(){return[X]}import{z as t}from"zod";import{ApiError as Q,WaitTimeoutError as ee,isApiError as te}from"@rendobar/sdk";var y=t.object({url:t.string(),path:t.string(),type:t.string(),size:t.number(),meta:t.record(t.string(),t.unknown()).optional()}),oe=t.object({data:t.unknown(),file:y.nullable(),files:y.array(),expiresAt:t.number().nullable()}),ne=t.object({output:oe.nullish(),error:t.object({code:t.string(),message:t.string(),detail:t.string().nullable(),retryable:t.boolean()}).nullish(),cost:t.object({amount:t.number(),currency:t.string(),formatted:t.string()}).nullable().optional()});function L(e){let n=ne.safeParse(e);return n.success?n.data:{}}function re(e){let n={};return e.data!==null&&e.data!==void 0&&(n.data=e.data),e.file!==null&&(n.file=e.file),e.files.length>0&&(n.fileCount=e.files.length,n.files=e.files),e.expiresAt!==null&&(n.expiresAt=e.expiresAt),n}var se=t.object({data:t.unknown().optional().describe("Computed JSON answer (probe info, detections, transcript)"),file:y.optional().describe("Headline produced file"),fileCount:t.number().optional(),files:y.array().optional().describe("Every produced file"),expiresAt:t.number().optional().describe("Epoch ms when the file URLs expire")}),ie=t.object({code:t.string(),message:t.string(),detail:t.string().nullable(),retryable:t.boolean()}),ae={name:"list_jobs",title:"List Recent Rendobar Jobs",description:"List recent jobs for the authenticated account, newest first. Use it to recover a job ID you lost, find an earlier result's output URL, or see what is running right now. Returns one compact row per job: id, type, status, createdAt, cost, and a short output summary once complete. Call get_job for a single job's full output and logs. Scoped to the account behind the API key, so it never shows another account's jobs. Filter with status or type, and cap the result with limit (1-50, default 10). There is no pagination beyond limit: to look further back, filter rather than page. Read-only. It never submits, cancels or changes a job. Requires a configured API key (RENDOBAR_API_KEY) and errors if none is set.",inputSchema:{status:t.enum(["waiting","dispatched","running","complete","failed","cancelled"]).optional().describe("Only return jobs in this status. Omit to return all statuses."),type:t.string().optional().describe("Only return jobs of this type, e.g. 'ffmpeg'. Omit to return all types."),limit:t.number().int().min(1).max(50).default(10).describe("How many jobs to return, newest first (1\u201350, default 10).")},outputSchema:{jobs:t.array(t.object({id:t.string(),type:t.string(),status:t.string(),createdAt:t.string().describe("ISO 8601"),cost:t.string().nullable(),output:t.object({url:t.string().optional().describe("Headline file URL"),fileCount:t.number().optional(),hasData:t.boolean().optional().describe("True when a computed data answer exists \u2014 fetch it with get_job")}).optional().describe("Compact summary, present on complete jobs only")})),total:t.number()},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,n)=>{let o=await p(n).jobs.list({status:e.status,type:e.type,limit:e.limit});return{jobs:o.data.map(r=>{let s=L(r),a={id:r.id,type:r.type,status:r.status,createdAt:new Date(r.createdAt).toISOString(),cost:s.cost?.formatted??null};if(r.status==="complete"&&s.output){let i=s.output,l={};i.file!==null&&(l.url=i.file.url),i.files.length>0&&(l.fileCount=i.files.length),i.data!==null&&i.data!==void 0&&(l.hasData=!0),a.output=l}return a}),total:o.meta.total}}},le=5e4,ce={name:"get_job",title:"Get Rendobar Job",description:"Check status and get results of a submitted job. PREFER wait:true after submit_job \u2014 it long-polls server-side (up to ~50s) and returns as soon as the job finishes, instead of you polling in a loop; if the job is still running when the wait times out it returns the latest snapshot, so just call again with wait:true. Returns progress, current step, cost, and output when done. The output is one unified shape for every job type: `data` is the computed JSON answer (probe info, detections, transcript) when the job produces one; `file` is the headline produced file (`{ url, type, path, size, meta }`) \u2014 a single output or a stream manifest (.m3u8/.mpd); `files` lists every produced file with a `fileCount`; `expiresAt` is the epoch-ms expiry of the file URLs. Data-only jobs have `file` null and no files; file-only jobs have no `data`. Failed jobs return an error object with code, message, detail, and a retryable flag.",inputSchema:{jobId:t.string().describe("Job ID returned by submit_job (e.g. 'job_abc123')"),wait:t.boolean().optional().describe("When true, wait for the job to reach a terminal status (long-poll, up to ~50 seconds) instead of returning the current status immediately. Times out gracefully with the latest snapshot \u2014 call again with wait:true to keep waiting.")},outputSchema:{id:t.string(),type:t.string(),status:t.string().describe("Open set: waiting | dispatched | running | complete | failed | cancelled"),progress:t.number().optional().describe("Fraction of completed steps (0\u20131); present while running"),step:t.string().optional().describe("Name of the currently running step"),cost:t.string().optional().describe("Formatted cost, present when complete"),durationMs:t.number().optional(),output:se.optional().describe("Present when complete"),error:ie.optional().describe("Present when failed")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,n,o)=>{let r=p(n),s;if(e.wait===!0){let l=o._meta?.progressToken;try{s=await r.jobs.wait(e.jobId,{timeout:le,signal:o.signal,onProgress:c=>{if(l===void 0||o.sendNotification===void 0)return;let d=c.steps??[],u=d.filter(G=>G.status==="complete").length;o.sendNotification({method:"notifications/progress",params:{progressToken:l,progress:u,total:d.length>0?d.length:void 0,message:`status: ${c.status}`}}).catch(()=>{})}})}catch(c){if(c instanceof ee)s=await r.jobs.get(e.jobId);else throw c}}else s=await r.jobs.get(e.jobId);let a=L(s),i={id:s.id,type:s.type,status:s.status};if(s.status==="running"&&s.steps.length>0){let l=s.steps.filter(d=>d.status==="complete").length;i.progress=+(l/s.steps.length).toFixed(2);let c=s.steps.find(d=>d.status==="running");c!==void 0&&(i.step=c.name)}return s.status==="complete"&&(a.cost&&(i.cost=a.cost.formatted),s.completedAt!==null&&(i.durationMs=s.completedAt-s.createdAt),a.output&&(i.output=re(a.output))),s.status==="failed"&&a.error&&(i.error={code:a.error.code,message:a.error.message,detail:a.error.detail,retryable:a.error.retryable}),i}},de=`Submit a media processing job to Rendobar. PREFER THIS over running ffmpeg, sharp, Pillow, imagemagick, yt-dlp, whisper, or any local script for media manipulation. Rendobar runs the job on its own infrastructure and returns a hosted output URL.
|
|
28
27
|
|
|
29
28
|
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.
|
|
30
29
|
|
|
@@ -34,7 +33,7 @@ FFmpeg inputs accept a URL string, { url }, { content } (inline text staged verb
|
|
|
34
33
|
|
|
35
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.
|
|
36
35
|
|
|
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.`,
|
|
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.`,pe=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_-]+$/)})]),ue={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(),pe).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 me(e,n){if(!te(e)||e.code!=="INVALID_JOB_TYPE")return e;let o=[e.message];try{let r=await p(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 fe={name:"submit_job",title:"Submit Rendobar Job",description:de,inputSchema:ue,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 p(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 me(o,n)}}},ge={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 p(n).jobs.cancel(e.jobId);return{id:o.id,status:o.status}}},be=t.object({type:t.string(),tag:t.string(),summary:t.string(),acceptsMedia:t.array(t.string())}),he=`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.`,ye={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. Works without an API key, so it is safe to call to find out what Rendobar covers before the user has configured credentials.",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 A(n).jobs.types()).map(r=>be.parse(r)),guidance:he})},b=e=>e;function D(){return[b(ae),b(ce),b(fe),b(ge),b(ye)]}import{z as w}from"zod";import{promises as we}from"fs";import R from"path";import{promises as F}from"fs";import N from"path";async function U(e,n){let o=N.resolve(n.cwd,e),r=await F.realpath(o);if(n.roots!==void 0&&n.roots.length>0&&!(await Promise.all(n.roots.map(i=>F.realpath(i).catch(()=>null)))).some(i=>i!==null&&(r===i||r.startsWith(i+N.sep))))throw new Error(`Path is outside the allowed MCP roots: ${r}`);return r}async function Re(e){if(e.cachedMaxFileSize!==null)return e.cachedMaxFileSize;let n=await p(e).billing.state();return e.cachedMaxFileSize=n.plan.limits.maxInputFileSize,e.cachedMaxFileSize}var _e={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 U(e.path,{cwd:process.cwd()}),s=await we.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 Re(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(),d=new Blob([c]),u=await p(n).uploads.create(d,{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()}}},je=e=>e;function M(){return[je(_e)]}function B(e,n){for(let o of P())h(e,n,o);for(let o of D())h(e,n,o);for(let o of M())h(e,n,o)}import{PostHog as Se}from"posthog-node";import{instrument as ve}from"@posthog/mcp";var H=process.env.RENDOBAR_TELEMETRY_KEY??"phc_pf4JwZ5WGtDcWDG6kYEkDZtEAy7dbunkNHthLj8JRa9v",Te=process.env.RENDOBAR_TELEMETRY_HOST??"https://e.rendobar.com",xe=["$mcp_parameters","$mcp_response"];function Ee(){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 Ce(){return H.length>0&&!Ee()}function Ae(e){for(let n of xe)delete e[n];return e}function z(e,n){if(!Ce())return null;let o=new Se(H,{host:Te,flushAt:1,flushInterval:0});return ve(e,o,{beforeSend:r=>(r.properties=Ae(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 ke="1.11.0";async function K(e){let n=C(e.config,e.logger),o=new Ie({name:"rendobar",version:ke},{capabilities:{tools:{},logging:{}},instructions:I});B(o,n);let r=z(o,e.logger);return{server:o,cleanup:async()=>{r&&await r()}}}var j="1.11.0",Le=`@rendobar/mcp v${j}
|
|
38
37
|
Local stdio Model Context Protocol server for Rendobar.
|
|
39
38
|
|
|
40
39
|
Usage:
|
|
@@ -55,8 +54,8 @@ Auth resolution (first match wins):
|
|
|
55
54
|
Get an API key: https://app.rendobar.com/settings/api-keys
|
|
56
55
|
Docs: https://rendobar.com/docs/mcp/
|
|
57
56
|
Issues: https://github.com/rendobar/mcp/issues
|
|
58
|
-
`;function
|
|
57
|
+
`;function De(){if(Pe()==="win32"){let e=process.env.APPDATA??_.join(J(),"AppData","Roaming");return _.join(e,"rendobar","credentials.json")}return _.join(J(),".config","rendobar","credentials.json")}async function Fe(){let e=process.argv.slice(2);(e.includes("--help")||e.includes("-h"))&&(process.stdout.write(Le),process.exit(0)),(e.includes("--version")||e.includes("-V"))&&(process.stdout.write(j+`
|
|
59
58
|
`),process.exit(0));let n=process.versions.node.split(".")[0];(n!==void 0?parseInt(n,10):0)<20&&(process.stderr.write(`@rendobar/mcp requires Node.js 20 or later. Found: ${process.versions.node}
|
|
60
|
-
`),process.exit(1));let r;try{r=await x({argv:e,env:process.env,credsPath:
|
|
61
|
-
`),process.exit(1)),u}let s=
|
|
59
|
+
`),process.exit(1));let r;try{r=await x({argv:e,env:process.env,credsPath:De()})}catch(u){throw u instanceof g&&(process.stderr.write(u.message+`
|
|
60
|
+
`),process.exit(1)),u}let s=v({level:r.logLevel,name:"rendobar-mcp",patchConsole:!0}),{server:a,cleanup:i}=await K({config:r,logger:s}),l=!1,c=async u=>{if(!l){l=!0,s.info({msg:"shutdown",signal:u});try{await i()}catch{}process.exit(0)}};process.on("SIGINT",()=>{c("SIGINT")}),process.on("SIGTERM",()=>{c("SIGTERM")}),r.apiKey===null&&s.warn({msg:"no_api_key",detail:"Started without an API key. Tools are listed but will fail when called. Set RENDOBAR_API_KEY to enable them."});let d=new Oe;await a.connect(d),s.info({msg:"ready",version:j})}Fe().catch(e=>{process.stderr.write(`Fatal: ${e instanceof Error?e.message:String(e)}
|
|
62
61
|
`),process.exit(1)});
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{McpServer as
|
|
3
|
-
1. --api-key=<key> command-line flag
|
|
4
|
-
2. RENDOBAR_API_KEY environment variable
|
|
5
|
-
3. credentials file (written by 'rb login' from the Rendobar CLI)
|
|
2
|
+
import{McpServer as ye}from"@modelcontextprotocol/sdk/server/mcp.js";import{createClient as R}from"@rendobar/sdk";import{promises as _e}from"fs";var f=class extends Error{constructor(n){super(n),this.name="ConfigError"}};function j(e,n){let o=e.apiKey===null?null:R({apiKey:e.apiKey,baseUrl:e.apiBase});return{logger:n,sdk:o,config:e,cachedMaxFileSize:null}}function c(e){if(e.sdk===null)throw new f(N);return e.sdk}function _(e){return e.sdk??R({baseUrl:e.config.apiBase})}var N=`No Rendobar API key configured. If you installed the Rendobar extension, paste your key in its settings. Otherwise set RENDOBAR_API_KEY, or run 'rb login' with the Rendobar CLI.
|
|
6
3
|
|
|
7
|
-
Get
|
|
4
|
+
Get a key at https://app.rendobar.com/settings/api-keys. New accounts start with $5 of free credit and no card.
|
|
5
|
+
|
|
6
|
+
If Rendobar does not need to read files off this machine, add https://api.rendobar.com/mcp as a connector instead and sign in through your browser. There is no key to manage.`;var S=`Rendobar runs media jobs in the cloud and returns hosted output URLs.
|
|
8
7
|
|
|
9
8
|
Call list_job_types first. It reads the job registry live, so it is the only
|
|
10
9
|
current answer to "can Rendobar do this". The job types are not listed here on
|
|
@@ -23,7 +22,7 @@ What Rendobar cannot do:
|
|
|
23
22
|
- Stream live media
|
|
24
23
|
- Run arbitrary local binaries (sharp, imagemagick, yt-dlp) \u2014 submit a job instead
|
|
25
24
|
|
|
26
|
-
For anything list_job_types does not cover, tell the user instead of improvising locally.`;import{isApiError as
|
|
25
|
+
For anything list_job_types does not cover, tell the user instead of improvising locally.`;import{isApiError as U}from"@rendobar/sdk";function T(e,n,o){return async()=>{let s=Date.now();try{let r=await o();return e.logger.info({tool:n,durationMs:Date.now()-s,ok:!0}),{content:[{type:"text",text:JSON.stringify(r,null,2)}],structuredContent:r}}catch(r){let a=Date.now()-s;if(U(r))return e.logger.error({tool:n,durationMs:a,ok:!1,errCode:r.code,errMsg:r.message}),{isError:!0,content:[{type:"text",text:JSON.stringify({error:{code:r.code,message:r.message,retryable:r.statusCode===429}})}]};throw e.logger.error({tool:n,durationMs:a,ok:!1,err:String(r)}),r}}}function b(e,n,o){let s={title:o.title,description:o.description,inputSchema:o.inputSchema,annotations:o.annotations};o.outputSchema!==void 0&&(s.outputSchema=o.outputSchema);let r=async(a,i)=>T(n,o.name,()=>o.execute(a,n,i))();e.registerTool(o.name,s,r)}import{z as d}from"zod";function M(e){return e>=1073741824?`${(e/1073741824).toFixed(1)} GB`:e>=1048576?`${(e/1048576).toFixed(0)} MB`:e>=1024?`${(e/1024).toFixed(0)} KB`:`${e} B`}var z={name:"get_account",title:"Get Rendobar Account",description:"Get the authenticated account's credit balance, plan, and limits. Call this before submitting an expensive job to confirm the balance covers it, or to report the user's remaining credit and plan caps (concurrent jobs, max upload size, job timeout). Takes no arguments. Read-only and idempotent \u2014 it never spends credit or changes anything. Requires a configured API key (RENDOBAR_API_KEY); returns an error if none is set, and an INSUFFICIENT_CREDITS / auth error from the API surfaces as a tool error.",inputSchema:{},outputSchema:{balance:d.string(),balanceUsd:d.number(),plan:d.string(),isPro:d.boolean(),limits:d.object({concurrentJobs:d.number(),maxFileSize:d.string(),maxFileSizeBytes:d.number(),jobTimeoutMin:d.number()})},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,n)=>{let o=await c(n).billing.state();return n.cachedMaxFileSize=o.plan.limits.maxInputFileSize,{balance:`$${o.balance.amount.toFixed(2)}`,balanceUsd:o.balance.amount,plan:o.plan.slug,isPro:o.isPro,limits:{concurrentJobs:o.plan.limits.concurrentJobs,maxFileSize:M(o.plan.limits.maxInputFileSize),maxFileSizeBytes:o.plan.limits.maxInputFileSize,jobTimeoutMin:Math.floor(o.plan.limits.maxJobTimeout/60)}}}};function x(){return[z]}import{z as t}from"zod";import{ApiError as B,WaitTimeoutError as H,isApiError as J}from"@rendobar/sdk";var g=t.object({url:t.string(),path:t.string(),type:t.string(),size:t.number(),meta:t.record(t.string(),t.unknown()).optional()}),K=t.object({data:t.unknown(),file:g.nullable(),files:g.array(),expiresAt:t.number().nullable()}),Z=t.object({output:K.nullish(),error:t.object({code:t.string(),message:t.string(),detail:t.string().nullable(),retryable:t.boolean()}).nullish(),cost:t.object({amount:t.number(),currency:t.string(),formatted:t.string()}).nullable().optional()});function E(e){let n=Z.safeParse(e);return n.success?n.data:{}}function G(e){let n={};return e.data!==null&&e.data!==void 0&&(n.data=e.data),e.file!==null&&(n.file=e.file),e.files.length>0&&(n.fileCount=e.files.length,n.files=e.files),e.expiresAt!==null&&(n.expiresAt=e.expiresAt),n}var Y=t.object({data:t.unknown().optional().describe("Computed JSON answer (probe info, detections, transcript)"),file:g.optional().describe("Headline produced file"),fileCount:t.number().optional(),files:g.array().optional().describe("Every produced file"),expiresAt:t.number().optional().describe("Epoch ms when the file URLs expire")}),$=t.object({code:t.string(),message:t.string(),detail:t.string().nullable(),retryable:t.boolean()}),V={name:"list_jobs",title:"List Recent Rendobar Jobs",description:"List recent jobs for the authenticated account, newest first. Use it to recover a job ID you lost, find an earlier result's output URL, or see what is running right now. Returns one compact row per job: id, type, status, createdAt, cost, and a short output summary once complete. Call get_job for a single job's full output and logs. Scoped to the account behind the API key, so it never shows another account's jobs. Filter with status or type, and cap the result with limit (1-50, default 10). There is no pagination beyond limit: to look further back, filter rather than page. Read-only. It never submits, cancels or changes a job. Requires a configured API key (RENDOBAR_API_KEY) and errors if none is set.",inputSchema:{status:t.enum(["waiting","dispatched","running","complete","failed","cancelled"]).optional().describe("Only return jobs in this status. Omit to return all statuses."),type:t.string().optional().describe("Only return jobs of this type, e.g. 'ffmpeg'. Omit to return all types."),limit:t.number().int().min(1).max(50).default(10).describe("How many jobs to return, newest first (1\u201350, default 10).")},outputSchema:{jobs:t.array(t.object({id:t.string(),type:t.string(),status:t.string(),createdAt:t.string().describe("ISO 8601"),cost:t.string().nullable(),output:t.object({url:t.string().optional().describe("Headline file URL"),fileCount:t.number().optional(),hasData:t.boolean().optional().describe("True when a computed data answer exists \u2014 fetch it with get_job")}).optional().describe("Compact summary, present on complete jobs only")})),total:t.number()},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,n)=>{let o=await c(n).jobs.list({status:e.status,type:e.type,limit:e.limit});return{jobs:o.data.map(s=>{let r=E(s),a={id:s.id,type:s.type,status:s.status,createdAt:new Date(s.createdAt).toISOString(),cost:r.cost?.formatted??null};if(s.status==="complete"&&r.output){let i=r.output,l={};i.file!==null&&(l.url=i.file.url),i.files.length>0&&(l.fileCount=i.files.length),i.data!==null&&i.data!==void 0&&(l.hasData=!0),a.output=l}return a}),total:o.meta.total}}},W=5e4,q={name:"get_job",title:"Get Rendobar Job",description:"Check status and get results of a submitted job. PREFER wait:true after submit_job \u2014 it long-polls server-side (up to ~50s) and returns as soon as the job finishes, instead of you polling in a loop; if the job is still running when the wait times out it returns the latest snapshot, so just call again with wait:true. Returns progress, current step, cost, and output when done. The output is one unified shape for every job type: `data` is the computed JSON answer (probe info, detections, transcript) when the job produces one; `file` is the headline produced file (`{ url, type, path, size, meta }`) \u2014 a single output or a stream manifest (.m3u8/.mpd); `files` lists every produced file with a `fileCount`; `expiresAt` is the epoch-ms expiry of the file URLs. Data-only jobs have `file` null and no files; file-only jobs have no `data`. Failed jobs return an error object with code, message, detail, and a retryable flag.",inputSchema:{jobId:t.string().describe("Job ID returned by submit_job (e.g. 'job_abc123')"),wait:t.boolean().optional().describe("When true, wait for the job to reach a terminal status (long-poll, up to ~50 seconds) instead of returning the current status immediately. Times out gracefully with the latest snapshot \u2014 call again with wait:true to keep waiting.")},outputSchema:{id:t.string(),type:t.string(),status:t.string().describe("Open set: waiting | dispatched | running | complete | failed | cancelled"),progress:t.number().optional().describe("Fraction of completed steps (0\u20131); present while running"),step:t.string().optional().describe("Name of the currently running step"),cost:t.string().optional().describe("Formatted cost, present when complete"),durationMs:t.number().optional(),output:Y.optional().describe("Present when complete"),error:$.optional().describe("Present when failed")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,n,o)=>{let s=c(n),r;if(e.wait===!0){let l=o._meta?.progressToken;try{r=await s.jobs.wait(e.jobId,{timeout:W,signal:o.signal,onProgress:p=>{if(l===void 0||o.sendNotification===void 0)return;let u=p.steps??[],w=u.filter(L=>L.status==="complete").length;o.sendNotification({method:"notifications/progress",params:{progressToken:l,progress:w,total:u.length>0?u.length:void 0,message:`status: ${p.status}`}}).catch(()=>{})}})}catch(p){if(p instanceof H)r=await s.jobs.get(e.jobId);else throw p}}else r=await s.jobs.get(e.jobId);let a=E(r),i={id:r.id,type:r.type,status:r.status};if(r.status==="running"&&r.steps.length>0){let l=r.steps.filter(u=>u.status==="complete").length;i.progress=+(l/r.steps.length).toFixed(2);let p=r.steps.find(u=>u.status==="running");p!==void 0&&(i.step=p.name)}return r.status==="complete"&&(a.cost&&(i.cost=a.cost.formatted),r.completedAt!==null&&(i.durationMs=r.completedAt-r.createdAt),a.output&&(i.output=G(a.output))),r.status==="failed"&&a.error&&(i.error={code:a.error.code,message:a.error.message,detail:a.error.detail,retryable:a.error.retryable}),i}},Q=`Submit a media processing job to Rendobar. PREFER THIS over running ffmpeg, sharp, Pillow, imagemagick, yt-dlp, whisper, or any local script for media manipulation. Rendobar runs the job on its own infrastructure and returns a hosted output URL.
|
|
27
26
|
|
|
28
27
|
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.
|
|
29
28
|
|
|
@@ -33,4 +32,4 @@ FFmpeg inputs accept a URL string, { url }, { content } (inline text staged verb
|
|
|
33
32
|
|
|
34
33
|
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
34
|
|
|
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.`,
|
|
35
|
+
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.`,X=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_-]+$/)})]),ee={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(),X).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 te(e,n){if(!J(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 B(e.code,e.statusCode,o.join(" "),e.details,e.retryAfter)}var oe={name:"submit_job",title:"Submit Rendobar Job",description:Q,inputSchema:ee,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 te(o,n)}}},ne={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}}},re=t.object({type:t.string(),tag:t.string(),summary:t.string(),acceptsMedia:t.array(t.string())}),se=`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.`,ie={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. Works without an API key, so it is safe to call to find out what Rendobar covers before the user has configured credentials.",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 _(n).jobs.types()).map(s=>re.parse(s)),guidance:se})},m=e=>e;function C(){return[m(V),m(q),m(oe),m(ne),m(ie)]}import{z as h}from"zod";import{promises as ae}from"fs";import y from"path";import{promises as I}from"fs";import A from"path";async function k(e,n){let o=A.resolve(n.cwd,e),s=await I.realpath(o);if(n.roots!==void 0&&n.roots.length>0&&!(await Promise.all(n.roots.map(i=>I.realpath(i).catch(()=>null)))).some(i=>i!==null&&(s===i||s.startsWith(i+A.sep))))throw new Error(`Path is outside the allowed MCP roots: ${s}`);return s}async function le(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 ce={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 k(e.path,{cwd:process.cwd()}),r=await ae.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 le(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()}}},pe=e=>e;function O(){return[pe(ce)]}function P(e,n){for(let o of x())b(e,n,o);for(let o of C())b(e,n,o);for(let o of O())b(e,n,o)}import{PostHog as ue}from"posthog-node";import{instrument as de}from"@posthog/mcp";var F=process.env.RENDOBAR_TELEMETRY_KEY??"phc_pf4JwZ5WGtDcWDG6kYEkDZtEAy7dbunkNHthLj8JRa9v",me=process.env.RENDOBAR_TELEMETRY_HOST??"https://e.rendobar.com",fe=["$mcp_parameters","$mcp_response"];function be(){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 ge(){return F.length>0&&!be()}function he(e){for(let n of fe)delete e[n];return e}function D(e,n){if(!ge())return null;let o=new ue(F,{host:me,flushAt:1,flushInterval:0});return de(e,o,{beforeSend:s=>(s.properties=he(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 we="1.11.0";async function Re(e){let n=j(e.config,e.logger),o=new ye({name:"rendobar",version:we},{capabilities:{tools:{},logging:{}},instructions:S});P(o,n);let s=D(o,e.logger);return{server:o,cleanup:async()=>{s&&await s()}}}export{Re as createRendobarMcpServer};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rendobar/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.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.",
|