@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.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Names, versions and scopes shared by the server, the edge and the published package. No
3
+ * imports from private workspace packages, so the published types stay self-contained.
4
+ */
5
+ export declare const SERVER_NAME = "50heads";
6
+ /** Calendar version (YYYY.MDD.N). Stamped at release by scripts/release.ts; never edit by hand. */
7
+ export declare const SERVER_VERSION = "2026.925.1";
8
+ /** Sent by the CLI on every request, so the API can tell releases apart. */
9
+ export declare const USER_AGENT = "50heads-mcp/2026.925.1";
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 get_results: "questions:read";
16
+ readonly wait_for_results: "questions:read";
17
+ readonly list_questions: "questions:read";
18
+ readonly cancel: "questions:write";
19
+ readonly templates: null;
20
+ readonly balance: "account:read";
21
+ readonly upload_image: "questions:write";
22
+ readonly get_answers: "questions:read";
23
+ readonly add_heads: "questions:write";
24
+ readonly flag_answer: "questions:write";
25
+ readonly export: "questions:read";
26
+ readonly build_ask_link: null;
27
+ readonly list_targeting: null;
28
+ readonly search_help: null;
29
+ readonly send_feedback: null;
30
+ };
31
+ export type ToolName = keyof typeof TOOL_SCOPES;
32
+ export declare const TOOL_NAMES: ToolName[];
33
+ /** Resources that need no sign-in; the rest need questions:read. */
34
+ export declare const PUBLIC_RESOURCE_PREFIXES: string[];
35
+ export declare function resourceScope(uri: string): string | null;
36
+ export declare const PROMPT_SCOPES: Record<string, string | null>;
@@ -0,0 +1,23 @@
1
+ import type { FollowUpInput as ApiFollowUp, QuestionDraftInput, QuestionOption } from "@50heads/shared";
2
+ import type { z } from "zod";
3
+ import type { FollowUpInput, QuestionInput, VariantInput } from "./schemas.js";
4
+ /** The MCP question object → the API's draft. `language` must be settled by the caller. */
5
+ export declare function toDraft(q: QuestionInput, language: string): QuestionDraftInput;
6
+ /** A variant is the same question with translated text and options. */
7
+ export declare function variantDraft(base: QuestionDraftInput, v: z.infer<typeof VariantInput>): QuestionDraftInput;
8
+ export declare function toFollowUp(f: z.infer<typeof FollowUpInput>): ApiFollowUp;
9
+ /** Image count for the pricing line: option images plus a stimulus image. */
10
+ export declare function imageCount(d: QuestionDraftInput): number;
11
+ /** API draft → the MCP question object (templates). */
12
+ export declare function fromDraft(d: {
13
+ type: string;
14
+ text: string;
15
+ context?: string;
16
+ language: string;
17
+ options: QuestionOption[];
18
+ n: number;
19
+ tier: number;
20
+ rush?: boolean;
21
+ neither?: boolean;
22
+ reason?: string;
23
+ }): Record<string, unknown>;
@@ -0,0 +1,58 @@
1
+ import { ProtocolError } from "@modelcontextprotocol/server";
2
+ import { ApiRequestError } from "@50heads/shared/client";
3
+ import type { ApiErrorDetails, ValidationIssue } from "@50heads/shared";
4
+ /**
5
+ * The MCP error model (spec: "Safety and error model"). Every failure a model sees is a
6
+ * JSON-RPC error with `error.data.code`, a `retryable` hint and one plain sentence it can act on.
7
+ */
8
+ export declare const ERROR_CODES: {
9
+ /** JSON-RPC Invalid Params: schema/content validation and unknown questions or tasks. */
10
+ readonly invalidParams: -32602;
11
+ /** Account and policy refusals the caller can fix (credits, caps, keys, content). */
12
+ readonly refused: -32001;
13
+ /** Transient: rate limits, kill switches, maintenance, upstream failures. */
14
+ readonly transient: -32000;
15
+ readonly internal: -32603;
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";
18
+ export type McpErrorData = {
19
+ code: McpErrorCode;
20
+ retryable: boolean;
21
+ /** Milliseconds to wait before retrying, when known. */
22
+ retry_after_ms?: number;
23
+ validation?: ValidationIssue[];
24
+ credits_required?: number;
25
+ credits_available?: number;
26
+ top_up_url?: string;
27
+ cap_remaining?: number;
28
+ resets_at?: string;
29
+ category?: string;
30
+ scope?: string;
31
+ until?: string | null;
32
+ /** W3C trace id, for support. */
33
+ trace_id?: string;
34
+ };
35
+ /** A tool, resource, prompt or task failure, carried as a JSON-RPC error. */
36
+ export declare class McpToolError extends ProtocolError {
37
+ readonly mcpCode: McpErrorCode;
38
+ readonly details: McpErrorData;
39
+ constructor(code: McpErrorCode, message: string, extra?: Omit<McpErrorData, "code" | "retryable"> & {
40
+ retryable?: boolean;
41
+ });
42
+ }
43
+ type Urls = {
44
+ portalUrl: string;
45
+ };
46
+ /** Reads `{ error: { details } }` (or loose extra fields) from an API error body. */
47
+ export declare function apiErrorDetails(err: ApiRequestError): ApiErrorDetails;
48
+ /** Next UTC midnight, when daily caps reset. */
49
+ export declare function nextUtcMidnight(now?: number): string;
50
+ /**
51
+ * Maps an API error onto the MCP error model. The API speaks `{ error: { code, message } }`
52
+ * with an HTTP status; codes are matched first, statuses second, so a new API code still lands
53
+ * somewhere sensible.
54
+ */
55
+ export declare function fromApiError(err: ApiRequestError, urls: Urls, traceId?: string): McpToolError;
56
+ /** Any thrown value as an McpToolError. */
57
+ export declare function toMcpError(err: unknown, urls: Urls, traceId?: string): McpToolError;
58
+ export {};
@@ -0,0 +1,6 @@
1
+ /** The skill's version (front matter metadata.version); advertised as skillVersion. */
2
+ export declare const SKILL_VERSION = "2026.925.1";
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";
5
+ /** Every file in the skill bundle, by path relative to skills/50heads. */
6
+ export declare const SKILL_FILES: Record<string, string>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Idempotency keys, derived per the MCP spec: the caller supplies one UUID per ask; the server
3
+ * derives the key it sends to the API from the caller's principal, the tool and that UUID, so
4
+ * the same UUID from two connections never collides, a retry from the same connection always
5
+ * replays, and variants and follow-ups get stable keys of their own without the caller
6
+ * inventing more UUIDs. Keys are opaque, fixed length and never contain the principal.
7
+ */
8
+ export declare function sha256(text: string): Promise<string>;
9
+ /**
10
+ * The key sent to the API as `Idempotency-Key`: "mcp_" + 43 base64url characters.
11
+ * `parts` extend the derivation (a variant's language, a follow-up's position).
12
+ */
13
+ export declare function deriveIdempotencyKey(principal: string, tool: string, clientKey: string, ...parts: string[]): Promise<string>;
14
+ /**
15
+ * A stable principal for a bearer token when the connection id is not known: a hash of the
16
+ * token itself (API keys never change for their lifetime; OAuth tokens carry `conn`).
17
+ */
18
+ export declare function principalForToken(token: string | null | undefined): Promise<string>;
19
+ /**
20
+ * Stable fingerprint of what was asked, to spot an idempotency key reused for a different
21
+ * question: the API replays the first question for a repeated key, and if that question's
22
+ * fingerprint differs from this request's, the key was reused.
23
+ */
24
+ export declare function draftFingerprint(d: {
25
+ type: string;
26
+ text: string;
27
+ language?: string;
28
+ options?: {
29
+ label: string;
30
+ imageUrl?: string;
31
+ }[];
32
+ n?: number;
33
+ tier?: number;
34
+ reason?: string;
35
+ }): string;
@@ -0,0 +1,10 @@
1
+ import { QUESTION_LANGUAGES } from "@50heads/shared";
2
+ /**
3
+ * A small, conservative language guess for questions that arrive without `language`. It only
4
+ * answers when the text is clearly one of the languages heads answer in; otherwise the server
5
+ * asks the caller (elicitation) rather than guess.
6
+ */
7
+ type Lang = (typeof QUESTION_LANGUAGES)[number];
8
+ /** A language code, or null when the text is not clearly one language. */
9
+ export declare function detectLanguage(text: string): Lang | null;
10
+ export {};
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Discovery documents: the server card at /.well-known/mcp.json (and
3
+ * /.well-known/mcp/server-card.json) and server.json for the official MCP registry.
4
+ */
5
+ export declare const REGISTRY_NAME = "com.50heads/mcp";
6
+ export declare const NPM_PACKAGE = "@50heads/mcp";
7
+ export declare const DESCRIPTION = "Ask fifty verified real people a five-second question and get the split back.";
8
+ export type ManifestOptions = {
9
+ /** The hosted endpoint, https://mcp.50heads.com/mcp. */
10
+ endpoint?: string;
11
+ webUrl?: string;
12
+ resourceMetadataUrl?: string;
13
+ };
14
+ /** The server card: enough for a directory or host to connect without a round trip. */
15
+ export declare function serverCard(opts?: ManifestOptions): {
16
+ $schema: string;
17
+ version: string;
18
+ protocolVersion: string;
19
+ supportedProtocolVersions: string[];
20
+ serverInfo: {
21
+ name: string;
22
+ title: string;
23
+ version: string;
24
+ };
25
+ description: string;
26
+ instructions: string;
27
+ iconUrl: string;
28
+ documentationUrl: string;
29
+ transport: {
30
+ type: string;
31
+ endpoint: string;
32
+ };
33
+ authentication: {
34
+ required: boolean;
35
+ schemes: string[];
36
+ resourceMetadata: string;
37
+ authorizationServers: string[];
38
+ scopes: ("questions:read" | "questions:write" | "templates:read" | "account:read")[];
39
+ };
40
+ capabilities: {
41
+ tools: {
42
+ listChanged: boolean;
43
+ };
44
+ resources: {
45
+ listChanged: boolean;
46
+ };
47
+ prompts: {
48
+ listChanged: boolean;
49
+ };
50
+ extensions: {
51
+ "io.modelcontextprotocol/tasks": {
52
+ methods: string[];
53
+ tools: string[];
54
+ pollIntervalMs: number;
55
+ };
56
+ "io.modelcontextprotocol/apps": {
57
+ mimeTypes: string[];
58
+ views: string[];
59
+ };
60
+ "com.50heads/skills": {
61
+ skillVersion: string;
62
+ skills: {
63
+ name: string;
64
+ version: string;
65
+ uri: string;
66
+ files: string[];
67
+ download: string;
68
+ }[];
69
+ };
70
+ "com.50heads/auth": {
71
+ authorizationServer: string;
72
+ apiKeys: string;
73
+ resourceMetadata?: string | undefined;
74
+ };
75
+ };
76
+ };
77
+ tools: {
78
+ name: string;
79
+ scope: "questions:read" | "questions:write" | "account:read" | null;
80
+ }[];
81
+ resources: string[];
82
+ prompts: string[];
83
+ skillVersion: string;
84
+ };
85
+ /** server.json for the official MCP registry: the hosted remote and the npm stdio package. */
86
+ export declare function registryServerJson(): {
87
+ $schema: string;
88
+ name: string;
89
+ title: string;
90
+ description: string;
91
+ version: string;
92
+ websiteUrl: string;
93
+ repository: {
94
+ url: string;
95
+ source: string;
96
+ subfolder: string;
97
+ };
98
+ icons: {
99
+ src: string;
100
+ mimeType: string;
101
+ sizes: string[];
102
+ }[];
103
+ remotes: {
104
+ type: string;
105
+ url: string;
106
+ headers: {
107
+ name: string;
108
+ description: string;
109
+ isRequired: boolean;
110
+ isSecret: boolean;
111
+ }[];
112
+ }[];
113
+ packages: {
114
+ registryType: string;
115
+ registryBaseUrl: string;
116
+ identifier: string;
117
+ version: string;
118
+ runtimeHint: string;
119
+ transport: {
120
+ type: string;
121
+ };
122
+ environmentVariables: {
123
+ name: string;
124
+ description: string;
125
+ isRequired: boolean;
126
+ isSecret: boolean;
127
+ format: string;
128
+ }[];
129
+ }[];
130
+ _meta: {
131
+ "io.modelcontextprotocol.registry/publisher-provided": {
132
+ categories: string[];
133
+ authorization: {
134
+ resourceMetadata: string;
135
+ authorizationServer: string;
136
+ scopes: ("questions:read" | "questions:write" | "templates:read" | "account:read")[];
137
+ };
138
+ skillVersion: string;
139
+ };
140
+ };
141
+ };
@@ -0,0 +1,89 @@
1
+ import { DISPLAY_CURRENCIES, type Rates } from "@50heads/shared";
2
+ export type Currency = (typeof DISPLAY_CURRENCIES)[number];
3
+ export type Price = {
4
+ amount: number;
5
+ currency: Currency;
6
+ };
7
+ export declare function isCurrency(value: unknown): value is Currency;
8
+ /**
9
+ * The currency to show when the account's is unknown (signed out, or the balance could not be
10
+ * read). US dollars, so nothing reads as UK-only; a signed-in account always sees its own.
11
+ */
12
+ export declare const UNKNOWN_CURRENCY: Currency;
13
+ /**
14
+ * The locale an amount in a currency is written in, so each reads the way a banking app in its
15
+ * home market writes it: "£10.00", "$12.70" (US and Canadian dollars alike), "11,80 €".
16
+ */
17
+ export declare const MONEY_LOCALES: Record<Currency, string>;
18
+ export declare function moneyLocale(currency: string): string;
19
+ /** Credits as money in a display currency, to the cent. The ledger stays in credits. */
20
+ export declare function price(credits: number, currency: Currency, rates?: Rates): Price;
21
+ /** "£17.00", "$21.59", "20,06 €", in the currency's own locale (moneyLocale). */
22
+ export declare function formatPrice(p: {
23
+ amount: number;
24
+ currency: string;
25
+ }, locale?: string): string;
26
+ /** "about 10 minutes", "about 2 hours". */
27
+ export declare function aboutMinutes(minutes: number): string;
28
+ /** Cost and time in one sentence, per BRAND.md: "50 answers, about 10 minutes, $12.70." */
29
+ export declare function costLine(n: number, etaMinutes: number, p: {
30
+ amount: number;
31
+ currency: string;
32
+ }): string;
33
+ /** The pricing document: tier bases, multipliers and image bands, in one currency. */
34
+ export declare function pricingTable(currency: Currency, rates?: Rates): {
35
+ currency: "GBP" | "USD" | "EUR" | "CAD";
36
+ creditValue: Price;
37
+ tiers: {
38
+ tier: 1 | 2 | 3;
39
+ name: string;
40
+ verified: string;
41
+ goodFor: string;
42
+ creditsPerAnswer: number;
43
+ pricePerAnswer: Price;
44
+ priceFor50: Price;
45
+ }[];
46
+ length: {
47
+ upToCharacters: number;
48
+ multiplier: number;
49
+ }[];
50
+ format: {
51
+ [k: string]: number;
52
+ };
53
+ reason: {
54
+ optional: number;
55
+ required: number;
56
+ note: string;
57
+ };
58
+ images: {
59
+ creditsPerImagePerAnswer: number;
60
+ creditsPerAudioPerAnswer: number;
61
+ note: string;
62
+ };
63
+ rush: {
64
+ multiplier: number;
65
+ note: string;
66
+ };
67
+ fiveSecond: {
68
+ multiplier: number;
69
+ note: string;
70
+ };
71
+ targeting: {
72
+ creditsPerTraitPerAnswer: number;
73
+ maxTraits: number;
74
+ note: string;
75
+ };
76
+ heads: {
77
+ min: 10;
78
+ max: 5000;
79
+ default: number;
80
+ };
81
+ connectionDailyCapDefault: Price;
82
+ formula: string;
83
+ };
84
+ /**
85
+ * The pricing reference as markdown (the skill's references/pricing.md). A static file read by
86
+ * every account, so it is in credits; `estimate`, `balance` and 50heads://pricing give money
87
+ * in the account's own currency.
88
+ */
89
+ export declare function pricingMarkdown(): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The published entry of @50heads/mcp (npm). Inside the monorepo, apps import src/index.ts,
3
+ * which also exposes the internals the hosted server needs.
4
+ *
5
+ * ```ts
6
+ * import { serveStdio } from "@modelcontextprotocol/server/stdio";
7
+ * import { serverFactory } from "@50heads/mcp";
8
+ * serveStdio(serverFactory({ apiKey: process.env.FIFTYHEADS_API_KEY }));
9
+ * ```
10
+ */
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";
13
+ export { CLIENTS, configSnippets, type ClientId, type Snippet, type SnippetOptions } from "./snippets.js";
14
+ export { SKILL_BODY, SKILL_FILES, SKILL_VERSION } from "./generated/skill.js";
15
+ export { registryServerJson, serverCard } from "./manifest.js";
16
+ export type ServerFactoryOptions = {
17
+ /** An API key (fh_live_…) or an OAuth access token for https://mcp.50heads.com/mcp. */
18
+ apiKey?: string | null;
19
+ /** Called before each API request when tokens rotate; wins over apiKey. */
20
+ token?: () => Promise<string | null>;
21
+ apiUrl?: string;
22
+ portalUrl?: string;
23
+ webUrl?: string;
24
+ };
25
+ /**
26
+ * A server factory for `serveStdio` or `createMcpHandler` from @modelcontextprotocol/server:
27
+ * the 50heads tools, resources and prompts, calling the 50heads API directly.
28
+ */
29
+ export declare function serverFactory(options?: ServerFactoryOptions): (ctx: {
30
+ era: "modern" | "legacy";
31
+ }) => Promise<McpServer>;
32
+ /**
33
+ * Runs the full stdio server (tools, resources, prompts and tasks) on this process's stdin
34
+ * and stdout: what `npx @50heads/mcp` does.
35
+ */
36
+ export declare function serveStdioServer(options?: ServerFactoryOptions): Promise<void>;
@@ -0,0 +1,18 @@
1
+ import { type QuestionType } from "@50heads/shared";
2
+ /**
3
+ * Per-type rules and examples. One source for the 50heads://question-types resource, the skill's
4
+ * references/question-types.md (generated) and the tool descriptions.
5
+ */
6
+ export type QuestionTypeGuide = {
7
+ type: QuestionType;
8
+ title: string;
9
+ use: string;
10
+ options: string;
11
+ minTier: 1 | 2;
12
+ /** How a head answers, and what the distribution counts. */
13
+ answer: string;
14
+ example: Record<string, unknown>;
15
+ };
16
+ export declare const QUESTION_TYPE_GUIDES: Record<QuestionType, QuestionTypeGuide>;
17
+ /** The markdown for the question-types resource and the skill reference. */
18
+ export declare function questionTypesMarkdown(): string;
@@ -0,0 +1,29 @@
1
+ import { type QuestionV2, type ResultFilterInput, type ResultV2 } from "@50heads/shared";
2
+ import { type Currency } from "./pricing.js";
3
+ import type { ListQuestionsOutput, ResultsOutput } from "./schemas.js";
4
+ import type { Rates } from "@50heads/shared";
5
+ /** ResultV2 (API, camelCase) → the MCP results object (snake_case). Never carries a head id. */
6
+ export declare function toResults(r: ResultV2, currency: Currency, rates?: Rates): ResultsOutput;
7
+ /** The MCP filter arguments (snake_case, `keyword`) → the API filter. */
8
+ export declare function toFilter(a: {
9
+ option?: number;
10
+ tier?: number;
11
+ country?: string;
12
+ age_band?: string;
13
+ gender?: "woman" | "man" | "non_binary";
14
+ keyword?: string;
15
+ }): ResultFilterInput;
16
+ /** A head's free-text answer as one quotable line: no control characters, no double quotes. */
17
+ export declare function quoteAnswer(text: string): string;
18
+ /**
19
+ * 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."
21
+ */
22
+ export declare function resultsText(r: ResultsOutput): string;
23
+ export declare function toListItem(q: QuestionV2): ListQuestionsOutput["questions"][number];
24
+ export declare function listText(items: ListQuestionsOutput["questions"]): string;
25
+ /** How long a result may be cached: seconds while live, a day once settled. */
26
+ export declare function resultsCacheHint(status: ResultsOutput["status"]): {
27
+ ttlMs: number;
28
+ cacheScope: "private";
29
+ };