Learn how to implement MCP securely in enterprise environments with zero API key exposure, local tool execution, and Azure-managed authentication for maximum security and compliance.
Introduction
The Model Context Protocol (MCP) enables AI systems to interact with external data sources and tools through a standardized interface. While powerful, MCP can introduce security risks in enterprise environments. This tutorial shows you how to implement MCP securely using local servers, Azure OpenAI with APIM, and proper authentication.
Understanding MCP's Security Risks
There are a couple of key security concerns to consider before implementing MCP:
- Data Exfiltration: External MCP servers could expose sensitive data.
- Unauthorized Access: Third-party services become potential security risks.
- Loss of Control: Unknown how external services handle your data.
- Compliance Issues: Difficulty meeting regulatory requirements with external dependencies.
The solution? Keep everything local and controlled.
Secure Architecture
Before we dive into implementation, let's take a look at the overall architecture of our secure MCP solution:
Figure 1: Secure Model Context Protocol Implementation with Azure InfrastructureThis architecture consists of three key components working together:
- Local MCP Server - Your custom tools run entirely within your local environment, reducing external exposure risks.
- Azure OpenAI + APIM Gateway - All AI requests are routed through Azure API Management with Microsoft Entra ID authentication, providing enterprise-grade security controls and compliance.
- Authenticated Proxy - A lightweight proxy service handles token management and request forwarding, ensuring seamless integration.
One of the key benefits of this architecture is that no API key is required. Traditional implementations often require storing OpenAI API keys in configuration files, environment variables, or secrets management systems, creating potential security vulnerabilities. This approach uses Azure Managed Identity for backend authentication and Azure CLI credentials for client authentication, meaning no sensitive API keys are ever stored, logged, or exposed in your codebase.
For more security, APIM and Azure OpenAI resources can be configured with IP restrictions or network rules to only accept traffic from certain sources. These configurations are available for most Azure resources and provide an additional layer of network-level security.
This security-forward approach gives you the full power of MCP's tool integration capabilities while keeping your implementation completely under your control.
How to Implement MCP Securely
1. Local MCP Server Implementation
Building the MCP Server
Let's start by creating a simple MCP server in .NET Core.
1. Create a web application
dotnet new web -n McpServer
2.Add MCP packages
dotnet add package ModelContextProtocol --prerelease
dotnet add package ModelContextProtocol.AspNetCore --prerelease
3. Configure Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer()
.WithHttpTransport()
.WithToolsFromAssembly();
var app = builder.Build();
app.MapMcp();
app.Run();
WithToolsFromAssembly() automatically discovers and registers tools from the current assembly. Look into the C# SDK for other ways to register tools for your use case.
4. Define Tools
Now, we can define some tools that our MCP server can expose. here is a simple example for tools that echo input back to the client:
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Tools;
[McpServerToolType]
public static class EchoTool
{
[McpServerTool]
[Description("Echoes the input text back to the client in all capital letters.")]
public static string EchoCaps(string input)
{
return new string(input.ToUpperInvariant());
}
[McpServerTool]
[Description("Echoes the input text back to the client in reverse.")]
public static string ReverseEcho(string input)
{
return new string(input.Reverse().ToArray());
}
}
Key components of MCP tools are the McpServerToolType class decorator indicating that this class contains MCP tools, and the McpServerTool method decorator with a description that explains what the tool does.
Alternative: STDIO Transport
If you want to use STDIO transport instead of SSE (implemented here), check out this guide: Build a Model Context Protocol (MCP) Server in C#
2. Create a MCP Client with Cline
Now that we have our MCP server set up with tools, we need a client that can discover and invoke these tools. For this implementation, we'll use Cline as our MCP client, configured to work through our secure Azure infrastructure.
1. Install Cline VS Code Extension
Install the Cline extension in VS Code.
2. Deploy secure Azure OpenAI Endpoint with APIM
Instead of connecting Cline directly to external AI services (which could expose the secure implementation to external bad actors), we will route through Azure API Management (APIM) for enterprise security. With this implementation, all requests go through Microsoft Entra ID and we use managed identity for all authentications.
Quick Setup: Deploy the Azure OpenAI with APIM solution.
Ensure your Azure OpenAI resources are configured to allow your APIM's managed identity to make calls. The APIM policy below uses managed identity authentication to connect to Azure OpenAI backends. Refer to the Azure OpenAI documentation on managed identity authentication for detailed setup instructions.
3. Configure APIM Policy
After deploying APIM, configure the following policy to enable Azure AD token validation, managed identity authentication, and load balancing across multiple OpenAI backends:
<!-- Azure API Management Policy for OpenAI Endpoint -->
<!-- Implements Azure AD Token validation, managed identity authentication -->
<!-- Supports round-robin load balancing across multiple OpenAI backends -->
<!-- Requests with 'gpt-5' in the URL are routed to a single backend -->
<!-- The client application ID '04b07795-8ddb-461a-bbee-02f9e1bf7b46' is the official Azure CLI app registration -->
<!-- This policy allows requests authenticated by Azure CLI (az login) when the required claims are present -->
<policies>
<inbound>
<!-- IP Allow List Fragment (external fragment for client IP restrictions) -->
<include-fragment fragment-id="YourCompany-IPAllowList" />
<!-- Azure AD Token Validation for Azure CLI app ID -->
<validate-azure-ad-token tenant-id="YOUR-TENANT-ID-HERE" header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
<client-application-ids>
<application-id>04b07795-8ddb-461a-bbee-02f9e1bf7b46</application-id>
</client-application-ids>
<audiences>
<audience>api://YOUR-API-AUDIENCE-ID-HERE</audience>
</audiences>
<required-claims>
<claim name="roles" match="any">
<value>YourApp.User</value>
</claim>
</required-claims>
</validate-azure-ad-token>
<!-- Acquire Managed Identity access token for backend authentication -->
<authentication-managed-identity resource="https://cognitiveservices.azure.com" output-token-variable-name="managed-id-access-token" ignore-error="false" />
<!-- Set Authorization header for backend using the managed identity token -->
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + (string)context.Variables["managed-id-access-token"])</value>
</set-header>
<!-- Check if URL contains 'gpt-5' and set backend accordingly -->
<choose>
<when condition="@(context.Request.Url.Path.ToLower().Contains("gpt-5"))">
<set-variable name="selected-backend-url" value="https://your-region1-oai.openai.azure.com/openai" />
</when>
<otherwise>
<cache-lookup-value key="backend-counter" variable-name="backend-counter" />
<choose>
<when condition="@(context.Variables.ContainsKey("backend-counter") == false)">
<set-variable name="backend-counter" value="@(0)" />
</when>
</choose>
<set-variable name="current-backend-index" value="@((int)context.Variables["backend-counter"] % 7)" />
<choose>
<when condition="@((int)context.Variables["current-backend-index"] == 0)">
<set-variable name="selected-backend-url" value="https://your-region1-oai.openai.azure.com/openai" />
</when>
<when condition="@((int)context.Variables["current-backend-index"] == 1)">
<set-variable name="selected-backend-url" value="https://your-region2-oai.openai.azure.com/openai" />
</when>
<when condition="@((int)context.Variables["current-backend-index"] == 2)">
<set-variable name="selected-backend-url" value="https://your-region3-oai.openai.azure.com/openai" />
</when>
<when condition="@((int)context.Variables["current-backend-index"] == 3)">
<set-variable name="selected-backend-url" value="https://your-region4-oai.openai.azure.com/openai" />
</when>
<when condition="@((int)context.Variables["current-backend-index"] == 4)">
<set-variable name="selected-backend-url" value="https://your-region5-oai.openai.azure.com/openai" />
</when>
<when condition="@((int)context.Variables["current-backend-index"] == 5)">
<set-variable name="selected-backend-url" value="https://your-region6-oai.openai.azure.com/openai" />
</when>
<when condition="@((int)context.Variables["current-backend-index"] == 6)">
<set-variable name="selected-backend-url" value="https://your-region7-oai.openai.azure.com/openai" />
</when>
</choose>
<set-variable name="next-counter" value="@(((int)context.Variables["backend-counter"] + 1) % 1000)" />
<cache-store-value key="backend-counter" value="@((int)context.Variables["next-counter"])" duration="300" />
</otherwise>
</choose>
<!-- Always set backend service using selected-backend-url variable -->
<set-backend-service base-url="@((string)context.Variables["selected-backend-url"])" />
<!-- Inherit any base policies defined outside this section -->
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
This policy creates a secure gateway that validates Azure AD tokens from your local Azure CLI session, then uses APIM's managed identity to authenticate with Azure OpenAI backends, eliminating the need for API keys. It automatically load-balances requests across multiple Azure OpenAI regions using round-robin distribution for optimal performance.
4. Create Azure APIM proxy for Cline
This FastAPI-based proxy forwards OpenAI-compatible API requests from Cline through APIM using Azure AD authentication via Azure CLI credentials, eliminating the need to store or manage OpenAI API keys.
Prerequisites:
- Python 3.8 or higher
- Azure CLI (ensure az login has been run at least once)
Ensure the user running the proxy script has appropriate Azure AD roles and permissions. This script uses Azure CLI credentials to obtain bearer tokens. Your user account must have the correct roles assigned and access to the target API audience configured in the APIM policy above.
Quick setup for the proxy:
Create this requirements.txt:
fastapi
uvicorn
requests
azure-identity
Create this Python script for the proxy source code azure_proxy.py:
import os
import requests
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import uvicorn
from azure.identity import AzureCliCredential
# CONFIGURATION
APIM_BASE_URL = <APIM BASE URL HERE>
AZURE_SCOPE = <AZURE SCOPE HERE>
PORT = int(os.environ.get("PORT", 8080))
app = FastAPI()
credential = AzureCliCredential()
# Use a single requests.Session for connection pooling
from requests.adapters import HTTPAdapter
session = requests.Session()
session.mount("https://", HTTPAdapter(pool_connections=100, pool_maxsize=100))
import time
_cached_token = None
_cached_expiry = 0
def get_bearer_token(scope: str) -> str:
"""Get an access token using AzureCliCredential, caching until expiry is within 30 seconds."""
global _cached_token, _cached_expiry
now = int(time.time())
if _cached_token and (_cached_expiry - now > 30):
return _cached_token
try:
token_obj = credential.get_token(scope)
_cached_token = token_obj.token
_cached_expiry = token_obj.expires_on
return _cached_token
except Exception as e:
raise RuntimeError(f"Could not get Azure access token: {e}")
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
async def proxy(request: Request, path: str):
# Assemble the destination URL (preserve trailing slash logic)
dest_url = f"{APIM_BASE_URL.rstrip('/')}/{path}".rstrip("/")
if request.url.query:
dest_url += "?" + request.url.query
# Get the Bearer token
bearer_token = get_bearer_token(AZURE_SCOPE)
# Prepare headers (copy all, overwrite Authorization)
headers = dict(request.headers)
headers["Authorization"] = f"Bearer {bearer_token}"
headers.pop("host", None)
# Read body
body = await request.body()
# Send the request to APIM using the pooled session
resp = session.request(
method=request.method,
url=dest_url,
headers=headers,
data=body if body else None,
stream=True,
)
# Stream the response back to the client
return StreamingResponse(
resp.raw,
status_code=resp.status_code,
headers={k: v for k, v in resp.headers.items() if k.lower() != "transfer-encoding"},
)
if __name__ == "__main__":
# Bind the app to 127.0.0.1 to avoid any Firewall updates
uvicorn.run(app, host="127.0.0.1", port=PORT)
Run the setup:
pip install -r requirements.txt
az login # Authenticate with Azure
python azure_proxy.py
Configure Cline to use the proxy:
Using the OpenAI Compatible API Provider:
- Base URL: http://localhost:8080
- API Key: <any random string>
- Model ID: <your Azure OpenAI deployment name>
- API Version: <your Azure OpenAI deployment version>
The API key field is required by Cline but unused in our implementation - any random string works since authentication happens via Azure AD.
5. Configure Cline to listen to your MCP Server
Now that we have both our MCP server running and Cline configured with secure OpenAI access, the final step is connecting them together. To enable Cline to discover and use your custom tools, navigate to your installed MCP servers on Cline, select Configure MCP Servers, and add in the configuration for your server:
{
"mcpServers": {
"mcp-tools": {
"autoApprove": [
"EchoCaps",
"ReverseEcho", ],
"disabled": false,
"timeout": 60,
"type": "sse",
"url": "http://<your localhost url>/sse"
}
}
}
Now, you can use Cline's chat interface to interact with your secure MCP tools. Try asking Cline to use your custom tools - for example, "Can you echo 'Hello World' in capital letters?" and watch as it calls your local MCP server through the infrastructure you've built.
Conclusion
There you have it: A secure implementation of MCP that can be tailored to your specific use case. This approach gives you the power of MCP while maintaining enterprise security. You get:
- AI capabilities through secure Azure infrastructure.
- Custom tools that never leave your environment.
- Standard MCP interface for easy integration.
- Complete control over your data and tools.
The key is keeping MCP servers local while routing AI requests through your secure Azure infrastructure. This way, you gain MCP's benefits without compromising security.
Disclaimer
While this tutorial provides a secure foundation for MCP implementation, organizations are responsible for configuring their Azure resources according to their specific security requirements and compliance standards. Ensure proper review of network rules, access policies, and authentication configurations before deploying to production environments.
Resources
MCP SDKs and Tools:
Azure API Management Network Security:
- Azure API Management - restrict caller IPs
- Azure API Management with an Azure virtual network
- Set up inbound private endpoint for Azure API Management
Azure OpenAI and AI Services Network Security: