DEV Community

Cover image for Why Exposing Facebook Comments to MCP Agents Times Out on 300 Second Runs
Crawler Bros
Crawler Bros

Posted on

Why Exposing Facebook Comments to MCP Agents Times Out on 300 Second Runs

Exposing Facebook Comments to AI Agents via Model Context Protocol

When you expose a data extraction tool to an AI agent using the Model Context Protocol (MCP), the agent does not see a web interface or a simplified SDK wrapper. It sees a raw JSON Schema. If you expose facebook-comments-scraper (which extracts public comments from Facebook posts, Watch videos, and photo stories) directly to an LLM via the Apify MCP server, the agent must parse, understand, and accurately populate a highly nested input structure.

The integration relies on the Apify MCP server at https://mcp.apify.com, scoped using the ?tools=facebook-comments-scraper query parameter. This scoping isolates the toolset so the agent is not overwhelmed by thousands of unrelated utilities. However, once isolated, the agent is solely responsible for generating valid payloads that conform to the scraper's exact technical specifications.

When an agent processes a user request like "find out what people think about this post," it has to translate that intent into a tool call. If the input schema is too permissive or if the agent misunderstands the default properties, the execution fails before a single container starts. The most common point of failure is how the agent handles the transition from natural language requests to structured API payloads, particularly around URL formats and proxy configurations.

To expose facebook-comments-scraper to an AI agent, you can configure your MCP client to connect to the Apify MCP host with the specific actor tool scoped in the connection URL. This exposes only the target actor as a callable tool, reducing context window bloat and preventing the model from hallucinating input parameters.

Here is an example configuration for a Claude Desktop developer setup or any standard MCP host file that registers the scoped tool:

{
  "mcpServers": {
    "apify-facebook-comments": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-env",
        "https://mcp.apify.com?tools=facebook-comments-scraper"
      ],
      "env": {
        "APIFY_TOKEN": "YOUR_APIFY_API_TOKEN"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

When this configuration loads, the MCP server queries the actor's metadata and translates its input schema into a tool definition. The agent receives a tool called facebook-comments-scraper along with a structured JSON Schema describing what arguments it must generate.

What tool signature does the agent interpret?

The tool signature is an OpenAPI-like schema that maps the Actor's input fields into JSON Schema definitions for function calling. The AI model inspects field types, descriptions, and defaults to determine required properties and optional operational flags.

Checked against the Actor's input schema and Apify docs on 2026-09-23.

Here is the exact schema signature that the AI agent interprets when deciding how to invoke the tool:

{
  "name": "facebook-comments-scraper",
  "description": "Scrape public comments from Facebook posts, Watch videos, and photo stories. Extract comment text, author info, reactions, timestamps, and nested replies via GraphQL pagination.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "startUrls": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "url": {
              "type": "string",
              "format": "uri"
            }
          },
          "required": ["url"]
        },
        "description": "Start URLs | Facebook post, Reel, Watch, or photo URLs to scrape comments from. Supports /posts/, /reel/, /watch?v=, /photo?fbid=, /groups/.../permalink/, and /share/p/ URLs."
      },
      "maxItems": {
        "type": "integer",
        "default": 100,
        "description": "Max items per URL | Maximum number of comment rows to collect per start URL (not global across all URLs)."
      },
      "onlyCommentsNewerThan": {
        "type": "string",
        "description": "Only comments newer than | Limit results to comments posted after this date. Accepts YYYY-MM-DD, a full ISO 8601 timestamp (e.g. 2024-06-15T12:00:00Z), or a relative value like '1 day', '2 weeks', '3 months'"
      },
      "commentsMode": {
        "type": "string",
        "enum": ["ALL", "NEWEST", "MOST_RELEVANT"],
        "default": "ALL",
        "description": "Comments mode | Which comment view to scrape: ALL (Facebook's unfiltered 'All comments' view, in Facebook's own ranked ordering), NEWEST (newest first), or MOST_RELEVANT (Facebook's ranked 'Most relevant' view)."
      },
      "includeNestedComments": {
        "type": "boolean",
        "default": true,
        "description": "Include nested comments | If true, also scrape reply rows under top-level comments (with parentComment/parentReply metadata). If false, only top-level comments are collected (faster)."
      },
      "maxReplyDepth": {
        "type": "integer",
        "default": 1,
        "description": "Max reply nesting depth | How many levels of nested replies to follow when 'Include nested comments' is enabled. 1 = only direct replies to top-level comments (Facebook's most reliably available level)."
      },
      "proxy": {
        "type": "object",
        "default": {
          "useApifyProxy": true,
          "apifyProxyGroups": ["RESIDENTIAL"]
        },
        "description": "Proxy Configuration | Residential proxy recommended for reliable results, especially at higher volume or concurrency."
      }
    },
    "required": ["startUrls"]
  }
}
Enter fullscreen mode Exit fullscreen mode

