Skip to main content
Glama

get_campaign_leads

Retrieve leads from a specific campaign by campaign ID. Requires leads_retrieval permission.

Instructions

Get leads generated by a specific campaign. Requires leads_retrieval permission.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
campaign_idYesCampaign ID
fieldsNoComma-separated fields to return
limitNoNumber of results (default 25)
afterNoPagination cursor for next page

Implementation Reference

  • The handler function for the 'get_campaign_leads' tool. It accepts campaign_id, fields, limit, and after parameters, then calls the AdsClient.get() with the campaign ID and `/leads` endpoint to retrieve leads.
    // ─── get_campaign_leads ────────────────────────────────────
    server.tool(
      "get_campaign_leads",
      "Get leads generated by a specific campaign. Requires leads_retrieval permission.",
      {
        campaign_id: z.string().describe("Campaign ID"),
        fields: z.string().optional().describe("Comma-separated fields to return"),
        limit: z.number().optional().default(25).describe("Number of results (default 25)"),
        after: z.string().optional().describe("Pagination cursor for next page"),
      },
      async ({ campaign_id, fields, limit, after }) => {
        try {
          const params: Record<string, unknown> = {};
          if (fields) params.fields = fields;
          if (limit) params.limit = limit;
          if (after) params.after = after;
          const { data, rateLimit } = await client.get(`/${campaign_id}/leads`, params);
          return { content: [{ type: "text" as const, text: JSON.stringify({ ...data as object, _rateLimit: rateLimit }, null, 2) }] };
        } catch (error) {
          return { content: [{ type: "text" as const, text: `Failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
        }
      }
    );
  • Input schema definition for get_campaign_leads using zod: campaign_id (required string), fields (optional string), limit (optional number, default 25), after (optional pagination cursor string).
    server.tool(
      "get_campaign_leads",
      "Get leads generated by a specific campaign. Requires leads_retrieval permission.",
      {
        campaign_id: z.string().describe("Campaign ID"),
        fields: z.string().optional().describe("Comma-separated fields to return"),
        limit: z.number().optional().default(25).describe("Number of results (default 25)"),
        after: z.string().optional().describe("Pagination cursor for next page"),
      },
  • The registerCampaignTools function registers all campaign tools (including get_campaign_leads) on the McpServer instance. Called from src/index.ts line 50.
    export function registerCampaignTools(server: McpServer, client: AdsClient): void {
      // ─── list_campaigns ────────────────────────────────────────
      server.tool(
        "list_campaigns",
        "List campaigns in the ad account. Supports filtering by status and objective. Returns paginated results.",
        {
          status: z.string().optional().describe("Filter by status: ACTIVE, PAUSED, DELETED, ARCHIVED"),
          objective: z.string().optional().describe("Filter by objective: OUTCOME_AWARENESS, OUTCOME_ENGAGEMENT, OUTCOME_LEADS, OUTCOME_SALES, OUTCOME_TRAFFIC, OUTCOME_APP_PROMOTION"),
          fields: z.string().optional().describe("Comma-separated fields to return"),
          limit: z.number().optional().default(25).describe("Number of results (default 25)"),
          after: z.string().optional().describe("Pagination cursor for next page"),
          before: z.string().optional().describe("Pagination cursor for previous page"),
        },
        async ({ status, objective, fields, limit, after, before }) => {
          try {
            const params: Record<string, unknown> = {};
            if (fields) params.fields = fields;
            if (limit) params.limit = limit;
            if (after) params.after = after;
            if (before) params.before = before;
            if (status) params.effective_status = `["${status}"]`;
            if (objective) params.objective = objective;
            const { data, rateLimit } = await client.get(`${client.accountPath}/campaigns`, params);
            return { content: [{ type: "text" as const, text: JSON.stringify({ ...data as object, _rateLimit: rateLimit }, null, 2) }] };
          } catch (error) {
            return { content: [{ type: "text" as const, text: `Failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
          }
        }
      );
    
      // ─── get_campaign ──────────────────────────────────────────
      server.tool(
        "get_campaign",
        "Get details of a specific campaign by ID.",
        {
          campaign_id: z.string().describe("Campaign ID"),
          fields: z.string().optional().describe("Comma-separated fields to return"),
        },
        async ({ campaign_id, fields }) => {
          try {
            const params: Record<string, unknown> = {};
            if (fields) params.fields = fields;
            const { data, rateLimit } = await client.get(`/${campaign_id}`, params);
            return { content: [{ type: "text" as const, text: JSON.stringify({ ...data as object, _rateLimit: rateLimit }, null, 2) }] };
          } catch (error) {
            return { content: [{ type: "text" as const, text: `Failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
          }
        }
      );
    
      // ─── create_campaign ───────────────────────────────────────
      server.tool(
        "create_campaign",
        "Create a new ad campaign. Defaults to PAUSED status. Requires name and objective. Budget can be set at campaign or ad set level.",
        {
          name: z.string().describe("Campaign name"),
          objective: z.enum([
            "OUTCOME_AWARENESS",
            "OUTCOME_ENGAGEMENT",
            "OUTCOME_LEADS",
            "OUTCOME_SALES",
            "OUTCOME_TRAFFIC",
            "OUTCOME_APP_PROMOTION",
          ]).describe("Campaign objective"),
          status: z.string().optional().default("PAUSED").describe("Campaign status (default PAUSED)"),
          daily_budget: z.string().optional().describe("Daily budget in account currency cents (e.g. '5000' = $50.00)"),
          lifetime_budget: z.string().optional().describe("Lifetime budget in account currency cents"),
          special_ad_categories: z.string().optional().describe("JSON array of special ad categories: CREDIT, EMPLOYMENT, HOUSING, ISSUES_ELECTIONS_POLITICS"),
          start_time: z.string().optional().describe("Campaign start time (ISO 8601 format)"),
          stop_time: z.string().optional().describe("Campaign stop time (ISO 8601 format)"),
        },
        async ({ name, objective, status, daily_budget, lifetime_budget, special_ad_categories, start_time, stop_time }) => {
          try {
            const params: Record<string, unknown> = { name, objective, status };
            if (daily_budget) params.daily_budget = daily_budget;
            if (lifetime_budget) params.lifetime_budget = lifetime_budget;
            if (special_ad_categories) params.special_ad_categories = special_ad_categories;
            if (start_time) params.start_time = start_time;
            if (stop_time) params.stop_time = stop_time;
            const { data, rateLimit } = await client.post(`${client.accountPath}/campaigns`, params);
            return { content: [{ type: "text" as const, text: JSON.stringify({ ...data as object, _rateLimit: rateLimit }, null, 2) }] };
          } catch (error) {
            return { content: [{ type: "text" as const, text: `Failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
          }
        }
      );
    
      // ─── update_campaign ───────────────────────────────────────
      server.tool(
        "update_campaign",
        "Update an existing campaign. Only provided fields will be modified.",
        {
          campaign_id: z.string().describe("Campaign ID to update"),
          name: z.string().optional().describe("New campaign name"),
          status: z.string().optional().describe("New status: ACTIVE, PAUSED, DELETED, ARCHIVED"),
          daily_budget: z.string().optional().describe("New daily budget in currency cents"),
          lifetime_budget: z.string().optional().describe("New lifetime budget in currency cents"),
          start_time: z.string().optional().describe("New start time (ISO 8601)"),
          stop_time: z.string().optional().describe("New stop time (ISO 8601)"),
        },
        async ({ campaign_id, name, status, daily_budget, lifetime_budget, start_time, stop_time }) => {
          try {
            const params: Record<string, unknown> = {};
            if (name) params.name = name;
            if (status) params.status = status;
            if (daily_budget) params.daily_budget = daily_budget;
            if (lifetime_budget) params.lifetime_budget = lifetime_budget;
            if (start_time) params.start_time = start_time;
            if (stop_time) params.stop_time = stop_time;
            const { data, rateLimit } = await client.post(`/${campaign_id}`, params);
            return { content: [{ type: "text" as const, text: JSON.stringify({ ...data as object, _rateLimit: rateLimit }, null, 2) }] };
          } catch (error) {
            return { content: [{ type: "text" as const, text: `Failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
          }
        }
      );
    
      // ─── delete_campaign ───────────────────────────────────────
      server.tool(
        "delete_campaign",
        "Delete a campaign. This action is irreversible.",
        {
          campaign_id: z.string().describe("Campaign ID to delete"),
        },
        async ({ campaign_id }) => {
          try {
            const { data, rateLimit } = await client.delete(`/${campaign_id}`);
            return { content: [{ type: "text" as const, text: JSON.stringify({ success: true, ...data as object, _rateLimit: rateLimit }, null, 2) }] };
          } catch (error) {
            return { content: [{ type: "text" as const, text: `Failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
          }
        }
      );
    
      // ─── copy_campaign ──────────────────────────────────────────
      server.tool(
        "copy_campaign",
        "Copy an existing campaign. Creates a duplicate with optional name override. Copies structure including ad sets and ads.",
        {
          campaign_id: z.string().describe("Source campaign ID to copy"),
          name: z.string().optional().describe("Name for the copied campaign. Defaults to 'Copy of <original>'"),
          status: z.string().optional().default("PAUSED").describe("Status for copied campaign (default PAUSED)"),
          deep_copy: z.boolean().optional().default(true).describe("Copy ad sets and ads within the campaign (default true)"),
        },
        async ({ campaign_id, name, status, deep_copy }) => {
          try {
            const params: Record<string, unknown> = {};
            if (name) params.rename_options = JSON.stringify({ rename_suffix: "", rename_prefix: "", new_name_prefix: name });
            if (status) params.status_option = status;
            if (deep_copy !== undefined) params.deep_copy = deep_copy;
            const { data, rateLimit } = await client.post(`/${campaign_id}/copies`, params);
            return { content: [{ type: "text" as const, text: JSON.stringify({ ...data as object, _rateLimit: rateLimit }, null, 2) }] };
          } catch (error) {
            return { content: [{ type: "text" as const, text: `Failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
          }
        }
      );
    
      // ─── get_campaign_adsets ───────────────────────────────────
      server.tool(
        "get_campaign_adsets",
        "Get all ad sets belonging to a specific campaign.",
        {
          campaign_id: z.string().describe("Campaign ID"),
          fields: z.string().optional().describe("Comma-separated fields to return"),
          limit: z.number().optional().default(25).describe("Number of results (default 25)"),
          after: z.string().optional().describe("Pagination cursor for next page"),
        },
        async ({ campaign_id, fields, limit, after }) => {
          try {
            const params: Record<string, unknown> = {};
            if (fields) params.fields = fields;
            if (limit) params.limit = limit;
            if (after) params.after = after;
            const { data, rateLimit } = await client.get(`/${campaign_id}/adsets`, params);
            return { content: [{ type: "text" as const, text: JSON.stringify({ ...data as object, _rateLimit: rateLimit }, null, 2) }] };
          } catch (error) {
            return { content: [{ type: "text" as const, text: `Failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
          }
        }
      );
    
      // ─── get_campaign_ads ──────────────────────────────────────
      server.tool(
        "get_campaign_ads",
        "Get all ads belonging to a specific campaign.",
        {
          campaign_id: z.string().describe("Campaign ID"),
          fields: z.string().optional().describe("Comma-separated fields to return"),
          limit: z.number().optional().default(25).describe("Number of results (default 25)"),
          after: z.string().optional().describe("Pagination cursor for next page"),
        },
        async ({ campaign_id, fields, limit, after }) => {
          try {
            const params: Record<string, unknown> = {};
            if (fields) params.fields = fields;
            if (limit) params.limit = limit;
            if (after) params.after = after;
            const { data, rateLimit } = await client.get(`/${campaign_id}/ads`, params);
            return { content: [{ type: "text" as const, text: JSON.stringify({ ...data as object, _rateLimit: rateLimit }, null, 2) }] };
          } catch (error) {
            return { content: [{ type: "text" as const, text: `Failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
          }
        }
      );
    
      // ─── get_campaign_leads ────────────────────────────────────
      server.tool(
        "get_campaign_leads",
        "Get leads generated by a specific campaign. Requires leads_retrieval permission.",
        {
          campaign_id: z.string().describe("Campaign ID"),
          fields: z.string().optional().describe("Comma-separated fields to return"),
          limit: z.number().optional().default(25).describe("Number of results (default 25)"),
          after: z.string().optional().describe("Pagination cursor for next page"),
        },
        async ({ campaign_id, fields, limit, after }) => {
          try {
            const params: Record<string, unknown> = {};
            if (fields) params.fields = fields;
            if (limit) params.limit = limit;
            if (after) params.after = after;
            const { data, rateLimit } = await client.get(`/${campaign_id}/leads`, params);
            return { content: [{ type: "text" as const, text: JSON.stringify({ ...data as object, _rateLimit: rateLimit }, null, 2) }] };
          } catch (error) {
            return { content: [{ type: "text" as const, text: `Failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
          }
        }
      );
    }
  • The AdsClient helper class used by the handler to make API calls. The get_campaign_leads handler uses client.get() to fetch leads from the Facebook Graph API.
    export class AdsClient {
      private config: AdsConfig;
      private baseUrl = "https://graph.facebook.com/v25.0";
    
      constructor(config: AdsConfig) {
        this.config = config;
      }

Schema Changelog

Changes observed during successful MCP inspections.

  1. First observedv1.2.0

TDQS

A3.7/5.0
Behavior3/5

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

No annotations are provided, so the description carries the burden. 'Get' implies a read-only operation, and the permission note is useful, but the description does not explicitly state non-destructive behavior, return format, pagination behavior, or rate limits. It adds some context beyond the schema but omits important behavioral details.

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

Conciseness5/5

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

Two concise sentences, no fluff, front-loaded with the main action. The permission note is relevant and efficiently stated. Every word earns its place.

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

Completeness3/5

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

For a simple getter with 100% parameter coverage and no output schema, the description is adequate but leaves gaps. It states what it does and a permission requirement, but does not describe what the response looks like, how pagination works, or any exclusions. Given no output schema, the description should have provided a bit more clarity on return behavior.

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 schema already documents all parameters. The description adds no additional meaning to parameters beyond 'specific campaign' mapping to campaign_id, which is already stated in the schema. Baseline 3 applies because the schema does the heavy lifting.

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 a specific verb and resource: 'Get leads generated by a specific campaign.' It distinguishes itself from sibling tools like get_campaign_ads (ads, not leads), get_adset_leads (leads at adset level), and get_form_leads (form-specific leads) by scoping to campaign. The permission requirement adds precision.

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

Usage Guidelines3/5

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

The description implies usage context ('specific campaign') but does not explicitly state when to use this tool versus alternatives like get_adset_leads or get_form_leads. It provides a prerequisite (permission) but no alternatives or exclusions. Usage guidance is implied, not explicit.

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

Deploy Server

Other Tools