@meterapp/car-image-sdk 1.1.0 → 1.3.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/dist/mcp/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * entry point of the SDK has no runtime dependencies.
11
11
  */
12
12
  import * as z from "zod";
13
- import { CATALOG, COLORS, CREDITS_PER_DOLLAR, CREDITS_PER_IMAGE, DEFAULT_BASE_URL, FORMATS, FREE_CREDITS, MAX_DIMENSION, MIN_YEAR, REFERRAL_SOURCES, REQUEST_KINDS, REQUEST_SORTS, REQUEST_STATUSES, SIZES, VIEWS, } from "../types.js";
13
+ import { CATALOG, COLORS, CREDITS_PER_DOLLAR, CREDITS_PER_IMAGE, DEFAULT_BASE_URL, FITS, FORMATS, FREE_CREDITS, MAX_DIMENSION, MAX_PADDING_PERCENT, MIN_YEAR, REFERRAL_SOURCES, REQUEST_KINDS, REQUEST_SORTS, REQUEST_STATUSES, SIZES, VIEWS, } from "../types.js";
14
14
  import { SDK_VERSION } from "../version.js";
15
15
  /**
16
16
  * Catalog size as agents read it in the instructions and tool descriptions.
@@ -26,7 +26,49 @@ export const MCP_SERVER_INFO = {
26
26
  version: SDK_VERSION,
27
27
  websiteUrl: DEFAULT_BASE_URL,
28
28
  };
29
- export const MCP_INSTRUCTIONS = `Car Image API by Meter: studio-quality, transparent-background renders of any vehicle in the open @meterapp/vehicle-db catalog (${CATALOG_MAKES} makes, ${CATALOG_MODELS} models, model years ${CATALOG_YEARS}). Six camera views, fifteen preset colors, PNG/WebP/JPG up to 1024 px.
29
+ /**
30
+ * Which tools a server exposes. `core` is what an agent needs to find, render
31
+ * and embed a car; the request board and the two "about you" tools live in
32
+ * `all`, for hosts that opt in (`?toolset=all` on the remote URL,
33
+ * `car-image mcp --toolset all` locally). Fewer, sharper tools cost less
34
+ * context and make the right one easier to pick.
35
+ */
36
+ export const MCP_TOOLSETS = {
37
+ core: [
38
+ "get_car_image",
39
+ "create_car_image_urls",
40
+ "search_vehicles",
41
+ "resolve_vehicle",
42
+ "list_image_options",
43
+ "get_account",
44
+ "rate_image",
45
+ "describe_api",
46
+ ],
47
+ all: [
48
+ "get_car_image",
49
+ "create_car_image_urls",
50
+ "search_vehicles",
51
+ "resolve_vehicle",
52
+ "list_image_options",
53
+ "get_account",
54
+ "rate_image",
55
+ "describe_api",
56
+ "list_requests",
57
+ "request_vehicle",
58
+ "request_feature",
59
+ "get_request",
60
+ "upvote_request",
61
+ "comment_on_request",
62
+ "share_building",
63
+ "share_referral",
64
+ ],
65
+ };
66
+ export const DEFAULT_MCP_TOOLSET = "core";
67
+ export const MCP_TOOLSET_NAMES = Object.keys(MCP_TOOLSETS);
68
+ export function isMcpToolset(value) {
69
+ return typeof value === "string" && MCP_TOOLSET_NAMES.includes(value);
70
+ }
71
+ const INSTRUCTIONS_COMMON = `Car Image API by Meter: studio-quality, transparent-background renders of any vehicle in the open @meterapp/vehicle-db catalog (${CATALOG_MAKES} makes, ${CATALOG_MODELS} models, model years ${CATALOG_YEARS}). Six camera views, fifteen preset colors, PNG/WebP/JPG up to 1024 px, any width and height (fit contain|cover|inside), transparent or a solid background color.
30
72
 
31
73
  Costs: every delivered image costs exactly ${CREDITS_PER_IMAGE} credit ($1 = ${CREDITS_PER_DOLLAR.toLocaleString("en-US")} credits; ${FREE_CREDITS} free credits per account), cold or CDN hit. create_car_image_urls charges 1 credit per URL when the URL is created; redemptions are free. search_vehicles, resolve_vehicle, list_image_options, get_account, rate_image and describe_api are free.
32
74
 
@@ -36,9 +78,15 @@ Security: never place the API key in URLs, HTML, logs, screenshots or client-sid
36
78
 
37
79
  Money: on an insufficient-credits error (HTTP 402) stop and ask the human to buy credits at ${DEFAULT_BASE_URL}/billing or with \`car-image billing\`; never purchase credits autonomously. On a rate limit (HTTP 429) wait the number of seconds in retry_after before trying again.
38
80
 
39
- Quality: after a human judges a render, call rate_image (good/bad or 1-5) so future renders improve.
40
-
41
- Requests: when search_vehicles or resolve_vehicle finds no match for a vehicle the human needs, offer request_vehicle so it gets added (an existing open request for the same car is upvoted instead of duplicated); ideas for the API go through request_feature, and list_requests shows what others have asked for.`;
81
+ Quality: after a human judges a render, call rate_image (good/bad or 1-5) so future renders improve.`;
82
+ const INSTRUCTIONS_REQUESTS = `Requests: when search_vehicles or resolve_vehicle finds no match for a vehicle the human needs, offer request_vehicle so it gets added (an existing open request for the same car is upvoted instead of duplicated); ideas for the API go through request_feature, and list_requests shows what others have asked for.`;
83
+ const INSTRUCTIONS_CORE_ONLY = `Missing vehicles: when search_vehicles or resolve_vehicle finds no match for a vehicle the human needs, do not substitute another car; point the human to ${DEFAULT_BASE_URL}/requests to ask for it (or connect with toolset=all for the request board tools).`;
84
+ /** The server instructions for a toolset; `all` includes the request board paragraph. */
85
+ export function mcpInstructions(toolset = DEFAULT_MCP_TOOLSET) {
86
+ return `${INSTRUCTIONS_COMMON}\n\n${toolset === "all" ? INSTRUCTIONS_REQUESTS : INSTRUCTIONS_CORE_ONLY}`;
87
+ }
88
+ /** Instructions for the full toolset; kept for callers that predate toolsets. */
89
+ export const MCP_INSTRUCTIONS = mcpInstructions("all");
42
90
  const yearSchema = z.coerce
43
91
  .number()
44
92
  .int()
@@ -58,8 +106,25 @@ export const imageInputSchema = z.object({
58
106
  .enum(["thumb", "small", "medium", "large"])
59
107
  .optional()
60
108
  .describe(`Square size preset: thumb=${SIZES.thumb}, small=${SIZES.small}, medium=${SIZES.medium}, large=${SIZES.large} px. Omit for ${MAX_DIMENSION} px.`),
61
- width: z.number().int().min(1).max(MAX_DIMENSION).optional().describe(`Explicit width in px (1-${MAX_DIMENSION}); overrides size.`),
109
+ width: z.number().int().min(1).max(MAX_DIMENSION).optional().describe(`Explicit width in px (1-${MAX_DIMENSION}); overrides size. With height too, the output is exactly width x height.`),
62
110
  height: z.number().int().min(1).max(MAX_DIMENSION).optional().describe(`Explicit height in px (1-${MAX_DIMENSION}).`),
111
+ fit: z
112
+ .enum(FITS)
113
+ .optional()
114
+ .describe("With width and height: contain (default) keeps the whole car and pads the rest; cover fills the box and crops; inside may return a smaller image."),
115
+ background: z
116
+ .string()
117
+ .max(20)
118
+ .optional()
119
+ .describe('Solid background: a hex color such as "#f4f4f4", "white" or "black". Omit for transparent (jpg is white).'),
120
+ trim: z.boolean().optional().describe("Crop to the car's own bounds before sizing, so it fills the box instead of floating in the square source frame."),
121
+ padding: z
122
+ .number()
123
+ .int()
124
+ .min(0)
125
+ .max(MAX_PADDING_PERCENT)
126
+ .optional()
127
+ .describe(`Margin around a trimmed car, percent of its longer side (0-${MAX_PADDING_PERCENT}). Only with trim.`),
63
128
  format: z
64
129
  .enum(FORMATS)
65
130
  .default("png")
@@ -87,6 +152,17 @@ export const MCP_TOOL_SCHEMAS = {
87
152
  .max(1_000_000)
88
153
  .optional()
89
154
  .describe("Maximum redemptions per URL; 0 (default) means unlimited until expiry."),
155
+ renew: z
156
+ .boolean()
157
+ .optional()
158
+ .describe("Auto-renew past the TTL: the first load in each further window of ttl_seconds bills one more credit and renews the URL, until renew_days. Use for email, CMS pages and documents that outlive a TTL; pair with ttl_seconds 604800 so the cost ceiling is 1 credit per image per opened week. Requires max_uses 0."),
159
+ renew_days: z
160
+ .number()
161
+ .int()
162
+ .min(1)
163
+ .max(365)
164
+ .optional()
165
+ .describe("How long a renewable URL keeps renewing, 1 to 365 days from creation (default 365). Only with renew: true."),
90
166
  }),
91
167
  search_vehicles: z.object({
92
168
  query: z
@@ -167,19 +243,287 @@ export const MCP_TOOL_SCHEMAS = {
167
243
  detail: z.string().max(500).optional().describe('Optional specifics, e.g. the article, the person, or the search query.'),
168
244
  }),
169
245
  };
246
+ /* --------------------------------------------------------------- outputs ---
247
+ *
248
+ * A tool may advertise an `outputSchema` (MCP 2025-06-18 and later). Hosts show
249
+ * it next to the input schema, validate `structuredContent` against it, and can
250
+ * hand the model typed data instead of re-parsing the text block. The server SDK
251
+ * enforces the promise from the other side: a tool that declares an output
252
+ * schema and then returns no structured content, or content that does not match,
253
+ * fails the call. Two rules follow from that, and both are load-bearing:
254
+ *
255
+ * - Every object here is a `looseObject`. These payloads are the REST API's,
256
+ * which gains fields as the product does (`usage_30d`, `year_ranges`,
257
+ * `limits`). A strict object advertises `additionalProperties: false`, which
258
+ * would make a validating client reject a response for being newer than it.
259
+ * - Only what BOTH servers guarantee is required. Where the remote server and
260
+ * the stdio one disagreed, they were made to agree (`toVehicleMatch`,
261
+ * `toImageOptions`, `etag` and `credits_usd` in the stdio handlers) rather
262
+ * than the schema being weakened to cover both.
263
+ *
264
+ * `describe_api` deliberately has none: its result *is* the reference prose, and
265
+ * restating it as structured content would double the tokens of every call.
266
+ */
267
+ /** The vehicle echoed back with an image: exactly what would render it again. */
268
+ const vehicleDescriptorSchema = z.looseObject({
269
+ make: z.string(),
270
+ model: z.string(),
271
+ year: z.number().int(),
272
+ view: z.enum(VIEWS),
273
+ color: z.enum(COLORS),
274
+ width: z.number().int().optional(),
275
+ height: z.number().int().optional(),
276
+ fit: z.enum(FITS).optional(),
277
+ background: z.string().optional(),
278
+ trim: z.boolean().optional(),
279
+ padding: z.number().int().optional(),
280
+ format: z.enum([...FORMATS, "auto"]),
281
+ });
282
+ /** `free_credits` is absent from the account endpoint's copy; the packs are not. */
283
+ const pricingSchema = z.looseObject({
284
+ credits_per_image: z.number(),
285
+ credits_per_dollar: z.number(),
286
+ free_credits: z.number().optional(),
287
+ packs: z
288
+ .array(z.looseObject({ credits: z.number().int(), cents: z.number().int(), label: z.string().optional() }))
289
+ .optional(),
290
+ });
291
+ /** One catalog hit, in the single spelling both servers emit (see {@link toVehicleMatch}). */
292
+ const vehicleMatchSchema = z.looseObject({
293
+ kind: z.enum(["make", "model"]),
294
+ make: z.string(),
295
+ make_slug: z.string(),
296
+ model: z.string().nullable(),
297
+ model_slug: z.string().nullable(),
298
+ vehicle_type: z.string().nullable(),
299
+ years: z.array(z.number().int()),
300
+ match_kind: z.string().nullable(),
301
+ });
302
+ /** Shared by the list/create/vote results and, minus the count, by the detail. */
303
+ const requestFields = {
304
+ id: z.string(),
305
+ kind: z.enum(REQUEST_KINDS),
306
+ title: z.string(),
307
+ body: z.string().nullable(),
308
+ vehicle: z.looseObject({ make: z.string(), model: z.string(), year: z.number().int().nullable() }).nullable(),
309
+ status: z.enum(REQUEST_STATUSES),
310
+ votes: z.number().int(),
311
+ author: z.string(),
312
+ url: z.string(),
313
+ created_at: z.string(),
314
+ updated_at: z.string(),
315
+ viewer: z.looseObject({ voted: z.boolean() }).optional(),
316
+ };
317
+ const requestCommentSchema = z.looseObject({
318
+ id: z.string(),
319
+ request_id: z.string(),
320
+ body: z.string(),
321
+ author: z.string(),
322
+ created_at: z.string(),
323
+ });
324
+ const requestRecordSchema = z.looseObject({ ...requestFields, comments: z.number().int() });
325
+ /** On the detail the thread takes the `comments` name, so the count moves aside. */
326
+ const requestDetailSchema = z.looseObject({
327
+ ...requestFields,
328
+ comment_count: z.number().int(),
329
+ comments: z.array(requestCommentSchema),
330
+ });
331
+ const requestListResult = z.looseObject({
332
+ data: z.array(requestRecordSchema),
333
+ next_cursor: z.string().nullable(),
334
+ request_id: z.string(),
335
+ });
336
+ const requestResult = z.looseObject({ data: requestRecordSchema, request_id: z.string() });
337
+ const createdRequestResult = z.looseObject({
338
+ data: requestRecordSchema,
339
+ /** True when an open vehicle request already existed and took the vote instead. */
340
+ deduplicated: z.boolean(),
341
+ request_id: z.string(),
342
+ });
343
+ export const MCP_TOOL_OUTPUT_SCHEMAS = {
344
+ get_car_image: z.looseObject({
345
+ credits_charged: z.number().nullable(),
346
+ credits_remaining: z.number().nullable(),
347
+ source: z.enum(["cache", "generated"]).nullable(),
348
+ width: z.number().int().nullable(),
349
+ height: z.number().int().nullable(),
350
+ content_type: z.string(),
351
+ etag: z.string().nullable(),
352
+ request_id: z.string().nullable(),
353
+ vehicle: vehicleDescriptorSchema,
354
+ }),
355
+ create_car_image_urls: z.looseObject({
356
+ data: z.array(z.looseObject({
357
+ id: z.string(),
358
+ /** Signed delivery URL; needs no key and is safe to put in HTML. */
359
+ url: z.string(),
360
+ expires_at: z.string(),
361
+ /** 0 means unlimited redemptions until it expires. */
362
+ max_uses: z.number().int(),
363
+ /** Auto-renewing URLs keep serving, one credit per opened window, until this instant. */
364
+ renews_until: z.string().nullable().optional(),
365
+ vehicle: vehicleDescriptorSchema,
366
+ })),
367
+ billing: z.looseObject({
368
+ charged_on: z.literal("creation"),
369
+ credits_charged: z.number(),
370
+ credits_remaining: z.number(),
371
+ renewal: z.looseObject({ window_seconds: z.number(), credits_per_window: z.number(), until: z.string() }).nullable().optional(),
372
+ }),
373
+ request_id: z.string(),
374
+ }),
375
+ search_vehicles: z.looseObject({
376
+ data: z.looseObject({ query: z.string(), results: z.array(vehicleMatchSchema) }),
377
+ }),
378
+ resolve_vehicle: z.looseObject({
379
+ data: z.looseObject({
380
+ /** Ready to pass straight to get_car_image. */
381
+ params: z.looseObject({
382
+ make: z.string(),
383
+ model: z.string(),
384
+ year: z.number().int(),
385
+ view: z.enum(VIEWS),
386
+ color: z.enum(COLORS),
387
+ }),
388
+ display: z.looseObject({ make_name: z.string(), model_name: z.string() }).optional(),
389
+ /** Other catalog vehicles that fit the phrase; ask when confidence is low. */
390
+ candidates: z.array(z.looseObject({
391
+ make_name: z.string().optional(),
392
+ make_slug: z.string().optional(),
393
+ model_name: z.string().optional(),
394
+ model_slug: z.string().optional(),
395
+ years: z.array(z.number().int()).optional(),
396
+ match_kind: z.string().optional(),
397
+ })),
398
+ /** high | medium | low; confirm with the human before spending on low. */
399
+ confidence: z.enum(["high", "medium", "low"]),
400
+ image_path: z.string().optional(),
401
+ /** The attributes read out of the phrase; nulls are what it did not say. */
402
+ extracted: z.looseObject({}).optional(),
403
+ }),
404
+ /** The equivalent CLI invocation, for a human who wants to repeat it. */
405
+ cli: z.string(),
406
+ }),
407
+ list_image_options: z.looseObject({
408
+ data: z.looseObject({
409
+ views: z.array(z.looseObject({
410
+ id: z.enum(VIEWS),
411
+ label: z.string(),
412
+ yaw_degrees: z.number(),
413
+ description: z.string(),
414
+ aliases: z.array(z.string()),
415
+ })),
416
+ colors: z.array(z.looseObject({ name: z.enum(COLORS), hex: z.string() })),
417
+ sizes: z.looseObject({ presets: z.record(z.string(), z.number().int()), max: z.number().int() }),
418
+ formats: z.array(z.enum(FORMATS)),
419
+ pricing: pricingSchema,
420
+ catalog: z.looseObject({
421
+ makes: z.number().int(),
422
+ models: z.number().int(),
423
+ /** [first, last] model year in the catalog. */
424
+ years: z.tuple([z.number().int(), z.number().int()]),
425
+ sources: z.array(z.string()),
426
+ }),
427
+ }),
428
+ }),
429
+ get_account: z.looseObject({
430
+ data: z.looseObject({
431
+ credits: z.number(),
432
+ credits_usd: z.number(),
433
+ auto_reload: z.boolean(),
434
+ has_payment_method: z.boolean(),
435
+ pricing: pricingSchema,
436
+ key: z.looseObject({ id: z.string().optional(), scopes: z.array(z.string()) }).optional(),
437
+ }),
438
+ links: z.record(z.string(), z.string()).optional(),
439
+ request_id: z.string().optional(),
440
+ }),
441
+ rate_image: z.looseObject({
442
+ data: z.looseObject({
443
+ asset_id: z.string().nullable().optional(),
444
+ /** True when the render was marked for re-rendering. */
445
+ flagged: z.boolean(),
446
+ rating: z.number().int().optional(),
447
+ verdict: z.enum(["good", "bad"]).optional(),
448
+ }),
449
+ request_id: z.string(),
450
+ }),
451
+ list_requests: requestListResult,
452
+ request_vehicle: createdRequestResult,
453
+ request_feature: createdRequestResult,
454
+ get_request: z.looseObject({ data: requestDetailSchema, request_id: z.string() }),
455
+ upvote_request: requestResult,
456
+ comment_on_request: z.looseObject({ data: requestCommentSchema, request_id: z.string() }),
457
+ share_building: z.looseObject({
458
+ data: z.looseObject({ building: z.string(), updated_at: z.string() }),
459
+ request_id: z.string(),
460
+ }),
461
+ share_referral: z.looseObject({
462
+ data: z.looseObject({ source: z.enum(REFERRAL_SOURCES), detail: z.string().nullable(), updated_at: z.string() }),
463
+ request_id: z.string(),
464
+ }),
465
+ };
466
+ /**
467
+ * Normalizes a catalog hit into the one shape `search_vehicles` promises.
468
+ *
469
+ * The remote server reads the catalog library (camelCase) and the stdio one
470
+ * reads `GET /api/v1/vehicles?q=` (snake_case, and it omits `model_name`
471
+ * entirely on a make hit rather than sending null). Agents pass these straight
472
+ * to get_car_image, so both are mapped onto `make`/`model` with explicit nulls.
473
+ */
474
+ export function toVehicleMatch(source) {
475
+ return {
476
+ kind: source.kind,
477
+ make: source.makeName ?? source.make_name ?? "",
478
+ make_slug: source.makeSlug ?? source.make_slug ?? "",
479
+ model: source.modelName ?? source.model_name ?? null,
480
+ model_slug: source.modelSlug ?? source.model_slug ?? null,
481
+ vehicle_type: source.vehicleType ?? source.vehicle_type ?? null,
482
+ years: source.years ?? [],
483
+ match_kind: source.matchKind ?? source.match_kind ?? null,
484
+ };
485
+ }
486
+ function idsOf(values) {
487
+ if (!Array.isArray(values))
488
+ return [];
489
+ return values.map((value) => (typeof value === "string" ? value : String(value?.id ?? ""))).filter(Boolean);
490
+ }
491
+ /**
492
+ * Normalizes the options payload into the one shape `list_image_options`
493
+ * promises. `GET /api/v1/images/options` describes formats and catalog sources
494
+ * as objects and the year span as `{from, to}`; agents want bare ids and a
495
+ * `[from, to]` pair, so both that shape and an already-flat one are accepted.
496
+ * Anything else in the payload (`limits`, `sizes.source`) rides along untouched.
497
+ */
498
+ export function toImageOptions(data) {
499
+ const source = (data ?? {});
500
+ const catalog = (source.catalog ?? {});
501
+ const years = catalog.years;
502
+ return {
503
+ ...source,
504
+ formats: idsOf(source.formats),
505
+ catalog: {
506
+ ...catalog,
507
+ years: Array.isArray(years) ? [years[0], years[1]] : [years?.from ?? MIN_YEAR, years?.to ?? MIN_YEAR],
508
+ sources: idsOf(catalog.sources),
509
+ },
510
+ };
511
+ }
170
512
  export const MCP_TOOLS = {
171
513
  get_car_image: {
172
514
  name: "get_car_image",
173
515
  title: "Get car image",
174
- description: `Render or fetch a studio-quality, transparent-background image of a specific vehicle (make, model, year) from one of six camera angles in one of fifteen preset colors. Returns the image inline (PNG/WebP/JPG, up to ${MAX_DIMENSION} px) plus usage metadata (credits_charged, credits_remaining, source cache|generated, request_id). COSTS ${CREDITS_PER_IMAGE} CREDIT per call, cached or generated. Use resolve_vehicle first when the request is free text; use create_car_image_urls instead when the image must be embedded in a web page or document.`,
516
+ description: `Render or fetch a studio-quality, transparent-background image of a specific vehicle (make, model, year) from one of six camera angles in one of fifteen preset colors, at any width and height up to ${MAX_DIMENSION} px (fit contain|cover|inside), transparent or on a solid background, optionally trimmed to the car. Returns the image inline (PNG/WebP/JPG) plus usage metadata (credits_charged, credits_remaining, source cache|generated, request_id). COSTS ${CREDITS_PER_IMAGE} CREDIT per call, cached or generated. Use resolve_vehicle first when the request is free text; use create_car_image_urls instead when the image must be embedded in a web page or document.`,
175
517
  inputSchema: MCP_TOOL_SCHEMAS.get_car_image,
518
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.get_car_image,
176
519
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
177
520
  },
178
521
  create_car_image_urls: {
179
522
  name: "create_car_image_urls",
180
523
  title: "Create signed car image URLs",
181
- description: "Create 1-50 signed, key-free delivery URLs that browsers, emails or documents can load directly (<img src>). Each URL costs 1 credit at creation; redemptions within the TTL are free and publicly cacheable. Returns id, url, expires_at, max_uses and the normalized vehicle for each image. Prefer this over get_car_image whenever the output is HTML, Markdown, a document or a website.",
524
+ description: "Create 1-50 signed, key-free delivery URLs that browsers, emails or documents can load directly (<img src>). Each URL costs 1 credit at creation; redemptions within the TTL are free and publicly cacheable. Each image takes the same sizing options as get_car_image (width, height, fit, background, trim). For email, CMS pages or documents that outlive a TTL, set renew: true with ttl_seconds 604800: the URL keeps serving past expiry at one more credit per opened week, for up to renew_days. Returns id, url, expires_at, max_uses, renews_until and the normalized vehicle for each image. Prefer this over get_car_image whenever the output is HTML, Markdown, a document or a website.",
182
525
  inputSchema: MCP_TOOL_SCHEMAS.create_car_image_urls,
526
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.create_car_image_urls,
183
527
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
184
528
  },
185
529
  search_vehicles: {
@@ -187,13 +531,15 @@ export const MCP_TOOLS = {
187
531
  title: "Search the vehicle catalog",
188
532
  description: `Free. Fuzzy-search the open vehicle catalog (${CATALOG_MAKES} makes, ${CATALOG_MODELS} models, ${CATALOG_YEARS}) by make and/or model with typo tolerance. Returns canonical make/model names, available years and match kind, ready to pass to get_car_image. Use it to confirm a vehicle exists or to list a make's models before rendering.`,
