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
8 changes: 8 additions & 0 deletions deploy/rbac/clusterrole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ rules:
- apiGroups: ["mcp.x-k8s.io"]
resources: ["mcpservers"]
verbs: ["get", "list", "watch", "create", "update", "delete"]
# Create/delete HTTPRoutes for mcp-gateway integration
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["httproutes"]
verbs: ["create", "delete", "list"]
# Create/delete MCPServerRegistrations for mcp-gateway integration
- apiGroups: ["mcp.kagenti.com"]
resources: ["mcpserverregistrations"]
verbs: ["create", "delete", "list"]
# List service accounts and namespaces for dropdowns
- apiGroups: [""]
resources: ["serviceaccounts"]
Expand Down
8 changes: 8 additions & 0 deletions dist/mcp-launcher.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ rules:
- apiGroups: ["mcp.x-k8s.io"]
resources: ["mcpservers"]
verbs: ["get", "list", "watch", "create", "update", "delete"]
# Create/delete HTTPRoutes for mcp-gateway integration
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["httproutes"]
verbs: ["create", "delete", "list"]
# Create/delete MCPServerRegistrations for mcp-gateway integration
- apiGroups: ["mcp.kagenti.com"]
resources: ["mcpserverregistrations"]
verbs: ["create", "delete", "list"]
# List service accounts and namespaces for dropdowns
- apiGroups: [""]
resources: ["serviceaccounts"]
Expand Down
167 changes: 167 additions & 0 deletions handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,33 @@ var mcpServerGVR = schema.GroupVersionResource{
Resource: "mcpservers",
}

var httpRouteGVR = schema.GroupVersionResource{
Group: "gateway.networking.k8s.io",
Version: "v1",
Resource: "httproutes",
}

var mcpServerRegistrationGVR = schema.GroupVersionResource{
Group: "mcp.kagenti.com",
Version: "v1alpha1",
Resource: "mcpserverregistrations",
}

// GatewayConfig holds mcp-gateway integration settings.
type GatewayConfig struct {
Enabled bool
GatewayName string
GatewayNamespace string
}

// Handler holds dependencies for HTTP handlers.
type Handler struct {
catalog *catalog.Store
clientset kubernetes.Interface
dynamicClient dynamic.Interface
targetNamespace string
templateDir string
gateway GatewayConfig
}

// New creates a new Handler.
Expand All @@ -40,13 +60,15 @@ func New(
clientset kubernetes.Interface,
dynamicClient dynamic.Interface,
targetNamespace string,
gateway GatewayConfig,
) *Handler {
return &Handler{
catalog: catalogStore,
clientset: clientset,
dynamicClient: dynamicClient,
targetNamespace: targetNamespace,
templateDir: "templates",
gateway: gateway,
}
}

Expand Down Expand Up @@ -383,6 +405,20 @@ func (h *Handler) Run(w http.ResponseWriter, r *http.Request) {
}
}

// Create gateway resources if mcp-gateway integration is enabled
if h.gateway.Enabled {
mcpPort, _, _ := unstructured.NestedInt64(created.Object, "spec", "config", "port")
if mcpPort <= 0 {
mcpPort = 8080
}
if err := h.createHTTPRoute(ctx, namespace, instanceName, mcpPort, ownerRef); err != nil {
log.Printf("failed to create HTTPRoute: %v", err)
}
if err := h.createMCPServerRegistration(ctx, namespace, instanceName, ownerRef); err != nil {
log.Printf("failed to create MCPServerRegistration: %v", err)
}
}

http.Redirect(w, r, "/running", http.StatusSeeOther)
}

Expand Down Expand Up @@ -502,6 +538,20 @@ func (h *Handler) QuickDeploy(w http.ResponseWriter, r *http.Request) {
}
}

// Create gateway resources if mcp-gateway integration is enabled
if h.gateway.Enabled {
mcpPort, _, _ := unstructured.NestedInt64(created.Object, "spec", "config", "port")
if mcpPort <= 0 {
mcpPort = 8080
}
if err := h.createHTTPRoute(ctx, namespace, instanceName, mcpPort, ownerRef); err != nil {
log.Printf("failed to create HTTPRoute: %v", err)
}
if err := h.createMCPServerRegistration(ctx, namespace, instanceName, ownerRef); err != nil {
log.Printf("failed to create MCPServerRegistration: %v", err)
}
}

http.Redirect(w, r, "/running", http.StatusSeeOther)
}

Expand Down Expand Up @@ -608,6 +658,31 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
}
}

