mcp-bing-ads 1.0.15 → 1.1.0
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 +16 -0
- package/dist/build-info.json +1 -1
- package/dist/errors.js +2 -1
- package/dist/index.js +11 -3
- package/dist/updateNotifier.d.ts +7 -0
- package/dist/updateNotifier.js +59 -0
- package/dist/writeGate.d.ts +18 -0
- package/dist/writeGate.js +36 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -65,8 +65,24 @@ npm install mcp-bing-ads
|
|
|
65
65
|
export BING_ADS_REFRESH_TOKEN="your_refresh_token"
|
|
66
66
|
# Optional:
|
|
67
67
|
export BING_ADS_CLIENT_SECRET="your_client_secret"
|
|
68
|
+
# Optional: opt into mutating tools (read-only by default)
|
|
69
|
+
export BING_ADS_MCP_WRITE="true"
|
|
68
70
|
```
|
|
69
71
|
|
|
72
|
+
### Environment Variables
|
|
73
|
+
|
|
74
|
+
| Variable | Required | Default | Description |
|
|
75
|
+
| --- | --- | --- | --- |
|
|
76
|
+
| `BING_ADS_DEVELOPER_TOKEN` | yes | -- | Microsoft Advertising developer token |
|
|
77
|
+
| `BING_ADS_CLIENT_ID` | yes | -- | Azure AD app client ID |
|
|
78
|
+
| `BING_ADS_REFRESH_TOKEN` | yes | -- | OAuth refresh token |
|
|
79
|
+
| `BING_ADS_CLIENT_SECRET` | no | -- | Azure AD app client secret (if confidential client) |
|
|
80
|
+
| `BING_ADS_MCP_WRITE` | no | `false` | Set to `true`, `1`, or `yes` to expose mutating tools (pause/update/add). Any other value -- or unset -- keeps the server read-only. |
|
|
81
|
+
|
|
82
|
+
### Read-only by default
|
|
83
|
+
|
|
84
|
+
Mutating tools (`bing_ads_pause_keywords`, `bing_ads_update_campaign_budget`, `bing_ads_add_shared_negatives`) are **hidden from the tool list and refused at call time** unless `BING_ADS_MCP_WRITE=true` is set in the server environment. This is a safety gate against casual write actions -- for example, pausing a keyword or editing a budget based on a throwaway chat message. To make write changes, set the env var explicitly in your `.mcp.json` or shell profile for the session that needs it, then unset it afterwards. Read tools (list/report/performance) are always available.
|
|
85
|
+
|
|
70
86
|
## Usage
|
|
71
87
|
|
|
72
88
|
### Start the server
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"sha":"
|
|
1
|
+
{"sha":"202808b","builtAt":"2026-04-18T18:10:54.717Z"}
|
package/dist/errors.js
CHANGED
|
@@ -55,7 +55,8 @@ export function classifyError(error) {
|
|
|
55
55
|
message.includes("AuthenticationTokenExpired") ||
|
|
56
56
|
message.includes("InvalidCredentials") ||
|
|
57
57
|
bodyError?.code === "AuthenticationTokenExpired") {
|
|
58
|
-
return new BingAdsAuthError(`Bing Ads auth failed: ${message}. Refresh token may be expired.
|
|
58
|
+
return new BingAdsAuthError(`Bing Ads auth failed: ${message}. Refresh token may be expired. Run 'node get-refresh-token.cjs' and update your BING_ADS_REFRESH_TOKEN environment variable.` +
|
|
59
|
+
(process.platform === "darwin" ? ` On macOS: security add-generic-password -a bing-ads-mcp -s BING_ADS_REFRESH_TOKEN -w '<token>' -U` : ""), error);
|
|
59
60
|
}
|
|
60
61
|
if (status === 429 || message.includes("RateLimit") || message.includes("CallRateExceeded")) {
|
|
61
62
|
const retryMs = 60_000;
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { readFileSync, existsSync } from "fs";
|
|
|
6
6
|
import { join, dirname } from "path";
|
|
7
7
|
import { BingAdsAuthError, BingAdsRateLimitError, BingAdsServiceError, classifyError, validateCredentials, } from "./errors.js";
|
|
8
8
|
import { tools } from "./tools.js";
|
|
9
|
+
import { filterTools, assertWriteAllowed, isWriteEnabled } from "./writeGate.js";
|
|
9
10
|
import { withResilience, safeResponse, logger } from "./resilience.js";
|
|
10
11
|
import v8 from "v8";
|
|
11
12
|
// CLI package info
|
|
@@ -61,7 +62,9 @@ const envTrimmed = (key) => (process.env[key] || "").trim().replace(/^["']|["']$
|
|
|
61
62
|
function loadConfig() {
|
|
62
63
|
const configPath = join(dirname(new URL(import.meta.url).pathname), "..", "config.json");
|
|
63
64
|
if (!existsSync(configPath)) {
|
|
64
|
-
throw new Error(`Config file not found at ${configPath}. Create config.json from config.example.json with your client entries
|
|
65
|
+
throw new Error(`Config file not found at ${configPath}. Create config.json from config.example.json with your client entries, ` +
|
|
66
|
+
`or set env vars BING_ADS_DEVELOPER_TOKEN, BING_ADS_CLIENT_ID, BING_ADS_CLIENT_SECRET, and BING_ADS_REFRESH_TOKEN. ` +
|
|
67
|
+
`Run 'node get-refresh-token.cjs' to obtain a refresh token.`);
|
|
65
68
|
}
|
|
66
69
|
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
67
70
|
}
|
|
@@ -89,7 +92,9 @@ class BingAdsManager {
|
|
|
89
92
|
// Validate credentials at startup — fail fast
|
|
90
93
|
const creds = validateCredentials();
|
|
91
94
|
if (!creds.valid) {
|
|
92
|
-
const msg = `[STARTUP ERROR] Missing required credentials: ${creds.missing.join(", ")}.
|
|
95
|
+
const msg = `[STARTUP ERROR] Missing required credentials: ${creds.missing.join(", ")}. ` +
|
|
96
|
+
`Set these environment variables before starting the server.` +
|
|
97
|
+
(process.platform === "darwin" ? ` On macOS, tokens can be stored in Keychain and loaded via run-mcp.sh.` : "");
|
|
93
98
|
console.error(msg);
|
|
94
99
|
throw new BingAdsAuthError(msg);
|
|
95
100
|
}
|
|
@@ -599,12 +604,13 @@ const server = new Server({
|
|
|
599
604
|
});
|
|
600
605
|
// Handle list tools
|
|
601
606
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
602
|
-
return { tools };
|
|
607
|
+
return { tools: filterTools(tools) };
|
|
603
608
|
});
|
|
604
609
|
// Handle tool calls
|
|
605
610
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
606
611
|
const { name, arguments: args } = request.params;
|
|
607
612
|
try {
|
|
613
|
+
assertWriteAllowed(name);
|
|
608
614
|
// Resolve client from account_id or working directory context
|
|
609
615
|
const resolveClient = (accountId) => {
|
|
610
616
|
if (accountId) {
|
|
@@ -818,6 +824,8 @@ async function main() {
|
|
|
818
824
|
console.error(`[STARTUP WARNING] Auth check FAILED: ${err.message}`);
|
|
819
825
|
console.error(`[STARTUP WARNING] MCP will start but ALL API calls will fail until auth is fixed.`);
|
|
820
826
|
}
|
|
827
|
+
const writeMode = isWriteEnabled();
|
|
828
|
+
console.error(`[startup] Write mode: ${writeMode ? "ENABLED (mutating tools exposed)" : "DISABLED (read-only; set BING_ADS_MCP_WRITE=true to enable writes)"}`);
|
|
821
829
|
const transport = new StdioServerTransport();
|
|
822
830
|
await server.connect(transport);
|
|
823
831
|
console.error("[startup] MCP Bing Ads server running");
|
|
@@ -0,0 +1,7 @@
|
|
|
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>;
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tools that mutate Bing Ads state. These are hidden from the tool list
|
|
4
|
+
* and refused at call time unless BING_ADS_MCP_WRITE=true.
|
|
5
|
+
*
|
|
6
|
+
* Adding a new tool? Put it in this set if it creates, modifies, pauses,
|
|
7
|
+
* enables, removes, links, unlinks, or applies anything.
|
|
8
|
+
*/
|
|
9
|
+
export declare const WRITE_TOOLS: ReadonlySet<string>;
|
|
10
|
+
export declare function isWriteTool(name: string): boolean;
|
|
11
|
+
export declare function isWriteEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
12
|
+
export declare function filterTools(allTools: readonly Tool[], env?: NodeJS.ProcessEnv): Tool[];
|
|
13
|
+
export declare const WRITE_DISABLED_MESSAGE = "Write operations are disabled. Set BING_ADS_MCP_WRITE=true in the MCP server environment to enable mutating tools (create/update/pause/enable/remove/apply).";
|
|
14
|
+
/**
|
|
15
|
+
* Assert that a tool call is allowed under the current write-mode setting.
|
|
16
|
+
* Throws a clear Error if the tool mutates state and writes are disabled.
|
|
17
|
+
*/
|
|
18
|
+
export declare function assertWriteAllowed(toolName: string, env?: NodeJS.ProcessEnv): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools that mutate Bing Ads state. These are hidden from the tool list
|
|
3
|
+
* and refused at call time unless BING_ADS_MCP_WRITE=true.
|
|
4
|
+
*
|
|
5
|
+
* Adding a new tool? Put it in this set if it creates, modifies, pauses,
|
|
6
|
+
* enables, removes, links, unlinks, or applies anything.
|
|
7
|
+
*/
|
|
8
|
+
export const WRITE_TOOLS = new Set([
|
|
9
|
+
"bing_ads_add_shared_negatives",
|
|
10
|
+
"bing_ads_pause_keywords",
|
|
11
|
+
"bing_ads_update_campaign_budget",
|
|
12
|
+
]);
|
|
13
|
+
export function isWriteTool(name) {
|
|
14
|
+
return WRITE_TOOLS.has(name);
|
|
15
|
+
}
|
|
16
|
+
export function isWriteEnabled(env = process.env) {
|
|
17
|
+
const v = (env.BING_ADS_MCP_WRITE || "").trim().toLowerCase();
|
|
18
|
+
return v === "true" || v === "1" || v === "yes";
|
|
19
|
+
}
|
|
20
|
+
export function filterTools(allTools, env = process.env) {
|
|
21
|
+
if (isWriteEnabled(env))
|
|
22
|
+
return [...allTools];
|
|
23
|
+
return allTools.filter((t) => !WRITE_TOOLS.has(t.name));
|
|
24
|
+
}
|
|
25
|
+
export const WRITE_DISABLED_MESSAGE = "Write operations are disabled. Set BING_ADS_MCP_WRITE=true in the MCP server environment to enable mutating tools (create/update/pause/enable/remove/apply).";
|
|
26
|
+
/**
|
|
27
|
+
* Assert that a tool call is allowed under the current write-mode setting.
|
|
28
|
+
* Throws a clear Error if the tool mutates state and writes are disabled.
|
|
29
|
+
*/
|
|
30
|
+
export function assertWriteAllowed(toolName, env = process.env) {
|
|
31
|
+
if (!isWriteTool(toolName))
|
|
32
|
+
return;
|
|
33
|
+
if (isWriteEnabled(env))
|
|
34
|
+
return;
|
|
35
|
+
throw new Error(`Tool "${toolName}" is a write operation. ${WRITE_DISABLED_MESSAGE}`);
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-bing-ads",
|
|
3
3
|
"mcpName": "io.github.mharnett/bing-ads",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"description": "MCP server for Microsoft Advertising (Bing Ads) API with campaign, ad group, keyword, and performance reporting. First comprehensive open-source Bing Ads MCP.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"zod": "^3.22.4"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
+
"@drak-marketing/mcp-test-harness": "^0.1.2",
|
|
61
62
|
"@types/node": "^20.10.0",
|
|
62
63
|
"tsx": "^4.7.0",
|
|
63
64
|
"typescript": "^5.3.0",
|