@50heads/mcp 2026.925.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 +20 -0
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/cli.js +7447 -0
- package/dist/index.js +7123 -0
- package/dist/snippets.js +100 -0
- package/dist/types/account.d.ts +22 -0
- package/dist/types/actions.d.ts +53 -0
- package/dist/types/apps.d.ts +22 -0
- package/dist/types/cli/stdio.d.ts +1735 -0
- package/dist/types/constants.d.ts +36 -0
- package/dist/types/draft.d.ts +23 -0
- package/dist/types/errors.d.ts +58 -0
- package/dist/types/generated/skill.d.ts +6 -0
- package/dist/types/idempotency.d.ts +35 -0
- package/dist/types/language.d.ts +10 -0
- package/dist/types/manifest.d.ts +141 -0
- package/dist/types/pricing.d.ts +89 -0
- package/dist/types/public.d.ts +36 -0
- package/dist/types/question-types.d.ts +18 -0
- package/dist/types/results.d.ts +29 -0
- package/dist/types/schemas.d.ts +2015 -0
- package/dist/types/server.d.ts +71 -0
- package/dist/types/snippets.d.ts +31 -0
- package/dist/types/tasks.d.ts +87 -0
- package/package.json +74 -0
- package/server.json +84 -0
- package/skills/50heads/LICENSE +21 -0
- package/skills/50heads/SKILL.md +51 -0
- package/skills/50heads/references/pricing.md +27 -0
- package/skills/50heads/references/question-types.md +235 -0
- package/skills/50heads/references/tools.md +25 -0
- package/skills/50heads/scripts/estimate.py +90 -0
- package/skills/50heads.zip +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# 50heads tools beyond asking
|
|
2
|
+
|
|
3
|
+
The core loop is `estimate`, `ask`, then the task (or `wait_for_results`) and `get_results`. These tools cover the rest.
|
|
4
|
+
|
|
5
|
+
## Before asking
|
|
6
|
+
|
|
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_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
|
+
- `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
|
+
|
|
12
|
+
## After the answers
|
|
13
|
+
|
|
14
|
+
- `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
|
+
- `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
|
+
- `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 where the API serves them.
|
|
18
|
+
- `cancel`: stops a live question and refunds the heads who have not answered.
|
|
19
|
+
|
|
20
|
+
## Help
|
|
21
|
+
|
|
22
|
+
- `search_help`: the help centre, for questions about credits, refunds, verification, tiers, the API or this server.
|
|
23
|
+
- `send_feedback`: tells support about a problem or an idea. Signed out, add `email`. Leave out personal data about other people.
|
|
24
|
+
|
|
25
|
+
Tools that are new to the API (`add_heads`, `flag_answer`, PDF and PNG `export`) answer `unavailable` until the API serves them. That is not a failure of your call: carry on without them.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Price a 50heads question before asking it. Never spends.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 scripts/estimate.py question.json
|
|
6
|
+
echo '{"type":"single_choice","text":"Which name sounds most like a bakery?","language":"en","options":[{"label":"Crumb & Co"},{"label":"Loafers"}]}' | python3 scripts/estimate.py -
|
|
7
|
+
|
|
8
|
+
The question is the same object the MCP `estimate` and `ask` tools take (see
|
|
9
|
+
references/question-types.md). FIFTYHEADS_API_KEY is optional: estimates are public, but a key
|
|
10
|
+
gives a higher rate limit. FIFTYHEADS_API_URL overrides https://api.50heads.com.
|
|
11
|
+
Standard library only.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import urllib.error
|
|
18
|
+
import urllib.request
|
|
19
|
+
|
|
20
|
+
API_URL = os.environ.get("FIFTYHEADS_API_URL", "https://api.50heads.com").rstrip("/")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def to_draft(q):
|
|
24
|
+
"""The MCP question object (snake_case) to the API draft (camelCase)."""
|
|
25
|
+
draft = {
|
|
26
|
+
"type": q["type"],
|
|
27
|
+
"text": q["text"],
|
|
28
|
+
"language": q.get("language", "en"),
|
|
29
|
+
"options": [
|
|
30
|
+
{k: v for k, v in {"label": o.get("label", ""), "imageUrl": o.get("image_url")}.items() if v is not None}
|
|
31
|
+
for o in q.get("options", [])
|
|
32
|
+
],
|
|
33
|
+
"n": q.get("n", 50),
|
|
34
|
+
"tier": q.get("tier", 2 if q["type"] == "free_text" else 1),
|
|
35
|
+
"rush": q.get("rush", False),
|
|
36
|
+
"neither": q.get("neither", False),
|
|
37
|
+
}
|
|
38
|
+
if q.get("context"):
|
|
39
|
+
draft["context"] = q["context"]
|
|
40
|
+
stimulus = q.get("stimulus") or {}
|
|
41
|
+
if stimulus:
|
|
42
|
+
draft["stimulus"] = {k: v for k, v in {"imageUrl": stimulus.get("image_url"), "text": stimulus.get("text")}.items() if v}
|
|
43
|
+
targeting = q.get("targeting") or {}
|
|
44
|
+
if targeting:
|
|
45
|
+
draft["targeting"] = {"countries": targeting.get("country", []), "tags": targeting.get("tags", [])}
|
|
46
|
+
return draft
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def money(price):
|
|
50
|
+
"""{"amount": 12.70, "currency": "USD"} as "$12.70", "£10.00", "11,80 €"."""
|
|
51
|
+
amount, currency = price["amount"], price["currency"]
|
|
52
|
+
if currency == "EUR":
|
|
53
|
+
return ("%.2f" % amount).replace(".", ",") + "\u00a0€"
|
|
54
|
+
symbol = {"GBP": "£", "USD": "$", "CAD": "$"}.get(currency)
|
|
55
|
+
return symbol + "%.2f" % amount if symbol else "%.2f %s" % (amount, currency)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main():
|
|
59
|
+
if len(sys.argv) != 2:
|
|
60
|
+
print(__doc__.strip(), file=sys.stderr)
|
|
61
|
+
return 2
|
|
62
|
+
source = sys.stdin if sys.argv[1] == "-" else open(sys.argv[1], encoding="utf-8")
|
|
63
|
+
question = json.load(source)
|
|
64
|
+
body = json.dumps({"draft": to_draft(question), "surface": "web"}).encode("utf-8")
|
|
65
|
+
headers = {"content-type": "application/json", "accept": "application/json", "x-50heads-source": "mcp"}
|
|
66
|
+
key = os.environ.get("FIFTYHEADS_API_KEY")
|
|
67
|
+
if key:
|
|
68
|
+
headers["authorization"] = "Bearer " + key
|
|
69
|
+
request = urllib.request.Request(API_URL + "/v1/billing/estimate", data=body, headers=headers, method="POST")
|
|
70
|
+
try:
|
|
71
|
+
with urllib.request.urlopen(request, timeout=20) as response:
|
|
72
|
+
quote = json.load(response)
|
|
73
|
+
except urllib.error.HTTPError as err:
|
|
74
|
+
detail = json.loads(err.read().decode("utf-8") or "{}").get("error", {})
|
|
75
|
+
print(detail.get("message", "The estimate failed (HTTP %d)." % err.code), file=sys.stderr)
|
|
76
|
+
return 1
|
|
77
|
+
n = question.get("n", 50)
|
|
78
|
+
# The price is in the key's account currency (credits alone when the API sends none).
|
|
79
|
+
cost = "%d credits" % quote["creditsTotal"]
|
|
80
|
+
if quote.get("price"):
|
|
81
|
+
cost += " (%s)" % money(quote["price"])
|
|
82
|
+
print("%d answers, about %d minutes, %s." % (n, quote["etaMinutes"], cost))
|
|
83
|
+
print(quote.get("breakdownLine", ""))
|
|
84
|
+
for issue in quote.get("validation", []):
|
|
85
|
+
print("Fix: " + issue["message"])
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
sys.exit(main())
|
|
Binary file
|