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.