MCP Security: Permissions, Secrets, and Tool Boundaries

Table of contents
An MCP server built once tends to accumulate more access than any single caller actually needs, because it has to serve every agent that connects to it, and the easiest way to do that is to grant it everything up front. That habit is where most MCP security problems start: not with a dramatic exploit, just with a server that was given broad access because narrowing it felt like premature optimization.
Three things actually need separate answers: who is calling, what are they allowed to do, and how do credentials reach a tool without ever passing through the model.
Permissions: scope by caller, not by server
A single API key shared across every agent that talks to an MCP server means every agent effectively has every other agent’s access. If a support-reply agent and an admin agent connect to the same server with the same token, a prompt injection that reaches the support agent inherits the admin agent’s reach too.
Nexus scopes this per project instead: sources are grouped into named projects, and API keys are issued per project with roles (admin, member, viewer) attached. A key scoped to one project’s sources can’t see another project’s data even if an agent holding it is compromised, because the permission boundary lives at the token, not at the server.
Secrets: the model should never see a raw credential
The model’s context window is the wrong place for a credential to ever appear, even briefly. If a secret reaches the prompt, it can leak through a logged conversation, a cached response, or the model simply repeating it back in an unrelated answer. The tool should hold the credential and use it directly; the model should only ever see the result of the call, never the key that authorized it.
Nexus encrypts every source credential at rest with Fernet and key rotation, and the UI only ever displays *** for a configured secret, never the value itself, not even to an admin re-editing the connection. That’s the same principle applied to the human side of the system: if a credential doesn’t need to be visible to do its job, it shouldn’t be visible.
flowchart LR
accTitle: Where a credential should and should not travel
accDescr: A tool call reaches the MCP server, which holds the encrypted credential and uses it directly against the external API. The credential never passes through the model's context window or the agent's visible reasoning, only the result of the call does.
AGENT[Agent / model]
TOOL[MCP tool]
VAULT[(Encrypted credential store)]
API[External API]
AGENT -->|call, no secret| TOOL
TOOL -->|fetch| VAULT
TOOL -->|authenticated request| API
API -->|result| TOOL
TOOL -->|result only| AGENT
Tool boundaries: a tool’s reach is a subset, never a superset
A tool’s permission should never exceed the permission of whoever is calling it. That sounds obvious until a shared internal tool gets built with a service account that has broader access than any one caller is supposed to have, on the assumption that access can be restricted “at the agent level” later. It usually isn’t, and the tool quietly becomes the widest door into the system.
Separate read and write access at the tool boundary, not just at the permission layer. A search tool and a delete tool should not be two modes of the same tool, and ideally shouldn’t even live on the same MCP server if the two have meaningfully different blast radii.
Step by step
- Issue scoped tokens per project or per agent, never one shared key for an entire MCP server.
- Store secrets encrypted at rest, with the tool decrypting and using them server-side, never passing them into the model’s context.
- Mask secrets everywhere a human can see them too, including your own admin UI. If nobody needs to read it back, nobody should be able to.
- Separate tools by blast radius. Read-only tools on one boundary, anything destructive on another, with its own stricter gate.
- Audit log every tool call with the caller’s identity and scope, not just the action, so a scope that’s too broad shows up in review before it shows up in an incident.
Pitfalls
- A key rotation policy that exists on paper but isn’t enforced anywhere is the same as not having one. Automate it or assume it won’t happen.
- “We’ll restrict this later” is how most over-broad service accounts get created. Scope it narrow on day one; widening a permission later is a one-line change, narrowing one after the fact means finding every caller that depended on the extra access.
- A secrets vault doesn’t help if the application still logs full request payloads that happen to include the decrypted value. Check what actually ends up in your logs, not just what you intended to log.
FAQ
Is per-project or per-agent scoping better than role-based access control?
They’re not mutually exclusive. Roles (admin, member, viewer) describe what a caller can do; project or source scoping describes which data they can do it to. Nexus uses both together: a role bounds the actions, a project boundary bounds the data those actions can touch.
How do you rotate a secret without breaking a running agent?
Support both the old and new credential for a short overlap window, rotate the stored value, then revoke the old one once nothing is using it. This needs the credential store to be a layer the tool calls into, not a value baked directly into the tool’s code, which is itself a reason to keep secrets out of the model’s context and out of static config.
Does a self-hosted MCP server need this level of security if it's only used internally?
Yes, maybe more than a hosted one. “Internal” describes who’s supposed to use it, not what happens if a prompt injection, a compromised dependency, or a mis-scoped token exposes it to something that wasn’t supposed to have access. Internal-only is a deployment detail, not a security boundary.


