Five questions to answer before you put MCP in production
SOIT Team

MCP standardised the boring part of giving an agent tools. One streamable HTTP endpoint, one list_tools call, and the tools appear in the model's callable list. The first time we wired one up internally it took an afternoon, and that part is genuinely solved. Then we tried to put it in production, someone from security asked five questions, and none of them had an answer. This post is those five questions and how they ended up being answered in SOIT, an open-source agent runtime with governance sitting in the middle of the call path. Every claim below points at a file in the repository, because posts like this are unusually easy to write as a slide deck instead of as software.
The first question is who is allowed to call a given tool. MCP has no opinion here: whatever list_tools returns is what the model can call, so visibility is capability. In a multi-tenant, multi-workspace deployment that is not enough, so SOIT installs an MCP server as a plugin artifact rather than as a config entry. Tool references are namespaced as mcp_tool followed by the server and tool names, and every resolution carries a request context holding the tenant, workspace and user. Two things follow. From the agent's point of view a plugin tool, an MCP tool and a built-in adapter look identical, with typed and versioned bindings. And permission checks, secret injection, egress limits, audit, cost attribution, tracing and replay apply to MCP tools automatically, so nobody writes the governance path twice. Each agent version also carries a capability allowlist covering models, knowledge bases, workflows, tools, plugins and MCP servers, which makes the question of what version three of an agent may call something you can diff and roll back rather than a runtime toggle somebody flipped.
The second question is where the credentials live. Most MCP integration examples put a bearer token in plain text in a config file, which is how it ends up in git, in logs, and in the config backup somebody exported to a laptop. SOIT rejects that outright: the auth builder looks for a literal token or value field and raises an error saying credentials must use a secret id. Only a secret reference is accepted, resolved through the secrets port at call time, and API keys are supported only in headers, never in a query string, because query strings leak through logs and referrers. The real value exists in memory for the duration of the call and nowhere else; what gets persisted to the database, to audit records and to traces is a redacted copy built in the same pass that resolved the secret, keeping only the secret id and the signing policy reference. Three auth types are supported, and OAuth follows the 2.1 shape with authorization-server discovery and resource-bound tokens using the client credentials grant. One limitation is worth stating plainly: the browser-based authorization code flow is not implemented, because SOIT calls MCP servers on its own behalf rather than on behalf of a user sitting in front of a browser.
The third question should worry you most: where can the thing connect to. An MCP server makes network requests on your behalf, so put a URL in the tool arguments and it will fetch it, and the classic shape of that attack is asking it for the cloud metadata address that holds temporary credentials. SOIT's egress policy is deny-by-default in three layers. The first is domain policy, matching the target against tenant-scoped and workspace-scoped allowlists and blocklists with the blocklist winning; the allowlist defaults to empty, so nothing is permitted until you say so, and if the policy lookup itself throws, the answer is denial rather than permission. Fail-closed is not a slogan, it is whatever you actually wrote in each exception branch. The second layer is per-address validation after resolution, because DNS rebinding lets an allowlisted hostname resolve to a loopback or private address: the guard resolves the hostname and requires every returned address to be globally routable, refusing the whole request if one is not, while also denying non-http schemes, URLs carrying userinfo, and treating a DNS failure as a denial rather than a retry. The third layer is authorization per hop, because a URL that cleared both checks can still return a redirect pointing at your internal network. Authorization therefore hangs off the outbound HTTP client's request event hook, so every request that actually goes out is checked, redirect hops included, and redirects are not followed by default. The MCP adapter builds its sessions with that client, so initialisation, tool listing and every tool call sit inside these constraints.
The fourth question is whether you can find out afterwards what happened. Tool calls are the only place an agent produces real side effects: a model that says something wrong can be asked again, but a tool that changed a row in the production database changed it. SOIT persists each tool call as a step of a run and writes two pieces of evidence per call. The gateway audit records the tool reference, the redacted parameters and the egress decision on the request side, and success, result type, metadata and error on the response side; the failure path writes one too, and emitting that record is the first thing the exception branch does, which is the part people forget and the part you need when something has gone wrong. Alongside it, step metrics capture latency, the success flag, summarised arguments and results, and the error code and details. The same call writes a cost entry naming the provider and the source port, so the question of what an agent's MCP tools cost this month is a query you can drill into by agent, workflow, tool and source kind. On top of that sits an OpenTelemetry span carrying the tenant, workspace, run and step identifiers, for whatever APM you already run.
The fifth question is whether you can replay a call. The most frustrating property of agent debugging is that it does not reproduce: same input, different reasoning. At the tool layer you can at least be deterministic. Every tool call carries an idempotency key, defaulting to the run and tool-call identifiers, and claims a leased execution record; if the claim lands on a record that already completed, the cached response comes straight back and the external tool is not called again. The retry policy changes accordingly, dropping to a single attempt whenever an idempotency key is present, and the comment in the code says why better than a paragraph can: durable agent calls are at-most-once at this boundary, because not every downstream adapter can honour an idempotency key. Calling once too few beats calling once too many, and for writes that trade is not really a choice. Rate limits and daily quotas come along with it, keyed by the tool reference plus tenant, workspace and user, so one runaway agent does not burn a whole tenant's third-party API budget.
The honest list of what this does not do: transport is streamable HTTP only against the MCP SDK v1 line, and the stateless 2026-07-28 protocol revision is not supported yet; OAuth is client credentials only; a marketplace for one-click MCP installation is on the roadmap, so today you install plugin artifacts by hand; and the default egress allowlist being empty means your first MCP server will be refused until you add its domain explicitly, which is deliberate but does add a step to the quickstart. A question that comes up constantly is how any of this relates to the agent frameworks. They are not the same layer. A framework answers how to orchestrate a call; a runtime answers under whose identity the call ran, with whose credentials, what it could reach, what evidence it left, and whether it can be replayed. The first is a concern while you write the code, the second is a concern after the code ships and someone else asks. You can certainly put permission checks inside a framework, but then every new tool integration reimplements the governance logic, whereas pushing it down into the runtime's port layer is precisely why MCP tools, plugin tools and built-in tools all travel the same path. SOIT is Apache-2.0 and the code is on GitHub at github.com/soit-ai/soit, with the quickstart and a twenty-minute governance demo script in the docs directory. If you are pushing MCP toward production right now, the hardest of the five questions is usually not technical at all: it is who gets to decide what goes on the allowlist.