mcp-reddit-ads 1.0.4 → 1.0.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.
package/README.md CHANGED
@@ -20,7 +20,7 @@ npm install mcp-reddit-ads
20
20
  Or clone the repository:
21
21
 
22
22
  ```bash
23
- git clone https://github.com/drak-marketing/mcp-reddit-ads.git
23
+ git clone https://github.com/mharnett/mcp-reddit-ads.git
24
24
  cd mcp-reddit-ads
25
25
  npm install
26
26
  npm run build
@@ -98,54 +98,54 @@ Environment variables take precedence over config file values.
98
98
 
99
99
  | Tool | Description |
100
100
  |---|---|
101
- | `get_client_context` | Get account info and verify API connectivity |
102
- | `get_accounts` | List all ad accounts accessible to the authenticated user |
101
+ | `reddit_ads_get_client_context` | Get account info and verify API connectivity |
102
+ | `reddit_ads_get_accounts` | List all ad accounts accessible to the authenticated user |
103
103
 
104
104
  ### Read
105
105
 
106
106
  | Tool | Description |
107
107
  |---|---|
108
- | `get_campaigns` | List campaigns with optional status filter |
109
- | `get_ad_groups` | List ad groups for a campaign |
110
- | `get_ads` | List ads for an ad group |
111
- | `get_performance_report` | Aggregated performance metrics for campaigns/ad groups/ads |
112
- | `get_daily_performance` | Day-by-day performance breakdown |
108
+ | `reddit_ads_get_campaigns` | List campaigns with optional status filter |
109
+ | `reddit_ads_get_ad_groups` | List ad groups for a campaign |
110
+ | `reddit_ads_get_ads` | List ads for an ad group |
111
+ | `reddit_ads_get_performance_report` | Aggregated performance metrics for campaigns/ad groups/ads |
112
+ | `reddit_ads_get_daily_performance` | Day-by-day performance breakdown |
113
113
 
114
114
  ### Write: Campaigns
115
115
 
116
116
  | Tool | Description |
117
117
  |---|---|
118
- | `create_campaign` | Create a new campaign (PAUSED by default) |
119
- | `update_campaign` | Update campaign name, budget, objective, or status |
118
+ | `reddit_ads_create_campaign` | Create a new campaign (PAUSED by default) |
119
+ | `reddit_ads_update_campaign` | Update campaign name, budget, objective, or status |
120
120
 
121
121
  ### Write: Ad Groups
122
122
 
123
123
  | Tool | Description |
124
124
  |---|---|
125
- | `create_ad_group` | Create a new ad group with targeting (PAUSED by default) |
126
- | `update_ad_group` | Update ad group bid, targeting, or status |
125
+ | `reddit_ads_create_ad_group` | Create a new ad group with targeting (PAUSED by default) |
126
+ | `reddit_ads_update_ad_group` | Update ad group bid, targeting, or status |
127
127
 
128
128
  ### Write: Ads
129
129
 
130
130
  | Tool | Description |
131
131
  |---|---|
132
- | `create_ad` | Create a new ad with headline, body, URL, and media (PAUSED by default) |
133
- | `update_ad` | Update ad creative or status |
132
+ | `reddit_ads_create_ad` | Create a new ad with headline, body, URL, and media (PAUSED by default) |
133
+ | `reddit_ads_update_ad` | Update ad creative or status |
134
134
 
135
135
  ### Bulk Operations
136
136
 
137
137
  | Tool | Description |
138
138
  |---|---|
139
- | `pause_items` | Pause multiple campaigns, ad groups, or ads at once |
140
- | `enable_items` | Enable multiple campaigns, ad groups, or ads at once |
139
+ | `reddit_ads_pause_items` | Pause multiple campaigns, ad groups, or ads at once |
140
+ | `reddit_ads_enable_items` | Enable multiple campaigns, ad groups, or ads at once |
141
141
 
142
142
  ### Targeting
143
143
 
144
144
  | Tool | Description |
145
145
  |---|---|
146
- | `search_subreddits` | Search for subreddits by keyword for targeting |
147
- | `get_interest_categories` | List available interest categories for targeting |
148
- | `search_geo_targets` | Search for geographic targeting options (countries, regions, metros) |
146
+ | `reddit_ads_search_subreddits` | Search for subreddits by keyword for targeting |
147
+ | `reddit_ads_get_interest_categories` | List available interest categories for targeting |
148
+ | `reddit_ads_search_geo_targets` | Search for geographic targeting options (countries, regions, metros) |
149
149
 
150
150
  ## Key Conventions
151
151
 
@@ -1 +1 @@
1
- {"sha":"4534c7a","builtAt":"2026-04-09T19:37:19.842Z"}
1
+ {"sha":"6453862","builtAt":"2026-04-09T21:28:34.019Z"}
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@ import { join, dirname } from "path";
7
7
  import { RedditAdsAuthError, RedditAdsRateLimitError, RedditAdsServiceError, classifyError, validateCredentials, } from "./errors.js";
8
8
  import { tools } from "./tools.js";
9
9
  import { withResilience, safeResponse, logger } from "./resilience.js";
10
+ // CLI package info
11
+ const __cliPkg = JSON.parse(readFileSync(join(dirname(new URL(import.meta.url).pathname), "..", "package.json"), "utf-8"));
10
12
  // Log build fingerprint at startup
11
13
  try {
12
14
  const __buildInfoDir = dirname(new URL(import.meta.url).pathname);
@@ -14,22 +16,21 @@ try {
14
16
  console.error(`[build] SHA: ${buildInfo.sha} (${buildInfo.builtAt})`);
15
17
  }
16
18
  catch {
17
- // build-info.json not present (dev mode)
19
+ console.error(`[build] ${__cliPkg.name}@${__cliPkg.version} (dev mode)`);
18
20
  }
19
21
  // CLI flags
20
- const __cliPkg = JSON.parse(readFileSync(join(dirname(new URL(import.meta.url).pathname), "..", "package.json"), "utf-8"));
21
22
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
22
- console.log(`${__cliPkg.name} v${__cliPkg.version}\n`);
23
- console.log(`Usage: ${__cliPkg.name} [options]\n`);
24
- console.log("MCP server communicating via stdio. Configure in your .mcp.json.\n");
25
- console.log("Options:");
26
- console.log(" --help, -h Show this help message");
27
- console.log(" --version, -v Show version number");
28
- console.log(`\nDocumentation: https://github.com/mharnett/mcp-reddit-ads`);
23
+ console.error(`${__cliPkg.name} v${__cliPkg.version}\n`);
24
+ console.error(`Usage: ${__cliPkg.name} [options]\n`);
25
+ console.error("MCP server communicating via stdio. Configure in your .mcp.json.\n");
26
+ console.error("Options:");
27
+ console.error(" --help, -h Show this help message");
28
+ console.error(" --version, -v Show version number");
29
+ console.error(`\nDocumentation: https://github.com/mharnett/mcp-reddit-ads`);
29
30
  process.exit(0);
