@scavio/mcp-server 0.12.0 → 0.13.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 (101) hide show
  1. package/README.md +365 -4
  2. package/dist/index.js +26 -7
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/env.d.ts +1 -0
  5. package/dist/lib/env.js +4 -0
  6. package/dist/lib/env.js.map +1 -1
  7. package/dist/lib/tool-error.d.ts +22 -0
  8. package/dist/lib/tool-error.js +28 -0
  9. package/dist/lib/tool-error.js.map +1 -0
  10. package/dist/tools/airbnb.d.ts +3 -0
  11. package/dist/tools/airbnb.js +121 -0
  12. package/dist/tools/airbnb.js.map +1 -0
  13. package/dist/tools/amazon.js +1 -12
  14. package/dist/tools/amazon.js.map +1 -1
  15. package/dist/tools/appstore.d.ts +3 -0
  16. package/dist/tools/appstore.js +59 -0
  17. package/dist/tools/appstore.js.map +1 -0
  18. package/dist/tools/booking.d.ts +3 -0
  19. package/dist/tools/booking.js +116 -0
  20. package/dist/tools/booking.js.map +1 -0
  21. package/dist/tools/capterra.d.ts +3 -0
  22. package/dist/tools/capterra.js +53 -0
  23. package/dist/tools/capterra.js.map +1 -0
  24. package/dist/tools/companieshouse.d.ts +3 -0
  25. package/dist/tools/companieshouse.js +56 -0
  26. package/dist/tools/companieshouse.js.map +1 -0
  27. package/dist/tools/ebay.d.ts +3 -0
  28. package/dist/tools/ebay.js +79 -0
  29. package/dist/tools/ebay.js.map +1 -0
  30. package/dist/tools/extract.d.ts +3 -0
  31. package/dist/tools/extract.js +21 -0
  32. package/dist/tools/extract.js.map +1 -0
  33. package/dist/tools/g2.d.ts +3 -0
  34. package/dist/tools/g2.js +69 -0
  35. package/dist/tools/g2.js.map +1 -0
  36. package/dist/tools/glassdoor.d.ts +3 -0
  37. package/dist/tools/glassdoor.js +72 -0
  38. package/dist/tools/glassdoor.js.map +1 -0
  39. package/dist/tools/google.js +1 -12
  40. package/dist/tools/google.js.map +1 -1
  41. package/dist/tools/googleads.d.ts +3 -0
  42. package/dist/tools/googleads.js +61 -0
  43. package/dist/tools/googleads.js.map +1 -0
  44. package/dist/tools/googleplay.d.ts +3 -0
  45. package/dist/tools/googleplay.js +57 -0
  46. package/dist/tools/googleplay.js.map +1 -0
  47. package/dist/tools/homedepot.d.ts +3 -0
  48. package/dist/tools/homedepot.js +71 -0
  49. package/dist/tools/homedepot.js.map +1 -0
  50. package/dist/tools/indeed.d.ts +3 -0
  51. package/dist/tools/indeed.js +98 -0
  52. package/dist/tools/indeed.js.map +1 -0
  53. package/dist/tools/index.d.ts +42 -1
  54. package/dist/tools/index.js +204 -15
  55. package/dist/tools/index.js.map +1 -1
  56. package/dist/tools/instagram.js +1 -12
  57. package/dist/tools/instagram.js.map +1 -1
  58. package/dist/tools/kuaishou.d.ts +3 -0
  59. package/dist/tools/kuaishou.js +207 -0
  60. package/dist/tools/kuaishou.js.map +1 -0
  61. package/dist/tools/linkedin.js +1 -12
  62. package/dist/tools/linkedin.js.map +1 -1
  63. package/dist/tools/metaads.d.ts +3 -0
  64. package/dist/tools/metaads.js +63 -0
  65. package/dist/tools/metaads.js.map +1 -0
  66. package/dist/tools/reddit.js +1 -12
  67. package/dist/tools/reddit.js.map +1 -1
  68. package/dist/tools/redfin.d.ts +3 -0
  69. package/dist/tools/redfin.js +100 -0
  70. package/dist/tools/redfin.js.map +1 -0
  71. package/dist/tools/sec.d.ts +3 -0
  72. package/dist/tools/sec.js +104 -0
  73. package/dist/tools/sec.js.map +1 -0
  74. package/dist/tools/target.d.ts +3 -0
  75. package/dist/tools/target.js +105 -0
  76. package/dist/tools/target.js.map +1 -0
  77. package/dist/tools/threads.d.ts +3 -0
  78. package/dist/tools/threads.js +93 -0
  79. package/dist/tools/threads.js.map +1 -0
  80. package/dist/tools/tiktok-shop.js +3 -11
  81. package/dist/tools/tiktok-shop.js.map +1 -1
  82. package/dist/tools/tiktok.js +1 -12
  83. package/dist/tools/tiktok.js.map +1 -1
  84. package/dist/tools/tripadvisor.d.ts +3 -0
  85. package/dist/tools/tripadvisor.js +103 -0
  86. package/dist/tools/tripadvisor.js.map +1 -0
  87. package/dist/tools/usage.js +2 -10
  88. package/dist/tools/usage.js.map +1 -1
  89. package/dist/tools/walmart.js +108 -41
  90. package/dist/tools/walmart.js.map +1 -1
  91. package/dist/tools/x.js +1 -12
  92. package/dist/tools/x.js.map +1 -1
  93. package/dist/tools/yelp.d.ts +3 -0
  94. package/dist/tools/yelp.js +65 -0
  95. package/dist/tools/yelp.js.map +1 -0
  96. package/dist/tools/youtube.js +1 -12
  97. package/dist/tools/youtube.js.map +1 -1
  98. package/dist/tools/zillow.d.ts +3 -0
  99. package/dist/tools/zillow.js +123 -0
  100. package/dist/tools/zillow.js.map +1 -0
  101. package/package.json +12 -2
