mcp-reddit-ads 1.1.1 → 1.1.2
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/dist/build-info.json +1 -1
- package/dist/index.js +5 -4
- package/dist/writeGate.js +14 -11
- package/package.json +9 -2
- package/dist/updateNotifier.d.ts +0 -7
- package/dist/updateNotifier.js +0 -59
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"sha":"
|
|
1
|
+
{"sha":"19917ed","builtAt":"2026-07-09T14:42:15.009Z"}
|
package/dist/index.js
CHANGED
|
@@ -4,17 +4,18 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { readFileSync, existsSync } from "fs";
|
|
6
6
|
import { join, dirname } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
7
8
|
import { RedditAdsAuthError, RedditAdsRateLimitError, RedditAdsServiceError, classifyError, validateCredentials, } from "./errors.js";
|
|
8
9
|
import { tools } from "./tools.js";
|
|
9
10
|
import { withResilience, safeResponse, logger } from "./resilience.js";
|
|
10
11
|
import { filterTools, assertWriteAllowed, isWriteEnabled } from "./writeGate.js";
|
|
11
|
-
import { checkForUpdate } from "
|
|
12
|
+
import { checkForUpdate } from "mcp-updatenotifier";
|
|
12
13
|
import v8 from "v8";
|
|
13
14
|
// CLI package info
|
|
14
|
-
const __cliPkg = JSON.parse(readFileSync(join(dirname(
|
|
15
|
+
const __cliPkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
|
|
15
16
|
// Log build fingerprint at startup
|
|
16
17
|
try {
|
|
17
|
-
const __buildInfoDir = dirname(
|
|
18
|
+
const __buildInfoDir = dirname(fileURLToPath(import.meta.url));
|
|
18
19
|
const buildInfo = JSON.parse(readFileSync(join(__buildInfoDir, "build-info.json"), "utf-8"));
|
|
19
20
|
console.error(`[build] SHA: ${buildInfo.sha} (${buildInfo.builtAt})`);
|
|
20
21
|
}
|
|
@@ -64,7 +65,7 @@ if (heapLimit < 256 * 1024 * 1024) {
|
|
|
64
65
|
const envTrimmed = (key) => (process.env[key] || "").trim().replace(/^["']|["']$/g, "");
|
|
65
66
|
function loadConfig() {
|
|
66
67
|
// Try config.json first
|
|
67
|
-
const configPath = join(dirname(
|
|
68
|
+
const configPath = join(dirname(fileURLToPath(import.meta.url)), "..", "config.json");
|
|
68
69
|
let config;
|
|
69
70
|
if (existsSync(configPath)) {
|
|
70
71
|
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
package/dist/writeGate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createWriteGate } from "mcp-write-gate";
|
|
1
2
|
/**
|
|
2
3
|
* Tools that mutate Reddit Ads state. These are hidden from the tool list
|
|
3
4
|
* and refused at call time unless REDDIT_ADS_MCP_WRITE=true.
|
|
@@ -15,17 +16,18 @@ export const WRITE_TOOLS = new Set([
|
|
|
15
16
|
"reddit_ads_pause_items",
|
|
16
17
|
"reddit_ads_enable_items",
|
|
17
18
|
]);
|
|
19
|
+
const gate = createWriteGate({
|
|
20
|
+
writeTools: WRITE_TOOLS,
|
|
21
|
+
envPrefix: "REDDIT_ADS",
|
|
22
|
+
});
|
|
18
23
|
export function isWriteTool(name) {
|
|
19
|
-
return
|
|
24
|
+
return gate.isWriteTool(name);
|
|
20
25
|
}
|
|
21
26
|
export function isWriteEnabled(env = process.env) {
|
|
22
|
-
|
|
23
|
-
return v === "true" || v === "1" || v === "yes";
|
|
27
|
+
return gate.isWriteEnabled(env);
|
|
24
28
|
}
|
|
25
29
|
export function filterTools(allTools, env = process.env) {
|
|
26
|
-
|
|
27
|
-
return [...allTools];
|
|
28
|
-
return allTools.filter((t) => !WRITE_TOOLS.has(t.name));
|
|
30
|
+
return gate.filterTools(allTools, env);
|
|
29
31
|
}
|
|
30
32
|
export const WRITE_DISABLED_MESSAGE = "Write operations are disabled. Set REDDIT_ADS_MCP_WRITE=true in the MCP server environment to enable mutating tools (create/update/pause/enable).";
|
|
31
33
|
/**
|
|
@@ -33,9 +35,10 @@ export const WRITE_DISABLED_MESSAGE = "Write operations are disabled. Set REDDIT
|
|
|
33
35
|
* Throws a clear Error if the tool mutates state and writes are disabled.
|
|
34
36
|
*/
|
|
35
37
|
export function assertWriteAllowed(toolName, env = process.env) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
try {
|
|
39
|
+
gate.assertWriteAllowed(toolName, env);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
throw new Error(`Tool "${toolName}" is a write operation. ${WRITE_DISABLED_MESSAGE}`);
|
|
43
|
+
}
|
|
41
44
|
}
|
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.1.
|
|
4
|
+
"version": "1.1.2",
|
|
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": {
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"build": "tsc && node -e \"fs=require('fs');cp=require('child_process');sha=cp.execSync('git rev-parse --short HEAD 2>/dev/null||echo unknown').toString().trim();fs.writeFileSync('dist/build-info.json',JSON.stringify({sha,builtAt:new Date().toISOString()}))\"",
|
|
26
26
|
"start": "node dist/index.js",
|
|
27
27
|
"dev": "tsx src/index.ts",
|
|
28
|
-
"test": "vitest run"
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"smoke": "scripts/healthcheck.sh"
|
|
29
30
|
},
|
|
30
31
|
"author": "drak-marketing",
|
|
31
32
|
"license": "MIT",
|
|
@@ -35,6 +36,8 @@
|
|
|
35
36
|
"dependencies": {
|
|
36
37
|
"@modelcontextprotocol/sdk": "^0.5.0",
|
|
37
38
|
"cockatiel": "^3.2.1",
|
|
39
|
+
"mcp-updatenotifier": "^1.0.0",
|
|
40
|
+
"mcp-write-gate": "^1.0.0",
|
|
38
41
|
"pino": "^8.21.0",
|
|
39
42
|
"pino-pretty": "^13.1.3"
|
|
40
43
|
},
|
|
@@ -44,5 +47,9 @@
|
|
|
44
47
|
"tsx": "^4.7.0",
|
|
45
48
|
"typescript": "^5.3.0",
|
|
46
49
|
"vitest": "^4.0.18"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/mharnett/mcp-reddit-ads.git"
|
|
47
54
|
}
|
|
48
55
|
}
|
package/dist/updateNotifier.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export type FetchLatestVersion = () => Promise<string>;
|
|
2
|
-
export interface UpdateNotifierDeps {
|
|
3
|
-
fetchLatestVersion?: FetchLatestVersion;
|
|
4
|
-
log?: (msg: string) => void;
|
|
5
|
-
env?: NodeJS.ProcessEnv;
|
|
6
|
-
}
|
|
7
|
-
export declare function checkForUpdate(pkgName: string, currentVersion: string, deps?: UpdateNotifierDeps): Promise<void>;
|
package/dist/updateNotifier.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// Fire-and-forget npm registry check at startup. Logs to stderr when a
|
|
2
|
-
// newer version is available. stdout is reserved for MCP JSON-RPC, so the
|
|
3
|
-
// message never goes there. Silent on network error, timeout, or when the
|
|
4
|
-
// installed version is equal to or ahead of the registry latest.
|
|
5
|
-
//
|
|
6
|
-
// Opt out by setting MCP_DISABLE_UPDATE_CHECK=1 (CI, offline, air-gapped).
|
|
7
|
-
// Also skipped when NODE_ENV=test to keep vitest runs silent.
|
|
8
|
-
export async function checkForUpdate(pkgName, currentVersion, deps = {}) {
|
|
9
|
-
const env = deps.env ?? process.env;
|
|
10
|
-
if (env.MCP_DISABLE_UPDATE_CHECK === "1" || env.NODE_ENV === "test") {
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
const log = deps.log ?? ((msg) => process.stderr.write(msg + "\n"));
|
|
14
|
-
const fetcher = deps.fetchLatestVersion ?? (() => defaultFetch(pkgName));
|
|
15
|
-
let latest;
|
|
16
|
-
try {
|
|
17
|
-
latest = await fetcher();
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
if (!latest || !semverLt(currentVersion, latest)) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
log(`[update] ${pkgName}@${latest} is available (running ${currentVersion}). ` +
|
|
26
|
-
`Upgrade: npx -y ${pkgName}@latest (and relaunch Claude Desktop).`);
|
|
27
|
-
}
|
|
28
|
-
async function defaultFetch(pkgName) {
|
|
29
|
-
const controller = new AbortController();
|
|
30
|
-
const timer = setTimeout(() => controller.abort(), 2_000);
|
|
31
|
-
try {
|
|
32
|
-
const res = await fetch(`https://registry.npmjs.org/${pkgName}/latest`, {
|
|
33
|
-
signal: controller.signal,
|
|
34
|
-
headers: { Accept: "application/json" },
|
|
35
|
-
});
|
|
36
|
-
if (!res.ok) {
|
|
37
|
-
throw new Error(`HTTP ${res.status}`);
|
|
38
|
-
}
|
|
39
|
-
const body = (await res.json());
|
|
40
|
-
if (typeof body.version !== "string") {
|
|
41
|
-
throw new Error("registry response missing version field");
|
|
42
|
-
}
|
|
43
|
-
return body.version;
|
|
44
|
-
}
|
|
45
|
-
finally {
|
|
46
|
-
clearTimeout(timer);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function semverLt(a, b) {
|
|
50
|
-
const pa = a.split(".").map((x) => parseInt(x, 10) || 0);
|
|
51
|
-
const pb = b.split(".").map((x) => parseInt(x, 10) || 0);
|
|
52
|
-
for (let i = 0; i < 3; i++) {
|
|
53
|
-
if ((pa[i] ?? 0) < (pb[i] ?? 0))
|
|
54
|
-
return true;
|
|
55
|
-
if ((pa[i] ?? 0) > (pb[i] ?? 0))
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
return false;
|
|
59
|
-
}
|