@vprop/mcp 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/dist/index.js +271 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -215,7 +215,15 @@ Any client that follows the [MCP specification](https://modelcontextprotocol.io)
|
|
|
215
215
|
### Webhooks
|
|
216
216
|
| Tool | Description |
|
|
217
217
|
|------|-------------|
|
|
218
|
-
| `register_webhook` | Register an HTTPS URL to receive project events |
|
|
218
|
+
| `register_webhook` | Register an HTTPS URL to receive project events (replaces any existing registration) |
|
|
219
|
+
| `get_webhook` | Get the current webhook registration for this API key |
|
|
220
|
+
| `delete_webhook` | Deactivate a webhook by its ID |
|
|
221
|
+
| `rotate_webhook_secret` | Roll the HMAC signing secret for a webhook |
|
|
222
|
+
|
|
223
|
+
### Usage
|
|
224
|
+
| Tool | Description |
|
|
225
|
+
|------|-------------|
|
|
226
|
+
| `get_usage` | Get usage counts and credit balance for this API key |
|
|
219
227
|
|
|
220
228
|
## Example prompts
|
|
221
229
|
|
|
@@ -234,14 +242,19 @@ When calling `create_project`, you can customize:
|
|
|
234
242
|
|
|
235
243
|
| Option | Values | Default |
|
|
236
244
|
|--------|--------|---------|
|
|
237
|
-
| `theme` | `modern` `classic` `bold` `elegant` | `modern` |
|
|
245
|
+
| `theme` | `modern` `classic` `bold` `elegant` `luxe` `basic` `rosewood` `keystone` `pixel` `editorial` `sketch` | `modern` |
|
|
246
|
+
| `background` | `black` `white` | theme default |
|
|
238
247
|
| `resolution` | `portrait` (9:16) `landscape` (16:9) | `portrait` |
|
|
239
248
|
| `caption` | `none` `keyword` `description` | `keyword` |
|
|
240
249
|
| `language` | `en` `es` `ko` `zh-Hans` | `en` |
|
|
241
250
|
| `voice_id` | ID from `list_voices` | agent default |
|
|
242
251
|
| `bgm_id` | ID from `list_bgms` | theme default |
|
|
252
|
+
| `background_music` | `true` `false` | `true` |
|
|
253
|
+
| `narration_highlights` | free text (≤2000 chars) | — |
|
|
254
|
+
| `word_highlight` | `true` `false` | `false` |
|
|
243
255
|
| `use_agent_avatar` | `true` `false` | `true` |
|
|
244
256
|
| `use_lipsync` | `true` `false` | `false` |
|
|
257
|
+
| `display_info` | object — per-field booleans for agent card | platform defaults |
|
|
245
258
|
|
|
246
259
|
## Notes
|
|
247
260
|
|
package/dist/index.js
CHANGED
|
@@ -53,14 +53,15 @@ async function uploadBuffer(uploadUrl, data, contentType) {
|
|
|
53
53
|
// ────────────────────────────────────────────────────────────────────────────
|
|
54
54
|
const server = new McpServer({
|
|
55
55
|
name: "vprop",
|
|
56
|
-
version: "1.
|
|
56
|
+
version: "1.1.0",
|
|
57
57
|
description: "vProp Public API — create real-estate video projects from listings and agents. " +
|
|
58
58
|
"Typical flow: create_listing → upload_listing_image (10+ photos) → create_agent → " +
|
|
59
59
|
"upload_agent_photo → create_project → poll get_project_status → get_project (for video_url).",
|
|
60
60
|
});
|
|
61
61
|
// ── Voices ───────────────────────────────────────────────────────────────────
|
|
62
62
|
server.registerTool("list_voices", {
|
|
63
|
-
description: "List all available narration voices. Use the returned `id` as `voice_id` when creating agents or projects."
|
|
63
|
+
description: "List all available narration voices. Use the returned `id` as `voice_id` when creating agents or projects. " +
|
|
64
|
+
"Filter on `verified_languages` to find a voice that matches your project's `language` option.",
|
|
64
65
|
}, async () => toText(await vpropFetch("/voices")));
|
|
65
66
|
// ── BGMs ─────────────────────────────────────────────────────────────────────
|
|
66
67
|
server.registerTool("list_bgms", {
|
|
@@ -68,23 +69,62 @@ server.registerTool("list_bgms", {
|
|
|
68
69
|
}, async () => toText(await vpropFetch("/bgms")));
|
|
69
70
|
// ── Listings ─────────────────────────────────────────────────────────────────
|
|
70
71
|
server.registerTool("create_listing", {
|
|
71
|
-
description: "Create a new property listing. Returns a listing ID (`lst_…`) used for image uploads and project creation."
|
|
72
|
+
description: "Create a new property listing. Returns a listing ID (`lst_…`) used for image uploads and project creation. " +
|
|
73
|
+
"The server enriches the listing in parallel: an address-based property search (fills propertyType, lotSize, lat/long, etc.) " +
|
|
74
|
+
"and an LLM pass over the optional `options` free-form text. Merge precedence: explicit body fields > options-parsed > search.",
|
|
72
75
|
inputSchema: {
|
|
73
|
-
address: z.string().describe("Full US
|
|
76
|
+
address: z.string().describe("Full US street address, e.g. '123 Main St' (required)"),
|
|
77
|
+
city: z.string().optional().describe("City name (overrides address search and LLM parse)"),
|
|
78
|
+
state: z.string().optional().describe("State abbreviation, e.g. 'CA' (overrides address search and LLM parse)"),
|
|
79
|
+
zip_code: z.string().optional().describe("ZIP code (overrides address search and LLM parse)"),
|
|
74
80
|
bedrooms: z.number().optional().describe("Number of bedrooms"),
|
|
75
81
|
bathrooms: z.number().optional().describe("Number of bathrooms"),
|
|
76
|
-
square_feet: z.number().optional().describe("
|
|
82
|
+
square_feet: z.number().optional().describe("Living area in square feet"),
|
|
77
83
|
price: z.number().optional().describe("Listing price in USD"),
|
|
78
84
|
year_built: z.number().optional().describe("Year the property was built"),
|
|
79
|
-
|
|
85
|
+
property_status: z
|
|
86
|
+
.enum(["coming soon", "active", "under contract", "pending", "rent", "sold"])
|
|
87
|
+
.optional()
|
|
88
|
+
.describe("Listing status (default: active)"),
|
|
89
|
+
property_type: z
|
|
90
|
+
.enum([
|
|
91
|
+
"single family",
|
|
92
|
+
"townhome",
|
|
93
|
+
"condo",
|
|
94
|
+
"vacant land",
|
|
95
|
+
"multi family",
|
|
96
|
+
"manufactured",
|
|
97
|
+
"co-op",
|
|
98
|
+
"office",
|
|
99
|
+
"retail",
|
|
100
|
+
"industrial",
|
|
101
|
+
"hospitality",
|
|
102
|
+
"mixed-use",
|
|
103
|
+
"restaurant",
|
|
104
|
+
"other",
|
|
105
|
+
])
|
|
106
|
+
.optional()
|
|
107
|
+
.describe("Property type (default: other)"),
|
|
108
|
+
listing_type: z
|
|
109
|
+
.enum(["sale", "rental"])
|
|
110
|
+
.optional()
|
|
111
|
+
.describe("Sale vs rental (default: sale)"),
|
|
112
|
+
metadata: z.record(z.string(), z.unknown()).optional().describe("Arbitrary key-value metadata (≤ 4KB)"),
|
|
113
|
+
options: z
|
|
114
|
+
.string()
|
|
115
|
+
.max(8192)
|
|
116
|
+
.optional()
|
|
117
|
+
.describe("Free-form natural language description of the listing " +
|
|
118
|
+
"(e.g. 'renovated kitchen, corner lot, HOA $250/month, listed for sale'). " +
|
|
119
|
+
"The server runs an LLM pass to extract additional fields. Explicit body fields always win."),
|
|
80
120
|
},
|
|
81
|
-
}, async ({ address, bedrooms, bathrooms, square_feet, price, year_built, metadata }) => {
|
|
121
|
+
}, async ({ address, city, state, zip_code, bedrooms, bathrooms, square_feet, price, year_built, property_status, property_type, listing_type, metadata, options }) => {
|
|
82
122
|
const attributes = bedrooms !== undefined || bathrooms !== undefined || square_feet !== undefined || price !== undefined || year_built !== undefined
|
|
83
123
|
? { bedrooms, bathrooms, square_feet, price, year_built }
|
|
84
124
|
: undefined;
|
|
85
125
|
return toText(await vpropFetch("/listings", {
|
|
86
126
|
method: "POST",
|
|
87
|
-
body: JSON.stringify({ address, attributes, metadata }),
|
|
127
|
+
body: JSON.stringify({ address, city, state, zip_code, attributes, property_status, property_type, listing_type, metadata, options }),
|
|
88
128
|
}));
|
|
89
129
|
});
|
|
90
130
|
server.registerTool("get_listing", {
|
|
@@ -109,24 +149,52 @@ server.registerTool("list_listings", {
|
|
|
109
149
|
return toText(await vpropFetch(`/listings${qs}`));
|
|
110
150
|
});
|
|
111
151
|
server.registerTool("update_listing", {
|
|
112
|
-
description: "Update an existing listing's
|
|
152
|
+
description: "Update an existing listing's fields. Only the fields you send are written; the rest keep their current value. " +
|
|
153
|
+
"The address-based property search is NOT re-run on update (updates are partner-driven corrections); " +
|
|
154
|
+
"the LLM `options` parse does run if `options` is provided.",
|
|
113
155
|
inputSchema: {
|
|
114
156
|
id: z.string().describe("Listing ID to update"),
|
|
115
|
-
address: z.string().optional()
|
|
157
|
+
address: z.string().optional(),
|
|
158
|
+
city: z.string().optional(),
|
|
159
|
+
state: z.string().optional(),
|
|
160
|
+
zip_code: z.string().optional(),
|
|
116
161
|
bedrooms: z.number().optional(),
|
|
117
162
|
bathrooms: z.number().optional(),
|
|
118
163
|
square_feet: z.number().optional(),
|
|
119
164
|
price: z.number().optional(),
|
|
120
165
|
year_built: z.number().optional(),
|
|
166
|
+
property_status: z
|
|
167
|
+
.enum(["coming soon", "active", "under contract", "pending", "rent", "sold"])
|
|
168
|
+
.optional(),
|
|
169
|
+
property_type: z
|
|
170
|
+
.enum([
|
|
171
|
+
"single family",
|
|
172
|
+
"townhome",
|
|
173
|
+
"condo",
|
|
174
|
+
"vacant land",
|
|
175
|
+
"multi family",
|
|
176
|
+
"manufactured",
|
|
177
|
+
"co-op",
|
|
178
|
+
"office",
|
|
179
|
+
"retail",
|
|
180
|
+
"industrial",
|
|
181
|
+
"hospitality",
|
|
182
|
+
"mixed-use",
|
|
183
|
+
"restaurant",
|
|
184
|
+
"other",
|
|
185
|
+
])
|
|
186
|
+
.optional(),
|
|
187
|
+
listing_type: z.enum(["sale", "rental"]).optional(),
|
|
121
188
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
189
|
+
options: z.string().max(8192).optional().describe("Free-form text for LLM field extraction"),
|
|
122
190
|
},
|
|
123
|
-
}, async ({ id, address, bedrooms, bathrooms, square_feet, price, year_built, metadata }) => {
|
|
191
|
+
}, async ({ id, address, city, state, zip_code, bedrooms, bathrooms, square_feet, price, year_built, property_status, property_type, listing_type, metadata, options }) => {
|
|
124
192
|
const attributes = bedrooms !== undefined || bathrooms !== undefined || square_feet !== undefined || price !== undefined || year_built !== undefined
|
|
125
193
|
? { bedrooms, bathrooms, square_feet, price, year_built }
|
|
126
194
|
: undefined;
|
|
127
195
|
return toText(await vpropFetch(`/listings/${id}`, {
|
|
128
196
|
method: "PATCH",
|
|
129
|
-
body: JSON.stringify({ address, attributes, metadata }),
|
|
197
|
+
body: JSON.stringify({ address, city, state, zip_code, attributes, property_status, property_type, listing_type, metadata, options }),
|
|
130
198
|
}));
|
|
131
199
|
});
|
|
132
200
|
server.registerTool("delete_listing", {
|
|
@@ -209,17 +277,51 @@ server.registerTool("delete_listing_image", {
|
|
|
209
277
|
// ── Agents ───────────────────────────────────────────────────────────────────
|
|
210
278
|
server.registerTool("create_agent", {
|
|
211
279
|
description: "Create a realtor agent profile used in video projects. Returns an agent ID (`agt_…`). " +
|
|
212
|
-
"After creation, call upload_agent_photo to
|
|
280
|
+
"After creation, call upload_agent_photo to upload a local headshot file, or pass profile_photo_url if the photo is already hosted.",
|
|
213
281
|
inputSchema: {
|
|
214
282
|
name: z.string().describe("Agent's full name (required)"),
|
|
215
283
|
email: z.string().email().optional().describe("Agent's email address"),
|
|
216
|
-
phone: z.string().optional().describe("Agent's phone number"),
|
|
217
|
-
|
|
218
|
-
|
|
284
|
+
phone: z.string().optional().describe("Agent's direct phone number"),
|
|
285
|
+
profile_photo_url: z
|
|
286
|
+
.string()
|
|
287
|
+
.url()
|
|
288
|
+
.optional()
|
|
289
|
+
.describe("URL of an already-hosted headshot photo. " +
|
|
290
|
+
"Use this when you have a CDN/web URL. " +
|
|
291
|
+
"To upload a local file instead, call upload_agent_photo after creating the agent."),
|
|
292
|
+
license_number: z.string().optional().describe("Real-estate license / DRE / RECO number"),
|
|
293
|
+
website: z.string().optional().describe("Agent or business website URL"),
|
|
294
|
+
area: z.string().optional().describe("Service area or neighborhoods covered"),
|
|
295
|
+
team_name: z.string().optional().describe("Team name within the brokerage"),
|
|
296
|
+
company_name: z.string().optional().describe("Brokerage / company name"),
|
|
297
|
+
brokerage_phone: z
|
|
298
|
+
.string()
|
|
299
|
+
.optional()
|
|
300
|
+
.describe("Brokerage office phone (distinct from the agent's direct phone)"),
|
|
301
|
+
bio: z.string().optional().describe("Short agent biography"),
|
|
302
|
+
logo_url: z
|
|
303
|
+
.string()
|
|
304
|
+
.url()
|
|
305
|
+
.optional()
|
|
306
|
+
.describe("Agent logo image URL (shown on the agent card in the video)"),
|
|
307
|
+
team_logo_url: z.string().url().optional().describe("Team logo image URL"),
|
|
308
|
+
company_logo_url: z.string().url().optional().describe("Company / brokerage logo image URL"),
|
|
309
|
+
voice_id: z
|
|
310
|
+
.string()
|
|
311
|
+
.optional()
|
|
312
|
+
.describe("Default voice ID from list_voices. Used as the fallback when a project omits voice_id."),
|
|
313
|
+
metadata: z.record(z.string(), z.unknown()).optional().describe("Arbitrary key-value metadata (≤ 4KB)"),
|
|
314
|
+
options: z
|
|
315
|
+
.string()
|
|
316
|
+
.max(4096)
|
|
317
|
+
.optional()
|
|
318
|
+
.describe("Free-form natural language description of the agent " +
|
|
319
|
+
"(e.g. 'Team name is The Smith Group at Compass. DRE #01234567. Serves the SF Bay Area.'). " +
|
|
320
|
+
"The server runs an LLM pass to extract additional profile fields. Explicit body fields always win."),
|
|
219
321
|
},
|
|
220
|
-
}, async ({ name, email, phone, voice_id, metadata }) => toText(await vpropFetch("/agents", {
|
|
322
|
+
}, async ({ name, email, phone, profile_photo_url, license_number, website, area, team_name, company_name, brokerage_phone, bio, logo_url, team_logo_url, company_logo_url, voice_id, metadata, options }) => toText(await vpropFetch("/agents", {
|
|
221
323
|
method: "POST",
|
|
222
|
-
body: JSON.stringify({ name, email, phone, voice_id, metadata }),
|
|
324
|
+
body: JSON.stringify({ name, email, phone, profile_photo_url, license_number, website, area, team_name, company_name, brokerage_phone, bio, logo_url, team_logo_url, company_logo_url, voice_id, metadata, options }),
|
|
223
325
|
})));
|
|
224
326
|
server.registerTool("get_agent", {
|
|
225
327
|
description: "Get full details of an agent by their ID.",
|
|
@@ -243,18 +345,34 @@ server.registerTool("list_agents", {
|
|
|
243
345
|
return toText(await vpropFetch(`/agents${qs}`));
|
|
244
346
|
});
|
|
245
347
|
server.registerTool("update_agent", {
|
|
246
|
-
description: "Update an existing agent's information.",
|
|
348
|
+
description: "Update an existing agent's information. Only the fields you send are written; the rest keep their current value.",
|
|
247
349
|
inputSchema: {
|
|
248
350
|
id: z.string().describe("Agent ID to update"),
|
|
249
351
|
name: z.string().optional(),
|
|
250
352
|
email: z.string().email().optional(),
|
|
251
353
|
phone: z.string().optional(),
|
|
354
|
+
profile_photo_url: z
|
|
355
|
+
.string()
|
|
356
|
+
.url()
|
|
357
|
+
.optional()
|
|
358
|
+
.describe("URL of an already-hosted headshot photo. To upload a local file instead, call upload_agent_photo."),
|
|
359
|
+
license_number: z.string().optional(),
|
|
360
|
+
website: z.string().optional(),
|
|
361
|
+
area: z.string().optional(),
|
|
362
|
+
team_name: z.string().optional(),
|
|
363
|
+
company_name: z.string().optional(),
|
|
364
|
+
brokerage_phone: z.string().optional(),
|
|
365
|
+
bio: z.string().optional(),
|
|
366
|
+
logo_url: z.string().url().optional(),
|
|
367
|
+
team_logo_url: z.string().url().optional(),
|
|
368
|
+
company_logo_url: z.string().url().optional(),
|
|
252
369
|
voice_id: z.string().optional().describe("New default voice ID"),
|
|
253
370
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
371
|
+
options: z.string().max(4096).optional().describe("Free-form text for LLM field extraction"),
|
|
254
372
|
},
|
|
255
|
-
}, async ({ id, name, email, phone, voice_id, metadata }) => toText(await vpropFetch(`/agents/${id}`, {
|
|
373
|
+
}, async ({ id, name, email, phone, profile_photo_url, license_number, website, area, team_name, company_name, brokerage_phone, bio, logo_url, team_logo_url, company_logo_url, voice_id, metadata, options }) => toText(await vpropFetch(`/agents/${id}`, {
|
|
256
374
|
method: "PATCH",
|
|
257
|
-
body: JSON.stringify({ name, email, phone, voice_id, metadata }),
|
|
375
|
+
body: JSON.stringify({ name, email, phone, profile_photo_url, license_number, website, area, team_name, company_name, brokerage_phone, bio, logo_url, team_logo_url, company_logo_url, voice_id, metadata, options }),
|
|
258
376
|
})));
|
|
259
377
|
server.registerTool("delete_agent", {
|
|
260
378
|
description: "Delete an agent by their ID.",
|
|
@@ -266,8 +384,9 @@ server.registerTool("delete_agent", {
|
|
|
266
384
|
return toText({ deleted: true, id });
|
|
267
385
|
});
|
|
268
386
|
server.registerTool("upload_agent_photo", {
|
|
269
|
-
description: "Upload a headshot photo
|
|
270
|
-
"Required for the avatar and lipsync features in video projects."
|
|
387
|
+
description: "Upload a headshot photo from a local file (JPEG/PNG/WebP) for an agent. " +
|
|
388
|
+
"Required for the avatar and lipsync features in video projects. " +
|
|
389
|
+
"If the photo is already hosted at a URL, pass `profile_photo_url` in create_agent or update_agent instead.",
|
|
271
390
|
inputSchema: {
|
|
272
391
|
agent_id: z.string().describe("Agent ID to attach the photo to"),
|
|
273
392
|
file_path: z.string().describe("Absolute local path to the photo file"),
|
|
@@ -309,20 +428,84 @@ server.registerTool("create_project", {
|
|
|
309
428
|
agent_id: z.string().describe("Agent ID (`agt_…`) to feature in the video"),
|
|
310
429
|
title: z.string().max(120).optional().describe("Video title shown in the intro scene (max 120 chars)"),
|
|
311
430
|
cta: z.string().max(120).optional().describe("Call-to-action text in the agent scene (default: 'Schedule a tour')"),
|
|
312
|
-
theme: z
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
431
|
+
theme: z
|
|
432
|
+
.enum(["modern", "classic", "bold", "elegant", "luxe", "basic", "rosewood", "keystone", "pixel", "editorial", "sketch"])
|
|
433
|
+
.optional()
|
|
434
|
+
.describe("Visual theme (default: modern). `basic` is an unbranded template."),
|
|
435
|
+
background: z
|
|
436
|
+
.enum(["black", "white"])
|
|
437
|
+
.optional()
|
|
438
|
+
.describe("Scene background color. Defaults to the theme's natural background (white for `pixel`, black otherwise)."),
|
|
439
|
+
resolution: z
|
|
440
|
+
.enum(["portrait", "landscape"])
|
|
441
|
+
.optional()
|
|
442
|
+
.describe("Video orientation. portrait = 1080×1920 (Reels/TikTok); landscape = 1920×1080. Default: portrait."),
|
|
443
|
+
caption: z
|
|
444
|
+
.enum(["none", "keyword", "description"])
|
|
445
|
+
.optional()
|
|
446
|
+
.describe("Caption style. none = audio only; keyword = short pulses; description = full sentences. Default: keyword."),
|
|
447
|
+
language: z
|
|
448
|
+
.enum(["en", "es", "ko", "zh-Hans"])
|
|
449
|
+
.optional()
|
|
450
|
+
.describe("Narration + caption language (BCP-47). Default: en. " +
|
|
451
|
+
"Use list_voices and filter on verified_languages to find a matching voice."),
|
|
316
452
|
voice_id: z.string().optional().describe("Per-project voice override (from list_voices)"),
|
|
317
|
-
bgm_id: z.string().optional().describe("Background music ID (from list_bgms)"),
|
|
453
|
+
bgm_id: z.string().optional().describe("Background music ID (from list_bgms). Unknown IDs return 400."),
|
|
318
454
|
use_agent_avatar: z.boolean().optional().describe("Show animated agent avatar (default: true, needs agent photo)"),
|
|
319
|
-
use_lipsync: z.boolean().optional().describe("Lipsync the avatar (default: false)"),
|
|
455
|
+
use_lipsync: z.boolean().optional().describe("Lipsync the avatar to the narration (default: false, adds render time)"),
|
|
456
|
+
background_music: z
|
|
457
|
+
.boolean()
|
|
458
|
+
.optional()
|
|
459
|
+
.describe("Play background music under the video. Default: true. Set false for a music-free render."),
|
|
460
|
+
narration_highlights: z
|
|
461
|
+
.string()
|
|
462
|
+
.max(2000)
|
|
463
|
+
.optional()
|
|
464
|
+
.describe("Free-text key points to emphasize in the narration script " +
|
|
465
|
+
"(e.g. 'newly renovated kitchen, corner lot, walking distance to BART'). " +
|
|
466
|
+
"The model weaves these into the relevant scenes."),
|
|
467
|
+
word_highlight: z
|
|
468
|
+
.boolean()
|
|
469
|
+
.optional()
|
|
470
|
+
.describe("Render gallery captions word-by-word, synced to the narration. " +
|
|
471
|
+
"Supported on all themes except `basic`. Default: false."),
|
|
472
|
+
display_info: z
|
|
473
|
+
.object({
|
|
474
|
+
name: z.boolean().optional(),
|
|
475
|
+
email: z.boolean().optional(),
|
|
476
|
+
logo: z.boolean().optional(),
|
|
477
|
+
contact: z.boolean().optional(),
|
|
478
|
+
license_number: z.boolean().optional(),
|
|
479
|
+
team_info: z.boolean().optional(),
|
|
480
|
+
company_info: z.boolean().optional(),
|
|
481
|
+
company_name: z.boolean().optional(),
|
|
482
|
+
brokerage_phone: z.boolean().optional(),
|
|
483
|
+
})
|
|
484
|
+
.optional()
|
|
485
|
+
.describe("Control which agent-card fields appear on screen. " +
|
|
486
|
+
"Each flag defaults to the platform default when omitted (most on; email off). " +
|
|
487
|
+
"Useful for MLS/compliance rules that require hiding specific fields."),
|
|
320
488
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
321
489
|
},
|
|
322
|
-
}, async ({ listing_id, agent_id, title, cta, theme, resolution, caption, language, voice_id, bgm_id, use_agent_avatar, use_lipsync, metadata, }) => {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
490
|
+
}, async ({ listing_id, agent_id, title, cta, theme, background, resolution, caption, language, voice_id, bgm_id, use_agent_avatar, use_lipsync, background_music, narration_highlights, word_highlight, display_info, metadata, }) => {
|
|
491
|
+
// Build options object — use Object.values check so falsy booleans
|
|
492
|
+
// (background_music:false, word_highlight:false) are not silently dropped.
|
|
493
|
+
const optionFields = {
|
|
494
|
+
theme,
|
|
495
|
+
background,
|
|
496
|
+
resolution,
|
|
497
|
+
caption,
|
|
498
|
+
language,
|
|
499
|
+
voice_id,
|
|
500
|
+
bgm_id,
|
|
501
|
+
use_agent_avatar,
|
|
502
|
+
use_lipsync,
|
|
503
|
+
background_music,
|
|
504
|
+
narration_highlights,
|
|
505
|
+
word_highlight,
|
|
506
|
+
display_info,
|
|
507
|
+
};
|
|
508
|
+
const options = Object.values(optionFields).some((v) => v !== undefined) ? optionFields : undefined;
|
|
326
509
|
return toText(await vpropFetch("/projects", {
|
|
327
510
|
method: "POST",
|
|
328
511
|
body: JSON.stringify({ listing_id, agent_id, title, cta, options, metadata }),
|
|
@@ -365,19 +548,71 @@ server.registerTool("list_projects", {
|
|
|
365
548
|
// ── Webhooks ──────────────────────────────────────────────────────────────────
|
|
366
549
|
server.registerTool("register_webhook", {
|
|
367
550
|
description: "Register an HTTPS webhook URL to receive project lifecycle events. " +
|
|
368
|
-
"One webhook per API key is allowed
|
|
369
|
-
"
|
|
551
|
+
"One active webhook per API key is allowed — re-registering replaces the previous one. " +
|
|
552
|
+
"The response includes a `secret` used to verify the `X-VProp-Signature` HMAC on each delivery; " +
|
|
553
|
+
"it is returned only once, so store it immediately. " +
|
|
554
|
+
"Use get_webhook to inspect the current registration, delete_webhook to stop deliveries, " +
|
|
555
|
+
"and rotate_webhook_secret to roll the signing secret.",
|
|
370
556
|
inputSchema: {
|
|
371
557
|
url: z.string().url().describe("HTTPS endpoint that will receive POST requests"),
|
|
372
558
|
events: z
|
|
373
559
|
.array(z.enum(["project.created", "project.processing", "project.completed", "project.failed"]))
|
|
374
560
|
.min(1)
|
|
375
|
-
.
|
|
561
|
+
.optional()
|
|
562
|
+
.describe("Event types to subscribe to. Omit to subscribe to all events."),
|
|
376
563
|
},
|
|
377
564
|
}, async ({ url, events }) => toText(await vpropFetch("/webhooks", {
|
|
378
565
|
method: "POST",
|
|
379
566
|
body: JSON.stringify({ url, events }),
|
|
380
567
|
})));
|
|
568
|
+
server.registerTool("get_webhook", {
|
|
569
|
+
description: "Get the current webhook registration for this API key. " +
|
|
570
|
+
"The signing secret is NOT returned here (it is shown only once, at registration or after rotate_webhook_secret). " +
|
|
571
|
+
"Returns 404 if no webhook is registered.",
|
|
572
|
+
}, async () => toText(await vpropFetch("/webhooks")));
|
|
573
|
+
server.registerTool("delete_webhook", {
|
|
574
|
+
description: "Deactivate (soft-delete) a webhook by its ID. After this, no further events are delivered. " +
|
|
575
|
+
"Idempotent-ish: a second delete of the same ID returns 404 (already inactive). " +
|
|
576
|
+
"Use get_webhook to find the current webhook ID.",
|
|
577
|
+
inputSchema: {
|
|
578
|
+
id: z.string().describe("Webhook ID (UUID, from register_webhook or get_webhook)"),
|
|
579
|
+
},
|
|
580
|
+
}, async ({ id }) => toText(await vpropFetch(`/webhooks/${id}`, { method: "DELETE" })));
|
|
581
|
+
server.registerTool("rotate_webhook_secret", {
|
|
582
|
+
description: "Issue a new HMAC signing secret for an existing webhook, keeping its URL and events unchanged. " +
|
|
583
|
+
"The new `secret` is returned once and the previous secret is immediately invalidated. " +
|
|
584
|
+
"Deliveries sent between rotation and your partner updating their stored secret will fail signature verification " +
|
|
585
|
+
"(they will be retried by the server). " +
|
|
586
|
+
"Use get_webhook to find the current webhook ID.",
|
|
587
|
+
inputSchema: {
|
|
588
|
+
id: z.string().describe("Webhook ID (UUID, from register_webhook or get_webhook)"),
|
|
589
|
+
},
|
|
590
|
+
}, async ({ id }) => toText(await vpropFetch(`/webhooks/${id}/rotate-secret`, { method: "POST" })));
|
|
591
|
+
// ── Usage ─────────────────────────────────────────────────────────────────────
|
|
592
|
+
server.registerTool("get_usage", {
|
|
593
|
+
description: "Get this API key's usage statistics and credit balance. " +
|
|
594
|
+
"Returns counts of projects, listings, and agents created, plus credits consumed, over a time window. " +
|
|
595
|
+
"For prepaid keys, also returns the team's current `credit_balance`. " +
|
|
596
|
+
"Defaults to the last 30 days when `from`/`to` are omitted.",
|
|
597
|
+
inputSchema: {
|
|
598
|
+
from: z
|
|
599
|
+
.string()
|
|
600
|
+
.optional()
|
|
601
|
+
.describe("Start of the reporting window, ISO 8601 (e.g. '2026-06-01T00:00:00Z'). Default: 30 days ago."),
|
|
602
|
+
to: z
|
|
603
|
+
.string()
|
|
604
|
+
.optional()
|
|
605
|
+
.describe("End of the reporting window, ISO 8601. Default: now."),
|
|
606
|
+
},
|
|
607
|
+
}, async ({ from, to }) => {
|
|
608
|
+
const params = new URLSearchParams();
|
|
609
|
+
if (from)
|
|
610
|
+
params.set("from", from);
|
|
611
|
+
if (to)
|
|
612
|
+
params.set("to", to);
|
|
613
|
+
const qs = params.size ? `?${params}` : "";
|
|
614
|
+
return toText(await vpropFetch(`/usage${qs}`));
|
|
615
|
+
});
|
|
381
616
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
382
617
|
const transport = new StdioServerTransport();
|
|
383
618
|
await server.connect(transport);
|