@rendobar/mcp 1.4.0 → 1.6.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 CHANGED
@@ -39,13 +39,31 @@ The difference from the hosted MCP at `api.rendobar.com`: this server runs local
39
39
 
40
40
  ## Install
41
41
 
42
+ ### Fastest: Claude Code, no API key
43
+
44
+ Claude Code can connect to Rendobar's hosted MCP over OAuth. No key to copy, no config file to edit — your browser opens once to approve:
45
+
46
+ ```bash
47
+ claude mcp add --transport http rendobar https://api.rendobar.com/mcp
48
+ ```
49
+
50
+ The hosted server cannot read files on your disk. If you want an agent to upload local files (the reason this package exists), use the local stdio server below instead.
51
+
52
+ ### Local stdio server (this package)
53
+
42
54
  You don't install it. Configure your MCP client to spawn it via `npx`.
43
55
 
44
- ### Get an API key
56
+ #### Get an API key
45
57
 
46
58
  Sign up at [app.rendobar.com](https://app.rendobar.com) → Settings → API Keys.
47
59
 
48
- ### Configure your client
60
+ #### Claude Code (terminal)
61
+
62
+ ```bash
63
+ claude mcp add rendobar -s user --env RENDOBAR_API_KEY=rb_... -- npx -y @rendobar/mcp
64
+ ```
65
+
66
+ Already ran `rb login` with the Rendobar CLI? Drop the `--env` part — the server reads the credentials file automatically.
49
67
 
50
68
  #### Claude Desktop
51
69
 
@@ -111,12 +129,6 @@ Edit `.vscode/mcp.json`:
111
129
  }
