Skip to content
Open
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
102 changes: 102 additions & 0 deletions cmd/shifu-mcp-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"context"
"flag"
"fmt"
"log"
"net/http"

"github.com/edgenesis/shifu/pkg/deviceapi"
"github.com/edgenesis/shifu/pkg/k8s/api/v1alpha1"
mcpserver "github.com/edgenesis/shifu/pkg/mcp/server"
"github.com/modelcontextprotocol/go-sdk/mcp"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/kubernetes"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)

func main() {
var (
kubeconfig string
addr string
)
flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to kubeconfig file (uses in-cluster config if empty)")
flag.StringVar(&addr, "addr", ":8443", "Address to listen on")
flag.Parse()

config, err := getRestConfig(kubeconfig)
if err != nil {
log.Fatalf("Failed to get Kubernetes config: %v", err)
}

clientset, err := kubernetes.NewForConfig(config)
if err != nil {
log.Fatalf("Failed to create Kubernetes clientset: %v", err)
}

edClient, err := newEdgeDeviceRestClient(config)
if err != nil {
log.Fatalf("Failed to create EdgeDevice REST client: %v", err)
}

edgeLister := func(ctx context.Context) ([]v1alpha1.EdgeDevice, error) {
edList := &v1alpha1.EdgeDeviceList{}
err := edClient.Get().
Resource("edgedevices").
Do(ctx).
Into(edList)
if err != nil {
return nil, fmt.Errorf("listing EdgeDevices: %w", err)
}
return edList.Items, nil
}

resolver := deviceapi.NewResolver(clientset, edgeLister)
apiClient := deviceapi.NewClient(resolver)
server := mcpserver.New(apiClient)

handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
return server
}, nil)

mux := http.NewServeMux()
mux.Handle("/mcp", handler)

log.Printf("Shifu MCP Server listening on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatalf("Server failed: %v", err)
}
}

func getRestConfig(kubeconfig string) (*rest.Config, error) {
if kubeconfig != "" {
return clientcmd.BuildConfigFromFlags("", kubeconfig)
}
return rest.InClusterConfig()
}

func newEdgeDeviceRestClient(config *rest.Config) (*rest.RESTClient, error) {
scheme := runtime.NewScheme()
if err := v1alpha1.AddToScheme(scheme); err != nil {
return nil, err
}
if err := clientgoscheme.AddToScheme(scheme); err != nil {
return nil, err
}

crdConfig := *config
crdConfig.ContentConfig.GroupVersion = &schema.GroupVersion{
Group: v1alpha1.GroupVersion.Group,
Version: v1alpha1.GroupVersion.Version,
}
crdConfig.APIPath = "/apis"
crdConfig.NegotiatedSerializer = serializer.NewCodecFactory(scheme)
crdConfig.UserAgent = rest.DefaultKubernetesUserAgent()

return rest.UnversionedRESTClientFor(&crdConfig)
}
19 changes: 19 additions & 0 deletions dockerfiles/Dockerfile.mcpServer
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Build stage
FROM golang:1.26 AS builder

WORKDIR /shifu

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 go build -o /out/shifu-mcp-server ./cmd/shifu-mcp-server/

# Runtime stage
FROM gcr.io/distroless/static-debian11

COPY --from=builder /out/shifu-mcp-server /shifu-mcp-server

EXPOSE 8443

