listingbureau-mcp 0.1.5 → 0.1.6

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.
@@ -30,6 +30,6 @@ export declare class LBClient {
30
30
  * Make an authenticated API request.
31
31
  * Retries once on 401 (token expired mid-request).
32
32
  */
33
- request<T>(method: string, path: string, body?: Record<string, unknown>, query?: Record<string, string>): Promise<ApiSuccessResponse<T>>;
33
+ request<T>(method: string, path: string, body?: Record<string, unknown>, query?: Record<string, string>, toolName?: string): Promise<ApiSuccessResponse<T>>;
34
34
  private doRequest;
35
35
  }
@@ -153,14 +153,14 @@ export class LBClient {
153
153
  * Make an authenticated API request.
154
154
  * Retries once on 401 (token expired mid-request).
155
155
  */
156
- async request(method, path, body, query) {
156
+ async request(method, path, body, query, toolName) {
157
157
  await this.ensureAuth();
158
- const response = await this.doRequest(method, path, body, query);
158
+ const response = await this.doRequest(method, path, body, query, toolName);
159
159
  // Single retry on 401
160
160
  if (response.status === "error" && response._statusCode === 401) {
161
161
  this.jwt = null;
162
162
  await this.ensureAuth();
163
- const retry = await this.doRequest(method, path, body, query);
163
+ const retry = await this.doRequest(method, path, body, query, toolName);
164
164
  if (retry.status === "error") {
165
165
  throw new LBApiError(retry._statusCode ?? 500, retry.error.code, retry.error.message);
166
166
  }
@@ -171,7 +171,7 @@ export class LBClient {
171
171
  }
172
172
  return response;
173
173
  }
174
- async doRequest(method, path, body, query) {
174
+ async doRequest(method, path, body, query, toolName) {
175
175
  let url = `${this.baseUrl}${path}`;
176
176
  if (query) {
177
177
  const params = new URLSearchParams(Object.entries(query).filter(([, v]) => v !== undefined && v !== ""));
@@ -181,6 +181,8 @@ export class LBClient {
181
181
  }
182
182
  const headers = {
183
183
  Authorization: `Bearer ${this.jwt.access_token}`,
184
+ "X-LB-Source": "mcp",
185
+ ...(toolName && { "X-LB-Tool": toolName }),
184
186
  };
185
187
  const options = { method, headers };
186
188
  if (body && method !== "GET") {
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { registerOrdersTools } from "./tools/orders.tools.js";
10
10
  import { registerFeedbackTools } from "./tools/feedback.tools.js";
11
11
  import { registerCostTools } from "./tools/cost.tools.js";
12
12
  import { validateBaseUrl } from "./utils/validate-url.js";
13
+ import { checkForUpdate } from "./utils/update-check.js";
13
14
  import { createRequire } from "node:module";
14
15
  const require = createRequire(import.meta.url);
15
16
  const pkg = require("../package.json");
@@ -42,3 +43,4 @@ registerFeedbackTools(server, client);
42
43
  registerCostTools(server, client);
43
44
  const transport = new StdioServerTransport();
44
45
  await server.connect(transport);
46
+ checkForUpdate(pkg.version).catch(() => { });
@@ -4,8 +4,8 @@ export function registerAccountTools(server, client) {
4
4
  server.tool("lb_account_get", "Get Listing Bureau account info (email, name, account status, wallet balance)", {}, { readOnlyHint: true }, async () => {
5
5
  try {
6
6
  const [accountRes, walletRes] = await Promise.all([
7
- client.request("GET", "/api/v1/account"),
8
- client.request("GET", "/api/v1/wallet"),
7
+ client.request("GET", "/api/v1/account", undefined, undefined, "lb_account_get"),
8
+ client.request("GET", "/api/v1/wallet", undefined, undefined, "lb_account_get"),
9
9
  ]);
10
10
  const account = accountRes.data;
11
11
  const wallet = walletRes.data;
@@ -55,7 +55,7 @@ export function registerAccountTools(server, client) {
55
55
  if (Object.keys(body).length === 0) {
56
56
  return formatErrorResult(new Error("At least one field (first_name, last_name, company) must be provided"));
57
57
  }
58
- const res = await client.request("PATCH", "/api/v1/account/profile", body);
58
+ const res = await client.request("PATCH", "/api/v1/account/profile", body, undefined, "lb_account_update_profile");
59
59
  return formatResult(res.data);
60
60
  }
61
61
  catch (e) {
@@ -64,7 +64,7 @@ export function registerAccountTools(server, client) {
64
64
  });
65
65
  server.tool("lb_account_get_service_rates", "Get current Listing Bureau service pricing rates. Returns empty object if no plan is active.", {}, { readOnlyHint: true }, async () => {
66
66
  try {
67
- const res = await client.request("GET", "/api/v1/account/service-rates");
67
+ const res = await client.request("GET", "/api/v1/account/service-rates", undefined, undefined, "lb_account_get_service_rates");
68
68
  return formatResult(res.data);
69
69
  }
70
70
  catch (e) {
@@ -73,7 +73,7 @@ export function registerAccountTools(server, client) {
73
73
  });
74
74
  server.tool("lb_account_get_subscription", "Get Listing Bureau subscription info (plan label, fee, discount, wallet usage)", {}, { readOnlyHint: true }, async () => {
75
75
  try {
76
- const res = await client.request("GET", "/api/v1/account/subscription");
76
+ const res = await client.request("GET", "/api/v1/account/subscription", undefined, undefined, "lb_account_get_subscription");
77
77
  return formatResult(res.data);
78
78
  }
79
79
  catch (e) {
@@ -34,8 +34,8 @@ export function registerCostTools(server, client) {
34
34
  return formatErrorResult(new Error("Provide either a 'schedule' array, OR at least one volume (atc/sfb/pgv > 0) with 'num_days'"));
35
35
  }
36
36
  const [ratesRes, walletRes] = await Promise.all([
37
- client.request("GET", "/api/v1/account/service-rates"),
38
- client.request("GET", "/api/v1/wallet"),
37
+ client.request("GET", "/api/v1/account/service-rates", undefined, undefined, "lb_estimate_cost"),
38
+ client.request("GET", "/api/v1/wallet", undefined, undefined, "lb_estimate_cost"),
39
39
  ]);
40
40
  const rates = ratesRes.data;
41
41
  const wallet = walletRes.data;
@@ -9,7 +9,7 @@ export function registerFeedbackTools(server, client) {
9
9
  .describe("Feedback text (10-5000 characters)"),
10
10
  }, { idempotentHint: false }, async (params) => {
11
11
  try {
12
- const res = await client.request("POST", "/api/v1/feedback", { feedback: params.feedback });
12
+ const res = await client.request("POST", "/api/v1/feedback", { feedback: params.feedback }, undefined, "lb_feedback_submit");
13
13
  return formatResult(res.data);
14
14
  }
15
15
  catch (e) {
@@ -17,7 +17,7 @@ export function registerOrdersTools(server, client) {
17
17
  query.page = String(params.page);
18
18
  if (params.per_page !== undefined)
19
19
  query.per_page = String(params.per_page);
20
- const res = await client.request("GET", "/api/v1/orders", undefined, query);
20
+ const res = await client.request("GET", "/api/v1/orders", undefined, query, "lb_orders_list");
21
21
  if (res.meta) {
22
22
  return formatPaginatedResult(res.data, res.meta);
23
23
  }
@@ -31,7 +31,7 @@ export function registerOrdersTools(server, client) {
31
31
  order_id: z.string().describe("Order identifier"),
32
32
  }, { readOnlyHint: true }, async (params) => {
33
33
  try {
34
- const res = await client.request("GET", `/api/v1/orders/${encodeURIComponent(params.order_id)}`);
34
+ const res = await client.request("GET", `/api/v1/orders/${encodeURIComponent(params.order_id)}`, undefined, undefined, "lb_orders_get");
35
35
  return formatResult(res.data);
36
36
  }
37
37
  catch (e) {
@@ -47,7 +47,7 @@ export function registerOrdersTools(server, client) {
47
47
  .describe("Issue description (1-1000 characters)"),
48
48
  }, { idempotentHint: false }, async (params) => {
49
49
  try {
50
- const res = await client.request("POST", `/api/v1/orders/${encodeURIComponent(params.order_id)}/report-issue`, { description: params.description });
50
+ const res = await client.request("POST", `/api/v1/orders/${encodeURIComponent(params.order_id)}/report-issue`, { description: params.description }, undefined, "lb_orders_report_issue");
51
51
  return formatResult(res.data);
52
52
  }
53
53
  catch (e) {
@@ -22,7 +22,7 @@ export function registerProjectsTools(server, client) {
22
22
  query.region = params.region;
23
23
  if (params.active !== undefined)
24
24
  query.active = String(params.active);
25
- const res = await client.request("GET", "/api/v1/projects", undefined, query);
25
+ const res = await client.request("GET", "/api/v1/projects", undefined, query, "lb_projects_list");
26
26
  return formatResult(res.data);
27
27
  }
28
28
  catch (e) {
@@ -57,7 +57,7 @@ export function registerProjectsTools(server, client) {
57
57
  if (params.expected_retail_price !== undefined) {
58
58
  body.expected_retail_price = params.expected_retail_price;
59
59
  }
60
- const res = await client.request("POST", "/api/v1/projects", body);
60
+ const res = await client.request("POST", "/api/v1/projects", body, undefined, "lb_projects_create");
61
61
  return formatResult(res.data);
62
62
  }
63
63
  catch (e) {
@@ -68,7 +68,7 @@ export function registerProjectsTools(server, client) {
68
68
  ui_id: z.string().describe("Project unique identifier"),
69
69
  }, { readOnlyHint: true }, async (params) => {
70
70
  try {
71
- const res = await client.request("GET", `/api/v1/projects/${encodeURIComponent(params.ui_id)}`);
71
+ const res = await client.request("GET", `/api/v1/projects/${encodeURIComponent(params.ui_id)}`, undefined, undefined, "lb_projects_get");
72
72
  return formatResult(res.data);
73
73
  }
74
74
  catch (e) {
@@ -86,7 +86,7 @@ export function registerProjectsTools(server, client) {
86
86
  if (Object.keys(body).length === 0) {
87
87
  return formatErrorResult(new Error("At least one field (active) must be provided"));
88
88
  }
89
- const res = await client.request("PATCH", `/api/v1/projects/${encodeURIComponent(params.ui_id)}`, body);
89
+ const res = await client.request("PATCH", `/api/v1/projects/${encodeURIComponent(params.ui_id)}`, body, undefined, "lb_projects_update");
90
90
  return formatResult(res.data);
91
91
  }
92
92
  catch (e) {
@@ -97,7 +97,7 @@ export function registerProjectsTools(server, client) {
97
97
  ui_id: z.string().describe("Project unique identifier"),
98
98
  }, { destructiveHint: true }, async (params) => {
99
99
  try {
100
- const res = await client.request("DELETE", `/api/v1/projects/${encodeURIComponent(params.ui_id)}`);
100
+ const res = await client.request("DELETE", `/api/v1/projects/${encodeURIComponent(params.ui_id)}`, undefined, undefined, "lb_projects_archive");
101
101
  return formatResult(res.data);
102
102
  }
103
103
  catch (e) {
@@ -118,7 +118,7 @@ export function registerProjectsTools(server, client) {
118
118
  const query = {};
119
119
  if (params.days !== undefined)
120
120
  query.days = String(params.days);
121
- const res = await client.request("GET", `/api/v1/projects/${encodeURIComponent(params.ui_id)}/stats`, undefined, query);
121
+ const res = await client.request("GET", `/api/v1/projects/${encodeURIComponent(params.ui_id)}/stats`, undefined, query, "lb_projects_get_stats");
122
122
  return formatResult(res.data);
123
123
  }
124
124
  catch (e) {
@@ -18,6 +18,7 @@ const scheduleEntrySchema = z.object({
18
18
  async function appendCostSummary(data, client) {
19
19
  const result = { ...data };
20
20
  try {
21
+ // Internal enrichment fetch — no tool attribution in audit log
21
22
  const ratesRes = await client.request("GET", "/api/v1/account/service-rates");
22
23
  const rates = ratesRes.data;
23
24
  const { dated, ongoing } = mapScheduleEntries(data.scheduling);
@@ -102,7 +103,7 @@ export function registerScheduleTools(server, client) {
102
103
  ui_id: z.string().describe("Project unique identifier"),
103
104
  }, { readOnlyHint: true }, async (params) => {
104
105
  try {
105
- const res = await client.request("GET", `/api/v1/projects/${encodeURIComponent(params.ui_id)}/schedule`);
106
+ const res = await client.request("GET", `/api/v1/projects/${encodeURIComponent(params.ui_id)}/schedule`, undefined, undefined, "lb_schedule_get");
106
107
  const augmented = await appendCostSummary(res.data, client);
107
108
  return formatResult(augmented);
108
109
  }
@@ -119,7 +120,7 @@ export function registerScheduleTools(server, client) {
119
120
  .describe("Array of daily schedule entries"),
120
121
  }, { destructiveHint: true, idempotentHint: true }, async (params) => {
121
122
  try {
122
- const res = await client.request("PUT", `/api/v1/projects/${encodeURIComponent(params.ui_id)}/schedule`, { schedule: params.schedule });
123
+ const res = await client.request("PUT", `/api/v1/projects/${encodeURIComponent(params.ui_id)}/schedule`, { schedule: params.schedule }, undefined, "lb_schedule_set");
123
124
  const augmented = await appendCostSummary(res.data, client);
124
125
  return formatResult(augmented);
125
126
  }
@@ -139,7 +140,7 @@ export function registerScheduleTools(server, client) {
139
140
  sfb: params.sfb ?? 0,
140
141
  pgv: params.pgv ?? 0,
141
142
  };
142
- const res = await client.request("POST", `/api/v1/projects/${encodeURIComponent(params.ui_id)}/schedule/quick`, body);
143
+ const res = await client.request("POST", `/api/v1/projects/${encodeURIComponent(params.ui_id)}/schedule/quick`, body, undefined, "lb_schedule_quick_set");
143
144
  const augmented = await appendCostSummary(res.data, client);
144
145
  return formatResult(augmented);
145
146
  }
@@ -3,7 +3,7 @@ import { formatResult, formatPaginatedResult, formatErrorResult, } from "../util
3
3
  export function registerWalletTools(server, client) {
4
4
  server.tool("lb_wallet_get_balance", "Get Listing Bureau wallet balance (credits and USD). May include a warning if data is temporarily unavailable.", {}, { readOnlyHint: true }, async () => {
5
5
  try {
6
- const res = await client.request("GET", "/api/v1/wallet");
6
+ const res = await client.request("GET", "/api/v1/wallet", undefined, undefined, "lb_wallet_get_balance");
7
7
  return formatResult(res.data);
8
8
  }
9
9
  catch (e) {
@@ -26,7 +26,7 @@ export function registerWalletTools(server, client) {
26
26
  query.page = String(params.page);
27
27
  if (params.per_page !== undefined)
28
28
  query.per_page = String(params.per_page);
29
- const res = await client.request("GET", "/api/v1/wallet/transactions", undefined, query);
29
+ const res = await client.request("GET", "/api/v1/wallet/transactions", undefined, query, "lb_wallet_get_transactions");
30
30
  if (res.meta) {
31
31
  return formatPaginatedResult(res.data, res.meta);
32
32
  }
@@ -44,7 +44,7 @@ export function registerWalletTools(server, client) {
44
44
  .describe("Top-up amount in USD (minimum $5, maximum $5,000)"),
45
45
  }, { destructiveHint: true }, async (params) => {
46
46
  try {
47
- const res = await client.request("POST", "/api/v1/wallet/topup", { amount: params.amount });
47
+ const res = await client.request("POST", "/api/v1/wallet/topup", { amount: params.amount }, undefined, "lb_wallet_topup");
48
48
  return formatResult(res.data);
49
49
  }
50
50
  catch (e) {
@@ -1,4 +1,5 @@
1
1
  import { formatError } from "./errors.js";
2
+ import { getUpdateNotice } from "./update-check.js";
2
3
  /**
3
4
  * Format a successful API response as an MCP tool result.
4
5
  * Handles both entity responses and message-only responses.
@@ -36,6 +37,10 @@ export function formatResult(data) {
36
37
  for (const w of warnings) {
37
38
  text += `\n\n⚠️ Warning: ${w}`;
38
39
  }
40
+ const notice = getUpdateNotice();
41
+ if (notice) {
42
+ text += `\n\n${notice}`;
43
+ }
39
44
  return {
40
45
  content: [{ type: "text", text }],
41
46
  };
@@ -49,8 +54,13 @@ export function formatPaginatedResult(data, meta) {
49
54
  data,
50
55
  pagination: meta,
51
56
  };
57
+ let text = JSON.stringify(result, null, 2);
58
+ const notice = getUpdateNotice();
59
+ if (notice) {
60
+ text += `\n\n${notice}`;
61
+ }
52
62
  return {
53
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
63
+ content: [{ type: "text", text }],
54
64
  };
55
65
  }
56
66
  /** Format an error as an MCP tool error result. */
@@ -0,0 +1,8 @@
1
+ /** Compare semver strings. Returns true only if latest > current. */
2
+ export declare function isNewerVersion(latest: string, current: string): boolean;
3
+ /** Fire-and-forget npm registry check. Stores notice for later retrieval. */
4
+ export declare function checkForUpdate(currentVersion: string): Promise<void>;
5
+ /** Returns the update notice once, then null for all subsequent calls. */
6
+ export declare function getUpdateNotice(): string | null;
7
+ /** Reset state for testing. */
8
+ export declare function resetForTesting(): void;
@@ -0,0 +1,61 @@
1
+ // Module-level state — reset via resetForTesting() in test files.
2
+ // Once noticeConsumed is true, subsequent checkForUpdate calls still overwrite
3
+ // updateNotice but getUpdateNotice will return null. This is intentional:
4
+ // the notice is designed to fire once per process lifetime.
5
+ let updateNotice = null;
6
+ let noticeConsumed = false;
7
+ /** Compare semver strings. Returns true only if latest > current. */
8
+ export function isNewerVersion(latest, current) {
9
+ const latestParts = latest.split(".").map(Number);
10
+ const currentParts = current.split(".").map(Number);
11
+ if (latestParts.length !== 3 || currentParts.length !== 3)
12
+ return false;
13
+ if (latestParts.some((n) => !Number.isFinite(n)) || currentParts.some((n) => !Number.isFinite(n)))
14
+ return false;
15
+ for (let i = 0; i < 3; i++) {
16
+ if (latestParts[i] > currentParts[i])
17
+ return true;
18
+ if (latestParts[i] < currentParts[i])
19
+ return false;
20
+ }
21
+ return false;
22
+ }
23
+ /** Fire-and-forget npm registry check. Stores notice for later retrieval. */
24
+ export async function checkForUpdate(currentVersion) {
25
+ try {
26
+ const res = await fetch("https://registry.npmjs.org/listingbureau-mcp/latest", {
27
+ signal: AbortSignal.timeout(5000),
28
+ });
29
+ if (!res.ok)
30
+ return;
31
+ const json = (await res.json());
32
+ const latest = json.version;
33
+ if (typeof latest !== "string" || !/^\d+\.\d+\.\d+$/.test(latest))
34
+ return;
35
+ if (isNewerVersion(latest, currentVersion)) {
36
+ updateNotice =
37
+ `📦 Update available: v${currentVersion} → v${latest}\n` +
38
+ `To update:\n` +
39
+ ` npm: npx listingbureau-mcp@latest (or npm update -g listingbureau-mcp)\n` +
40
+ ` Desktop: Download from https://github.com/listingbureau/listingbureau-mcp/releases/latest`;
41
+ }
42
+ }
43
+ catch {
44
+ // Silent failure — network errors, TimeoutError, AbortError, bad JSON all swallowed
45
+ }
46
+ }
47
+ /** Returns the update notice once, then null for all subsequent calls. */
48
+ export function getUpdateNotice() {
49
+ if (noticeConsumed)
50
+ return null;
51
+ if (updateNotice) {
52
+ noticeConsumed = true;
53
+ return updateNotice;
54
+ }
55
+ return null;
56
+ }
57
+ /** Reset state for testing. */
58
+ export function resetForTesting() {
59
+ updateNotice = null;
60
+ noticeConsumed = false;
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "listingbureau-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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",