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"]