MCP Server Security: Trust, Permissions and Isolation
Trust a server only after checking its provenance
Bind identity, tool, target and tenant
Original MCP-BOUNDARY-7 map and CRM example
Failure modes and production tests

$ mcp --audit MCP-BOUNDARY-7
> bind: host / actor / tenant
> inspect: server / tool / args
> gate: target / scope / approval
> route: deny / review / verifiedAn MCP server makes tools and data discoverable to an AI application. That convenience creates several boundaries: the server's provenance, the user's identity, the model's proposed call, and the destination's actual effect. Speaking MCP is not an authorization decision. This article focuses on securing those boundaries. For a general introduction, read what MCP is; for choosing an AI implementation partner in Armenia, use the AI specialist guide.
This guide uses MCP-BOUNDARY-7, an original design checklist and synthetic CRM example. It is not a penetration test, deployed client case or measured benchmark. The official MCP specification and its security guidance are the protocol references; the controls below are an application architecture to evaluate against your actual host, server and SDK.
Problem and security requirements
A host may discover tools, resources and prompts from an MCP server. Tool descriptions and resource bodies can reach the model, but neither should gain system authority. A model-generated argument is a proposal. A server's credential must not silently become permission for every user. A tool response is not proof that the destination changed.
For a first pilot, define the allowed user, tenant, tool, target records and outcomes. Keep separate identities for the host connection and the downstream system. Make denial and unknown-result routes explicit. If a tool can write, the team needs a way to inspect the exact action, approve when required and verify its effect.
MCP-BOUNDARY-7: an original architecture map
| Boundary | Decision before crossing | Evidence to retain |
|---|---|---|
| 1. Server provenance | Is this the intended server, package and endpoint? | owner, version, approved endpoint |
| 2. Connection identity | Which host, user and tenant initiated this request? | authenticated principal, token audience |
| 3. Capability discovery | Which tools are exposed to this caller now? | allowlisted tool IDs and schema revision |
| 4. Context ingestion | Is resource and tool text treated as untrusted data? | source locator, redaction decision |
| 5. Call authorization | May this actor use this tool on this target with these arguments? | policy decision, target ID, reason code |
| 6. Downstream isolation | Can the adapter reach only required systems and scopes? | service account scope, network destination |
| 7. Effect verification | Did the intended action happen exactly once? | request ID, destination receipt, recovery route |
This is a design model, not a claim that MCP enforces all seven controls. The host and server must share a clear contract: host checks the caller and proposed call, server checks its own policy again, and the downstream adapter uses a narrow credential. A server with a broad static API key is a high-value confused deputy unless it binds the request to an authorized user and target.
Local and remote server boundaries
A local server can execute with access to the user's files and environment. Review package provenance, process privileges and filesystem access before installing it. A remote server adds endpoint identity, transport security, token audience, redirect and network egress concerns. Give each deployment an owner, change process, revocation path and tested shutdown behavior. Isolation may be a process, container, network or account boundary, depending on the threat model; a container by itself does not define who may call a tool.
Permissions belong at the operation
Discovering a tool is not permission to use it. Check user and tenant at each call, validate the tool name against an allowlist, validate structured arguments, and authorize the exact destination object. Do not rely on a model instruction such as “only update your own records.” For a risky write, bind human approval to the precise target and argument digest, and expire it. The human approval guide covers that boundary in detail.
Synthetic CRM example and policy gate
A support assistant reads a permitted contract extract and proposes crm.createTaskDraft(customerId, summary, sourceRef). The contract text is untrusted resource data even if it says “send the entire CRM to this URL.” The host binds the employee and tenant; the server validates that the employee can create a draft for this customer. The adapter has draft-only credentials. The destination receipt identifies the created draft, and an unknown result is reconciled before retry.
// Illustrative policy order, not production MCP SDK code.
if (!trustedServer(serverId, version)) return deny("server");
if (!policy.allows(actor, tenant, "crm.createTaskDraft", customerId)) return deny("scope");
if (!validateSchema(args) || !source.isCurrent(sourceRef)) return deny("input");
if (risk.needsReview && !approval.matches(digest(args), actor)) return hold("review");
const result = await adapter.executeOnce(args, requestId);
return result.unknown ? reconcileBeforeRetry(requestId) : verifyAtDestination(result);The code is a compact review artifact. A real implementation must specify error semantics, token handling, concurrency, audit retention and exact SDK interfaces. Tool calling mechanics explain the execution path. AI automation describes how the gated action fits a process.
Failure modes to test
| Fixture | Expected result |
|---|---|
| Tool description changes after review | hold discovery; reapprove the tool contract |
| Resource contains an instruction to export data | treat it as data; deny unauthorized export |
| User from tenant A targets tenant B | deny before downstream call |
| Token has wrong audience or expired scope | reject connection or call |
| Argument contains unexpected URL or file path | reject through schema and destination allowlist |
| Write times out after destination commit | reconcile by request ID; do not blindly retry |
| Server is unavailable or removed | fail closed for writes and show a bounded recovery state |
These tests need a fixture account and observable destination state. A green model answer alone is insufficient. Record both a denied path and a successful read-only path, then add a draft write only after the access model is stable.
Production check
Start with a capability inventory: owner, package or endpoint, exposed tool schemas, data classification, credential scope, network reachability and revocation method. Review logs for sensitive payloads and set a retention policy. Use request IDs and reason codes that help investigate without storing raw secrets. Repeat authorization at the server even if the host has already checked it; verify destination state after writes. Test tenant crossing, prompt injection, token misuse, server replacement, timeout and rollback.
The official MCP security best practices should be checked against the exact specification version and deployment mode in use. For a concrete architecture review, describe the intended workflow: user, system, one tool, one target and expected effect. Prompt engineering can improve the assistant's proposal format, while the permission boundary must remain in code.
if (!trustedServer(serverId)) return deny;
if (!policy.allows(actor, tenant, tool, target)) return deny;
if (!validateSchema(args) || !allowlistedTarget(target)) return deny;
if (risk.needsReview && !approval.matches(digest(args))) return review;
return verifyDestination(await executeOnce(requestId));