Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ clean:

arm:
@echo "Building arm Docker image $(FULL_IMAGE_NAME)..."
docker build \
docker buildx build \
--platform linux/amd64,linux/arm64 \
-f $(DOCKERFILE_PATH) \
-t $(FULL_ARM_IMAGE) \
.
--load .
@echo "Docker image $(FULL_ARM_IMAGE) built successfully."

.PHONY: all build push clean arm
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ mcpserver start -t http --port 8089 --config ./examples/jobspec/mcpserver.yaml

We will provide examples for jobspec translation functions in [fractale-mcp](https://github.com/compspec/fractale-mcp).

### Kubernetes (kind)

This example is for basic manifests to work in Kind (or Kubernetes/Openshift). Note that we use the default base container with a custom function added via ConfigMap. You can take this approach, or build ON our base container and pip install your own functions for use.

- [examples/kind](examples/kind)

We will be making a Kubernetes Operator to create this set of stuff soon.

### Design Choices

Expand Down
98 changes: 98 additions & 0 deletions examples/kind/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# HPC MCP Server Example

> Run the mcpserver in Kubernetes (or OpenShift) with basic manifests

## Prerequisites

- [Docker](https://docs.docker.com/get-docker/)
- [kind](https://kind.sigs.k8s.io/docs/user/quick-start/) (Kubernetes in Docker)
- [kubectl](https://kubernetes.io/docs/tasks/tools/)
- [Python 3.11+](https://www.python.org/downloads/) (for local testing)

## 1. Create a Kind Cluster

Start by creating a local Kubernetes cluster (with kind):

```bash
kind create cluster
```

## 2. Build and Load the Image

Build the container image locally and load it into the `kind` nodes so you don't have to push to a remote registry.
You can also pull and load.

```bash
# Build the image
docker build -t ghcr.io/converged-computing/mcp-server:latest .

# OR pull
docker pull ghcr.io/converged-computing/mcp-server:latest

# Load into kind
kind load docker-image ghcr.io/converged-computing/mcp-server:latest
```

## 3. Deploy the Server

The configuration and the custom tools are managed via a Kubernetes `ConfigMap`.
This contains our `mcpserver.yaml` (the configuration) and `echo.py` (the tool code).

```bash
kubectl apply -f config-map.yaml
```

This launches the pod, mounts the configuration, and exposes it via a Service.

```bash
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
```

Wait for the pod to reach the `Running` state:

```bash
kubectl get pods
```

See the server running:

```bash
kubectl logs mcp-server-5bbdcbbbdf-2sccc -f
```

Expose the service to your local machine:

```bash
kubectl port-forward svc/mcp-server-service 8080:80
```

Check health:

```bash
$ curl -s http://localhost:8080/health | jq
{
"status": 200,
"message": "OK"
}
```

Ask for pancakes (you need fastmcp installed for this).

```bash
$ python3 get_pancakes.py
⭐ Discovered tool: pancakes_tool
⭐ Discovered tool: simple_echo

CallToolResult(content=[TextContent(type='text', text='Pancakes for Vanessa 🥞', annotations=None, meta=None)], structured_content={'result': 'Pancakes for Vanessa 🥞'}, meta=None, data='Pancakes for Vanessa 🥞', is_error=False)
```

Note that we can also run the server in stdio mode and then echo json RPC to it, but nah, don't really want to do that.

## Clean Up

To delete the cluster and start over:

```bash
kind delete cluster
```
18 changes: 18 additions & 0 deletions examples/kind/config-map.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: mcp-server-config
data:
mcpserver.yaml: |
server:
transport: http
port: 8089
host: "0.0.0.0"
tools:
- path: "custom_tools.echo.pancakes"
name: "pancakes_tool"

echo.py: |
from fastmcp import FastMCP
def pancakes(name: str = "Me") -> str:
return f"Pancakes for {name} 🥞"
40 changes: 40 additions & 0 deletions examples/kind/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-server
labels:
app: hpc-mcp
spec:
replicas: 1
selector:
matchLabels:
app: hpc-mcp
template:
metadata:
labels:
app: hpc-mcp
spec:
containers:
- name: mcp-server
image: ghcr.io/converged-computing/mcp-server:latest
imagePullPolicy: Always
args: ["--config", "/etc/mcp/mcpserver.yaml"]
ports:
- containerPort: 8089
env:
- name: PYTHONPATH
value: "/code:/mnt/custom"
volumeMounts:
- name: config-volume
mountPath: /etc/mcp/mcpserver.yaml
subPath: mcpserver.yaml
- name: tool-volume
mountPath: /mnt/custom/custom_tools/echo.py
subPath: echo.py
volumes:
- name: config-volume
configMap:
name: mcp-server-config
- name: tool-volume
configMap:
name: mcp-server-config
26 changes: 26 additions & 0 deletions examples/kind/get_pancakes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import asyncio
from fastmcp.client.transports import StreamableHttpTransport
from fastmcp import Client
import sys
import os


port = 8080
if len(sys.argv) > 1:
port = sys.argv[1]

client = Client(f"http://localhost:{port}/mcp")

async def call_tool(message: str):
async with client:
tools = await client.list_tools()
for tool in tools:
print(f" ⭐ Discovered tool: {tool.name}")
print()
result = await client.call_tool("pancakes_tool", {"name": message})
print(result)

try:
asyncio.run(call_tool("Vanessa"))
except RuntimeError:
print("Please set the correct FRACTALE_MCP_TOKEN")
49 changes: 49 additions & 0 deletions examples/kind/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---

apiVersion: v1
kind: Service
metadata:
name: mcp-server-service
spec:
selector:
app: hpc-mcp
ports:
- protocol: TCP
port: 80
targetPort: 8089
type: ClusterIP

---

apiVersion: v1
kind: Service
metadata:
name: mcp-server-headless
spec:
clusterIP: None
selector:
app: hpc-mcp
ports:
- port: 8089
targetPort: 8089

---

# Ingress for external access (note requires an Ingress Controller like NGINX)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mcp-server-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /mcp
pathType: Prefix
backend:
service:
name: mcp-server-service
port:
number: 80