DEV Community

Jeffrey Turov for Apify

Posted on

Your Actor should never see my credentials: the security case for MCP connectors

Every automation platform has the same dirty secret: the easiest way to connect a third-party service is to paste a token into an input field. It works on the first try. It is also how credentials end up in places nobody intended.

I learned this building lead-generation Actors on Apify. My first working version took a GitHub token as an Actor input. It shipped fast, and it was wrong. This piece is about the threat model behind that mistake, and how MCP connectors invert it: the Actor I run never sees a credential at all.

Everything below comes from real builds and real run logs, including the failures.

The threat model nobody writes down

Passing a service token as an Actor input creates three leak paths, and all three are boring:

  1. Run logs. Inputs are echoed, logged, and retained. Anyone with whom you share a run (a teammate, a support ticket, a screenshot in a bug report) potentially shares the token with it.
  2. Shared input JSON. Actors get cloned, forked, and re-run from saved inputs. A token inside an input object travels with every copy.
  3. Third-party Actor code. The moment you run code you did not write (a Store Actor, a fork, a colleague's experiment), any credential you hand it is only as safe as that code's worst console.log.

Notice what these have in common: none of them require an attacker. The leak vector is the architecture itself. The token is simply in more places than it needs to be.

The inversion: credentials live with the connector, not the code

Apify's MCP connectors restructure where the secret sits:

  • You authorize the third-party service (GitHub, Notion, Slack) once, in Apify Console under Settings, Integrations. The OAuth credential is stored with the connector, on the platform side.
  • At run time, the Actor receives a connector ID: an opaque string like ebw4ThD4cQbEKzC2l. It is not a token, and it is useless outside the platform.
  • The Actor talks to the Apify MCP proxy at ${ACTOR_MCP_CONNECTOR_BASE_URL}/<connectorId>, authenticating with the run's own APIFY_TOKEN.
  • The proxy enforces the tool permissions the Actor declared in its input schema.

The practical consequence: you can publish the Actor's source, share its runs, and paste its input JSON into a forum. There is nothing in any of those artifacts worth stealing.

Least privilege is declared, not hoped for

The second half of the model is the mcpServers rule in the input schema. This is where the Actor states which tools it needs:

"githubConnector": {
  "title": "GitHub connector",
  "type": "string",
  "resourceType": "mcpConnector",
  "mcpServers": [
    {
      "url": "*",
      "tools": {
        "required": ["create_*", "push_*", "update_*", "write_*", "commit_*", "get_*"],
        "readOnly": false
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Two layers constrain what the Actor can do: what the connector exposed at authorization time, and what the schema declares. The intersection is enforced by the proxy at run time, not by documentation.

Verified in a real run log. My Actor's schema used the declaration above. When it called list_tools() through the proxy, it saw exactly 7 tools: create_branch, create_or_update_file, create_pull_request, create_repository, push_files, update_pull_request, update_pull_request_branch. Nothing else existed as far as the Actor could tell. The constraint layer is not a promise on a marketing page; it is observable behavior.

The honest limits

A security argument that only lists strengths is marketing. Three limits I hit or verified:

1. The connector's tool set is frozen at authorization time. When you authorize a connector, the platform discovers the available tools once. If the upstream server later adds tools you want, you re-authorize. The set does not refresh itself. I lost time to this before reading it properly in the docs: it is "Layer 1" of their two-layer model, and it is by design (a silent expansion of your Actor's powers would be a security hole, not a feature).

2. The run still authenticates with a token: the starter's APIFY_TOKEN. The proxy trusts whoever started the run. That means access control moves to who can start your Actor with your connector, which is an Apify permissions question, not a code question. For Actors you share or publish, treat connector-enabled inputs as capability grants and review who can run what.

3. Connectors do not sanitize your payloads. The proxy enforces which tools the Actor calls, not what data flows through them. An Actor scraping personal data and pushing it to your repo is still your compliance problem. The connector solves credential custody, not data governance.

Pitfalls from real runs

Three failures I actually hit while building on this model, because they cost me runs:

SDK drift. The MCP Python SDK docs show streamable_http_client unpacking into three values (read, write, _). The version my Docker image installed yields two. ValueError: not enough values to unpack. Indexing the tuple (streams[0], streams[1]) works across versions. Same class of issue: the tool result error flag is result.is_error (snake_case), not the isError casing the TypeScript-flavored docs suggest.

Default branch assumptions. create_or_update_file failed with Branch main not found: my repo's default is master. The tool does not fall back to the repository default; it fails loudly, which is the correct behavior for a security boundary. Detect and retry with the other common name, or pass the branch explicitly.

Write-only connectors and the SHA problem. Updating an existing file on GitHub requires its current blob SHA. My write-scoped connector exposed no get_file_contents to fetch it. Rather than widening permissions, I switched the pipeline to immutable, timestamped snapshots: each run writes a new dated file, and Git itself becomes the history. The least-privilege constraint pushed me to a better design, which is what least privilege is supposed to do.

When to use what

Situation Token as input MCP connector
Local throwaway script, one run, you watch it Fine Overkill
Actor you will run on a schedule for weeks Leak path Right answer
Actor you publish or share Irresponsible Right answer
Running someone else's Actor with your services Never The only sane option

The last row is the one that convinced me. If you consume third-party Actors, connectors are the only way to grant access to your services where a sloppy or malicious print() cannot exfiltrate your credentials.

The takeaway

Security models for automation are usually about adding vigilance: rotate tokens, restrict scopes, audit logs. MCP connectors remove the attack surface instead: the credential never enters the Actor's world, so it cannot leak from it. You trade a small amount of convenience (one authorization step in Console, a frozen tool set) for the ability to share runs, publish source, and run untrusted code without a knot in your stomach.

The working example this article is drawn from is public: maps-to-stack on GitHub, and the build walkthrough with the scraping pitfalls is on dev.to. Connector documentation: docs.apify.com/integrations/mcp-connectors.


Build log details verified against run logs from August 2026: tool filtering (7 tools), branch fallback, SDK unpacking, and the immutable-snapshot pattern all occurred in real runs of the maps-to-stack Actor.

Top comments (0)