Skip to main content
Glama

leads.find

Find ranked social posts on Reddit, X, YouTube, and TikTok where users describe problems your SaaS solves. AI scores leads and provides outreach hints for your first 100 users.

Instructions

Find ranked social posts where people are describing the problem the user's SaaS solves, across Reddit, X, YouTube, and TikTok. Behavior: dispatches the full server-side pipeline (theme expansion, parallel platform search, AI scoring), persists a run row, blocks until the run completes (typically 60 to 120 seconds), and returns the scored leads. Consumes one credit on the user's plan. Idempotent only via the resulting run_id (use runs.get to re-read without spending another credit). Usage: call this when the user wants the full lead hunt for an idea. Do NOT call it twice for the same idea in the same session, use runs.get to re-analyse. Pair with idea.refine first if the idea is one or two words. After it returns, hand the run_id to outreach.plan for a Week-1 outreach plan and to outreach.draft for per-lead messages. Returns: scored leads (source, channel, title, url, lead_score 0-1, matched_signals including category and outreach hints), plus a header line with totals per source.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
ideaYesThe app idea or product description to find leads for

Implementation Reference

  • src/index.ts:265-294 (registration)
    The tool 'leads.find' is registered using server.tool() with the name 'leads.find', schema (idea: string), and handler callback.
    server.tool(
      "leads.find",
      "Find ranked social posts where people are describing the problem the user's SaaS solves, across Reddit, X, YouTube, and TikTok. Behavior: dispatches the full server-side pipeline (theme expansion, parallel platform search, AI scoring), persists a run row, blocks until the run completes (typically 60 to 120 seconds), and returns the scored leads. Consumes one credit on the user's plan. Idempotent only via the resulting run_id (use runs.get to re-read without spending another credit). Usage: call this when the user wants the full lead hunt for an idea. Do NOT call it twice for the same idea in the same session, use runs.get to re-analyse. Pair with idea.refine first if the idea is one or two words. After it returns, hand the run_id to outreach.plan for a Week-1 outreach plan and to outreach.draft for per-lead messages. Returns: scored leads (source, channel, title, url, lead_score 0-1, matched_signals including category and outreach hints), plus a header line with totals per source.",
      {
        idea: z
          .string()
          .describe("The app idea or product description to find leads for"),
      },
      {
        title: "Find leads",
        readOnlyHint: false,
        destructiveHint: false,
        idempotentHint: false,
        openWorldHint: true,
      },
      async ({ idea }) => {
        const err = requireKey();
        if (err) return err;
    
        const { run_id } = await call<{ run_id: string }>("POST", "run-pipeline", {
          idea,
        });
    
        const result = await waitForCompletion(run_id);
    
        return {
          content: [{ type: "text" as const, text: formatResults(result) }],
        };
      }
    );
  • The handler function for 'leads.find'. It validates the API key via requireKey(), calls the backend endpoint 'POST /run-pipeline' with the idea, waits for completion by polling 'GET /get-run', and formats the results.
    async ({ idea }) => {
      const err = requireKey();
      if (err) return err;
    
      const { run_id } = await call<{ run_id: string }>("POST", "run-pipeline", {
        idea,
      });
    
      const result = await waitForCompletion(run_id);
    
      return {
        content: [{ type: "text" as const, text: formatResults(result) }],
      };
    }
  • TypeScript interfaces for the data types used by leads.find: Post (individual lead result with lead_score, matched_signals, etc.), RunResult (the full pipeline output), and ThemeExpansion.
    interface Post {
      source: string;
      channel: { name: string };
      id: string;
      title: string;
      url: string;
      body_snippet: string;
      score: number;
      num_comments: number;
      created_utc: number;
      lead_score: number;
      validation_score: number;
      matched_signals: string[];
      metadata: Record<string, unknown>;
    }
    
    interface RunResult {
      run_id: string;
      status: "running" | "completed" | "failed" | "partial";
      idea: string;
      results: Post[];
      metadata: {
        total_posts: number;
        elapsed_ms: number;
        errors: string[];
        expansion?: Record<string, unknown>;
        product_title?: string | null;
      };
      steps?: Record<string, { status: string; message: string }>;
    }
  • formats the RunResult into a human-readable string for the MCP response, with lead source breakdown and top 50 leads.
    function formatResults(result: RunResult): string {
      const posts = result.results;
      if (posts.length === 0) {
        return `Run ${result.run_id} completed but found no leads.`;
      }
    
      const sorted = [...posts].sort((a, b) => b.lead_score - a.lead_score);
    
      const sourceCounts = new Map<string, number>();
      for (const p of sorted) {
        sourceCounts.set(p.source, (sourceCounts.get(p.source) ?? 0) + 1);
      }
      const breakdown = [...sourceCounts.entries()]
        .map(([s, n]) => `${n} ${s}`)
        .join(", ");
    
      const high = sorted.filter((p) => p.lead_score >= 0.7).length;
    
      const header = `Found ${sorted.length} leads (${breakdown}). ${high} high-intent.`;
      const body = sorted.slice(0, 50).map(formatPost).join("\n\n");
    
      return `${header}\n\n${body}${sorted.length > 50 ? `\n\n... and ${sorted.length - 50} more` : ""}`;
    }
  • Polls the backend (GET /get-run) until the run completes or times out after ~5 minutes.
    async function waitForCompletion(runId: string): Promise<RunResult> {
      for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
        const result = await call<RunResult>("GET", `get-run?id=${runId}`);
        if (result.status !== "running") return result;
        await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
      }
      throw new Error(`Run ${runId} did not complete within ${(MAX_POLL_ATTEMPTS * POLL_INTERVAL_MS) / 1000}s`);
    }
Behavior5/5

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

Discloses pipeline dispatch, blocking (60-120s), credit consumption, and idempotency via run_id, adding context beyond annotations.

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?

Somewhat lengthy but every sentence adds value; front-loaded with purpose and behavior.

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

Completeness5/5

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

Given no output schema, the description thoroughly details return structure and behavior (blocking, credit, run_id), making it complete for the agent.

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

Parameters4/5

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

Adds context to the 'idea' parameter (e.g., 'the problem the user's SaaS solves', suggests refining short ideas), which improves upon the schema's basic description.

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 the tool finds ranked social posts across multiple platforms matching the user's SaaS problem, distinguishing it from siblings like leads.search and idea.refine.

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?

Provides explicit guidance: when to call, not to call twice, pair with idea.refine, and hand run_id to outreach tools. Also warns against duplicate spending.

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/opusforge/gorilla-mcp'

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