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 +19 -19
- package/dist/build-info.json +1 -1
- package/dist/index.js +25 -1
- package/dist/resilience.js +42 -16
- package/package.json +1 -1
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/
|
|
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
|
-
| `
|
|
102
|
-
| `
|
|
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
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
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
|
-
| `
|
|
119
|
-
| `
|
|
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
|
-
| `
|
|
126
|
-
| `
|
|
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
|
-
| `
|
|
133
|
-
| `
|
|
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
|
-
| `
|
|
140
|
-
| `
|
|
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
|
-
| `
|
|
147
|
-
| `
|
|
148
|
-
| `
|
|
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
|
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"sha":"
|
|
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:
|
|
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);
|
package/dist/resilience.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { retry, circuitBreaker, wrap,
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
33
|
-
const obj =
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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.
|
|
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": {
|