In the previous posts, we explored the Model Context Protocol as a universal standard and examined how Microsoft’s Azure MCP Server enables AI agents to interact with Azure resources. Now we move into building your own custom MCP servers on Azure. This is where the protocol becomes truly powerful, enabling you to expose your proprietary business logic, internal APIs, and specialized workflows to AI agents through a standardized interface.
Building custom MCP servers allows organizations to extend AI capabilities beyond generic tools. Whether you need to integrate with legacy systems, provide access to proprietary databases, or expose complex business workflows, custom MCP servers give you complete control while maintaining compatibility with any MCP-compatible client.
Choosing Your Deployment Platform
Azure offers two primary platforms for deploying MCP servers, each with distinct characteristics that make them suitable for different scenarios.
Azure Container Apps
Azure Container Apps provides a fully managed serverless container platform optimized for microservices and event-driven architectures. It excels at running MCP servers that require:
- Long-running processes: Servers that maintain persistent connections or handle streaming responses
- Custom runtime environments: Any containerized application regardless of language or framework
- Multi-container deployments: Scenarios where multiple MCP servers need to work together
- Horizontal scaling: Automatic scaling based on HTTP requests, CPU, memory, or custom metrics
- Advanced networking: Integration with virtual networks, private endpoints, and service mesh capabilities
Azure Functions
Azure Functions offers a consumption-based serverless computing platform perfect for event-driven MCP servers. It works best when:
- Pay-per-execution matters: You only pay for actual function executions, not idle time
- Quick development cycles: Faster iteration with less infrastructure code
- Integrated tooling: Strong IDE support with debugging and local testing
- Simplified deployment: Direct code deployment without container management
- Built-in integrations: Native bindings to Azure services like Storage, Cosmos DB, and Service Bus
Building an MCP Server with Azure Container Apps
Let’s build a complete MCP server using TypeScript and deploy it to Azure Container Apps. This server will expose tools for managing customer data in a fictional CRM system.
Project Structure
Create the following project structure:
crm-mcp-server/
├── src/
│ ├── index.ts
│ ├── server.ts
│ ├── tools/
│ │ ├── customer-tools.ts
│ │ └── order-tools.ts
│ └── db/
│ └── customer-db.ts
├── Dockerfile
├── package.json
├── tsconfig.json
└── infra/
└── main.bicepImplementing the MCP Server
First, install the required dependencies:
npm init -y
npm install @modelcontextprotocol/sdk express sqlite3
npm install -D @types/node @types/express typescript ts-nodeCreate the main server implementation in src/server.ts:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { CustomerDatabase } from "./db/customer-db.js";
export class CRMServer {
private server: Server;
private db: CustomerDatabase;
constructor() {
this.server = new Server(
{
name: "crm-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.db = new CustomerDatabase();
this.setupHandlers();
}
private setupHandlers() {
// Handle tool list requests
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_customer",
description: "Retrieve customer details by ID",
inputSchema: {
type: "object",
properties: {
customer_id: {
type: "string",
description: "The customer's unique identifier",
},
},
required: ["customer_id"],
},
},
{
name: "create_customer",
description: "Create a new customer record",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Customer name",
},
email: {
type: "string",
description: "Customer email address",
},
company: {
type: "string",
description: "Company name",
},
},
required: ["name", "email"],
},
},
{
name: "search_customers",
description: "Search for customers by name or email",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query",
},
},
required: ["query"],
},
},
{
name: "update_customer",
description: "Update customer information",
inputSchema: {
type: "object",
properties: {
customer_id: {
type: "string",
description: "Customer ID to update",
},
updates: {
type: "object",
description: "Fields to update",
},
},
required: ["customer_id", "updates"],
},
},
],
};
});
// Handle tool execution requests
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "get_customer": {
const customer = await this.db.getCustomer(args.customer_id as string);
return {
content: [
{
type: "text",
text: JSON.stringify(customer, null, 2),
},
],
};
}
case "create_customer": {
const newCustomer = await this.db.createCustomer({
name: args.name as string,
email: args.email as string,
company: args.company as string,
});
return {
content: [
{
type: "text",
text: `Customer created successfully: ${JSON.stringify(newCustomer, null, 2)}`,
},
],
};
}
case "search_customers": {
const results = await this.db.searchCustomers(args.query as string);
return {
content: [
{
type: "text",
text: `Found ${results.length} customers:\n${JSON.stringify(results, null, 2)}`,
},
],
};
}
case "update_customer": {
const updated = await this.db.updateCustomer(
args.customer_id as string,
args.updates as Record
);
return {
content: [
{
type: "text",
text: `Customer updated: ${JSON.stringify(updated, null, 2)}`,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
isError: true,
};
}
});
}
async start() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("CRM MCP Server running on stdio");
}
}
export async function main() {
const server = new CRMServer();
await server.start();
}
if (require.main === module) {
main().catch(console.error);
} Implement the database layer in src/db/customer-db.ts:
import sqlite3 from "sqlite3";
import { promisify } from "util";
interface Customer {
id: string;
name: string;
email: string;
company?: string;
created_at: string;
updated_at: string;
}
export class CustomerDatabase {
private db: sqlite3.Database;
constructor() {
this.db = new sqlite3.Database(":memory:");
this.initialize();
}
private initialize() {
this.db.serialize(() => {
this.db.run(`
CREATE TABLE IF NOT EXISTS customers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
company TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`);
// Insert sample data
const sampleCustomers = [
{
id: "cust-001",
name: "John Doe",
email: "john@example.com",
company: "Acme Corp",
},
{
id: "cust-002",
name: "Jane Smith",
email: "jane@example.com",
company: "Tech Solutions",
},
];
const now = new Date().toISOString();
const stmt = this.db.prepare(`
INSERT INTO customers (id, name, email, company, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
`);
sampleCustomers.forEach((customer) => {
stmt.run(customer.id, customer.name, customer.email, customer.company, now, now);
});
stmt.finalize();
});
}
async getCustomer(id: string): Promise {
return new Promise((resolve, reject) => {
this.db.get(
"SELECT * FROM customers WHERE id = ?",
[id],
(err, row: Customer) => {
if (err) reject(err);
else resolve(row || null);
}
);
});
}
async createCustomer(data: {
name: string;
email: string;
company?: string;
}): Promise {
const id = `cust-${Date.now()}`;
const now = new Date().toISOString();
return new Promise((resolve, reject) => {
this.db.run(
`INSERT INTO customers (id, name, email, company, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)`,
[id, data.name, data.email, data.company || null, now, now],
function (err) {
if (err) reject(err);
else
resolve({
id,
name: data.name,
email: data.email,
company: data.company,
created_at: now,
updated_at: now,
});
}
);
});
}
async searchCustomers(query: string): Promise {
return new Promise((resolve, reject) => {
this.db.all(
`SELECT * FROM customers
WHERE name LIKE ? OR email LIKE ? OR company LIKE ?`,
[`%${query}%`, `%${query}%`, `%${query}%`],
(err, rows: Customer[]) => {
if (err) reject(err);
else resolve(rows);
}
);
});
}
async updateCustomer(
id: string,
updates: Record
): Promise {
const fields = Object.keys(updates)
.filter((key) => ["name", "email", "company"].includes(key))
.map((key) => `${key} = ?`)
.join(", ");
if (!fields) {
throw new Error("No valid fields to update");
}
const values = Object.keys(updates)
.filter((key) => ["name", "email", "company"].includes(key))
.map((key) => updates[key]);
const now = new Date().toISOString();
values.push(now, id);
return new Promise((resolve, reject) => {
this.db.run(
`UPDATE customers SET ${fields}, updated_at = ? WHERE id = ?`,
values,
function (err) {
if (err) reject(err);
else resolve(this.changes > 0 ? { id, ...updates } as Customer : null);
}
);
});
}
} Containerizing the Application
Create a Dockerfile for the MCP server:
FROM node:20-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY tsconfig.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY src ./src
# Build TypeScript
RUN npm run build
# Expose port for HTTP transport (optional)
EXPOSE 3000
# Run the MCP server
CMD ["node", "dist/index.js"]Azure Infrastructure with Bicep
Create the infrastructure definition in infra/main.bicep:
@description('The location for all resources')
param location string = resourceGroup().location
@description('The name of the container app environment')
param environmentName string = 'mcp-env-${uniqueString(resourceGroup().id)}'
@description('The name of the container app')
param containerAppName string = 'crm-mcp-server'
@description('Container registry name')
param containerRegistryName string = 'mcpregistry${uniqueString(resourceGroup().id)}'
// Log Analytics Workspace for monitoring
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: 'mcp-logs-${uniqueString(resourceGroup().id)}'
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 30
}
}
// Container Registry
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = {
name: containerRegistryName
location: location
sku: {
name: 'Basic'
}
properties: {
adminUserEnabled: true
}
}
// Container Apps Environment
resource containerAppEnv 'Microsoft.App/managedEnvironments@2023-05-01' = {
name: environmentName
location: location
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalytics.properties.customerId
sharedKey: logAnalytics.listKeys().primarySharedKey
}
}
}
}
// Container App
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
name: containerAppName
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
managedEnvironmentId: containerAppEnv.id
configuration: {
ingress: {
external: true
targetPort: 3000
transport: 'http'
allowInsecure: false
}
registries: [
{
server: containerRegistry.properties.loginServer
username: containerRegistry.listCredentials().username
passwordSecretRef: 'registry-password'
}
]
secrets: [
{
name: 'registry-password'
value: containerRegistry.listCredentials().passwords[0].value
}
]
}
template: {
containers: [
{
name: 'crm-mcp-server'
image: '${containerRegistry.properties.loginServer}/crm-mcp-server:latest'
resources: {
cpu: json('0.5')
memory: '1Gi'
}
}
]
scale: {
minReplicas: 1
maxReplicas: 10
rules: [
{
name: 'http-rule'
http: {
metadata: {
concurrentRequests: '100'
}
}
}
]
}
}
}
}
output containerAppUrl string = 'https://${containerApp.properties.configuration.ingress.fqdn}'
output containerRegistryLoginServer string = containerRegistry.properties.loginServerDeployment with Azure Developer CLI
Create an azure.yaml file for azd:
name: crm-mcp-server
metadata:
template: crm-mcp-server@0.0.1
services:
web:
project: .
language: ts
host: containerappDeploy the application:
# Initialize azd
azd init
# Login to Azure
azd auth login
# Provision and deploy
azd upBuilding an MCP Server with Azure Functions
Now let’s create an equivalent MCP server using Azure Functions with Python. This approach offers simpler deployment and consumption-based pricing.
Python Function Implementation
Create the project structure:
crm-mcp-function/
├── function_app.py
├── requirements.txt
└── host.jsonInstall dependencies in requirements.txt:
azure-functions
mcpImplement the MCP server in function_app.py:
import azure.functions as func
import logging
import json
from typing import Dict, Any, List
app = func.FunctionApp()
# In-memory customer database
customers_db = {
"cust-001": {
"id": "cust-001",
"name": "John Doe",
"email": "john@example.com",
"company": "Acme Corp"
},
"cust-002": {
"id": "cust-002",
"name": "Jane Smith",
"email": "jane@example.com",
"company": "Tech Solutions"
}
}
@app.function_name(name="GetCustomer")
@app.mcp_tool_trigger(
arg_name="req",
name="get_customer",
description="Retrieve customer details by ID"
)
async def get_customer(req: func.HttpRequest) -> func.HttpResponse:
"""
Get customer by ID
Parameters:
- customer_id (string): The customer's unique identifier
"""
try:
params = req.get_json()
customer_id = params.get("customer_id")
if not customer_id:
return func.HttpResponse(
json.dumps({"error": "customer_id is required"}),
status_code=400,
mimetype="application/json"
)
customer = customers_db.get(customer_id)
if not customer:
return func.HttpResponse(
json.dumps({"error": f"Customer {customer_id} not found"}),
status_code=404,
mimetype="application/json"
)
return func.HttpResponse(
json.dumps(customer),
mimetype="application/json"
)
except Exception as e:
logging.error(f"Error in get_customer: {str(e)}")
return func.HttpResponse(
json.dumps({"error": str(e)}),
status_code=500,
mimetype="application/json"
)
@app.function_name(name="CreateCustomer")
@app.mcp_tool_trigger(
arg_name="req",
name="create_customer",
description="Create a new customer record"
)
async def create_customer(req: func.HttpRequest) -> func.HttpResponse:
"""
Create a new customer
Parameters:
- name (string): Customer name
- email (string): Customer email address
- company (string, optional): Company name
"""
try:
params = req.get_json()
name = params.get("name")
email = params.get("email")
company = params.get("company", "")
if not name or not email:
return func.HttpResponse(
json.dumps({"error": "name and email are required"}),
status_code=400,
mimetype="application/json"
)
# Generate new customer ID
customer_id = f"cust-{len(customers_db) + 1:03d}"
new_customer = {
"id": customer_id,
"name": name,
"email": email,
"company": company
}
customers_db[customer_id] = new_customer
return func.HttpResponse(
json.dumps(new_customer),
mimetype="application/json"
)
except Exception as e:
logging.error(f"Error in create_customer: {str(e)}")
return func.HttpResponse(
json.dumps({"error": str(e)}),
status_code=500,
mimetype="application/json"
)
@app.function_name(name="SearchCustomers")
@app.mcp_tool_trigger(
arg_name="req",
name="search_customers",
description="Search for customers by name or email"
)
async def search_customers(req: func.HttpRequest) -> func.HttpResponse:
"""
Search customers
Parameters:
- query (string): Search query
"""
try:
params = req.get_json()
query = params.get("query", "").lower()
if not query:
return func.HttpResponse(
json.dumps({"error": "query is required"}),
status_code=400,
mimetype="application/json"
)
results = [
customer for customer in customers_db.values()
if query in customer["name"].lower() or
query in customer["email"].lower() or
query in customer.get("company", "").lower()
]
return func.HttpResponse(
json.dumps({
"count": len(results),
"customers": results
}),
mimetype="application/json"
)
except Exception as e:
logging.error(f"Error in search_customers: {str(e)}")
return func.HttpResponse(
json.dumps({"error": str(e)}),
status_code=500,
mimetype="application/json"
)
@app.function_name(name="UpdateCustomer")
@app.mcp_tool_trigger(
arg_name="req",
name="update_customer",
description="Update customer information"
)
async def update_customer(req: func.HttpRequest) -> func.HttpResponse:
"""
Update customer
Parameters:
- customer_id (string): Customer ID to update
- updates (object): Fields to update
"""
try:
params = req.get_json()
customer_id = params.get("customer_id")
updates = params.get("updates", {})
if not customer_id:
return func.HttpResponse(
json.dumps({"error": "customer_id is required"}),
status_code=400,
mimetype="application/json"
)
if customer_id not in customers_db:
return func.HttpResponse(
json.dumps({"error": f"Customer {customer_id} not found"}),
status_code=404,
mimetype="application/json"
)
# Update customer
customer = customers_db[customer_id]
for key, value in updates.items():
if key in ["name", "email", "company"]:
customer[key] = value
return func.HttpResponse(
json.dumps(customer),
mimetype="application/json"
)
except Exception as e:
logging.error(f"Error in update_customer: {str(e)}")
return func.HttpResponse(
json.dumps({"error": str(e)}),
status_code=500,
mimetype="application/json"
)Configure host.json:
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"maxTelemetryItemsPerSecond": 20
}
}
},
"extensions": {
"mcp": {
"serverInfo": {
"name": "crm-mcp-server",
"version": "1.0.0"
}
}
}
}Deployment to Azure Functions
Deploy using Azure Developer CLI:
# Initialize the project
azd init --template remote-mcp-functions-python
# Deploy to Azure
azd up
# Get the function endpoint
az functionapp show --name --resource-group --query "defaultHostName" -o tsv Implementing Security
Security is critical for production MCP servers. Let’s implement comprehensive security measures.
API Key Authentication
For Azure Functions, API keys are built-in. Retrieve the system key:
az functionapp keys list \
--resource-group \
--name \
--query "systemKeys.mcp_extension" -o tsv Configure your MCP client to use the key:
{
"mcpServers": {
"crm-server": {
"type": "sse",
"url": "https://.azurewebsites.net/runtime/webhooks/mcp/sse",
"headers": {
"x-functions-key": "your-system-key-here"
}
}
}
} Microsoft Entra ID Authentication
For Container Apps, implement OAuth with Entra ID. Add authentication middleware:
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";
const client = jwksClient({
jwksUri: `https://login.microsoftonline.com/${process.env.TENANT_ID}/discovery/v2.0/keys`,
});
function getKey(header: any, callback: any) {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
callback(err);
} else {
const signingKey = key?.getPublicKey();
callback(null, signingKey);
}
});
}
export async function validateToken(
req: Request,
res: Response,
next: NextFunction
) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ error: "No token provided" });
}
const token = authHeader.substring(7);
jwt.verify(
token,
getKey,
{
audience: process.env.CLIENT_ID,
issuer: `https://sts.windows.net/${process.env.TENANT_ID}/`,
algorithms: ["RS256"],
},
(err, decoded) => {
if (err) {
return res.status(401).json({ error: "Invalid token" });
}
req.user = decoded;
next();
}
);
}Monitoring and Observability
Implement comprehensive monitoring for your MCP servers using Azure Monitor and Application Insights.
Application Insights Integration
For Node.js applications, add telemetry:
import { TelemetryClient } from "applicationinsights";
const appInsights = require("applicationinsights");
appInsights.setup(process.env.APPLICATIONINSIGHTS_CONNECTION_STRING);
appInsights.start();
const client = new TelemetryClient();
// Track custom events
export function trackToolExecution(toolName: string, duration: number, success: boolean) {
client.trackEvent({
name: "ToolExecution",
properties: {
toolName,
success: success.toString(),
},
measurements: {
duration,
},
});
}
// Track dependencies
export function trackDependency(name: string, duration: number) {
client.trackDependency({
name,
data: name,
duration,
success: true,
resultCode: 200,
});
}Structured Logging
Implement structured logging for better queryability:
import winston from "winston";
const logger = winston.createLogger({
level: "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
],
});
export function logToolExecution(
toolName: string,
args: any,
userId: string,
duration: number
) {
logger.info("Tool executed", {
toolName,
arguments: JSON.stringify(args),
userId,
duration,
timestamp: new Date().toISOString(),
});
}C# Implementation for Azure Functions
For .NET developers, here is a complete C# implementation:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using System.Net;
using System.Text.Json;
namespace CrmMcpServer
{
public class CustomerFunctions
{
private readonly ILogger _logger;
private static Dictionary _customers = new()
{
["cust-001"] = new Customer
{
Id = "cust-001",
Name = "John Doe",
Email = "john@example.com",
Company = "Acme Corp"
},
["cust-002"] = new Customer
{
Id = "cust-002",
Name = "Jane Smith",
Email = "jane@example.com",
Company = "Tech Solutions"
}
};
public CustomerFunctions(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger();
}
[Function("GetCustomer")]
[McpToolsTrigger(
Name = "get_customer",
Description = "Retrieve customer details by ID"
)]
public async Task GetCustomer(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
try
{
var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var data = JsonSerializer.Deserialize>(requestBody);
if (!data.TryGetValue("customer_id", out var customerId))
{
var errorResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await errorResponse.WriteAsJsonAsync(new { error = "customer_id is required" });
return errorResponse;
}
if (_customers.TryGetValue(customerId, out var customer))
{
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(customer);
return response;
}
var notFoundResponse = req.CreateResponse(HttpStatusCode.NotFound);
await notFoundResponse.WriteAsJsonAsync(new { error = $"Customer {customerId} not found" });
return notFoundResponse;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in GetCustomer");
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteAsJsonAsync(new { error = ex.Message });
return errorResponse;
}
}
[Function("CreateCustomer")]
[McpToolsTrigger(
Name = "create_customer",
Description = "Create a new customer record"
)]
public async Task CreateCustomer(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
try
{
var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var data = JsonSerializer.Deserialize(requestBody);
if (string.IsNullOrEmpty(data.Name) || string.IsNullOrEmpty(data.Email))
{
var errorResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await errorResponse.WriteAsJsonAsync(new { error = "name and email are required" });
return errorResponse;
}
var customerId = $"cust-{_customers.Count + 1:D3}";
var newCustomer = new Customer
{
Id = customerId,
Name = data.Name,
Email = data.Email,
Company = data.Company
};
_customers[customerId] = newCustomer;
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(newCustomer);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in CreateCustomer");
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteAsJsonAsync(new { error = ex.Message });
return errorResponse;
}
}
}
public record Customer
{
public string Id { get; init; }
public string Name { get; init; }
public string Email { get; init; }
public string Company { get; init; }
}
public record CreateCustomerRequest
{
public string Name { get; init; }
public string Email { get; init; }
public string Company { get; init; }
}
} Testing Your MCP Server
Use the MCP Inspector tool to test your server:
npx @modelcontextprotocol/inspector https://your-server-url/sseOr test with GitHub Copilot by configuring your mcp.json and using natural language prompts:
Create a new customer named "Alice Johnson" with email alice@example.com from Tech Innovations
Search for customers from Acme Corp
Update customer cust-001 to change their company to Global SystemsBest Practices for Production
- Version your APIs: Include version numbers in your tool names or use API versioning
- Implement rate limiting: Protect your server from abuse using Azure API Management or application-level throttling
- Use connection pooling: For database connections, implement proper pooling to handle concurrent requests
- Cache responses: Implement caching for frequently accessed data to reduce backend load
- Validate inputs rigorously: Never trust client input, always validate and sanitize
- Handle errors gracefully: Return meaningful error messages that help clients recover
- Monitor performance: Track tool execution times, error rates, and resource utilization
- Document your tools: Provide clear descriptions and examples in your tool schemas
Looking Ahead
You now have the foundation for building custom MCP servers on Azure using both Container Apps and Functions. In the next post, we will explore how to integrate these custom servers with Azure AI Agent Service, enabling sophisticated multi-agent workflows that can orchestrate complex business processes.
We will cover agent-to-agent communication, context sharing between agents, implementing decision trees for agent routing, and building production-grade agentic systems that leverage MCP for tool access.
References
- Microsoft Learn – Build a TypeScript MCP server using Azure Container Apps (https://learn.microsoft.com/en-us/azure/developer/ai/build-mcp-server-ts)
- DEV Community – Building Remote MCP Servers with .NET and Azure Container Apps (https://dev.to/willvelida/building-remote-mcp-servers-with-net-and-azure-container-apps-cc2)
- Microsoft Community Hub – Host remote MCP servers in Azure Container Apps (https://techcommunity.microsoft.com/blog/appsonazureblog/host-remote-mcp-servers-in-azure-container-apps/4403550)
- Stochastic Coder – Deploying MCP Servers with Azure Container Apps (https://stochasticcoder.com/2025/04/29/deploying-mcp-servers-with-azure-container-apps/)
- Microsoft Learn – Build a .NET OpenAI Agent using an MCP server on Azure Container Apps (https://learn.microsoft.com/en-us/azure/developer/ai/build-openai-mcp-server-dotnet)
- Microsoft Learn – Remote MCP with Azure Container Apps (Node.js/TypeScript) (https://learn.microsoft.com/en-us/samples/azure-samples/mcp-container-ts/mcp-container-ts/)
- Microsoft Community Hub – Deploying MCP Server Using Azure Container Apps (https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/deploying-mcp-server-using-azure-container-apps/4426401)
- Build5Nines – How To Build And Deploy An MCP Server With TypeScript And Azure Developer CLI (https://build5nines.com/how-to-build-and-deploy-an-mcp-server-with-typescript-and-azure-developer-cli-azd-using-azure-container-apps-and-docker/)
- GitHub – Azure Container Apps MCP Sample (TypeScript) (https://github.com/Azure-Samples/mcp-container-ts)
- Microsoft Learn – Deploy Tools – Azure MCP Server (https://learn.microsoft.com/en-us/azure/developer/azure-mcp-server/tools/azure-deploy)
- Microsoft Learn – Remote MCP with Azure Functions (.NET/C#) (https://learn.microsoft.com/en-us/samples/azure-samples/remote-mcp-functions-dotnet/remote-mcp-functions-dotnet/)
- .NET Blog – Build MCP Remote Servers with Azure Functions (https://devblogs.microsoft.com/dotnet/build-mcp-remote-servers-with-azure-functions/)
- GitHub – Remote MCP Functions .NET (https://github.com/Azure-Samples/remote-mcp-functions-dotnet)
- Microsoft Learn – Build a custom remote MCP server using Azure Functions (https://learn.microsoft.com/en-us/azure/azure-functions/scenario-custom-remote-mcp-server)
- Apidog – How to Use Azure Functions MCP Servers (https://apidog.com/blog/azure-functions-mcp-servers/)
- Microsoft Learn – Remote MCP with Azure Functions (Python) (https://learn.microsoft.com/en-us/samples/azure-samples/remote-mcp-functions-python/remote-mcp-functions-python/)
- Microsoft Learn – Tutorial: Host an MCP server on Azure Functions (https://learn.microsoft.com/en-us/azure/azure-functions/functions-mcp-tutorial)
- Microsoft Learn – Remote MCP with Azure Functions (Node.js/TypeScript) (https://learn.microsoft.com/en-us/samples/azure-samples/remote-mcp-functions-typescript/remote-mcp-functions-typescript/)
- Microsoft Learn – Model context protocol bindings for Azure Functions (https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-mcp)
