Skip to main content

MCP gateway and proxy

The agent never talks to a registered MCP server directly. Every call passes through two things A2Flow owns, and they answer two different questions:

MCP gatewayMCP proxy
Where it runsInside the agent's process, next to the databaseIn a separate container, holding none of the agent's secrets
What it settlesWhether a call may happenWhere it happens — it starts or connects to the server
What it holdsIdentity, the policy chain, tool mocks, secret expansion, the audit recordOne call at a time: the server's address or command, the expanded values, the tool and its arguments

The gateway is the decision-maker, and the rest of this page is mostly about it. The proxy is the sandbox those decisions are handed to — its own section is below.

What the gateway decides​

The gateway ownsWhat it does
IdentityEstablishes which tenant and which run a call belongs to, from the conversation it arrived on
AuthorizationConsults an ordered chain of policies, any of which may veto
StubbingAnswers from a tool mock when the run has one for this tool
CredentialsExpands the registered server's secret references into a connection, at connect time only
IsolationHands the allowed call to the MCP proxy that actually runs the server

The MCP proxy: where a registered server runs​

An MCP server you register is not A2Flow's code. A local one is a program A2Flow starts on your behalf; a remote one is a service A2Flow connects out to. Either way, what it does is outside A2Flow's control once it is running.

So it does not run where A2Flow's own secrets are. The proxy is a sandbox: in a Docker Compose deployment it is the separate mcp-proxy container, which holds none of the following:

Not in the proxyWhere it stays
The database, and everything recorded in itWith the agent
The key that secrets are encrypted underWith the agent
The credentials for an external secret storeWith the agent
The LLM provider's API keyWith the agent
The agent skill repositoriesWith the agent

What the proxy gets is one call at a time: the address or command for the one server being reached, the values that server needs — already expanded from your secret references — and the tool and arguments. Nothing else is there to find.

The two ends prove who they are to each other. The channel between them is encrypted, and neither end accepts an unidentified peer: the proxy refuses a connection from anything that does not hold a certificate this deployment issued. Each individual call additionally carries the certificate of the task making it, and the proxy checks that certificate again before it reaches anything — that it was issued here, has not expired, belongs to the caller, and covers this exact tool.

A call can ask a question on the way. An MCP server may stop mid-call to ask the person behind the run for a confirmation. The gateway is what knows who that person is, so the proxy hands the question back over the call's own connection, and the answer travels back up the same connection to the waiting server. Nothing about a call outlives its connection, so the proxy can run as any number of containers.

That second check is not the same question as the first. Being A2Flow does not mean being authorized to call a given tool, and holding a task's certificate does not mean the request is the one A2Flow sent. Both have to hold, so a request cannot keep a valid authorization while quietly pointing the proxy at a different program.

One call carries no task certificate: a test run of a script from the MCP server form, which has no task behind it. The proxy accepts it only for a script — never for an arbitrary command or a remote server — and loading a script's tool list already runs the whole script under the same A2Flow-only check, so this lets nothing new through.

The proxy is a second line, not the decision-maker. Whether the task is still running, whether the approval still stands, whether the certificate was withdrawn — none of that is answerable there, and all of it stays with the gateway.

Every launch in its own sandbox​

The proxy keeps registered servers away from A2Flow. It also keeps them away from each other. Every tenant's servers start in the same proxy, and some of them are scripts a tenant wrote themselves — so each server the proxy starts is sealed in a sandbox of its own, created for that one call and torn down when it ends.

Inside the sandboxWhat that rules out
The server sees only its own processesReading the secret values handed to any other server running at the same time
The system and A2Flow's own programs are read-onlyAltering what a later server — another tenant's, say — runs on its next start
Its home directory, where downloaded packages are kept, belongs to its tenant aloneA server picking up a package another tenant planted
Everything inside ends when the call endsA process lingering after the call to watch the calls that come after it
The proxy's own credentials are not there at allImpersonating the proxy to anyone

A remote server, which the proxy only connects out to, runs nothing in the proxy and so needs no sandbox.

