{"slug": "what-the-apify-mcp-server-does-to-your-actor-before-an-ai-agent-ever-sees-it", "title": "What the Apify MCP server does to your Actor before an AI agent ever sees it", "summary": "An audit of 121 public Apify Actors found that 46 are not reachable by AI agents through the Apify MCP server, and that a single JSON key in one input schema marked a mandatory field as optional, according to developer Olivier Reynaud, who published the findings from an August 8, 2026 snapshot along with reproducible audit scripts in a GitHub repo. Reynaud tested against apify-mcp-server version 0.14.2, which exposes eleven tools including search-actors, fetch-actor-details and call-actor, and noted that Actors are renamed in tool definitions because slashes are illegal in MCP tool names.", "body_md": "[Olivier Reynaud](https://apify.com/silentflow)as part of\n\n[Write for Apify](https://apify.com/resources/write-for-apify)- a program for developers sharing original articles about what they've built with Apify.\n\nI have been building Actors on [Apify Store](https://apify.com/store) for about ten months. When I wrote about [building 98 Actors on Apify Store](https://blog.apify.com/building-98-actors-on-apify-store/) in April, I was thinking about humans: how they find an Actor, how they read a README, and why they churn. Then the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp) turned every public Actor into a tool that an AI client can call on its own, and my customer changed. MCP is the [Model Context Protocol](https://modelcontextprotocol.io/), the standard that lets an AI client discover and call external tools. Some of my traffic now comes from Claude, or Cursor, or an agent someone wrote at 2 am, reading a machine-generated tool definition and deciding whether my Actor is worth calling.\n\nSo I spent a weekend auditing [my own Store account](https://apify.com/silentflow) the way an agent sees it. I own 151 Actors, 121 of them public. The throwaway I built for this article is one of the 151 and, being private, not one of the 121. Every number below is a snapshot taken on August 8, 2026: user and run counts are rolling 30-day windows, so yours will have moved by the time you read this, and so will mine. I expected to find some rough edges. I found that 46 of those public Actors are not reachable by an agent at all, and that a single JSON key in my input schema was telling agents that a mandatory field was optional.\n\nThis is what I measured, how I measured it, and what I changed. Everything here is reproducible against your own account, and I have put the audit scripts in a [GitHub repo](https://github.com/i9offline/apify-agent-audit) so you can run them on yours.\n\n## How an agent actually reaches your Actor\n\nBefore the audit, it helps to be precise about the path. When an AI client connects to `https://mcp.apify.com` with no configuration, it gets exactly eleven tools:\n\n```\nsearch-actors            fetch-actor-details      call-actor\nget-actor-run            get-dataset-items        get-key-value-store-record\nabort-actor-run          search-apify-docs        fetch-apify-docs\nreport-problem           apify--rag-web-browser\n```\n\nThe three that decide an Actor author's fate are the first three: `search-actors` queries Apify Store, `fetch-actor-details` returns an Actor's input schema, pricing, stats, and output schema, and `call-actor` runs it. All of them are ordinary [MCP tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools), which is why any MCP client can drive them.\n\nIf you are following a tutorial that mentions an `add-actor` tool, check the date. It is not in that list anymore.\n\nYou can scope a session to specific Actors with a query parameter, which is how I ran most of this audit:\n\n```\ncurl -sS -D - -X POST \"https://mcp.apify.com/?actors=silentflow/domain-enricher\" \\\n  -H \"Authorization: Bearer $APIFY_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\n        \"protocolVersion\":\"2025-06-18\",\"capabilities\":{},\n        \"clientInfo\":{\"name\":\"audit\",\"version\":\"1.0\"}}}'\n```\n\nThat handshake returns a session ID in the `Mcp-Session-Id` response header. You send `notifications/initialized`, then `tools/list`, and you get back the exact tool definitions a model would receive. Two details cost me twenty minutes each. The transport is Streamable HTTP, so responses can arrive as `event: message` / `data: {...}` frames rather than a plain JSON body. And your Actor arrives renamed: `silentflow/domain-enricher` becomes the tool `silentflow--domain-enricher`, because slashes are not legal in MCP tool names.\n\nThe version I tested against reports itself as `apify-mcp-server` `0.14.2`, on August 8, 2026. This layer moves fast, so check your own version before trusting any of my numbers.\n\n## 46 of my public Actors do not exist for an agent\n\nThe first result was the one I did not see coming.\n\nI pulled every Actor I own from `GET /v2/acts?my=1`, then pulled the public Store index and compared. The counts:\n\n|  | **Actors** | \n|---|---|\n| Owned | 151 | \n| Public | 121 | \n| Present in the Store index | 75 | \n| Public but absent from the index | **46** | \n\nThen I grouped the 46 missing ones by pricing model, expecting a mess. Instead, I got a single bucket:\n\n```\nhidden by pricing model: {'FLAT_PRICE_PER_MONTH': 46}\n```\n\nEvery one of them is on rental pricing. None came back from the Store index that `search-actors` queries, so an agent browsing Apify Store never sees them at any position, at any rank, under any query.\n\nThis is documented, and I found the documentation after I found the number, which is the wrong order. The [MCP docs](https://docs.apify.com/platform/integrations/mcp) have an \"Excluded Actors\" section: rental Actors are \"excluded because their subscription-based model doesn't fit the sporadic, on-demand way the MCP server runs Actors\". Full-permission Actors are excluded too, for security.\n\nSo the rule is not the discovery. The discovery is the size of the bill, which the docs do not put a number on: on my account it is 46 of 121 public Actors, and I had no idea until I counted. A pricing decision I made for billing reasons had quietly become a distribution decision. It is also a decision with an expiry date: Apify is [retiring rental pricing on October 1, 2026](https://docs.apify.com/actors/publishing/monetize/rental), and any rental Actor not migrated by then moves to pay per usage automatically.\n\nFor me, the practical consequence is blunt. I had been treating the migration off rental pricing as a billing chore. On my account it is also a discoverability setting. My [`reddit-scraper`](https://apify.com/silentflow/reddit-scraper) did 7,721 runs in the last 30 days on rental pricing, and no agent can find it; its pay-per-event twin, [`reddit-scraper-ppr`](https://apify.com/silentflow/reddit-scraper-ppr), is in the index and did 30.\n\n*Editor's note: Once rental pricing retires on October 1, 2026, this exclusion stops applying. Rental Actors that have not been migrated move to pay per usage, which agents can find in search. Migrating yours before the deadline lets you choose the new pricing yourself and puts those Actors in front of agents sooner.*\n\n## Your input schema is not a form; it is a tool definition\n\nHere is the part that changed how I write `INPUT_SCHEMA.json`.\n\nI had always thought of the input schema as the Console form. Titles, descriptions, and help text for a human filling in fields. The MCP server takes that same file and rewrites it into a tool definition for a language model, with five separate transformations along the way. Here is what changed between what I wrote and what `tools/list` returns.\n\n**The Store description becomes the tool description.** The generated tool description is a fixed preamble plus your Actor's Store description verbatim. That marketing paragraph you wrote for humans browsing Apify Store is what a model reads to decide whether your tool is relevant. There is no length limit on it: [the server source](https://github.com/apify/apify-mcp-server/blob/v0.14.2/src/tools/actors/actor_tools_factory.ts#L124-L126) concatenates the preamble and the Store text with no length check, and the three Actors I measured came back at 397 to 500 characters with the Store text whole. The limit is on the fields instead: [shortenProperties](https://github.com/apify/apify-mcp-server/blob/v0.14.2/src/tools/actor_input_schema.ts#L311-L318) cuts every input field description at 500 characters and appends three dots, and it runs before the example values and enum hints below are added, so those survive the cut.\n\n**`** REQUIRED**` is prepended to descriptions.** Any field named in your schema's `required` array gets its description prefixed with a literal `**REQUIRED**` marker.\n\n**`prefill` becomes example values.** A field with `\"prefill\": [\"doctolib.fr\"]` gets `Example values: [\"doctolib.fr\"]` appended to its description. This is free, and it is the cheapest way to make a field guessable.\n\n**Enums become a prose list, and the list is capped.** My [Google News Actor](https://apify.com/silentflow/google-news-scraper) has a `regionLanguage` enum with 74 locales. The generated description reads `Possible values: US:en,GB:en,AU:en,...` and stops after 20, a literal slice(0, 20) in the source. The full 74 values are still in the JSON Schema `enum` array, which the server keeps up to 2,000 characters of values, so a careful model can find them, but the text hint it reads first shows barely a quarter of them.\n\n**A `waitSecs` field appears that you never wrote.** The server injects an integer field, 0 to 45, default 30, that caps how long the call waits for your run to reach a terminal state. Worth knowing if your Actor typically takes four minutes.\n\n## The default that quietly makes a required field optional\n\nBack to the LinkedIn Actor. Its `INPUT_SCHEMA.json` says this:\n\n```\n{ \"required\": [\"cookies\", \"userAgent\"] }\n```\n\nIts generated tool definition says this:\n\n```\nsilentflow--linkedin-sales-navigator-scraper-ppr: props=39 required=['userAgent']\n```\n\n`cookies` had vanished from `required`, while its description still carried the `**REQUIRED**` marker. So a model reading that tool gets two contradictory signals: prose that says the field is mandatory, and a JSON Schema that says it is optional.\n\nI did not want to guess at the cause, so I built a throwaway Actor whose only job was to answer the question. Four fields, all four listed in `required`, differing only in what else they carry. The `properties` and `required` of its `INPUT_SCHEMA.json`, abridged from the full file in the repo:\n\n```\n{\n  \"required\": [\"requiredNoDefault\", \"requiredWithDefault\",\n               \"requiredArrayWithDefault\", \"requiredWithPrefill\"],\n  \"properties\": {\n    \"requiredNoDefault\":        { \"type\": \"string\", \"editor\": \"textfield\" },\n    \"requiredWithDefault\":      { \"type\": \"string\", \"editor\": \"textfield\",\n                                  \"default\": \"hello\" },\n    \"requiredArrayWithDefault\": { \"type\": \"array\",  \"editor\": \"json\",\n                                  \"default\": [{}] },\n    \"requiredWithPrefill\":      { \"type\": \"string\", \"editor\": \"textfield\",\n                                  \"prefill\": \"prefilled\" }\n  }\n}\n```\n\nOne `apify push`, then `tools/list`:\n\n```\nrequired exposed to agent: ['requiredNoDefault', 'requiredWithPrefill']\n\n  requiredNoDefault         default=None       prefill=None         REQUIRED_marker=True\n  requiredWithPrefill       default=None       prefill='prefilled'  REQUIRED_marker=True\n  requiredWithDefault       default='hello'    prefill=None         REQUIRED_marker=True\n  requiredArrayWithDefault  default=[{}]       prefill=None         REQUIRED_marker=True\n```\n\n**A `default` on a required field removes it from the `required` array the agent reads. A `prefill` does not.** All four keep the `**REQUIRED**` marker in their description.\n\nThe server's reasoning is easy to guess: if a field always resolves, a model should not have to supply it. That's the server's choice, not JSON Schema's, where default says nothing about required. The effect on my Actor was not defensible either way. `cookies` carried `\"default\": [{}]`, an array holding one empty object, left over from making the Console form render a friendly empty row. An agent reads the schema, concludes cookies are optional, and calls the Actor with an empty array. That run would die at LinkedIn authentication, and from the agent's side it looks like my Actor is broken.\n\nThen I grepped the whole monorepo for the pattern:\n\n``` python\npython3 - <<'EOF'\nimport glob, json\n\npaths = glob.glob(\"*/.actor/INPUT_SCHEMA.json\") + glob.glob(\"*/*/.actor/INPUT_SCHEMA.json\")\nfor p in sorted(paths):\n    schema = json.load(open(p))\n    props = schema.get(\"properties\", {})\n    bad = [k for k in schema.get(\"required\", []) if \"default\" in props.get(k, {})]\n    if bad:\n        print(p, bad)\nEOF\n```\n\nThe version I first wrote for this article used `grep | xargs` with a `</dev/null` on the end. It worked in my terminal and produced nothing under `bash`, because that redirect replaces `xargs`' stdin and discards the pipe. My shell is zsh, which duplicates the input instead and hides the bug. I only caught it because I tested the snippet in both shells before publishing it, which is not a habit I had until this article.\n\nSix Actors came back. They split into two different bugs, which is why I am glad I looked before doing a bulk replace.\n\nOn the LinkedIn Actors, `cookies` is genuinely mandatory, and the `default: [{}]` was pure Console decoration. Deleting it is the whole fix, and I pushed it: build 1.0.35 went out on August 9. Re-running `tools/list` against the new build closes the loop:\n\n```\nrequired exposed now: ['cookies', 'userAgent']\ncookies has default?  False\n```\n\nOne deleted line, and a field that had been quietly optional to every agent since I shipped the Actor is mandatory again.\n\nOn the other four, the field was never really mandatory. A `maxItems` with a default of 100 does not need the caller to supply anything, and `country` with a sensible default is the same. There, the honest fix is the opposite one: take the field out of `required`, because the schema was lying to humans too. Swapping those to `prefill` would have broken every existing API caller that relies on the default being applied.\n\n**A required field with a default is always a bug. Which of the two things is wrong depends on the field, so read them one at a time.**\n\n## I gave twelve agents a task and watched which Actor they picked\n\nSchema archaeology only tells you what a model *could* read. I wanted to know what actually gets chosen, so I ran twelve independent agent sessions. Each one got a single realistic task, access to the Apify MCP server, and no knowledge that I was measuring my own account. Each searched Apify Store freely, picked one Actor, and reported why. None of them ran anything.\n\nThe tasks covered ground where I have a public Actor: news monitoring, domain enrichment, European company registries, local business leads, job boards, G2 reviews, YouTube transcripts, US import records, French classifieds, refurbished phones, LinkedIn leads, and Pinterest.\n\n| **Outcome** | **Tasks** | \n|---|---|\n| One of my Actors was chosen | 2 | \n| Seen in results, not chosen | 4 | \n| Never appeared at all | 6 | \n\nTwo wins out of twelve, and I only got into the consideration set half the time.\n\nThe individual reasons were more useful than the score. Three patterns showed up repeatedly.\n\n**Required credentials are an elimination, not a friction.** On the LinkedIn task, the agent's note was explicit: my Actor was ruled out because `cookies` and `userAgent` were required. An autonomous agent cannot produce a LinkedIn session cookie. Every Actor that demands one is invisible to that entire class of caller, no matter how good it is. That is a product decision: this Actor is built for humans who bring their own session, and agents are not its market. I should have known I was making that choice.\n\n**Array inputs beat scalar inputs, said two agents, so I checked.** On the Pinterest task, the winning Actor exposed `queries` as an array while mine exposes `search` as a single string, and the agent's stated reason was that a single string forces one run per keyword. On the YouTube task, an agent passed over an Actor with 2,948 monthly users in favor of one with 1,043 and called batch input \"the decisive filter\". That is two sessions, and both reasons are the model's own account of its choice, which is not the same thing as the mechanism. So I looked for other differences. The Pinterest winner also had more monthly users than mine, and the YouTube Actor that was passed over also carries the lowest rating in its category, 3.7 stars over 49 reviews, so on those two tasks, input shape was never the only gap. Then I re-ran both tasks on September 7, 2026, with two fresh sessions that did not know which Actor was mine. Every single string Actor was dropped again, with the input named as the reason each time, including a YouTube Actor with a 5-star rating and more users than the eventual winner. Between the array input Actors, the output schema and traction decided. Read it as a filter, not a ranking: agents are orchestrating, and they say they optimize for fewer calls.\n\n**The output schema gets checked against the description.** This was the finding I did not expect, and the one that stung.\n\n## The schema inferred from your runs is what agents trust\n\n`fetch-actor-details` returns an output schema that Apify infers from recent successful runs. You never write it yourself; it is a record of what your Actor actually produced.\n\nOn the French real estate task, an agent looked at my [`leboncoin-scraper-ppr`](https://apify.com/silentflow/leboncoin-scraper-ppr), which has 392 total users, 45 monthly, and a 5-star rating from 2 reviews. It passed anyway and picked a smaller competitor. I went and looked at what it saw.\n\nMy Store description says the Actor extracts \"seller phone numbers\". Here is the contact-related part of the inferred output schema, abridged:\n\n```\n{\n  \"hasPhone\": \"boolean\",\n  \"ownerName\": \"string\",\n  \"ownerType\": \"string\",\n  \"ownerId\": \"string\"\n}\n```\n\nThere is a boolean saying whether a phone number exists. There is no field containing one.\n\nThe Actor the agent picked instead, [`mostafa-ennadi/leboncoin-immobilier-phone-leads-scraper`](https://apify.com/mostafa-ennadi/leboncoin-immobilier-phone-leads-scraper), has 4 monthly users to my 45. Its inferred schema carries both `has_phone: \"boolean\"` and `phonenumber: \"string\"`. It proves what my description only promises.\n\nThe description was written by me, for humans, and it oversells. The output schema was generated from real runs, and it does not. When those two disagree, the agent believes the runs. My Actor lost that task on a gap between my copy and my data, and it was right to.\n\nCompare that with [`domain-enricher`](https://apify.com/silentflow/domain-enricher), which `fetch-actor-details` reported to the agent as having a single monthly user, and which won its task outright. Its inferred schema carries nested `dns`, `registration`, `techStack`, and `website` objects a caller can inspect, rather than a flat row of labels. One Actor had traction and unverifiable copy; the other had 1 user and a schema you could check. The schema won.\n\n## Ranking first on my own name, invisible in the words people type\n\nThe last measurement was discoverability. I ran `search-actors` for the term a user would actually type for each of my Actors:\n\n| **Query** | **Rank of my Actor** | **Who is above me** | \n|---|---|---|\n| `northdata` | **1** | nobody | \n| `Back Market` | **1** | nobody | \n| `Pinterest` | 3 | fatihtahta, easyapi | \n| `ImportYeti` | 3 | zen-studio, jungle_synthesizer | \n| `leboncoin` | 4 | clearpath, fatihtahta | \n| `Google News` | **absent from top 10** | easyapi, data_xplorer | \n| `G2` | **absent** | zen-studio | \n| `YouTube transcript` | **absent** | pintostudio, starvibe | \n| `Google Maps` | **absent** | compass | \n\nOn narrow terms where few people compete, I am first. On contested terms, my public Actors are not in the first ten results. My YouTube transcript Actor has 1 monthly user, and on the query a person would actually type, two competitors come back, and it does not. I had spent months blaming that gap on my packaging: the name, the README, the missing reviews. Packaging cannot be the whole story for a result that never gets shown.\n\n## What I am changing\n\nThe audit produced a short list. Only the first item has actually shipped on the two LinkedIn Actors; the rest is still ahead of me, so read this as my backlog rather than a finished job. I would work it in this order.\n\n1. **Audit every required field that carries a `default`.** I had six. Where the field is genuinely mandatory, delete the default, or use`prefill` if you want a suggested value in Console: it survives into the tool definition as an example and does not corrupt`required` . Where it is not genuinely mandatory, take it out of`required` instead. This was the only outright bug the audit found.\n2. **Never require a credential if you want autonomous callers.** If your Actor genuinely needs a session cookie, accept that agents are not your market and optimize for humans. Do not pretend otherwise.\n3. **Make your primary input an array.** One call for twenty keywords instead of twenty calls. Agents notice, and so does your start event billing.\n4. **Read your own inferred output schema, then re-read your Store description.** If the description promises a field the schema does not contain, fix the Actor or fix the sentence. Agents compare them.\n5. **Treat the Store description as tool documentation.** It becomes the tool description verbatim. Lead with what the Actor returns, and spend the adjectives elsewhere.\n6. **Check what a 20-item cap does to your enums.** If your enum has 74 values, the model reads about 20 of them in prose. Put the important ones first.\n7. **If you are still on rental pricing, know what it costs you.** The exclusion is documented, but the docs do not tell you how many Actors it covers on your account. On mine it was 46 of 121.\n\n## What I would do differently\n\nI would have run `tools/list` against my own Actors on the day the MCP server shipped. I assumed that a schema which renders correctly in Console renders correctly for a model. They are two different consumers of the same file, and only one of them can follow up.\n\nI would also have been more suspicious of my own tooling. Twice during this audit, I nearly published a wrong number because I parsed a markdown response as JSON and got a confident, clean, entirely fictional zero. The first time it told me none of my Actors surfaced in search. The second time it told me none of them had an output schema. Both were false. When an audit result is dramatic, check the parser before you check the platform.\n\n## FAQ\n\n**Does this apply to Actors that are not public?** Private Actors are not in the Store index, so `search-actors` will not find them. You can still scope an MCP session to one with `?actors=username/name` if you own it, which is how I tested my throwaway Actor.\n\n**Will removing `default` break my Console form?** It changes what the form starts with. Switching to `prefill` keeps a suggested value visible in Console while leaving `required` intact in the generated tool. That was the behavior I wanted on the two LinkedIn Actors. On the other four, it was not: there the field should never have been in `required` at all.\n\n**How do I see the tool definition for my own Actor?** Open an MCP session against `https://mcp.apify.com/?actors=<username>/<name>` with your API token, complete the `initialize` and `notifications/initialized` handshake, then call `tools/list`. The `mcp_audit.py` script in the repo does this with nothing but the Python standard library.\n\n**Is there a cap on the description a model sees?** Not on the Store description. The source builds the tool description as preamble plus Store text with no length check, and my measurements agree: on my Sales Navigator Actor, a 291-character Store description plus a 209-character preamble came back as exactly 500 characters with nothing missing, which is arithmetic rather than a ceiling. The cap is on input field descriptions, which shortenProperties cuts at 500 characters, and on the enum hint, which lists 20 values. Both are unchanged in the current source as of September 7, 2026.\n\n**Does any of this change how I get paid?** Not directly, but pay-per-event maps onto agent callers much better than a monthly rental. My Google News Actor bills $0.0027 per article on the Bronze tier, and an agent can reason about that number when it decides how many articles to ask for. A monthly subscription it did not buy is not something it can reason about at all.", "url": "https://wpnews.pro/news/what-the-apify-mcp-server-does-to-your-actor-before-an-ai-agent-ever-sees-it", "canonical_source": "https://blog.apify.com/fixing-actors-invisible-to-agents/", "published_at": "2026-09-24 13:25:20+00:00", "updated_at": "2026-09-24 13:33:39.640092+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-tools", "developer-tools", "ai-products"], "entities": ["Apify", "Apify MCP server", "Olivier Reynaud", "Model Context Protocol", "Claude", "Cursor", "apify-mcp-server 0.14.2", "GitHub"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/what-the-apify-mcp-server-does-to-your-actor-before-an-ai-agent-ever-sees-it", "markdown": "https://wpnews.pro/news/what-the-apify-mcp-server-does-to-your-actor-before-an-ai-agent-ever-sees-it.md", "text": "https://wpnews.pro/news/what-the-apify-mcp-server-does-to-your-actor-before-an-ai-agent-ever-sees-it.txt", "jsonld": "https://wpnews.pro/news/what-the-apify-mcp-server-does-to-your-actor-before-an-ai-agent-ever-sees-it.jsonld"}}