I made my Apify Actor an AI agent tool, then read every byte it sent back A developer with roughly thirty Actors on the Apify Store documented what happens when an Apify Actor is exposed to an AI agent through the Apify MCP server, finding that the server rewrites the Actor's input schema before handing it to the model. Testing the Jobs API Actor via raw JSON-RPC over streamable HTTP with curl, the developer found that emoji and HTML markup written for the Apify Console input form pass through unchanged into the tool definition, and that the server adds an extra property to the schema's 23 fields. The developer said the setup itself took two minutes and was a footnote compared with the schema behavior observed in the responses. I have about thirty Actors on the Apify Store. Making one of them available to an AI agent through the Apify MCP server took me two minutes: add ?actors=lergassy/jobs-api to the server URL and the Actor shows up as a tool. That part is a footnote. The useful part was what came back. I spent an afternoon calling my own Actor the way an agent calls it — raw JSON-RPC over the wire, no client in between — and logging every response. Four things surprised me, and three of them changed how I write input schemas. The Actor here is Jobs API https://apify.com/lergassy/jobs-api : job listings from Indeed, LinkedIn and company career boards in one schema. Nothing about what follows is specific to jobs, though. If your Actor has more than three inputs, the same things will happen to you. Every walkthrough I found used Claude Desktop or Cursor. I wanted the traffic, not a chat transcript, so I used curl . The Apify MCP server speaks streamable HTTP: you POST JSON-RPC, you get back server-sent events. python import json, subprocess, pathlib HERE = pathlib.Path file .parent TOKEN = HERE / "tok" .read text .strip URL = "https://mcp.apify.com/?actors=lergassy/jobs-api" def post body, sid=None : cmd = "curl", "-s", "--max-time", "300", "-D", str HERE / "h.txt" , "-X", "POST", URL, "-H", f"Authorization: Bearer {TOKEN}", "-H", "Content-Type: application/json", "-H", "Accept: application/json, text/event-stream" if sid: cmd += "-H", f"Mcp-Session-Id: {sid}" cmd += "-d", json.dumps body out = subprocess.run cmd, capture output=True, text=True .stdout return json.loads l 6: for l in out.splitlines if l.startswith "data: " def session : post {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "curl-client", "version": "1.0"}}} sid = next l.split ":", 1 1 .strip for l in HERE / "h.txt" .read text .splitlines if l.lower .startswith "mcp-session-id" post {"jsonrpc": "2.0", "method": "notifications/initialized"}, sid return sid Two things to get right or nothing works. The Accept header has to name both application/json and text/event-stream — the server rejects the request otherwise. And the session id comes back in a response header, not in the body, which is why I dump headers to a file and read them back. With a session open, tools/list shows what the agent gets: 5 tools - get-actor-run - get-dataset-items - get-key-value-store-record - abort-actor-run - lergassy--jobs-api My Actor is one tool. The other four are the plumbing around it: start a run, poll it, read the dataset, kill it. That shape matters later. My input schema.json has 23 properties. The tool definition the agent receives has 24. The server adds one of its own, and it is the single most important field in the whole exchange — I will come back to it. The rest is my schema, rewritten. Here is one property as I wrote it, and as the agent sees it: "keywords": { "title": "🔎 Job titles or keywords", "description": "One search per line: