Deploy an MCP Server on Kubernetes Engine
Deploy a Model Context Protocol (MCP) Server on KakaoCloud Kubernetes Engine and integrate it with AI applications.
- Estimated time: 45 minutes
- Recommended OS: macOS, Ubuntu
- Prerequisites
- IAM access key
- Install uv
- Install Docker
- Install Terraform
- References
About this scenario
This tutorial shows how to deploy a Model Context Protocol (MCP) Server on KakaoCloud Kubernetes Engine and test it by connecting to the remote server from a local MCP client, such as Claude Desktop.
MCP is an open protocol that enables AI applications to communicate with various data sources and tools. The example server is configured to query weather information using the US National Weather Service (NWS) API.
MCP transport method
MCP supports various transport methods, but this tutorial uses only Streamable HTTP (SSE). Streamable HTTP uses Server-Sent Events to enable bidirectional communication between client and server, and it is highly compatible with HTTP-based Load Balancers in cloud environments.
The stdio method is suitable for communication between local processes, but it cannot be used in a remote Kubernetes environment.
Main steps
- Prepare and containerize the MCP Server application
- Upload the image to Container Registry
- Create a Kubernetes cluster and deploy the MCP Server
- Configure and test external access
Architecture overview
[MCP Client (Claude Desktop)]
↓ HTTPS
[Load Balancer (Public IP)]
↓
[Kubernetes Service]
↓
[MCP Server Pods (2 replicas)]
↓
[NWS Weather API]
This scenario is a practical guide for developers who want to deploy and operate an MCP-based AI tool server in a cloud environment.
Before you start
1. Build the network environment
To create a Kubernetes cluster on KakaoCloud, you need a VPC network. Build the network environment by referring to Build a network using NAT instances in multi-availability zones.
After setup is complete, check the following information:
- VPC name (example:
main) - Subnet information (kr-central-2-a, kr-central-2-b)
2. Prepare Container Registry
Create a Container Registry to store container images. Configure the registry by referring to Create and manage repositories.
After creation, check the following information:
- Project name (example:
my-project) - Repository name (example:
mcp-repository) - Registry URL:
{PROJECT_NAME}.kr-central-2.kcr.dev
3. Create an SSH key pair
Create an SSH key pair to access Kubernetes nodes. Create the key pair by referring to Create and manage key pairs.
Check the name of the key pair you created (example: mcp-test-key).
Getting started
Step 1. Prepare the MCP Server application
Prepare a weather lookup MCP Server that uses the US National Weather Service (NWS) API. This server provides two main functions:
get_alerts: Query weather alerts by US stateget_forecast: Query weather forecasts based on latitude and longitude
Create the project directory
mkdir weather-mcp-server
cd weather-mcp-server
Download example code
Save the content below as server.py.
import asyncio
import logging
import os
from typing import Any
import httpx
from fastmcp import FastMCP
logger = logging.getLogger(__name__)
logging.basicConfig(format="[%(levelname)s]: %(message)s", level=logging.INFO)
# Initialize FastMCP server
mcp = FastMCP("weather MCP Server on Kakaocloud")
# Constants
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"
async def make_nws_request(url: str) -> dict[str, Any] | None:
"""Make a request to the NWS API with proper error handling."""
headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
async with httpx.AsyncClient() as client:
try:
response = await client.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return response.json()
except Exception:
return None
def format_alert(feature: dict) -> str:
"""Format an alert feature into a readable string."""
props = feature["properties"]
return f"""
Event: {props.get("event", "Unknown")}
Area: {props.get("areaDesc", "Unknown")}
Severity: {props.get("severity", "Unknown")}
Description: {props.get("description", "No description available")}
Instructions: {props.get("instruction", "No specific instructions provided")}
"""
@mcp.tool()
async def get_alerts(state: str) -> str:
"""Get weather alerts for a US state.
Args:
state: Two-letter US state code (e.g. CA, NY)
"""
url = f"{NWS_API_BASE}/alerts/active/area/{state}"
data = await make_nws_request(url)
if not data or "features" not in data:
return "Unable to fetch alerts or no alerts found."
if not data["features"]:
return "No active alerts for this state."
alerts = [format_alert(feature) for feature in data["features"]]
return "\n---\n".join(alerts)
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
"""Get weather forecast for a location.
Args:
latitude: Latitude of the location
longitude: Longitude of the location
"""
# First get the forecast grid endpoint
points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
points_data = await make_nws_request(points_url)
if not points_data:
return "Unable to fetch forecast data for this location."
# Get the forecast URL from the points response
forecast_url = points_data["properties"]["forecast"]
forecast_data = await make_nws_request(forecast_url)
if not forecast_data:
return "Unable to fetch detailed forecast."
# Format the periods into a readable forecast
periods = forecast_data["properties"]["periods"]
forecasts = []
for period in periods[:5]: # Only show next 5 periods
forecast = f"""
{period["name"]}:
Temperature: {period["temperature"]}°{period["temperatureUnit"]}
Wind: {period["windSpeed"]} {period["windDirection"]}
Forecast: {period["detailedForecast"]}
"""
forecasts.append(forecast)
return "\n---\n".join(forecasts)
def main():
try:
port = int(os.getenv("PORT", "8000"))
logger.info(f"Starting MCP server on port {port}")
# Run the server
asyncio.run(
mcp.run_http_async(
transport="streamable-http",
host="0.0.0.0",
port=port,
)
)
except Exception as e:
logger.error(f"Failed to start MCP server: {e}", exc_info=True)
raise
if __name__ == "__main__":
main()
Configure the Python environment
Use uv to initialize the project environment.
uv init --name "weather-mcp-server" --description "Weather MCP server for KakaoCloud" --bare --python 3.10
uv add fastmcp==2.13.1 --no-sync
After execution, pyproject.toml and uv.lock files are created.
Local test (optional)
uv run server.py
Send an initialization request from another terminal:
curl -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": "init-1",
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {
"name": "curl-test",
"version": "1.0"
}
}
}'
If a normal response is returned, stop the server with Ctrl+C.
Step 2. Build the container image
Write the Dockerfile
Create a Dockerfile in the project root with the following content.
FROM python:3.10-slim
# Install uv
RUN pip install uv
# Set working directory
WORKDIR /app
# Copy dependency files and source code
COPY uv.lock pyproject.toml server.py /app/
# Install dependencies
RUN uv sync
# Expose port
EXPOSE 8000
# Run application
CMD ["uv", "run", "server.py"]
Build the Docker image
Build the image for the AMD64 architecture.
docker build --platform linux/amd64 -t weather-mcp-server:latest .
For macOS Apple Silicon (M1/M2), the --platform linux/amd64 option is required. KakaoCloud Kubernetes nodes use the AMD64 architecture.
Local test (optional)
Run the built image locally to verify it.
docker run --rm -p 8000:8000 weather-mcp-server:latest
Send an initialization request from another terminal:
curl -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": "init-1",
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {
"name": "curl-test",
"version": "1.0"
}
}
}'
If a normal response is returned, stop the server with Ctrl+C.
Step 3. Push the image to Container Registry
Upload the built Docker image to KakaoCloud Container Registry.
Authenticate to Container Registry
CR_URL={PROJECT_NAME}.kr-central-2.kcr.dev
docker login $CR_URL \
--username {ACCESS_KEY_ID} \
--password {ACCESS_SECRET_KEY}
| Parameter | Description | Example |
|---|---|---|
| PROJECT_NAME | Project name where Container Registry was created | my-project |
| ACCESS_KEY_ID | IAM access key ID | AKIA... |
| ACCESS_SECRET_KEY | IAM secret access key | wJal... |
If login succeeds, the Login Succeeded message is displayed.
Tag the image
Tag the image so it can be pushed to Container Registry.
docker tag \
weather-mcp-server:latest \
{CR_URL}/{CR_REPO_NAME}/weather-mcp-server:latest
| Parameter | Description | Example |
|---|---|---|
| CR_URL | Container Registry URL | my-project.kr-central-2.kcr.dev |
| CR_REPO_NAME | Created repository name | tutorial |
Push the image
Upload the tagged image to Container Registry.
docker push {CR_URL}/{CR_REPO_NAME}/weather-mcp-server:latest
After the push is complete, you can check the image in Container Pack > Container Registry in the KakaoCloud console.
Step 4. Create a Kubernetes cluster with Terraform
Use Terraform to create a Kubernetes Engine cluster and node pool.
Prepare Terraform files
Create a working directory and download the provided Terraform files.
cd .. # Exit the weather-mcp-server directory
mkdir kc-terraform
cd kc-terraform
Save the following Terraform files in the kc-terraform directory:
main.tf: Terraform provider settingsvariables.tf: Variable definitionsterraform.tfvars: Variable value settingsget_vpc.tf: VPC information lookupcreate_ke.tf: Kubernetes cluster creationcreate_np.tf: Node pool creationget_kubeconfig.tf.origin: kubeconfig creation template
Configure variables
Edit the terraform.tfvars file and adjust the values for your environment.
vpc_name = {VPC_NAME} # VPC name
ke_cluster_name = "mcp-test" # Name of the cluster to create
node_flavor = "m2a.large" # Node instance type to create
ke_nodepool_name = "node-pool-1" # Name of the node pool to create
ssh_key_name = {SSH_KEY_NAME} # SSH key name
| Parameter | Description | Example |
|---|---|---|
| VPC_NAME | VPC name created in the prerequisites | main |
| SSH_KEY_NAME | SSH key name created in the prerequisites | mcp-test-key |
Initialize Terraform
terraform init
Check the execution plan
terraform plan \
-var="access_key_id={ACCESS_KEY_ID}" \
-var="access_key_secret={ACCESS_SECRET_KEY}"
The list of resources to be created is displayed:
- VPC and subnet lookup (existing resources)
- One Kubernetes cluster
- One node pool (two nodes)
Create infrastructure
terraform apply \
-var="access_key_id={ACCESS_KEY_ID}" \
-var="access_key_secret={ACCESS_SECRET_KEY}"
When the confirmation message appears, enter yes.
Creating the cluster and nodes may take some time.
If resource creation succeeds, the Apply complete! message is displayed.
Create kubeconfig
Create a kubeconfig file to access the cluster.
cp get_kubeconfig.tf.origin get_kubeconfig.tf
terraform plan \
-var="access_key_id={ACCESS_KEY_ID}" \
-var="access_key_secret={ACCESS_SECRET_KEY}"
terraform apply \
-var="access_key_id={ACCESS_KEY_ID}" \
-var="access_key_secret={ACCESS_SECRET_KEY}"
After execution, the kubeconfig-mcp-test.yaml file is created.
Verify cluster access
Access the cluster using the generated kubeconfig.
export KUBECONFIG=$PWD/kubeconfig-mcp-test.yaml
kubectl get nodes
Expected output:
NAME STATUS ROLES AGE VERSION
host-10-x-x-x Ready <none> 5m v1.31.x
host-10-y-y-y Ready <none> 5m v1.31.x
Step 5. Deploy MCP Server to Kubernetes
Create a Container Registry authentication Secret
Store authentication information so Kubernetes can pull images from Container Registry.
CR_URL={PROJECT_NAME}.kr-central-2.kcr.dev
kubectl create secret docker-registry kc-cr-secret \
--docker-server=${CR_URL} \
--docker-username={ACCESS_KEY_ID} \
--docker-password={ACCESS_SECRET_KEY}
If resource creation succeeds, the secret/kc-cr-secret created message is displayed.
Create a Deployment
Create a Deployment that deploys the MCP Server.
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: weather-mcp-server
labels:
app: weather-mcp-server
spec:
replicas: 2
selector:
matchLabels:
app: weather-mcp-server
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: weather-mcp-server
spec:
imagePullSecrets:
- name: kc-cr-secret
containers:
- name: mcp-server
image: {PROJECT_NAME}.kr-central-2.kcr.dev/{CR_REPO_NAME}/weather-mcp-server:latest
ports:
- name: http
containerPort: 8000
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
EOF
| Parameter | Description |
|---|---|
| PROJECT_NAME | Container Registry project name |
| CR_REPO_NAME | Container Registry repository name |
Check deployment status
Check the Pod creation status.
kubectl get pods -l app=weather-mcp-server -w
Wait until all Pods are in the Running state.
Create a Service
Create a LoadBalancer type Service so the MCP Server can be accessed externally.
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: weather-mcp-service
labels:
app: weather-mcp-server
spec:
type: LoadBalancer
selector:
app: weather-mcp-server
ports:
- protocol: TCP
port: 80
targetPort: 8000
name: http
EOF
Verify Service creation:
kubectl get service weather-mcp-service -w
Initially, EXTERNAL-IP is displayed as <pending>. Wait until EXTERNAL-IP is assigned.
Step 6. Configure external access
Associate a public IP
When the LoadBalancer Service is created, a load balancer is automatically created in the KakaoCloud console.
- In the KakaoCloud console, go to Load Balancing.
- In the list of created load balancers, find the item whose name corresponds to
weather-mcp-service. - On the right side of the load balancer, click [⋮] menu > Associate public IP.
- Select an available public IP or create a new one and assign it.
Check the assigned public IP.
Step 7. Test the MCP Server
Basic connection test (using curl)
Test server initialization using the MCP protocol and save the mcp-session-id from the response header.
MCP_SESSION_ID=$(
curl -s -D - -o /dev/null \
-X POST http://{LOAD_BALANCER_PUBLIC_IP}/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": "init-1",
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {
"name": "curl-test",
"version": "1.0"
}
}
}' \
| awk 'BEGIN{IGNORECASE=1}
/^mcp-session-id:/ {
sub(/^[^:]*:[[:space:]]*/, "", $0);
sub(/\r$/, "", $0);
print; exit
}'
)
echo $MCP_SESSION_ID
| Parameter | Description | Example |
|---|---|---|
| LOAD_BALANCER_PUBLIC_IP | Load balancer public IP assigned in Step 6 | 123.456.789.10 |
Check the tool list (using curl)
curl -X POST http://{LOAD_BALANCER_PUBLIC_IP}/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: ${MCP_SESSION_ID}" \
-d '{
"jsonrpc": "2.0",
"id": "list-1",
"method": "tools/list",
"params": {}
}'
The get_alerts and get_forecast tools should be displayed.
Function test (using curl)
Test by directly calling tools on the MCP Server.
Query weather alerts for California:
curl -X POST http://{LOAD_BALANCER_PUBLIC_IP}/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: ${MCP_SESSION_ID}" \
-d '{
"jsonrpc": "2.0",
"id": "call-1",
"method": "tools/call",
"params": {
"name": "get_alerts",
"arguments": {
"state": "CA"
}
}
}'
If the MCP Server is working correctly, real-time weather information is returned.
Connect an MCP client
You can connect the deployed MCP Server to an MCP client as follows.
Claude Desktop configuration example:
{
"mcpServers": {
"weather-mcp": {
"url": "http://{LOAD_BALANCER_PUBLIC_IP}/mcp"
}
}
}
HTTP transport support may vary by MCP client. For stable integration testing, use the curl commands above.