listingbureau-mcp 0.1.6 → 0.1.8

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 CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  Organic ranking campaigns for Amazon products, managed through your AI assistant.
10
10
 
11
- The only ranking-focused Amazon MCP server. Create ranking campaigns, set velocity schedules, track keyword positions, and monitor rank movement from Claude, Cursor, or any MCP-compatible client.
11
+ The only ranking-focused Amazon MCP server. Create ranking campaigns, set campaign schedules, track keyword positions, and monitor rank movement from Claude, Cursor, or any MCP-compatible client.
12
12
 
13
- Built on the same infrastructure that moved 1,700+ products to page one. Median time: 48 days.
13
+ Built on the same infrastructure associated with 1,700+ products reaching page one. Median time: 48 days. Past performance does not predict future results.
14
14
 
15
15
  <p align="center">
16
16
  <img src="assets/demo.gif" alt="listingbureau-mcp demo" width="800" />
@@ -35,10 +35,10 @@ Built on the same infrastructure that moved 1,700+ products to page one. Median
35
35
  This MCP server connects your AI assistant to [Listing Bureau](https://listingbureau.com/mcp?utm_source=github&utm_medium=readme&utm_campaign=mcp)'s Amazon ranking infrastructure. Three signal types drive organic rank improvement:
36
36
 
37
37
  - **Search-Find-Buy (SFB)** signals the A10 algorithm that real shoppers searched for a keyword, found the product, and purchased it
38
- - **Add-to-Cart (ATC)** builds conversion velocity and purchase intent signals
38
+ - **Add-to-Cart (ATC)** builds conversion intent and purchase intent signals
39
39
  - **Page Views (PGV)** generate session-level browsing behavior that supports organic discovery
40
40
 
41
- Your assistant handles the entire workflow: create a project for any ASIN and keyword, set a daily velocity schedule, estimate costs before committing, and pull position reports to track movement.
41
+ Your assistant handles the entire workflow: create a project for any ASIN and keyword, set a daily campaign schedule, estimate costs before committing, and pull position reports to track movement.
42
42
 
43
43
  ## 📦 Quick start
44
44
 
@@ -164,7 +164,7 @@ Create an account at [listingbureau.com](https://listingbureau.com/mcp?utm_sourc
164
164
 
165
165
  | Tool | Description |
166
166
  |------|-------------|
167
- | `lb_feedback_submit` | Submit feedback (10-5000 characters) |
167
+ | `lb_feedback_submit` | Submit feedback, feature requests, or suggestions (10-5000 characters) |
168
168
 
169
169
  ## 🛠️ Development
170
170
 
package/dist/index.js CHANGED
@@ -33,6 +33,22 @@ const client = new LBClient(apiKey, baseUrl);
33
33
  const server = new McpServer({
34
34
  name: "listingbureau",
35
35
  version: pkg.version,
36
+ }, {
37
+ instructions: [
38
+ "Listing Bureau provides Amazon organic ranking services: SFB (Search Find Buy), ATC (Add to Cart), and PGV (page views).",
39
+ "SFB drives purchases that directly boost organic ranking. ATC and PGV support the funnel but alone rarely move ranking unless the keyword is very uncompetitive.",
40
+ "When a user schedules only ATC/PGV with no SFB, note that these are typically paired with SFB to maintain a healthy conversion ratio.",
41
+ "",
42
+ "When advising on service volumes, aim for a natural Amazon funnel with realistic conversion rates (10-15%). These are starting points — adjust based on the user's product, price point, and keyword competitiveness:",
43
+ "Default ratio per SFB: ~2 ATC, ~8-10 PGV.",
44
+ "Cheaper/high-intent keywords (e.g. branded): ~1-1.5 ATC, ~5-7 PGV per SFB.",
45
+ "Expensive/competitive keywords: ~2-3 ATC, ~10-15 PGV per SFB to reflect more browsing.",
46
+ "Avoid SFB-only schedules with near-100% conversion. Keep ratios consistent per keyword so traffic looks like natural shopper behavior.",
47
+ "",
48
+ "When the user asks about something outside current capabilities (other marketplaces, unsupported features), offer to submit their input as feedback via lb_feedback_submit.",
49
+ "",
50
+ "SFB units are real Amazon purchases of the user's product. The retail price in the SFB cost is not a net expense — the seller receives it back as normal Amazon sale proceeds. The actual out-of-pocket cost per SFB is the service fee plus the 11% tax/transfer overhead on the retail price.",
51
+ ].join("\n"),
36
52
  });
37
53
  registerAccountTools(server, client);
38
54
  registerWalletTools(server, client);
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { sfbUnitCost, estimateCost, round2 } from "../utils/cost.js";
3
3
  import { formatResult, formatErrorResult } from "../utils/response.js";
4
+ import { ACCEPTED_REGIONS, normalizeRegion, assertSfbAllowed } from "../utils/regions.js";
4
5
  const scheduleItemSchema = z.object({
5
6
  date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format").describe("Date in YYYY-MM-DD format"),
6
7
  atc: z.number().int().min(0).optional().describe("Add-to-cart volume (default 0)"),
@@ -8,9 +9,13 @@ const scheduleItemSchema = z.object({
8
9
  pgv: z.number().int().min(0).optional().describe("Page view volume (default 0)"),
9
10
  });
10
11
  const estimateCostShape = {
11
- atc: z.number().int().min(0).optional().describe("Uniform daily add-to-cart volume"),
12
- sfb: z.number().int().min(0).optional().describe("Uniform daily Search Find Buy (SFB) volume"),
13
- pgv: z.number().int().min(0).optional().describe("Uniform daily page view volume"),
12
+ region: z
13
+ .enum(ACCEPTED_REGIONS)
14
+ .optional()
15
+ .describe("Amazon region code — if provided with SFB volumes, validates SFB is allowed (US only). GB accepted as alias for UK."),
16
+ atc: z.number().int().min(0).optional().describe("Uniform daily add-to-cart volume (all regions; lower execution rate outside US)"),
17
+ sfb: z.number().int().min(0).optional().describe("Uniform daily Search Find Buy (SFB) volume (US-region projects only)"),
18
+ pgv: z.number().int().min(0).optional().describe("Uniform daily page view volume (all regions; lower execution rate outside US)"),
14
19
  num_days: z.number().int().min(1).max(365).optional().describe("Number of days for uniform volumes"),
15
20
  schedule: z
16
21
  .array(scheduleItemSchema)
@@ -25,7 +30,7 @@ const estimateCostShape = {
25
30
  .describe("Product retail price in USD — needed for accurate SFB cost including product price and fees"),
26
31
  };
27
32
  export function registerCostTools(server, client) {
28
- server.tool("lb_estimate_cost", "Estimate campaign cost before committing. Fetches current rates and wallet balance, then computes total cost, daily averages, and wallet sustainability. Provide either uniform daily volumes (atc/sfb/pgv + num_days) or a per-day schedule array. Include retail_price for accurate SFB costs.", estimateCostShape, { readOnlyHint: true }, async (params) => {
33
+ server.tool("lb_estimate_cost", "Estimate campaign cost before committing. Fetches current rates and wallet balance, then computes total cost, daily averages, and wallet sustainability. Provide either uniform daily volumes (atc/sfb/pgv + num_days) or a per-day schedule array. Include retail_price for accurate SFB costs. SFB is US-region only; ATC/PGV work in all regions (lower execution rate outside US). Pass region to validate SFB eligibility.", estimateCostShape, { readOnlyHint: true }, async (params) => {
29
34
  try {
30
35
  // Validate: either schedule array, or (volumes + num_days)
31
36
  const hasSchedule = params.schedule && params.schedule.length > 0;
@@ -33,6 +38,16 @@ export function registerCostTools(server, client) {
33
38
  if (!hasSchedule && !(hasVolume && params.num_days != null)) {
34
39
  return formatErrorResult(new Error("Provide either a 'schedule' array, OR at least one volume (atc/sfb/pgv > 0) with 'num_days'"));
35
40
  }
41
+ // Normalize region once for all downstream use
42
+ const normalizedRegion = params.region ? normalizeRegion(params.region) : undefined;
43
+ // SFB region validation
44
+ const hasSfbInput = params.schedule
45
+ ? params.schedule.some((d) => (d.sfb ?? 0) > 0)
46
+ : (params.sfb ?? 0) > 0;
47
+ if (normalizedRegion) {
48
+ // Hard reject: region provided + non-US + has SFB
49
+ assertSfbAllowed(normalizedRegion, hasSfbInput);
50
+ }
36
51
  const [ratesRes, walletRes] = await Promise.all([
37
52
  client.request("GET", "/api/v1/account/service-rates", undefined, undefined, "lb_estimate_cost"),
38
53
  client.request("GET", "/api/v1/wallet", undefined, undefined, "lb_estimate_cost"),
@@ -72,8 +87,12 @@ export function registerCostTools(server, client) {
72
87
  if (hasSchedule && (hasVolume || params.num_days != null)) {
73
88
  warnings.push("schedule array provided — num_days and uniform volumes (atc/sfb/pgv) were ignored.");
74
89
  }
75
- // SFB without retail_price warning (0 is effectively the same as omitting)
90
+ // SFB without region warning
76
91
  const hasSfb = schedule.some((d) => d.sfb > 0);
92
+ if (hasSfb && !params.region) {
93
+ warnings.push("SFB volumes provided without region — estimate assumes US pricing. Pass region to validate SFB eligibility.");
94
+ }
95
+ // SFB without retail_price warning (0 is effectively the same as omitting)
77
96
  if (hasSfb && (params.retail_price == null || params.retail_price === 0)) {
78
97
  warnings.push("SFB volumes provided without retail_price — estimate uses service fee only ($" +
79
98
  rates.sfb_service_fee.toFixed(2) +
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { formatResult, formatErrorResult } from "../utils/response.js";
3
3
  export function registerFeedbackTools(server, client) {
4
- server.tool("lb_feedback_submit", "Submit feedback about the Listing Bureau service (10-5000 characters).", {
4
+ server.tool("lb_feedback_submit", "Submit feedback, feature requests, or suggestions (10-5000 characters)", {
5
5
  feedback: z
6
6
  .string()
7
7
  .min(10)
@@ -1,25 +1,18 @@
1
1
  import { z } from "zod";
2
2
  import { formatResult, formatErrorResult } from "../utils/response.js";
3
- // Valid Amazon region codes (validated against CONSTANTS['MARKETPLACES'] in backend)
4
- const VALID_REGIONS = [
5
- "US", "CA", "MX", // Americas
6
- "UK", "DE", "FR", "IT", // Europe
7
- "ES", "NL", "SE", "TR", // Europe cont.
8
- "JP", "AU", "IN", // Asia-Pacific
9
- "AE", "BR", "SG", "SA", // Rest of world
10
- ];
3
+ import { ACCEPTED_REGIONS, normalizeRegion } from "../utils/regions.js";
11
4
  export function registerProjectsTools(server, client) {
12
5
  server.tool("lb_projects_list", "List Listing Bureau Amazon projects with optional filters. Returns ASIN, keyword, active status, and current service volumes.", {
13
6
  region: z
14
- .enum(VALID_REGIONS)
7
+ .enum(ACCEPTED_REGIONS)
15
8
  .optional()
16
- .describe("Filter by Amazon region"),
9
+ .describe("Filter by Amazon region code (GB accepted as alias for UK)"),
17
10
  active: z.boolean().optional().describe("Filter by active status"),
18
11
  }, { readOnlyHint: true }, async (params) => {
19
12
  try {
20
13
  const query = { marketplace: "amazon" };
21
14
  if (params.region !== undefined)
22
- query.region = params.region;
15
+ query.region = normalizeRegion(params.region);
23
16
  if (params.active !== undefined)
24
17
  query.active = String(params.active);
25
18
  const res = await client.request("GET", "/api/v1/projects", undefined, query, "lb_projects_list");
@@ -31,8 +24,8 @@ export function registerProjectsTools(server, client) {
31
24
  });
32
25
  server.tool("lb_projects_create", "Create a new Listing Bureau Amazon project. If a project with the same ASIN+keyword+region was previously archived, it will be reactivated instead (returns 201). ASIN must be a valid Amazon ASIN. Keyword must be 3-200 characters.", {
33
26
  region: z
34
- .enum(VALID_REGIONS)
35
- .describe("Amazon region code"),
27
+ .enum(ACCEPTED_REGIONS)
28
+ .describe("Amazon region code (GB accepted as alias for UK)"),
36
29
  asin: z
37
30
  .string()
38
31
  .describe("Amazon ASIN (e.g. 'B01MTJK06C')"),
@@ -50,7 +43,7 @@ export function registerProjectsTools(server, client) {
50
43
  try {
51
44
  const body = {
52
45
  marketplace: "amazon",
53
- region: params.region,
46
+ region: normalizeRegion(params.region),
54
47
  asin: params.asin,
55
48
  keyword: params.keyword,
56
49
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { formatResult, formatErrorResult } from "../utils/response.js";
3
+ import { assertSfbAllowed, SfbRegionError } from "../utils/regions.js";
3
4
  import { sfbUnitCost, estimateCost, mapScheduleEntries, round2 } from "../utils/cost.js";
4
5
  // Write schema: only YYYY-MM-DD dates (backend manages 'ongoing' entries internally)
5
6
  const scheduleEntrySchema = z.object({
@@ -7,9 +8,9 @@ const scheduleEntrySchema = z.object({
7
8
  .string()
8
9
  .regex(/^\d{4}-\d{2}-\d{2}$/, "Must be YYYY-MM-DD format")
9
10
  .describe("Date in YYYY-MM-DD format"),
10
- atc: z.number().int().min(0).describe("Add-to-cart volume"),
11
- sfb: z.number().int().min(0).describe("Search Find Buy (SFB) volume"),
12
- pgv: z.number().int().min(0).describe("Page view volume"),
11
+ atc: z.number().int().min(0).describe("Add-to-cart volume (all regions; lower execution rate outside US)"),
12
+ sfb: z.number().int().min(0).describe("Search Find Buy (SFB) volume (US-region projects only)"),
13
+ pgv: z.number().int().min(0).describe("Page view volume (all regions; lower execution rate outside US)"),
13
14
  });
14
15
  /**
15
16
  * Best-effort: fetch rates and compute cost summary for a schedule response.
@@ -25,7 +26,8 @@ async function appendCostSummary(data, client) {
25
26
  if (dated.length === 0 && !ongoing)
26
27
  return result;
27
28
  const sfbNote = "SFB costs use service fee only ($" + rates.sfb_service_fee.toFixed(2) +
28
- "), matching backend balance check behavior. Use lb_estimate_cost with retail_price for full SFB cost.";
29
+ "), matching backend balance check behavior. Use lb_estimate_cost with retail_price for full SFB cost." +
30
+ " Note: the retail price portion is not a net expense — it returns as Amazon sale proceeds.";
29
31
  if (dated.length > 0 && ongoing) {
30
32
  // Mixed: dated entries + ongoing
31
33
  const est = estimateCost(dated, rates);
@@ -99,7 +101,7 @@ async function appendCostSummary(data, client) {
99
101
  return result;
100
102
  }
101
103
  export function registerScheduleTools(server, client) {
102
- server.tool("lb_schedule_get", "Get the current schedule for a Listing Bureau project. Shows per-day service volumes (atc, sfb, pgv).", {
104
+ server.tool("lb_schedule_get", "Get the current schedule for a Listing Bureau project. Shows per-day service volumes (atc, sfb, pgv). Note: SFB is only available for US-region projects.", {
103
105
  ui_id: z.string().describe("Project unique identifier"),
104
106
  }, { readOnlyHint: true }, async (params) => {
105
107
  try {
@@ -111,7 +113,7 @@ export function registerScheduleTools(server, client) {
111
113
  return formatErrorResult(e);
112
114
  }
113
115
  });
114
- server.tool("lb_schedule_set", "Set the full per-day schedule for a Listing Bureau project. Replaces any existing schedule. Each entry represents one day with volumes for atc, sfb, and pgv. Max 365 entries.", {
116
+ server.tool("lb_schedule_set", "Set the full per-day schedule for a Listing Bureau project. Replaces any existing schedule. Each entry represents one day with volumes for atc, sfb, and pgv. Max 365 entries. SFB is US-region only; ATC/PGV work in all regions (lower execution rate outside US).", {
115
117
  ui_id: z.string().describe("Project unique identifier"),
116
118
  schedule: z
117
119
  .array(scheduleEntrySchema)
@@ -120,21 +122,64 @@ export function registerScheduleTools(server, client) {
120
122
  .describe("Array of daily schedule entries"),
121
123
  }, { destructiveHint: true, idempotentHint: true }, async (params) => {
122
124
  try {
125
+ // SFB region gate: only fetch project when schedule contains SFB > 0
126
+ let regionWarning;
127
+ if (params.schedule.some((e) => e.sfb > 0)) {
128
+ try {
129
+ const projRes = await client.request("GET", `/api/v1/projects/${encodeURIComponent(params.ui_id)}`, undefined, undefined, "lb_schedule_set");
130
+ assertSfbAllowed(projRes.data.region, true);
131
+ }
132
+ catch (fetchErr) {
133
+ // If assertSfbAllowed threw, re-throw (it's a validation error, not a fetch failure)
134
+ if (fetchErr instanceof SfbRegionError) {
135
+ throw fetchErr;
136
+ }
137
+ // Fetch failure: warn and proceed — backend enforces the real restriction
138
+ regionWarning = "Could not verify project region for SFB eligibility. The backend will enforce region restrictions.";
139
+ }
140
+ }
123
141
  const res = await client.request("PUT", `/api/v1/projects/${encodeURIComponent(params.ui_id)}/schedule`, { schedule: params.schedule }, undefined, "lb_schedule_set");
124
142
  const augmented = await appendCostSummary(res.data, client);
143
+ if (regionWarning) {
144
+ augmented.region_warning = regionWarning;
145
+ }
146
+ // Note when schedule has ATC/PGV but no SFB
147
+ const hasAtcOrPgv = params.schedule.some((e) => e.atc > 0 || e.pgv > 0);
148
+ const hasSfb = params.schedule.some((e) => e.sfb > 0);
149
+ if (hasAtcOrPgv && !hasSfb) {
150
+ augmented.service_note =
151
+ "This schedule has ATC/PGV but no SFB. " +
152
+ "These services alone rarely move ranking unless the keyword is very uncompetitive. " +
153
+ "They're typically paired with SFB to maintain a healthy organic conversion ratio. " +
154
+ "You can update the schedule to add SFB if needed.";
155
+ }
125
156
  return formatResult(augmented);
126
157
  }
127
158
  catch (e) {
128
159
  return formatErrorResult(e);
129
160
  }
130
161
  });
131
- server.tool("lb_schedule_quick_set", "Quick-set uniform daily volumes for a Listing Bureau project. WARNING: This clears any existing per-day schedule and replaces it with uniform values. All omitted fields default to 0.", {
162
+ server.tool("lb_schedule_quick_set", "Quick-set uniform daily volumes for a Listing Bureau project. WARNING: This clears any existing per-day schedule and replaces it with uniform values. All omitted fields default to 0. SFB is US-region only; ATC/PGV work in all regions (lower execution rate outside US).", {
132
163
  ui_id: z.string().describe("Project unique identifier"),
133
- atc: z.number().int().min(0).optional().describe("Add-to-cart volume per day (default 0)"),
134
- sfb: z.number().int().min(0).optional().describe("Search Find Buy (SFB) volume per day (default 0)"),
135
- pgv: z.number().int().min(0).optional().describe("Page view volume per day (default 0)"),
164
+ atc: z.number().int().min(0).optional().describe("Add-to-cart volume per day (default 0) (all regions; lower execution rate outside US)"),
165
+ sfb: z.number().int().min(0).optional().describe("Search Find Buy (SFB) volume per day (default 0) (US-region projects only)"),
166
+ pgv: z.number().int().min(0).optional().describe("Page view volume per day (default 0) (all regions; lower execution rate outside US)"),
136
167
  }, { destructiveHint: true, idempotentHint: true }, async (params) => {
137
168
  try {
169
+ // SFB region gate: only fetch project when SFB > 0
170
+ let regionWarning;
171
+ if ((params.sfb ?? 0) > 0) {
172
+ try {
173
+ const projRes = await client.request("GET", `/api/v1/projects/${encodeURIComponent(params.ui_id)}`, undefined, undefined, "lb_schedule_quick_set");
174
+ assertSfbAllowed(projRes.data.region, true);
175
+ }
176
+ catch (fetchErr) {
177
+ if (fetchErr instanceof SfbRegionError) {
178
+ throw fetchErr;
179
+ }
180
+ regionWarning = "Could not verify project region for SFB eligibility. The backend will enforce region restrictions.";
181
+ }
182
+ }
138
183
  const body = {
139
184
  atc: params.atc ?? 0,
140
185
  sfb: params.sfb ?? 0,
@@ -142,6 +187,19 @@ export function registerScheduleTools(server, client) {
142
187
  };
143
188
  const res = await client.request("POST", `/api/v1/projects/${encodeURIComponent(params.ui_id)}/schedule/quick`, body, undefined, "lb_schedule_quick_set");
144
189
  const augmented = await appendCostSummary(res.data, client);
190
+ if (regionWarning) {
191
+ augmented.region_warning = regionWarning;
192
+ }
193
+ // Note when schedule has ATC/PGV but no SFB
194
+ const hasAtcOrPgv = (params.atc ?? 0) > 0 || (params.pgv ?? 0) > 0;
195
+ const hasSfb = (params.sfb ?? 0) > 0;
196
+ if (hasAtcOrPgv && !hasSfb) {
197
+ augmented.service_note =
198
+ "This schedule has ATC/PGV but no SFB. " +
199
+ "These services alone rarely move ranking unless the keyword is very uncompetitive. " +
200
+ "They're typically paired with SFB to maintain a healthy organic conversion ratio. " +
201
+ "You can update the schedule to add SFB if needed.";
202
+ }
145
203
  return formatResult(augmented);
146
204
  }
147
205
  catch (e) {
@@ -0,0 +1,24 @@
1
+ /** Canonical 18 backend region codes (matches CONSTANTS['MARKETPLACES'] in Flask). */
2
+ export declare const VALID_REGIONS: readonly ["US", "CA", "MX", "UK", "DE", "FR", "IT", "ES", "NL", "SE", "TR", "JP", "AU", "IN", "AE", "BR", "SG", "SA"];
3
+ /**
4
+ * All accepted region codes for MCP tool input (19-element tuple).
5
+ * Includes GB as an alias for UK. Declared as a flat literal tuple
6
+ * because z.enum() requires a literal non-empty tuple type.
7
+ */
8
+ export declare const ACCEPTED_REGIONS: readonly ["US", "CA", "MX", "UK", "DE", "FR", "IT", "ES", "NL", "SE", "TR", "JP", "AU", "IN", "AE", "BR", "SG", "SA", "GB"];
9
+ /** Regions where SFB (Search Find Buy) is available. */
10
+ export declare const SFB_REGIONS: Set<"US" | "CA" | "MX" | "UK" | "DE" | "FR" | "IT" | "ES" | "NL" | "SE" | "TR" | "JP" | "AU" | "IN" | "AE" | "BR" | "SG" | "SA">;
11
+ /** Normalize user-supplied region code: GB -> UK, all others passthrough. */
12
+ export declare function normalizeRegion(region: string): string;
13
+ /** Typed error thrown by assertSfbAllowed for non-US SFB requests. */
14
+ export declare class SfbRegionError extends Error {
15
+ constructor(region: string);
16
+ }
17
+ /**
18
+ * Assert that SFB is allowed for the given region.
19
+ * Throws SfbRegionError if the region is not in SFB_REGIONS.
20
+ *
21
+ * @param region - Canonical region code (already normalized)
22
+ * @param hasSfb - Whether the request includes SFB volumes > 0
23
+ */
24
+ export declare function assertSfbAllowed(region: string, hasSfb: boolean): void;
@@ -0,0 +1,47 @@
1
+ // Centralized region constants and validation for Listing Bureau MCP tools.
2
+ /** Canonical 18 backend region codes (matches CONSTANTS['MARKETPLACES'] in Flask). */
3
+ export const VALID_REGIONS = [
4
+ "US", "CA", "MX", // Americas
5
+ "UK", "DE", "FR", "IT", // Europe
6
+ "ES", "NL", "SE", "TR", // Europe cont.
7
+ "JP", "AU", "IN", // Asia-Pacific
8
+ "AE", "BR", "SG", "SA", // Rest of world
9
+ ];
10
+ /**
11
+ * All accepted region codes for MCP tool input (19-element tuple).
12
+ * Includes GB as an alias for UK. Declared as a flat literal tuple
13
+ * because z.enum() requires a literal non-empty tuple type.
14
+ */
15
+ export const ACCEPTED_REGIONS = [
16
+ "US", "CA", "MX",
17
+ "UK", "DE", "FR", "IT",
18
+ "ES", "NL", "SE", "TR",
19
+ "JP", "AU", "IN",
20
+ "AE", "BR", "SG", "SA",
21
+ "GB",
22
+ ];
23
+ /** Regions where SFB (Search Find Buy) is available. */
24
+ export const SFB_REGIONS = new Set(["US"]);
25
+ /** Normalize user-supplied region code: GB -> UK, all others passthrough. */
26
+ export function normalizeRegion(region) {
27
+ return region === "GB" ? "UK" : region;
28
+ }
29
+ /** Typed error thrown by assertSfbAllowed for non-US SFB requests. */
30
+ export class SfbRegionError extends Error {
31
+ constructor(region) {
32
+ super(`SFB (Search Find Buy) is only available for US-region projects. This project's region is ${region}. Remove SFB volumes or use a US-region project.`);
33
+ this.name = "SfbRegionError";
34
+ }
35
+ }
36
+ /**
37
+ * Assert that SFB is allowed for the given region.
38
+ * Throws SfbRegionError if the region is not in SFB_REGIONS.
39
+ *
40
+ * @param region - Canonical region code (already normalized)
41
+ * @param hasSfb - Whether the request includes SFB volumes > 0
42
+ */
43
+ export function assertSfbAllowed(region, hasSfb) {
44
+ if (hasSfb && !SFB_REGIONS.has(region)) {
45
+ throw new SfbRegionError(region);
46
+ }
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "listingbureau-mcp",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Amazon organic ranking MCP server. Run ranking campaigns, track keyword positions, and monitor rank movement from any AI assistant.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",