@50heads/mcp 2026.1009.1 → 2026.1013.2

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.
@@ -4,34 +4,103 @@
4
4
  */
5
5
  export declare const SERVER_NAME = "50heads";
6
6
  /** Calendar version (YYYY.MDD.N). Stamped at release by scripts/release.ts; never edit by hand. */
7
- export declare const SERVER_VERSION = "2026.1009.1";
7
+ export declare const SERVER_VERSION = "2026.1013.2";
8
8
  /** Sent by the CLI on every request, so the API can tell releases apart. */
9
- export declare const USER_AGENT = "50heads-mcp/2026.1009.1";
9
+ export declare const USER_AGENT = "50heads-mcp/2026.1013.2";
10
10
  export declare const MCP_RESOURCE_URL = "https://mcp.50heads.com/mcp";
11
- /** The scope each tool needs (spec: Authorization → Scopes). null: no sign-in needed. */
12
- export declare const TOOL_SCOPES: {
13
- readonly estimate: "questions:write";
14
- readonly ask: "questions:write";
15
- readonly ask_set: "questions:write";
16
- readonly get_results: "questions:read";
17
- readonly wait_for_results: "questions:read";
18
- readonly list_questions: "questions:read";
19
- readonly cancel: "questions:write";
20
- readonly templates: null;
21
- readonly balance: "account:read";
22
- readonly upload_image: "questions:write";
23
- readonly get_answers: "questions:read";
24
- readonly add_heads: "questions:write";
25
- readonly flag_answer: "questions:write";
26
- readonly export: "questions:read";
27
- readonly build_ask_link: null;
28
- readonly list_targeting: null;
29
- readonly list_audiences: null;
30
- readonly search_help: null;
31
- readonly send_feedback: null;
11
+ /**
12
+ * The tools and, inside each, the actions it takes, with the scope each action needs (spec:
13
+ * Authorization → Scopes). null: no sign-in needed. A tool has at most MAX_TOOLS (10) entries
14
+ * (decision G12): hosts choose well from few tools, so related calls are actions of one tool, and
15
+ * every action is checked against its own scope, never the tool's.
16
+ */
17
+ export declare const TOOL_ACTIONS: {
18
+ readonly estimate: {
19
+ readonly quote: "questions:write";
20
+ readonly balance: "account:read";
21
+ readonly templates: null;
22
+ readonly audiences: null;
23
+ readonly targeting: null;
24
+ };
25
+ readonly ask: {
26
+ readonly ask: "questions:write";
27
+ readonly set: "questions:write";
28
+ readonly upload_image: "questions:write";
29
+ readonly build_link: null;
30
+ };
31
+ readonly results: {
32
+ readonly get: "questions:read";
33
+ readonly wait: "questions:read";
34
+ readonly answers: "questions:read";
35
+ readonly export: "questions:read";
36
+ readonly list: "questions:read";
37
+ };
38
+ readonly questions: {
39
+ readonly cancel: "questions:write";
40
+ readonly add_heads: "questions:write";
41
+ readonly flag_answer: "questions:write";
42
+ readonly file: "questions:write";
43
+ };
44
+ readonly projects: {
45
+ readonly list: "questions:read";
46
+ readonly create: "questions:write";
47
+ readonly update: "questions:write";
48
+ readonly delete: "questions:write";
49
+ readonly labels: "questions:read";
50
+ };
51
+ readonly webhooks: {
52
+ readonly list: "webhooks:read";
53
+ readonly create: "webhooks:write";
54
+ readonly update: "webhooks:write";
55
+ readonly rotate_secret: "webhooks:write";
56
+ readonly test: "webhooks:write";
57
+ readonly deliveries: "webhooks:read";
58
+ readonly events: "webhooks:read";
59
+ readonly replay: "webhooks:write";
60
+ readonly delete: "webhooks:write";
61
+ };
62
+ readonly organization: {
63
+ readonly get: "organization:read";
64
+ readonly domains: "organization:read";
65
+ readonly scim: "organization:read";
66
+ readonly member_sessions: "members:read";
67
+ readonly service_accounts: "organization:read";
68
+ readonly apps: "apps:read";
69
+ };
70
+ readonly audit: {
71
+ readonly list: "audit:read";
72
+ readonly export: "audit:read";
73
+ };
74
+ readonly help: {
75
+ readonly search: null;
76
+ readonly feedback: null;
77
+ readonly status: null;
78
+ readonly whoami: "account:read";
79
+ };
32
80
  };
33
- export type ToolName = keyof typeof TOOL_SCOPES;
81
+ export type ToolName = keyof typeof TOOL_ACTIONS;
34
82
  export declare const TOOL_NAMES: ToolName[];
83
+ /** At most this many tools, ever (decision G12). A guard test fails above it. */
84
+ export declare const MAX_TOOLS = 10;
85
+ /** "tool.action" for every action: the id used in logs, tags and the tests. */
86
+ export type ActionId = {
87
+ [T in ToolName]: `${T}.${keyof (typeof TOOL_ACTIONS)[T] & string}`;
88
+ }[ToolName];
89
+ export declare const ACTION_IDS: ActionId[];
90
+ /** The actions of a tool, in the order the tool lists them. */
91
+ export declare function actionsOf(tool: string): string[];
92
+ /**
93
+ * The scope one action needs: a string, null when no sign-in is needed, or undefined when there
94
+ * is no such tool or action. Per action, not per tool.
95
+ */
96
+ export declare function actionScope(tool: string, action: unknown): string | null | undefined;
97
+ /**
98
+ * The actions that change something of the account's: asks and spends, uploads, cancels, flags,
99
+ * filing, projects and webhooks. Each needs a :write scope, so a read-only grant never reaches
100
+ * one. (The estimate quote also needs questions:write, as it always has, but changes nothing.
101
+ * Feedback opens a support ticket and needs no sign-in, so it is not an account change.)
102
+ */
103
+ export declare const WRITE_ACTIONS: ReadonlySet<string>;
35
104
  /** Resources that need no sign-in; the rest need questions:read. */
36
105
  export declare const PUBLIC_RESOURCE_PREFIXES: string[];
37
106
  export declare function resourceScope(uri: string): string | null;
@@ -1,8 +1,16 @@
1
- import type { FollowUpInput as ApiFollowUp, QuestionDraftInput, QuestionOption } from "@50heads/shared";
1
+ import { type FollowUpInput as ApiFollowUp, type GoRule, type QuestionDraftInput, type QuestionOption } from "@50heads/shared";
2
2
  import type { z } from "zod";
3
3
  import type { FollowUpInput, QuestionInput, VariantInput } from "./schemas.js";
4
4
  /** The MCP question object → the API's draft. `language` must be settled by the caller. */
5
5
  export declare function toDraft(q: QuestionInput, language: string): QuestionDraftInput;
6
+ /**
7
+ * The MCP go rule → the API's. Answers needed defaults to 80% of the heads asked, rounded down,
8
+ * at least the floor (the composer's default).
9
+ */
10
+ export declare function toGoRule(g: {
11
+ go_above: number;
12
+ min_answers?: number;
13
+ }, n: number): GoRule;
6
14
  /** A variant is the same question with translated text and options. */
7
15
  export declare function variantDraft(base: QuestionDraftInput, v: z.infer<typeof VariantInput>): QuestionDraftInput;
8
16
  export declare function toFollowUp(f: z.infer<typeof FollowUpInput>): ApiFollowUp;
@@ -20,4 +28,5 @@ export declare function fromDraft(d: {
20
28
  rush?: boolean;
21
29
  neither?: boolean;
22
30
  reason?: string;
31
+ goRule?: GoRule;
23
32
  }): Record<string, unknown>;
@@ -14,7 +14,7 @@ export declare const ERROR_CODES: {
14
14
  readonly transient: -32000;
15
15
  readonly internal: -32603;
16
16
  };
17
- export type McpErrorCode = "validation" | "not_found" | "insufficient_credits" | "spend_cap_exceeded" | "idempotency_conflict" | "content_refused" | "underfilled" | "unauthorised" | "insufficient_scope" | "rate_limited" | "unavailable" | "internal";
17
+ export type McpErrorCode = "validation" | "not_found" | "insufficient_credits" | "spend_cap_exceeded" | "idempotency_conflict" | "content_refused" | "underfilled" | "unauthorized" | "insufficient_scope" | "rate_limited" | "unavailable" | "internal";
18
18
  export type McpErrorData = {
19
19
  code: McpErrorCode;
20
20
  retryable: boolean;
@@ -1,6 +1,6 @@
1
1
  /** The skill's version (front matter metadata.version); advertised as skillVersion. */
2
- export declare const SKILL_VERSION = "2026.1009.1";
2
+ export declare const SKILL_VERSION = "2026.1013.2";
3
3
  /** SKILL.md without its front matter: the 50heads://guide resource. */
4
- export declare const SKILL_BODY = "# Asking 50heads\n\n50heads puts one short question to verified people on their phones and returns how they split. Answers cost credits, priced in your currency; fifty take about ten minutes.\n\n## When to ask\n\nAsk when the answer is a human reaction: which name, headline, image or menu people prefer, whether copy is clear, what they notice first. Never ask for facts, predictions, advice or research. Before asking again, call `list_questions` and reuse a recent answer.\n\n## Eight rules for a good question\n\n1. One question a head can answer in five seconds.\n2. Ask about a preference or first impression, never a fact.\n3. Show the thing: images (`ab_image`, `upload_image`) for anything visual.\n4. Keep options short, parallel and few: two to four is best. Add Neither when a forced pick would mislead.\n5. Word it neutrally. No leading words, no hint of the answer you want.\n6. Put background in `context`, one line, only when a head needs it.\n7. Pick the type that fits: `single_choice` to pick, `pairwise` for two, `ranking` for order, `scale_1_5` for strength, `free_text` (Tier 2) for why.\n8. Match heads and tier to the stakes: 50 at Tier 1 for a quick read, 100 or more or Tier 2 for a decision; target the countries and language of the real audience.\n\n## How to ask\n\n1. Call `estimate` and, when a person is present, show them the price and time together: \"50 answers, about 10 minutes, $12.70.\"\n2. Call `ask` with a fresh UUID as `idempotency_key`. Reuse that key if you retry.\n3. Wait on the task, or call `wait_for_results` on older hosts. `get_results` shows it while live.\n\n## Reading results\n\nLead with the winner, the margin in points and the confidence. With 50 heads a margin under 10 points is noise: say so and suggest more heads, not a verdict. Report underfilled as underfilled. Quote the split, not adjectives.\n\n## Worked example\n\n\"Which menu would you order from?\" with two menu photos, `ab_image`, 50 heads, Tier 1. Menu B 68%, Menu A 32%: \"Menu B wins by 36 points. Confidence high.\" Follow up with 20 Tier 2 heads saying why.\n\n## Avoid\n\n- Double questions (\"Is it clear and friendly?\").\n- Yes-or-no questions that lead.\n- Asking for personal data or anything that identifies someone.\n- Re-asking the same question to fish for a different answer.\n\nMore: `references/question-types.md`, `pricing.md`, `tools.md`.\n";
4
+ export declare const SKILL_BODY = "# Asking 50heads\n\n50heads puts one short question to verified people and shows what they think.\n\n## When to ask\n\nAsk when the answer is a human reaction: which name, headline, image or menu people prefer, whether copy is clear, what they notice first. Never ask for facts, predictions, advice or research. Check recent `results` (`list`) first.\n\n## Eight rules for a good question\n\n1. One question a head can answer in five seconds.\n2. Ask about a preference or first impression, never a fact.\n3. Show the thing: images (`ab_image`, `upload_image`) for anything visual.\n4. Keep options short, parallel and few: two to four is best. Add Neither when a forced pick would mislead.\n5. Word it neutrally, with no hint of the answer you want.\n6. Put needed background in `context`, one line.\n7. Pick the type that fits: `single_choice` to pick, `pairwise` for two, `ranking` for order, `scale_1_5` for strength, `yes_no` with `go_rule` to approve, `free_text` (Tier 2) for why.\n8. Match heads and tier to the stakes: 50 at Tier 1 for a quick read, 100 or more or Tier 2 for a decision. Target the real audience's countries and language.\n\n## How to ask\n\n1. Call `estimate` (`quote`); with a person present, show the price: \"50 answers, $12.70.\" Don't promise a time.\n2. Call `ask` with a fresh UUID as `idempotency_key`. Reuse it to retry.\n3. Wait on the task (older hosts: `results`, `wait`).\n\n## Reading results\n\nLead with the winner, the margin in points and the confidence. With 50 heads a margin under 10 points is noise: say so and suggest more heads. Report underfilled as underfilled. Quote the percentages, not adjectives.\n\nWith a `go_rule`, lead with the `verdict`: go, no go or unclear against the asker's line. `head_to_head` says who beats whom; overlapping ranges mean the lead may be chance.\n\n## Worked example\n\n\"Which menu would you order from?\": two photos, `ab_image`, 50 heads, Tier 1. B 68%, A 32%: \"Menu B wins by 36 points. Confidence high.\"\n\n## Avoid\n\n- Double questions (\"Is it clear and friendly?\").\n- Personal data, or anything that identifies someone.\n- Re-asking to fish for a different answer.\n\n## Your data\n\nWe don't use your questions, the answers you buy or the reasons heads write to train AI models, and we don't sell them.\n\nMore: `references/question-types.md`, `pricing.md`, `tools.md`.\n";
5
5
  /** Every file in the skill bundle, by path relative to skills/50heads. */
6
6
  export declare const SKILL_FILES: Record<string, string>;
@@ -4,7 +4,19 @@
4
4
  */
5
5
  export declare const REGISTRY_NAME = "com.50heads/mcp";
6
6
  export declare const NPM_PACKAGE = "@50heads/mcp";
7
- export declare const DESCRIPTION = "Ask fifty verified people a five-second question and get the split back.";
7
+ export declare const DESCRIPTION = "Ask fifty verified people a five-second question and see what they think.";
8
+ /**
9
+ * Public links every directory listing and the registry entry carry (docs/listings). The no-train
10
+ * statement is the Help article; the privacy policy has the same words under "AI and your data".
11
+ */
12
+ export declare const LISTING_URLS: {
13
+ readonly website: "https://50heads.com/agents";
14
+ readonly documentation: "https://50heads.com/en-us/docs/mcp";
15
+ readonly privacyPolicy: "https://50heads.com/en-gb/legal/privacy";
16
+ readonly noTrain: "https://50heads.com/en-gb/help/privacy/ai-and-your-data";
17
+ readonly terms: "https://50heads.com/en-gb/legal/terms";
18
+ readonly support: "https://50heads.com/en-gb/contact?purpose=support";
19
+ };
8
20
  export type ManifestOptions = {
9
21
  /** The hosted endpoint, https://mcp.50heads.com/mcp. */
10
22
  endpoint?: string;
@@ -35,7 +47,7 @@ export declare function serverCard(opts?: ManifestOptions): {
35
47
  schemes: string[];
36
48
  resourceMetadata: string;
37
49
  authorizationServers: string[];
38
- scopes: ("questions:read" | "questions:write" | "templates:read" | "account:read")[];
50
+ scopes: ("questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read")[];
39
51
  };
40
52
  capabilities: {
41
53
  tools: {
@@ -75,8 +87,11 @@ export declare function serverCard(opts?: ManifestOptions): {
75
87
  };
76
88
  };
77
89
  tools: {
78
- name: string;
79
- scope: "questions:read" | "questions:write" | "account:read" | null;
90
+ name: "organization" | "ask" | "questions" | "results" | "projects" | "audit" | "estimate" | "webhooks" | "help";
91
+ actions: {
92
+ action: string;
93
+ scope: "questions:read" | "questions:write" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "organization:read" | "members:read" | "audit:read" | null;
94
+ }[];
80
95
  }[];
81
96
  resources: string[];
82
97
  prompts: string[];
@@ -89,7 +104,7 @@ export declare function registryServerJson(): {
89
104
  title: string;
90
105
  description: string;
91
106
  version: string;
92
- websiteUrl: string;
107
+ websiteUrl: "https://50heads.com/agents";
93
108
  repository: {
94
109
  url: string;
95
110
  source: string;
@@ -129,10 +144,15 @@ export declare function registryServerJson(): {
129
144
  _meta: {
130
145
  "io.modelcontextprotocol.registry/publisher-provided": {
131
146
  categories: string[];
147
+ documentationUrl: "https://50heads.com/en-us/docs/mcp";
148
+ privacyPolicyUrl: "https://50heads.com/en-gb/legal/privacy";
149
+ noTrainStatementUrl: "https://50heads.com/en-gb/help/privacy/ai-and-your-data";
150
+ termsOfServiceUrl: "https://50heads.com/en-gb/legal/terms";
151
+ supportUrl: "https://50heads.com/en-gb/contact?purpose=support";
132
152
  authorization: {
133
153
  resourceMetadata: string;
134
154
  authorizationServer: string;
135
- scopes: ("questions:read" | "questions:write" | "templates:read" | "account:read")[];
155
+ scopes: ("questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read")[];
136
156
  };
137
157
  skillVersion: string;
138
158
  };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The organization as the read-only tools return it (plan 2 stage 5): snake_case, credits, and one
3
+ * plain line per item for the text summary. Changes (domains, policies, single sign-on, SCIM,
4
+ * service accounts) are made in the portal: these tools never write.
5
+ */
6
+ import type { AuditEvent, MemberSessions, Organization, OrganizationDomain, ScimSettings, ScimToken, ServiceAccount } from "@50heads/shared";
7
+ import type { AuditEventOut, DomainOut, OrganizationOut, ServiceAccountOut } from "./schemas.js";
8
+ export declare function toOrganization(o: Organization): OrganizationOut;
9
+ /** "Acme (org_1): you are an admin. 2 workspaces; domains acme.com; single sign-on required; SCIM on." */
10
+ export declare function organizationLine(o: OrganizationOut): string;
11
+ export declare function toDomain(d: OrganizationDomain): DomainOut;
12
+ /** "acme.com: verified on 2026-10-01." or "beta.com: pending. Publish TXT _50heads-challenge.beta.com = …" */
13
+ export declare function domainLine(d: DomainOut): string;
14
+ export declare function toAuditEvent(e: AuditEvent): AuditEventOut;
15
+ /** "2026-10-11 09:30 · domain.added · acme.com · by Ana (API key)" */
16
+ export declare function auditLine(e: AuditEventOut): string;
17
+ export declare function scimStatus(settings: ScimSettings, tokens: ScimToken[]): {
18
+ base_url: string;
19
+ default_role: "admin" | "member" | "viewer" | "billing";
20
+ default_workspace_id: string | null;
21
+ provisioned_people: number;
22
+ active_people: number;
23
+ groups: number;
24
+ tokens: {
25
+ token_id: string;
26
+ label: string;
27
+ prefix: string;
28
+ created_at: string;
29
+ expires_at: string | null;
30
+ last_used_at: string | null;
31
+ }[];
32
+ };
33
+ export declare function memberSessions(m: MemberSessions): {
34
+ user_id: string;
35
+ sessions: {
36
+ session_id: string;
37
+ kind: "app" | "web";
38
+ label: string;
39
+ country: string | null;
40
+ sign_in_method: "apple" | "google" | "email" | "sso" | "passkey" | "break_glass" | null;
41
+ created_at: string;
42
+ last_seen_at: string | null;
43
+ }[];
44
+ };
45
+ export declare function toServiceAccount(s: ServiceAccount): ServiceAccountOut;
46
+ /** "sa_1 · Nightly agent (on) in Acme: 400 of 10,000 credits spent today; last token 2026-10-11." */
47
+ export declare function serviceAccountLine(s: ServiceAccountOut): string;
@@ -28,10 +28,8 @@ export declare function formatPrice(p: {
28
28
  * total stays the Tier 1 price from packages/shared (50 × per-answer credits).
29
29
  */
30
30
  export declare function readmeLead(): string;
31
- /** "about 10 minutes", "about 2 hours". */
32
- export declare function aboutMinutes(minutes: number): string;
33
- /** Cost and time in one sentence, per BRAND.md: "50 answers, about 10 minutes, $12.70." */
34
- export declare function costLine(n: number, etaMinutes: number, p: {
31
+ /** The price in one sentence: "50 answers, $12.70." No time estimate. */
32
+ export declare function costLine(n: number, p: {
35
33
  amount: number;
36
34
  currency: string;
37
35
  }): string;
@@ -89,7 +87,7 @@ export declare function pricingTable(currency: Currency, rates?: Rates): {
89
87
  };
90
88
  /**
91
89
  * The pricing reference as markdown (the skill's references/pricing.md). A static file read by
92
- * every account, so it is in credits; `estimate`, `balance` and 50heads://pricing give money
90
+ * every account, so it is in credits; `estimate` (quote and balance) and 50heads://pricing give money
93
91
  * in the account's own currency.
94
92
  */
95
93
  export declare function pricingMarkdown(): string;
@@ -9,7 +9,7 @@
9
9
  * ```
10
10
  */
11
11
  import type { McpServer } from "@modelcontextprotocol/server";
12
- export { MCP_RESOURCE_URL, PROMPT_SCOPES, SERVER_NAME, SERVER_VERSION, TOOL_NAMES, TOOL_SCOPES, type ToolName, } from "./constants.js";
12
+ export { MCP_RESOURCE_URL, PROMPT_SCOPES, SERVER_NAME, SERVER_VERSION, TOOL_ACTIONS, TOOL_NAMES, type ActionId, type ToolName, } from "./constants.js";
13
13
  export { CLIENTS, configSnippets, type ClientId, type Snippet, type SnippetOptions } from "./snippets.js";
14
14
  export { SKILL_BODY, SKILL_FILES, SKILL_VERSION } from "./generated/skill.js";
15
15
  export { registryServerJson, serverCard } from "./manifest.js";
@@ -15,9 +15,16 @@ export declare function toFilter(a: {
15
15
  }): ResultFilterInput;
16
16
  /** A head's free-text answer as one quotable line: no control characters, no double quotes. */
17
17
  export declare function quoteAnswer(text: string): string;
18
+ /** The verdict in the API's words, from the snake_case result. */
19
+ export declare function verdictText(v: NonNullable<ResultsOutput["verdict"]>): string;
20
+ /**
21
+ * Who beats whom in one line: each compared pair once, the one more people put above the other
22
+ * first, strongest rows first ("Mushroom over Olives 71%, ...").
23
+ */
24
+ export declare function headToHeadText(h: NonNullable<ResultsOutput["head_to_head"]>): string;
18
25
  /**
19
26
  * One paragraph for hosts that only show text: "Menu B wins by 36 points (68% to 32%).
20
- * Confidence high. 50 of 50 answered, complete, £10.00."
27
+ * Confidence high. 50 of 50 answered, complete, £10.00." With a go rule the verdict leads.
21
28
  */
22
29
  export declare function resultsText(r: ResultsOutput): string;
23
30
  export declare function toListItem(q: QuestionV2): ListQuestionsOutput["questions"][number];