@@ -0,0 +1,121 @@
1
+ import { z } from "zod";
2
+ import { handleApiError } from "../lib/tool-error.js";
3
+ // No zod `.default()` anywhere in this file, on purpose. The MCP SDK applies a
4
+ // zod default BEFORE the handler runs, so a defaulted field is posted on every
5
+ // single call whether the model set it or not — which is exactly how the
6
+ // shipped Walmart tool ended up sending a value the backend enum rejected.
7
+ // Upstream defaults are documented in the .describe() text instead and left for
8
+ // the backend to apply.
9
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
10
+ // Search and listing take the same stay + party block. Built per-tool so no two
11
+ // tool schemas share an instance.
12
+ const stayFields = () => ({
13
+ check_in: z.string().regex(ISO_DATE, "check_in must be YYYY-MM-DD").optional()
14
+ .describe("Check-in date, YYYY-MM-DD. MUST be sent together with check_out — Airbnb prices a window, not a day."),
15
+ check_out: z.string().regex(ISO_DATE, "check_out must be YYYY-MM-DD").optional()
16
+ .describe("Check-out date, YYYY-MM-DD. Must be after check_in and sent together with it."),
17
+ adults: z.number().int().min(1).optional()
18
+ .describe("Adults in the party."),
19
+ children: z.number().int().min(0).optional()
20
+ .describe("Children aged 2-12."),
21
+ infants: z.number().int().min(0).optional()
22
+ .describe("Infants under 2."),
23
+ pets: z.number().int().min(0).optional()
24
+ .describe("Pets travelling with the party."),
25
+ });
26
+ const currencyField = () => z.string().regex(/^[A-Za-z]{3}$/, "currency must be a 3-letter ISO 4217 code").optional()
27
+ .describe("ISO 4217 display currency, e.g. 'USD'. Defaults to USD; without it Airbnb prices off the proxy exit and two identical requests can disagree.");
28
+ const listingIdField = () => z.string().min(1).max(500)
29
+ .describe("Airbnb listing id, e.g. '1704452615554844556', or a full airbnb.com/rooms/ URL. Query parameters on a pasted URL are discarded — they carry someone else's dates.");
30
+ export function registerAirbnbTools(server, getClient) {
31
+ server.tool("search_airbnb", `Search Airbnb stays in a location as JSON. Each listing carries the STAY-TOTAL and PER-NIGHT price with the full discount ledger, rating and review count, bedrooms/beds/baths, coordinates, badges (Superhost, Guest Favourite), images, and the listing id.
32
+
33
+ This is the ONLY source of Airbnb prices. get_airbnb_listing returns no nightly rate under any parameters, so any pricing question has to be answered from here.
34
+
35
+ Send check_in and check_out TOGETHER. A dateless search silently defaults to a window 30 days out for 5 nights AND Airbnb A/B-tests both the window and the prices — the same URL has returned first-row prices of $680, $802 and $2,238 across runs. The response flags this as dates_are_defaulted. Pass explicit dates for anything you intend to reproduce or compare.
36
+
37
+ min_price and max_price are WHOLE-STAY totals, not per night. currency defaults to USD. room_type and the amenity names are validated before the scrape on purpose: an unrecognised value is not an error upstream, it returns the UNFILTERED set under a 200.
38
+
39
+ Paginate with page (18 listings per page) OR with cursor from a previous response — never both, because cursor wins and the request is rejected rather than silently billed for a discarded page number. Costs 1 credit per page.`, {
40
+ location: z.string().min(1).max(200)
41
+ .describe("Where to search: a city ('Austin, TX'), a region ('Lake Tahoe'), a ZIP, or a pasted airbnb.com/s/ URL. A location Airbnb cannot resolve is a 404, not an empty result."),
42
+ ...stayFields(),
43
+ min_price: z.number().min(0).optional()
44
+ .describe("Minimum price for the WHOLE STAY (not per night), in the currency field."),
45
+ max_price: z.number().min(0).optional()
46
+ .describe("Maximum price for the WHOLE STAY (not per night), in the currency field. Must not be below min_price."),
47
+ room_type: z.enum(["entire_home", "private_room", "shared_room", "hotel_room"]).optional()
48
+ .describe("Restrict to one kind of place. Closed set — an unrecognised value would return the UNFILTERED set under a 200 upstream, so it is rejected here."),
49
+ min_bedrooms: z.number().int().min(0).optional()
50
+ .describe("Minimum bedrooms."),
51
+ min_beds: z.number().int().min(0).optional()
52
+ .describe("Minimum beds."),
53
+ min_bathrooms: z.number().int().min(0).optional()
54
+ .describe("Minimum bathrooms."),
55
+ superhost: z.boolean().optional()
56
+ .describe("Only Superhost listings."),
57
+ instant_book: z.boolean().optional()
58
+ .describe("Only Instant Book listings."),
59
+ guest_favorite: z.boolean().optional()
60
+ .describe("Only listings badged Guest Favourite."),
61
+ free_cancellation: z.boolean().optional()
62
+ .describe("Only listings offering free cancellation."),
63
+ amenities: z.string().min(1).max(200).optional()
64
+ .describe("Comma-separated amenity filter, e.g. 'wifi,pool'. Named vocabulary: wifi, air_conditioning, pool, kitchen, free_parking, washer, self_check_in, tv — or raw numeric Airbnb amenity ids. An unrecognised NAME is rejected before the scrape, because upstream it would silently return the unfiltered set."),
65
+ currency: currencyField(),
66
+ page: z.number().int().min(1).optional()
67
+ .describe("Results page, 1-based. 18 listings per page. CANNOT be combined with cursor — sending both is rejected."),
68
+ cursor: z.string().min(1).max(500).optional()
69
+ .describe("A next_cursor from a previous response. Use INSTEAD of page — cursor wins, so sending both is rejected."),
70
+ }, async (params) => {
71
+ try {
72
+ const data = await getClient().post("/api/v1/airbnb/search", params);
73
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
74
+ }
75
+ catch (err) {
76
+ return handleApiError(err);
77
+ }
78
+ });
79
+ server.tool("get_airbnb_listing", `Get one Airbnb listing in full as JSON: description, property and room type, capacity and room counts, the complete grouped amenity list (including the ones the place does NOT have), host profile and stats, house rules with parsed check-in/check-out times, cancellation policy, sleeping arrangements, photo tour and every image — and the RATING BREAKDOWN: six category ratings, the five-bucket star distribution, and Airbnb's AI-synthesised review tags.
80
+
81
+ IT RETURNS NO NIGHTLY PRICE. The Airbnb room page carries no rate under ANY parameters, with or without dates. Prices are search-only: use search_airbnb for them.
82
+
83
+ The rating breakdown lives here and NOT on get_airbnb_reviews, so this is the tool for aggregate review sentiment; get_airbnb_reviews is for individual review bodies.
84
+
85
+ Accepts a listing id or a full /rooms/ URL (query parameters are discarded — they carry someone else's dates). The stay parameters are accepted and echoed back, but they do not price anything. No pagination. Costs 1 credit.`, {
86
+ listing_id: listingIdField(),
87
+ ...stayFields(),
88
+ currency: currencyField(),
89
+ }, async (params) => {
90
+ try {
91
+ const data = await getClient().post("/api/v1/airbnb/listing", params);
92
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
93
+ }
94
+ catch (err) {
95
+ return handleApiError(err);
96
+ }
97
+ });
98
+ server.tool("get_airbnb_reviews", `Get Airbnb review BODIES for one listing as JSON: the guest's written comment, per-review rating, review date, and the reviewer's name, photo and location.
99
+
100
+ It does NOT carry the rating breakdown or the review tags — those are on get_airbnb_listing. Use this tool when you need what guests actually wrote.
101
+
102
+ Paginate with limit and offset: count is the listing's TOTAL review count, returned is how many this response holds, so page while offset + returned is below count. limit accepts 1-50 and defaults to 30 upstream. Send limit explicitly when paging — with no explicit limit the upstream returns a fixed 7 rows.
103
+
104
+ Accepts a listing id or a full /rooms/ URL. Costs 1 credit per page.`, {
105
+ listing_id: listingIdField(),
106
+ currency: currencyField(),
107
+ limit: z.number().int().min(1).max(50).optional()
108
+ .describe("Review bodies per page, 1-50. Upstream default is 30. Send it explicitly when paging — with no explicit limit the upstream returns a fixed 7 rows."),
109
+ offset: z.number().int().min(0).optional()
110
+ .describe("Rows to skip, 0-based. Use with limit to page past the first set."),
111
+ }, async (params) => {
112
+ try {
113
+ const data = await getClient().post("/api/v1/airbnb/reviews", params);
114
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
115
+ }
116
+ catch (err) {
117
+ return handleApiError(err);
118
+ }
119
+ });
120
+ }
121
+ //# sourceMappingURL=airbnb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"airbnb.js","sourceRoot":"","sources":["../../src/tools/airbnb.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,5 @@
1
1
  import { z } from "zod";