ENTRYPOINT ["/shifu-mcp-server"]
52 changes: 52 additions & 0 deletions examples/mcp-demo/configmap-robot-arm.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: deviceshifu-robot-arm-configmap
namespace: deviceshifu
data:
driverProperties: |
driverSku: FANUC-M20iD
driverImage: edgehub/deviceshifu-http-mqtt:nightly
instructions: |
instructions:
move_joint:
readWrite: W
safe: false
description: |
Move a specific joint to a target angle.
## Topic
`robot-arm/commands/move_joint`
## Message format (JSON)
```json
{"joint": 1, "angle": 45.0, "speed": 50}
```
- `joint`: 1-6 (axis number)
- `angle`: degrees. Safe ranges: J1 ±170, J2 -100/+75, J3 -70/+200, J4 ±190, J5 ±125, J6 ±360
- `speed`: 1-100 (% of max speed)
gripper:
readWrite: W
safe: false
description: |
Open or close the gripper.
## Topic
`robot-arm/commands/gripper`
## Message format
```json
{"action": "close", "force": 80}
```
joint_positions:
readWrite: R
safe: true
description: |
Real-time joint positions. Subscribe to receive continuous updates.
## Topic
`robot-arm/status/joint_positions`
Published every 100ms. Array is [J1..J6] in degrees.
emergency_stop:
readWrite: W
safe: false
description: |
Immediately halt all motion.
## Topic
`robot-arm/commands/emergency_stop`
Publish any message to trigger E-stop.
31 changes: 31 additions & 0 deletions examples/mcp-demo/configmap-sensor-array.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: deviceshifu-sensor-array-configmap
namespace: deviceshifu
data:
driverProperties: |
driverSku: SensorArray-RS485
driverImage: edgehub/deviceshifu-http-nats:nightly
instructions: |
instructions:
temperature:
readWrite: R
safe: true
description: |
Temperature readings. Subject: `sensors.<node_id>.temperature`
Wildcard: `sensors.*.temperature` for all nodes.
Published every 5 seconds per node.
vibration:
readWrite: R
safe: true
description: |
Vibration readings. Subject: `sensors.<node_id>.vibration`
Values above 0.5g indicate potential failure.
configure_interval:
readWrite: W
safe: false
description: |
Change reporting interval. Uses NATS request/reply.
Subject: `sensors.<node_id>.config.interval`
Valid intervals: 1-60 seconds. Default is 5.
51 changes: 51 additions & 0 deletions examples/mcp-demo/configmap-thermometer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: thermometer-configmap
namespace: deviceshifu
data:
driverProperties: |
driverSku: Acme-Thermo-3000
driverImage: edgehub/deviceshifu-http-http:nightly
instructions: |
instructionSettings:
defaultTimeoutSeconds: 5
instructions:
read_temp:
readWrite: R
safe: true
description: |
GET /read_temp
Returns the current temperature in Celsius as a plain-text float (e.g. "23.5").
No request body required. Read-only, no side effects.
read_humidity:
readWrite: R
safe: true
description: |
GET /read_humidity
Returns relative humidity as a percentage (e.g. "62.3").
No request body required.
set_threshold:
readWrite: W
safe: false
description: |
POST /set_threshold
Sets the temperature alert threshold. Requires JSON body:
{"min": <float>, "max": <float>}
Returns 200 OK on success. This WRITES device configuration.
get_status:
readWrite: R
safe: true
description: |
GET /get_status
Returns device health info as JSON:
{"status": "ok", "uptime_hours": 1234, "firmware": "2.1.4"}
telemetries: |
telemetrySettings:
telemetryUpdateIntervalInMilliseconds: 5000
telemetries:
device_health:
properties:
instruction: get_status
initialDelayMs: 1000
intervalMs: 5000
49 changes: 49 additions & 0 deletions examples/mcp-demo/deployment-robot-arm.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: deviceshifu-robot-arm
namespace: deviceshifu
labels:
app: deviceshifu-robot-arm
spec:
replicas: 1
selector:
matchLabels:
app: deviceshifu-robot-arm
template:
metadata:
labels:
app: deviceshifu-robot-arm
spec:
containers:
- name: deviceshifu-http
image: edgehub/deviceshifu-http-mqtt:nightly
ports:
- containerPort: 8080
volumeMounts:
- name: deviceshifu-config
mountPath: "/etc/edgedevice/config"
readOnly: true
env:
- name: EDGEDEVICE_NAME
value: "edgedevice-robot-arm"
- name: EDGEDEVICE_NAMESPACE
value: "devices"
volumes:
- name: deviceshifu-config
configMap:
name: deviceshifu-robot-arm-configmap
---
apiVersion: v1
kind: Service
metadata:
name: deviceshifu-robot-arm
namespace: deviceshifu
labels:
app: deviceshifu-robot-arm
spec:
selector:
app: deviceshifu-robot-arm
ports:
- port: 8080
targetPort: 8080
49 changes: 49 additions & 0 deletions examples/mcp-demo/deployment-sensor-array.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: deviceshifu-sensor-array
namespace: deviceshifu
labels:
app: deviceshifu-sensor-array
spec:
replicas: 1
selector:
matchLabels:
app: deviceshifu-sensor-array
template:
metadata:
labels:
app: deviceshifu-sensor-array
spec:
containers:
- name: deviceshifu-http
image: edgehub/deviceshifu-http-nats:nightly
ports:
- containerPort: 8080
volumeMounts:
- name: deviceshifu-config
mountPath: "/etc/edgedevice/config"
readOnly: true
env:
- name: EDGEDEVICE_NAME
value: "edgedevice-sensor-array"
- name: EDGEDEVICE_NAMESPACE
value: "devices"
volumes:
- name: deviceshifu-config
configMap:
name: deviceshifu-sensor-array-configmap
---
apiVersion: v1
kind: Service
metadata:
name: deviceshifu-sensor-array
namespace: deviceshifu
labels:
app: deviceshifu-sensor-array
spec:
selector:
app: deviceshifu-sensor-array
ports:
- port: 8080
targetPort: 8080
Loading
Loading