@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.
Files changed (3) hide show
  1. package/README.md +15 -2
  2. package/dist/index.js +271 -36
  3. 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.0.0",
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 property address, e.g. '123 Main St, San Francisco, CA 94105' (required)"),
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("Property size in square feet"),
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
- metadata: z.record(z.string(), z.unknown()).optional().describe("Arbitrary key-value metadata"),
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 address, attributes, or metadata.",
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().describe("New property address"),
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 add a headshot for the avatar feature.",
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
- voice_id: z.string().optional().describe("Default voice ID from list_voices (overridable per-project)"),
218
- metadata: z.record(z.string(), z.unknown()).optional().describe("Arbitrary key-value metadata"),
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 for an agent (JPEG/PNG/WebP). " +
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.enum(["modern", "classic", "bold", "elegant"]).optional().describe("Visual theme (default: modern)"),
313
- resolution: z.enum(["portrait", "landscape"]).optional().describe("Video orientation (default: portrait/9:16)"),
314
- caption: z.enum(["none", "keyword", "description"]).optional().describe("Caption style (default: keyword)"),
315
- language: z.enum(["en", "es", "ko", "zh-Hans"]).optional().describe("Narration + caption language (default: en). Use list_voices to find a voice whose verified_languages includes your chosen language."),
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
- const options = theme || resolution || caption || language || voice_id || bgm_id || use_agent_avatar !== undefined || use_lipsync !== undefined
324
- ? { theme, resolution, caption, language, voice_id, bgm_id, use_agent_avatar, use_lipsync }
325
- : undefined;
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 (re-registering replaces the previous one). " +
369
- "Note: GET/DELETE/rotate-secret webhook endpoints are not yet implemented in the API.",
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
- .describe("Event types to subscribe to"),
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vprop/mcp",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for the vProp Public API — generate real-estate listing videos with AI",
5
5
  "type": "module",
6
6
  "bin": {