corent-mcp 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -4
- package/dist/server.js +357 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,13 +8,19 @@ Give any AI agent the ability to generate images, videos, voice, and text throug
|
|
|
8
8
|
|---|---|
|
|
9
9
|
| `plan` | Describe a request in plain language; Corent returns the plan (image vs video, tier, settings) + cost estimate, without generating |
|
|
10
10
|
| `create` | Describe what you want + a spend ceiling; Corent decides everything and generates it (the "zero decisions" path) |
|
|
11
|
-
| `generate_image` | Text → image, synchronous, returns a permanent URL |
|
|
11
|
+
| `generate_image` | Text → image, synchronous, returns a permanent URL. Takes `reference_image_urls` for character and product consistency |
|
|
12
12
|
| `generate_video` | Text (or image) → video, async job |
|
|
13
13
|
| `generate_speech` | Text → spoken audio, synchronous |
|
|
14
|
-
| `generate_text` | Prompt → text from a frontier language model
|
|
15
|
-
| `
|
|
14
|
+
| `generate_text` | Prompt or full conversation → text from a frontier language model. Supports tool calling and JSON mode |
|
|
15
|
+
| `generate_image_batch` | Up to 50 images in one call, async |
|
|
16
|
+
| `generate_video_batch` | Up to 50 clips in one call, async |
|
|
17
|
+
| `get_batch` | Progress of a submitted batch |
|
|
18
|
+
| `list_models` | The direct-access menu: every model that can be pinned by name (`corent-*`), with quality and live status |
|
|
19
|
+
| `list_tiers` | The tier menu: prices, shapes, video resolutions, and which tiers accept reference images or render sound |
|
|
20
|
+
| `list_voices` | The voices `generate_speech` accepts, with previews and descriptors |
|
|
21
|
+
| `cancel_job` | Stop a running job and release its money hold. Costs nothing |
|
|
16
22
|
| `get_job` | Poll a job until completed |
|
|
17
|
-
| `get_balance` |
|
|
23
|
+
| `get_balance` | Balance, holds, and what a new request can actually spend |
|
|
18
24
|
| `get_status` | Live tier health |
|
|
19
25
|
|
|
20
26
|
The **`plan`** and **`create`** tools are the agent-native path: an agent says
|
|
@@ -31,9 +37,53 @@ can't deliver, the call fails and nothing is billed). Use it only when the user
|
|
|
31
37
|
asked for a particular model by name — otherwise omit it and let Corent route
|
|
32
38
|
to the best fit for the prompt, which is what the tiers are for.
|
|
33
39
|
|
|
40
|
+
Every name in the menu is spelled `corent-*` — `corent-flux-schnell`,
|
|
41
|
+
`corent-seedance-2.0`, `corent-eleven-multilingual-v2`,
|
|
42
|
+
`corent-claude-opus-5` — and that is the name the receipt echoes back. Pass one
|
|
43
|
+
back verbatim.
|
|
44
|
+
|
|
34
45
|
`generate_text` is the language-model lane: it puts every frontier lab on the
|
|
35
46
|
same key and the same bill, so an agent can get a named model's answer or a
|
|
36
47
|
second opinion from another lab without the user holding that lab's account.
|
|
48
|
+
Pass `messages` instead of `prompt` to continue a conversation or feed tool
|
|
49
|
+
results back, and `tools` / `response_format` for function calling and JSON.
|
|
50
|
+
|
|
51
|
+
### Keeping a character or product consistent
|
|
52
|
+
|
|
53
|
+
`generate_image` takes **`reference_image_urls`**: 1 to 4 public https images
|
|
54
|
+
that the prompt is applied as an *edit* of, so the same face, character, or
|
|
55
|
+
product survives into a new scene. Use it whenever the user wants "the same
|
|
56
|
+
person again", a product placed somewhere, or a matching series. Edit-capable
|
|
57
|
+
models sit at premium and up, so pass `tier` `premium` / `pro` / `max_pro`, or
|
|
58
|
+
no tier at all — `air` and `lite` are refused with a message saying so.
|
|
59
|
+
|
|
60
|
+
### What the generate tools can ask for
|
|
61
|
+
|
|
62
|
+
`generate_image` takes a `seed` (so a picture can be re-rendered and tweaked),
|
|
63
|
+
a `negative_prompt`, a `source_image_url` with `strength` to start from an
|
|
64
|
+
existing picture, `transparent` for logos and cutouts, an explicit `width` and
|
|
65
|
+
`height`, `output_format`, and `n` for several versions at once. `n` is n real
|
|
66
|
+
renders at full price, so confirm before spending on more than a couple.
|
|
67
|
+
|
|
68
|
+
`generate_video` takes `audio` for native sound (true routes only to models
|
|
69
|
+
that actually render it, so a silent model can never quietly serve the ask),
|
|
70
|
+
`end_image_url` for the frame to finish on, a `camera` move, `negative_prompt`,
|
|
71
|
+
`seed` and `fps`.
|
|
72
|
+
|
|
73
|
+
`generate_speech` takes `stability`, `similarity`, `style`, `speed` and
|
|
74
|
+
`language`. Call `list_voices` first whenever the user wants a particular
|
|
75
|
+
sounding narrator: `voice_id` cannot be guessed.
|
|
76
|
+
|
|
77
|
+
Anything the chosen model could not honour comes back in
|
|
78
|
+
`meta.unsupported_options`, so an ignored setting never reads as an applied
|
|
79
|
+
one. `enhance_prompt: false` sends the user's wording verbatim.
|
|
80
|
+
|
|
81
|
+
### Spending safety
|
|
82
|
+
|
|
83
|
+
Every money-spending call carries an `Idempotency-Key`, and a 429 or 5xx is
|
|
84
|
+
retried under that same key, so a retry replays the original job instead of
|
|
85
|
+
buying a second one. Batches bill per item: a 30-item image batch costs 30
|
|
86
|
+
generations, so check `get_balance` first.
|
|
37
87
|
|
|
38
88
|
## Setup
|
|
39
89
|
|
package/dist/server.js
CHANGED
|
@@ -17,7 +17,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import { MEDIA_WIDGET_HTML } from "./widget-html.js";
|
|
19
19
|
export const DEFAULT_API_URL = "https://api.corent.tech";
|
|
20
|
-
export const SERVER_VERSION = "0.
|
|
20
|
+
export const SERVER_VERSION = "0.7.0";
|
|
21
|
+
// Transport resilience, matching the two SDKs: a 429 or 5xx is retried with
|
|
22
|
+
// backoff, but ONLY on calls that carry an Idempotency-Key (or are GETs), so a
|
|
23
|
+
// retry can never buy a second generation.
|
|
24
|
+
const MAX_RETRIES = 3;
|
|
25
|
+
const RETRY_BASE_MS = 500;
|
|
26
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
21
27
|
// --- MCP Apps (SEP-1865): the media tools render an inline widget in hosts
|
|
22
28
|
// that support it (claude.ai web/desktop, others). Hosts without the extension
|
|
23
29
|
// ignore the _meta and fall back to plain text results, so this is additive.
|
|
@@ -80,6 +86,16 @@ function errorCode(err) {
|
|
|
80
86
|
export function createCorentServer(config = {}) {
|
|
81
87
|
const apiUrl = config.apiUrl ?? DEFAULT_API_URL;
|
|
82
88
|
const apiKey = config.apiKey;
|
|
89
|
+
/**
|
|
90
|
+
* A fresh Idempotency-Key for one money-spending call. The MCP used to send
|
|
91
|
+
* none at all, so any retry -- the agent's, the host's, or a user clicking
|
|
92
|
+
* again -- bought a second generation at full price (parity audit
|
|
93
|
+
* 2026-08-30). The API keys replays off (account, key), so the same key
|
|
94
|
+
* returns the ORIGINAL job instead of generating twice.
|
|
95
|
+
*/
|
|
96
|
+
function idempotencyKey() {
|
|
97
|
+
return globalThis.crypto?.randomUUID?.() ?? `mcp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
98
|
+
}
|
|
83
99
|
async function corent(path, init) {
|
|
84
100
|
// Key is enforced here (at call time) rather than at startup, so the server
|
|
85
101
|
// can advertise its tools to catalogs/agents without a key configured.
|
|
@@ -88,19 +104,40 @@ export function createCorentServer(config = {}) {
|
|
|
88
104
|
message: "No Corent credentials. On claude.ai or other remote MCP clients, reconnect the Corent connector and approve access (OAuth). For local/stdio use, set CORENT_API_KEY in your MCP client config. Keys: https://corent.tech/dashboard/api-keys",
|
|
89
105
|
});
|
|
90
106
|
}
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
const headers = {
|
|
108
|
+
Authorization: `Bearer ${apiKey}`,
|
|
109
|
+
"Content-Type": "application/json",
|
|
110
|
+
...(init?.headers ?? {}),
|
|
111
|
+
};
|
|
112
|
+
// Retrying is only safe when the call cannot be charged twice: a GET, or a
|
|
113
|
+
// POST carrying an Idempotency-Key (the API replays the original job for a
|
|
114
|
+
// repeated key). A POST without one is sent exactly once, forever.
|
|
115
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
116
|
+
const replaySafe = method === "GET" || Boolean(headers["Idempotency-Key"]);
|
|
117
|
+
const maxAttempts = replaySafe ? MAX_RETRIES : 1;
|
|
118
|
+
let lastErr;
|
|
119
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
120
|
+
if (attempt > 0)
|
|
121
|
+
await sleep(RETRY_BASE_MS * 2 ** (attempt - 1));
|
|
122
|
+
let res;
|
|
123
|
+
try {
|
|
124
|
+
res = await fetch(`${apiUrl}${path}`, { ...init, headers });
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
// Transport failure: on a replay-safe call the same key returns the
|
|
128
|
+
// original job, so retrying cannot double-charge.
|
|
129
|
+
lastErr = err;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const body = await res.json().catch(() => ({}));
|
|
133
|
+
if (res.ok)
|
|
134
|
+
return body;
|
|
135
|
+
const retriable = res.status === 429 || (res.status >= 500 && res.status !== 501);
|
|
136
|
+
lastErr = new CorentApiError(res.status, body.detail ?? body);
|
|
137
|
+
if (!retriable)
|
|
138
|
+
throw lastErr;
|
|
102
139
|
}
|
|
103
|
-
|
|
140
|
+
throw lastErr;
|
|
104
141
|
}
|
|
105
142
|
/** Success: typed structuredContent plus a text fallback for older clients. */
|
|
106
143
|
function ok(data) {
|
|
@@ -159,7 +196,13 @@ export function createCorentServer(config = {}) {
|
|
|
159
196
|
// yet) -- must be nullable, not just optional, or the SDK's output validator
|
|
160
197
|
// throws on the normal poll-loop response.
|
|
161
198
|
const mediaMeta = z
|
|
162
|
-
.object({
|
|
199
|
+
.object({
|
|
200
|
+
model: z.string().nullable().optional(),
|
|
201
|
+
cost_cents: z.number().nullable().optional(),
|
|
202
|
+
seed: z.number().nullable().optional(),
|
|
203
|
+
unsupported_options: z.array(z.string()).nullable().optional(),
|
|
204
|
+
has_audio: z.boolean().nullable().optional(),
|
|
205
|
+
})
|
|
163
206
|
.passthrough();
|
|
164
207
|
const imageOutput = {
|
|
165
208
|
id: z.string().optional(),
|
|
@@ -167,14 +210,34 @@ export function createCorentServer(config = {}) {
|
|
|
167
210
|
images: z.array(z.object({ url: z.string() }).passthrough()).optional(),
|
|
168
211
|
meta: mediaMeta.optional(),
|
|
169
212
|
};
|
|
213
|
+
// meta carries two honesty fields worth surfacing to an agent: the seed
|
|
214
|
+
// that was actually used (so a render can be repeated) and the settings the
|
|
215
|
+
// chosen model could NOT honour (so an ignored knob never reads as applied).
|
|
170
216
|
const jobOutput = {
|
|
171
217
|
id: z.string().optional(),
|
|
172
218
|
status: z.string().optional(),
|
|
173
219
|
images: z.array(z.object({ url: z.string() }).passthrough()).optional(),
|
|
174
220
|
videos: z.array(z.object({ url: z.string() }).passthrough()).optional(),
|
|
221
|
+
// Speech jobs answer with `audio` (routers/jobs.py). It was missing from
|
|
222
|
+
// this schema, so a polled speech job validated as a result with no
|
|
223
|
+
// deliverable in it (parity audit 2026-08-30).
|
|
224
|
+
audio: z.array(z.object({ url: z.string() }).passthrough()).optional(),
|
|
225
|
+
progress_percent: z.number().nullable().optional(),
|
|
175
226
|
meta: mediaMeta.optional(),
|
|
176
227
|
error: z.string().optional(),
|
|
177
228
|
};
|
|
229
|
+
const batchOutput = {
|
|
230
|
+
batch_id: z.string().optional(),
|
|
231
|
+
status: z.string().optional(),
|
|
232
|
+
jobs: z.array(z.object({ id: z.string().optional() }).passthrough()).optional(),
|
|
233
|
+
counts: z.object({}).passthrough().optional(),
|
|
234
|
+
error: z.string().optional(),
|
|
235
|
+
};
|
|
236
|
+
// Shared with generate_image and the image batch items, so the two can never
|
|
237
|
+
// drift apart on which shapes and styles are reachable.
|
|
238
|
+
const IMAGE_ASPECTS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"];
|
|
239
|
+
const IMAGE_STYLES = ["photorealistic", "artistic", "anime", "logo", "text_focused", "cinematic"];
|
|
240
|
+
const TIERS = ["air", "lite", "premium", "pro", "max_pro"];
|
|
178
241
|
server.registerTool("generate_image", {
|
|
179
242
|
title: "Generate image",
|
|
180
243
|
_meta: WIDGET_TOOL_META,
|
|
@@ -182,24 +245,78 @@ export function createCorentServer(config = {}) {
|
|
|
182
245
|
inputSchema: {
|
|
183
246
|
prompt: z.string().describe("What to generate, in plain language"),
|
|
184
247
|
tier: z
|
|
185
|
-
.enum(
|
|
248
|
+
.enum(TIERS)
|
|
186
249
|
.optional()
|
|
187
250
|
.describe("Quality tier: air=cheapest (best value, not the quickest), lite=everyday, premium=production, pro=advanced, max_pro=maximum fidelity (flagship models). Omit for a sensible default."),
|
|
188
251
|
model: z
|
|
189
252
|
.string()
|
|
190
253
|
.optional()
|
|
191
|
-
.describe(
|
|
254
|
+
.describe('Direct model access: pin an exact model by name, e.g. "corent-flux-schnell" (list_models shows the menu; every name there is spelled corent-*). Bypasses Corent\'s routing -- never substituted, flat cost-plus price. Mutually exclusive with tier; prefer tier unless the user named a specific model.'),
|
|
192
255
|
style: z
|
|
193
|
-
.enum(
|
|
256
|
+
.enum(IMAGE_STYLES)
|
|
194
257
|
.optional()
|
|
195
258
|
.describe("Optional content style hint"),
|
|
196
|
-
aspect_ratio: z.enum(
|
|
259
|
+
aspect_ratio: z.enum(IMAGE_ASPECTS).default("1:1"),
|
|
260
|
+
n: z
|
|
261
|
+
.number()
|
|
262
|
+
.int()
|
|
263
|
+
.min(1)
|
|
264
|
+
.max(10)
|
|
265
|
+
.optional()
|
|
266
|
+
.describe("How many versions to render, 1 to 10. Each is a REAL render at the normal price, so n=4 " +
|
|
267
|
+
"costs four images; confirm with the user before spending on more than a couple. They come " +
|
|
268
|
+
"back together and meta.cost_cents is the total."),
|
|
269
|
+
seed: z
|
|
270
|
+
.number()
|
|
271
|
+
.int()
|
|
272
|
+
.min(0)
|
|
273
|
+
.optional()
|
|
274
|
+
.describe("Reproducibility. The same seed with the same prompt and model gives the same image, so use " +
|
|
275
|
+
"it to re-render a picture the user liked and change one thing. The seed that was used " +
|
|
276
|
+
"comes back in meta.seed even when you did not pass one."),
|
|
277
|
+
negative_prompt: z.string().optional().describe('What must NOT appear, e.g. "text, watermark".'),
|
|
278
|
+
source_image_url: z
|
|
279
|
+
.string()
|
|
280
|
+
.url()
|
|
281
|
+
.optional()
|
|
282
|
+
.describe("Image-to-image: start from this picture rather than from scratch. Pair with `strength`. " +
|
|
283
|
+
"Different from reference_image_urls, which pins an identity across scenes."),
|
|
284
|
+
strength: z
|
|
285
|
+
.number()
|
|
286
|
+
.min(0)
|
|
287
|
+
.max(1)
|
|
288
|
+
.optional()
|
|
289
|
+
.describe("How far to travel from source_image_url. 0 keeps it, 1 ignores it."),
|
|
290
|
+
output_format: z.enum(["png", "jpeg", "webp"]).optional().describe("Delivered file type."),
|
|
291
|
+
transparent: z
|
|
292
|
+
.boolean()
|
|
293
|
+
.optional()
|
|
294
|
+
.describe("Transparent background, for logos and product cutouts. Forces png."),
|
|
295
|
+
width: z.number().int().min(256).max(4096).optional().describe("Explicit pixel width; pass with height."),
|
|
296
|
+
height: z.number().int().min(256).max(4096).optional().describe("Explicit pixel height; pass with width."),
|
|
297
|
+
enhance_prompt: z
|
|
298
|
+
.boolean()
|
|
299
|
+
.optional()
|
|
300
|
+
.describe("Corent rewrites the prompt before dispatch to get a better render. Pass false when the user " +
|
|
301
|
+
"has carefully worded their own prompt and wants it sent verbatim."),
|
|
302
|
+
reference_image_urls: z
|
|
303
|
+
.array(z.string().url())
|
|
304
|
+
.min(1)
|
|
305
|
+
.max(4)
|
|
306
|
+
.optional()
|
|
307
|
+
.describe("1 to 4 public https image URLs that keep a CHARACTER, FACE, or PRODUCT consistent across " +
|
|
308
|
+
"generations. The prompt is applied as an EDIT of these references, so use it whenever the " +
|
|
309
|
+
"user wants the same person/object again in a new scene, a product placed somewhere, or a " +
|
|
310
|
+
"series that has to match. Served only by edit-capable models, which sit at premium and up: " +
|
|
311
|
+
"pass tier premium/pro/max_pro (or omit tier), never air or lite. Omit entirely for plain " +
|
|
312
|
+
"text-to-image."),
|
|
197
313
|
},
|
|
198
314
|
outputSchema: imageOutput,
|
|
199
315
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
200
|
-
}, wrap(async (
|
|
316
|
+
}, wrap(async (args) => corent("/v1/images/generate", {
|
|
201
317
|
method: "POST",
|
|
202
|
-
|
|
318
|
+
headers: { "Idempotency-Key": idempotencyKey() },
|
|
319
|
+
body: JSON.stringify(args),
|
|
203
320
|
})));
|
|
204
321
|
server.registerTool("generate_video", {
|
|
205
322
|
title: "Generate video",
|
|
@@ -208,9 +325,13 @@ export function createCorentServer(config = {}) {
|
|
|
208
325
|
inputSchema: {
|
|
209
326
|
prompt: z.string().describe("What to generate, in plain language"),
|
|
210
327
|
tier: z
|
|
211
|
-
.enum(
|
|
328
|
+
.enum(TIERS)
|
|
212
329
|
.optional()
|
|
213
330
|
.describe("Quality tier: air=cheapest (best value, not the quickest), lite=everyday, premium=production, pro=advanced, max_pro=maximum fidelity (flagship models). Omit for a sensible default."),
|
|
331
|
+
style: z
|
|
332
|
+
.enum(IMAGE_STYLES)
|
|
333
|
+
.optional()
|
|
334
|
+
.describe("Optional content style hint, same vocabulary as generate_image"),
|
|
214
335
|
aspect_ratio: z
|
|
215
336
|
.enum(["16:9", "9:16", "1:1"])
|
|
216
337
|
.optional()
|
|
@@ -221,16 +342,41 @@ export function createCorentServer(config = {}) {
|
|
|
221
342
|
.optional()
|
|
222
343
|
.describe("Pixel resolution (default 720p). Tier-capped: pro allows up to 1080p, max_pro up to 4k; above the tier's cap it is clamped down, never rejected. Higher resolutions cost more (1080p ~2.5x, 4k ~5.5x). GET /v1/tiers lists each tier's menu with prices."),
|
|
223
344
|
image_url: z.string().url().optional().describe("If set, animates this image instead of pure text-to-video"),
|
|
345
|
+
audio: z
|
|
346
|
+
.boolean()
|
|
347
|
+
.optional()
|
|
348
|
+
.describe("Ask for native SOUND. Some models render audio and some are silent. true routes only to " +
|
|
349
|
+
"models that actually deliver sound, so a silent model can never quietly serve the request; " +
|
|
350
|
+
"false prefers a silent one; omit to let Corent choose. list_tiers reports supports_audio " +
|
|
351
|
+
"per tier. Use true whenever the user asks for a clip with sound, music, or speech."),
|
|
352
|
+
end_image_url: z
|
|
353
|
+
.string()
|
|
354
|
+
.url()
|
|
355
|
+
.optional()
|
|
356
|
+
.describe("The frame the clip should END on. Together with image_url this is the go-from-A-to-B / morph " +
|
|
357
|
+
"effect; alone it is a target to move toward."),
|
|
358
|
+
camera: z
|
|
359
|
+
.enum([
|
|
360
|
+
"static", "pan_left", "pan_right", "zoom_in", "zoom_out",
|
|
361
|
+
"orbit_left", "orbit_right", "tilt_up", "tilt_down",
|
|
362
|
+
])
|
|
363
|
+
.optional()
|
|
364
|
+
.describe("Camera move."),
|
|
365
|
+
negative_prompt: z.string().optional().describe("What must NOT appear in the clip."),
|
|
366
|
+
seed: z.number().int().min(0).optional().describe("Reproducibility, same contract as generate_image."),
|
|
367
|
+
fps: z.number().int().min(8).max(60).optional().describe("Frames per second, where the model offers a choice."),
|
|
368
|
+
enhance_prompt: z.boolean().optional().describe("false sends the prompt exactly as written."),
|
|
224
369
|
model: z
|
|
225
370
|
.string()
|
|
226
371
|
.optional()
|
|
227
|
-
.describe(
|
|
372
|
+
.describe('Direct model access: pin an exact model by name, e.g. "corent-seedance-2.0" (list_models shows the menu; every name there is spelled corent-*). Bypasses routing -- never substituted; duration and resolution snap to THAT model\'s own menu rather than a tier cap. Mutually exclusive with tier.'),
|
|
228
373
|
},
|
|
229
374
|
outputSchema: jobOutput,
|
|
230
375
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
231
|
-
}, wrap(async (
|
|
376
|
+
}, wrap(async (args) => corent("/v1/videos/generate", {
|
|
232
377
|
method: "POST",
|
|
233
|
-
|
|
378
|
+
headers: { "Idempotency-Key": idempotencyKey() },
|
|
379
|
+
body: JSON.stringify(args),
|
|
234
380
|
})));
|
|
235
381
|
server.registerTool("generate_speech", {
|
|
236
382
|
title: "Generate speech (voice)",
|
|
@@ -245,7 +391,18 @@ export function createCorentServer(config = {}) {
|
|
|
245
391
|
model: z
|
|
246
392
|
.string()
|
|
247
393
|
.optional()
|
|
248
|
-
.describe(
|
|
394
|
+
.describe('Direct model access: pin an exact speech model by name, e.g. "corent-eleven-multilingual-v2" (list_models shows the menu with kind="speech"; every name there is spelled corent-*).'),
|
|
395
|
+
stability: z
|
|
396
|
+
.number()
|
|
397
|
+
.min(0)
|
|
398
|
+
.max(1)
|
|
399
|
+
.optional()
|
|
400
|
+
.describe("0 to 1. Low is more expressive and varies more take to take; high is steadier."),
|
|
401
|
+
similarity: z.number().min(0).max(1).optional().describe("0 to 1. How closely to hold the voice's character."),
|
|
402
|
+
style: z.number().min(0).max(1).optional().describe("0 to 1. Extra expressiveness."),
|
|
403
|
+
speed: z.number().min(0.5).max(2).optional().describe("Playback speed; 1.0 is natural pace."),
|
|
404
|
+
language: z.string().optional().describe('ISO code for the multilingual models, e.g. "en" or "pt-BR".'),
|
|
405
|
+
output_format: z.string().optional().describe('Audio format, e.g. "mp3" or "wav".'),
|
|
249
406
|
},
|
|
250
407
|
outputSchema: {
|
|
251
408
|
id: z.string().optional(),
|
|
@@ -255,24 +412,41 @@ export function createCorentServer(config = {}) {
|
|
|
255
412
|
error: z.string().optional(),
|
|
256
413
|
},
|
|
257
414
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
258
|
-
}, wrap(async (
|
|
415
|
+
}, wrap(async (args) => corent("/v1/audio/speech", {
|
|
259
416
|
method: "POST",
|
|
260
|
-
|
|
417
|
+
headers: { "Idempotency-Key": idempotencyKey() },
|
|
418
|
+
body: JSON.stringify(args),
|
|
261
419
|
})));
|
|
262
420
|
server.registerTool("generate_text", {
|
|
263
421
|
title: "Generate text (language model)",
|
|
264
422
|
description: "Run a prompt through a frontier language model on the user's Corent account. Synchronous: returns the finished text, the token usage and the exact charge. Billed per token (a short answer is a fraction of a cent). Use this when the user wants a SPECIFIC model's answer, a second opinion from another lab, or work billed to their Corent balance -- not for your own reasoning, which costs them nothing. Failed generations are never billed.",
|
|
265
423
|
inputSchema: {
|
|
266
|
-
prompt: z.string().min(1).describe("What to ask, in plain language"),
|
|
424
|
+
prompt: z.string().min(1).optional().describe("What to ask, in plain language. Use this for a single question; use `messages` instead to continue a conversation."),
|
|
267
425
|
system: z.string().optional().describe("Optional system instruction that frames the request"),
|
|
426
|
+
messages: z
|
|
427
|
+
.array(z
|
|
428
|
+
.object({
|
|
429
|
+
role: z.enum(["system", "user", "assistant", "tool"]),
|
|
430
|
+
content: z.unknown().optional(),
|
|
431
|
+
name: z.string().optional(),
|
|
432
|
+
tool_call_id: z.string().optional(),
|
|
433
|
+
tool_calls: z.array(z.object({}).passthrough()).optional(),
|
|
434
|
+
})
|
|
435
|
+
.passthrough())
|
|
436
|
+
.min(1)
|
|
437
|
+
.max(200)
|
|
438
|
+
.optional()
|
|
439
|
+
.describe("Full OpenAI-shaped conversation, for multi-turn work: prior assistant turns, and the tool " +
|
|
440
|
+
"results you are feeding back after a tool call. Takes precedence over prompt/system. " +
|
|
441
|
+
"Without this the model sees only one question and has no memory of the exchange."),
|
|
268
442
|
tier: z
|
|
269
|
-
.enum(
|
|
443
|
+
.enum(TIERS)
|
|
270
444
|
.optional()
|
|
271
445
|
.describe("Quality tier: air=cheapest, lite=everyday, premium=production, pro=advanced, max_pro=frontier flagships. Omit for a sensible default."),
|
|
272
446
|
model: z
|
|
273
447
|
.string()
|
|
274
448
|
.optional()
|
|
275
|
-
.describe(
|
|
449
|
+
.describe('Direct model access: pin an exact text model by name, e.g. "corent-claude-opus-5" (list_models shows the menu with kind="text"; every name there is spelled corent-*). Bypasses routing -- never substituted, flat cost-plus price. Mutually exclusive with tier.'),
|
|
276
450
|
max_tokens: z
|
|
277
451
|
.number()
|
|
278
452
|
.int()
|
|
@@ -281,9 +455,25 @@ export function createCorentServer(config = {}) {
|
|
|
281
455
|
.optional()
|
|
282
456
|
.describe("Cap on the reply length in tokens (default 1024). The balance gate reserves against this, so keep it realistic."),
|
|
283
457
|
temperature: z.number().min(0).max(2).optional().describe("Sampling temperature; omit for the model's default"),
|
|
458
|
+
tools: z
|
|
459
|
+
.array(z.object({}).passthrough())
|
|
460
|
+
.optional()
|
|
461
|
+
.describe("OpenAI-shaped function definitions the model may call. When the model answers with tool " +
|
|
462
|
+
"calls, they come back in `tool_calls` and the reply text is empty: run them, then call " +
|
|
463
|
+
"again with `messages` carrying the assistant turn and your tool results."),
|
|
464
|
+
tool_choice: z
|
|
465
|
+
.unknown()
|
|
466
|
+
.optional()
|
|
467
|
+
.describe('How the model may use tools: "auto", "none", "required", or {type:"function",function:{name}}.'),
|
|
468
|
+
response_format: z
|
|
469
|
+
.object({})
|
|
470
|
+
.passthrough()
|
|
471
|
+
.optional()
|
|
472
|
+
.describe('Structured output, e.g. {"type":"json_object"} for JSON mode, or a json_schema block.'),
|
|
284
473
|
},
|
|
285
474
|
outputSchema: {
|
|
286
475
|
text: z.string().optional(),
|
|
476
|
+
tool_calls: z.array(z.object({}).passthrough()).nullable().optional(),
|
|
287
477
|
model: z.string().optional(),
|
|
288
478
|
finish_reason: z.string().optional(),
|
|
289
479
|
usage: z
|
|
@@ -298,13 +488,21 @@ export function createCorentServer(config = {}) {
|
|
|
298
488
|
error: z.string().optional(),
|
|
299
489
|
},
|
|
300
490
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
301
|
-
}, wrap(async ({ prompt, system, tier, model, max_tokens, temperature }) => {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
491
|
+
}, wrap(async ({ prompt, system, messages: history, tier, model, max_tokens, temperature, tools, tool_choice, response_format }) => {
|
|
492
|
+
if (!history && !prompt) {
|
|
493
|
+
throw new CorentApiError(422, { message: "Pass either `prompt` (single question) or `messages` (conversation)." });
|
|
494
|
+
}
|
|
495
|
+
// An explicit conversation wins: it carries the turns and tool results a
|
|
496
|
+
// single prompt cannot express.
|
|
497
|
+
const messages = history ??
|
|
498
|
+
(system
|
|
499
|
+
? [{ role: "system", content: system }, { role: "user", content: prompt }]
|
|
500
|
+
: [{ role: "user", content: prompt }]);
|
|
305
501
|
// The API's `model` field is required and carries both modes: a pinned
|
|
306
|
-
//
|
|
307
|
-
//
|
|
502
|
+
// model name ("corent-claude-opus-5"), or "corent/text-<tier>" for the
|
|
503
|
+
// brain-routed lane -- note the slash, a separate namespace from the
|
|
504
|
+
// dash-prefixed model names. lite matches the router's own default for an
|
|
505
|
+
// unrecognised tier.
|
|
308
506
|
const r = await corent("/v1/chat/completions", {
|
|
309
507
|
method: "POST",
|
|
310
508
|
body: JSON.stringify({
|
|
@@ -312,11 +510,18 @@ export function createCorentServer(config = {}) {
|
|
|
312
510
|
messages,
|
|
313
511
|
max_tokens,
|
|
314
512
|
temperature,
|
|
513
|
+
tools,
|
|
514
|
+
tool_choice,
|
|
515
|
+
response_format,
|
|
315
516
|
}),
|
|
316
517
|
});
|
|
317
518
|
const choice = (r.choices ?? [{}])[0] ?? {};
|
|
318
519
|
return {
|
|
319
520
|
text: choice.message?.content ?? "",
|
|
521
|
+
// When the model answers with tool calls the content is empty and THIS
|
|
522
|
+
// is the answer. Returning only `text` made a tool-calling reply look
|
|
523
|
+
// like an empty response (parity audit 2026-08-30).
|
|
524
|
+
tool_calls: choice.message?.tool_calls ?? null,
|
|
320
525
|
model: r.model,
|
|
321
526
|
finish_reason: choice.finish_reason,
|
|
322
527
|
usage: r.usage,
|
|
@@ -326,7 +531,7 @@ export function createCorentServer(config = {}) {
|
|
|
326
531
|
}));
|
|
327
532
|
server.registerTool("list_models", {
|
|
328
533
|
title: "List available models",
|
|
329
|
-
description:
|
|
534
|
+
description: 'The direct-access menu: every model that can be pinned by name on generate_image / generate_video / generate_speech / generate_text, with its kind (image, video, speech, text), quality score and live status. Names are spelled corent-* (e.g. "corent-flux-schnell", "corent-claude-opus-5") -- pass one back verbatim as `model`, and that is the name the receipt echoes. Carries NO price -- do not promise the user a per-model rate from this; billing is flat cost-plus and the exact charge is returned on each generation. Read-only and free. Use this only when the user wants a SPECIFIC model -- otherwise omit `model` and let Corent route to the best one for the prompt.',
|
|
330
535
|
inputSchema: {},
|
|
331
536
|
outputSchema: {
|
|
332
537
|
models: z
|
|
@@ -339,6 +544,115 @@ export function createCorentServer(config = {}) {
|
|
|
339
544
|
},
|
|
340
545
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
341
546
|
}, wrap(async () => corent("/v1/models")));
|
|
547
|
+
server.registerTool("list_voices", {
|
|
548
|
+
title: "List voices",
|
|
549
|
+
description: "The voices generate_speech will accept as voice_id, with a display name, a preview clip, and neutral descriptors (gender, age, accent, use case). Read-only and free. CALL THIS BEFORE generate_speech whenever the user wants a particular sounding narrator: the voice_id is required to pick anything other than the default, and there is no other way to discover a legal value.",
|
|
550
|
+
inputSchema: {},
|
|
551
|
+
outputSchema: {
|
|
552
|
+
voices: z
|
|
553
|
+
.array(z.object({ voice_id: z.string() }).passthrough())
|
|
554
|
+
.optional(),
|
|
555
|
+
count: z.number().optional(),
|
|
556
|
+
default_voice_id: z.string().nullable().optional(),
|
|
557
|
+
error: z.string().optional(),
|
|
558
|
+
},
|
|
559
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
560
|
+
}, wrap(async () => corent("/v1/voices")));
|
|
561
|
+
server.registerTool("cancel_job", {
|
|
562
|
+
title: "Cancel a running job",
|
|
563
|
+
description: "Stop a job that has not finished yet and release the money held for it. Charges nothing. Use it as soon as the user says they did not mean to start something, especially for video, which runs for minutes against a reserved balance. If the job already finished, this reports that and changes nothing: a clip that landed a moment earlier stays completed and stays billed for what was actually delivered.",
|
|
564
|
+
inputSchema: { job_id: z.string().describe("The job id to cancel") },
|
|
565
|
+
outputSchema: {
|
|
566
|
+
id: z.string().optional(),
|
|
567
|
+
status: z.string().optional(),
|
|
568
|
+
cancelled: z.boolean().optional(),
|
|
569
|
+
detail: z.string().optional(),
|
|
570
|
+
error: z.string().optional(),
|
|
571
|
+
},
|
|
572
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
573
|
+
}, wrap(async ({ job_id }) => corent(`/v1/jobs/${job_id}/cancel`, { method: "POST" })));
|
|
574
|
+
server.registerTool("list_tiers", {
|
|
575
|
+
title: "List quality tiers and prices",
|
|
576
|
+
description: "The live tier menu for image and video: each tier's price estimate, the shapes it can render, video durations and per-resolution pricing, and whether it accepts reference images (image) or a source image (video). Read-only and free. Several other tools tell you to check this before quoting a price or offering a resolution -- this is that endpoint. Tier-level only: it never names a model.",
|
|
577
|
+
inputSchema: {},
|
|
578
|
+
outputSchema: {
|
|
579
|
+
tiers: z
|
|
580
|
+
.array(z
|
|
581
|
+
.object({
|
|
582
|
+
name: z.string().optional(),
|
|
583
|
+
estimated_cost_cents: z.number().nullable().optional(),
|
|
584
|
+
capabilities: z.object({}).passthrough().optional(),
|
|
585
|
+
})
|
|
586
|
+
.passthrough())
|
|
587
|
+
.optional(),
|
|
588
|
+
pricing_note: z.string().optional(),
|
|
589
|
+
error: z.string().optional(),
|
|
590
|
+
},
|
|
591
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
592
|
+
}, wrap(async () => corent("/v1/tiers")));
|
|
593
|
+
// --- Batch tools: one call, up to 50 renders. The single-item tools are the
|
|
594
|
+
// right default; these exist for the "make me 30 variations" ask, where 30
|
|
595
|
+
// separate tool calls would be slower and far more expensive in context. ---
|
|
596
|
+
const imageBatchItem = z
|
|
597
|
+
.object({
|
|
598
|
+
prompt: z.string(),
|
|
599
|
+
tier: z.enum(TIERS).optional(),
|
|
600
|
+
style: z.enum(IMAGE_STYLES).optional(),
|
|
601
|
+
aspect_ratio: z.enum(IMAGE_ASPECTS).optional(),
|
|
602
|
+
})
|
|
603
|
+
.describe("One image in the batch. Batch items are tier-routed: to pin an exact model, use generate_image.");
|
|
604
|
+
server.registerTool("generate_image_batch", {
|
|
605
|
+
title: "Generate many images (batch)",
|
|
606
|
+
description: "Submit up to 50 image generations in ONE call. Asynchronous: returns a batch_id and one job id per item immediately; poll get_batch for progress and get_job for each finished image. Every item bills at the normal rate, so a 30-item batch costs 30 generations -- check get_balance first. Items are tier-routed: pin an exact model with generate_image instead. Reference images are single-item only.",
|
|
607
|
+
inputSchema: {
|
|
608
|
+
items: z.array(imageBatchItem).min(1).max(50).describe("1 to 50 image requests"),
|
|
609
|
+
},
|
|
610
|
+
outputSchema: batchOutput,
|
|
611
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
612
|
+
}, wrap(async ({ items }) => corent("/v1/images/generate/batch", {
|
|
613
|
+
method: "POST",
|
|
614
|
+
headers: { "Idempotency-Key": idempotencyKey() },
|
|
615
|
+
body: JSON.stringify({ items }),
|
|
616
|
+
})));
|
|
617
|
+
server.registerTool("generate_video_batch", {
|
|
618
|
+
title: "Generate many videos (batch)",
|
|
619
|
+
description: "Submit up to 50 video generations in ONE call. Asynchronous: returns a batch_id and one job id per item; poll get_batch, then get_job for each clip. Video is the expensive lane -- a 10-item batch can run to several dollars, so confirm with the user and check get_balance first. Items are tier-routed: pin an exact model with generate_video instead.",
|
|
620
|
+
inputSchema: {
|
|
621
|
+
items: z
|
|
622
|
+
.array(z.object({
|
|
623
|
+
prompt: z.string(),
|
|
624
|
+
tier: z.enum(TIERS).optional(),
|
|
625
|
+
style: z.enum(IMAGE_STYLES).optional(),
|
|
626
|
+
aspect_ratio: z.enum(["16:9", "9:16", "1:1"]).optional(),
|
|
627
|
+
duration_s: z.number().int().min(1).max(30).optional(),
|
|
628
|
+
resolution: z.enum(["720p", "1080p", "4k"]).optional(),
|
|
629
|
+
}))
|
|
630
|
+
.min(1)
|
|
631
|
+
.max(50)
|
|
632
|
+
.describe("1 to 50 video requests"),
|
|
633
|
+
},
|
|
634
|
+
outputSchema: batchOutput,
|
|
635
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
636
|
+
}, wrap(async ({ items }) => corent("/v1/videos/generate/batch", {
|
|
637
|
+
method: "POST",
|
|
638
|
+
headers: { "Idempotency-Key": idempotencyKey() },
|
|
639
|
+
body: JSON.stringify({ items }),
|
|
640
|
+
})));
|
|
641
|
+
server.registerTool("get_batch", {
|
|
642
|
+
title: "Check batch progress",
|
|
643
|
+
description: "Progress of a batch submitted with generate_image_batch or generate_video_batch: how many items are completed, failed, and still pending, plus each item's job id. Read-only and free. Fetch a finished item's media with get_job.",
|
|
644
|
+
inputSchema: { batch_id: z.string().describe("The batch_id returned by a batch tool") },
|
|
645
|
+
outputSchema: {
|
|
646
|
+
batch_id: z.string().optional(),
|
|
647
|
+
total: z.number().optional(),
|
|
648
|
+
completed: z.number().optional(),
|
|
649
|
+
failed: z.number().optional(),
|
|
650
|
+
pending: z.number().optional(),
|
|
651
|
+
jobs: z.array(z.object({ id: z.string(), status: z.string() }).passthrough()).optional(),
|
|
652
|
+
error: z.string().optional(),
|
|
653
|
+
},
|
|
654
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
655
|
+
}, wrap(async ({ batch_id }) => corent(`/v1/batches/${batch_id}`)));
|
|
342
656
|
server.registerTool("get_job", {
|
|
343
657
|
title: "Check job status",
|
|
344
658
|
_meta: WIDGET_TOOL_META,
|
|
@@ -349,9 +663,13 @@ export function createCorentServer(config = {}) {
|
|
|
349
663
|
}, wrap(async ({ job_id }) => corent(`/v1/jobs/${job_id}`)));
|
|
350
664
|
server.registerTool("get_balance", {
|
|
351
665
|
title: "Check account balance",
|
|
352
|
-
description: "Get the Corent account's
|
|
666
|
+
description: "Get the Corent account's balance in cents. balance_cents is the total; held_cents is reserved by generations still running; available_cents is what a new request can actually spend -- check that one before an expensive video, since it is what a 402 is decided against. Read-only and free.",
|
|
353
667
|
inputSchema: {},
|
|
354
|
-
outputSchema: {
|
|
668
|
+
outputSchema: {
|
|
669
|
+
balance_cents: z.number().optional(),
|
|
670
|
+
held_cents: z.number().optional(),
|
|
671
|
+
available_cents: z.number().optional(),
|
|
672
|
+
},
|
|
355
673
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
356
674
|
}, wrap(async () => corent("/v1/account/balance")));
|
|
357
675
|
server.registerTool("get_status", {
|
package/package.json
CHANGED