The agent uses this system description to construct its payload. If the user provides a raw URL string, the agent must wrap it inside the startUrls array of objects. Any failure to build this specific structure results in a validation error before the execution reaches the scraper.

Why does relying on UI prefill fields break programmatic agent calls?

The Apify API completely ignores schema prefill fields because they are console UI helpers rather than backend defaults. Programmatic calls and MCP drivers only apply schema default values, so relying on prefill hints causes runs to execute without intended configurations.

On the Apify platform, there is a distinct separation between how the Console UI is rendered and how the underlying API processes inputs. The prefill field in an actor's input schema controls what a human operator sees when they open the scraper inside their web browser. It is a user-experience design pattern to help humans get started quickly.

However, the Apify API does not apply prefill fields when executing runs via API endpoints or tool calls. Only true default values are merged into the payload if a key is omitted. This means if you program your agent to rely on UI-level hints or if the agent assumes certain complex objects (like proxy parameters) will automatically populate with human-centric prefilled data, the API will receive an incomplete structure.

To guarantee that your agent runs do not fail due to missing network routing parameters, the agent must be instructed to always construct and pass an explicit input payload containing all required keys. You cannot count on the platform to merge UI-level prefills behind the scenes.

How do you handle the 300 second synchronous run cap?

You handle the cap by initiating the Actor run asynchronously and polling its status before fetching dataset items. Synchronous endpoints cut connections after 300 seconds with an HTTP 408 error, making asynchronous polling mandatory for large thread extraction.

The synchronous run endpoint on the Apify platform has a strict physical cap of 300 seconds. If an Actor run exceeds this limit during a synchronous HTTP POST request, the platform terminates the HTTP connection and returns an HTTP 408 (Request Timeout) status.

When an AI agent triggers a run synchronously via an MCP tool call, it waits for the HTTP response. For threads with thousands of comments, deep nesting levels, or complex anti-scraping challenges, scraping can easily push past the 5-minute barrier.

To avoid this failure mode, any agent integration that handles large data sets must run the Actor asynchronously. Instead of waiting for the results in a single blocking request, the agent must POST to the asynchronous runs endpoint, receive a run ID, poll the run status, and then fetch the items from the default dataset once the run completes successfully.

Here is a Python implementation that demonstrates how to execute this asynchronous execution flow and handle potential timeouts gracefully:

import time
import requests

def run_facebook_scraper_async(api_token, start_url, max_items=100):
    actor_id = "facebook-comments-scraper"
    initiate_url = f"https://api.apify.com/v2/acts/{actor_id}/runs?token={api_token}"

    payload = {
        "startUrls": [{"url": start_url}],
        "maxItems": max_items,
        "commentsMode": "ALL",
        "includeNestedComments": True,
        "maxReplyDepth": 1,
        "proxy": {
            "useApifyProxy": True,
            "apifyProxyGroups": ["RESIDENTIAL"]
        }
    }

    response = requests.post(initiate_url, json=payload)
    if response.status_code != 201:
        raise RuntimeError(f"Failed to start Actor run: {response.text}")

    run_data = response.json().get("data", {})
    run_id = run_data.get("id")
    dataset_id = run_data.get("defaultDatasetId")

    status_url = f"https://api.apify.com/v2/actor-runs/{run_id}?token={api_token}"
    while True:
        status_response = requests.get(status_url)
        if status_response.status_code != 200:
            raise RuntimeError(f"Failed to fetch run status: {status_response.text}")

        status_data = status_response.json().get("data", {})
        status = status_data.get("status")

        if status == "SUCCEEDED":
            break
        elif status in ["FAILED", "ABORTED", "TIMED-OUT"]:
            raise RuntimeError(f"Actor run terminated with status: {status}")

        time.sleep(10)

    items_url = f"https://api.apify.com/v2/datasets/{dataset_id}/items?token={api_token}"
    items_response = requests.get(items_url)
    if items_response.status_code != 200:
        raise RuntimeError(f"Failed to fetch dataset items: {items_response.text}")

    return items_response.json()
