Skip to content
Draft
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
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,20 @@ $(CMDS):
$(DEVICES):
$(GO) build -ldflags '$(GO_BUILD_LDFLAGS)' -o ${OUTPUT_DIR}/$@-device-plugin ./cmd/device-plugin/$@

mcp-server:
$(GO) build -ldflags '$(GO_BUILD_LDFLAGS)' -o ${OUTPUT_DIR}/mcp-server ./cmd/mcp-server

docker-mcp:
docker build \
--build-arg GOLANG_IMAGE=${GOLANG_IMAGE} \
--build-arg VERSION=${VERSION} \
. -f=docker/Dockerfile.mcp -t ${IMG_NAME}-mcp:${IMG_TAG}

clean:
$(GO) clean -r -x ./cmd/...
-rm -rf $(OUTPUT_DIR)

.PHONY: all build docker clean test $(CMDS)
.PHONY: all build docker clean test $(CMDS) mcp-server

test:
mkdir -p ./_output/coverage/
Expand Down
7 changes: 7 additions & 0 deletions charts/hami/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ The app name for DevicePlugin
{{- printf "%s-device-plugin" ( include "hami-vgpu.fullname" . ) | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{/*
The app name for MCP Server
*/}}
{{- define "hami-vgpu.mcp-server" -}}
{{- printf "%s-mcp-server" ( include "hami-vgpu.fullname" . ) | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{/*
The app name for MockDevicePlugin
*/}}
Expand Down
23 changes: 23 additions & 0 deletions charts/hami/templates/mcp-server/clusterrole.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{{- if .Values.mcpServer.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "hami-vgpu.mcp-server" . }}
labels:
app.kubernetes.io/component: "hami-mcp-server"
{{- include "hami-vgpu.labels" . | nindent 4 }}
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
resourceNames: ["hami-scheduler-config"]
{{- end }}
17 changes: 17 additions & 0 deletions charts/hami/templates/mcp-server/clusterrolebinding.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{{- if .Values.mcpServer.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "hami-vgpu.mcp-server" . }}
labels:
app.kubernetes.io/component: "hami-mcp-server"
{{- include "hami-vgpu.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "hami-vgpu.mcp-server" . }}
subjects:
- kind: ServiceAccount
name: {{ include "hami-vgpu.mcp-server" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
{{- end }}
49 changes: 49 additions & 0 deletions charts/hami/templates/mcp-server/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{{- if .Values.mcpServer.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "hami-vgpu.mcp-server" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: "hami-mcp-server"
{{- include "hami-vgpu.labels" . | nindent 4 }}
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: "hami-mcp-server"
{{- include "hami-vgpu.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
app.kubernetes.io/component: "hami-mcp-server"
{{- include "hami-vgpu.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "hami-vgpu.mcp-server" . }}
containers:
- name: mcp-server
image: "{{ .Values.global.imageRegistry }}{{ .Values.mcpServer.image.repository }}:{{ .Values.mcpServer.image.tag | default .Values.global.imageTag }}"
imagePullPolicy: {{ .Values.mcpServer.image.pullPolicy }}
args:
- --listen-addr=:9395
- --prometheus-url={{ .Values.mcpServer.prometheusUrl }}
- --log-level={{ .Values.mcpServer.logLevel }}
ports:
- name: http
containerPort: 9395
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
resources:
{{- toYaml .Values.mcpServer.resources | nindent 12 }}
{{- end }}
20 changes: 20 additions & 0 deletions charts/hami/templates/mcp-server/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{{- if .Values.mcpServer.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "hami-vgpu.mcp-server" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: "hami-mcp-server"
{{- include "hami-vgpu.labels" . | nindent 4 }}
spec:
type: ClusterIP
ports:
- name: http
port: 9395
targetPort: http
protocol: TCP
selector:
app.kubernetes.io/component: "hami-mcp-server"
{{- include "hami-vgpu.selectorLabels" . | nindent 4 }}
{{- end }}
10 changes: 10 additions & 0 deletions charts/hami/templates/mcp-server/serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{{- if .Values.mcpServer.enabled }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "hami-vgpu.mcp-server" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: "hami-mcp-server"
{{- include "hami-vgpu.labels" . | nindent 4 }}
{{- end }}
17 changes: 17 additions & 0 deletions charts/hami/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -526,3 +526,20 @@ legacyMetrics: false

prometheus:
enabled: false

# MCP Server configuration (experimental)
mcpServer:
enabled: false
image:
repository: "hami-mcp"
tag: ""
pullPolicy: IfNotPresent
prometheusUrl: "http://localhost:9090"
logLevel: "info"
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi
50 changes: 50 additions & 0 deletions cmd/mcp-server/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright 2024 The HAMi Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"github.com/spf13/pflag"
)

// MCPFlags holds the CLI flags for the MCP server.
type MCPFlags struct {
Kubeconfig string
PrometheusURL string
LogLevel string
ListenAddr string
MetricsPort int
}

// NewMCPFlags creates a new MCPFlags instance with default values.
func NewMCPFlags() *MCPFlags {
return &MCPFlags{
Kubeconfig: "",
PrometheusURL: "http://localhost:9090",
LogLevel: "info",
ListenAddr: "",
MetricsPort: 9395,
}
}

// AddFlags adds the MCP server flags to the given flag set.
func (f *MCPFlags) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&f.Kubeconfig, "kubeconfig", f.Kubeconfig, "Path to kubeconfig file. If not provided, in-cluster config will be used.")
fs.StringVar(&f.PrometheusURL, "prometheus-url", f.PrometheusURL, "URL of the Prometheus server for metrics queries.")
fs.StringVar(&f.LogLevel, "log-level", f.LogLevel, "Log level (debug, info, warn, error).")
fs.StringVar(&f.ListenAddr, "listen-addr", f.ListenAddr, "If set (e.g. ':9395'), serve the MCP streamable HTTP endpoint at /mcp on this address. If empty, run over stdio.")
fs.IntVar(&f.MetricsPort, "metrics-port", f.MetricsPort, "Port for HAMi scheduler metrics endpoint.")
}
105 changes: 105 additions & 0 deletions cmd/mcp-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
Copyright 2024 The HAMi Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"strconv"
"syscall"

"github.com/spf13/pflag"
klog "k8s.io/klog/v2"

"github.com/Project-HAMi/HAMi/pkg/mcp"
"github.com/Project-HAMi/HAMi/pkg/version"
)

func main() {
flags := NewMCPFlags()
flags.AddFlags(pflag.CommandLine)

showVersion := pflag.Bool("version", false, "Print version and exit")

klogFlags := flag.NewFlagSet("klog", flag.ExitOnError)
klog.InitFlags(klogFlags)
pflag.CommandLine.AddGoFlagSet(klogFlags)

pflag.Parse()

if *showVersion {
fmt.Println(version.Print())
os.Exit(0)
}

if err := setupLogging(klogFlags, flags.LogLevel); err != nil {
fmt.Fprintf(os.Stderr, "Failed to setup logging: %v\n", err)
os.Exit(1)
}

klog.InfoS("Starting HAMi MCP Server",
"prometheusURL", flags.PrometheusURL,
"logLevel", flags.LogLevel,
"listenAddr", flags.ListenAddr,
)

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

server, err := mcp.NewServer(ctx, &mcp.ServerConfig{
Kubeconfig: flags.Kubeconfig,
PrometheusURL: flags.PrometheusURL,
MetricsPort: flags.MetricsPort,
})
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create MCP server: %v\n", err)
os.Exit(1)
}

var runErr error
if flags.ListenAddr != "" {
runErr = server.RunHTTP(ctx, flags.ListenAddr)
} else {
runErr = server.Run(ctx)
}
if runErr != nil {
fmt.Fprintf(os.Stderr, "MCP server error: %v\n", runErr)
os.Exit(1)
}
}

// setupLogging maps a human-readable log level to klog's -v verbosity flag on
// the supplied flagset. The flagset must already have klog flags registered.
func setupLogging(klogFlags *flag.FlagSet, level string) error {
var v int
switch level {
case "debug":
v = 4
case "info":
v = 2
case "warn":
v = 1
case "error":
v = 0
default:
return fmt.Errorf("invalid log level: %s (must be debug, info, warn, or error)", level)
}
return klogFlags.Set("v", strconv.Itoa(v))
}
15 changes: 15 additions & 0 deletions docker/Dockerfile.mcp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
ARG GOLANG_IMAGE=golang:1.26.2-bookworm

FROM $GOLANG_IMAGE AS builder
ARG GOPROXY
ARG VERSION
WORKDIR /workspace
COPY . .
RUN go mod vendor
RUN make mcp-server VERSION=$VERSION

FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/bin/mcp-server /mcp-server
USER nonroot:nonroot
ENTRYPOINT ["/mcp-server"]
Loading
Loading