Implementation guide · 10 steps
How to Secure an MCP Server: 10 Steps
An ordered procedure, not a checklist. The sequence matters: scoping the credential first shrinks the worst case, and every step after it is cheaper to get right against a smaller worst case.
The short answer
To secure an MCP server: scope its credential to the narrowest workable permission, authenticate every caller, expose the fewest tools that work, bound every parameter, treat tool output as untrusted input, require approval for irreversible calls, rate-limit per identity, log every attempt including refusals, isolate the runtime, and verify by attempting the calls that should fail. Ten steps, in that order, roughly two days of engineering for a server you own.
Before you start
Three things have to exist before any of the ten steps produces a real improvement, and all three are usually missing. Collect them first; each takes minutes and each prevents a step from being done on a guess.
The real tool list
Not the documented one. Call tools/list and read what the server actually advertises, including anything left in from development.
The credential’s true reach
Look up what the server’s credential permits on its backing system — the widest action, not the intended one. This number is your worst case.
A named owner
One human who can decide which tools stay exposed. Steps 1 and 3 need a decision, not an engineer’s guess about what someone might need.
The ten steps
Ordered by leverage. Steps 1 to 3 change what the server is capable of; steps 4 to 8 change what it will accept and record; steps 9 and 10 contain and confirm. Doing them out of order mostly wastes effort — there is little point tuning approval flows for a tool whose credential should have been revoked in step 1.
Scope the credential first
Reduce the credential the server holds on its backing system to the narrowest permission the use case needs. A server built to look up orders should hold a read-only role on the orders table — not a database user that can also update and delete, however convenient that was during development.
This step is first because it is the only one that shrinks the worst case rather than reducing the chance of reaching it. Every other control on this list is a filter in front of a capability; if the capability is gone, the filter cannot fail open. Expect this to be the step that requires the owner’s agreement and takes the longest in calendar time.
Authenticate every caller
Require a short-lived, verifiable token per calling agent and validate it on every request, not just at connection. A static shared key in a client config proves only that someone holds the key: you cannot attribute a log line to an actor, revoke one caller, or rotate without breaking all of them at once.
Preserve the original caller’s authority rather than collapsing everyone into one service account — otherwise a low-privilege user’s request executes with the service account’s privileges, which is the confused-deputy problem with a protocol in the middle. MCP authentication patterns compares what each option actually proves.
Expose the fewest tools that work
Delete the tools nobody calls. Split read tools from write tools into separate servers with separate credentials, so a read-only deployment cannot be persuaded into mutating anything regardless of what reaches the model. Then filter the tools/list response by caller identity.
Filtering discovery matters more than it sounds. A tool the model never learns exists cannot be talked into existence by injected content, and a shorter tool list also measurably improves the model’s selection accuracy. Security and quality point the same way here, which is rare enough to use as an argument.
Bound every parameter
Validate against a schema with real limits: amount ceilings, allow-listed recipients, row caps, date ranges, enumerated values. A transfer tool with no maximum is a different tool at 10 and at 10 million, and tool-level permission alone cannot tell those apart.
Reject out-of-range values rather than silently clamping them — a clamp hides the attempt, and the attempt is the signal you want in the log. Never build SQL, shell commands or file paths by concatenating model-supplied strings. The values arriving here were produced by something that can be persuaded, so they get the same treatment as input from the public internet.
Treat tool output as untrusted input
Whatever your server returns flows back into the model’s context, where it competes with your instructions. If any of it originates outside your control — a support ticket, a web page, a document, a field a customer can type into — assume it may contain instructions aimed at the model.
Strip or neutralise control markup, label the boundary between data and instruction explicitly, and cap response size so one call cannot flood the window and push your system prompt out of effective range. This does not solve indirect prompt injection — nothing at the server level does — but it removes the cheapest version of the attack.
Require approval for irreversible calls
List the tools that move money, delete data, change access or are visible outside the company, and hold those calls for a human decision. The test is not how sensitive the system feels but whether the action can be taken back: an email sent to a customer is irreversible in a way a database read never is.
Show the approver the tool, the parameters and the calling identity — not a generic “allow this action?” prompt. An approver who cannot see what they are approving becomes a rubber stamp within a week, which is worse than no approval because it manufactures a record of oversight that did not happen. Designing approval thresholds covers where to draw the line.
Rate-limit per identity
Ceilings per agent, per minute and per day, plus a spend ceiling wherever calls cost money. The most common MCP incident is not an attack — it is an agent in a retry loop calling the same tool ten thousand times, and without a per-identity limit that is indistinguishable from abuse until the invoice or the rate-limit response from the backing system arrives.
Limit per identity rather than per server, or one runaway agent consumes the budget of every well-behaved one. Agent spend ceilings goes into the cost side.
Log every attempt, including refusals
One structured record per invocation: caller identity, tool, parameters, policy decision, approver where applicable, result status and timestamp. Write it where the decision is made, and send it somewhere the security team can query without asking an engineer to grep a container.
Refusals are the more valuable half. A successful call tells you the system worked; a rising refusal rate against one tool tells you either your policy is wrong or an agent is being steered, and both are things you want to notice early. Redact secrets at write time — a log that has to be sanitised before anyone may read it is a log nobody reads. How to audit AI agent actions covers the record in detail.
Isolate the runtime
Non-root user, its own container, no inbound network beyond the client port, egress restricted to the systems it legitimately calls, secrets injected at runtime rather than baked into an image or read from a file on a developer’s laptop, and a pinned dependency set you can rebuild reproducibly.
Egress restriction is the item most often skipped and the one that most limits damage: a server that can only reach its own backing system cannot be used to move data somewhere else, whatever it is persuaded to attempt.
Verify by attempting what should fail
Controls that have never refused anything are assumptions. Write tests that expect refusal and assert the refusal reaches the log — the five below take an afternoon and then run on every deploy, which is what stops a refactor quietly widening what an agent can reach.
A server you did not write
Most estates contain servers nobody in the building wrote. You cannot harden their internals, so the goal changes from hardening to containment: assume the server may behave badly and arrange things so that it matters less.
- 01Give it its own narrowly scoped credential. Never a credential shared with a server you control.
- 02Run it in its own container with egress limited to the one system it needs.
- 03Pin the version. An auto-updating third-party server changes what your model can do without review.
- 04Diff its tool descriptions on every upgrade. Those descriptions are text your model trusts; a changed description is a changed instruction.
- 05Put a gateway in front of it, so identity, entitlement, rate limits and audit are enforced outside code you cannot read.
The last point is the general case: for any server whose internals you do not control, enforcement has to live outside it. Securing third-party MCP servers works through the review questions in full.
Verifying it actually holds
Five tests, each expecting a refusal, each asserting the refusal was recorded. If a test cannot be written because the control has no observable failure mode, that is itself the finding.
| Attempt | Expected result | Proves step |
|---|---|---|
| Call with an expired or unsigned token | Refused before the tool runs; refusal logged with the caller | 2, 8 |
| Call a tool the identity is not entitled to | Refused, and absent from that identity’s tools/list | 3, 8 |
| Pass a parameter one increment outside its bound | Rejected with the value recorded — not clamped silently | 4, 8 |
| Return content containing an instruction to call a T3 tool | The follow-on call is refused by entitlement, whatever the model attempts | 3, 5, 6 |
| Burst well above the per-identity rate limit | Throttled for that identity only; other identities unaffected | 7 |
The fourth test is the one worth running by hand at least once. Watching a model try to follow an instruction it found in returned data, and watching the enforcement point refuse it anyway, is the fastest way to convince a sceptical team that the control belongs outside the prompt.
Built on this thinking
Steps 2, 3, 6, 7 and 8 are cheaper at one enforcement point
Implemented per server, five of these ten steps get re-implemented every time you add a server, and the weakest implementation sets your real posture. Barzel Central Gateway is the single identity, routing and audit point in front of your servers; BarzelVault is the entitlement, approval and evidence layer behind it.
Five mistakes worth naming
Hardening the prompt instead of the perimeter
No wording reliably beats an instruction found in content. Enforcement outside the model is the control; prompt text is a preference.
One service account for all callers
Convenient, and it deletes attribution, individual revocation and per-agent limits in a single stroke.
Permitting tool names, not parameter values
A policy that names tools and ignores values approves the tool’s entire range, including the part nobody would sign off on.
Logging successes only
The refusals are where the intelligence is. Without them you cannot distinguish a wrong policy from a steered agent.
Shipping a write tool before governance exists
The first mutating tool changes the worst case from disclosure to action. MCP governance should precede it, not follow it.
Frequently asked questions
How do you secure an MCP server?
Scope its credential to the narrowest workable permission, authenticate every caller with a short-lived token, expose the fewest tools that work, bound every parameter, treat tool output as untrusted input, require human approval for irreversible calls, rate-limit per identity, log every attempt including refusals, isolate the runtime, and verify by attempting the calls that should be refused.
What is the single most important MCP server security step?
Scoping the credential the server uses on its backing system. It is the only step that reduces the worst case rather than reducing the probability of reaching it, and it makes every subsequent control cheaper to get right.
Is an API key enough to secure an MCP server?
No. A static shared key proves that someone holds the key, not which agent is calling. It cannot be attributed in a log, revoked for one caller, or rotated without breaking every caller simultaneously. Use short-lived per-agent tokens instead.
Should read and write tools live in the same MCP server?
Preferably not. Splitting them lets each server hold a credential matching its own worst case, so a read-only deployment cannot be talked into mutating state regardless of what reaches the model.
How do you secure an MCP server you did not write?
You cannot harden its internals, so contain it: give it its own narrowly scoped credential, run it in an isolated container with restricted egress, pin the version and re-review on upgrade, put a gateway in front of it so identity, entitlement and audit are enforced outside it, and diff its tool descriptions on every update because those descriptions are text your model trusts.
Can prompt engineering secure an MCP server?
No. Instructions in a system prompt compete with instructions in returned content, and there is no wording that reliably wins. Enforcement has to sit outside the model, where an out-of-scope call is refused regardless of the reasoning that produced it.
How do you test that an MCP server is secure?
With tests that expect failure: an expired token, a tool the caller is not entitled to, a parameter outside its bounds, an injected instruction inside returned content, and a burst above the rate limit. Each should be refused, and each refusal should appear in the log.
How long does hardening an MCP server take?
For a server you own, roughly two days of engineering for the first nine steps and half a day for the verification tests. Credential scoping and tool reduction usually take longer than the code, because both need the system owner to agree.
Sources and further reading
Primary specifications and standards this procedure relies on. The ordering of the ten steps is our own judgement, drawn from building and operating five MCP servers — where a claim is judgement rather than something a standard states, the text says so.
- 01 · MCP project Model Context Protocol — specification ↗ Normative source for tool schemas, capability negotiation and the authorization sections steps 2 and 3 build on.
- 02 · MCP project Model Context Protocol — official documentation ↗ Primary source for protocol structure, transports and the shape of a tools/list response.
- 03 · OWASP GenAI Security Project OWASP GenAI LLM Top 10 (2026) ↗ Consensus risk list; prompt injection and excessive agency are the entries steps 3 to 6 exist to bound.
- 04 · IETF RFC 8693 — OAuth 2.0 Token Exchange ↗ The mechanism behind preserving caller authority across the hop in step 2.
- 05 · OWASP Application Security Verification Standard ↗ Input-validation and logging requirements that steps 4 and 8 restate in MCP terms.
- 06 · OpenSSF SLSA — Supply-chain Levels for Software Artifacts ↗ The provenance and reproducibility framing behind pinning and rebuilding in step 9.
- 07 · Simon Willison Prompt injection — ongoing series ↗ The most consistently updated practitioner record of the attack class step 5 contains.
Last reviewed 24 August 2026. External links open in a new tab; we do not control their content.
Practise the first step on a real server
Before you start, you need the real tool list rather than the documented one. To see what that call returns against a live server — and to confirm your client works before pointing it at anything that governs production — Barzel Scripture Intelligence is free and public at scripture-intelligence-server.mcpize.run: no signup, no key, 54 tools. A tools/list call takes about a minute. Setup is in the reference.
Reference documentation
Want the specification rather than the argument?
Transport, auth, capability enumeration, policy evaluation and refusal semantics for every Barzel server.
Dev free · 10,000 calls/mo · paid plans from $199/mo on the MCPize marketplace
Written by
Mark Alex
Founder of Real Biz Digital and architect of the Barzel ecosystem — five MCP servers published and callable in public. Software developer, technology entrepreneur and mechatronics engineer, working across AI agent governance, MCP security, AI infrastructure, FinOps and intelligent operations.