mcp-reddit-ads 1.0.3 → 1.0.5

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":"0d0fac2","builtAt":"2026-04-09T18:53:33.093Z"}
1
+ {"sha":"b27d773","builtAt":"2026-04-09T21:18:55.693Z"}
package/dist/index.js CHANGED
@@ -16,6 +16,22 @@ try {
16
16
  catch {
17
17
  // build-info.json not present (dev mode)
18
18
  }
19
+ // CLI flags
20
+ const __cliPkg = JSON.parse(readFileSync(join(dirname(new URL(import.meta.url).pathname), "..", "package.json"), "utf-8"));
21
+ 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`);
29
+ process.exit(0);
30
+ }
31
+ if (process.argv.includes("--version") || process.argv.includes("-v")) {
32
+ console.log(__cliPkg.version);
33
+ process.exit(0);
34
+ }
19
35
  function loadConfig() {
20
36
  // Try config.json first
21
37
  const configPath = join(dirname(new URL(import.meta.url).pathname), "..", "config.json");
@@ -280,7 +296,7 @@ class RedditAdsManager {
280
296
  // ============================================
281
297
  const config = loadConfig();
282
298
  const adsManager = new RedditAdsManager(config);
283
- const server = new Server({ name: "mcp-reddit-ads", version: "1.0.0" }, { capabilities: { tools: {} } });
299
+ const server = new Server({ name: __cliPkg.name, version: __cliPkg.version }, { capabilities: { tools: {} } });
284
300
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
285
301
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
286
302
  const { name, arguments: args } = request.params;
@@ -517,4 +533,12 @@ async function main() {
517
533
  await server.connect(transport);
518
534
  console.error("[startup] MCP Reddit Ads server running");
519
535
  }
536
+ process.on("SIGTERM", () => {
537
+ console.error("[shutdown] SIGTERM received, exiting");
538
+ process.exit(0);
539
+ });
540
+ process.on("SIGINT", () => {
541
+ console.error("[shutdown] SIGINT received, exiting");
542
+ process.exit(0);
543
+ });
520
544
  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
@@ -21,25 +21,38 @@ export const logger = pino({
21
21
  // ============================================
22
22
  const MAX_RESPONSE_SIZE = 200_000; // 200KB
23
23
  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;
24
+ let current = data;
25
+ for (let pass = 0; pass < 10; pass++) {
26
+ const jsonStr = JSON.stringify(current);
27
+ const sizeBytes = Buffer.byteLength(jsonStr, "utf-8");
28
+ if (sizeBytes <= MAX_RESPONSE_SIZE)
29
+ return current;
30
+ logger.warn({ sizeBytes, maxSize: MAX_RESPONSE_SIZE, context, pass }, "Response exceeds size limit, truncating");
31
+ if (Array.isArray(current)) {
32
+ current = current.slice(0, Math.max(1, Math.floor(current.length * 0.5)));
33
+ continue;
31
34
  }
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])) {
35
+ if (typeof current === "object" && current !== null) {
36
+ const obj = current;
37
+ let truncated = false;
38
+ for (const key of ["items", "results", "data", "rows", "tags", "triggers", "variables"]) {
39
+ if (Array.isArray(obj[key]) && obj[key].length > 1) {
36
40
  obj[key] = obj[key].slice(0, Math.max(1, Math.floor(obj[key].length * 0.5)));
37
- return obj;
41
+ if ("count" in obj)
42
+ obj.count = obj[key].length;
43
+ if ("row_count" in obj)
44
+ obj.row_count = obj[key].length;
45
+ obj.truncated = true;
46
+ truncated = true;
47
+ break;
38
48
  }
39
49
  }
50
+ if (truncated)
51
+ continue;
40
52
  }
53
+ break;
41
54
  }
42
- return data;
55
+ return current;
43
56
  }
44
57
  // ============================================
45
58
  // RETRY + CIRCUIT BREAKER + TIMEOUT
@@ -48,11 +61,24 @@ const backoff = new ExponentialBackoff({
48
61
  initialDelay: 100,
49
62
  maxDelay: 5_000,
50
63
  });
51
- const retryPolicy = retry(handleAll, {
64
+ const isTransient = handleWhen((err) => {
65
+ const msg = (err?.message || "").toLowerCase();
66
+ const code = err?.code || err?.status;
67
+ if (code === 401 || code === 403 || code === 7 || code === 16)
68
+ return false;
69
+ if (msg.includes("unauthenticated") || msg.includes("permission_denied") || msg.includes("invalid_grant"))
70
+ return false;
71
+ if (code === 429 || msg.includes("rate"))
72
+ return true;
73
+ if (code >= 400 && code < 500)
74
+ return false;
75
+ return true;
76
+ });
77
+ const retryPolicy = retry(isTransient, {
52
78
  maxAttempts: 3,
53
79
  backoff,
54
80
  });
55
- const circuitBreakerPolicy = circuitBreaker(handleAll, {
81
+ const circuitBreakerPolicy = circuitBreaker(isTransient, {
56
82
  halfOpenAfter: 60_000,
57
83
  breaker: new ConsecutiveBreaker(5),
58
84
  });
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.3",
4
+ "version": "1.0.5",
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": {