adtest-mcp 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/LICENSE +21 -0
- package/README.md +102 -0
- package/index.js +145 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AdTest.AI (Eyal Menaker)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# AdTest.AI MCP server
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/adtest-mcp) · Registry name: `ai.adtest/adtest-mcp` · License: MIT
|
|
4
|
+
|
|
5
|
+
Adds a single **`analyze_advert`** tool to Claude (Desktop, Code, or any MCP client) that
|
|
6
|
+
runs AdTest.AI's **AI-only** 13-dimension analysis of an advert — **image, video, or text** —
|
|
7
|
+
and returns a detailed scoring report. (Human-panel validation is available in the
|
|
8
|
+
[web app](https://app.adtest.ai) only, not through this server.)
|
|
9
|
+
|
|
10
|
+
Quick add for Claude Code:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
claude mcp add adtest -e ADTEST_API_KEY=adk_your_key_here -- npx -y adtest-mcp
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
1. **Get an API key:** sign in at <https://app.adtest.ai> → the **Developer** (gear)
|
|
19
|
+
icon → **Generate key**. Make sure your wallet has funds — each successful
|
|
20
|
+
analysis costs **$1**.
|
|
21
|
+
|
|
22
|
+
2. **Add the server to your Claude config** (Claude Desktop:
|
|
23
|
+
`claude_desktop_config.json`; Claude Code: `.mcp.json`):
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"adtest": {
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["-y", "adtest-mcp"],
|
|
31
|
+
"env": { "ADTEST_API_KEY": "adk_your_key_here" }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or run it from a local checkout (after `npm install` in this folder):
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"adtest": {
|
|
43
|
+
"command": "node",
|
|
44
|
+
"args": ["/absolute/path/to/mcp-server/index.js"],
|
|
45
|
+
"env": { "ADTEST_API_KEY": "adk_your_key_here" }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
3. **Restart Claude**, then just ask:
|
|
52
|
+
- *“Analyze this advert: https://example.com/ad.jpg”*
|
|
53
|
+
- *“Analyze this ad video file: /Users/me/promo.mp4”*
|
|
54
|
+
- *“Score this ad copy: 50% off all shoes this weekend — shop now.”*
|
|
55
|
+
|
|
56
|
+
## The tool
|
|
57
|
+
|
|
58
|
+
`analyze_advert` — provide **one** of:
|
|
59
|
+
|
|
60
|
+
| arg | meaning |
|
|
61
|
+
|-----|---------|
|
|
62
|
+
| `url` | public URL of an image or video advert (we download it) |
|
|
63
|
+
| `file_path` | local image/video file to upload |
|
|
64
|
+
| `text` | ad copy to analyze |
|
|
65
|
+
| `brand_url` *(optional)* | advertiser website — improves brand/logo detection |
|
|
66
|
+
|
|
67
|
+
Returns the AI analysis text plus the amount charged and your remaining balance.
|
|
68
|
+
|
|
69
|
+
## How it works (async — so video works)
|
|
70
|
+
|
|
71
|
+
The server submits the job to `POST /developer/ai-analysis/jobs`, gets a
|
|
72
|
+
`job_id` back immediately, then polls `GET /developer/ai-analysis/jobs/{job_id}`
|
|
73
|
+
every few seconds until it's `complete` or `failed`. This is why **video works**:
|
|
74
|
+
a single synchronous request would be cut off by the CDN edge timeout (~100s),
|
|
75
|
+
but video analysis can take several minutes. You're charged **once, on success**,
|
|
76
|
+
when the job completes — a failed job is never billed.
|
|
77
|
+
|
|
78
|
+
## Notes
|
|
79
|
+
|
|
80
|
+
- Registered in the official MCP Registry as `ai.adtest/adtest-mcp`.
|
|
81
|
+
|
|
82
|
+
- Auth is the `x-api-key` header, set from `ADTEST_API_KEY`.
|
|
83
|
+
- Endpoint defaults to `https://app.adtest.ai/adtest-api`; override with
|
|
84
|
+
`ADTEST_API_BASE` for staging.
|
|
85
|
+
- Poll ceiling defaults to 15 min; override with `ADTEST_POLL_TIMEOUT_MS`
|
|
86
|
+
(milliseconds) for very long video.
|
|
87
|
+
- Rate limits: 30 submits/min and 2000/day per key (60/min per IP); polling is
|
|
88
|
+
generous (240/min) and handled for you.
|
|
89
|
+
- Errors come back as readable text: `invalid_api_key`, `insufficient_funds`,
|
|
90
|
+
`no_creative`, `invalid_url`, `unsupported_media_type`, `too_many_requests`,
|
|
91
|
+
`analysis_failed` (the last is **not** charged).
|
|
92
|
+
- Requires Node ≥ 18.
|
|
93
|
+
|
|
94
|
+
## Links
|
|
95
|
+
|
|
96
|
+
- Product: <https://adtest.ai>
|
|
97
|
+
- App and API keys: <https://app.adtest.ai>
|
|
98
|
+
- Issues: <https://github.com/menaker/adtest-mcp/issues>
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT — see [LICENSE](LICENSE).
|
package/index.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AdTest MCP server — exposes the AI-only ad-analysis API as a Claude tool.
|
|
4
|
+
*
|
|
5
|
+
* Auth: set ADTEST_API_KEY to your AdTest developer API key
|
|
6
|
+
* (https://app.adtest.ai → Developer settings). Override the endpoint with
|
|
7
|
+
* ADTEST_API_BASE if needed (default https://app.adtest.ai/adtest-api).
|
|
8
|
+
*
|
|
9
|
+
* Tool: analyze_advert(url | file_path | text, brand_url?) → AI analysis text.
|
|
10
|
+
*
|
|
11
|
+
* Uses the ASYNC job flow (submit → poll) so VIDEO works: a long synchronous
|
|
12
|
+
* request would be cut off by the CDN edge timeout (~100s), but video analysis
|
|
13
|
+
* can take several minutes. Each successful analysis costs 1 credit from the
|
|
14
|
+
* wallet tied to the API key.
|
|
15
|
+
*/
|
|
16
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
17
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
|
+
import {
|
|
19
|
+
CallToolRequestSchema,
|
|
20
|
+
ListToolsRequestSchema,
|
|
21
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
23
|
+
import { basename } from "node:path";
|
|
24
|
+
|
|
25
|
+
const API_KEY = process.env.ADTEST_API_KEY || "";
|
|
26
|
+
const API_BASE = (process.env.ADTEST_API_BASE || "https://app.adtest.ai/adtest-api").replace(/\/+$/, "");
|
|
27
|
+
|
|
28
|
+
// Poll cadence + ceiling. Video (analyzeVideo + TwelveLabs) can take minutes.
|
|
29
|
+
const POLL_INTERVAL_MS = 3000;
|
|
30
|
+
const POLL_TIMEOUT_MS = Number(process.env.ADTEST_POLL_TIMEOUT_MS || 15 * 60 * 1000);
|
|
31
|
+
|
|
32
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
33
|
+
|
|
34
|
+
const TOOL = {
|
|
35
|
+
name: "analyze_advert",
|
|
36
|
+
description:
|
|
37
|
+
"Run an AI-only analysis of an advert and get a detailed scoring report. " +
|
|
38
|
+
"Provide exactly ONE of: `url` (a public image or video URL), `file_path` " +
|
|
39
|
+
"(a local image/video file to upload), or `text` (ad copy). Optionally pass " +
|
|
40
|
+
"`brand_url` to improve brand/logo detection. Video is supported and may take " +
|
|
41
|
+
"a few minutes. Each successful analysis costs 1 credit from your AdTest wallet.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
url: { type: "string", description: "Public URL of an image or video advert" },
|
|
46
|
+
file_path: { type: "string", description: "Local path to an image or video file to upload" },
|
|
47
|
+
text: { type: "string", description: "Ad copy text to analyze" },
|
|
48
|
+
brand_url: { type: "string", description: "Advertiser brand/website URL (optional)" },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const server = new Server(
|
|
54
|
+
{ name: "adtest-ai-analysis", version: "1.1.0" },
|
|
55
|
+
{ capabilities: { tools: {} } },
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [TOOL] }));
|
|
59
|
+
|
|
60
|
+
const errorResult = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
61
|
+
|
|
62
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
63
|
+
if (req.params.name !== TOOL.name) {
|
|
64
|
+
return errorResult(`Unknown tool: ${req.params.name}`);
|
|
65
|
+
}
|
|
66
|
+
if (!API_KEY) {
|
|
67
|
+
return errorResult(
|
|
68
|
+
"Missing ADTEST_API_KEY. Set it to your AdTest developer API key (app.adtest.ai → Developer settings).",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const { url, file_path, text, brand_url } = req.params.arguments || {};
|
|
72
|
+
if (!url && !file_path && !text) {
|
|
73
|
+
return errorResult("Provide one of: url, file_path, or text.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const submitEndpoint = `${API_BASE}/developer/ai-analysis/jobs`;
|
|
77
|
+
|
|
78
|
+
// 1) Submit the job → expect 202 { job_id }.
|
|
79
|
+
let resp;
|
|
80
|
+
try {
|
|
81
|
+
if (file_path) {
|
|
82
|
+
const buf = readFileSync(file_path);
|
|
83
|
+
const form = new FormData();
|
|
84
|
+
form.append("file", new Blob([buf]), basename(file_path));
|
|
85
|
+
if (brand_url) form.append("brand_url", brand_url);
|
|
86
|
+
resp = await fetch(submitEndpoint, { method: "POST", headers: { "x-api-key": API_KEY }, body: form });
|
|
87
|
+
} else {
|
|
88
|
+
resp = await fetch(submitEndpoint, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: { "x-api-key": API_KEY, "content-type": "application/json" },
|
|
91
|
+
body: JSON.stringify({ url, text, brand_url }),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return errorResult(`Request error: ${e.message}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let sub = {};
|
|
99
|
+
try { sub = await resp.json(); } catch { /* non-JSON body */ }
|
|
100
|
+
if (!resp.ok || !sub.job_id) {
|
|
101
|
+
const err = sub.error || `HTTP ${resp.status}`;
|
|
102
|
+
const hint = err === "insufficient_funds" ? " — top up your AdTest wallet." : "";
|
|
103
|
+
const detail = sub.detail ? ` (${sub.detail})` : "";
|
|
104
|
+
return errorResult(`Analysis failed: ${err}${detail}${hint}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 2) Poll until the job is complete or failed.
|
|
108
|
+
const pollUrl = `${submitEndpoint}/${sub.job_id}`;
|
|
109
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
110
|
+
let data = null;
|
|
111
|
+
while (Date.now() < deadline) {
|
|
112
|
+
await sleep(POLL_INTERVAL_MS);
|
|
113
|
+
let pr;
|
|
114
|
+
try {
|
|
115
|
+
pr = await fetch(pollUrl, { headers: { "x-api-key": API_KEY } });
|
|
116
|
+
} catch {
|
|
117
|
+
continue; // transient network blip — keep polling
|
|
118
|
+
}
|
|
119
|
+
if (pr.status === 429) continue; // rate-limited poll — back off one interval
|
|
120
|
+
let pj = {};
|
|
121
|
+
try { pj = await pr.json(); } catch { continue; }
|
|
122
|
+
if (pj.status === "complete") { data = pj; break; }
|
|
123
|
+
if (pj.status === "failed") {
|
|
124
|
+
return errorResult(`Analysis failed: ${pj.error || "analysis_failed"}`);
|
|
125
|
+
}
|
|
126
|
+
// status === "processing" → keep polling
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!data) {
|
|
130
|
+
return errorResult(
|
|
131
|
+
`Analysis is still processing after ${Math.round(POLL_TIMEOUT_MS / 60000)} min ` +
|
|
132
|
+
`(job ${sub.job_id}). It may still finish — try again shortly.`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const a = data.analysis || {};
|
|
137
|
+
const c = data.charge || {};
|
|
138
|
+
const sym = c.symbol || "$";
|
|
139
|
+
const footer = `\n\n— charged ${sym}${c.amount ?? 1} ${c.currency || ""} (balance: ${sym}${c.balance_after ?? "?"}), model: ${a.model || "?"}`.replace(/\s+\)/, ")");
|
|
140
|
+
return { content: [{ type: "text", text: (a.text || "(no analysis returned)") + footer }] };
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const transport = new StdioServerTransport();
|
|
144
|
+
await server.connect(transport);
|
|
145
|
+
console.error("adtest-ai-analysis MCP server running on stdio");
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "adtest-mcp",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "AI-only ad scoring for Claude: score image, video or text ads on 13 dimensions before you spend.",
|
|
5
|
+
"mcpName": "ai.adtest/adtest-mcp",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"adtest-mcp": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"model-context-protocol",
|
|
18
|
+
"claude",
|
|
19
|
+
"ad-testing",
|
|
20
|
+
"creative-testing",
|
|
21
|
+
"advertising",
|
|
22
|
+
"marketing",
|
|
23
|
+
"ai"
|
|
24
|
+
],
|
|
25
|
+
"homepage": "https://adtest.ai/",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/menaker/adtest-mcp/issues"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/menaker/adtest-mcp.git"
|
|
32
|
+
},
|
|
33
|
+
"author": "AdTest.AI <support@adtest.ai> (https://adtest.ai)",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|