@ppcassist/amazon-ads-mcp 1.0.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 ADDED
@@ -0,0 +1,67 @@
1
+ # @ppcassist/amazon-ads-mcp
2
+
3
+ Local MCP proxy for the Amazon Ads API. Sits between Claude Desktop and Amazon's official Advertising MCP server, handling OAuth authentication and token refresh automatically.
4
+
5
+ ## How it works
6
+
7
+ 1. On startup, exchanges your refresh token for an access token via Amazon OAuth
8
+ 2. Auto-refreshes the token every 50 minutes
9
+ 3. Proxies all MCP requests from Claude Desktop to the Amazon Ads MCP server
10
+ 4. Injects required authentication headers on every request
11
+ 5. Streams SSE responses back to Claude without buffering
12
+
13
+ ## Prerequisites
14
+
15
+ You need Amazon Ads API credentials:
16
+
17
+ - **CLIENT_ID** - Your Amazon Ads API client ID (Login with Amazon)
18
+ - **CLIENT_SECRET** - Your Amazon Ads API client secret
19
+ - **REFRESH_TOKEN** - OAuth refresh token for your Amazon Ads account
20
+ - **PROFILE_ID** - Your Amazon Advertising profile ID
21
+ - **REGION** - One of `EU`, `NA`, or `FE`
22
+
23
+ ## Setup with Claude Desktop
24
+
25
+ Add to your Claude Desktop configuration (`claude_desktop_config.json`):
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "amazon-ads": {
31
+ "command": "npx",
32
+ "args": ["-y", "@ppcassist/amazon-ads-mcp"],
33
+ "env": {
34
+ "CLIENT_ID": "amzn1.application-oa2-client.xxxxx",
35
+ "CLIENT_SECRET": "your-client-secret",
36
+ "REFRESH_TOKEN": "Atzr|your-refresh-token",
37
+ "PROFILE_ID": "your-profile-id",
38
+ "REGION": "EU"
39
+ }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ ## Supported regions
46
+
47
+ | Region | Endpoint |
48
+ |--------|----------|
49
+ | NA | `https://advertising-ai.amazon.com/mcp` |
50
+ | EU | `https://advertising-ai-eu.amazon.com/mcp` |
51
+ | FE | `https://advertising-ai-fe.amazon.com/mcp` |
52
+
53
+ ## How it proxies
54
+
55
+ All MCP JSON-RPC messages (`initialize`, `tools/list`, `tools/call`, etc.) are forwarded to Amazon's MCP server with these headers injected:
56
+
57
+ - `Authorization: Bearer <access_token>`
58
+ - `Amazon-Ads-ClientId: <CLIENT_ID>`
59
+ - `Amazon-Advertising-API-Scope: <PROFILE_ID>`
60
+ - `Amazon-Ads-AI-Account-Selection-Mode: FIXED`
61
+ - `Accept: application/json, text/event-stream`
62
+
63
+ Responses (including SSE streams) are forwarded back to Claude as-is.
64
+
65
+ ## License
66
+
67
+ MIT
package/index.js ADDED
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { TokenManager } = require("./lib/auth");
4
+ const { McpProxy } = require("./lib/proxy");
5
+
6
+ const log = (msg) => process.stderr.write(`[amazon-ads-mcp] ${msg}\n`);
7
+
8
+ function requireEnv(name) {
9
+ const value = process.env[name];
10
+ if (!value) {
11
+ log(`ERROR: Missing required environment variable: ${name}`);
12
+ process.exit(1);
13
+ }
14
+ return value;
15
+ }
16
+
17
+ async function main() {
18
+ const clientId = requireEnv("CLIENT_ID");
19
+ const clientSecret = requireEnv("CLIENT_SECRET");
20
+ const refreshToken = requireEnv("REFRESH_TOKEN");
21
+ const profileId = requireEnv("PROFILE_ID");
22
+ const region = requireEnv("REGION");
23
+
24
+ // Initialize OAuth token manager
25
+ const tokenManager = new TokenManager({ clientId, clientSecret, refreshToken });
26
+ try {
27
+ await tokenManager.init();
28
+ } catch (err) {
29
+ log(`Failed to obtain access token: ${err.message}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ // Initialize MCP proxy
34
+ const proxy = new McpProxy({ tokenManager, clientId, profileId, region });
35
+
36
+ log(`Started (region=${region})`);
37
+
38
+ // Read JSON-RPC messages from stdin (newline-delimited)
39
+ let buffer = "";
40
+
41
+ process.stdin.setEncoding("utf8");
42
+ process.stdin.on("data", (chunk) => {
43
+ buffer += chunk;
44
+
45
+ // Process all complete lines in the buffer
46
+ let newlineIdx;
47
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
48
+ const line = buffer.slice(0, newlineIdx).trim();
49
+ buffer = buffer.slice(newlineIdx + 1);
50
+
51
+ if (!line) continue;
52
+
53
+ let request;
54
+ try {
55
+ request = JSON.parse(line);
56
+ } catch (err) {
57
+ log(`Failed to parse JSON-RPC message: ${err.message}`);
58
+ continue;
59
+ }
60
+
61
+ handleRequest(proxy, request);
62
+ }
63
+ });
64
+
65
+ process.stdin.on("end", () => {
66
+ log("stdin closed, shutting down");
67
+ tokenManager.destroy();
68
+ process.exit(0);
69
+ });
70
+ }
71
+
72
+ async function handleRequest(proxy, request) {
73
+ const { id, method } = request;
74
+
75
+ try {
76
+ const result = await proxy.forward(request);
77
+
78
+ if (result.type === "stream") {
79
+ // SSE stream: pipe each event line directly to stdout
80
+ result.stream.on("data", (chunk) => {
81
+ process.stdout.write(chunk);
82
+ });
83
+
84
+ result.stream.on("end", () => {
85
+ // SSE streams end naturally; nothing extra needed
86
+ });
87
+
88
+ result.stream.on("error", (err) => {
89
+ log(`SSE stream error (id=${id}): ${err.message}`);
90
+ writeJsonRpc({
91
+ jsonrpc: "2.0",
92
+ id,
93
+ error: { code: -32000, message: `Stream error: ${err.message}` },
94
+ });
95
+ });
96
+ } else if (result.type === "json") {
97
+ writeJsonRpc(result.data);
98
+ } else {
99
+ // Raw / unexpected response
100
+ log(`Unexpected response (status=${result.statusCode}): ${result.data}`);
101
+ writeJsonRpc({
102
+ jsonrpc: "2.0",
103
+ id,
104
+ error: {
105
+ code: -32000,
106
+ message: `Upstream error (HTTP ${result.statusCode})`,
107
+ },
108
+ });
109
+ }
110
+ } catch (err) {
111
+ log(`Proxy error (method=${method}, id=${id}): ${err.message}`);
112
+ writeJsonRpc({
113
+ jsonrpc: "2.0",
114
+ id,
115
+ error: { code: -32000, message: err.message },
116
+ });
117
+ }
118
+ }
119
+
120
+ function writeJsonRpc(obj) {
121
+ process.stdout.write(JSON.stringify(obj) + "\n");
122
+ }
123
+
124
+ main().catch((err) => {
125
+ log(`Fatal: ${err.message}`);
126
+ process.exit(1);
127
+ });
package/lib/auth.js ADDED
@@ -0,0 +1,85 @@
1
+ const https = require("https");
2
+
3
+ const TOKEN_URL = "https://api.amazon.com/auth/o2/token";
4
+ const REFRESH_INTERVAL_MS = 50 * 60 * 1000; // 50 minutes
5
+
6
+ class TokenManager {
7
+ constructor({ clientId, clientSecret, refreshToken }) {
8
+ this.clientId = clientId;
9
+ this.clientSecret = clientSecret;
10
+ this.refreshToken = refreshToken;
11
+ this.accessToken = null;
12
+ this.timer = null;
13
+ }
14
+
15
+ async init() {
16
+ await this.refresh();
17
+ this.timer = setInterval(() => this.refresh(), REFRESH_INTERVAL_MS);
18
+ this.timer.unref(); // don't keep process alive just for refresh
19
+ }
20
+
21
+ async refresh() {
22
+ const body = new URLSearchParams({
23
+ grant_type: "refresh_token",
24
+ client_id: this.clientId,
25
+ client_secret: this.clientSecret,
26
+ refresh_token: this.refreshToken,
27
+ }).toString();
28
+
29
+ const data = await this._post(TOKEN_URL, body);
30
+
31
+ if (data.error) {
32
+ throw new Error(`OAuth error: ${data.error} - ${data.error_description || ""}`);
33
+ }
34
+
35
+ this.accessToken = data.access_token;
36
+ process.stderr.write(`[auth] Token refreshed successfully\n`);
37
+ }
38
+
39
+ getToken() {
40
+ if (!this.accessToken) {
41
+ throw new Error("Access token not available - call init() first");
42
+ }
43
+ return this.accessToken;
44
+ }
45
+
46
+ destroy() {
47
+ if (this.timer) {
48
+ clearInterval(this.timer);
49
+ this.timer = null;
50
+ }
51
+ }
52
+
53
+ _post(urlStr, body) {
54
+ return new Promise((resolve, reject) => {
55
+ const url = new URL(urlStr);
56
+ const options = {
57
+ hostname: url.hostname,
58
+ path: url.pathname,
59
+ method: "POST",
60
+ headers: {
61
+ "Content-Type": "application/x-www-form-urlencoded",
62
+ "Content-Length": Buffer.byteLength(body),
63
+ },
64
+ };
65
+
66
+ const req = https.request(options, (res) => {
67
+ let chunks = [];
68
+ res.on("data", (chunk) => chunks.push(chunk));
69
+ res.on("end", () => {
70
+ try {
71
+ resolve(JSON.parse(Buffer.concat(chunks).toString()));
72
+ } catch (e) {
73
+ reject(new Error(`Failed to parse token response: ${e.message}`));
74
+ }
75
+ });
76
+ });
77
+
78
+ req.on("error", reject);
79
+ req.write(body);
80
+ req.end();
81
+ });
82
+ }
83
+ }
84
+
85
+ module.exports = { TokenManager };
package/lib/proxy.js ADDED
@@ -0,0 +1,70 @@
1
+ const https = require("https");
2
+
3
+ const ENDPOINTS = {
4
+ EU: "https://advertising-ai-eu.amazon.com/mcp",
5
+ NA: "https://advertising-ai.amazon.com/mcp",
6
+ FE: "https://advertising-ai-fe.amazon.com/mcp",
7
+ };
8
+
9
+ class McpProxy {
10
+ constructor({ tokenManager, clientId, profileId, region }) {
11
+ this.tokenManager = tokenManager;
12
+ this.clientId = clientId;
13
+ this.profileId = profileId;
14
+
15
+ const endpoint = ENDPOINTS[region.toUpperCase()];
16
+ if (!endpoint) {
17
+ throw new Error(`Invalid REGION "${region}". Must be EU, NA, or FE.`);
18
+ }
19
+ this.endpoint = new URL(endpoint);
20
+ }
21
+
22
+ async forward(jsonRpcRequest) {
23
+ const body = JSON.stringify(jsonRpcRequest);
24
+ const token = this.tokenManager.getToken();
25
+
26
+ const options = {
27
+ hostname: this.endpoint.hostname,
28
+ path: this.endpoint.pathname,
29
+ method: "POST",
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ "Content-Length": Buffer.byteLength(body),
33
+ Authorization: `Bearer ${token}`,
34
+ "Amazon-Ads-ClientId": this.clientId,
35
+ "Amazon-Advertising-API-Scope": this.profileId,
36
+ "Amazon-Ads-AI-Account-Selection-Mode": "FIXED",
37
+ Accept: "application/json, text/event-stream",
38
+ },
39
+ };
40
+
41
+ return new Promise((resolve, reject) => {
42
+ const req = https.request(options, (res) => {
43
+ const contentType = res.headers["content-type"] || "";
44
+ const isSSE = contentType.includes("text/event-stream");
45
+
46
+ if (isSSE) {
47
+ resolve({ type: "stream", stream: res });
48
+ } else {
49
+ let chunks = [];
50
+ res.on("data", (chunk) => chunks.push(chunk));
51
+ res.on("end", () => {
52
+ const raw = Buffer.concat(chunks).toString();
53
+ try {
54
+ resolve({ type: "json", data: JSON.parse(raw) });
55
+ } catch {
56
+ // Return raw text if not valid JSON (error pages, etc.)
57
+ resolve({ type: "raw", data: raw, statusCode: res.statusCode });
58
+ }
59
+ });
60
+ }
61
+ });
62
+
63
+ req.on("error", reject);
64
+ req.write(body);
65
+ req.end();
66
+ });
67
+ }
68
+ }
69
+
70
+ module.exports = { McpProxy };
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@ppcassist/amazon-ads-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Local MCP proxy for Amazon Ads API - authenticates and forwards requests to Amazon's official MCP server",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "amazon-ads-mcp": "./index.js"
8
+ },
9
+ "keywords": [
10
+ "amazon-ads",
11
+ "mcp",
12
+ "model-context-protocol",
13
+ "ppc",
14
+ "advertising",
15
+ "proxy"
16
+ ],
17
+ "author": "PPCAssist",
18
+ "license": "MIT",
19
+ "engines": {
20
+ "node": ">=18.0.0"
21
+ },
22
+ "files": [
23
+ "index.js",
24
+ "lib/",
25
+ "README.md"
26
+ ]
27
+ }