Large language models (LLMs) are revolutionizing the way we build software. But as the scope of intelligent applications grows, so does the need for structured, contextual communication between LLMs and real-world data, services, and business logic.
This is where the Model Context Protocol (MCP) comes in — a lightweight but powerful standard for exposing structured context and functional APIs to LLMs. Think of it as the REST for AI-native applications.
In this article, you’ll learn how to:
- Understand what MCP is and why it matters
- Build a custom MCP Server in TypeScript
- Host it in a Docker container
- Deploy it to Microsoft Azure using the Azure Developer CLI (azd)
- Extend the server with your own tools and data
We’ll use the excellent powergentic/azd-mcp-ts open-source template, from Powergentic.ai, as our base — a production-friendly scaffold for building MCP-compatible services.
Whether you’re building internal tools, AI copilots, or advanced chat workflows — this article will help you build the bridge between your data and the model.
What Is the Model Context Protocol (MCP)?
The Model Context Protocol (MCP) is an open, standardized way for large language models (LLMs) to interact with structured data, tools, and workflows provided by an external server. Instead of throwing everything into a giant text prompt, MCP gives LLMs well-defined interfaces — making it easier to build more powerful, predictable, and maintainable AI-driven applications.
MCP is used by clients like Claude Desktop, in-app LLM agents, and even automated orchestration systems. It enables composability between tools, safe access to structured data, and stronger interaction patterns between LLMs and apps.
In simple terms, MCP is like an API for LLMs, but purpose-built for their unique needs.
Why Was MCP Created?
Traditional LLM prompting relies on stuffing context (like documents, data, and instructions) into a single, unstructured input. As your app grows, this becomes brittle and hard to scale.
MCP solves this by:
- Separating content from control – your app manages what data/tools are exposed, and the LLM simply consumes them
- Encouraging composability – you can build reusable interfaces to structured information and actions
- Improving safety and auditability – every tool call and resource read is trackable
This model is already influencing advanced LLM platforms like Claude Desktop, multi-agent frameworks, and autonomous agents.
MCP Core Primitives
At the heart of MCP are three primitive building blocks that your server can implement:
| 🔹 Primitive | 📋 Description | 🧑💻 Analogy |
|---|---|---|
| Resource | Read-only, contextual data the model can query. Think files, config, schemas, user profiles, etc. | GET endpoint (REST) |
| Tool | Actionable functions that the model can invoke. They may trigger side effects, compute values, or call external services. | POST endpoint (REST) |
| Prompt | Reusable message templates that shape how the LLM responds. These can be invoked by the user or triggered programmatically. | Slash command / macro |
These primitives are designed to support LLM-native use cases, where understanding, decision-making, and interaction are central to the app’s functionality.
How MCP Works at Runtime
Here’s what a typical MCP interaction looks like:
- A client (like Claude Desktop or a custom frontend) connects to your MCP server via Server-Sent Events (SSE).
- The server advertises what resources, tools, and prompts it supports.
- The LLM (or user) requests a resource like
greeting://alice, or calls a tool liketools://calculate-bmi. - Your server returns the requested data or executes the tool, streaming the response back to the client.
This approach gives you:
- Real-time communication via SSE
- Declarative descriptions of what your server offers
- A clear separation of roles between server logic and LLM usage
Benefits of Using MCP
Here are several of the benefits of using Model Context Protocol (MCP) servers with your LLM / AI Agent solution:
- ✅ Better control over what data and actions an LLM can access
- 🧱 Modular server design using tools, resources, and prompts
- 🛡️ Safer and more auditable than arbitrary code generation
- 🔄 Composable across clients — use the same MCP server in Claude Desktop, your internal chatbots, or custom LLM agents
- 🌐 Language-agnostic — servers can be written in Python, TypeScript, or any language with an SDK
Think of it like building an API for your LLM — but way more tailored for how language models consume information and execute tasks.
Why Use TypeScript to Build an MCP Server?
TypeScript is a natural choice for building MCP servers. Here’s why:
- ✅ Strong typing makes your tool/resource definitions safer and easier to maintain.
- ⚡ Fast iteration with familiar tooling (Node.js, npm, etc.)
- 🧩 The official @modelcontextprotocol/sdk is built for modern TypeScript workflows.
And with the powergentic/azd-mcp-ts template, you get a ready-to-run project scaffold that uses:
- Express.js for HTTP + Server-Sent Events (SSE)
- Docker for consistent builds
- azd for seamless Azure deployment
At the time or writing this, the Model Context Protocol (MCP) and it’s SDKs include more examples and better documentation around using TypeScript and Node.js for building MCP servers. You can also check out the modelcontextprotocol/server project on GitHub for a ton of great MCP server examples.
Inside the powergentic/azd-mcp-ts Template
Let’s look at the powergentic/azd-mcp-ts project layout you can use as the base foundation for building your own MCP servers using TypeScript, Docker and Azure Container Apps (ACA):
azd-mcp-ts/
├── src/
│ └── mcpserver/
│ └── server.ts # Main MCP server definition
├── infra/ # Infrastructure-as-code for Azure
├── Dockerfile # Docker image for local + cloud use
├── azure.yaml # azd metadata config
└── package.json
The heart of the server lives in src/mcpserver/server.ts, which uses the MCP SDK to expose a resource and wire up the transport layer.
Here’s a very simplified version of the server code (see the project for the full code):
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
const server = new McpServer({ name: "Demo", version: "1.0.0" });
server.resource(
"greeting",
new ResourceTemplate("greeting://{name}", { list: undefined }),
async (uri, { name }) => ({
contents: [{ uri: uri.href, text: `Hello, ${name}!` }]
})
);
const app = express();
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
await server.connect(transport);
});
This defines a dynamic MCP resource that returns personalized greetings when the LLM queries greeting://your-name.
What Is SSE (Server-Send Events) and Why Does MCP Use It?
One of the most important technical underpinnings of the Model Context Protocol is its use of Server-Sent Events (SSE) for real-time, event-driven communication between your MCP server and clients (like Claude Desktop or an LLM app).
Let’s break down what SSE is, how it compares to alternatives, and why it’s such a natural fit for MCP.
What Are Server-Sent Events (SSE)?
SSE is a web technology that allows a server to push updates to a client over a single, long-lived HTTP connection. It’s part of the HTML5 standard and works over plain HTTP/1.1, making it widely supported and easy to implement.
Key properties:
- One-way communication: server → client
- Streamed as
text/event-stream - Reconnection and heartbeat built in
- Works great for streaming logs, updates, or – in the case of MCP – model responses
In contrast, traditional HTTP is request-response. SSE lets the server proactively send new information as it becomes available.
-
The Client Connects
The client (e.g. Claude Desktop) opens a persistent connection to the server’s
/sseendpoint using HTTP and starts listening for events.GET /sse HTTP/1.1 Accept: text/event-streamYour MCP server responds and begins streaming messages:
Content-Type: text/event-stream event: resourceUpdate data: {"uri": "greeting://alice", "text": "Hello, Alice!"} event: toolResponse data: {"tool": "calculate-bmi", "result": "22.4"} -
Server Streams Responses
As your server receives and handles requests — like reading a resource or executing a tool — it streams back events over the open SSE channel. These events are structured using the MCP message protocol.
That might include:
-
Results from a tool invocation
-
Errors or status updates
-
Output from a long-running task
-
Progress updates during file processing
-
Content from a streaming model output
-
-
Client Sends Request Separately
The client sends requests (like calling a tool or reading a resource) via a separate
/messagesendpoint, typically using HTTP POST.This separation of concerns — read (SSE) vs write (POST) — helps keep the protocol simple and reliable. It’s also well-suited for environments like Azure Container Apps, which support HTTP-based communication out of the box.

