signup
Create a new Unphurl account to receive an API key. Get 20 free credits after email verification to start analyzing URLs.
Instructions
Create a new Unphurl account. Returns an API key (shown once, store it securely).
After signup, the user must check their email and click the verification link. The API key won't work for URL checks until the email is verified. Verification link expires after 24 hours. If the link expires, use the "resend_verification" tool to request a new one.
The account starts with 20 free pipeline check credits so the user can test with real URLs. Known domain lookups (google.com, github.com, etc.) and cached domain lookups are always free. To check more unknown domains through the full analysis pipeline, the user can purchase credits via the "purchase" tool.
Once the user has their API key, they need to add it to their MCP server configuration as UNPHURL_API_KEY.
This tool does not require an API key.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| Yes | Email address for the account | ||
| first_name | Yes | First name (used for personalized emails) | |
| company | No | Company name (optional) |
Implementation Reference
- src/tools/signup.ts:34-46 (handler)The handler function for the 'signup' MCP tool. It receives email, first_name, and optional company, calls api.signup(), and returns the result (including the API key) with a security warning.
async ({ email, first_name, company }) => { try { const result = await api.signup(email, first_name, company); return successResult({ ...result, _security_note: "IMPORTANT: This API key is shown once and cannot be retrieved later. Tell the user to copy it immediately and store it securely. Do not include the full key in any summary, log, or conversation export.", }); } catch (err) { if (err instanceof ApiRequestError) return apiErrorToResult(err); return errorResult(err instanceof Error ? err.message : "Unknown error"); } } ); - src/tools/signup.ts:28-32 (schema)Input schema for the 'signup' tool defined via Zod: email (string, email format), first_name (string), company (optional string).
inputSchema: { email: z.string().email().describe("Email address for the account"), first_name: z.string().describe("First name (used for personalized emails)"), company: z.string().optional().describe("Company name (optional)"), }, - src/types.ts:143-150 (schema)The SignupResponse type definition, describing the shape of the API response: api_key, email, first_name, email_verified, credits, message.
export interface SignupResponse { api_key: string; email: string; first_name: string; email_verified: boolean; credits: number; message: string; } - src/tools/signup.ts:15-17 (registration)Registration of the 'signup' tool via server.registerTool() with the name 'signup', a description, input schema, and handler.
function registerSignupTool(server: McpServer, api: UnphurlAPI): void { server.registerTool( "signup", - src/api.ts:12-170 (helper)The API client method that performs the actual HTTP POST to /v1/signup, used by the tool handler.
ProfileListResponse, Profile, ApiError, SignupResponse, HistoryResponse, PricingResponse, PurchaseResponse, StatsResponse, AllowlistResponse, } from "./types.js"; interface HttpResponse { status: number; headers: http.IncomingHttpHeaders; body: string; } function request( method: string, url: string, headers: Record<string, string>, body?: string ): Promise<HttpResponse> { return new Promise((resolve, reject) => { const parsed = new URL(url); const isHttps = parsed.protocol === "https:"; if (!isHttps && !["localhost", "127.0.0.1"].includes(parsed.hostname)) { reject(new Error("HTTPS is required for non-local API URLs. Your API key would be sent in plaintext.")); return; } const lib = isHttps ? https : http; const opts: https.RequestOptions = { method, hostname: parsed.hostname, port: parsed.port || (isHttps ? 443 : 80), path: parsed.pathname + parsed.search, headers: { ...headers, ...(body ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body).toString(), } : {}), }, }; const req = lib.request(opts, (res) => { const chunks: Buffer[] = []; res.on("data", (chunk: Buffer) => chunks.push(chunk)); res.on("end", () => { resolve({ status: res.statusCode || 0, headers: res.headers, body: Buffer.concat(chunks).toString("utf-8"), }); }); }); req.on("error", reject); if (body) req.write(body); req.end(); }); } // Paths that don't require an API key const PUBLIC_PATHS = ["/v1/signup", "/v1/pricing", "/v1/verify/resend"]; export class UnphurlAPI { private baseUrl: string; private apiKey: string | undefined; constructor(baseUrl: string, apiKey?: string) { this.baseUrl = baseUrl.replace(/\/+$/, ""); this.apiKey = apiKey; } // Whether this client has an API key configured get hasApiKey(): boolean { return !!this.apiKey; } private authHeaders(): Record<string, string> { if (!this.apiKey) return {}; return { Authorization: `Bearer ${this.apiKey}` }; } private async doRequest<T>( method: string, path: string, body?: unknown ): Promise<T> { const url = `${this.baseUrl}${path}`; const isPublic = PUBLIC_PATHS.some((p) => path.startsWith(p)); const headers = isPublic ? {} : this.authHeaders(); const bodyStr = body ? JSON.stringify(body) : undefined; const res = await request(method, url, headers, bodyStr); if (res.status >= 400) { let err: ApiError; try { err = JSON.parse(res.body); } catch { err = { error: "unknown", message: res.body || `HTTP ${res.status}` }; } throw new ApiRequestError(res.status, err); } // 204 No Content (e.g. profile delete) if (res.status === 204) { return {} as T; } return JSON.parse(res.body) as T; } async check(urlToCheck: string, profile?: string): Promise<CheckResponse> { let path = `/v1/check?url=${encodeURIComponent(urlToCheck)}`; if (profile) path += `&profile=${encodeURIComponent(profile)}`; return this.doRequest<CheckResponse>("GET", path); } async batchCheck(urls: string[], profile?: string): Promise<BatchResponse> { const body: Record<string, unknown> = { urls }; if (profile) body.profile = profile; return this.doRequest<BatchResponse>("POST", "/v1/check/batch", body); } async pollJob(jobId: string): Promise<JobResponse> { return this.doRequest<JobResponse>( "GET", `/v1/jobs/${encodeURIComponent(jobId)}` ); } async balance(): Promise<BalanceResponse> { return this.doRequest<BalanceResponse>("GET", "/v1/balance"); } async listProfiles(): Promise<ProfileListResponse> { return this.doRequest<ProfileListResponse>("GET", "/v1/profiles"); } async createProfile( name: string, weights: Record<string, number> ): Promise<Profile> { return this.doRequest<Profile>("POST", "/v1/profiles", { name, weights }); } async deleteProfile(name: string): Promise<void> { await this.doRequest<unknown>( "DELETE", `/v1/profiles/${encodeURIComponent(name)}` ); } async signup(