30
31
  }
31
32
  if (process.argv.includes("--version") || process.argv.includes("-v")) {
32
- console.log(__cliPkg.version);
33
+ console.error(__cliPkg.version);
33
34
  process.exit(0);
34
35
  }
35
36
  function loadConfig() {
@@ -296,7 +297,7 @@ class RedditAdsManager {
296
297
  // ============================================
297
298
  const config = loadConfig();
298
299
  const adsManager = new RedditAdsManager(config);
299
- const server = new Server({ name: "mcp-reddit-ads", version: "1.0.0" }, { capabilities: { tools: {} } });
300
+ const server = new Server({ name: __cliPkg.name, version: __cliPkg.version }, { capabilities: { tools: {} } });
300
301
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
301
302
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
302
303
  const { name, arguments: args } = request.params;
@@ -533,4 +534,15 @@ async function main() {
533
534
  await server.connect(transport);
534
535
  console.error("[startup] MCP Reddit Ads server running");
535
536
  }
537
+ process.on("SIGTERM", () => {
538
+ console.error("[shutdown] SIGTERM received, exiting");
539
+ process.exit(0);
540
+ });
541
+ process.on("SIGINT", () => {
542
+ console.error("[shutdown] SIGINT received, exiting");
543
+ process.exit(0);
544
+ });
545
+ process.on("SIGPIPE", () => {
546
+ // Client disconnected -- expected during shutdown
547
+ });
536
548
  main().catch(console.error);
@@ -1,4 +1,4 @@
1
- import { retry, circuitBreaker, wrap, handleAll, timeout, TimeoutStrategy, ExponentialBackoff, ConsecutiveBreaker, } from "cockatiel";
1
+ import { retry, circuitBreaker, wrap, handleWhen, timeout, TimeoutStrategy, ExponentialBackoff, ConsecutiveBreaker, } from "cockatiel";
2
2
  import pino from "pino";
3
3
  // ============================================
4
4
  // LOGGER
@@ -12,34 +12,50 @@ export const logger = pino({
12
12
  colorize: true,
13
13
  singleLine: true,
14
14
  translateTime: "SYS:standard",
15
+ destination: 2, // stderr -- stdout is reserved for MCP JSON-RPC
15
16
  },
16
17
  },
17
18
  }),