Enter fullscreen mode Exit fullscreen mode

How can an AI agent detect that the max total charge cap has been tripped?

An AI agent detects a tripped charge cap by inspecting run metadata for an ABORTED status combined with budget limit termination flags. Reading these fields lets the agent distinguish between empty post engagement and intentional cost enforcement shutdowns.

Apify allows you to pass a limit on how much a run can cost by using the maxTotalChargeUsd query parameter on the run endpoint. This maps internally to the environment variable ACTOR_MAX_TOTAL_CHARGE_USD inside the running container.

When the cost of the run reaches this threshold, the platform flags the run to abort. However, this is not an instantaneous hard kill. The container continues to run briefly while cleaning up resources, which means it may still consume minor compute overhead during its termination phase.

An AI agent processing the outputs of a run must check the termination metadata. If the run ended because it hit the budget ceiling, the agent must inform the user that the data is incomplete due to budget limits rather than assuming the post simply had no more comments.

Here is a JavaScript example showing how to inspect the run details and determine if a run was stopped by the cost safety cap:

async function checkRunBudgetStatus(apiToken, runId) {
    const url = `https://api.apify.com/v2/actor-runs/${runId}?token=${apiToken}`;
    const response = await fetch(url);
    if (!response.ok) {
        throw new Error(`Failed to fetch run metadata: ${response.statusText}`);
    }

    const { data } = await response.json();
    const terminationReason = data.terminationReason || '';
    const wasAborted = data.status === 'ABORTED';

    if (wasAborted && terminationReason.includes('MAX_CHARGES_REACHED')) {
        return {
            complete: false,
            reason: "Budget limit reached. The output data represents a partial scrape.",
            chargedAmount: data.usageUsd
        };
    }

    return {
        complete: data.status === 'SUCCEEDED',
        reason: `Run completed with status: ${data.status}`,
        chargedAmount: data.usageUsd
    };
}
Enter fullscreen mode Exit fullscreen mode

What are the limitations and caveats of this scraper?

When utilizing facebook-comments-scraper, several hard limitations exist in both the scraper itself and the Apify infrastructure that can cause an AI agent to fail if not handled in code.

  • Reels Require Login: The scraper explicitly states that Reels (/reel/ URLs) are not accessible without a Facebook session. Because this scraper operates without requiring authentication, passing Reel URLs will result in zero scraped items. You must use post or video URL formats instead.
  • Storage Rate Limits: Apify enforces strict storage rate limits: 60 requests per second for individual storage objects and 400 requests per second for dataset item pushes or request queue CRUD operations. High-concurrency agent workflows that exceed these rates will encounter throttling.
  • Request Queue Processing Restrictions: On the Apify platform, a request queue can only be processed by one Actor or task run at a time. Multiple concurrent runs cannot process a single shared queue in parallel; fan-out patterns must allocate separate queues.
  • Unnamed Storage Expiration: Unnamed storages on free Apify accounts retain only the 10 most recent runs for a maximum of 4 months before deletion. Integrations relying on persistent historical datasets must use named storages, which are permanently exempt from deletion rules.
  • Proxy Session Lifespans: While datacenter proxies persist for 26 hours, residential proxy sessions (recommended for Facebook scraping) last approximately 30 minutes. Extremely long runs that paginate through large threads may lose their session IP, causing minor connection pauses while establishing new proxy sessions.

What is the exact cost model for executing runs?

The facebook-comments-scraper Actor uses the PAY_PER_EVENT pricing model. You are charged solely for specific named events emitted during execution. There are no separate compute time fees or usage-based platform charges applied to the run.

The charged events and their exact costs are:

  • apify-actor-start: Charged once per run at $0.05 per GB of memory allocated to the run.
  • apify-default-dataset-item: Charged at $0.005 per event for every item written to the default dataset (representing a single scraped comment record).