Why SSE instead of WebSockets?
At first glance, you might wonder why the Model Context Protocol doesn’t use WebSockets, which are a more common choice for real-time communication in modern web apps. After all, WebSockets offer full-duplex (two-way) messaging and are popular in chat apps, multiplayer games, and collaborative tools.
But MCP has a different set of priorities — simplicity, compatibility, and reliability in cloud-native environments. For the types of interactions that LLMs require, Server-Sent Events (SSE) offers a better balance of performance and practicality.
Here’s a closer comparison between SSE and WebSockets:
| Feature | SSE | WebSocket |
|---|---|---|
| Protocol | HTTP/1.1 (text/event-stream) |
Custom TCP protocol |
| Direction | One-way (server → client) | Two-way |
| Complexity | Simple | More complex to manage |
| HTTP-compatible | ✅ Yes | ❌ Requires upgrade |
| Cloud support | ✅ Yes | ❌ Not always supported |
| Ideal for… | Real-time updates, streaming | Games, chat apps, full duplex scenarios |
MCP’s use case is mostly server-push – streaming data and updates to LLM clients. So SSE is simpler, more compatible, and gets the job done.
Containerizing Your MCP Server with Docker
Before deploying to the cloud, we need a consistent runtime environment — enter Docker.
The powergentic/azd-mcp-ts template includes a preconfigured Dockerfile that packages the MCP server into a lean container. Here’s what it does at a high level:
# 1. Use a minimal Node.js base image
FROM node:20-slim
# 2. Set working directory
WORKDIR /app
# 3. Copy dependencies and install
COPY package*.json ./
RUN npm install --production
# 4. Copy source code
COPY . .
# 5. Expose port and run the server
EXPOSE 3000
CMD ["npm", "start"]
Here’s the docker commands to build and test the container locally:
docker build -t mcp-server .
docker run -p 3000:3000 mcp-server
Once built, this image can be:
- ✅ Run locally for development
- 🚀 Deployed to a Docker host; like Azure Container Apps or Kubernetes
Deploying MCP Server to Azure with the Azure Developer CLI (azd)
The Azure Developer CLI (azd) is a modern developer experience that simplifies deploying full-stack apps to Azure. It uses convention over configuration and supports Infra-as-Code out of the box.
With the powergentic/azd-mcp-ts Azure Developer CLI template, you can get started and deploy your own custom Model Context Protocol (MCP) server with just a few commands:
-
Create a new folder for your
azdproject:mkdir mcp-server cd mcp-server -
Initialize the project from the template:
azd init --template powergentic/azd-mcp-ts -
Login to Azure:
azd auth login -
Deploy the MCP Server:
azd upThis step does the following:
- Builds Docker image
- Provisions Azure Container Registry, Azure Container Apps, Log Analytics, and a managed environment
- Deploys your MCP server behind a public HTTPS endpoint
🎉 Your MCP Server is live and ready to connect with clients like Claude Desktop or a custom MCP client.
Extending the MCP Server with Tools and Prompts
With your server deployed, it’s time to make it your own.
Let’s add a new tool that calculates BMI (Body Mass Index):
Edit src/mcpserver/server.ts
import { z } from "zod";
// Add this after your greeting resource
server.tool(
"calculate-bmi",
{
weightKg: z.number(),
heightM: z.number()
},
async ({ weightKg, heightM }) => ({
content: [{
type: "text",
text: `Your BMI is ${(weightKg / (heightM * heightM)).toFixed(2)}`
}]
})
);
This exposes a tool that can be called by an LLM or agent when integrated with your MCP server via:
tools://calculate-bmi
Wrapping Up: The Power of MCP + TypeScript + Azure
In this article, you learned how to:
✅ Understand what Model Context Protocol (MCP) is and why it matters
✅ Use TypeScript and the official SDK to define MCP resources and tools
✅ Build a lightweight MCP Server with real-time communication via SSE
✅ Package it into a Docker container
✅ Deploy it to Azure using the Azure Developer CLI (azd)
✅ Customize the server with your own logic, endpoints, and context
This will give you the foundation for building your own production-grade Model Context Protocol (MCP) server for extending your AI Agent with additional functionality!
Original Article Source: How to Build and Deploy an MCP Server with TypeScript and Azure Developer CLI (azd) using Azure Container Apps and Docker written by Chris Pietschmann (If you're reading this somewhere other than Build5Nines.com, it was republished without permission.)
Implementing Azure Naming Conventions at Scale with Terraform and Build5Nines/naming/azure (AzureRM + Region Pairs)
Create Azure Architecture Diagrams with Microsoft Visio
Microsoft Azure Regions: Interactive Map of Global Datacenters
New Book: Build and Deploy Apps using Azure Developer CLI by Chris Pietschmann
Prompt Noise Is Killing Your AI Accuracy: How to Optimize Context for Grounded Output



Great article Chris..
How does it work when we’ve multiple mcp server pods running? Since for every tool call it makes a new http request, how is it guaranteed that it’ll reach the same pod?
Excellent question. The current state of the MCP SDK does not include built-in support for multi-instance MCP servers. As a result, you’ll need to either have a single pod, or implement session affinity so the `/messages` calls go to the same pod the client has an active session open on the `/sse` endpoint.