Skip to main content
Glama
123Ergo

unphurl-mcp

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

TableJSON Schema
NameRequiredDescriptionDefault
emailYesEmail address for the account
first_nameYesFirst name (used for personalized emails)
companyNoCompany name (optional)

Implementation Reference

  • 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");
        }
      }
    );
  • 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)"),
    },
  • 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;
    }
  • 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",
  • 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(
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description fully discloses behavior: account creation, email verification within 24 hours, free credits, and the need to configure the API key. No hidden effects.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with logical flow and each sentence adds value, though slightly lengthy. Could be tightened slightly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers the full signup workflow, including verification, credits, and configuration. Lacks error response details but sufficient for an account creation tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the description adds little beyond the schema. It provides context but no new parameter-level details.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'Create a new Unphurl account' and details the return of an API key. It implicitly distinguishes from siblings like resend_verification and purchase by explaining post-signup actions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly instructs to use resend_verification if the verification link expires and purchase for credit top-ups. Also notes that this tool does not require an API key, implying other tools do.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/123Ergo/unphurl-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server