This per-item event cost scales down based on Apify volume tiers:

  • FREE: $0.005 per event
  • BRONZE: $0.00367 per event
  • SILVER: $0.00233 per event
  • GOLD: $0.001 per event
  • PLATINUM: $0.001 per event
  • DIAMOND: $0.001 per event

The primary controls over total cost are the allocated container memory and the number of returned comments. Setting maxItems limits the total dataset item events generated per URL.

How do you process nested comments without double-charging?

Set includeNestedComments to false in your input configuration when you only need top-level comment text. Every nested reply emitted as a record incurs a distinct apify-default-dataset-item charge, so omitting reply rows directly reduces event volume.

When includeNestedComments is enabled (the default setting), the scraper collects reply rows under top-level comments and outputs them as individual records. Top-level records have a threadingDepth of 0, while replies have a threadingDepth of 1 or higher and include a replyToCommentId referencing their parent comment.

Because every item emitted to the default dataset generates a billed event, extracting deep reply threads for posts with heavy discussion increases event counts rapidly. If your agent only evaluates overall post sentiment or top-level engagement, setting includeNestedComments to false prevents the scraper from fetching replies, saving both runtime and event costs.

If you do collect nested comments, here is a Python helper to assemble the flat dataset items into a hierarchical tree in memory:

def structure_comment_tree(dataset_items):
    comments_by_id = {}
    top_level_comments = []

    for item in dataset_items:
        comment_id = item.get("commentId")
        if not comment_id:
            continue

        comments_by_id[comment_id] = {
            "commentId": comment_id,
            "text": item.get("text"),
            "author": item.get("profileName"),
            "likes": item.get("likesCount", 0),
            "date": item.get("date"),
            "threadingDepth": item.get("threadingDepth", 0),
            "replies": []
        }

    for item in dataset_items:
        comment_id = item.get("commentId")
        parent_id = item.get("replyToCommentId")

        if not comment_id or comment_id not in comments_by_id:
            continue

        node = comments_by_id[comment_id]

        if parent_id and parent_id in comments_by_id:
            comments_by_id[parent_id]["replies"].append(node)
        elif item.get("threadingDepth", 0) == 0:
            top_level_comments.append(node)

    return top_level_comments
Enter fullscreen mode Exit fullscreen mode

How do you handle non-existent pages or authentication blocks?

You handle failure states by checking for an empty dataset output and searching the run log for diagnostic warning phrases. Inspecting logs for keywords like "block" or "invalid url" allows your application to differentiate blocked requests from quiet posts.

When scraping public Facebook URLs, invalid links, deleted posts, or restricted group content will result in empty datasets because no public comments can be rendered without login.

An AI agent must be able to tell whether zero output records signify a newly created post with no comments or a blocked extraction attempt. Parsing the container log output provides immediate context regarding network or access issues.

Here is a Node.js script using the official client that checks the dataset output, inspects run logs upon encountering an empty result, and raises distinct errors based on the output:

import { ApifyClient } from 'apify-client';

async function scrapeAndVerifyComments(apiToken, targetUrl) {
    const client = new ApifyClient({ token: apiToken });

    const run = await client.actor('facebook-comments-scraper').call({
        startUrls: [{ url: targetUrl }],
        maxItems: 50,
        commentsMode: "ALL",
        includeNestedComments: false
    });

    const { items } = await client.dataset(run.defaultDatasetId).listItems();

    if (items.length === 0) {
        const logText = await client.log(run.id).get();
        const lowerLog = (logText || '').toLowerCase();

        if (lowerLog.includes('block') || lowerLog.includes('captcha')) {
            throw new Error("Scraping was blocked by anti-bot protections. Ensure residential proxies are enabled.");
        } else if (lowerLog.includes('not found') || lowerLog.includes('invalid url')) {
            throw new Error(`Target URL is private, restricted, or non-existent: ${targetUrl}`);
        } else {
            return {
                status: "EMPTY",
                message: "Run succeeded, but no public comments were found on this post.",
                data: []
            };
        }
    }

    return {
        status: "SUCCESS",
        count: items.length,
        data: items
    };
}
Enter fullscreen mode Exit fullscreen mode

The Actor's README is the source of truth for its inputs, outputs and limits. Need a hand wiring this into your stack? Email info@crawlerbros.com

Top comments (0)