corent-mcp 0.3.0 → 0.4.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/dist/server.js CHANGED
@@ -15,8 +15,26 @@
15
15
  */
16
16
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
17
  import { z } from "zod";
18
+ import { MEDIA_WIDGET_HTML } from "./widget-html.js";
18
19
  export const DEFAULT_API_URL = "https://api.corent.tech";
19
- export const SERVER_VERSION = "0.3.0";
20
+ export const SERVER_VERSION = "0.4.0";
21
+ // --- MCP Apps (SEP-1865): the media tools render an inline widget in hosts
22
+ // that support it (claude.ai web/desktop, others). Hosts without the extension
23
+ // ignore the _meta and fall back to plain text results, so this is additive.
24
+ const WIDGET_URI = "ui://corent/media";
25
+ const WIDGET_MIME = "text/html;profile=mcp-app";
26
+ // Both the canonical nested key and the legacy flat key — the spec tells hosts
27
+ // to check both, and older host builds only know the flat one.
28
+ const WIDGET_TOOL_META = {
29
+ ui: { resourceUri: WIDGET_URI },
30
+ "ui/resourceUri": WIDGET_URI,
31
+ };
32
+ // Generated media lives on Supabase storage; the logo and any brand assets on
33
+ // corent.tech. Everything else stays blocked by the host's CSP.
34
+ const WIDGET_CSP = {
35
+ connectDomains: [],
36
+ resourceDomains: ["https://hzokyylcctfybulkgmcb.supabase.co", "https://corent.tech"],
37
+ };
20
38
  class CorentApiError extends Error {
21
39
  status;
22
40
  detail;
@@ -33,6 +51,8 @@ function errorCode(err) {
33
51
  return "invalid_api_key";
34
52
  if (err.status === 402)
35
53
  return "insufficient_balance";
54
+ if (err.status === 404)
55
+ return "not_found";
36
56
  if (err.status === 422 && detail.includes("content"))
37
57
  return "prompt_not_allowed";
38
58
  if (err.status === 422)
@@ -60,7 +80,7 @@ export function createCorentServer(config = {}) {
60
80
  // can advertise its tools to catalogs/agents without a key configured.
61
81
  if (!apiKey) {
62
82
  throw new CorentApiError(401, {
63
- message: "CORENT_API_KEY is not set. Get a key at https://corent.tech and add it to your MCP client config.",
83
+ 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",
64
84
  });
65
85
  }
66
86
  const res = await fetch(`${apiUrl}${path}`, {
@@ -104,10 +124,38 @@ export function createCorentServer(config = {}) {
104
124
  }
105
125
  };
106
126
  }
107
- const server = new McpServer({ name: "corent", version: SERVER_VERSION });
127
+ // icons/websiteUrl: spec-standard server identity — MCP clients that support
128
+ // it (claude.ai included) show the square Corent mark instead of a letter tile.
129
+ const server = new McpServer({
130
+ name: "corent",
131
+ title: "Corent",
132
+ version: SERVER_VERSION,
133
+ icons: [{ src: "https://corent.tech/brand/intro-mark.png", mimeType: "image/png", sizes: ["512x512"] }],
134
+ websiteUrl: "https://corent.tech",
135
+ });
136
+ server.registerResource("corent-media-widget", WIDGET_URI, {
137
+ title: "Corent media widget",
138
+ description: "Inline card that tracks a generation and plays the result.",
139
+ mimeType: WIDGET_MIME,
140
+ _meta: { ui: { csp: WIDGET_CSP } },
141
+ }, async () => ({
142
+ contents: [
143
+ {
144
+ uri: WIDGET_URI,
145
+ mimeType: WIDGET_MIME,
146
+ text: MEDIA_WIDGET_HTML,
147
+ _meta: { ui: { csp: WIDGET_CSP } },
148
+ },
149
+ ],
150
+ }));
108
151
  // Loose output shapes: every field optional so an additive API change can
109
152
  // never break a client's schema validation.
110
- const mediaMeta = z.object({ model: z.string().optional(), cost_cents: z.number().optional() }).passthrough();
153
+ // model is null while a video job is still processing (no provider chosen
154
+ // yet) -- must be nullable, not just optional, or the SDK's output validator
155
+ // throws on the normal poll-loop response.
156
+ const mediaMeta = z
157
+ .object({ model: z.string().nullable().optional(), cost_cents: z.number().nullable().optional() })
158
+ .passthrough();
111
159
  const imageOutput = {
112
160
  id: z.string().optional(),
113
161
  status: z.string().optional(),
@@ -124,9 +172,14 @@ export function createCorentServer(config = {}) {
124
172
  };
125
173
  server.registerTool("generate_image", {
126
174
  title: "Generate image",
175
+ _meta: WIDGET_TOOL_META,
127
176
  description: "Generate an image from a text prompt. Returns a permanent public URL. Synchronous: typically completes in 2-20 seconds. Costs a few cents, billed to the Corent account. Failed generations are never billed.",
128
177
  inputSchema: {
129
178
  prompt: z.string().describe("What to generate, in plain language"),
179
+ tier: z
180
+ .enum(["air", "lite", "premium", "pro", "max_pro"])
181
+ .optional()
182
+ .describe("Quality tier (preferred): air=cheapest/fastest, lite=everyday, premium=production, pro=advanced, max_pro=maximum fidelity (flagship models). Overrides preference."),
130
183
  preference: z
131
184
  .enum(["fast", "cheap", "quality", "balanced"])
132
185
  .default("balanced")
@@ -139,15 +192,20 @@ export function createCorentServer(config = {}) {
139
192
  },
140
193
  outputSchema: imageOutput,
141
194
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
142
- }, wrap(async ({ prompt, preference, style, aspect_ratio }) => corent("/v1/images/generate", {
195
+ }, wrap(async ({ prompt, tier, preference, style, aspect_ratio }) => corent("/v1/images/generate", {
143
196
  method: "POST",
144
- body: JSON.stringify({ prompt, preference, style, aspect_ratio }),
197
+ body: JSON.stringify({ prompt, tier, preference, style, aspect_ratio }),
145
198
  })));
146
199
  server.registerTool("generate_video", {
147
200
  title: "Generate video",
201
+ _meta: WIDGET_TOOL_META,
148
202
  description: "Start generating a video from a text prompt (optionally animating a source image). Asynchronous: returns a job id immediately; poll get_job until status is 'completed'. Video costs more than images (tens of cents to a few dollars depending on tier). Failed generations are never billed.",
149
203
  inputSchema: {
150
204
  prompt: z.string().describe("What to generate, in plain language"),
205
+ tier: z
206
+ .enum(["air", "lite", "premium", "pro", "max_pro"])
207
+ .optional()
208
+ .describe("Quality tier (preferred): air=cheapest/fastest, lite=everyday, premium=production, pro=advanced, max_pro=maximum fidelity (flagship models). Overrides preference."),
151
209
  preference: z.enum(["fast", "cheap", "quality", "balanced"]).default("balanced"),
152
210
  aspect_ratio: z.enum(["16:9", "9:16", "1:1"]).default("16:9"),
153
211
  duration_s: z.number().int().min(1).max(30).optional().describe("Requested duration in seconds"),
@@ -155,12 +213,13 @@ export function createCorentServer(config = {}) {
155
213
  },
156
214
  outputSchema: jobOutput,
157
215
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
158
- }, wrap(async ({ prompt, preference, aspect_ratio, duration_s, image_url }) => corent("/v1/videos/generate", {
216
+ }, wrap(async ({ prompt, tier, preference, aspect_ratio, duration_s, image_url }) => corent("/v1/videos/generate", {
159
217
  method: "POST",
160
- body: JSON.stringify({ prompt, preference, aspect_ratio, duration_s, image_url }),
218
+ body: JSON.stringify({ prompt, tier, preference, aspect_ratio, duration_s, image_url }),
161
219
  })));
162
220
  server.registerTool("get_job", {
163
221
  title: "Check job status",
222
+ _meta: WIDGET_TOOL_META,
164
223
  description: "Check the status of a generation job (mainly videos). When completed, the response includes the permanent media URL and the cost. Read-only and free.",
165
224
  inputSchema: { job_id: z.string().describe("The job id returned by generate_video or generate_image") },
166
225
  outputSchema: jobOutput,
@@ -199,6 +258,7 @@ export function createCorentServer(config = {}) {
199
258
  }, wrap(async ({ intent }) => corent("/v1/intent", { method: "POST", body: JSON.stringify({ intent }) })));
200
259
  server.registerTool("create", {
201
260
  title: "Create (plan + generate, budget-capped)",
261
+ _meta: WIDGET_TOOL_META,
202
262
  description: "Describe what you want in plain language and Corent plans AND generates it — choosing image vs video, tier, aspect ratio, and duration for you (the 'zero decisions' path). You MUST pass max_cost_cents as a spend ceiling; if the estimated cost exceeds it, nothing is generated and you're told the estimate so you can raise the ceiling. Image results come back with a URL; video results come back as a job id to poll with get_job. Failed generations are never billed.",
203
263
  inputSchema: {
204
264
  intent: z.string().describe("What you want, in natural language"),