// Clean up gateway resources (safety net — ownerReferences handle the common case)
if h.gateway.Enabled {
routes, err := h.dynamicClient.Resource(httpRouteGVR).Namespace(namespace).List(ctx, metav1.ListOptions{
LabelSelector: managedLabel,
})
if err == nil {
for _, r := range routes.Items {
if r.GetName() == name {
_ = h.dynamicClient.Resource(httpRouteGVR).Namespace(namespace).Delete(ctx, r.GetName(), metav1.DeleteOptions{})
}
}
}

regs, err := h.dynamicClient.Resource(mcpServerRegistrationGVR).Namespace(namespace).List(ctx, metav1.ListOptions{
LabelSelector: managedLabel,
})
if err == nil {
for _, r := range regs.Items {
if r.GetName() == name {
_ = h.dynamicClient.Resource(mcpServerRegistrationGVR).Namespace(namespace).Delete(ctx, r.GetName(), metav1.DeleteOptions{})
}
}
}
}

// Delete the MCPServer CR itself
err = h.dynamicClient.Resource(mcpServerGVR).Namespace(namespace).Delete(
ctx, name, metav1.DeleteOptions{},
Expand Down Expand Up @@ -661,6 +736,98 @@ func (h *Handler) createConfigMap(ctx context.Context, namespace, name string, d
return err
}

func toolPrefix(name string) string {
s := name
for _, suffix := range []string{"-mcp-server", "-server", "-mcp"} {
if strings.HasSuffix(s, suffix) {
s = strings.TrimSuffix(s, suffix)
break
}
}
return strings.ReplaceAll(s, "-", "_") + "_"
}

func (h *Handler) createHTTPRoute(ctx context.Context, namespace, name string, port int64, ownerRef metav1.OwnerReference) error {
route := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "gateway.networking.k8s.io/v1",
"kind": "HTTPRoute",
"metadata": map[string]any{
"name": name,
"namespace": namespace,
"labels": map[string]any{
"app.kubernetes.io/managed-by": "mcp-launcher",
},
"ownerReferences": []any{
map[string]any{
"apiVersion": ownerRef.APIVersion,
"kind": ownerRef.Kind,
"name": ownerRef.Name,
"uid": string(ownerRef.UID),
},
},
},
"spec": map[string]any{
"hostnames": []any{
name + ".mcp.local",
},
"parentRefs": []any{
map[string]any{
"name": h.gateway.GatewayName,
"namespace": h.gateway.GatewayNamespace,
},
},
"rules": []any{
map[string]any{
"backendRefs": []any{
map[string]any{
"name": name,
"port": port,
},
},
},
},
},
},
}
_, err := h.dynamicClient.Resource(httpRouteGVR).Namespace(namespace).Create(ctx, route, metav1.CreateOptions{})
return err
}

func (h *Handler) createMCPServerRegistration(ctx context.Context, namespace, name string, ownerRef metav1.OwnerReference) error {
reg := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "mcp.kagenti.com/v1alpha1",
"kind": "MCPServerRegistration",
"metadata": map[string]any{
"name": name,
"namespace": namespace,
"labels": map[string]any{
"app.kubernetes.io/managed-by": "mcp-launcher",
},
"ownerReferences": []any{
map[string]any{
"apiVersion": ownerRef.APIVersion,
"kind": ownerRef.Kind,
"name": ownerRef.Name,
"uid": string(ownerRef.UID),
},
},
},
"spec": map[string]any{
"toolPrefix": toolPrefix(name),
"targetRef": map[string]any{
"group": "gateway.networking.k8s.io",
"kind": "HTTPRoute",
"name": name,
},
},
},
}
_, err := h.dynamicClient.Resource(mcpServerRegistrationGVR).Namespace(namespace).Create(ctx, reg, metav1.CreateOptions{})
return err
}

func parsePort(s string, defaultPort int32) int64 {
if s == "" {
return int64(defaultPort)
Expand Down
2 changes: 1 addition & 1 deletion handlers/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func setupHandler(t *testing.T, catalogEntries []*corev1.ConfigMap, dynObjects .

dynClient := newDynamicClient(dynObjects...)
store := catalog.NewStore(client, "catalog-ns")
h := New(store, client, dynClient, "default")
h := New(store, client, dynClient, "default", GatewayConfig{})
h.templateDir = "../templates"
return h
}
Expand Down
8 changes: 7 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,14 @@ func main() {
log.Fatalf("Failed to create dynamic client: %v", err)
}

gwCfg := handlers.GatewayConfig{
Enabled: envOr("MCP_GATEWAY_ENABLED", "true") != "false",
GatewayName: envOr("MCP_GATEWAY_NAME", "mcp-gateway"),
GatewayNamespace: envOr("MCP_GATEWAY_NAMESPACE", "gateway-system"),
}

store := catalog.NewStore(clientset, catalogNamespace)
h := handlers.New(store, clientset, dynClient, targetNamespace)
h := handlers.New(store, clientset, dynClient, targetNamespace, gwCfg)

mux := http.NewServeMux()
mux.HandleFunc("GET /", h.Catalog)
Expand Down
Loading