Skip to main content

Deploy an MCP Server on Kubernetes Engine

Deploy a Model Context Protocol (MCP) Server on KakaoCloud Kubernetes Engine and integrate it with AI applications.

Basic information

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.

info

The stdio method is suitable for communication between local processes, but it cannot be used in a remote Kubernetes environment.

Main steps​

  1. Prepare and containerize the MCP Server application
  2. Upload the image to Container Registry
  3. Create a Kubernetes cluster and deploy the MCP Server
  4. 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 state
  • get_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 .
info

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}
ParameterDescriptionExample
PROJECT_NAMEProject name where Container Registry was createdmy-project
ACCESS_KEY_IDIAM access key IDAKIA...
ACCESS_SECRET_KEYIAM secret access keywJal...

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
ParameterDescriptionExample
CR_URLContainer Registry URLmy-project.kr-central-2.kcr.dev
CR_REPO_NAMECreated repository nametutorial

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 settings
  • variables.tf: Variable definitions
  • terraform.tfvars: Variable value settings
  • get_vpc.tf: VPC information lookup
  • create_ke.tf: Kubernetes cluster creation
  • create_np.tf: Node pool creation
  • get_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
ParameterDescriptionExample
VPC_NAMEVPC name created in the prerequisitesmain
SSH_KEY_NAMESSH key name created in the prerequisitesmcp-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.

info

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
ParameterDescription
PROJECT_NAMEContainer Registry project name
CR_REPO_NAMEContainer 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.

  1. In the KakaoCloud console, go to Load Balancing.
  2. In the list of created load balancers, find the item whose name corresponds to weather-mcp-service.
  3. On the right side of the load balancer, click [⋮] menu > Associate public IP.
  4. 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
ParameterDescriptionExample
LOAD_BALANCER_PUBLIC_IPLoad balancer public IP assigned in Step 6123.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"
}
}
}
info

HTTP transport support may vary by MCP client. For stable integration testing, use the curl commands above.