MCP makes it possible for AI applications to discover and call tools exposed by external servers. That flexibility is one of its biggest strengths, but it also creates an important security question:
Should an MCP client that can connect to a server automatically be allowed to call every tool on that server?
In a production environment, the answer should usually be no.
A user may be allowed to read customer information but not modify it. An agent may be allowed to search documents but not delete them. A support workflow may need access to ticket APIs while a reporting agent should have read-only access.
This is where zero-trust tool authorization becomes important.
The basic principle is straightforward:
Establish identity, authenticate the request, authorize the specific capability, and grant only the permissions required for that operation.
The current MCP specification has strengthened its OAuth authorization model, including authorization-server discovery, issuer validation, resource-bound credentials, scope challenges, and a stronger emphasis on least-privilege scope selection.
For .NET developers, this means MCP authorization should be designed as an explicit security boundary rather than relying only on whether a client successfully connected to the server.
Why Server-Level Authorization Is Not Enough
Consider an MCP server exposing these tools:
Customer MCP Server
│
├── get_customer
├── search_orders
├── update_customer
├── refund_order
└── delete_customer
A simple server-level policy might say:
Authenticated client = access to MCP server
That is too broad.
A reporting agent might only need:
customer:read
orders:read
A customer-service agent might need:
customer:read
orders:read
customer:write
A finance agent might additionally need:
orders:refund
The security model should therefore distinguish authentication from authorization.
Client
|
v
Authentication
|
v
Identity + Claims
|
v
Authorization Policy
|
v
Tool Permission
|
v
Tool Execution
A valid token answers:
Who is calling?
Authorization answers:
What is this caller allowed to do?
Understanding Zero-Trust for MCP
Zero-trust does not mean denying every request.
It means that trust is not automatically inherited from network location, connection status, or previous requests.
For MCP, a practical zero-trust model can apply these checks:
Authenticate the client.
Validate the token issuer.
Validate the token audience/resource.
Validate expiration.
Validate required scopes.
Apply user or workload permissions.
Authorize the requested tool.
Execute only after authorization succeeds.
The current MCP authorization specification requires clients and servers to follow OAuth-based authorization and discovery mechanisms, while access tokens are bound to the resource they were issued for.
Designing Tool Scopes
Scopes provide one mechanism for expressing permissions.
A simple model could be:
customer:read
customer:write
orders:read
orders:write
orders:refund
Then map tools to required scopes:
| MCP Tool | Required Scope |
|---|
| get_customer | customer:read |
| search_orders | orders:read |
| update_customer | customer:write |
| create_order | orders:write |
| refund_order | orders:refund |
| delete_customer | customer:write |
The names are application-specific. The important point is that permissions should represent meaningful capabilities rather than simply copying internal implementation details.
The MCP authorization specification explicitly recommends least-privilege scope selection and allows servers to communicate the scopes required for a particular operation through the WWW-Authenticate challenge.
Implementing Authentication in ASP.NET Core
An MCP server built on ASP.NET Core can use the standard authentication middleware.
A simplified configuration looks like:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority =
builder.Configuration["Identity:Authority"];
options.Audience =
builder.Configuration["Identity:Audience"];
options.RequireHttpsMetadata = true;
});
builder.Services.AddAuthorization();
builder.Services
.AddMcpServer()
.WithHttpTransport()
.WithToolsFromAssembly();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapMcp();
app.Run();
The exact identity-provider configuration will depend on the organization's OAuth or OpenID Connect infrastructure.
The important architectural point is that MCP sits behind an established identity boundary rather than implementing token validation manually inside every tool.
Do Not Put Authorization Logic Only Inside Tools
A common implementation is:
public async Task<string> RefundOrder(
string orderId,
ClaimsPrincipal user)
{
if (!user.IsInRole("Finance"))
{
throw new UnauthorizedAccessException();
}
// Refund order...
}
This can work, but it mixes business logic and security policy.
A better design is to establish authorization policies centrally.
For example:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(
"OrdersRead",
policy => policy.RequireClaim(
"scope",
"orders:read"));
options.AddPolicy(
"OrdersRefund",
policy => policy.RequireClaim(
"scope",
"orders:refund"));
});
Then the MCP boundary can enforce the appropriate policy before the tool reaches business logic.
This separation makes security policy easier to audit and change.
Per-Server vs Per-Tool Authorization
There are two useful models.
Per-Server Authorization
Every MCP request requires authentication.
MCP Server
|
+-- Authentication required
|
+-- get_customer
+-- search_orders
+-- refund_order
This is simpler when every capability is sensitive.
Per-Tool Authorization
Only selected tools require authorization.
MCP Server
│
├── ping Public
├── documentation Public
├── get_customer Protected
├── update_customer Protected
└── refund_order Highly Protected
The MCP authorization ecosystem supports both approaches. Per-tool authorization allows public capabilities to remain accessible while sensitive operations trigger authorization requirements.
For enterprise systems, per-tool authorization is often useful when a single server exposes capabilities with different risk levels.
Mapping Tools to Policies
A practical .NET implementation can maintain an explicit policy map.
public static class McpAuthorizationPolicies
{
public const string CustomerRead = "customer:read";
public const string CustomerWrite = "customer:write";
public const string OrdersRead = "orders:read";
public const string OrdersRefund = "orders:refund";
}
Then:
var toolPolicies = new Dictionary<string, string>
{
["get_customer"] =
McpAuthorizationPolicies.CustomerRead,
["update_customer"] =
McpAuthorizationPolicies.CustomerWrite,
["search_orders"] =
McpAuthorizationPolicies.OrdersRead,
["refund_order"] =
McpAuthorizationPolicies.OrdersRefund
};
This creates a visible security inventory.
When a new tool is added, the development team has to decide what permission it requires.
That is much safer than allowing every newly registered tool to inherit broad server permissions.
Handling Insufficient Scope
Suppose a client has:
orders:read
and attempts:
refund_order
The server should not execute the tool and then return a generic application error.
The authorization layer should reject the request because the required permission is missing.
The current MCP specification defines 403 Forbidden with an insufficient_scope challenge for cases where the presented token lacks the required permission.
Conceptually:
Client
|
| refund_order
v
MCP Server
|
| Token = orders:read
|
| Required = orders:refund
v
403 insufficient_scope
This is different from authentication failure.
401 = Authentication is missing or invalid
403 = Authentication succeeded, but permission is insufficient
Keeping these states distinct makes both client behavior and operational troubleshooting clearer.
Step-Up Authorization
A useful pattern is step-up authorization.
A client may initially have:
orders:read
It can therefore search orders.
Later, the user asks the agent to issue a refund.
The server requires:
orders:refund
The client can initiate a new authorization flow with the additional scope rather than requesting every possible permission at startup.
The MCP authorization specification describes scope accumulation during step-up authorization and recommends that clients retain previously granted scopes when requesting additional permissions.
This supports a better user experience while maintaining least privilege.
Protecting High-Risk Tools
Not every tool has the same security impact.
A useful classification is:
| Risk | Example | Suggested Control |
|---|
| Low | get_time | Basic authentication |
| Medium | get_customer | Read scope |
| High | update_customer | Write scope |
| Very High | refund_order | Dedicated scope + additional policy |
| Critical | delete_customer | Dedicated scope + approval/step-up |
This is not a universal classification.
Each organization should define its own risk model.
The key principle is to avoid treating read and destructive operations as equivalent.
Tool Annotations Are Not Authorization
MCP supports tool annotations that can describe behavioral properties such as whether a tool is read-only, destructive, or idempotent.
Those annotations are useful security signals, but they should not replace authorization.
For example:
Tool annotation:
destructive = true
Authorization:
orders:delete
These answer different questions.
The annotation describes the tool.
The authorization policy determines whether the caller may use it.
Recent MCP security guidance explicitly emphasizes that tool annotations are risk vocabulary and should not be treated as a complete security mechanism.
Preventing Confused-Deputy Problems
A particularly important risk occurs when an MCP server has access to more resources than the requesting user should be able to access.
Consider:
User
|
v
AI Agent
|
v
MCP Server
|
+--> CRM
+--> Billing
+--> Documents
If the MCP server uses its own powerful service identity for every request, the server may accidentally act as a privileged deputy.
The authorization context should therefore be preserved as far as the architecture requires.
For example:
User Identity
|
v
Agent Identity
|
v
MCP Authorization
|
v
Downstream Authorization
The exact token-exchange design depends on the identity platform and deployment architecture, but the principle is important:
Do not allow an agent to gain the MCP server's entire privilege set merely because the server can access those systems.
Resource Audience Validation
A token should not simply be accepted because it is a valid JWT.
The MCP authorization model requires access tokens to be intended for the protected resource they are accessing. The 2026-07-28 specification also strengthens authorization-server and issuer validation.
At a high level, validation should cover:
Signature
+
Issuer
+
Audience / Resource
+
Expiration
+
Scopes
+
Identity
Skipping audience or resource validation can allow a token intended for another service to be misused.
Authorization Server Discovery
MCP authorization is designed around discovery rather than hard-coded assumptions.
The server advertises protected-resource metadata, which allows the client to discover the associated authorization server. MCP clients are required to support the defined authorization-server discovery mechanisms.
The architecture is approximately:
MCP Client
|
| Request
v
MCP Server
|
| 401 + metadata
v
Authorization Discovery
|
v
Authorization Server
|
v
Access Token
|
v
MCP Server
This becomes particularly important when organizations operate multiple MCP servers backed by different identity systems.
Enterprise-Managed Authorization
Large organizations may not want every user to authorize every MCP server independently.
The MCP ecosystem now includes Enterprise-Managed Authorization (EMA) as a stable extension for centrally provisioning MCP server access through an organization's identity provider.
This changes the enterprise architecture:
Identity Provider
|
+----------------+
| |
v v
MCP Server A MCP Server B
| |
v v
Agent Agent
Central provisioning can reduce repeated consent flows and gives administrators a centralized control plane for MCP connectivity.
For large .NET deployments, this is worth considering alongside normal OAuth authorization rather than building an independent enterprise access-management system.
Zero-Trust MCP Architecture
A production architecture can combine these controls:
Identity Provider
|
v
Access Token
|
v
Client ---> API Gateway ---> MCP Server
| |
| +--> Token Validation
| |
| +--> Scope Check
| |
| +--> Tool Policy
| |
| +--> Tenant Policy
| |
| +--> Tool Execution
|
+--> Rate Limiting
+--> Logging
+--> WAF
MCP 2026-07-28 also introduces standardized Mcp-Method and Mcp-Name HTTP headers, allowing gateways and security infrastructure to route, authorize, and meter requests using headers rather than parsing the JSON-RPC body.
That can simplify policy enforcement at the infrastructure layer.
Common Mistakes
Trusting Any Authenticated Client
Authentication does not grant permission to every tool.
Using One Broad Scope
A scope such as:
mcp:all
may be convenient but undermines least privilege.
Authorizing Only at the UI
The agent interface should never be the security boundary. Authorization must be enforced by the server.
Treating Tool Descriptions as Security Policies
Descriptions help agents understand capabilities. They do not enforce access control.
Ignoring Token Audience
A valid token issued for another resource should not automatically be accepted.
Giving Agents Service-Account Privileges
The MCP server's permissions should not automatically become the user's permissions.
Putting Security Logic Inside Every Tool
Duplicated authorization code becomes difficult to audit and easy to forget when new tools are added.
Testing MCP Authorization
Security controls should be tested like application behavior.
Create a test matrix:
| Scenario | Expected Result |
|---|
| No token | 401 |
| Expired token | 401 |
| Invalid issuer | 401 |
| Wrong audience/resource | 401 |
| Missing scope | 403 |
| Correct read scope | Success |
| Correct write scope | Success |
| Read scope calling write tool | 403 |
| Revoked user | Denied |
| Tenant mismatch | Denied |
For example:
[Fact]
public async Task RefundOrder_RequiresRefundScope()
{
var client = CreateClient(
scopes: ["orders:read"]);
var response = await client.CallToolAsync(
"refund_order",
new Dictionary<string, object?>
{
["orderId"] = "ORD-1001"
});
Assert.Equal(
HttpStatusCode.Forbidden,
response.StatusCode);
}
The exact test API will depend on the MCP client and transport implementation, but the security scenarios should remain explicit.
Troubleshooting Authorization Failures
When an MCP request fails, determine where the failure occurred.
401 Unauthorized
Check:
403 Forbidden
Check:
Required scope
Granted scopes
User permissions
Tenant membership
Tool policy
Tool Works Without Authorization
Check whether the endpoint is accidentally bypassing authentication middleware or whether the tool has been classified as public.
Authorization Works but Downstream Call Fails
The MCP server may be correctly enforcing its own policy while using an incorrect downstream identity or token.
Trace the complete identity chain.
Best Practices
Authenticate every protected MCP request.
Validate issuer, audience/resource, signature, and expiration.
Use least-privilege scopes.
Map sensitive tools to explicit policies.
Separate read, write, and destructive capabilities.
Use step-up authorization for higher-risk operations.
Do not treat tool annotations as authorization.
Preserve tenant and user authorization context.
Centralize authorization policy where possible.
Test 401 and 403 scenarios explicitly.
Log authorization decisions without exposing sensitive tokens.
Use enterprise-managed authorization where centralized provisioning is appropriate.
Conclusion
MCP makes tools discoverable and callable by AI clients, but discoverability should never imply unrestricted access.
A zero-trust architecture treats every tool invocation as an authorization decision. The server authenticates the caller, validates the token, determines the required scope, checks the caller's permissions, and only then executes the requested capability.
The current MCP specification strengthens this model with OAuth authorization hardening, resource-bound credentials, scope challenges, issuer validation, and least-privilege guidance.
For .NET teams, the practical design is to keep authentication and authorization at the MCP and ASP.NET Core boundaries, maintain explicit tool-to-policy mappings, and use narrowly defined scopes for sensitive operations.
The most important rule is simple:
An agent should receive exactly the permissions required for the task it is performing, not every permission available to the MCP server.
That principle turns MCP from a powerful tool integration mechanism into a platform that can be deployed with a security model appropriate for enterprise workloads.
Frequently Asked Questions
Is authentication enough for an MCP server?
No. Authentication establishes identity. Authorization determines which tools and operations that identity may use.
Should every MCP tool require a separate OAuth scope?
Not necessarily. Scope granularity should match the application's security model. Related low-risk operations may share a scope, while sensitive operations should generally have more specific permissions.
What is the difference between 401 and 403?
401 Unauthorized generally means the request lacks valid authentication. 403 Forbidden indicates that authentication succeeded but the caller does not have sufficient permission for the requested operation.
Can an MCP server have both public and protected tools?
Yes. MCP authorization patterns support both server-wide authorization and per-tool authorization, allowing public capabilities to coexist with protected tools when appropriate.
Should an MCP gateway perform authorization?
It can enforce coarse-grained controls such as authentication, rate limiting, and routing, but sensitive tool authorization should still be enforced at the MCP resource boundary. The 2026-07-28 protocol's standardized headers can make gateway-level routing and policy enforcement easier.