@rendobar/mcp 1.8.0 → 1.8.1
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 +83 -140
- package/dist/bin.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,16 +15,12 @@
|
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
|
-
<a href="https://rendobar.com">
|
|
19
|
-
<a href="https://rendobar.com/docs/mcp/">MCP docs</a> ·
|
|
18
|
+
<a href="https://rendobar.com/docs/mcp-server">Docs</a> ·
|
|
20
19
|
<a href="https://www.npmjs.com/package/@rendobar/mcp">npm</a> ·
|
|
20
|
+
<a href="https://glama.ai/mcp/servers/kwdj3f0u3z">Glama</a> ·
|
|
21
21
|
<a href="https://discord.gg/kAGqjBzx8N">Discord</a>
|
|
22
22
|
</p>
|
|
23
|
-
|
|
24
|
-
<a href="https://glama.ai/mcp/servers/kwdj3f0u3z">
|
|
25
|
-
<img src="https://glama.ai/mcp/servers/kwdj3f0u3z/badge" alt="Rendobar MCP server on Glama" width="380">
|
|
26
|
-
</a>
|
|
27
|
-
</p>
|
|
23
|
+
|
|
28
24
|
<p align="center">
|
|
29
25
|
<a href="https://www.npmjs.com/package/@rendobar/mcp"><img src="https://img.shields.io/npm/v/@rendobar/mcp?style=flat-square&color=059669&label=npm" alt="npm version"></a>
|
|
30
26
|
<a href="https://www.npmjs.com/package/@rendobar/mcp"><img src="https://img.shields.io/npm/dm/@rendobar/mcp?style=flat-square&color=059669" alt="npm downloads"></a>
|
|
@@ -32,42 +28,60 @@
|
|
|
32
28
|
<img src="https://img.shields.io/node/v/@rendobar/mcp?style=flat-square&color=059669" alt="Node version">
|
|
33
29
|
</p>
|
|
34
30
|
|
|
31
|
+
`@rendobar/mcp` is the official Model Context Protocol server for [Rendobar](https://rendobar.com), a serverless media processing API. The server runs locally over stdio, reads files straight from your disk, and gives an AI agent six job types: raw FFmpeg commands, timeline composition, compression to a target size, subtitle burn-in, animated captions, and media inspection. Jobs run on Rendobar's infrastructure and return a hosted URL.
|
|
35
32
|
|
|
36
|
-
`@rendobar/mcp`
|
|
33
|
+
Published to npm as `@rendobar/mcp` and to the [official MCP Registry](https://registry.modelcontextprotocol.io) as `com.rendobar/mcp`.
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
## Without it
|
|
39
36
|
|
|
40
|
-
|
|
37
|
+
> **You:** Compress `talk.mp4` to under 50 MB.
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
The agent tells you to install FFmpeg. You look up whether to use CRF or two-pass, guess a number, wait, check the size, guess again. Twenty minutes later you have a file and no idea if the quality was worth it.
|
|
43
40
|
|
|
44
|
-
|
|
41
|
+
## With it
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
> **You:** Compress `talk.mp4` to under 50 MB.
|
|
44
|
+
|
|
45
|
+
```jsonc
|
|
46
|
+
upload_file { "path": "~/talks/talk.mp4" }
|
|
47
|
+
submit_job { "type": "compress.target",
|
|
48
|
+
"inputs": { "source": "https://cdn.rendobar.com/u/abc123/talk.mp4" },
|
|
49
|
+
"params": { "target": { "maxSize": "50mb" } } }
|
|
50
|
+
get_job { "jobId": "job_9f2a", "wait": true }
|
|
51
|
+
// → complete · $0.01 · https://cdn.rendobar.com/o/job_9f2a/out.mp4
|
|
48
52
|
```
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
No FFmpeg on your machine. No guessing at flags. The agent reports what it actually achieved.
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
## Install
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
Rendobar has two MCP servers. Pick by whether the agent needs your filesystem.
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
| | `@rendobar/mcp` (this package) | Hosted (`api.rendobar.com/mcp`) |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| Transport | stdio, spawned by your client | Streamable HTTP |
|
|
63
|
+
| Reads local files | Yes. That is the reason it exists | No. The server has no disk |
|
|
64
|
+
| Auth | API key | OAuth in the browser, or a Bearer key |
|
|
65
|
+
| Best for | Claude Desktop, Cursor, Cline, Zed | claude.ai, ChatGPT, hosted gateways |
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
**Hosted, no API key**, one command:
|
|
59
68
|
|
|
60
|
-
|
|
69
|
+
```bash
|
|
70
|
+
claude mcp add --transport http rendobar https://api.rendobar.com/mcp
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Local**, for filesystem access. Get a key at [app.rendobar.com](https://app.rendobar.com) → Settings → API Keys, then:
|
|
61
74
|
|
|
62
75
|
```bash
|
|
63
76
|
claude mcp add rendobar -s user --env RENDOBAR_API_KEY=rb_... -- npx -y @rendobar/mcp
|
|
64
77
|
```
|
|
65
78
|
|
|
66
|
-
Already ran `rb login` with the Rendobar CLI? Drop
|
|
79
|
+
Already ran `rb login` with the Rendobar CLI? Drop `--env`. The server finds the credentials file.
|
|
67
80
|
|
|
68
|
-
|
|
81
|
+
<details>
|
|
82
|
+
<summary><strong>Claude Desktop, Cursor, Cline, Windsurf</strong></summary>
|
|
69
83
|
|
|
70
|
-
|
|
84
|
+
Same block for all four. Claude Desktop: `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows). Cursor: `~/.cursor/mcp.json`. Windsurf: `~/.codeium/windsurf/mcp_config.json`. Cline: MCP panel → Configure.
|
|
71
85
|
|
|
72
86
|
```json
|
|
73
87
|
{
|
|
@@ -81,23 +95,13 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) o
|
|
|
81
95
|
}
|
|
82
96
|
```
|
|
83
97
|
|
|
84
|
-
Restart
|
|
85
|
-
|
|
86
|
-
#### Cursor
|
|
87
|
-
|
|
88
|
-
Edit `~/.cursor/mcp.json` or `<project>/.cursor/mcp.json`. Same schema as Claude Desktop.
|
|
98
|
+
Restart the client afterwards.
|
|
99
|
+
</details>
|
|
89
100
|
|
|
90
|
-
|
|
101
|
+
<details>
|
|
102
|
+
<summary><strong>Zed, VS Code, Continue</strong></summary>
|
|
91
103
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#### Windsurf
|
|
95
|
-
|
|
96
|
-
Edit `~/.codeium/windsurf/mcp_config.json`. Same schema.
|
|
97
|
-
|
|
98
|
-
#### Zed
|
|
99
|
-
|
|
100
|
-
Edit `~/.config/zed/settings.json`:
|
|
104
|
+
Zed uses `context_servers` instead of `mcpServers`, in `~/.config/zed/settings.json`:
|
|
101
105
|
|
|
102
106
|
```json
|
|
103
107
|
{
|
|
@@ -112,9 +116,7 @@ Edit `~/.config/zed/settings.json`:
|
|
|
112
116
|
}
|
|
113
117
|
```
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
Edit `.vscode/mcp.json`:
|
|
119
|
+
VS Code 1.101+, in `.vscode/mcp.json`, prompts for the key instead of storing it:
|
|
118
120
|
|
|
119
121
|
```json
|
|
120
122
|
{
|
|
@@ -129,9 +131,7 @@ Edit `.vscode/mcp.json`:
|
|
|
129
131
|
}
|
|
130
132
|
```
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
Create `.continue/mcpServers/rendobar.yaml`:
|
|
134
|
+
Continue, in `.continue/mcpServers/rendobar.yaml`:
|
|
135
135
|
|
|
136
136
|
```yaml
|
|
137
137
|
type: stdio
|
|
@@ -140,87 +140,38 @@ args: ["-y", "@rendobar/mcp"]
|
|
|
140
140
|
env:
|
|
141
141
|
RENDOBAR_API_KEY: rb_...
|
|
142
142
|
```
|
|
143
|
+
</details>
|
|
144
|
+
|
|
145
|
+
Needs Node 20.10 or later. The server checks at startup and exits with a clear message on older versions.
|
|
143
146
|
|
|
144
147
|
## Tools
|
|
145
148
|
|
|
146
149
|
| Tool | Purpose |
|
|
147
150
|
|---|---|
|
|
148
|
-
| `upload_file` | Upload a local file. Returns a
|
|
149
|
-
| `list_job_types` |
|
|
150
|
-
| `submit_job` | Submit
|
|
151
|
-
| `get_job` |
|
|
152
|
-
| `list_jobs` |
|
|
153
|
-
| `cancel_job` | Cancel a waiting
|
|
154
|
-
| `get_account` |
|
|
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
|
-
|
|
163
|
-
### Job types
|
|
164
|
-
|
|
165
|
-
`submit_job` takes a `type`. The active types:
|
|
166
|
-
|
|
167
|
-
| `type` | What it does |
|
|
168
|
-
|---|---|
|
|
169
|
-
| `ffmpeg` | Run any FFmpeg command (transcode, trim, mux, filter, concat). |
|
|
170
|
-
| `captions.animate` | Burn animated word-level captions onto a video (Hormozi / MrBeast / TikTok / pill presets). |
|
|
171
|
-
| `caption.burn` | Burn static styled subtitles from an SRT/VTT/ASS file, or auto-transcribe when none is given. |
|
|
172
|
-
|
|
173
|
-
### Example
|
|
174
|
-
|
|
175
|
-
A typical exchange once the server is configured in your client:
|
|
151
|
+
| `upload_file` | Upload a local file. Returns a URL to use in `submit_job`. |
|
|
152
|
+
| `list_job_types` | Every active job type, read live. Call this first. |
|
|
153
|
+
| `submit_job` | Submit a job of any type. |
|
|
154
|
+
| `get_job` | Status and result. Pass `wait: true` to long-poll for ~50s. |
|
|
155
|
+
| `list_jobs` | Recent jobs. |
|
|
156
|
+
| `cancel_job` | Cancel a waiting or dispatched job. |
|
|
157
|
+
| `get_account` | Balance, plan limits, active job count. |
|
|
176
158
|
|
|
177
|
-
|
|
159
|
+
## Job types
|
|
178
160
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
submit_job {
|
|
188
|
-
"type": "ffmpeg",
|
|
189
|
-
"inputs": { "intro.mp4": "https://cdn.rendobar.com/u/abc123/intro.mp4" },
|
|
190
|
-
"params": { "command": "-i intro.mp4 -af \"volume=enable='lt(t,3)':volume=0\" -c:v copy out.mp4" }
|
|
191
|
-
}
|
|
192
|
-
// → { "jobId": "job_9f2a", "status": "waiting" }
|
|
193
|
-
|
|
194
|
-
// 3. Poll until done
|
|
195
|
-
get_job { "jobId": "job_9f2a" }
|
|
196
|
-
// → { "status": "complete", "cost": "$0.01", "output": { "file": { "url": "https://cdn.rendobar.com/o/job_9f2a/out.mp4", "type": "video" } } }
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
> **Agent:** Done — muted the first 3 seconds. Output: https://cdn.rendobar.com/o/job_9f2a/out.mp4
|
|
200
|
-
|
|
201
|
-
Auto-caption a clip with animated word-level captions — no subtitle file needed:
|
|
202
|
-
|
|
203
|
-
```jsonc
|
|
204
|
-
submit_job {
|
|
205
|
-
"type": "captions.animate",
|
|
206
|
-
"inputs": { "clip.mp4": "https://cdn.rendobar.com/u/abc123/clip.mp4" },
|
|
207
|
-
"params": { "preset": "hormozi" }
|
|
208
|
-
}
|
|
209
|
-
// → { "jobId": "job_7c1b", "status": "waiting" }
|
|
210
|
-
```
|
|
161
|
+
| `type` | Accepts | What it does |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `ffmpeg` | video | Any FFmpeg command: transcode, trim, mux, filter, concat. |
|
|
164
|
+
| `ffprobe` | video, image, audio | Read codec, resolution, duration, rotation before committing to parameters. |
|
|
165
|
+
| `compose` | video | Render from a JSON timeline: clips, transitions, text overlays, keyframes, multi-track audio. |
|
|
166
|
+
| `compress.target` | video, image, audio | Hit a size or quality budget and report what it achieved. |
|
|
167
|
+
| `caption.burn` | video | Burn an SRT, VTT, or ASS file into the video, or transcribe when none is given. |
|
|
168
|
+
| `captions.animate` | video | Word-level animated captions. Eleven presets including Hormozi, MrBeast, TikTok, karaoke. |
|
|
211
169
|
|
|
212
|
-
|
|
213
|
-
and directories can list them; calls that need the API return a clear error until
|
|
214
|
-
`RENDOBAR_API_KEY` is set.
|
|
170
|
+
That table is a snapshot of this release. `list_job_types` reads the registry on every call, so it never goes stale. Prefer it.
|
|
215
171
|
|
|
216
|
-
|
|
172
|
+
### Chaining
|
|
217
173
|
|
|
218
|
-
|
|
219
|
-
|---|---|---|
|
|
220
|
-
| Transport | stdio, spawned by your client | Streamable HTTP |
|
|
221
|
-
| Local file upload | Yes, the whole point | No, server has no disk |
|
|
222
|
-
| Setup | `npx` line in a config file | OAuth in the browser (`claude mcp add --transport http`), or a Bearer API key |
|
|
223
|
-
| Best for | Claude Desktop, Cursor, Cline, Zed, local agents | claude.ai web, ChatGPT, hosted gateways |
|
|
174
|
+
A `submit_job` input can point at a previous job's output, so a multi-step edit never round-trips through your disk. For `ffmpeg` inputs, pass `{ job: "job_..." }`. For other types, read the output URL from `get_job` and pass that.
|
|
224
175
|
|
|
225
176
|
## Authentication
|
|
226
177
|
|
|
@@ -228,45 +179,37 @@ Three sources, first match wins:
|
|
|
228
179
|
|
|
229
180
|
1. `--api-key=<key>` flag
|
|
230
181
|
2. `RENDOBAR_API_KEY` environment variable
|
|
231
|
-
3. `~/.config/rendobar/credentials.json`
|
|
232
|
-
|
|
233
|
-
## Troubleshooting
|
|
234
|
-
|
|
235
|
-
### Cursor on macOS (Dock launch) can't find npx
|
|
236
|
-
|
|
237
|
-
Cursor launched from the Dock has the GUI PATH, not the shell PATH. Use the absolute path to `npx` in your `mcp.json`:
|
|
238
|
-
|
|
239
|
-
```json
|
|
240
|
-
"command": "/Users/you/.nvm/versions/node/v20.x/bin/npx"
|
|
241
|
-
```
|
|
182
|
+
3. `~/.config/rendobar/credentials.json` on Unix, `%APPDATA%\rendobar\credentials.json` on Windows, written by `rb login` (Rendobar CLI 1.1+)
|
|
242
183
|
|
|
243
|
-
|
|
184
|
+
The server starts without a key so clients and directories can list its tools, and it reads the job registry unauthenticated, so the type list is live rather than baked into the build. Calls that reach the API return a clear error until a key is set.
|
|
244
185
|
|
|
245
|
-
|
|
186
|
+
## Telemetry
|
|
246
187
|
|
|
247
|
-
|
|
188
|
+
The server reports anonymous usage through PostHog's MCP Analytics SDK: tool name, success, duration, and the agent's stated intent.
|
|
248
189
|
|
|
249
|
-
|
|
190
|
+
It never sends your parameters or responses. File URLs, job configs, and outputs are stripped before anything leaves the process. Events carry no account identity and build no person profile. It is off in CI automatically.
|
|
250
191
|
|
|
251
|
-
|
|
192
|
+
```bash
|
|
193
|
+
DO_NOT_TRACK=1 # or RENDOBAR_TELEMETRY=0
|
|
194
|
+
```
|
|
252
195
|
|
|
253
|
-
|
|
196
|
+
## Troubleshooting
|
|
254
197
|
|
|
255
|
-
|
|
198
|
+
<details>
|
|
199
|
+
<summary><strong>Common problems</strong></summary>
|
|
256
200
|
|
|
257
|
-
|
|
201
|
+
**Cursor on macOS can't find `npx`.** Launched from the Dock, Cursor gets the GUI PATH rather than your shell PATH. Use an absolute path: `"command": "/Users/you/.nvm/versions/node/v20.x/bin/npx"`.
|
|
258
202
|
|
|
259
|
-
|
|
203
|
+
**Windows can't find `npx`.** Use `"command": "npx.cmd"` if your client doesn't resolve it.
|
|
260
204
|
|
|
261
|
-
|
|
205
|
+
**Tools appear but calls fail with "No Rendobar API key configured".** Expected with no key set. The server advertises tools so clients can list them, but calls need credentials. Set `RENDOBAR_API_KEY`, pass `--api-key`, or run `rb login`. Startup logs a `no_api_key` warning to stderr.
|
|
262
206
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
```
|
|
207
|
+
**The server won't start.** It writes JSON lines to stderr. Check your client's output panel for entries with `level: "error"`.
|
|
208
|
+
</details>
|
|
266
209
|
|
|
267
210
|
## Contributing
|
|
268
211
|
|
|
269
|
-
See [CONTRIBUTING.md](./CONTRIBUTING.md). For AI-assisted development,
|
|
212
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md). For AI-assisted development, [AGENTS.md](./AGENTS.md) and [CLAUDE.md](./CLAUDE.md).
|
|
270
213
|
|
|
271
214
|
## License
|
|
272
215
|
|
package/dist/bin.js
CHANGED
|
@@ -42,7 +42,7 @@ ${e.map(o=>` ${o.type} \u2014 ${o.summary}`).join(`
|
|
|
42
42
|
|
|
43
43
|
The job type registry was unreachable at startup. Call list_job_types for the current list.`;return{name:"submit_job",title:"Submit Rendobar Job",description:ie+n+`
|
|
44
44
|
|
|
45
|
-
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,r)=>{let s=await u(r).jobs.create({type:o.type,inputs:o.inputs,params:o.params,idempotencyKey:o.idempotencyKey});return{jobId:s.id,status:s.status}}}}var pe={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 o=await u(n).jobs.cancel(e.jobId);return{id:o.id,status:o.status}}},ue=t.object({type:t.string(),tag:t.string(),summary:t.string(),acceptsMedia:t.array(t.string())}),de=`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.`,me={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(r=>ue.parse(r)),guidance:de})},b=e=>e;async function fe(e){let n=await fetch(new URL("/jobs/types",e),{headers:{Accept:"application/json"},signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`GET /jobs/types returned ${n.status}`);let o=await n.json();return t.object({data:t.array(t.object({type:t.string(),summary:t.string()}))}).parse(o).data}async function O(e,n){let o=[];try{o=e!==null?await e.jobs.types():await fe(n)}catch{}return[b(ne),b(se),b(ce(o)),b(pe),b(me)]}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 o=D.resolve(n.cwd,e),r=await L.realpath(o);if(n.roots!==void 0&&n.roots.length>0&&!(await Promise.all(n.roots.map(i=>L.realpath(i).catch(()=>null)))).some(i=>i!==null&&(r===i||r.startsWith(i+D.sep))))throw new Error(`Path is outside the allowed MCP roots: ${r}`);return r}async function 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,o)=>{let r=await F(e.path,{cwd:process.cwd()}),s=await ge.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 be(n);if(i>l)throw new Error(`File size (${i} bytes) exceeds plan limit (${l} bytes). Upgrade your plan for larger uploads.`);n.logger.debug({msg:"upload_start",basename:R.basename(r),sizeBytes:i});let c=await s.readFile(),p=new Blob([c]),d=await u(n).uploads.create(p,{filename:e.filename??R.basename(r),signal:o.signal});return n.logger.info({msg:"upload_complete",basename:R.basename(r),sizeBytes:i}),{downloadUrl:d.url,sizeBytes:i}}finally{await s.close()}}},ye=e=>e;function N(){return[ye(he)]}async function U(e,n){for(let o of P())h(e,n,o);for(let o of await O(n.sdk,n.config.apiBase))h(e,n,o);for(let o of N())h(e,n,o)}import{PostHog as we}from"posthog-node";import{instrument as Re}from"@posthog/mcp";var M=process.env.RENDOBAR_TELEMETRY_KEY??"phc_pf4JwZ5WGtDcWDG6kYEkDZtEAy7dbunkNHthLj8JRa9v",je=process.env.RENDOBAR_TELEMETRY_HOST??"https://e.rendobar.com",Se=["$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 Se)delete e[n];return e}function H(e,n){if(!ve())return null;let o=new we(M,{host:je,flushAt:1,flushInterval:0});return Re(e,o,{beforeSend:r=>(r.properties=Te(r.properties),r)}),n.info({msg:"Anonymous MCP usage analytics on (tool name, success, duration, intent \u2014 never your parameters, responses, or credentials). Disable with DO_NOT_TRACK=1 or RENDOBAR_TELEMETRY=0."}),async()=>{try{await o.shutdown()}catch{}}}var Ae="1.8.
|
|
45
|
+
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,r)=>{let s=await u(r).jobs.create({type:o.type,inputs:o.inputs,params:o.params,idempotencyKey:o.idempotencyKey});return{jobId:s.id,status:s.status}}}}var pe={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 o=await u(n).jobs.cancel(e.jobId);return{id:o.id,status:o.status}}},ue=t.object({type:t.string(),tag:t.string(),summary:t.string(),acceptsMedia:t.array(t.string())}),de=`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.`,me={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(r=>ue.parse(r)),guidance:de})},b=e=>e;async function fe(e){let n=await fetch(new URL("/jobs/types",e),{headers:{Accept:"application/json"},signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`GET /jobs/types returned ${n.status}`);let o=await n.json();return t.object({data:t.array(t.object({type:t.string(),summary:t.string()}))}).parse(o).data}async function O(e,n){let o=[];try{o=e!==null?await e.jobs.types():await fe(n)}catch{}return[b(ne),b(se),b(ce(o)),b(pe),b(me)]}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 o=D.resolve(n.cwd,e),r=await L.realpath(o);if(n.roots!==void 0&&n.roots.length>0&&!(await Promise.all(n.roots.map(i=>L.realpath(i).catch(()=>null)))).some(i=>i!==null&&(r===i||r.startsWith(i+D.sep))))throw new Error(`Path is outside the allowed MCP roots: ${r}`);return r}async function 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,o)=>{let r=await F(e.path,{cwd:process.cwd()}),s=await ge.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 be(n);if(i>l)throw new Error(`File size (${i} bytes) exceeds plan limit (${l} bytes). Upgrade your plan for larger uploads.`);n.logger.debug({msg:"upload_start",basename:R.basename(r),sizeBytes:i});let c=await s.readFile(),p=new Blob([c]),d=await u(n).uploads.create(p,{filename:e.filename??R.basename(r),signal:o.signal});return n.logger.info({msg:"upload_complete",basename:R.basename(r),sizeBytes:i}),{downloadUrl:d.url,sizeBytes:i}}finally{await s.close()}}},ye=e=>e;function N(){return[ye(he)]}async function U(e,n){for(let o of P())h(e,n,o);for(let o of await O(n.sdk,n.config.apiBase))h(e,n,o);for(let o of N())h(e,n,o)}import{PostHog as we}from"posthog-node";import{instrument as Re}from"@posthog/mcp";var M=process.env.RENDOBAR_TELEMETRY_KEY??"phc_pf4JwZ5WGtDcWDG6kYEkDZtEAy7dbunkNHthLj8JRa9v",je=process.env.RENDOBAR_TELEMETRY_HOST??"https://e.rendobar.com",Se=["$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 Se)delete e[n];return e}function H(e,n){if(!ve())return null;let o=new we(M,{host:je,flushAt:1,flushInterval:0});return Re(e,o,{beforeSend:r=>(r.properties=Te(r.properties),r)}),n.info({msg:"Anonymous MCP usage analytics on (tool name, success, duration, intent \u2014 never your parameters, responses, or credentials). Disable with DO_NOT_TRACK=1 or RENDOBAR_TELEMETRY=0."}),async()=>{try{await o.shutdown()}catch{}}}var Ae="1.8.1";async function B(e){let n=A(e.config,e.logger),o=new xe({name:"rendobar",version:Ae},{capabilities:{tools:{},logging:{}},instructions:E});await U(o,n);let r=H(o,e.logger);return{server:o,cleanup:async()=>{r&&await r()}}}var S="1.8.1",Ie=`@rendobar/mcp v${S}
|
|
46
46
|
Local stdio Model Context Protocol server for Rendobar.
|
|
47
47
|
|
|
48
48
|
Usage:
|
package/dist/index.js
CHANGED
|
@@ -41,4 +41,4 @@ ${t.map(o=>` ${o.type} \u2014 ${o.summary}`).join(`
|
|
|
41
41
|
|
|
42
42
|
The job type registry was unreachable at startup. Call list_job_types for the current list.`;return{name:"submit_job",title:"Submit Rendobar Job",description:V+n+`
|
|
43
43
|
|
|
44
|
-
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:Y,outputSchema:{jobId:e.string(),status:e.string().describe("Initial status, normally 'waiting'")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(o,s)=>{let r=await c(s).jobs.create({type:o.type,inputs:o.inputs,params:o.params,idempotencyKey:o.idempotencyKey});return{jobId:r.id,status:r.status}}}}var Q={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,n)=>{let o=await c(n).jobs.cancel(t.jobId);return{id:o.id,status:o.status}}},X=e.object({type:e.string(),tag:e.string(),summary:e.string(),acceptsMedia:e.array(e.string())}),ee=`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.`,te={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,n)=>({jobTypes:(await c(n).jobs.types()).map(s=>X.parse(s)),guidance:ee})},m=t=>t;async function oe(t){let n=await fetch(new URL("/jobs/types",t),{headers:{Accept:"application/json"},signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`GET /jobs/types returned ${n.status}`);let o=await n.json();return e.object({data:e.array(e.object({type:e.string(),summary:e.string()}))}).parse(o).data}async function v(t,n){let o=[];try{o=t!==null?await t.jobs.types():await oe(n)}catch{}return[m(G),m($),m(q(o)),m(Q),m(te)]}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,n){let o=A.resolve(n.cwd,t),s=await E.realpath(o);if(n.roots!==void 0&&n.roots.length>0&&!(await Promise.all(n.roots.map(a=>E.realpath(a).catch(()=>null)))).some(a=>a!==null&&(s===a||s.startsWith(a+A.sep))))throw new Error(`Path is outside the allowed MCP roots: ${s}`);return s}async function re(t){if(t.cachedMaxFileSize!==null)return t.cachedMaxFileSize;let n=await c(t).billing.state();return t.cachedMaxFileSize=n.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,n,o)=>{let s=await C(t.path,{cwd:process.cwd()}),r=await ne.open(s,"r");try{let i=await r.stat();if(!i.isFile())throw new Error(`Path is not a regular file: ${y.basename(s)}`);let a=i.size,l=await re(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:y.basename(s),sizeBytes:a});let p=await r.readFile(),u=new Blob([p]),w=await c(n).uploads.create(u,{filename:t.filename??y.basename(s),signal:o.signal});return n.logger.info({msg:"upload_complete",basename:y.basename(s),sizeBytes:a}),{downloadUrl:w.url,sizeBytes:a}}finally{await r.close()}}},ae=t=>t;function O(){return[ae(se)]}async function I(t,n){for(let o of T())g(t,n,o);for(let o of await v(n.sdk,n.config.apiBase))g(t,n,o);for(let o of O())g(t,n,o)}import{PostHog as ie}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 n of pe)delete t[n];return t}function P(t,n){if(!de())return null;let o=new ie(k,{host:ce,flushAt:1,flushInterval:0});return le(t,o,{beforeSend:s=>(s.properties=me(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 ge="1.8.
|
|
44
|
+
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:Y,outputSchema:{jobId:e.string(),status:e.string().describe("Initial status, normally 'waiting'")},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},execute:async(o,s)=>{let r=await c(s).jobs.create({type:o.type,inputs:o.inputs,params:o.params,idempotencyKey:o.idempotencyKey});return{jobId:r.id,status:r.status}}}}var Q={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,n)=>{let o=await c(n).jobs.cancel(t.jobId);return{id:o.id,status:o.status}}},X=e.object({type:e.string(),tag:e.string(),summary:e.string(),acceptsMedia:e.array(e.string())}),ee=`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.`,te={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,n)=>({jobTypes:(await c(n).jobs.types()).map(s=>X.parse(s)),guidance:ee})},m=t=>t;async function oe(t){let n=await fetch(new URL("/jobs/types",t),{headers:{Accept:"application/json"},signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`GET /jobs/types returned ${n.status}`);let o=await n.json();return e.object({data:e.array(e.object({type:e.string(),summary:e.string()}))}).parse(o).data}async function v(t,n){let o=[];try{o=t!==null?await t.jobs.types():await oe(n)}catch{}return[m(G),m($),m(q(o)),m(Q),m(te)]}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,n){let o=A.resolve(n.cwd,t),s=await E.realpath(o);if(n.roots!==void 0&&n.roots.length>0&&!(await Promise.all(n.roots.map(a=>E.realpath(a).catch(()=>null)))).some(a=>a!==null&&(s===a||s.startsWith(a+A.sep))))throw new Error(`Path is outside the allowed MCP roots: ${s}`);return s}async function re(t){if(t.cachedMaxFileSize!==null)return t.cachedMaxFileSize;let n=await c(t).billing.state();return t.cachedMaxFileSize=n.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,n,o)=>{let s=await C(t.path,{cwd:process.cwd()}),r=await ne.open(s,"r");try{let i=await r.stat();if(!i.isFile())throw new Error(`Path is not a regular file: ${y.basename(s)}`);let a=i.size,l=await re(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:y.basename(s),sizeBytes:a});let p=await r.readFile(),u=new Blob([p]),w=await c(n).uploads.create(u,{filename:t.filename??y.basename(s),signal:o.signal});return n.logger.info({msg:"upload_complete",basename:y.basename(s),sizeBytes:a}),{downloadUrl:w.url,sizeBytes:a}}finally{await r.close()}}},ae=t=>t;function O(){return[ae(se)]}async function I(t,n){for(let o of T())g(t,n,o);for(let o of await v(n.sdk,n.config.apiBase))g(t,n,o);for(let o of O())g(t,n,o)}import{PostHog as ie}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 n of pe)delete t[n];return t}function P(t,n){if(!de())return null;let o=new ie(k,{host:ce,flushAt:1,flushInterval:0});return le(t,o,{beforeSend:s=>(s.properties=me(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 ge="1.8.1";async function be(t){let n=R(t.config,t.logger),o=new fe({name:"rendobar",version:ge},{capabilities:{tools:{},logging:{}},instructions:j});await I(o,n);let s=P(o,t.logger);return{server:o,cleanup:async()=>{s&&await s()}}}export{be as createRendobarMcpServer};
|