Building Custom MCP Servers on Azure

Building Custom MCP Servers on Azure

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.bicep

Implementing 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-node

Create 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.loginServer

Deployment 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: containerapp

Deploy the application:

# Initialize azd
azd init

# Login to Azure
azd auth login

# Provision and deploy
azd up

Building 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.json

Install dependencies in requirements.txt:

azure-functions
mcp

Implement 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/sse

Or 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 Systems

Best 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

Written by:

535 Posts

View All Posts
Follow Me :
How to whitelist website on AdBlocker?

How to whitelist website on AdBlocker?

  1. 1 Click on the AdBlock Plus icon on the top right corner of your browser
  2. 2 Click on "Enabled on this site" from the AdBlock Plus option
  3. 3 Refresh the page and start browsing the site