@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/README.md +62 -14
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +21 -3
- package/dist/client.js.map +1 -1
- package/dist/mcp/index.d.ts +595 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +383 -23
- package/dist/mcp/index.js.map +1 -1
- package/dist/params.d.ts.map +1 -1
- package/dist/params.js +28 -1
- package/dist/params.js.map +1 -1
- package/dist/types.d.ts +137 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
`
|
|
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
|
|
328
|
-
errors: ["
|
|
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:
|
|
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
|
},
|