What the Apify MCP server does to your Actor before an AI agent ever sees it 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. Olivier Reynaud https://apify.com/silentflow as part of Write for Apify https://apify.com/resources/write-for-apify - a program for developers sharing original articles about what they've built with Apify. I 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. So 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. This 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. How an agent actually reaches your Actor Before 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: search-actors fetch-actor-details call-actor get-actor-run get-dataset-items get-key-value-store-record abort-actor-run search-apify-docs fetch-apify-docs report-problem apify--rag-web-browser The 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. If you are following a tutorial that mentions an add-actor tool, check the date. It is not in that list anymore. You can scope a session to specific Actors with a query parameter, which is how I ran most of this audit: curl -sS -D - -X POST "https://mcp.apify.com/?actors=silentflow/domain-enricher" \ -H "Authorization: Bearer $APIFY TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{ "protocolVersion":"2025-06-18","capabilities":{}, "clientInfo":{"name":"audit","version":"1.0"}}}' That 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. The 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. 46 of my public Actors do not exist for an agent The first result was the one I did not see coming. I pulled every Actor I own from GET /v2/acts?my=1 , then pulled the public Store index and compared. The counts: | | Actors | |---|---| | Owned | 151 | | Public | 121 | | Present in the Store index | 75 | | Public but absent from the index | 46 | Then I grouped the 46 missing ones by pricing model, expecting a mess. Instead, I got a single bucket: hidden by pricing model: {'FLAT PRICE PER MONTH': 46} Every 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. This 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. So 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. For 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. 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. Your input schema is not a form; it is a tool definition Here is the part that changed how I write INPUT SCHEMA.json . I 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. 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. REQUIRED is prepended to descriptions. Any field named in your schema's required array gets its description prefixed with a literal REQUIRED marker. 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. 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. 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. The default that quietly makes a required field optional Back to the LinkedIn Actor. Its INPUT SCHEMA.json says this: { "required": "cookies", "userAgent" } Its generated tool definition says this: silentflow--linkedin-sales-navigator-scraper-ppr: props=39 required= 'userAgent' 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. I 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: { "required": "requiredNoDefault", "requiredWithDefault", "requiredArrayWithDefault", "requiredWithPrefill" , "properties": { "requiredNoDefault": { "type": "string", "editor": "textfield" }, "requiredWithDefault": { "type": "string", "editor": "textfield", "default": "hello" }, "requiredArrayWithDefault": { "type": "array", "editor": "json", "default": {} }, "requiredWithPrefill": { "type": "string", "editor": "textfield", "prefill": "prefilled" } } } One apify push , then tools/list : required exposed to agent: 'requiredNoDefault', 'requiredWithPrefill' requiredNoDefault default=None prefill=None REQUIRED marker=True requiredWithPrefill default=None prefill='prefilled' REQUIRED marker=True requiredWithDefault default='hello' prefill=None REQUIRED marker=True requiredArrayWithDefault default= {} prefill=None REQUIRED marker=True 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. The 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. Then I grepped the whole monorepo for the pattern: python python3 - <<'EOF' import glob, json paths = glob.glob " /.actor/INPUT SCHEMA.json" + glob.glob " / /.actor/INPUT SCHEMA.json" for p in sorted paths : schema = json.load open p props = schema.get "properties", {} bad = k for k in schema.get "required", if "default" in props.get k, {} if bad: print p, bad EOF The version I first wrote for this article used grep | xargs with a