priorrun-mcp 0.1.0__tar.gz
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.
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
.env
|
|
5
|
+
.env.local
|
|
6
|
+
.mcp.json
|
|
7
|
+
api/.env
|
|
8
|
+
web/.env.local
|
|
9
|
+
api/venv/
|
|
10
|
+
web/node_modules/
|
|
11
|
+
web/.next/
|
|
12
|
+
.next/
|
|
13
|
+
CSV/
|
|
14
|
+
.DS_Store
|
|
15
|
+
*.egg-info/
|
|
16
|
+
dist/
|
|
17
|
+
build/
|
|
18
|
+
.modal/
|
|
19
|
+
data/ab_tests/screenshots/
|
|
20
|
+
.gstack/
|
|
21
|
+
.claude/
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: priorrun-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Run Prior.Run audience simulations from Claude Desktop, Claude Code, Cursor, or any MCP-compatible agent.
|
|
5
|
+
Project-URL: Homepage, https://prior.run
|
|
6
|
+
Project-URL: Documentation, https://prior.run/docs/mcp
|
|
7
|
+
Project-URL: API Reference, https://prior.run/docs/api
|
|
8
|
+
Author-email: "Prior.Run" <hello@prior.run>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: ab-testing,audience-simulation,creative-testing,mcp,prior-run,synthetic-users
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: httpx>=0.27.0
|
|
20
|
+
Requires-Dist: mcp>=1.2.0
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# priorrun-mcp
|
|
24
|
+
|
|
25
|
+
MCP server for [Prior.Run](https://prior.run) — run synthetic-audience simulations
|
|
26
|
+
on design variants and ad creatives from Claude Desktop, Claude Code, Cursor, or
|
|
27
|
+
any MCP-compatible agent.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
Requires Python 3.11+. [Install `uv`](https://docs.astral.sh/uv/getting-started/installation/) if you don't have it.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uvx priorrun-mcp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
That's it — `uvx` fetches and runs the server on demand.
|
|
38
|
+
|
|
39
|
+
## Configure
|
|
40
|
+
|
|
41
|
+
Grab an API key from [prior.run/settings](https://prior.run/settings), then register the server with your agent host.
|
|
42
|
+
|
|
43
|
+
### Claude Desktop
|
|
44
|
+
|
|
45
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"prior-run": {
|
|
51
|
+
"command": "uvx",
|
|
52
|
+
"args": ["priorrun-mcp"],
|
|
53
|
+
"env": {
|
|
54
|
+
"PRIORRUN_API_KEY": "pr_live_xxxxxxxxxxxxxxxxxxxxxxxx"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Claude Code / Cursor
|
|
62
|
+
|
|
63
|
+
`~/.claude/mcp.json` (Claude Code) or `.cursor/mcp.json` (Cursor):
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"prior-run": {
|
|
69
|
+
"command": "uvx",
|
|
70
|
+
"args": ["priorrun-mcp"],
|
|
71
|
+
"env": { "PRIORRUN_API_KEY": "pr_live_..." }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Tools
|
|
78
|
+
|
|
79
|
+
| Tool | What it does |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `create_memo_review` | single design review |
|
|
82
|
+
| `create_memo_compare` | two design variants head-to-head |
|
|
83
|
+
| `create_memo_multi` | 3–5 design variants |
|
|
84
|
+
| `create_memo_flow` | two funnel flows (each 2–5 screens) |
|
|
85
|
+
| `create_ads_single` | single ad creative evaluation |
|
|
86
|
+
| `create_ads_compare` | two ad creatives head-to-head |
|
|
87
|
+
| `create_ads_multi` | 3–5 ad creatives |
|
|
88
|
+
| `get_memo` | fetch status + full memo JSON by id |
|
|
89
|
+
| `wait_for_memo` | block until synthesis completes |
|
|
90
|
+
|
|
91
|
+
Image arguments accept local file paths, `https://` URLs, or base64. Create
|
|
92
|
+
tools default to `wait=True` — the agent gets the completed memo in one tool
|
|
93
|
+
call.
|
|
94
|
+
|
|
95
|
+
## Example prompt
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
Run a Prior.Run compare on ~/desktop/landing-a.png vs ~/desktop/landing-b.png
|
|
99
|
+
for a Gen Z skincare awareness campaign. Hypothesis: the warmer palette
|
|
100
|
+
will lift click-through.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The agent calls `create_memo_compare`, waits ~90s for synthesis, and hands
|
|
104
|
+
you back the verdict, audience quotes, and memo URL.
|
|
105
|
+
|
|
106
|
+
## Environment variables
|
|
107
|
+
|
|
108
|
+
| Variable | Required | Default | Notes |
|
|
109
|
+
|---|---|---|---|
|
|
110
|
+
| `PRIORRUN_API_KEY` | yes | — | `pr_live_...` format. Generate at [prior.run/settings](https://prior.run/settings). |
|
|
111
|
+
| `PRIORRUN_API_BASE` | no | `https://api.prior.run` | Override for staging or local dev. |
|
|
112
|
+
|
|
113
|
+
## Links
|
|
114
|
+
|
|
115
|
+
- Product: [prior.run](https://prior.run)
|
|
116
|
+
- API docs: [prior.run/docs/api](https://prior.run/docs/api)
|
|
117
|
+
- MCP docs: [prior.run/docs/mcp](https://prior.run/docs/mcp)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# priorrun-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [Prior.Run](https://prior.run) — run synthetic-audience simulations
|
|
4
|
+
on design variants and ad creatives from Claude Desktop, Claude Code, Cursor, or
|
|
5
|
+
any MCP-compatible agent.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Requires Python 3.11+. [Install `uv`](https://docs.astral.sh/uv/getting-started/installation/) if you don't have it.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uvx priorrun-mcp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That's it — `uvx` fetches and runs the server on demand.
|
|
16
|
+
|
|
17
|
+
## Configure
|
|
18
|
+
|
|
19
|
+
Grab an API key from [prior.run/settings](https://prior.run/settings), then register the server with your agent host.
|
|
20
|
+
|
|
21
|
+
### Claude Desktop
|
|
22
|
+
|
|
23
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"prior-run": {
|
|
29
|
+
"command": "uvx",
|
|
30
|
+
"args": ["priorrun-mcp"],
|
|
31
|
+
"env": {
|
|
32
|
+
"PRIORRUN_API_KEY": "pr_live_xxxxxxxxxxxxxxxxxxxxxxxx"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Claude Code / Cursor
|
|
40
|
+
|
|
41
|
+
`~/.claude/mcp.json` (Claude Code) or `.cursor/mcp.json` (Cursor):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"prior-run": {
|
|
47
|
+
"command": "uvx",
|
|
48
|
+
"args": ["priorrun-mcp"],
|
|
49
|
+
"env": { "PRIORRUN_API_KEY": "pr_live_..." }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Tools
|
|
56
|
+
|
|
57
|
+
| Tool | What it does |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `create_memo_review` | single design review |
|
|
60
|
+
| `create_memo_compare` | two design variants head-to-head |
|
|
61
|
+
| `create_memo_multi` | 3–5 design variants |
|
|
62
|
+
| `create_memo_flow` | two funnel flows (each 2–5 screens) |
|
|
63
|
+
| `create_ads_single` | single ad creative evaluation |
|
|
64
|
+
| `create_ads_compare` | two ad creatives head-to-head |
|
|
65
|
+
| `create_ads_multi` | 3–5 ad creatives |
|
|
66
|
+
| `get_memo` | fetch status + full memo JSON by id |
|
|
67
|
+
| `wait_for_memo` | block until synthesis completes |
|
|
68
|
+
|
|
69
|
+
Image arguments accept local file paths, `https://` URLs, or base64. Create
|
|
70
|
+
tools default to `wait=True` — the agent gets the completed memo in one tool
|
|
71
|
+
call.
|
|
72
|
+
|
|
73
|
+
## Example prompt
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Run a Prior.Run compare on ~/desktop/landing-a.png vs ~/desktop/landing-b.png
|
|
77
|
+
for a Gen Z skincare awareness campaign. Hypothesis: the warmer palette
|
|
78
|
+
will lift click-through.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The agent calls `create_memo_compare`, waits ~90s for synthesis, and hands
|
|
82
|
+
you back the verdict, audience quotes, and memo URL.
|
|
83
|
+
|
|
84
|
+
## Environment variables
|
|
85
|
+
|
|
86
|
+
| Variable | Required | Default | Notes |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| `PRIORRUN_API_KEY` | yes | — | `pr_live_...` format. Generate at [prior.run/settings](https://prior.run/settings). |
|
|
89
|
+
| `PRIORRUN_API_BASE` | no | `https://api.prior.run` | Override for staging or local dev. |
|
|
90
|
+
|
|
91
|
+
## Links
|
|
92
|
+
|
|
93
|
+
- Product: [prior.run](https://prior.run)
|
|
94
|
+
- API docs: [prior.run/docs/api](https://prior.run/docs/api)
|
|
95
|
+
- MCP docs: [prior.run/docs/mcp](https://prior.run/docs/mcp)
|
|
File without changes
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prior.Run MCP server — exposes memo generation as tools for Claude Desktop, Cursor, Claude Code.
|
|
3
|
+
|
|
4
|
+
Env:
|
|
5
|
+
PRIORRUN_API_KEY — required, format pr_live_...
|
|
6
|
+
PRIORRUN_API_BASE — optional, defaults to https://api.prior.run
|
|
7
|
+
|
|
8
|
+
Run:
|
|
9
|
+
uvx priorrun-mcp (from PyPI — recommended)
|
|
10
|
+
python -m priorrun_mcp.server (from a local clone)
|
|
11
|
+
|
|
12
|
+
Tools:
|
|
13
|
+
create_memo_compare — two-variant design comparison (non-ads)
|
|
14
|
+
create_ads_single — single ad creative evaluation
|
|
15
|
+
create_ads_compare — two ad creatives head-to-head
|
|
16
|
+
get_memo — fetch memo by id (polls-friendly)
|
|
17
|
+
wait_for_memo — block until a memo reaches status=complete
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import base64
|
|
24
|
+
import os
|
|
25
|
+
import pathlib
|
|
26
|
+
import time
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
import httpx
|
|
30
|
+
from mcp.server.fastmcp import FastMCP
|
|
31
|
+
|
|
32
|
+
API_KEY = os.environ.get("PRIORRUN_API_KEY", "")
|
|
33
|
+
API_BASE = os.environ.get("PRIORRUN_API_BASE", "https://api.prior.run").rstrip("/")
|
|
34
|
+
|
|
35
|
+
if not API_KEY:
|
|
36
|
+
raise SystemExit("PRIORRUN_API_KEY is required (create one at /account/api-keys)")
|
|
37
|
+
|
|
38
|
+
mcp = FastMCP("prior-run")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _headers() -> dict[str, str]:
|
|
42
|
+
return {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _load_image(src: str) -> str:
|
|
46
|
+
"""
|
|
47
|
+
Normalize image input to base64.
|
|
48
|
+
Accepts: local file path, https URL (passed through), or already-base64 string.
|
|
49
|
+
"""
|
|
50
|
+
if src.startswith("http://") or src.startswith("https://"):
|
|
51
|
+
return src # let server fetch
|
|
52
|
+
path = pathlib.Path(os.path.expanduser(src))
|
|
53
|
+
if path.exists() and path.is_file():
|
|
54
|
+
return base64.b64encode(path.read_bytes()).decode()
|
|
55
|
+
return src # assume already base64
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def _post(endpoint: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
59
|
+
async with httpx.AsyncClient(timeout=600) as client:
|
|
60
|
+
r = await client.post(f"{API_BASE}{endpoint}", json=payload, headers=_headers())
|
|
61
|
+
if r.status_code >= 400:
|
|
62
|
+
raise RuntimeError(f"{endpoint} failed ({r.status_code}): {r.text[:400]}")
|
|
63
|
+
return r.json()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def _get(endpoint: str) -> dict[str, Any]:
|
|
67
|
+
async with httpx.AsyncClient(timeout=60) as client:
|
|
68
|
+
r = await client.get(f"{API_BASE}{endpoint}", headers=_headers())
|
|
69
|
+
if r.status_code >= 400:
|
|
70
|
+
raise RuntimeError(f"{endpoint} failed ({r.status_code}): {r.text[:400]}")
|
|
71
|
+
return r.json()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@mcp.tool()
|
|
75
|
+
async def create_memo_compare(
|
|
76
|
+
image_a: str,
|
|
77
|
+
image_b: str,
|
|
78
|
+
audience_template: str = "general_consumer",
|
|
79
|
+
custom_audience: str | None = None,
|
|
80
|
+
metric: str = "conversion rate",
|
|
81
|
+
hypothesis: str = "",
|
|
82
|
+
) -> dict:
|
|
83
|
+
"""
|
|
84
|
+
Create a two-variant comparison memo. Returns {memo_id, url, status, memo}.
|
|
85
|
+
|
|
86
|
+
image_a / image_b: local file path, https URL, or base64 string.
|
|
87
|
+
audience_template: e.g. general_consumer, enterprise_buyer, gen_z (see Prior.Run docs).
|
|
88
|
+
custom_audience: Pro+ only — natural-language audience description.
|
|
89
|
+
"""
|
|
90
|
+
payload = {
|
|
91
|
+
"image_a": _load_image(image_a),
|
|
92
|
+
"image_b": _load_image(image_b),
|
|
93
|
+
"audience_template": audience_template,
|
|
94
|
+
"metric": metric,
|
|
95
|
+
"hypothesis": hypothesis,
|
|
96
|
+
}
|
|
97
|
+
if custom_audience:
|
|
98
|
+
payload["custom_audience"] = custom_audience
|
|
99
|
+
return await _post("/api/v1/memo/compare", payload)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@mcp.tool()
|
|
103
|
+
async def create_ads_single(
|
|
104
|
+
image: str,
|
|
105
|
+
campaign_context: str,
|
|
106
|
+
creative_name: str = "Untitled Creative",
|
|
107
|
+
run_platform: str | None = None,
|
|
108
|
+
wait: bool = True,
|
|
109
|
+
) -> dict:
|
|
110
|
+
"""
|
|
111
|
+
Evaluate a single ad creative. Returns memo_id + url; with wait=True blocks until complete.
|
|
112
|
+
|
|
113
|
+
image: local file path, https URL, or base64.
|
|
114
|
+
campaign_context: what is this ad trying to accomplish? (required, min 10 chars)
|
|
115
|
+
run_platform: meta | tiktok | google (optional — tailors benchmarks).
|
|
116
|
+
"""
|
|
117
|
+
payload = {
|
|
118
|
+
"image": _load_image(image),
|
|
119
|
+
"campaign_context": campaign_context,
|
|
120
|
+
"creative_name": creative_name,
|
|
121
|
+
}
|
|
122
|
+
if run_platform:
|
|
123
|
+
payload["run_platform"] = run_platform
|
|
124
|
+
created = await _post("/api/v1/ads/single", payload)
|
|
125
|
+
if wait:
|
|
126
|
+
return await wait_for_memo(created["memo_id"])
|
|
127
|
+
return created
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@mcp.tool()
|
|
131
|
+
async def create_memo_review(
|
|
132
|
+
image: str,
|
|
133
|
+
audience_template: str = "general_consumer",
|
|
134
|
+
custom_audience: str | None = None,
|
|
135
|
+
metric: str = "conversion rate",
|
|
136
|
+
hypothesis: str = "",
|
|
137
|
+
wait: bool = True,
|
|
138
|
+
) -> dict:
|
|
139
|
+
"""Single-variant design review memo. With wait=True blocks until complete."""
|
|
140
|
+
payload = {
|
|
141
|
+
"image": _load_image(image),
|
|
142
|
+
"audience_template": audience_template,
|
|
143
|
+
"metric": metric,
|
|
144
|
+
"hypothesis": hypothesis,
|
|
145
|
+
}
|
|
146
|
+
if custom_audience:
|
|
147
|
+
payload["custom_audience"] = custom_audience
|
|
148
|
+
created = await _post("/api/v1/memo/review", payload)
|
|
149
|
+
return await wait_for_memo(created["memo_id"]) if wait else created
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@mcp.tool()
|
|
153
|
+
async def create_memo_multi(
|
|
154
|
+
images: list[str],
|
|
155
|
+
audience_template: str = "general_consumer",
|
|
156
|
+
custom_audience: str | None = None,
|
|
157
|
+
metric: str = "conversion rate",
|
|
158
|
+
hypothesis: str = "",
|
|
159
|
+
wait: bool = True,
|
|
160
|
+
) -> dict:
|
|
161
|
+
"""Compare 3–5 design variants. images: list of paths/URLs/base64."""
|
|
162
|
+
payload = {
|
|
163
|
+
"images": [_load_image(i) for i in images],
|
|
164
|
+
"audience_template": audience_template,
|
|
165
|
+
"metric": metric,
|
|
166
|
+
"hypothesis": hypothesis,
|
|
167
|
+
}
|
|
168
|
+
if custom_audience:
|
|
169
|
+
payload["custom_audience"] = custom_audience
|
|
170
|
+
created = await _post("/api/v1/memo/multi", payload)
|
|
171
|
+
return await wait_for_memo(created["memo_id"]) if wait else created
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@mcp.tool()
|
|
175
|
+
async def create_memo_flow(
|
|
176
|
+
flow_a: list[str],
|
|
177
|
+
flow_b: list[str],
|
|
178
|
+
audience_template: str = "general_consumer",
|
|
179
|
+
custom_audience: str | None = None,
|
|
180
|
+
metric: str = "conversion rate",
|
|
181
|
+
hypothesis: str = "",
|
|
182
|
+
wait: bool = True,
|
|
183
|
+
) -> dict:
|
|
184
|
+
"""Compare two ordered funnel flows (each 2–5 screens)."""
|
|
185
|
+
payload = {
|
|
186
|
+
"flow_a": [_load_image(i) for i in flow_a],
|
|
187
|
+
"flow_b": [_load_image(i) for i in flow_b],
|
|
188
|
+
"audience_template": audience_template,
|
|
189
|
+
"metric": metric,
|
|
190
|
+
"hypothesis": hypothesis,
|
|
191
|
+
}
|
|
192
|
+
if custom_audience:
|
|
193
|
+
payload["custom_audience"] = custom_audience
|
|
194
|
+
created = await _post("/api/v1/memo/flow", payload)
|
|
195
|
+
return await wait_for_memo(created["memo_id"]) if wait else created
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@mcp.tool()
|
|
199
|
+
async def create_ads_multi(
|
|
200
|
+
images: list[str],
|
|
201
|
+
campaign_context: str,
|
|
202
|
+
names: list[str] | None = None,
|
|
203
|
+
run_platform: str | None = None,
|
|
204
|
+
wait: bool = True,
|
|
205
|
+
) -> dict:
|
|
206
|
+
"""Compare 3–5 ad creatives in one memo."""
|
|
207
|
+
payload: dict = {
|
|
208
|
+
"images": [_load_image(i) for i in images],
|
|
209
|
+
"campaign_context": campaign_context,
|
|
210
|
+
}
|
|
211
|
+
if names:
|
|
212
|
+
payload["names"] = names
|
|
213
|
+
if run_platform:
|
|
214
|
+
payload["run_platform"] = run_platform
|
|
215
|
+
created = await _post("/api/v1/ads/multi", payload)
|
|
216
|
+
return await wait_for_memo(created["memo_id"]) if wait else created
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@mcp.tool()
|
|
220
|
+
async def create_ads_compare(
|
|
221
|
+
image_a: str,
|
|
222
|
+
image_b: str,
|
|
223
|
+
campaign_context: str,
|
|
224
|
+
name_a: str = "Creative A",
|
|
225
|
+
name_b: str = "Creative B",
|
|
226
|
+
run_platform: str | None = None,
|
|
227
|
+
wait: bool = True,
|
|
228
|
+
) -> dict:
|
|
229
|
+
"""Compare two ad creatives. With wait=True (default) blocks until synthesis finishes."""
|
|
230
|
+
payload = {
|
|
231
|
+
"image_a": _load_image(image_a),
|
|
232
|
+
"image_b": _load_image(image_b),
|
|
233
|
+
"campaign_context": campaign_context,
|
|
234
|
+
"name_a": name_a,
|
|
235
|
+
"name_b": name_b,
|
|
236
|
+
}
|
|
237
|
+
if run_platform:
|
|
238
|
+
payload["run_platform"] = run_platform
|
|
239
|
+
created = await _post("/api/v1/ads/compare", payload)
|
|
240
|
+
if wait:
|
|
241
|
+
return await wait_for_memo(created["memo_id"])
|
|
242
|
+
return created
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@mcp.tool()
|
|
246
|
+
async def get_memo(memo_id: str) -> dict:
|
|
247
|
+
"""Fetch memo JSON. Returns {memo_id, status, url, memo, created_at}."""
|
|
248
|
+
return await _get(f"/api/v1/memo/{memo_id}")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@mcp.tool()
|
|
252
|
+
async def wait_for_memo(memo_id: str, timeout_seconds: int = 300) -> dict:
|
|
253
|
+
"""Poll GET /memo/{id} until status == 'complete' or timeout."""
|
|
254
|
+
deadline = time.time() + timeout_seconds
|
|
255
|
+
delay = 2.0
|
|
256
|
+
while time.time() < deadline:
|
|
257
|
+
data = await _get(f"/api/v1/memo/{memo_id}")
|
|
258
|
+
if data.get("status") == "complete":
|
|
259
|
+
return data
|
|
260
|
+
await asyncio.sleep(delay)
|
|
261
|
+
delay = min(delay * 1.3, 10.0)
|
|
262
|
+
raise TimeoutError(f"Memo {memo_id} did not complete within {timeout_seconds}s")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def main() -> None:
|
|
266
|
+
"""Entry point for `uvx priorrun-mcp` / console_scripts."""
|
|
267
|
+
mcp.run()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
if __name__ == "__main__":
|
|
271
|
+
main()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "priorrun-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Run Prior.Run audience simulations from Claude Desktop, Claude Code, Cursor, or any MCP-compatible agent."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Prior.Run", email = "hello@prior.run" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["mcp", "prior-run", "synthetic-users", "ab-testing", "creative-testing", "audience-simulation"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"mcp>=1.2.0",
|
|
23
|
+
"httpx>=0.27.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://prior.run"
|
|
28
|
+
Documentation = "https://prior.run/docs/mcp"
|
|
29
|
+
"API Reference" = "https://prior.run/docs/api"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
priorrun-mcp = "priorrun_mcp.server:main"
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["hatchling"]
|
|
36
|
+
build-backend = "hatchling.build"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["priorrun_mcp"]
|