@50heads/mcp 2026.925.2 → 2026.1009.1
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/CHANGELOG.md +58 -9
- package/README.md +7 -4
- package/dist/cli.js +3538 -1542
- package/dist/index.js +3539 -1543
- package/dist/types/cli/stdio.d.ts +774 -44
- package/dist/types/constants.d.ts +4 -2
- package/dist/types/generated/skill.d.ts +1 -1
- package/dist/types/language.d.ts +14 -1
- package/dist/types/manifest.d.ts +1 -2
- package/dist/types/pricing.d.ts +5 -0
- package/dist/types/schemas.d.ts +1250 -53
- package/dist/types/server.d.ts +2 -1
- package/package.json +7 -8
- package/server.json +5 -6
- package/skills/50heads/SKILL.md +2 -2
- package/skills/50heads/references/pricing.md +4 -2
- package/skills/50heads/references/question-types.md +19 -2
- package/skills/50heads/references/tools.md +5 -3
- package/skills/50heads/scripts/estimate.py +31 -3
- package/skills/50heads.zip +0 -0
package/dist/types/server.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export declare function clientName(ctx: ServerContext): string | undefined;
|
|
|
66
66
|
export declare function createServer(opts: ServerOptions): McpServer;
|
|
67
67
|
/**
|
|
68
68
|
* A first draft from a goal and assets: two image URLs make an A or B question; listed
|
|
69
|
-
* options make a single choice; otherwise a yes
|
|
69
|
+
* options make a single choice; otherwise a yes or no check. The model refines it. Mostly is
|
|
70
|
+
* yes_mostly_no, and only when the asker asks for that middle answer.
|
|
70
71
|
*/
|
|
71
72
|
export declare function draftFromGoal(goal: string, assets?: string): Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@50heads/mcp",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.1009.1",
|
|
4
4
|
"mcpName": "com.50heads/mcp",
|
|
5
|
-
"description": "50heads MCP: ask fifty verified
|
|
5
|
+
"description": "50heads MCP: ask fifty verified people from Claude, ChatGPT, Cursor or your own agent.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mcp",
|
|
8
8
|
"model-context-protocol",
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
"homepage": "https://50heads.com/docs/mcp",
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
16
|
-
"url": "https://github.com/50heads/mcp.git"
|
|
17
|
-
"directory": "packages/mcp"
|
|
16
|
+
"url": "https://github.com/50heads/mcp.git"
|
|
18
17
|
},
|
|
19
18
|
"bugs": "https://github.com/50heads/mcp/issues",
|
|
20
19
|
"license": "MIT",
|
|
@@ -54,14 +53,14 @@
|
|
|
54
53
|
"zod": "^4.6.5"
|
|
55
54
|
},
|
|
56
55
|
"devDependencies": {
|
|
57
|
-
"@modelcontextprotocol/client": "2.
|
|
56
|
+
"@modelcontextprotocol/client": "2.2.0",
|
|
58
57
|
"@types/node": "^22.18.0",
|
|
59
58
|
"esbuild": "^0.28.2",
|
|
60
59
|
"typescript": "^5.9.3",
|
|
61
|
-
"@50heads/brand": "0.0.0",
|
|
62
60
|
"@50heads/login": "0.0.0",
|
|
63
|
-
"@50heads/
|
|
64
|
-
"@50heads/shared": "0.0.0"
|
|
61
|
+
"@50heads/brand": "0.0.0",
|
|
62
|
+
"@50heads/shared": "0.0.0",
|
|
63
|
+
"@50heads/tsconfig": "0.0.0"
|
|
65
64
|
},
|
|
66
65
|
"scripts": {
|
|
67
66
|
"generate": "node scripts/generate.ts",
|
package/server.json
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "com.50heads/mcp",
|
|
4
4
|
"title": "50heads",
|
|
5
|
-
"description": "Ask fifty verified
|
|
6
|
-
"version": "2026.
|
|
5
|
+
"description": "Ask fifty verified people a five-second question and get the split back.",
|
|
6
|
+
"version": "2026.1009.1",
|
|
7
7
|
"websiteUrl": "https://50heads.com/agents",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/50heads/mcp",
|
|
10
|
-
"source": "github"
|
|
11
|
-
"subfolder": "packages/mcp"
|
|
10
|
+
"source": "github"
|
|
12
11
|
},
|
|
13
12
|
"icons": [
|
|
14
13
|
{
|
|
@@ -45,7 +44,7 @@
|
|
|
45
44
|
"registryType": "npm",
|
|
46
45
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
47
46
|
"identifier": "@50heads/mcp",
|
|
48
|
-
"version": "2026.
|
|
47
|
+
"version": "2026.1009.1",
|
|
49
48
|
"runtimeHint": "npx",
|
|
50
49
|
"transport": {
|
|
51
50
|
"type": "stdio"
|
|
@@ -78,7 +77,7 @@
|
|
|
78
77
|
"account:read"
|
|
79
78
|
]
|
|
80
79
|
},
|
|
81
|
-
"skillVersion": "2026.
|
|
80
|
+
"skillVersion": "2026.1009.1"
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
}
|
package/skills/50heads/SKILL.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: 50heads
|
|
3
|
-
description: Ask fifty verified
|
|
3
|
+
description: Ask fifty verified people a five-second question and get a distribution back; use when the work in hand needs a human preference, first impression or sanity check that a model cannot supply.
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: "2026.
|
|
6
|
+
version: "2026.1009.1"
|
|
7
7
|
homepage: https://50heads.com/agents
|
|
8
8
|
allowed-tools: "50heads:*"
|
|
9
9
|
---
|
|
@@ -8,11 +8,11 @@ Per answer = tier base × length × format, plus images and targeting traits, ti
|
|
|
8
8
|
| --- | --- | --- | --- |
|
|
9
9
|
| 1 | phone-verified | 20 | 1000 |
|
|
10
10
|
| 2 | ID-verified | 42 | 2100 |
|
|
11
|
-
| 3 |
|
|
11
|
+
| 3 | established heads | 80 | 4000 |
|
|
12
12
|
|
|
13
13
|
Length (question, context and options together): up to 150 characters × 1.0, up to 300 × 1.1, up to 600 × 1.25.
|
|
14
14
|
|
|
15
|
-
Format: `single_choice` × 1.0, `multi_choice` × 1.1, `ab_image` × 1.0, `pairwise` × 1.1, `scale_1_5` × 1.0, `ranking` × 1.3, `yes_mostly_no` × 1.0, `free_text` × 2.0, `click_test` × 1.5, `reaction` × 1.0.
|
|
15
|
+
Format: `single_choice` × 1.0, `multi_choice` × 1.1, `ab_image` × 1.0, `pairwise` × 1.1, `scale_1_5` × 1.0, `ranking` × 1.3, `yes_no` × 1.0, `yes_mostly_no` × 1.0, `free_text` × 2.0, `click_test` × 1.5, `reaction` × 1.0.
|
|
16
16
|
|
|
17
17
|
A reason with every answer (`reason`, not for free_text) adds to the format: optional + 0.3, required + 0.6. A required reason on a Tier 1 single choice is 20 × 1.6 = 32 credits an answer. Heads get the same share of the higher price.
|
|
18
18
|
|
|
@@ -22,6 +22,8 @@ Targeting: countries are free. Age, gender and each tag group are one trait each
|
|
|
22
22
|
|
|
23
23
|
Heads: 10 to 5000, default 50.
|
|
24
24
|
|
|
25
|
+
Your audience (`answered_by: "private"`): a link you share instead of heads, 5 credits an accepted answer, reserved for the most answers the link accepts (`n`, default 100) and refunded for the rest when it closes. No tier, rush, targeting or images surcharge; no time estimate (people answer when you share the link); every result says the answers were not verified by 50heads.
|
|
26
|
+
|
|
25
27
|
Every connection has a daily spend cap (default 5,000 credits), set when you connect and editable in the dashboard. Call `balance` to see what is left.
|
|
26
28
|
|
|
27
29
|
Always call `estimate` first: it returns the exact price, the time and any fixes the question needs, and never spends.
|
|
@@ -4,7 +4,7 @@ Every question has `type`, `text` (8 to 600 characters), `language` (such as `en
|
|
|
4
4
|
|
|
5
5
|
The format multiplier is part of the price: per answer = tier base × length × format, plus 5 credits an image.
|
|
6
6
|
|
|
7
|
-
Five-second test: add `exposure_ms` (usually 5000) to an image stimulus on single_choice, multi_choice, yes_mostly_no, scale_1_5, reaction or free_text. The app shows the image for that long, hides it, then asks, so the answer is a first impression or what people remember. × 1.3.
|
|
7
|
+
Five-second test: add `exposure_ms` (usually 5000) to an image stimulus on single_choice, multi_choice, yes_no, yes_mostly_no, scale_1_5, reaction or free_text. The app shows the image for that long, hides it, then asks, so the answer is a first impression or what people remember. × 1.3.
|
|
8
8
|
Every type except `free_text` can ask for a short written reason with each answer: `reason: "optional"` (format + 0.3) or `"required"` (format + 0.6), 10 to 140 characters. Reasons come back on each answer, translated into your language when heads wrote in another, and `get_results` summarises them (themes with counts and quotes, a sentiment split and a takeaway). Use it instead of a follow-up when you want the which and the why from the same heads.
|
|
9
9
|
|
|
10
10
|
## Single choice (`single_choice`)
|
|
@@ -161,9 +161,26 @@ Every type except `free_text` can ask for a short written reason with each answe
|
|
|
161
161
|
}
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
+
## Yes or no (`yes_no`)
|
|
165
|
+
|
|
166
|
+
- Use: A binary check. Heads answer Yes or No. This is the yes/no type. For a middle answer, use yes_mostly_no.
|
|
167
|
+
- Options: Fixed: Yes, No. Do not send options.
|
|
168
|
+
- Answer: Yes or No.
|
|
169
|
+
- Tier: any. Format × 1.0.
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"type": "yes_no",
|
|
174
|
+
"text": "Does this button label tell you what will happen?",
|
|
175
|
+
"context": "Button: Save and continue",
|
|
176
|
+
"language": "en",
|
|
177
|
+
"n": 50
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
164
181
|
## Yes, mostly or no (`yes_mostly_no`)
|
|
165
182
|
|
|
166
|
-
- Use:
|
|
183
|
+
- Use: Yes, Mostly or No. Set this type when you want the middle answer. A plain yes/no question is yes_no.
|
|
167
184
|
- Options: Fixed: Yes, Mostly, No. Do not send options.
|
|
168
185
|
- Answer: One of the three.
|
|
169
186
|
- Tier: any. Format × 1.0.
|
|
@@ -5,16 +5,18 @@ The core loop is `estimate`, `ask`, then the task (or `wait_for_results`) and `g
|
|
|
5
5
|
## Before asking
|
|
6
6
|
|
|
7
7
|
- `upload_image`: for a local file or a generated image, send it as base64 in `data` (JPEG, PNG or WebP, up to 5 MB; about 700 KB through the hosted server, which takes 1 MB requests). For an image already online, pass `image_url` and 50heads copies it. Use the returned `image_url` in the question. Costs nothing.
|
|
8
|
+
- `list_audiences`: interest audiences, panels of heads who proved they fit (UK commuter cyclists, for example). Put an id in `question.audience` to ask one instead of targeting; the price is the audience's at the minimum grade.
|
|
8
9
|
- `list_targeting`: countries with the languages their heads read and pool size bands, and the tag ids you can target at Tier 2 and 3. Each condition shrinks the pool; `estimate` shows `pool_size`.
|
|
9
10
|
- `build_ask_link`: when a person should press Ask themselves, give them this link. It opens the portal composer with the question filled in and asks nothing. It also opens a template (`template_id`), a follow-up (`follow_up_of`) or a re-ask of the unanswered part (`reask`).
|
|
10
|
-
- `templates` and `list_questions`: a fixed-price template may fit, and a recent answer may already cover the question.
|
|
11
|
+
- `templates` and `list_questions`: a fixed-price template may fit, and a recent answer may already cover the question. `list_questions` takes `set_id` to list one set's questions in order.
|
|
12
|
+
- `ask_set`: two to ten questions for the person's own audience behind one link, answered in order, about five seconds each. Same question shapes as `ask` without tier, rush, targeting, reasons or free text; `max_answers`, `open_for_days`, `shown_as` and `public_results` are set once for the set. Returns the link, the `question_ids` in order and the reserve (questions × max answers × 5 credits). Results are per question.
|
|
11
13
|
|
|
12
14
|
## After the answers
|
|
13
15
|
|
|
14
16
|
- `get_answers`: individual answers, 50 a page (up to 200), newest first, with `next_cursor`. Filter by `option` (0 is the first), `tier`, `country`, `age_band` or a keyword `q` in written answers and reasons. Country and age filters show nothing when fewer than 5 answers match, so no head can be singled out.
|
|
15
17
|
- `add_heads`: when the margin is under 10 points with 50 heads, ask more of the same question instead of a new one. It needs its own fresh `idempotency_key` and spends at the same price per answer; say the price and time before you do it when a person is present.
|
|
16
18
|
- `flag_answer`: an answer that is off-topic, low effort, offensive or a duplicate, by its `attestation_ref`. Upheld flags remove the answer and refund it. Never flag an answer because you dislike it.
|
|
17
|
-
- `export`: `csv` has every answer (with the same filters as `get_answers`); `pdf` is a one-page report and `png` the result card
|
|
19
|
+
- `export`: `csv` has every answer (with the same filters as `get_answers`); `pdf` is a one-page report and `png` the result card. The API serves all three.
|
|
18
20
|
- `cancel`: stops a live question and refunds the heads who have not answered.
|
|
19
21
|
|
|
20
22
|
## Help
|
|
@@ -22,4 +24,4 @@ The core loop is `estimate`, `ask`, then the task (or `wait_for_results`) and `g
|
|
|
22
24
|
- `search_help`: the help centre, for questions about credits, refunds, verification, tiers, the API or this server.
|
|
23
25
|
- `send_feedback`: tells support about a problem or an idea. Signed out, add `email`. Leave out personal data about other people.
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
`add_heads`, `flag_answer` and `export` (CSV, PDF and PNG) are served by the API. Use them when the result calls for it.
|
|
@@ -20,6 +20,22 @@ import urllib.request
|
|
|
20
20
|
API_URL = os.environ.get("FIFTYHEADS_API_URL", "https://api.50heads.com").rstrip("/")
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def user_agent():
|
|
24
|
+
"""50heads-skill/<version>. Cloudflare answers 1010 to the bare Python-urllib agent."""
|
|
25
|
+
skill = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "SKILL.md")
|
|
26
|
+
version = "dev"
|
|
27
|
+
try:
|
|
28
|
+
with open(skill, encoding="utf-8") as handle:
|
|
29
|
+
for line in handle:
|
|
30
|
+
stripped = line.strip()
|
|
31
|
+
if stripped.startswith("version:"):
|
|
32
|
+
version = stripped.split(":", 1)[1].strip().strip("\"'")
|
|
33
|
+
break
|
|
34
|
+
except OSError:
|
|
35
|
+
pass
|
|
36
|
+
return "50heads-skill/" + version
|
|
37
|
+
|
|
38
|
+
|
|
23
39
|
def to_draft(q):
|
|
24
40
|
"""The MCP question object (snake_case) to the API draft (camelCase)."""
|
|
25
41
|
draft = {
|
|
@@ -62,7 +78,12 @@ def main():
|
|
|
62
78
|
source = sys.stdin if sys.argv[1] == "-" else open(sys.argv[1], encoding="utf-8")
|
|
63
79
|
question = json.load(source)
|
|
64
80
|
body = json.dumps({"draft": to_draft(question), "surface": "web"}).encode("utf-8")
|
|
65
|
-
headers = {
|
|
81
|
+
headers = {
|
|
82
|
+
"content-type": "application/json",
|
|
83
|
+
"accept": "application/json",
|
|
84
|
+
"User-Agent": user_agent(),
|
|
85
|
+
"x-50heads-source": "mcp",
|
|
86
|
+
}
|
|
66
87
|
key = os.environ.get("FIFTYHEADS_API_KEY")
|
|
67
88
|
if key:
|
|
68
89
|
headers["authorization"] = "Bearer " + key
|
|
@@ -71,8 +92,15 @@ def main():
|
|
|
71
92
|
with urllib.request.urlopen(request, timeout=20) as response:
|
|
72
93
|
quote = json.load(response)
|
|
73
94
|
except urllib.error.HTTPError as err:
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
raw = err.read().decode("utf-8", "replace")
|
|
96
|
+
message = "The estimate failed (HTTP %d)." % err.code
|
|
97
|
+
try:
|
|
98
|
+
detail = json.loads(raw or "{}").get("error", {})
|
|
99
|
+
if isinstance(detail, dict) and detail.get("message"):
|
|
100
|
+
message = detail["message"]
|
|
101
|
+
except json.JSONDecodeError:
|
|
102
|
+
pass
|
|
103
|
+
print(message, file=sys.stderr)
|
|
76
104
|
return 1
|
|
77
105
|
n = question.get("n", 50)
|
|
78
106
|
# The price is in the key's account currency (credits alone when the API sends none).
|
package/skills/50heads.zip
CHANGED
|
Binary file
|