@vidofy/mcp 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vidofy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # @vidofy/mcp
2
+
3
+ [![npm](https://img.shields.io/npm/v/@vidofy/mcp?color=cb3837&logo=npm)](https://www.npmjs.com/package/@vidofy/mcp)
4
+ [![node](https://img.shields.io/badge/node-%E2%89%A518-5fa04e?logo=node.js&logoColor=white)](https://nodejs.org)
5
+ [![licence](https://img.shields.io/badge/licence-MIT-blue)](./LICENSE)
6
+ [![MCP](https://img.shields.io/badge/Model_Context_Protocol-server-6f42c1)](https://modelcontextprotocol.io)
7
+
8
+ **MCP server for [Vidofy](https://vidofy.ai)** — generate images, video, audio and speech from
9
+ Claude Desktop, Cursor, or any MCP client, **billed to your own Vidofy account**, at the same
10
+ prices the website charges.
11
+
12
+ Over 570 models, including **Veo 3.1**, **Kling 3.0**, **Flux 2**, **Seedance 2.5**, **Wan 2.7**,
13
+ **Hailuo 2.3**, **Runway**, **Luma Ray 2**, **Qwen Image 3.0**, **Vidu Q3** and **LTX 2** —
14
+ text-to-video, image-to-video, text-to-image, image editing, video and photo effects, lipsync,
15
+ text-to-speech and voice cloning. The agent browses the catalogue, prices a generation before
16
+ running it, and follows one to its result.
17
+
18
+ > **Status: v0.1.0, the first public release.**
19
+ >
20
+ > **This server is for personal Vidofy accounts.** It takes one credential,
21
+ > `VIDOFY_TOKEN`, and spends **your own coins** — the same balance the website
22
+ > spends, at the same prices. There is no other billing mode: `VIDOFY_API_KEY` is
23
+ > refused at startup, and that is a decision, not a feature waiting on a release.
24
+
25
+ ## Tools
26
+
27
+ | Tool | What it does | Spends |
28
+ |---|---|---|
29
+ | `list_modes` | What Vidofy can generate: text-to-image, image-to-video, lipsync, speech… | no |
30
+ | `list_models` | The models in one mode, with each one's credit cost and rough duration | no |
31
+ | `get_model` | One model's full input contract: a JSON Schema, its file slots and their limits | no |
32
+ | `estimate_cost` | What a generation will cost, before running it | no |
33
+ | **`generate`** | **Runs it. The only tool that spends the balance.** | **yes** |
34
+ | `get_status` | Whether a generation has finished | no |
35
+ | `get_result` | The finished media | no |
36
+ | `get_balance` | Coins left, and how many expire with the subscription | no |
37
+ | `get_usage` | Recent generations and what they cost | no |
38
+
39
+ The usual order is `list_modes` → `list_models` → `get_model` → `estimate_cost` → `generate`
40
+ → `get_status` → `get_result`.
41
+
42
+ `generate` is the only tool without `readOnlyHint`, which is what tells a client to ask the user
43
+ before running it. It charges at **submit**, not on success, and returns immediately with an id —
44
+ a generation takes from ~30 seconds to several minutes, so the agent polls `get_status` rather
45
+ than holding the call open. Output is **private by default**; pass `public: true` only when the
46
+ user asked for a permanent public link.
47
+
48
+ File inputs take a **path on the machine running the server**. The package reads the user's own
49
+ file and streams it with the submit — it never makes a temporary copy — and checks the extension
50
+ and size against that model's own limits first, so a file the server would reject never leaves
51
+ the disk.
52
+
53
+ **Not exposed, deliberately:** checkout, auto top-up, purchases, referrals, the daily reward.
54
+ Nothing in this package can buy coins or change a plan, however it is prompted.
55
+
56
+ ## Setup
57
+
58
+ There are two ways in. **Take the first one** unless your client cannot do it.
59
+
60
+ ### 1. Remote connector — nothing to install
61
+
62
+ Give your client this URL:
63
+
64
+ ```
65
+ https://vidofy.ai/mcp-app
66
+ ```
67
+
68
+ You sign in **in your browser** and approve once. No token to copy, nothing to keep in a
69
+ config file, and nothing to update when this package changes.
70
+
71
+ | Client | How |
72
+ |---|---|
73
+ | **Claude.ai** · **Claude Desktop** | Settings → Connectors → *Add custom connector* → paste the URL → **Connect**, then approve the sign-in. They share one list: add it in either and it appears in both. Available on every plan, including Free — where you get one connector. |
74
+ | **ChatGPT** | Settings → Connectors → add a custom connector (no such option? turn on Developer Mode in Settings first) → paste the URL → **Connect**, then approve. On a Business or Enterprise workspace an administrator adds it for everyone. |
75
+ | **Claude Code** · **Codex** · **Cursor** | Each accepts a remote MCP server URL. Follow that client's own MCP documentation and give it the URL above. |
76
+
77
+ Then ask it: *"list Vidofy modes"* to confirm the connection, and
78
+ *"make me a 5-second clip of a red bicycle"* — it prices the generation before running it.
79
+
80
+ ### 2. Local stdio server — for a client that only speaks stdio
81
+
82
+ Create a personal MCP token at **vidofy.ai → Studio → Account → MCP Access**. It is shown once.
83
+
84
+ ```jsonc
85
+ // claude_desktop_config.json (Cursor: .cursor/mcp.json — same shape)
86
+ {
87
+ "mcpServers": {
88
+ "vidofy": {
89
+ "command": "npx",
90
+ "args": ["-y", "@vidofy/mcp"],
91
+ "env": { "VIDOFY_TOKEN": "vmt_..." }
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ `npx` fetches it on first run. Prefer a pinned copy? `npm i -g @vidofy/mcp`, then:
98
+
99
+ ```jsonc
100
+ { "command": "vidofy-mcp", "env": { "VIDOFY_TOKEN": "vmt_..." } }
101
+ ```
102
+
103
+ Both paths reach the same account, the same models and the same balance. The difference is
104
+ only where the process runs and how you prove who you are.
105
+
106
+ ### Environment
107
+
108
+ | Variable | Required | What it does |
109
+ |---|---|---|
110
+ | `VIDOFY_TOKEN` | **yes** | Personal MCP token (`vmt_…`). Spends **your own Vidofy coins**, exactly as the studio does. |
111
+ | `VIDOFY_API_BASE` | no | Override the origin the server talks to — an **origin only**, no path. Defaults to `https://vidofy.ai`, which is what you want. |
112
+
113
+ `VIDOFY_API_KEY` is recognised only in order to be **refused**: a `vky_…` key bills a
114
+ different balance, which this server does not serve. Setting it stops startup with a message
115
+ naming the token to use instead — and setting *both* is refused too, since the two bill
116
+ different balances and no precedence rule is worth having to remember.
117
+
118
+ ## Development
119
+
120
+ ```bash
121
+ npm install
122
+ npm run build
123
+ npm run inspect # MCP Inspector — spends nothing
124
+ ```
125
+
126
+ `VIDOFY_API_BASE` points it at a different origin, if you are running one.
127
+
128
+ **Nothing here writes to stdout.** With stdio transport, stdout *is* the protocol channel — a
129
+ single stray `console.log()` puts a non-JSON line in the stream and the client drops the
130
+ connection with an error that explains nothing. Diagnostics go to stderr via the `log()` helper
131
+ in `src/index.ts`.
132
+
133
+ ## Layout
134
+
135
+ ```
136
+ src/config.ts credential + mode + base URL, validated at startup
137
+ src/backend.ts the only place that talks HTTP: auth, retries, multipart, errors
138
+ src/schema.ts one model's m_options → a JSON Schema the agent can fill in
139
+ src/map/b2c.ts both response shapes → one; strips the provider cost
140
+ src/tools/info.ts list_modes, list_models, get_model
141
+ src/tools/generation.ts estimate_cost, generate, get_status, get_result
142
+ src/tools/account.ts get_balance, get_usage
143
+ src/index.ts the server: stdio transport, tool registration, annotations
144
+
145
+
146
+ server.json MCP registry manifest (name must match package.json "mcpName")
147
+ ```
148
+
149
+ ## Licence
150
+
151
+ MIT
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The one place that talks to Vidofy over HTTP.
3
+ *
4
+ * Everything else in this package builds a request description and hands it
5
+ * here, so door selection, authentication, retries, timeouts and error shape
6
+ * are decided once instead of in nine tools.
7
+ *
8
+ * WHAT THIS FILE IS RESPONSIBLE FOR
9
+ * · picking /app/v1 or /api/v1 from the configured mode
10
+ * · attaching the credential and the User-Agent on every call
11
+ * · multipart bodies — one form field per m_* key, exactly as the studio posts
12
+ * · carrying an Idempotency-Key when the caller supplies one
13
+ * · retrying the things that are worth retrying, and nothing else
14
+ * · turning both transport failures and API error envelopes into one error type
15
+ */
16
+ import { type Config } from './config.js';
17
+ /**
18
+ * Anything that went wrong, from either side of the wire.
19
+ *
20
+ * `code` is the machine-readable one — the API's own `error` field where there
21
+ * was a response, or a transport pseudo-code where there was not. `message` is
22
+ * what a person (or a model) should read.
23
+ */
24
+ export declare class VidofyError extends Error {
25
+ readonly code: string;
26
+ readonly httpStatus: number | null;
27
+ /** Extra fields the API returned, e.g. `allowed` on INVALID_MODE. */
28
+ readonly details: Record<string, unknown>;
29
+ constructor(code: string, message: string, httpStatus?: number | null,
30
+ /** Extra fields the API returned, e.g. `allowed` on INVALID_MODE. */
31
+ details?: Record<string, unknown>);
32
+ }
33
+ /** Exported for get_status, which holds its call open rather than letting the
34
+ * agent poll in a tight loop — one definition, not a second one that drifts. */
35
+ export declare const sleep: (ms: number) => Promise<void>;
36
+ /** A local file to attach to a multipart request. */
37
+ export interface FileField {
38
+ /** The form field name — m_image, m_first_frame, m_multi_image_file_0, … */
39
+ field: string;
40
+ /** Absolute or relative path on the machine running this server. */
41
+ path: string;
42
+ /**
43
+ * Extensions this slot accepts, with or without the dot. Comes from the
44
+ * model's own m_upload_*_settings via get_model, already defaulted the way
45
+ * the server defaults them. REQUIRED in practice: an empty or missing list
46
+ * is refused, not waved through — see the check in buildBody.
47
+ */
48
+ accept?: readonly string[];
49
+ /** The slot's max_size in megabytes, from the same place. */
50
+ maxSizeMb?: number;
51
+ /**
52
+ * The slot's max_duration in seconds, from the same place. 0/absent = no
53
+ * cap. Enforced only when the file's own header states its length exactly
54
+ * — see media-duration.ts for which formats those are, and why a guess is
55
+ * not allowed to refuse an upload.
56
+ */
57
+ maxDurationSec?: number;
58
+ }
59
+ export interface RequestOptions {
60
+ method: 'GET' | 'POST';
61
+ /** Path AFTER the version prefix, e.g. 'info/modes'. No leading slash. */
62
+ path: string;
63
+ query?: Record<string, string | number | undefined>;
64
+ /**
65
+ * Form fields. Sent as multipart when `files` is present, otherwise as
66
+ * application/x-www-form-urlencoded — the two shapes the submit handler
67
+ * reads. JSON is deliberately NOT used on the account door: measured in
68
+ * early testing, a JSON body from a session caller is not decoded at all by
69
+ * the submit handler, so it would silently lose every field.
70
+ */
71
+ form?: Record<string, string | number | boolean | undefined>;
72
+ files?: FileField[];
73
+ /**
74
+ * Same value across every retry of one logical call — that is what makes
75
+ * retrying a POST safe rather than a way to pay twice. Generated once by
76
+ * the caller, never here.
77
+ */
78
+ idempotencyKey?: string;
79
+ timeoutMs?: number;
80
+ /**
81
+ * Return the body instead of throwing when a 200 carries `success: false`.
82
+ *
83
+ * For the two endpoints that report on a JOB: a generation that failed is
84
+ * a successful call whose answer happens to be bad news, and the caller's
85
+ * mapper is what turns it into `{done:true, status:'failed'}`. Everywhere
86
+ * else an envelope error is still an error.
87
+ */
88
+ allowEnvelopeError?: boolean;
89
+ /**
90
+ * Cap the attempts for this call. Defaults to MAX_ATTEMPTS.
91
+ *
92
+ * `generate` sets 1. Retrying it is safe on the SERVER — the
93
+ * Idempotency-Key dedupes — but the whole budget still has to finish
94
+ * inside the client's 60s tool-call timeout, or the client gives up while
95
+ * the charge lands and the agent retries with a new key. One attempt, well
96
+ * inside the window, beats four that outlive the caller.
97
+ */
98
+ maxAttempts?: number;
99
+ }
100
+ /** A fresh idempotency key. Callers make ONE per logical operation. */
101
+ export declare function newIdempotencyKey(): string;
102
+ /**
103
+ * An idempotency key derived from WHAT IS BEING ASKED FOR, inside a window.
104
+ *
105
+ * The server dedupes on (user, origin, m_client_request_id) and this package
106
+ * sent a randomUUID() per invocation, so that tuple could never repeat and
107
+ * the dedupe could never fire: two identical generate calls were two
108
+ * generations and two charges. The mechanism existed on both sides and was
109
+ * joined by a value guaranteed to differ.
110
+ *
111
+ * NO TIME COMPONENT IN THE KEY, deliberately — that was tried and is wrong.
112
+ * The expiry a derived key needs (the same prompt next week must generate
113
+ * again, not replay) is a property of the LOOKUP, and it now lives there:
114
+ * The server's idempotent-lookup takes a max age and the submit
115
+ * handler passes 60 seconds for an MCP caller. Hashing a time bucket into the
116
+ * key instead makes a tumbling grid, so two calls seconds apart miss each
117
+ * other whenever a boundary falls between them — measured on the first live
118
+ * pair tried: 11 seconds apart, two jobs, two charges. A bound on the query
119
+ * is a real "within 60s of each other" at any alignment.
120
+ *
121
+ * Files are identified by path AND by size+mtime, so editing an image and
122
+ * re-running inside the window is a different request rather than a cached
123
+ * answer. A path that cannot be stat'd contributes its raw path and the real
124
+ * complaint arrives later, from the code whose job that is.
125
+ */
126
+ export declare function contentIdempotencyKey(parts: Record<string, unknown>, filePaths?: readonly string[]): string;
127
+ /**
128
+ * Perform one request, with retries.
129
+ *
130
+ * Returns the parsed JSON body on success. Throws VidofyError on anything else,
131
+ * including a 200 whose envelope says `success: false`.
132
+ */
133
+ export declare function request<T = unknown>(cfg: Config, opts: RequestOptions): Promise<T>;