leads.search
Run targeted searches on Reddit, X (Twitter), YouTube, or TikTok using your own queries. Get raw posts without AI scoring. Use when automated search under-fetches or to test custom query phrasings.
Instructions
Run an ad-hoc search against ONE social platform (Reddit, X, YouTube, or TikTok) with caller-provided queries. Behavior: hits the platform-specific search edge function directly, bypassing theme-expansion and AI scoring. Consumes one credit per call. If a run_id is passed, results are written to that run for inspection later via runs.get. Without run_id, results are returned but not persisted. Usage: call this when leads.find under-fetched on a specific platform, or to test custom query phrasings (the queries you pass in ARE the queries that get run, no expansion). Do NOT use this as a substitute for leads.find when you want full pipeline behaviour: results from leads.search are unscored. To search all four platforms with AI scoring, call leads.find instead. Returns: leads array (raw posts with platform fields, no lead_score) and a count.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| source | Yes | Which platform to search. Use 'x' for X (formerly Twitter); 'twitter' is accepted as an alias. | |
| queries | Yes | Search queries to run on the platform | |
| run_id | No | Optional run ID to attach results to an existing run (writes to DB) |
Implementation Reference
- src/index.ts:397-463 (registration)Registration of the 'leads.search' tool on the MCP server via server.tool() with name, description, schema, and handler.
server.tool( "leads.search", "Run an ad-hoc search against ONE social platform (Reddit, X, YouTube, or TikTok) with caller-provided queries. Behavior: hits the platform-specific search edge function directly, bypassing theme-expansion and AI scoring. Consumes one credit per call. If a run_id is passed, results are written to that run for inspection later via runs.get. Without run_id, results are returned but not persisted. Usage: call this when leads.find under-fetched on a specific platform, or to test custom query phrasings (the queries you pass in ARE the queries that get run, no expansion). Do NOT use this as a substitute for leads.find when you want full pipeline behaviour: results from leads.search are unscored. To search all four platforms with AI scoring, call leads.find instead. Returns: leads array (raw posts with platform fields, no lead_score) and a count.", { source: z .enum(["reddit", "x", "twitter", "youtube", "tiktok"]) .describe("Which platform to search. Use 'x' for X (formerly Twitter); 'twitter' is accepted as an alias."), queries: z .array(z.string()) .describe("Search queries to run on the platform"), run_id: z .string() .optional() .describe( "Optional run ID to attach results to an existing run (writes to DB)" ), }, { title: "Search a single source", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, }, async ({ source, queries, run_id }) => { const err = requireKey(); if (err) return err; // Backend edge function is search-twitter. Accept 'x' as the canonical // user-facing name and route it to the same backend. const backendSource = source === "x" ? "twitter" : source; const body: Record<string, unknown> = { queries }; if (run_id) body.run_id = run_id; const { leads, count } = await call<{ leads: Post[]; count: number }>( "POST", `search-${backendSource}`, body ); if (count === 0) { return { content: [ { type: "text" as const, text: `No leads found on ${source} for queries: ${queries.join(", ")}`, }, ], }; } const formatted = leads .slice(0, 20) .map(formatPost) .join("\n\n"); return { content: [ { type: "text" as const, text: `Found ${count} leads on ${source}:\n\n${formatted}${count > 20 ? `\n\n... and ${count - 20} more` : ""}`, }, ], }; } ); - src/index.ts:400-413 (schema)Input schema for leads.search using Zod: source (enum: reddit, x, twitter, youtube, tiktok), queries (array of strings), and optional run_id.
{ source: z .enum(["reddit", "x", "twitter", "youtube", "tiktok"]) .describe("Which platform to search. Use 'x' for X (formerly Twitter); 'twitter' is accepted as an alias."), queries: z .array(z.string()) .describe("Search queries to run on the platform"), run_id: z .string() .optional() .describe( "Optional run ID to attach results to an existing run (writes to DB)" ), }, - src/index.ts:421-462 (handler)Handler function that validates the API key, normalizes 'x'/'twitter' source, calls the backend search-{platform} edge function via the call() helper, formats results using formatPost(), and returns up to 20 leads.
async ({ source, queries, run_id }) => { const err = requireKey(); if (err) return err; // Backend edge function is search-twitter. Accept 'x' as the canonical // user-facing name and route it to the same backend. const backendSource = source === "x" ? "twitter" : source; const body: Record<string, unknown> = { queries }; if (run_id) body.run_id = run_id; const { leads, count } = await call<{ leads: Post[]; count: number }>( "POST", `search-${backendSource}`, body ); if (count === 0) { return { content: [ { type: "text" as const, text: `No leads found on ${source} for queries: ${queries.join(", ")}`, }, ], }; } const formatted = leads .slice(0, 20) .map(formatPost) .join("\n\n"); return { content: [ { type: "text" as const, text: `Found ${count} leads on ${source}:\n\n${formatted}${count > 20 ? `\n\n... and ${count - 20} more` : ""}`, }, ], }; } - src/index.ts:119-133 (helper)Post interface used by leads.search - defines the shape of returned lead objects with fields like source, channel, id, title, url, body_snippet, score, lead_score, etc.
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>; } - src/index.ts:168-197 (helper)formatPost helper function used by leads.search to format a single Post into a human-readable string with score bucket, channel, title, score, comments, category, outreach, snippet, and URL.
function formatPost(p: Post): string { const bucket = scoreBucket(p.lead_score); const ch = p.source === "reddit" ? `r/${p.channel.name}` : p.source === "twitter" ? `@${p.channel.name}` : `${p.source}/${p.channel.name}`; const category = p.matched_signals.find((s) => s.startsWith("category:"))?.slice(9) ?? ""; const outreach = p.matched_signals.find((s) => s.startsWith("outreach:"))?.slice(9) ?? ""; const snippet = p.body_snippet.length > 150 ? `${p.body_snippet.slice(0, 150)}...` : p.body_snippet; return [ `[${bucket}] "${p.title}" · ${ch}`, ` Score: ${p.lead_score.toFixed(2)} | ${p.score} pts | ${p.num_comments} comments`, category ? ` Category: ${category}` : null, outreach ? ` Outreach: ${outreach}` : null, snippet ? ` ${snippet}` : null, ` ${p.url}`, ] .filter(Boolean) .join("\n"); }