189
533
  inputSchema: MCP_TOOL_SCHEMAS.search_vehicles,
534
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.search_vehicles,
190
535
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
191
536
  },
192
537
  resolve_vehicle: {
193
538
  name: "resolve_vehicle",
194
539
  title: "Resolve free text to image parameters",
195
- description: 'Free. Turn a natural-language request such as "red 2024 porsche 911 side view" into exact get_car_image parameters (make, model, year, view, color) plus alternative candidates and a 0-1 confidence. Call this before rendering when the user did not spell out the parameters; ask the user to choose when confidence is low or several candidates fit.',
540
+ description: 'Free. Turn a natural-language request such as "red 2024 porsche 911 side view" into exact get_car_image parameters (make, model, year, view, color) plus alternative candidates and a confidence of high, medium or low. Call this before rendering when the user did not spell out the parameters; ask the user to choose when confidence is low or several candidates fit.',
196
541
  inputSchema: MCP_TOOL_SCHEMAS.resolve_vehicle,
542
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.resolve_vehicle,
197
543
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
198
544
  },
199
545
  list_image_options: {
@@ -201,6 +547,7 @@ export const MCP_TOOLS = {
201
547
  title: "List image options",
202
548
  description: "Free. List the supported camera views (with yaw angles and aliases), the fifteen preset colors with hex swatches, size presets, output formats, current pricing and catalog coverage. Use it when you need to validate or explain what can be requested.",
203
549
  inputSchema: MCP_TOOL_SCHEMAS.list_image_options,
550
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.list_image_options,
204
551
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
205
552
  },
206
553
  get_account: {
@@ -208,6 +555,7 @@ export const MCP_TOOLS = {
208
555
  title: "Get account and credits",
209
556
  description: "Free. Show the caller's remaining credits (and the dollar equivalent), auto-reload status, whether a payment method is on file, recent usage and the key's scopes. Check this before large batches; if credits are insufficient, ask a human to top up rather than buying credits yourself.",
210
557
  inputSchema: MCP_TOOL_SCHEMAS.get_account,
558
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.get_account,
211
559
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
212
560
  },
213
561
  rate_image: {
@@ -215,6 +563,7 @@ export const MCP_TOOLS = {
215
563
  title: "Rate a delivered image",
216
564
  description: "Free. Record quality feedback on a delivered image, either by the request_id returned with it or by vehicle (make, model, year, view, color). Provide verdict good|bad or rating 1-5 and an optional reason. Feedback drives re-renders and prompt improvements.",
217
565
  inputSchema: MCP_TOOL_SCHEMAS.rate_image,
566
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.rate_image,
218
567
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
219
568
  },
220
569
  describe_api: {
@@ -229,6 +578,7 @@ export const MCP_TOOLS = {
229
578
  title: "List vehicle and feature requests",
230
579
  description: "Free, public. List what people have asked for: vehicles missing from the catalog and feature ideas, with vote and comment counts, status (open, planned, in_progress, done, declined) and a public page URL. Sort by votes or recency, filter by kind or status. Use it before request_vehicle or request_feature to find an existing request to upvote.",
231
580
  inputSchema: MCP_TOOL_SCHEMAS.list_requests,
581
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.list_requests,
232
582
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
233
583
  },
234
584
  request_vehicle: {
@@ -236,6 +586,7 @@ export const MCP_TOOLS = {
236
586
  title: "Request a missing vehicle",
237
587
  description: "Free. Ask for a vehicle that search_vehicles/resolve_vehicle cannot find. If an open request for the same make, model and year already exists it is upvoted instead and returned with deduplicated: true, so calling this is always safe. The requester's vote counts, and the human gets a short thank-you email with the request page.",
238
588
  inputSchema: MCP_TOOL_SCHEMAS.request_vehicle,
589
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.request_vehicle,
239
590
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
240
591
  },
241
592
  request_feature: {
@@ -243,6 +594,7 @@ export const MCP_TOOLS = {
243
594
  title: "Request a feature",
244
595
  description: "Free. File a feature request for the API (new views, formats, endpoints, anything). Not deduplicated: check list_requests first and upvote an existing one when it matches. Returns the request with its public page URL; the human gets a short thank-you email.",
245
596
  inputSchema: MCP_TOOL_SCHEMAS.request_feature,
597
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.request_feature,
246
598
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
247
599
  },
248
600
  get_request: {
@@ -250,6 +602,7 @@ export const MCP_TOOLS = {
250
602
  title: "Get a request",
251
603
  description: "Free, public. One request with its full comment thread, vote count and status.",
252
604
  inputSchema: MCP_TOOL_SCHEMAS.get_request,
605
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.get_request,
253
606
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
254
607
  },
255
608
  upvote_request: {
@@ -257,6 +610,7 @@ export const MCP_TOOLS = {
257
610
  title: "Upvote a request",
258
611
  description: "Free. Add the caller's vote to a request (idempotent; voting twice changes nothing). Returns the updated request.",
259
612
  inputSchema: MCP_TOOL_SCHEMAS.upvote_request,
613
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.upvote_request,
260
614
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
261
615
  },
262
616
  comment_on_request: {
@@ -264,6 +618,7 @@ export const MCP_TOOLS = {
264
618
  title: "Comment on a request",
265
619
  description: "Free. Add a public comment to a request, e.g. the body style or use case that matters. Comments show the author's first name only.",
266
620
  inputSchema: MCP_TOOL_SCHEMAS.comment_on_request,
621
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.comment_on_request,
267
622
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
268
623
  },
269
624
  share_building: {
@@ -271,6 +626,7 @@ export const MCP_TOOLS = {
271
626
  title: "Tell us what you are building",
272
627
  description: "Free. Record what the human is building with the API. Private to the Car Image team (never shown publicly); re-sharing overwrites the previous answer. Only call it with the human's own words and consent.",
273
628
  inputSchema: MCP_TOOL_SCHEMAS.share_building,
629
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.share_building,
274
630
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
275
631
  },
276
632
  share_referral: {
@@ -278,6 +634,7 @@ export const MCP_TOOLS = {
278
634
  title: "Tell us how you found us",
279
635
  description: "Free. Record where the human first heard about Car Image API (one of a fixed list of sources, plus optional detail). Private to the Car Image team; re-sharing overwrites. Only call it with the human's consent.",
280
636
  inputSchema: MCP_TOOL_SCHEMAS.share_referral,
637
+ outputSchema: MCP_TOOL_OUTPUT_SCHEMAS.share_referral,
281
638
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
282
639
  },
283
640
  };
@@ -297,10 +654,13 @@ export const API_REFERENCE = [
297
654
  `view (${VIEWS.join("|")}; default front-3-4)`,
298
655
  `color (${COLORS.join("|")}; default silver)`,
299
656
  "size (thumb=256|small=512|medium=768|large=1024)",
300
- `w, h (1-${MAX_DIMENSION} px)`,
301
- `format (${FORMATS.join("|")}; default png)`,
657
+ `w, h (1-${MAX_DIMENSION} px; both -> the output is exactly w x h)`,
658
+ `fit (${FITS.join("|")}; default contain; only matters with both w and h)`,
659
+ "background (transparent default; white|black|hex such as f4f4f4; jpg defaults to white)",
660
+ `trim (1 crops to the car's alpha bounds before sizing), padding (0-${MAX_PADDING_PERCENT} percent, only with trim)`,
661
+ `format (${FORMATS.join("|")}|auto; default png; auto negotiates webp/png from Accept)`,
302
662
  ],
303
- returns: "image bytes; headers X-Credits-Charged, X-Credits-Remaining, X-Image-Source (cache|generated), X-Image-Width, X-Image-Height, ETag, X-Request-Id. With Accept: application/json: {data:{url,expires_at,vehicle,width,height,format}, billing:{credits_charged,credits_remaining}, request_id}",
663
+ returns: "image bytes; headers X-Credits-Charged, X-Credits-Remaining, X-Image-Source (cache|generated), X-Image-Width, X-Image-Height, ETag, X-Request-Id (Vary: Accept with format=auto). With Accept: application/json: {data:{url,expires_at,vehicle,width,height,format}, billing:{credits_charged,credits_remaining}, request_id}",
304
664
  errors: ["400 invalid params", "401 missing/invalid key", "402 insufficient credits (balance, required_credits)", "404 vehicle not in catalog", "429 rate limited (Retry-After)", "502 render failed (refunded)"],
305
665
  notes: "Do not purchase credits automatically on 402; ask the human. Respect Retry-After on 429.",
306
666
  sdk: "client.getImage({ make, model, year, view, color, size, format }) / client.getImageUrl(...)",
@@ -312,20 +672,20 @@ export const API_REFERENCE = [
312
672
  summary: "Create 1-50 signed delivery URLs for browsers (no key needed to load them).",
313
673
  auth: "bearer",
314
674
  credits: "1 per URL, charged at creation; redemptions are free",
315
- params: ["images: [{make, model, year, view?, color?, width?, height?, format?}] (or a single image object)", "ttl_seconds (60-604800; default 3600)", "max_uses (0 = unlimited; default 0)"],
316
- returns: "201 {data:[{id,url,expires_at,max_uses,vehicle}], billing:{charged_on:'creation',credits_charged,credits_remaining}, request_id}",
317
- errors: ["400 invalid body or batch > 50", "401", "402", "404 vehicle not in catalog", "429"],
318
- sdk: "client.createImageUrls([{ make, model, year }], { ttlSeconds, maxUses })",
319
- cli: "car-image url --make BMW --model M3 --year 2022 --ttl 86400",
675
+ params: ["images: [{make, model, year, view?, color?, size?, width?, height?, fit?, background?, trim?, padding?, format?}] (or a single image object)", "ttl_seconds (60-604800; default 3600)", "max_uses (0 = unlimited; default 0)", "renew (auto-renew past the TTL at 1 credit per opened window; requires max_uses 0)", "renew_days (1-365; default 365)", "header Idempotency-Key (optional; a retry with the same key and body replays the first response instead of billing again)"],
676
+ returns: "201 {data:[{id,url,expires_at,max_uses,renews_until,vehicle}], billing:{charged_on:'creation',credits_charged,credits_remaining,credits_per_url,renewal?}, request_id}; a replay carries Idempotent-Replayed: true",
677
+ errors: ["400 invalid body or batch > 50", "401", "402", "404 vehicle not in catalog", "409 same Idempotency-Key still in flight", "422 same Idempotency-Key with a different body", "429"],
678
+ sdk: "client.createImageUrls([{ make, model, year }], { ttlSeconds, maxUses, renew, renewDays })",
679
+ cli: "car-image url --make BMW --model M3 --year 2022 --ttl 86400 [--renew] [--idempotency-key <key>]",
320
680
  },
321
681
  {
322
682
  method: "GET",
323
683
  path: "/api/v1/delivery/{token}",
324
- summary: "Redeem a signed URL; public within its TTL and max_uses.",
684
+ summary: "Redeem a signed URL; public within its TTL and max_uses. An auto-renewing URL loaded after its TTL bills 1 credit for the new window on first load.",
325
685
  auth: "public",
326
- credits: "free",
327
- returns: "image bytes with public Cache-Control for the remaining lifetime",
328
- errors: ["404/410 expired, exhausted or invalid token"],
686
+ credits: "free within a paid window; 1 per new window of a renewable URL",
687
+ returns: "image bytes with public Cache-Control for the remaining window",
688
+ errors: ["403 expired, revoked or invalid token", "402 renewal unpaid (resumes once credits land)", "410 exhausted"],
329
689
  },
330
690
  {
331
691
  method: "GET",
@@ -333,7 +693,7 @@ export const API_REFERENCE = [
333
693
  summary: "Views, colors, sizes, formats, pricing and catalog coverage.",
334
694
  auth: "public",
335
695
  credits: "free",
336
- returns: "{data:{views:[{id,label,yaw_degrees,description,aliases}], colors:[{name,hex}], sizes:{presets,max}, formats, pricing:{credits_per_image,credits_per_dollar,free_credits}, catalog:{makes,models,years,sources}}}",
696
+ returns: "{data:{views:[{id,label,yaw_degrees,description,aliases}], colors:[{name,hex}], sizes:{presets,max,source}, fits, default_fit, backgrounds, trim, formats, pricing:{credits_per_image,credits_per_dollar,free_credits}, catalog:{makes,models,years,sources}}}",
337
697
  sdk: "client.options()",
338
698
  cli: "car-image options",
339
699
  },
@@ -344,7 +704,7 @@ export const API_REFERENCE = [
344
704
  auth: "bearer",
345
705
  credits: "free",
346
706
  params: ['query: "red 2024 porsche 911 side view"'],
347
- returns: "{data:{params:{make,model,year,view,color}, candidates:[...], confidence:0-1}}",
707
+ returns: "{data:{params:{make,model,year,view,color}, display:{make_name,model_name}, candidates:[...], confidence:high|medium|low, image_path}}",
348
708
  sdk: "client.resolve(query)",
349
709
  cli: "car-image resolve red 2024 porsche 911 side view",
350
710
  },