2
- import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
3
- import { ApiError } from "../lib/errors.js";
2
+ import { handleApiError } from "../lib/tool-error.js";
4
3
  // Amazon moved to a new upstream provider and now returns a normalized shape
5
4
  // instead of the old raw passthrough. Two consequences for these tool schemas:
6
5
  //
@@ -34,16 +33,6 @@ const asinField = z
34
33
  .string()
35
34
  .length(10)
36
35
  .describe("Amazon ASIN - the 10-character product id, e.g. 'B09V3KXJPB'. Extract it from the product URL (/dp/ASIN).");
37
- function handleApiError(err) {
38
- if (err instanceof ApiError) {
39
- if (err.status === 429)
40
- return { isError: true, content: [{ type: "text", text: "Rate limited. Wait and retry." }] };
41
- if (err.status === 401)
42
- throw new McpError(ErrorCode.InternalError, "Invalid SCAVIO_API_KEY. Check your configuration.");
43
- return { isError: true, content: [{ type: "text", text: `Scavio API error (${err.status}): ${err.message}` }] };
44
- }
45
- throw new McpError(ErrorCode.InternalError, String(err));
46
- }
47
36
  export function registerAmazonTools(server, getClient) {
48
37
  const call = (path) => async (params) => {
49
38
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"amazon.js","sourceRoot":"","sources":["../../src/tools/amazon.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"amazon.js","sourceRoot":"","sources":["../../src/tools/amazon.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ScavioClient } from "../lib/client.js";
3
+ export declare function registerAppStoreTools(server: McpServer, getClient: () => ScavioClient): void;
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ import { handleApiError } from "../lib/tool-error.js";
3
+ // Two-letter ISO only. The transport falls back to the US storefront for
4
+ // anything that is not two letters, so 'usa' would silently buy a US result set
5
+ // on a billed call — rejecting it here costs nothing.
6
+ const countryField = z.string().regex(/^[A-Za-z]{2}$/, "country must be a two-letter ISO country code, e.g. us").optional()
7
+ .describe("Storefront to answer from, as a two-letter ISO code ('us', 'gb', 'jp'). Decides price, currency, localised title and whether the app is sold there at all. Must be exactly two letters - 'usa' is rejected rather than silently answered from the US. Defaults to 'us'.");
8
+ export function registerAppStoreTools(server, getClient) {
9
+ server.tool("search_app_store", `Search Apple's App Store (iPhone, iPad and Mac apps) and return up to 200 fully-shaped app rows as JSON - the same 43-field row get_app_store_app returns, so this doubles as a bulk metadata fetch. Apple matches an app name, a keyword OR a publisher name, so searching a developer returns their whole catalogue. SEARCH HAS NO PAGINATION: limit (1-200) is the only lever on result volume - raise limit, never ask for a page or an offset, which are silently ignored. country decides price, currency, localised title and whether the app is sold in that storefront at all. With entity=mac_software the iPad/Apple TV screenshots, advisories, features, supported devices and Game Center flag come back EMPTY rather than absent. Costs 1 credit.`, {
10
+ term: z.string().min(1).max(500)
11
+ .describe("What to search for. Apple matches an app name, a keyword, or a PUBLISHER name - searching a developer returns their catalogue even when the words appear in none of their app titles."),
12
+ limit: z.number().int().min(1).max(200).optional()
13
+ .describe("Apps to return, 1-200. THE ONLY lever on result volume - the search API has no pagination and there is no second page to ask for. Defaults to 25."),
14
+ country: countryField,
15
+ entity: z.enum(["software", "ipad_software", "mac_software"]).optional()
16
+ .describe("Which catalogue to search: 'software' (iPhone/iPad apps, the default), 'ipad_software', or 'mac_software'. Mac rows carry no iPad/Apple TV screenshots, advisories, features, supported devices or Game Center flag - those come back empty rather than absent."),
17
+ lang: z.string().regex(/^[A-Za-z]{2}_[A-Za-z]{2}$/, "lang must be a five-letter code, e.g. en_us or ja_jp").optional()
18
+ .describe("Language of the returned listing text as a five-letter code ('en_us', 'ja_jp'). Independent of country: the storefront decides the price, this decides the words."),
19
+ }, async (params) => {
20
+ try {
21
+ const data = await getClient().post("/api/v1/appstore/search", params);
22
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
23
+ }
24
+ catch (err) {
25
+ return handleApiError(err);
26
+ }
27
+ });
28
+ server.tool("get_app_store_app", `Get one Apple App Store listing in full as JSON: title, description, developer and seller identity, price and currency, all-time and current-version ratings, version and release notes, genres, content rating and advisories, icons at three sizes, screenshots, download size, minimum OS, languages, supported devices, Game Center and VPP flags. Accepts BOTH a numeric App Store id (the digits after 'id' in an apps.apple.com URL) and a bundle id such as 'notion.id' - both resolve to the identical payload. A pasted apps.apple.com URL is rejected with a free 400, so pull the id out of it first. An id Apple cannot resolve is a BILLED 404. Mac apps carry no iPad/Apple TV screenshots, advisories, features, supported devices or Game Center flag - empty rather than absent. Costs 1 credit.`, {
29
+ app_id: z.string().min(1).max(255).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "app_id must be a numeric App Store id (e.g. 1232780281) or a bundle id (e.g. notion.id)")
30
+ .describe("App Store id - the digits after 'id' in an apps.apple.com URL, e.g. '1232780281' - or the app's bundle id ('notion.id', 'com.burbn.instagram'). Both resolve to the identical payload. A full apps.apple.com URL is rejected."),
31
+ country: countryField,
32
+ }, async (params) => {
33
+ try {
34
+ const data = await getClient().post("/api/v1/appstore/app", params);
35
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
36
+ }
37
+ catch (err) {
38
+ return handleApiError(err);
39
+ }
40
+ });
41
+ server.tool("get_app_store_reviews", `Get a page of Apple App Store customer reviews as JSON: star rating, title, full text, author, and the APP VERSION each review was written against. NUMERIC APP STORE IDS ONLY - unlike get_app_store_app, the reviews feed has no bundle-id form. HARD STOP AT PAGE 10 at 50 reviews per page: 500 reviews per storefront is Apple's anonymous ceiling, and the way to reach further is to ask a different country. Under sort=most_recent the review vote fields come back as ZEROES (those reviews are too new to have been voted on); most_helpful returns them densely populated. These reviews CANNOT 404 - an unknown id and a real app with zero reviews return the same empty feed, so an empty result does not mean the app is missing. Costs 1 credit per page.`, {
42
+ app_id: z.string().regex(/^\d+$/, "app_id must be a numeric App Store id")
43
+ .describe("App Store id, e.g. '1232780281'. NUMERIC ONLY - unlike get_app_store_app, the reviews feed has no bundle-id form."),
44
+ country: countryField,
45
+ page: z.number().int().min(1).max(10).optional()
46
+ .describe("Reviews page, 1-10, 50 reviews each. Apple hard-stops at page 10, so 500 reviews per storefront is the ceiling - reach further by asking a different country. Defaults to 1."),
47
+ sort: z.enum(["most_recent", "most_helpful"]).optional()
48
+ .describe("Review ordering. Also decides whether the vote fields mean anything: under 'most_recent' almost every review is too new to have been voted on and the vote fields return zeroes, while 'most_helpful' returns them densely populated. Defaults to 'most_recent'."),
49
+ }, async (params) => {
50
+ try {
51
+ const data = await getClient().post("/api/v1/appstore/reviews", params);
52
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
53
+ }
54
+ catch (err) {
55
+ return handleApiError(err);
56
+ }
57
+ });
58
+ }
59
+ //# sourceMappingURL=appstore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appstore.js","sourceRoot":"","sources":["../../src/tools/appstore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ScavioClient } from "../lib/client.js";
3
+ export declare function registerBookingTools(server: McpServer, getClient: () => ScavioClient): void;
@@ -0,0 +1,116 @@
1
+ import { z } from "zod";
2
+ import { handleApiError } from "../lib/tool-error.js";
3
+ // No zod `.default()` anywhere in this file, on purpose. The MCP SDK applies a
4
+ // zod default BEFORE the handler runs, so a defaulted field is posted on every
5
+ // single call whether the model set it or not — which is exactly how the
6
+ // shipped Walmart tool ended up sending a value the backend enum rejected.
7
+ // Upstream defaults are documented in the .describe() text instead and left for
8
+ // the backend to apply.
9
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
10
+ // Booking prices a STAY, not a property, so all three endpoints take the same
11
+ // occupancy block. Built per-tool so no two tool schemas share an instance.
12
+ const stayFields = () => ({
13
+ checkin: z.string().regex(ISO_DATE, "checkin must be YYYY-MM-DD").optional()
14
+ .describe("Check-in date, YYYY-MM-DD. MUST be sent together with checkout — Booking ignores a lone checkin and prices a default range of its own instead."),
15
+ checkout: z.string().regex(ISO_DATE, "checkout must be YYYY-MM-DD").optional()
16
+ .describe("Check-out date, YYYY-MM-DD. Must be after checkin and sent together with it."),
17
+ adults: z.number().int().min(1).optional()
18
+ .describe("Adults in the party. Upstream default is 2."),
19
+ children_ages: z.array(z.number().int().min(0).max(17)).max(10).optional()
20
+ .describe("One AGE (0-17) per child, not a count — Booking prices children by age. Max 10 entries."),
21
+ rooms: z.number().int().min(1).optional()
22
+ .describe("Rooms required. Upstream default is 1."),
23
+ currency: z.string().regex(/^[A-Za-z]{3}$/, "currency must be a 3-letter ISO 4217 code").optional()
24
+ .describe("ISO 4217 display currency, e.g. 'USD'. Defaults to USD; without it Booking prices off the proxy exit and two identical requests can disagree."),
25
+ });
26
+ const hotelField = () => z.string().min(1).max(500)
27
+ .describe("A booking.com property URL, or the bare page slug, e.g. 'citizenm-austin-downtown'. Query parameters on a pasted URL are discarded. Prefer the url a search_booking row returns — a bare slug with the wrong country_code is a real, BILLED 404.");
28
+ const countryCodeField = () => z.string().regex(/^[A-Za-z]{2}$/, "country_code must be a two-letter country code").optional()
29
+ .describe("Two-letter country segment of the property path, e.g. 'us'. Only consulted for a bare slug (a full URL carries its own). Defaults to us — a wrong value is a billed 404.");
30
+ export function registerBookingTools(server, getClient) {
31
+ server.tool("search_booking", `Search Booking.com for hotels, apartments, hostels, resorts and other stays in a destination, as JSON. Each property carries its name and property url, the live price for the stay you asked for, review score and review count, star rating, location and distance, room type, and deal badges.
32
+
33
+ Either destination or dest_id is required. A search with NEITHER returns Booking's homepage — it still costs a credit and returns nothing. dest_type is silently ignored by Booking without dest_id, so it is rejected here rather than billed.
34
+
35
+ checkin and checkout must be sent TOGETHER. Send only one and Booking prices a default range of its own, returning real prices for dates you never asked for. currency defaults to USD; without it Booking prices off the proxy exit and two identical requests can disagree.
36
+
37
+ Chain the url on a result row into get_booking_hotel or get_booking_reviews — that is always cheaper than guessing a slug plus country_code, which is a real, BILLED 404 when wrong.
38
+
39
+ Paginate with page: 25 properties per page. min_review_score is an ENUM of '6', '7', '8' or '9' — Booking silently drops any other threshold. Costs 1 credit per page.`, {
40
+ destination: z.string().min(1).max(200).optional()
41
+ .describe("Free-text destination, e.g. 'Austin, Texas'. Required unless dest_id is given."),
42
+ dest_id: z.string().regex(/^\d+$/, "dest_id must be numeric").optional()
43
+ .describe("Booking's own NUMERIC destination id, e.g. '20126662'. Use it to pin an ambiguous name; a search response returns the dest_id it resolved. Required unless destination is given."),
44
+ dest_type: z.enum(["city", "region", "country", "district", "landmark", "airport", "hotel"]).optional()
45
+ .describe("What dest_id refers to. Only meaningful WITH dest_id and rejected without it, because Booking ignores it on its own."),
46
+ page: z.number().int().min(1).optional()
47
+ .describe("Results page, 1-based. 25 properties per page, 1 credit per page."),
48
+ sort_by: z.enum(["popularity", "price_low", "price_high", "stars_high", "stars_low", "stars_and_price", "distance", "review_score"]).optional()
49
+ .describe("Result sort order. Upstream default is 'popularity'. 'distance' ranks from the searched destination centre, not from your location."),
50
+ min_price: z.number().min(0).optional()
51
+ .describe("Minimum price PER NIGHT, in the currency field."),
52
+ max_price: z.number().min(0).optional()
53
+ .describe("Maximum price PER NIGHT, in the currency field. Must not be below min_price."),
54
+ stars: z.array(z.number().int().min(1).max(5)).min(1).max(5).optional()
55
+ .describe("Star ratings to keep, e.g. [4, 5]. Multiple values are OR'd."),
56
+ min_review_score: z.enum(["6", "7", "8", "9"]).optional()
57
+ .describe("Minimum guest review score. ONLY '6', '7', '8' or '9' — Booking offers exactly these four floors and silently drops any other threshold."),
58
+ property_type: z.union([
59
+ z.enum(["apartments", "hostels", "hotels", "motels", "resorts", "bed_and_breakfasts", "villas", "campgrounds", "vacation_homes", "lodges", "homestays"]),
60
+ z.number().int().min(1),
61
+ ]).optional()
62
+ .describe("Property type name, or a raw numeric Booking accommodation-type id for a type this list does not name."),
63
+ free_cancellation: z.boolean().optional()
64
+ .describe("Only properties offering free cancellation."),
65
+ no_prepayment: z.boolean().optional()
66
+ .describe("Only properties that take no prepayment."),
67
+ breakfast_included: z.boolean().optional()
68
+ .describe("Only rates that include breakfast."),
69
+ ...stayFields(),
70
+ }, async (params) => {
71
+ try {
72
+ const data = await getClient().post("/api/v1/booking/search", params);
73
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
74
+ }
75
+ catch (err) {
76
+ return handleApiError(err);
77
+ }
78
+ });
79
+ server.tool("get_booking_hotel", `Get one Booking.com property in full as JSON: its rooms and rate plans priced for the stay you asked for, facilities, house rules, check-in and check-out windows, cancellation and prepayment policies, images, location, and review scores.
80
+
81
+ It takes dates because Booking prices a STAY, not a property. Omit them and you do not get a property without prices — you get prices for a two-night range Booking chose. The response echoes whichever dates were used, so a price is never for an unknown stay. checkin and checkout must be sent together.
82
+
83
+ Accepts a booking.com property URL or the bare page slug. country_code is only consulted for a bare slug and defaults to 'us'; a wrong country on a bare slug is a real, BILLED 404, so chaining the url a search_booking row returns is always cheaper.
84
+
85
+ Single-property lookup with NO pagination. Costs 1 credit.`, {
86
+ hotel: hotelField(),
87
+ country_code: countryCodeField(),
88
+ ...stayFields(),
89
+ }, async (params) => {
90
+ try {
91
+ const data = await getClient().post("/api/v1/booking/hotel", params);
92
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
93
+ }
94
+ catch (err) {
95
+ return handleApiError(err);
96
+ }
97
+ });
98
+ server.tool("get_booking_reviews", `Get Booking.com guest reviews for one property as JSON, with the score breakdown by category (staff, cleanliness, value, location, comfort, facilities) and Booking's own summary of what guests praised and complained about.
99
+
100
+ There is NO PAGE PARAMETER — do not try to paginate this and do not invent one. One call returns one response: total_count is the property's WHOLE review history, count is how many reviews this response actually holds.
101
+
102
+ Accepts a booking.com property URL or the bare page slug, exactly like get_booking_hotel; country_code is only consulted for a bare slug and a wrong one is a real, BILLED 404. The stay parameters are accepted because Booking serves the review page inside a priced stay context. Costs 1 credit.`, {
103
+ hotel: hotelField(),
104
+ country_code: countryCodeField(),
105
+ ...stayFields(),
106
+ }, async (params) => {
107
+ try {
108
+ const data = await getClient().post("/api/v1/booking/reviews", params);
109
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
110
+ }
111
+ catch (err) {
112
+ return handleApiError(err);
113
+ }
114
+ });
115
+ }
116
+ //# sourceMappingURL=booking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking.js","sourceRoot":"","sources":["../../src/tools/booking.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ScavioClient } from "../lib/client.js";
3
+ export declare function registerCapterraTools(server: McpServer, getClient: () => ScavioClient): void;
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+ import { handleApiError } from "../lib/tool-error.js";
3
+ export function registerCapterraTools(server, getClient) {
4
+ server.tool("search_capterra_software", `Capterra, the B2B software review site. Search it for business software and get 20 ranked products back as JSON: name, vendor description, star rating, review count, logo and a paid-placement flag. Every row carries the product_id and slug that get_capterra_product and get_capterra_reviews take, so this is the entry point when you only have a product name or a category. THIS SEARCH DOES NOT PAGINATE AND CANNOT RETURN MORE THAN 20 PRODUCTS: Capterra fixes the result set at 20 and serves identical rows for ?page=2, so there is deliberately no page parameter - narrow the query instead of trying to page. A query (or a url that carries one) is REQUIRED: a term-less Capterra search serves a fixed popular-products list that has nothing to do with what you asked for. Costs 2 credits.`, {
5
+ query: z.string().min(1).max(200).optional()
6
+ .describe("Software search term, e.g. 'help desk' or 'notion'. Either this or url is required - without one, Capterra serves an unrelated popular-products list."),
7
+ url: z.string().min(1).max(1000).optional()
8
+ .describe("Full Capterra search URL to run verbatim (capterra.com, capterra.co.uk and capterra.com.br are accepted). Either this or query is required."),
9
+ }, async (params) => {
10
+ try {
11
+ const data = await getClient().post("/api/v1/capterra/search", params);
12
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
13
+ }
14
+ catch (err) {
15
+ return handleApiError(err);
16
+ }
17
+ });
18
+ server.tool("get_capterra_product", `Capterra, the B2B software review site. Get one software product's full Capterra profile as JSON: overall rating with the per-star histogram and the four scored criteria, likelihood to recommend, review sentiment and topics, the complete pricing table with every plan and its features, every rated feature, every integration, AI-derived pros and cons with the review each was quoted from, FAQs, screenshots, badges and awards, competitor comparisons and alternatives, and the buyer profile broken down by company size, industry and job function. THE 25 MOST RECENT REVIEWS RIDE ALONG AT NO EXTRA COST - only call get_capterra_reviews when you need to page past them. The vendor field IS ALWAYS NULL here: Capterra does not publish it as structured data on the product page (the individual reviews name the vendor instead). product_id MUST BE A STRING - a JSON number is rejected - and it is the number in a /p/186596/Notion/ URL. The slug parameter is cosmetic on this endpoint. No pagination. Pass product_id or url - at least one is required. Costs 2 credits.`, {
19
+ product_id: z.string().min(1).max(50).optional()
20
+ .describe("Capterra product id as a STRING - the number in /p/186596/Notion/, so '186596'. A JSON number is rejected. Either this or url is required."),
21
+ slug: z.string().min(1).max(200).optional()
22
+ .describe("Product slug. COSMETIC on this endpoint - /p/186596/Zzzjunk/ returns the same profile byte-for-byte. It is load-bearing on get_capterra_reviews."),
23
+ url: z.string().min(1).max(1000).optional()
24
+ .describe("Full Capterra product URL, e.g. 'https://www.capterra.com/p/186596/Notion/'. Either this or product_id is required."),
25
+ }, async (params) => {
26
+ try {
27
+ const data = await getClient().post("/api/v1/capterra/product", params);
28
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
29
+ }
30
+ catch (err) {
31
+ return handleApiError(err);
32
+ }
33
+ });
34
+ server.tool("get_capterra_reviews", `Capterra, the B2B software review site. Get one page of written reviews for a software product as JSON: the overall score plus five per-criterion scores, title, pros, cons, advice to others, how long the reviewer used it, the incentivized flag, alternatives they considered and what they switched from, reviewer job title, industry and company size, and the vendor's response. It also carries a richer competitor list than the profile page does, each alternative with its own rating histogram and starting price. USE THIS ONLY TO PAGE PAST THE FIRST 25 REVIEWS - get_capterra_product already returns the 25 most recent at no extra cost. 25 reviews per page, HARD-CAPPED AT PAGE 100 no matter how many reviews the product has; past page 100 Capterra answers with page one. The slug parameter is LOAD-BEARING here, unlike on the product endpoint: it is case-sensitive upstream and a wrong one silently serves PAGE ONE under a billed success, so pass back the slug or reviews_url you got from search_capterra_software or get_capterra_product. Pass product_id or url - at least one is required. Costs 2 credits per page.`, {
35
+ product_id: z.string().min(1).max(50).optional()
36
+ .describe("Capterra product id as a STRING, e.g. '186596'. Either this or url is required."),
37
+ slug: z.string().min(1).max(200).optional()
38
+ .describe("Product slug. LOAD-BEARING here: case-sensitive upstream, and a wrong one silently serves page one under a billed success. Pass back the slug from search_capterra_software or get_capterra_product."),
39
+ url: z.string().min(1).max(1000).optional()
40
+ .describe("Full Capterra reviews URL. Passing back reviews_url from get_capterra_product is the most reliable way to page. Either this or product_id is required."),
41
+ page: z.number().int().min(1).max(100).optional()
42
+ .describe("Page number, 1-100. 25 reviews per page. 100 is a hard cap whatever the review count says - past it Capterra serves page one again."),
43
+ }, async (params) => {
44
+ try {
45
+ const data = await getClient().post("/api/v1/capterra/reviews", params);
46
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
47
+ }
48
+ catch (err) {
49
+ return handleApiError(err);
50
+ }
51
+ });
52
+ }
53
+ //# sourceMappingURL=capterra.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capterra.js","sourceRoot":"","sources":["../../src/tools/capterra.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ScavioClient } from "../lib/client.js";
3
+ export declare function registerCompaniesHouseTools(server: McpServer, getClient: () => ScavioClient): void;
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+ import { handleApiError } from "../lib/tool-error.js";
3
+ // Companies House - the official UK company register. Four tools, 1 credit
4
+ // each, flat.
5
+ //
6
+ // LOOKUP FIRST: search_companies_house is the only entry point that takes a
7
+ // name; everything else is keyed by company_number. company_number is
8
+ // deliberately loose - the transport zero-pads and upper-cases, because the
9
+ // register 404s on /company/445790 and /company/sc090312 for companies that
10
+ // plainly exist.
11
+ //
12
+ // Paging is NOT uniform and the differences are billable. Search is capped at
13
+ // page 50 (the register serves a 1000-result window per term whatever hit count
14
+ // it prints, then answers page 51 with an HTTP 416). Officers and filing
15
+ // history have NO upper bound: past the last page the register returns an
16
+ // ordinary 200 with an empty list, which is byte-identical to a company that
17
+ // has no officers at all - so an empty page is not proof of a bad page number.
18
+ //
19
+ // No zod `.default()` anywhere in this file, deliberately: the MCP SDK applies
20
+ // a zod default BEFORE the handler runs, so a declared default is posted on
21
+ // every call. The backend applies its own defaults when a field is absent;
22
+ // they are documented in the .describe() text instead.
23
+ const companyNumber = z.string().min(1).max(20)
24
+ .describe("UK company number, from search_companies_house. Zero-padded and upper-cased for you, so '445790', '00445790' and 'sc090312' all resolve. Registry prefixes are supported: SC (Scotland), NI (Northern Ireland), OC/SO/NC (LLPs), FC (overseas), BR (UK establishment), CE (charitable incorporated organisation).");
25
+ export function registerCompaniesHouseTools(server, getClient) {
26
+ const call = (path) => async (params) => {
27
+ try {
28
+ const data = await getClient().post(path, params);
29
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30
+ }
31
+ catch (err) {
32
+ return handleApiError(err);
33
+ }
34
+ };
35
+ server.tool("search_companies_house", `START HERE for Companies House, the official UK company register. Search it by company name or number and get back, as JSON, the company_number that every other Companies House tool is keyed by, plus company name, status (active, dissolved, liquidation), incorporation or dissolution date, registered office address and any matched former names. Search matches CURRENT AND FORMER names, so a company that rebranded is still findable under its old one. Paginated with page at 20 results per page and CAPPED AT PAGE 50: the register serves only a 1000-result window per term however many matches it claims (it will report 10,000 for a broad term and then answer page 51 with an HTTP 416), so narrow the query rather than paging further. UK register only - it holds nothing about US or other non-UK companies. Costs 1 credit.`, {
36
+ query: z.string().min(1).max(200)
37
+ .describe("Company name or number to search for, e.g. 'tesco'. Must not be blank. Matches current AND former names."),
38
+ page: z.number().int().min(1).max(50)
39
+ .optional()
40
+ .describe("Results page, 1-based, 20 per page. Backend default is 1. Hard cap of 50 - the register only serves the first 1000 matches for a term."),
41
+ }, call("/api/v1/companieshouse/search"));
42
+ server.tool("get_companies_house_company", `Get a UK company's full entry on the Companies House register as JSON: status, company type, incorporation and dissolution dates, registered office address, SIC industry codes, previous names, accounts and confirmation-statement due dates with their overdue flags, and whether it has charges, insolvency history, officers or UK establishments. Overseas companies (FC) also return home registry, legal form and governing law; UK establishments (BR) return the parent company; charitable incorporated organisations (CE) return the charity number. Keyed by company_number - call search_companies_house first if you only have a name. Not paginated. Costs 1 credit.`, {
43
+ company_number: companyNumber,
44
+ }, call("/api/v1/companieshouse/company"));
45
+ server.tool("get_companies_house_officers", `List a UK company's officers, current and resigned, from the Companies House register as JSON: name, officer role (director, secretary, LLP member), appointment and resignation dates, correspondence address, nationality, country of residence, month-and-year date of birth, and identity-verification status. There is NO server-side active/resigned filter - the register's own is client-side, so filter on each officer's \`status\` in the response. officers_count is every appointment ever made and resignations_count how many ended, so active officers = the difference. Paginated with page at 35 per page with NO upper page bound: past the last page the register returns an ordinary 200 with an empty list, identical to a company that has no officers, so stop when a page comes back empty. Keyed by company_number from search_companies_house. Costs 1 credit.`, {
46
+ company_number: companyNumber,
47
+ page: z.number().int().min(1).optional()
48
+ .describe("Results page, 1-based, 35 officers per page. Backend default is 1. No upper bound - a page past the end is a 200 with an empty list, not an error."),
49
+ }, call("/api/v1/companieshouse/officers"));
50
+ server.tool("get_companies_house_filing_history", `List a UK company's filings from the Companies House register as JSON, most recent first: filing date, filing type code (AA annual accounts, CS01 confirmation statement, SH03 share buyback), description, register annotations and child documents, and a link to the filed PDF with its page count. A filing the register has not finished processing carries a processing_note instead of a document link. Paginated with page and NO upper page bound: past the last page it is an ordinary 200 with an empty list, so stop when a page comes back empty. Keyed by company_number from search_companies_house. Costs 1 credit.`, {
51
+ company_number: companyNumber,
52
+ page: z.number().int().min(1).optional()
53
+ .describe("Results page, 1-based. Backend default is 1. No upper bound - a page past the end is a 200 with an empty list, not an error."),
54
+ }, call("/api/v1/companieshouse/filing-history"));
55
+ }
56
+ //# sourceMappingURL=companieshouse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"companieshouse.js","sourceRoot":"","sources":["../../src/tools/companieshouse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,2EAA2E;AAC3E,cAAc;AACd,EAAE;AACF,4EAA4E;AAC5E,sEAAsE;AACtE,4EAA4E;AAC5E,4EAA4E;AAC5E,iBAAiB;AACjB,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,yEAAyE;AACzE,0EAA0E;AAC1E,6EAA6E;AAC7E,+EAA+E;AAC/E,EAAE;AACF,+EAA+E;AAC/E,4EAA4E;AAC5E,2EAA2E;AAC3E,uDAAuD;AAEvD,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;KAC5C,QAAQ,CAAC,mTAAmT,CAAC,CAAC;AAEjU,MAAM,UAAU,2BAA2B,CAAC,MAAiB,EAAE,SAA6B;IAC1F,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,EAAE,MAA+B,EAAE,EAAE;QACvE,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,SAAS,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAClD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QACvF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,cAAc,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,wBAAwB,EACxB,wzBAAwzB,EACxzB;QACE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;aAC9B,QAAQ,CAAC,0GAA0G,CAAC;QACvH,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;aAClC,QAAQ,EAAE;aACV,QAAQ,CAAC,wIAAwI,CAAC;KACtJ,EACD,IAAI,CAAC,+BAA+B,CAAC,CACtC,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,6BAA6B,EAC7B,spBAAspB,EACtpB;QACE,cAAc,EAAE,aAAa;KAC9B,EACD,IAAI,CAAC,gCAAgC,CAAC,CACvC,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,8BAA8B,EAC9B,21BAA21B,EAC31B;QACE,cAAc,EAAE,aAAa;QAC7B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;aACrC,QAAQ,CAAC,oJAAoJ,CAAC;KAClK,EACD,IAAI,CAAC,iCAAiC,CAAC,CACxC,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,oCAAoC,EACpC,qmBAAqmB,EACrmB;QACE,cAAc,EAAE,aAAa;QAC7B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;aACrC,QAAQ,CAAC,8HAA8H,CAAC;KAC5I,EACD,IAAI,CAAC,uCAAuC,CAAC,CAC9C,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ScavioClient } from "../lib/client.js";
3
+ export declare function registerEbayTools(server: McpServer, getClient: () => ScavioClient): void;