The isolation is built on bubblewrap, the Linux sandboxing tool behind Flatpak. The proxy checks that it can create a sandbox when it starts, and refuses to start if it cannot — it never falls back to running a server unsandboxed. See Deployment for what that asks of the host.

The policy chain​

OrderPolicyWhat it requires
1In-progress tool bindingThe (server, tool) pair must be bound to a task that is in_progress and assigned to the calling session — a task another session of the run is working does not count. Listing what a server advertises is not a call, so this rule does not apply to it: the design agent lists in order to decide what to bind, narrowed to the servers the designer may see under access-control tags, while a run's own listing is not narrowed by tags
2Tool certificateThe call must present the task's certificate — see Who authorized a call — and that certificate's signed grant must cover the tool. There is no exemption: a task no approval covers presents the grant its run's initiator holds
3Approved callWhen an approver's decision is what authorizes the task, the call must match the calls that decision listed — see The calls a decision covers. A tool the decision listed as taking any input passes this rule whatever it carries, having already been stopped by rule 2 until the decision was granted. A task running on its initiator's own grant has no such list and is not checked here

The chain stops at the first veto and is ordered cheapest first, so a call that already fails the binding rule never pays for a signature check. The last rule runs last for a second reason as well: it is the only one that needs to know which task is calling, and the rule before it is what establishes that. It also keeps a caller holding no authority at all from learning what an approver agreed to. Adding a rule means adding a policy to the chain rather than editing the gateway, which is what keeps "what may run" readable as one ordered list.

Refusals are written for the caller rather than for a log: a denied call comes back naming the tools that are allowed, so the model can correct itself instead of guessing.

Who authorized a call​

Every tool call a run makes carries a certificate naming the person whose authority it runs on. A task is granted one when it is marked In Progress; whose authority it carries depends on whether an approval covers it.

The taskGets its certificateOn whose authority
Is covered by an approval — its own, or one requested on a task it followswhen it starts, and only once that approval is grantedthe approver who granted it
Is covered by nonewhen it is marked In Progresswhoever started the run

The second row is the ordinary case: nobody was asked to weigh the task, so the person who ran the workflow authorizes the tools it binds, and the audit trail records that in as many words rather than leaving the call unattributed.

Three rules follow, and all are visible to whoever operates a workflow:

  • The tools are fixed when the task starts. A certificate covers exactly the tools bound to the task at that moment. A workflow that binds a tool to a task only after the task has started finds that tool refused — the design agent is told to bind first, but a hand-edited task can still get this wrong. A task an approval covers cannot have its tools edited at all.
  • Asking for an approval closes the door again. If an approval is requested that covers a task already running on someone's authority, that authority stands down immediately; the task waits for the decision like any other.
  • Each covered task gets its own certificate. One approval covering a chain of tasks does not put them all on one clock — each is granted its own when it starts.

What is recorded​

Recorded per decided callDeliberately not recorded
The tool, the server, and whether it was allowed or deniedThe arguments themselves — only a digest of them is kept
The refusal reason, when denied — for a call outside what was approved, the input at fault and the bound the approver agreed toThe refused input's actual value
The certificate presented, and therefore who authorized the call

That record is the run's Tool Invocations page, and it is what makes "who authorized this call" answerable after the fact. The certificates themselves are listed under Audit Logs → Certificates, one row per grant.

Tool mocks and dry runs​

A tool mock lets a draft workflow be exercised end to end without its tools' side effects. What matters here is where the stub sits.

The stub is consulted after the policy chain, never before it. A dry run therefore rehearses the same authorization a real run faces: the tool must still be bound to a task in progress, and the call must still present that task's certificate. The only thing a mock skips is the part that has an effect outside A2Flow — the proxy is never reached.

Two consequences are worth knowing:

  • Responses follow the call count. The first response answers the run's first call to that tool, the second its second, and the last one repeats once the list runs out. The counter belongs to the run, so it survives the many requests — and the possible replicas — that one agent run spans.
  • A stubbed call leaves no audit record. That record is for calls that reached, or were stopped on their way to, a real server; a row for a call that was always going to be answered from a snapshot would misread in either direction. The chat transcript is where a stubbed call is inspected, badged Mocked.