112
130
  ```
113
131
 
114
- #### Claude Code (terminal)
115
-
116
- ```bash
117
- claude mcp add rendobar -s user --env RENDOBAR_API_KEY=rb_... -- npx -y @rendobar/mcp
118
- ```
119
-
120
132
  #### Continue
121
133
 
122
134
  Create `.continue/mcpServers/rendobar.yaml`:
@@ -134,12 +146,20 @@ env:
134
146
  | Tool | Purpose |
135
147
  |---|---|
136
148
  | `upload_file` | Upload a local file. Returns a download URL to use in `submit_job`. |
149
+ | `list_job_types` | List every active job type with its summary and accepted media kinds. Call first when starting a media task. |
137
150
  | `submit_job` | Submit any Rendobar job. Its description lists the active job types. |
138
- | `get_job` | Poll job status, fetch result. |
151
+ | `get_job` | Fetch job status + result. Pass `wait: true` to long-poll until the job finishes (~50s cap, then returns a snapshot). |
139
152
  | `list_jobs` | List recent jobs. |
140
153
  | `cancel_job` | Cancel a waiting/dispatched job. |
141
154
  | `get_account` | Check balance, plan limits, active job count. |
142
155
 
156
+ ### Chaining jobs
157
+
158
+ A `submit_job` input can reference a previous job's output directly, instead of downloading and
159
+ re-uploading an intermediate result. For `ffmpeg` inputs, pass `{ job: "job_..." }` with a
160
+ completed job's ID. For every other job type, call `get_job` on the completed job, take its
161
+ output URL, and pass that URL as the input instead.
162
+
143
163
  ### Job types
144
164
 
145
165
  `submit_job` takes a `type`. The active types:
@@ -199,7 +219,7 @@ and directories can list them; calls that need the API return a clear error unti
199
219
  |---|---|---|
200
220
  | Transport | stdio, spawned by your client | Streamable HTTP |
201
221
  | Local file upload | Yes, the whole point | No, server has no disk |
202
- | Setup | `npx` line in a config file | Bearer API key over HTTP |
222
+ | Setup | `npx` line in a config file | OAuth in the browser (`claude mcp add --transport http`), or a Bearer API key |
203
223
  | Best for | Claude Desktop, Cursor, Cline, Zed, local agents | claude.ai web, ChatGPT, hosted gateways |
204
224
 
205
225
  ## Authentication
package/dist/bin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import{StdioServerTransport as we}from"@modelcontextprotocol/sdk/server/stdio.js";import{homedir as H,platform as Se}from"os";import w from"path";var v={debug:10,info:20,warn:30,error:40},p=null;function _(e){let n=v[e.level],o=(t,r)=>{if(v[t]<n)return;let a={level:t,time:Date.now(),...r};e.name!==void 0&&(a.name=e.name),process.stderr.write(JSON.stringify(a)+`
3
- `)};if(e.patchConsole===!0&&p===null){p={log:console.log.bind(console),info:console.info.bind(console),warn:console.warn.bind(console),debug:console.debug.bind(console)};let t=r=>(...a)=>{let i=a.map(l=>typeof l=="string"?l:JSON.stringify(l)).join(" ");o(r,{msg:i,source:"console"})};console.log=t("info"),console.info=t("info"),console.warn=t("warn"),console.debug=t("debug")}return{debug:t=>o("debug",t),info:t=>o("info",t),warn:t=>o("warn",t),error:t=>o("error",t),restoreConsole:()=>{p!==null&&(console.log=p.log,console.info=p.info,console.warn=p.warn,console.debug=p.debug,p=null)}}}import{promises as K}from"fs";var d=class extends Error{constructor(n){super(n),this.name="ConfigError"}},G="https://api.rendobar.com",J=new Set(["debug","info","warn","error"]);async function x(e){let n=T(e.argv,"--api-key"),o=T(e.argv,"--api-base"),t={};try{let g=await K.readFile(e.credsPath,"utf8"),m=JSON.parse(g);m!==null&&typeof m=="object"&&(t=m)}catch{}let r=n??e.env.RENDOBAR_API_KEY??t.apiKey,a=o??t.apiBase??G,i=r===void 0||r===""?null:r;if(i!==null&&!i.startsWith("rb_"))throw new d(`Invalid Rendobar API key: must start with 'rb_' (got '${i.slice(0,4)}...').`);let l=e.env.RENDOBAR_LOG_LEVEL??"info";if(!J.has(l))throw new d(`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 t=0;t<e.length;t++){let r=e[t];if(r!==void 0){if(r.startsWith(o))return r.slice(o.length);if(r===n){let a=e[t+1];if(a!==void 0&&!a.startsWith("--"))return a}}}}import{McpServer as he}from"@modelcontextprotocol/sdk/server/mcp.js";import{createClient as V}from"@rendobar/sdk";function A(e,n){let o=e.apiKey===null?null:V({apiKey:e.apiKey,baseUrl:e.apiBase});return{logger:n,sdk:o,config:e,cachedMaxFileSize:null}}function c(e){if(e.sdk===null)throw new d(`No Rendobar API key configured. Provide one via:
2
+ import{StdioServerTransport as Ee}from"@modelcontextprotocol/sdk/server/stdio.js";import{homedir as z,platform as Ce}from"os";import S from"path";var _={debug:10,info:20,warn:30,error:40},f=null;function v(e){let n=_[e.level],r=(o,s)=>{if(_[o]<n)return;let i={level:o,time:Date.now(),...s};e.name!==void 0&&(i.name=e.name),process.stderr.write(JSON.stringify(i)+`
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 o=s=>(...i)=>{let a=i.map(l=>typeof l=="string"?l:JSON.stringify(l)).join(" ");r(s,{msg:a,source:"console"})};console.log=o("info"),console.info=o("info"),console.warn=o("warn"),console.debug=o("debug")}return{debug:o=>r("debug",o),info:o=>r("info",o),warn:o=>r("warn",o),error:o=>r("error",o),restoreConsole:()=>{f!==null&&(console.log=f.log,console.info=f.info,console.warn=f.warn,console.debug=f.debug,f=null)}}}import{promises as J}from"fs";var g=class extends Error{constructor(n){super(n),this.name="ConfigError"}},G="https://api.rendobar.com",V=new Set(["debug","info","warn","error"]);async function x(e){let n=T(e.argv,"--api-key"),r=T(e.argv,"--api-base"),o={};try{let c=await J.readFile(e.credsPath,"utf8"),p=JSON.parse(c);p!==null&&typeof p=="object"&&(o=p)}catch{}let s=n??e.env.RENDOBAR_API_KEY??o.apiKey,i=r??o.apiBase??G,a=s===void 0||s===""?null:s;if(a!==null&&!a.startsWith("rb_"))throw new g(`Invalid Rendobar API key: must start with 'rb_' (got '${a.slice(0,4)}...').`);let l=e.env.RENDOBAR_LOG_LEVEL??"info";if(!V.has(l))throw new g(`Invalid RENDOBAR_LOG_LEVEL='${l}'. Use one of: debug, info, warn, error.`);return{apiKey:a,apiBase:i,logLevel:l}}function T(e,n){let r=`${n}=`;for(let o=0;o<e.length;o++){let s=e[o];if(s!==void 0){if(s.startsWith(r))return s.slice(r.length);if(s===n){let i=e[o+1];if(i!==void 0&&!i.startsWith("--"))return i}}}}import{McpServer as xe}from"@modelcontextprotocol/sdk/server/mcp.js";import{createClient as $}from"@rendobar/sdk";function A(e,n){let r=e.apiKey===null?null:$({apiKey:e.apiKey,baseUrl:e.apiBase});return{logger:n,sdk:r,config:e,cachedMaxFileSize:null}}function u(e){if(e.sdk===null)throw new g(`No Rendobar API key configured. Provide one via:
4
4
  1. --api-key=<key> command-line flag
5
5
  2. RENDOBAR_API_KEY environment variable
6
6
  3. credentials file (written by 'rb login' from the Rendobar CLI)
@@ -22,7 +22,7 @@ Workflow:
22
22
  1. If the file is at a public HTTPS URL, pass it directly to submit_job as inputs.source (or another input name referenced by your command).
23
23
  2. If the file is on the local disk, call upload_file first to get a downloadUrl, then use that as the input URL in submit_job.
24
24
  3. For expensive jobs, call get_account first to confirm the balance covers the cost.
25
- 4. After submit_job, call get_job to poll until status is complete or failed. The output URL is on the complete response.
25
+ 4. After submit_job, call get_job with wait:true \u2014 it blocks until the job finishes (up to ~50s, then returns a snapshot; call again to keep waiting). The output URL is on the complete response.
26
26
 
27
27
  What Rendobar cannot do:
28
28
  - Generate video from text or images (no diffusion models)
@@ -30,17 +30,17 @@ What Rendobar cannot do:
30
30
  - Stream live media
31
31
  - Run arbitrary local binaries (sharp, imagemagick, yt-dlp) \u2014 only the job types above
32
32
 
33
- For anything outside the supported job types, tell the user instead of improvising locally.`;import{isApiError as $}from"@rendobar/sdk";function j(e,n,o){return async()=>{let t=Date.now();try{let r=await o();return e.logger.info({tool:n,durationMs:Date.now()-t,ok:!0}),{content:[{type:"text",text:JSON.stringify(r,null,2)}],structuredContent:r}}catch(r){let a=Date.now()-t;if($(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}})}]};throw e.logger.error({tool:n,durationMs:a,ok:!1,err:String(r)}),r}}}function b(e,n,o){let t={title:o.title,description:o.description,inputSchema:o.inputSchema,annotations:o.annotations};o.outputSchema!==void 0&&(t.outputSchema=o.outputSchema);let r=async(a,i)=>j(n,o.name,()=>o.execute(a,n,i))();e.registerTool(o.name,t,r)}import{z as u}from"zod";function Z(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 Y={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:u.string(),balanceUsd:u.number(),plan:u.string(),isPro:u.boolean(),limits:u.object({concurrentJobs:u.number(),maxFileSize:u.string(),maxFileSizeBytes:u.number(),jobTimeoutMin:u.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:Z(o.plan.limits.maxInputFileSize),maxFileSizeBytes:o.plan.limits.maxInputFileSize,jobTimeoutMin:Math.floor(o.plan.limits.maxJobTimeout/60)}}}};function I(){return[Y]}import{z as s}from"zod";var P=s.object({url:s.string(),path:s.string(),type:s.string(),size:s.number(),meta:s.record(s.string(),s.unknown()).optional()}),W=s.object({data:s.unknown(),file:P.nullable(),files:P.array(),expiresAt:s.number().nullable()}),q=s.object({output:W.nullish(),error:s.object({code:s.string(),message:s.string(),detail:s.string().nullable(),retryable:s.boolean()}).nullish(),cost:s.object({amount:s.number(),currency:s.string(),formatted:s.string()}).nullable().optional()});function O(e){let n=q.safeParse(e);return n.success?n.data:{}}function X(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 Q={name:"list_jobs",title:"List Recent Rendobar Jobs",description:"List the most recent jobs for the authenticated account, newest first. Use it to find a previous result's output URL, check what is currently running, or recover a job ID you lost. Returns a compact summary per job (id, type, status, createdAt, cost, and a short output summary for completed jobs); call get_job for a job's full output. Optionally filter by status or job type. Read-only \u2014 never submits or changes a job. Requires a configured API key (RENDOBAR_API_KEY); errors if none is set.",inputSchema:{status:s.enum(["waiting","dispatched","running","complete","failed","cancelled"]).optional().describe("Only return jobs in this status. Omit to return all statuses."),type:s.string().optional().describe("Only return jobs of this type, e.g. 'ffmpeg'. Omit to return all types."),limit:s.number().int().min(1).max(50).default(10).describe("How many jobs to return, newest first (1\u201350, default 10).")},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(t=>{let r=O(t),a={id:t.id,type:t.type,status:t.status,createdAt:new Date(t.createdAt).toISOString(),cost:r.cost?.formatted??null};if(t.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}}},ee={name:"get_job",title:"Get Rendobar Job",description:"Check status and get results of a submitted job. Poll until status is 'complete' or 'failed'. 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:s.string().describe("Job ID returned by submit_job (e.g. 'job_abc123')")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,n)=>{let o=await c(n).jobs.get(e.jobId),t=O(o),r={id:o.id,type:o.type,status:o.status};if(o.status==="running"&&o.steps.length>0){let a=o.steps.filter(l=>l.status==="complete").length;r.progress=+(a/o.steps.length).toFixed(2);let i=o.steps.find(l=>l.status==="running");i!==void 0&&(r.step=i.name)}return o.status==="complete"&&(t.cost&&(r.cost=t.cost.formatted),o.completedAt!==null&&(r.durationMs=o.completedAt-o.createdAt),t.output&&(r.output=X(t.output))),o.status==="failed"&&t.error&&(r.error={code:t.error.code,message:t.error.message,detail:t.error.detail,retryable:t.error.retryable}),r}},te=`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.
33
+ For anything outside the supported job types, tell the user instead of improvising locally.`;import{isApiError as Z}from"@rendobar/sdk";function C(e,n,r){return async()=>{let o=Date.now();try{let s=await r();return e.logger.info({tool:n,durationMs:Date.now()-o,ok:!0}),{content:[{type:"text",text:JSON.stringify(s,null,2)}],structuredContent:s}}catch(s){let i=Date.now()-o;if(Z(s))return e.logger.error({tool:n,durationMs:i,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:i,ok:!1,err:String(s)}),s}}}function h(e,n,r){let o={title:r.title,description:r.description,inputSchema:r.inputSchema,annotations:r.annotations};r.outputSchema!==void 0&&(o.outputSchema=r.outputSchema);let s=async(i,a)=>C(n,r.name,()=>r.execute(i,n,a))();e.registerTool(r.name,o,s)}import{z as m}from"zod";function Y(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 W={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 r=await u(n).billing.state();return n.cachedMaxFileSize=r.plan.limits.maxInputFileSize,{balance:`$${r.balance.amount.toFixed(2)}`,balanceUsd:r.balance.amount,plan:r.plan.slug,isPro:r.isPro,limits:{concurrentJobs:r.plan.limits.concurrentJobs,maxFileSize:Y(r.plan.limits.maxInputFileSize),maxFileSizeBytes:r.plan.limits.maxInputFileSize,jobTimeoutMin:Math.floor(r.plan.limits.maxJobTimeout/60)}}}};function P(){return[W]}import{z as t}from"zod";import{WaitTimeoutError as q}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()}),X=t.object({data:t.unknown(),file:y.nullable(),files:y.array(),expiresAt:t.number().nullable()}),Q=t.object({output:X.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 O(e){let n=Q.safeParse(e);return n.success?n.data:{}}function ee(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 te=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")}),oe=t.object({code:t.string(),message:t.string(),detail:t.string().nullable(),retryable:t.boolean()}),ne={name:"list_jobs",title:"List Recent Rendobar Jobs",description:"List the most recent jobs for the authenticated account, newest first. Use it to find a previous result's output URL, check what is currently running, or recover a job ID you lost. Returns a compact summary per job (id, type, status, createdAt, cost, and a short output summary for completed jobs); call get_job for a job's full output. Optionally filter by status or job type. Read-only \u2014 never submits or changes a job. Requires a configured API key (RENDOBAR_API_KEY); 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 r=await u(n).jobs.list({status:e.status,type:e.type,limit:e.limit});return{jobs:r.data.map(o=>{let s=O(o),i={id:o.id,type:o.type,status:o.status,createdAt:new Date(o.createdAt).toISOString(),cost:s.cost?.formatted??null};if(o.status==="complete"&&s.output){let a=s.output,l={};a.file!==null&&(l.url=a.file.url),a.files.length>0&&(l.fileCount=a.files.length),a.data!==null&&a.data!==void 0&&(l.hasData=!0),i.output=l}return i}),total:r.meta.total}}},re=5e4,se={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:te.optional().describe("Present when complete"),error:oe.optional().describe("Present when failed")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,n,r)=>{let o=u(n),s;if(e.wait===!0){let l=r._meta?.progressToken;try{s=await o.jobs.wait(e.jobId,{timeout:re,signal:r.signal,onProgress:c=>{if(l===void 0||r.sendNotification===void 0)return;let p=c.steps??[],d=p.filter(K=>K.status==="complete").length;r.sendNotification({method:"notifications/progress",params:{progressToken:l,progress:d,total:p.length>0?p.length:void 0,message:`status: ${c.status}`}}).catch(()=>{})}})}catch(c){if(c instanceof q)s=await o.jobs.get(e.jobId);else throw c}}else s=await o.jobs.get(e.jobId);let i=O(s),a={id:s.id,type:s.type,status:s.status};if(s.status==="running"&&s.steps.length>0){let l=s.steps.filter(p=>p.status==="complete").length;a.progress=+(l/s.steps.length).toFixed(2);let c=s.steps.find(p=>p.status==="running");c!==void 0&&(a.step=c.name)}return s.status==="complete"&&(i.cost&&(a.cost=i.cost.formatted),s.completedAt!==null&&(a.durationMs=s.completedAt-s.createdAt),i.output&&(a.output=ee(i.output))),s.status==="failed"&&i.error&&(a.error={code:i.error.code,message:i.error.message,detail:i.error.detail,retryable:i.error.retryable}),a}},ie=`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. Call list_job_types first when starting a media task.
34
34
 
35
- FFmpeg inputs accept a URL string, { url }, { content } (inline text staged verbatim into the workdir, for subtitle files or ffmpeg concat lists), or { ref } (an already-uploaded asset, by its asset ID). The bare URL string and { url } are equivalent.
35
+ FFmpeg inputs accept a URL string, { url }, { content } (inline text staged verbatim into the workdir, for subtitle files or ffmpeg concat lists), or { job: "job_..." } (a completed job's output). The bare URL string and { url } are equivalent. To chain jobs, pass a completed job's output as the next job's input: { job: "job_..." } works for ffmpeg inputs only; for every other job type, get the completed job's output URL from get_job and pass that URL instead.
36
36
 
37
- 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.`,oe=s.union([s.string(),s.object({url:s.string()}),s.object({content:s.string()}),s.object({ref:s.string()})]),ne={type:s.string().describe("Job type from registry. Use 'ffmpeg' for custom FFmpeg commands."),inputs:s.record(s.string(),oe).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 { ref } (an uploaded asset's ID). For FFmpeg: keys match filenames in the command."),params:s.record(s.string(),s.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:s.string().optional().describe("Prevents duplicate jobs on retry. Unique value per logical operation.")},re=[{type:"ffmpeg",summary:"Run any FFmpeg command on hosted infrastructure (transcode, trim, mux, filter, concat)."},{type:"captions.animate",summary:"Burn animated word-level captions onto a video. Built-in style presets, custom position, translation, AI keyword highlighting, SRT/VTT output, and bring-your-own-transcript."},{type:"caption.burn",summary:"Burn static styled subtitles into a video from an SRT/VTT/ASS file, or auto-transcribe when none is given."}];function se(e){let o=`
37
+ 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.`,ae=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_-]+$/)})]),le={type:t.string().describe("Job type from registry. Use 'ffmpeg' for custom FFmpeg commands."),inputs:t.record(t.string(),ae).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.")},ce=[{type:"ffmpeg",summary:"Run any FFmpeg command on hosted infrastructure (transcode, trim, mux, filter, concat)."},{type:"captions.animate",summary:"Burn animated word-level captions onto a video. Built-in style presets, custom position, translation, AI keyword highlighting, SRT/VTT output, and bring-your-own-transcript."},{type:"caption.burn",summary:"Burn static styled subtitles into a video from an SRT/VTT/ASS file, or auto-transcribe when none is given."}];function pe(e){let r=`
38
38
 
39
39
  Active job types:
40
- ${(e.length>0?e:re).map(t=>` ${t.type} \u2014 ${t.summary}`).join(`
41
- `)}`;return{name:"submit_job",title:"Submit Rendobar Job",description:te+o+`
40
+ ${(e.length>0?e:ce).map(o=>` ${o.type} \u2014 ${o.summary}`).join(`
41
+ `)}`;return{name:"submit_job",title:"Submit Rendobar Job",description:ie+r+`
42
42
 
43
- For local files, call upload_file first to get a downloadUrl, then use it as inputs.source.`,inputSchema:ne,annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(t,r)=>{let a=await c(r).jobs.create({type:t.type,inputs:t.inputs,params:t.params,idempotencyKey:t.idempotencyKey});return{jobId:a.id,status:a.status}}}}var ie={name:"cancel_job",title:"Cancel Rendobar Job",description:"Cancel a job that has not started running. Only jobs in status 'waiting' or 'dispatched' can be cancelled. Use when the user changes their mind, or when you submitted the wrong job. Running, completed, failed, or already-cancelled jobs cannot be cancelled.",inputSchema:{jobId:s.string().describe("Job ID to cancel (e.g. 'job_abc123')")},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}}},y=e=>e;async function k(e){let n=[];try{e!==null&&(n=await e.jobs.types())}catch{}return[y(Q),y(ee),y(se(n)),y(ie)]}import{z as h}from"zod";import{promises as ae}from"fs";import R from"path";import{promises as D}from"fs";import L from"path";async function F(e,n){let o=L.resolve(n.cwd,e),t=await D.realpath(o);if(n.roots!==void 0&&n.roots.length>0&&!(await Promise.all(n.roots.map(i=>D.realpath(i).catch(()=>null)))).some(i=>i!==null&&(t===i||t.startsWith(i+L.sep))))throw new Error(`Path is outside the allowed MCP roots: ${t}`);return t}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 t=await F(e.path,{cwd:process.cwd()}),r=await ae.open(t,"r");try{let a=await r.stat();if(!a.isFile())throw new Error(`Path is not a regular file: ${R.basename(t)}`);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:R.basename(t),sizeBytes:i});let g=await r.readFile(),m=new Blob([g]),f=await c(n).uploads.create(m,{filename:e.filename??R.basename(t),signal:o.signal});return n.logger.info({msg:"upload_complete",basename:R.basename(t),sizeBytes:i}),{downloadUrl:f.url,sizeBytes:i}}finally{await r.close()}}},ue=e=>e;function N(){return[ue(ce)]}async function U(e,n){for(let o of I())b(e,n,o);for(let o of await k(n.sdk))b(e,n,o);for(let o of N())b(e,n,o)}import{PostHog as pe}from"posthog-node";import{instrument as de}from"@posthog/mcp";var M=process.env.RENDOBAR_TELEMETRY_KEY??"phc_pf4JwZ5WGtDcWDG6kYEkDZtEAy7dbunkNHthLj8JRa9v",me=process.env.RENDOBAR_TELEMETRY_HOST??"https://e.rendobar.com",fe=["$mcp_parameters","$mcp_response"];function ge(){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 be(){return M.length>0&&!ge()}function ye(e){for(let n of fe)delete e[n];return e}function B(e,n){if(!be())return null;let o=new pe(M,{host:me,flushAt:1,flushInterval:0});return de(e,o,{beforeSend:t=>(t.properties=ye(t.properties),t)}),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 Re="1.4.0";async function z(e){let n=A(e.config,e.logger),o=new he({name:"rendobar",version:Re},{capabilities:{tools:{},logging:{}},instructions:E});await U(o,n);let t=B(o,e.logger);return{server:o,cleanup:async()=>{t&&await t()}}}var S="1.4.0",ve=`@rendobar/mcp v${S}
43
+ 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.`,inputSchema:le,outputSchema:{jobId:t.string(),status:t.string().describe("Initial status, normally 'waiting'")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(o,s)=>{let i=await u(s).jobs.create({type:o.type,inputs:o.inputs,params:o.params,idempotencyKey:o.idempotencyKey});return{jobId:i.id,status:i.status}}}}var ue={name:"cancel_job",title:"Cancel Rendobar Job",description:"Cancel a job that has not started running. Only jobs in status 'waiting' or 'dispatched' can be cancelled. Use when the user changes their mind, or when you submitted the wrong job. Running, 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 r=await u(n).jobs.cancel(e.jobId);return{id:r.id,status:r.status}}},de=t.object({type:t.string(),tag:t.string(),summary:t.string(),acceptsMedia:t.array(t.string())}),me=`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.`,fe={name:"list_job_types",title:"List Rendobar Job Types",description:"List every active job type with its short summary and the media kinds it accepts. Call once at the start of a media task and again when planning a chain or unsure. Result is always current.",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 u(n).jobs.types()).map(o=>de.parse(o)),guidance:me})},b=e=>e;async function k(e){let n=[];try{e!==null&&(n=await e.jobs.types())}catch{}return[b(ne),b(se),b(pe(n)),b(ue),b(fe)]}import{z as w}from"zod";import{promises as ge}from"fs";import R from"path";import{promises as L}from"fs";import D from"path";async function F(e,n){let r=D.resolve(n.cwd,e),o=await L.realpath(r);if(n.roots!==void 0&&n.roots.length>0&&!(await Promise.all(n.roots.map(a=>L.realpath(a).catch(()=>null)))).some(a=>a!==null&&(o===a||o.startsWith(a+D.sep))))throw new Error(`Path is outside the allowed MCP roots: ${o}`);return o}async function be(e){if(e.cachedMaxFileSize!==null)return e.cachedMaxFileSize;let n=await u(e).billing.state();return e.cachedMaxFileSize=n.plan.limits.maxInputFileSize,e.cachedMaxFileSize}var he={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,r)=>{let o=await F(e.path,{cwd:process.cwd()}),s=await ge.open(o,"r");try{let i=await s.stat();if(!i.isFile())throw new Error(`Path is not a regular file: ${R.basename(o)}`);let a=i.size,l=await be(n);if(a>l)throw new Error(`File size (${a} bytes) exceeds plan limit (${l} bytes). Upgrade your plan for larger uploads.`);n.logger.debug({msg:"upload_start",basename:R.basename(o),sizeBytes:a});let c=await s.readFile(),p=new Blob([c]),d=await u(n).uploads.create(p,{filename:e.filename??R.basename(o),signal:r.signal});return n.logger.info({msg:"upload_complete",basename:R.basename(o),sizeBytes:a}),{downloadUrl:d.url,sizeBytes:a}}finally{await s.close()}}},ye=e=>e;function N(){return[ye(he)]}async function U(e,n){for(let r of P())h(e,n,r);for(let r of await k(n.sdk))h(e,n,r);for(let r of N())h(e,n,r)}import{PostHog as we}from"posthog-node";import{instrument as Re}from"@posthog/mcp";var M=process.env.RENDOBAR_TELEMETRY_KEY??"phc_pf4JwZ5WGtDcWDG6kYEkDZtEAy7dbunkNHthLj8JRa9v",Se=process.env.RENDOBAR_TELEMETRY_HOST??"https://e.rendobar.com",je=["$mcp_parameters","$mcp_response"];function _e(){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 ve(){return M.length>0&&!_e()}function Te(e){for(let n of je)delete e[n];return e}function B(e,n){if(!ve())return null;let r=new we(M,{host:Se,flushAt:1,flushInterval:0});return Re(e,r,{beforeSend:o=>(o.properties=Te(o.properties),o)}),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 r.shutdown()}catch{}}}var Ae="1.6.0";async function H(e){let n=A(e.config,e.logger),r=new xe({name:"rendobar",version:Ae},{capabilities:{tools:{},logging:{}},instructions:E});await U(r,n);let o=B(r,e.logger);return{server:r,cleanup:async()=>{o&&await o()}}}var j="1.6.0",Ie=`@rendobar/mcp v${j}
44
44
  Local stdio Model Context Protocol server for Rendobar.
45
45
 
46
46
  Usage:
@@ -61,8 +61,8 @@ Auth resolution (first match wins):
61
61
  Get an API key: https://app.rendobar.com/settings/api-keys
62
62
  Docs: https://rendobar.com/docs/mcp/
63
63
  Issues: https://github.com/rendobar/mcp/issues
64
- `;function _e(){if(Se()==="win32"){let e=process.env.APPDATA??w.join(H(),"AppData","Roaming");return w.join(e,"rendobar","credentials.json")}return w.join(H(),".config","rendobar","credentials.json")}async function Te(){let e=process.argv.slice(2);(e.includes("--help")||e.includes("-h"))&&(process.stdout.write(ve),process.exit(0)),(e.includes("--version")||e.includes("-V"))&&(process.stdout.write(S+`
64
+ `;function Pe(){if(Ce()==="win32"){let e=process.env.APPDATA??S.join(z(),"AppData","Roaming");return S.join(e,"rendobar","credentials.json")}return S.join(z(),".config","rendobar","credentials.json")}async function Oe(){let e=process.argv.slice(2);(e.includes("--help")||e.includes("-h"))&&(process.stdout.write(Ie),process.exit(0)),(e.includes("--version")||e.includes("-V"))&&(process.stdout.write(j+`
65
65
  `),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}
66
- `),process.exit(1));let t;try{t=await x({argv:e,env:process.env,credsPath:_e()})}catch(f){throw f instanceof d&&(process.stderr.write(f.message+`
67
- `),process.exit(1)),f}let r=_({level:t.logLevel,name:"rendobar-mcp",patchConsole:!0}),{server:a,cleanup:i}=await z({config:t,logger:r}),l=!1,g=async f=>{if(!l){l=!0,r.info({msg:"shutdown",signal:f});try{await i()}catch{}process.exit(0)}};process.on("SIGINT",()=>{g("SIGINT")}),process.on("SIGTERM",()=>{g("SIGTERM")}),t.apiKey===null&&r.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 m=new we;await a.connect(m),r.info({msg:"ready",version:S})}Te().catch(e=>{process.stderr.write(`Fatal: ${e instanceof Error?e.message:String(e)}
66
+ `),process.exit(1));let o;try{o=await x({argv:e,env:process.env,credsPath:Pe()})}catch(d){throw d instanceof g&&(process.stderr.write(d.message+`
67
+ `),process.exit(1)),d}let s=v({level:o.logLevel,name:"rendobar-mcp",patchConsole:!0}),{server:i,cleanup:a}=await H({config:o,logger:s}),l=!1,c=async d=>{if(!l){l=!0,s.info({msg:"shutdown",signal:d});try{await a()}catch{}process.exit(0)}};process.on("SIGINT",()=>{c("SIGINT")}),process.on("SIGTERM",()=>{c("SIGTERM")}),o.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 p=new Ee;await i.connect(p),s.info({msg:"ready",version:j})}Oe().catch(e=>{process.stderr.write(`Fatal: ${e instanceof Error?e.message:String(e)}
68
68
  `),process.exit(1)});
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import{McpServer as ae}from"@modelcontextprotocol/sdk/server/mcp.js";import{createClient as k}from"@rendobar/sdk";import{promises as ue}from"fs";var p=class extends Error{constructor(o){super(o),this.name="ConfigError"}};function b(e,o){let t=e.apiKey===null?null:k({apiKey:e.apiKey,baseUrl:e.apiBase});return{logger:o,sdk:t,config:e,cachedMaxFileSize:null}}function c(e){if(e.sdk===null)throw new p(`No Rendobar API key configured. Provide one via:
2
+ import{McpServer as fe}from"@modelcontextprotocol/sdk/server/mcp.js";import{createClient as D}from"@rendobar/sdk";import{promises as ye}from"fs";var f=class extends Error{constructor(o){super(o),this.name="ConfigError"}};function R(t,o){let n=t.apiKey===null?null:D({apiKey:t.apiKey,baseUrl:t.apiBase});return{logger:o,sdk:n,config:t,cachedMaxFileSize:null}}function c(t){if(t.sdk===null)throw new f(`No Rendobar API key configured. Provide one via:
3
3
  1. --api-key=<key> command-line flag
4
4
  2. RENDOBAR_API_KEY environment variable
5
5
  3. credentials file (written by 'rb login' from the Rendobar CLI)
6
6
 
7
- Get an API key at https://app.rendobar.com/settings/api-keys`);return e.sdk}var y=`Rendobar processes existing media files in the cloud.
7
+ Get an API key at https://app.rendobar.com/settings/api-keys`);return t.sdk}var j=`Rendobar processes existing media files in the cloud.
8
8
 
9
9
  Active job types:
10
10
  ffmpeg \u2014 run a custom FFmpeg command. inputs maps logical names to URLs;
@@ -21,7 +21,7 @@ Workflow:
21
21
  1. If the file is at a public HTTPS URL, pass it directly to submit_job as inputs.source (or another input name referenced by your command).
22
22
  2. If the file is on the local disk, call upload_file first to get a downloadUrl, then use that as the input URL in submit_job.
23
23
  3. For expensive jobs, call get_account first to confirm the balance covers the cost.
24
- 4. After submit_job, call get_job to poll until status is complete or failed. The output URL is on the complete response.
24
+ 4. After submit_job, call get_job with wait:true \u2014 it blocks until the job finishes (up to ~50s, then returns a snapshot; call again to keep waiting). The output URL is on the complete response.
25
25
 
26
26
  What Rendobar cannot do:
27
27
  - Generate video from text or images (no diffusion models)
@@ -29,14 +29,14 @@ What Rendobar cannot do:
29
29
  - Stream live media
30
30
  - Run arbitrary local binaries (sharp, imagemagick, yt-dlp) \u2014 only the job types above
31
31
 
32
- For anything outside the supported job types, tell the user instead of improvising locally.`;import{isApiError as F}from"@rendobar/sdk";function h(e,o,t){return async()=>{let n=Date.now();try{let s=await t();return e.logger.info({tool:o,durationMs:Date.now()-n,ok:!0}),{content:[{type:"text",text:JSON.stringify(s,null,2)}],structuredContent:s}}catch(s){let i=Date.now()-n;if(F(s))return e.logger.error({tool:o,durationMs:i,ok:!1,errCode:s.code,errMsg:s.message}),{isError:!0,content:[{type:"text",text:JSON.stringify({error:{code:s.code,message:s.message}})}]};throw e.logger.error({tool:o,durationMs:i,ok:!1,err:String(s)}),s}}}function d(e,o,t){let n={title:t.title,description:t.description,inputSchema:t.inputSchema,annotations:t.annotations};t.outputSchema!==void 0&&(n.outputSchema=t.outputSchema);let s=async(i,a)=>h(o,t.name,()=>t.execute(i,o,a))();e.registerTool(t.name,n,s)}import{z as u}from"zod";function L(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 U={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:u.string(),balanceUsd:u.number(),plan:u.string(),isPro:u.boolean(),limits:u.object({concurrentJobs:u.number(),maxFileSize:u.string(),maxFileSizeBytes:u.number(),jobTimeoutMin:u.number()})},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,o)=>{let t=await c(o).billing.state();return o.cachedMaxFileSize=t.plan.limits.maxInputFileSize,{balance:`$${t.balance.amount.toFixed(2)}`,balanceUsd:t.balance.amount,plan:t.plan.slug,isPro:t.isPro,limits:{concurrentJobs:t.plan.limits.concurrentJobs,maxFileSize:L(t.plan.limits.maxInputFileSize),maxFileSizeBytes:t.plan.limits.maxInputFileSize,jobTimeoutMin:Math.floor(t.plan.limits.maxJobTimeout/60)}}}};function w(){return[U]}import{z as r}from"zod";var S=r.object({url:r.string(),path:r.string(),type:r.string(),size:r.number(),meta:r.record(r.string(),r.unknown()).optional()}),N=r.object({data:r.unknown(),file:S.nullable(),files:S.array(),expiresAt:r.number().nullable()}),B=r.object({output:N.nullish(),error:r.object({code:r.string(),message:r.string(),detail:r.string().nullable(),retryable:r.boolean()}).nullish(),cost:r.object({amount:r.number(),currency:r.string(),formatted:r.string()}).nullable().optional()});function T(e){let o=B.safeParse(e);return o.success?o.data:{}}function M(e){let o={};return e.data!==null&&e.data!==void 0&&(o.data=e.data),e.file!==null&&(o.file=e.file),e.files.length>0&&(o.fileCount=e.files.length,o.files=e.files),e.expiresAt!==null&&(o.expiresAt=e.expiresAt),o}var z={name:"list_jobs",title:"List Recent Rendobar Jobs",description:"List the most recent jobs for the authenticated account, newest first. Use it to find a previous result's output URL, check what is currently running, or recover a job ID you lost. Returns a compact summary per job (id, type, status, createdAt, cost, and a short output summary for completed jobs); call get_job for a job's full output. Optionally filter by status or job type. Read-only \u2014 never submits or changes a job. Requires a configured API key (RENDOBAR_API_KEY); errors if none is set.",inputSchema:{status:r.enum(["waiting","dispatched","running","complete","failed","cancelled"]).optional().describe("Only return jobs in this status. Omit to return all statuses."),type:r.string().optional().describe("Only return jobs of this type, e.g. 'ffmpeg'. Omit to return all types."),limit:r.number().int().min(1).max(50).default(10).describe("How many jobs to return, newest first (1\u201350, default 10).")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,o)=>{let t=await c(o).jobs.list({status:e.status,type:e.type,limit:e.limit});return{jobs:t.data.map(n=>{let s=T(n),i={id:n.id,type:n.type,status:n.status,createdAt:new Date(n.createdAt).toISOString(),cost:s.cost?.formatted??null};if(n.status==="complete"&&s.output){let a=s.output,l={};a.file!==null&&(l.url=a.file.url),a.files.length>0&&(l.fileCount=a.files.length),a.data!==null&&a.data!==void 0&&(l.hasData=!0),i.output=l}return i}),total:t.meta.total}}},H={name:"get_job",title:"Get Rendobar Job",description:"Check status and get results of a submitted job. Poll until status is 'complete' or 'failed'. 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:r.string().describe("Job ID returned by submit_job (e.g. 'job_abc123')")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(e,o)=>{let t=await c(o).jobs.get(e.jobId),n=T(t),s={id:t.id,type:t.type,status:t.status};if(t.status==="running"&&t.steps.length>0){let i=t.steps.filter(l=>l.status==="complete").length;s.progress=+(i/t.steps.length).toFixed(2);let a=t.steps.find(l=>l.status==="running");a!==void 0&&(s.step=a.name)}return t.status==="complete"&&(n.cost&&(s.cost=n.cost.formatted),t.completedAt!==null&&(s.durationMs=t.completedAt-t.createdAt),n.output&&(s.output=M(n.output))),t.status==="failed"&&n.error&&(s.error={code:n.error.code,message:n.error.message,detail:n.error.detail,retryable:n.error.retryable}),s}},K=`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.
32
+ For anything outside the supported job types, tell the user instead of improvising locally.`;import{isApiError as L}from"@rendobar/sdk";function S(t,o,n){return async()=>{let r=Date.now();try{let s=await n();return t.logger.info({tool:o,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(L(s))return t.logger.error({tool:o,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 t.logger.error({tool:o,durationMs:a,ok:!1,err:String(s)}),s}}}function g(t,o,n){let r={title:n.title,description:n.description,inputSchema:n.inputSchema,annotations:n.annotations};n.outputSchema!==void 0&&(r.outputSchema=n.outputSchema);let s=async(a,i)=>S(o,n.name,()=>n.execute(a,o,i))();t.registerTool(n.name,r,s)}import{z as d}from"zod";function U(t){return t>=1073741824?`${(t/1073741824).toFixed(1)} GB`:t>=1048576?`${(t/1048576).toFixed(0)} MB`:t>=1024?`${(t/1024).toFixed(0)} KB`:`${t} B`}var N={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(t,o)=>{let n=await c(o).billing.state();return o.cachedMaxFileSize=n.plan.limits.maxInputFileSize,{balance:`$${n.balance.amount.toFixed(2)}`,balanceUsd:n.balance.amount,plan:n.plan.slug,isPro:n.isPro,limits:{concurrentJobs:n.plan.limits.concurrentJobs,maxFileSize:U(n.plan.limits.maxInputFileSize),maxFileSizeBytes:n.plan.limits.maxInputFileSize,jobTimeoutMin:Math.floor(n.plan.limits.maxJobTimeout/60)}}}};function _(){return[N]}import{z as e}from"zod";import{WaitTimeoutError as M}from"@rendobar/sdk";var b=e.object({url:e.string(),path:e.string(),type:e.string(),size:e.number(),meta:e.record(e.string(),e.unknown()).optional()}),B=e.object({data:e.unknown(),file:b.nullable(),files:b.array(),expiresAt:e.number().nullable()}),H=e.object({output:B.nullish(),error:e.object({code:e.string(),message:e.string(),detail:e.string().nullable(),retryable:e.boolean()}).nullish(),cost:e.object({amount:e.number(),currency:e.string(),formatted:e.string()}).nullable().optional()});function x(t){let o=H.safeParse(t);return o.success?o.data:{}}function z(t){let o={};return t.data!==null&&t.data!==void 0&&(o.data=t.data),t.file!==null&&(o.file=t.file),t.files.length>0&&(o.fileCount=t.files.length,o.files=t.files),t.expiresAt!==null&&(o.expiresAt=t.expiresAt),o}var J=e.object({data:e.unknown().optional().describe("Computed JSON answer (probe info, detections, transcript)"),file:b.optional().describe("Headline produced file"),fileCount:e.number().optional(),files:b.array().optional().describe("Every produced file"),expiresAt:e.number().optional().describe("Epoch ms when the file URLs expire")}),K=e.object({code:e.string(),message:e.string(),detail:e.string().nullable(),retryable:e.boolean()}),G={name:"list_jobs",title:"List Recent Rendobar Jobs",description:"List the most recent jobs for the authenticated account, newest first. Use it to find a previous result's output URL, check what is currently running, or recover a job ID you lost. Returns a compact summary per job (id, type, status, createdAt, cost, and a short output summary for completed jobs); call get_job for a job's full output. Optionally filter by status or job type. Read-only \u2014 never submits or changes a job. Requires a configured API key (RENDOBAR_API_KEY); errors if none is set.",inputSchema:{status:e.enum(["waiting","dispatched","running","complete","failed","cancelled"]).optional().describe("Only return jobs in this status. Omit to return all statuses."),type:e.string().optional().describe("Only return jobs of this type, e.g. 'ffmpeg'. Omit to return all types."),limit:e.number().int().min(1).max(50).default(10).describe("How many jobs to return, newest first (1\u201350, default 10).")},outputSchema:{jobs:e.array(e.object({id:e.string(),type:e.string(),status:e.string(),createdAt:e.string().describe("ISO 8601"),cost:e.string().nullable(),output:e.object({url:e.string().optional().describe("Headline file URL"),fileCount:e.number().optional(),hasData:e.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:e.number()},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(t,o)=>{let n=await c(o).jobs.list({status:t.status,type:t.type,limit:t.limit});return{jobs:n.data.map(r=>{let s=x(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:n.meta.total}}},V=5e4,Z={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:e.string().describe("Job ID returned by submit_job (e.g. 'job_abc123')"),wait:e.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:e.string(),type:e.string(),status:e.string().describe("Open set: waiting | dispatched | running | complete | failed | cancelled"),progress:e.number().optional().describe("Fraction of completed steps (0\u20131); present while running"),step:e.string().optional().describe("Name of the currently running step"),cost:e.string().optional().describe("Formatted cost, present when complete"),durationMs:e.number().optional(),output:J.optional().describe("Present when complete"),error:K.optional().describe("Present when failed")},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(t,o,n)=>{let r=c(o),s;if(t.wait===!0){let l=n._meta?.progressToken;try{s=await r.jobs.wait(t.jobId,{timeout:V,signal:n.signal,onProgress:p=>{if(l===void 0||n.sendNotification===void 0)return;let u=p.steps??[],w=u.filter(F=>F.status==="complete").length;n.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 M)s=await r.jobs.get(t.jobId);else throw p}}else s=await r.jobs.get(t.jobId);let a=x(s),i={id:s.id,type:s.type,status:s.status};if(s.status==="running"&&s.steps.length>0){let l=s.steps.filter(u=>u.status==="complete").length;i.progress=+(l/s.steps.length).toFixed(2);let p=s.steps.find(u=>u.status==="running");p!==void 0&&(i.step=p.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=z(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}},$=`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. Call list_job_types first when starting a media task.
33
33
 
34
- FFmpeg inputs accept a URL string, { url }, { content } (inline text staged verbatim into the workdir, for subtitle files or ffmpeg concat lists), or { ref } (an already-uploaded asset, by its asset ID). The bare URL string and { url } are equivalent.
34
+ FFmpeg inputs accept a URL string, { url }, { content } (inline text staged verbatim into the workdir, for subtitle files or ffmpeg concat lists), or { job: "job_..." } (a completed job's output). The bare URL string and { url } are equivalent. To chain jobs, pass a completed job's output as the next job's input: { job: "job_..." } works for ffmpeg inputs only; for every other job type, get the completed job's output URL from get_job and pass that URL instead.
35
35
 
36
- 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.`,J=r.union([r.string(),r.object({url:r.string()}),r.object({content:r.string()}),r.object({ref:r.string()})]),V={type:r.string().describe("Job type from registry. Use 'ffmpeg' for custom FFmpeg commands."),inputs:r.record(r.string(),J).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 { ref } (an uploaded asset's ID). For FFmpeg: keys match filenames in the command."),params:r.record(r.string(),r.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:r.string().optional().describe("Prevents duplicate jobs on retry. Unique value per logical operation.")},G=[{type:"ffmpeg",summary:"Run any FFmpeg command on hosted infrastructure (transcode, trim, mux, filter, concat)."},{type:"captions.animate",summary:"Burn animated word-level captions onto a video. Built-in style presets, custom position, translation, AI keyword highlighting, SRT/VTT output, and bring-your-own-transcript."},{type:"caption.burn",summary:"Burn static styled subtitles into a video from an SRT/VTT/ASS file, or auto-transcribe when none is given."}];function Z(e){let t=`
36
+ 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.`,Y=e.union([e.string(),e.object({url:e.string()}),e.object({content:e.string()}),e.object({job:e.string().regex(/^job_[A-Za-z0-9_-]+$/)})]),W={type:e.string().describe("Job type from registry. Use 'ffmpeg' for custom FFmpeg commands."),inputs:e.record(e.string(),Y).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:e.record(e.string(),e.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:e.string().optional().describe("Prevents duplicate jobs on retry. Unique value per logical operation.")},q=[{type:"ffmpeg",summary:"Run any FFmpeg command on hosted infrastructure (transcode, trim, mux, filter, concat)."},{type:"captions.animate",summary:"Burn animated word-level captions onto a video. Built-in style presets, custom position, translation, AI keyword highlighting, SRT/VTT output, and bring-your-own-transcript."},{type:"caption.burn",summary:"Burn static styled subtitles into a video from an SRT/VTT/ASS file, or auto-transcribe when none is given."}];function Q(t){let n=`
37
37
 
38
38
  Active job types:
39
- ${(e.length>0?e:G).map(n=>` ${n.type} \u2014 ${n.summary}`).join(`
40
- `)}`;return{name:"submit_job",title:"Submit Rendobar Job",description:K+t+`
39
+ ${(t.length>0?t:q).map(r=>` ${r.type} \u2014 ${r.summary}`).join(`
40
+ `)}`;return{name:"submit_job",title:"Submit Rendobar Job",description:$+n+`
41
41
 
42
- For local files, call upload_file first to get a downloadUrl, then use it as inputs.source.`,inputSchema:V,annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(n,s)=>{let i=await c(s).jobs.create({type:n.type,inputs:n.inputs,params:n.params,idempotencyKey:n.idempotencyKey});return{jobId:i.id,status:i.status}}}}var $={name:"cancel_job",title:"Cancel Rendobar Job",description:"Cancel a job that has not started running. Only jobs in status 'waiting' or 'dispatched' can be cancelled. Use when the user changes their mind, or when you submitted the wrong job. Running, completed, failed, or already-cancelled jobs cannot be cancelled.",inputSchema:{jobId:r.string().describe("Job ID to cancel (e.g. 'job_abc123')")},annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},execute:async(e,o)=>{let t=await c(o).jobs.cancel(e.jobId);return{id:t.id,status:t.status}}},m=e=>e;async function x(e){let o=[];try{e!==null&&(o=await e.jobs.types())}catch{}return[m(z),m(H),m(Z(o)),m($)]}import{z as f}from"zod";import{promises as Y}from"fs";import g from"path";import{promises as _}from"fs";import v from"path";async function j(e,o){let t=v.resolve(o.cwd,e),n=await _.realpath(t);if(o.roots!==void 0&&o.roots.length>0&&!(await Promise.all(o.roots.map(a=>_.realpath(a).catch(()=>null)))).some(a=>a!==null&&(n===a||n.startsWith(a+v.sep))))throw new Error(`Path is outside the allowed MCP roots: ${n}`);return n}async function W(e){if(e.cachedMaxFileSize!==null)return e.cachedMaxFileSize;let o=await c(e).billing.state();return e.cachedMaxFileSize=o.plan.limits.maxInputFileSize,e.cachedMaxFileSize}var q={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:f.string().describe("Absolute or working-dir-relative path to the file"),filename:f.string().optional().describe("Filename hint sent to Rendobar (defaults to basename of path)")},outputSchema:{downloadUrl:f.string().url(),sizeBytes:f.number()},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(e,o,t)=>{let n=await j(e.path,{cwd:process.cwd()}),s=await Y.open(n,"r");try{let i=await s.stat();if(!i.isFile())throw new Error(`Path is not a regular file: ${g.basename(n)}`);let a=i.size,l=await W(o);if(a>l)throw new Error(`File size (${a} bytes) exceeds plan limit (${l} bytes). Upgrade your plan for larger uploads.`);o.logger.debug({msg:"upload_start",basename:g.basename(n),sizeBytes:a});let I=await s.readFile(),P=new Blob([I]),D=await c(o).uploads.create(P,{filename:e.filename??g.basename(n),signal:t.signal});return o.logger.info({msg:"upload_complete",basename:g.basename(n),sizeBytes:a}),{downloadUrl:D.url,sizeBytes:a}}finally{await s.close()}}},Q=e=>e;function A(){return[Q(q)]}async function E(e,o){for(let t of w())d(e,o,t);for(let t of await x(o.sdk))d(e,o,t);for(let t of A())d(e,o,t)}import{PostHog as X}from"posthog-node";import{instrument as ee}from"@posthog/mcp";var C=process.env.RENDOBAR_TELEMETRY_KEY??"phc_pf4JwZ5WGtDcWDG6kYEkDZtEAy7dbunkNHthLj8JRa9v",te=process.env.RENDOBAR_TELEMETRY_HOST??"https://e.rendobar.com",oe=["$mcp_parameters","$mcp_response"];function ne(){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 re(){return C.length>0&&!ne()}function se(e){for(let o of oe)delete e[o];return e}function O(e,o){if(!re())return null;let t=new X(C,{host:te,flushAt:1,flushInterval:0});return ee(e,t,{beforeSend:n=>(n.properties=se(n.properties),n)}),o.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 t.shutdown()}catch{}}}var ie="1.4.0";async function le(e){let o=b(e.config,e.logger),t=new ae({name:"rendobar",version:ie},{capabilities:{tools:{},logging:{}},instructions:y});await E(t,o);let n=O(t,e.logger);return{server:t,cleanup:async()=>{n&&await n()}}}export{le as createRendobarMcpServer};
42
+ 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.`,inputSchema:W,outputSchema:{jobId:e.string(),status:e.string().describe("Initial status, normally 'waiting'")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(r,s)=>{let a=await c(s).jobs.create({type:r.type,inputs:r.inputs,params:r.params,idempotencyKey:r.idempotencyKey});return{jobId:a.id,status:a.status}}}}var X={name:"cancel_job",title:"Cancel Rendobar Job",description:"Cancel a job that has not started running. Only jobs in status 'waiting' or 'dispatched' can be cancelled. Use when the user changes their mind, or when you submitted the wrong job. Running, completed, failed, or already-cancelled jobs cannot be cancelled.",inputSchema:{jobId:e.string().describe("Job ID to cancel (e.g. 'job_abc123')")},outputSchema:{id:e.string(),status:e.string().describe("'cancelled' on success")},annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},execute:async(t,o)=>{let n=await c(o).jobs.cancel(t.jobId);return{id:n.id,status:n.status}}},ee=e.object({type:e.string(),tag:e.string(),summary:e.string(),acceptsMedia:e.array(e.string())}),te=`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.`,oe={name:"list_job_types",title:"List Rendobar Job Types",description:"List every active job type with its short summary and the media kinds it accepts. Call once at the start of a media task and again when planning a chain or unsure. Result is always current.",inputSchema:{},outputSchema:{jobTypes:e.array(e.object({type:e.string().describe("Job type identifier, e.g. 'ffmpeg'"),tag:e.string().describe("Category tag"),summary:e.string().describe("Short description"),acceptsMedia:e.array(e.string()).describe("Media kinds this type accepts, e.g. video, image, audio")})),guidance:e.string()},annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},execute:async(t,o)=>({jobTypes:(await c(o).jobs.types()).map(r=>ee.parse(r)),guidance:te})},m=t=>t;async function v(t){let o=[];try{t!==null&&(o=await t.jobs.types())}catch{}return[m(G),m(Z),m(Q(o)),m(X),m(oe)]}import{z as h}from"zod";import{promises as ne}from"fs";import y from"path";import{promises as E}from"fs";import A from"path";async function C(t,o){let n=A.resolve(o.cwd,t),r=await E.realpath(n);if(o.roots!==void 0&&o.roots.length>0&&!(await Promise.all(o.roots.map(i=>E.realpath(i).catch(()=>null)))).some(i=>i!==null&&(r===i||r.startsWith(i+A.sep))))throw new Error(`Path is outside the allowed MCP roots: ${r}`);return r}async function re(t){if(t.cachedMaxFileSize!==null)return t.cachedMaxFileSize;let o=await c(t).billing.state();return t.cachedMaxFileSize=o.plan.limits.maxInputFileSize,t.cachedMaxFileSize}var se={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(t,o,n)=>{let r=await C(t.path,{cwd:process.cwd()}),s=await ne.open(r,"r");try{let a=await s.stat();if(!a.isFile())throw new Error(`Path is not a regular file: ${y.basename(r)}`);let i=a.size,l=await re(o);if(i>l)throw new Error(`File size (${i} bytes) exceeds plan limit (${l} bytes). Upgrade your plan for larger uploads.`);o.logger.debug({msg:"upload_start",basename:y.basename(r),sizeBytes:i});let p=await s.readFile(),u=new Blob([p]),w=await c(o).uploads.create(u,{filename:t.filename??y.basename(r),signal:n.signal});return o.logger.info({msg:"upload_complete",basename:y.basename(r),sizeBytes:i}),{downloadUrl:w.url,sizeBytes:i}}finally{await s.close()}}},ie=t=>t;function O(){return[ie(se)]}async function I(t,o){for(let n of _())g(t,o,n);for(let n of await v(o.sdk))g(t,o,n);for(let n of O())g(t,o,n)}import{PostHog as ae}from"posthog-node";import{instrument as le}from"@posthog/mcp";var k=process.env.RENDOBAR_TELEMETRY_KEY??"phc_pf4JwZ5WGtDcWDG6kYEkDZtEAy7dbunkNHthLj8JRa9v",ce=process.env.RENDOBAR_TELEMETRY_HOST??"https://e.rendobar.com",pe=["$mcp_parameters","$mcp_response"];function ue(){if(process.env.DO_NOT_TRACK&&process.env.DO_NOT_TRACK!=="0")return!0;let t=(process.env.RENDOBAR_TELEMETRY??"").toLowerCase();return!!(t==="0"||t==="false"||t==="off"||t==="no"||process.env.RENDOBAR_NO_TELEMETRY||process.env.RENDOBAR_DISABLE_TELEMETRY||process.env.CI==="true")}function de(){return k.length>0&&!ue()}function me(t){for(let o of pe)delete t[o];return t}function P(t,o){if(!de())return null;let n=new ae(k,{host:ce,flushAt:1,flushInterval:0});return le(t,n,{beforeSend:r=>(r.properties=me(r.properties),r)}),o.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 n.shutdown()}catch{}}}var ge="1.6.0";async function be(t){let o=R(t.config,t.logger),n=new fe({name:"rendobar",version:ge},{capabilities:{tools:{},logging:{}},instructions:j});await I(n,o);let r=P(n,t.logger);return{server:n,cleanup:async()=>{r&&await r()}}}export{be as createRendobarMcpServer};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rendobar/mcp",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Rendobar — serverless media processing for AI agents",