18
- });
19
+ },
20
+ // When no transport (test mode), write to stderr directly
21
+ process.env.NODE_ENV === "test" ? pino.destination(2) : undefined);
19
22
  // ============================================
20
23
  // SAFE RESPONSE (Response Size Limiting)
21
24
  // ============================================
22
25
  const MAX_RESPONSE_SIZE = 200_000; // 200KB
23
26
  export function safeResponse(data, context) {
24
- const jsonStr = JSON.stringify(data);
25
- const sizeBytes = Buffer.byteLength(jsonStr, "utf-8");
26
- if (sizeBytes > MAX_RESPONSE_SIZE) {
27
- logger.warn({ sizeBytes, maxSize: MAX_RESPONSE_SIZE, context }, `Response exceeds size limit, truncating`);
28
- if (Array.isArray(data)) {
29
- const truncated = data.slice(0, Math.max(1, Math.floor(data.length * 0.5)));
30
- return truncated;
27
+ let current = data;
28
+ for (let pass = 0; pass < 10; pass++) {
29
+ const jsonStr = JSON.stringify(current);
30
+ const sizeBytes = Buffer.byteLength(jsonStr, "utf-8");
31
+ if (sizeBytes <= MAX_RESPONSE_SIZE)
32
+ return current;
33
+ logger.warn({ sizeBytes, maxSize: MAX_RESPONSE_SIZE, context, pass }, "Response exceeds size limit, truncating");
34
+ if (Array.isArray(current)) {
35
+ current = current.slice(0, Math.max(1, Math.floor(current.length * 0.5)));
36
+ continue;
31
37
  }
32
- if (typeof data === "object" && data !== null) {
33
- const obj = data;
34
- for (const key of ["items", "results", "data", "rows"]) {
35
- if (Array.isArray(obj[key])) {
38
+ if (typeof current === "object" && current !== null) {
39
+ const obj = current;
40
+ let truncated = false;
41
+ for (const key of ["items", "results", "data", "rows", "tags", "triggers", "variables"]) {
42
+ if (Array.isArray(obj[key]) && obj[key].length > 1) {
36
43
  obj[key] = obj[key].slice(0, Math.max(1, Math.floor(obj[key].length * 0.5)));
37
- return obj;
44
+ if ("count" in obj)
45
+ obj.count = obj[key].length;
46
+ if ("row_count" in obj)
47
+ obj.row_count = obj[key].length;
48
+ obj.truncated = true;
49
+ truncated = true;
50
+ break;
38
51
  }
39
52
  }
53
+ if (truncated)
54
+ continue;
40
55
  }
56
+ break;
41
57
  }
42
- return data;
58
+ return current;
43
59
  }
44
60
  // ============================================
45
61
  // RETRY + CIRCUIT BREAKER + TIMEOUT
@@ -48,11 +64,24 @@ const backoff = new ExponentialBackoff({
48
64
  initialDelay: 100,
49
65
  maxDelay: 5_000,
50
66
  });
51
- const retryPolicy = retry(handleAll, {
67
+ const isTransient = handleWhen((err) => {
68
+ const msg = (err?.message || "").toLowerCase();
69
+ const code = err?.code || err?.status;
70
+ if (code === 401 || code === 403 || code === 7 || code === 16)
71
+ return false;
72
+ if (msg.includes("unauthenticated") || msg.includes("permission_denied") || msg.includes("invalid_grant"))
73
+ return false;
74
+ if (code === 429 || msg.includes("rate"))
75
+ return true;
76
+ if (code >= 400 && code < 500)
77
+ return false;
78
+ return true;
79
+ });
80
+ const retryPolicy = retry(isTransient, {
52
81
  maxAttempts: 3,
53
82
  backoff,
54
83
  });
55
- const circuitBreakerPolicy = circuitBreaker(handleAll, {
84
+ const circuitBreakerPolicy = circuitBreaker(isTransient, {
56
85
  halfOpenAfter: 60_000,
57
86
  breaker: new ConsecutiveBreaker(5),
58
87
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-reddit-ads",
3
3
  "mcpName": "io.github.mharnett/reddit-ads",
4
- "version": "1.0.4",
4
+ "version": "1.0.6",
5
5
  "description": "MCP server for Reddit Ads API v3 with campaign, ad group, ad management, and performance reporting.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {