diff --git a/Makefile b/Makefile index f564c40..90cb893 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index b9124f6..82d3f6d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/kind/README.md b/examples/kind/README.md new file mode 100644 index 0000000..81e4639 --- /dev/null +++ b/examples/kind/README.md @@ -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 +``` diff --git a/examples/kind/config-map.yaml b/examples/kind/config-map.yaml new file mode 100644 index 0000000..f240259 --- /dev/null +++ b/examples/kind/config-map.yaml @@ -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} 🥞" diff --git a/examples/kind/deployment.yaml b/examples/kind/deployment.yaml new file mode 100644 index 0000000..e7455f3 --- /dev/null +++ b/examples/kind/deployment.yaml @@ -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 diff --git a/examples/kind/get_pancakes.py b/examples/kind/get_pancakes.py new file mode 100644 index 0000000..98bf0b7 --- /dev/null +++ b/examples/kind/get_pancakes.py @@ -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") diff --git a/examples/kind/service.yaml b/examples/kind/service.yaml new file mode 100644 index 0000000..cead14e --- /dev/null +++ b/examples/kind/service.yaml @@ -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