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: 4 additions & 1 deletion cyclops-ctrl/api/v1alpha1/module_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ const (
GitOpsWritePathAnnotation = "cyclops-ui.com/write-path"
GitOpsWriteRevisionAnnotation = "cyclops-ui.com/write-revision"

ModuleManagerAnnotation = "cyclops-ui.com/module-manager"
ModuleManagerLabel = "cyclops-ui.com/module-manager"

AddonModuleLabel = "cyclops-ui.com/addon"
MCPServerModuleLabel = "cyclops-ui.com/mcp-server"
)

type TemplateRef struct {
Expand Down
90 changes: 90 additions & 0 deletions cyclops-ctrl/internal/controller/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import (
"strings"
"time"

json "github.com/json-iterator/go"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"

"github.com/cyclops-ui/cyclops/cyclops-ctrl/pkg/template"
"github.com/cyclops-ui/cyclops/cyclops-ctrl/pkg/template/render"

Expand Down Expand Up @@ -1011,6 +1015,92 @@ func (m *Modules) GetResource(ctx *gin.Context) {
ctx.JSON(http.StatusOK, resource)
}

func (m *Modules) InstallMCPServer(ctx *gin.Context) {
ctx.Header("Access-Control-Allow-Origin", "*")

mcpModuleValues := map[string]interface{}{
"replicas": 1,
"version": "latest",
}

m.telemetryClient.AddonInstall("mcp-server")

valBytes, err := json.Marshal(mcpModuleValues)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to create MCP server module values",
"reason": err.Error(),
})
}

mcpServerModule := v1alpha1.Module{
TypeMeta: metav1.TypeMeta{
Kind: "Module",
APIVersion: "cyclops-ui.com/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "mcp-cyclops",
Labels: map[string]string{
v1alpha1.MCPServerModuleLabel: "true",
v1alpha1.AddonModuleLabel: "true",
},
},
Spec: v1alpha1.ModuleSpec{
TargetNamespace: "cyclops",
TemplateRef: v1alpha1.TemplateRef{
URL: "https://github.com/cyclops-ui/templates",
Path: "cyclops-mcp",
Version: "main",
SourceType: "git",
},
Values: apiextensionsv1.JSON{
Raw: valBytes,
},
},
History: make([]v1alpha1.HistoryEntry, 0),
}

if err := m.kubernetesClient.CreateModule(mcpServerModule); err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to create Cyclops MCP server module",
"reason": err.Error(),
})
return
}

ctx.Status(http.StatusCreated)
}

func (m *Modules) MCPServerStatus(ctx *gin.Context) {
ctx.Header("Access-Control-Allow-Origin", "*")

type MCPServerStatus struct {
Installed bool `json:"installed"`
}

module, err := m.kubernetesClient.GetModule("mcp-cyclops")
if err != nil {
if errors.IsNotFound(err) {
ctx.JSON(http.StatusOK, MCPServerStatus{Installed: false})
return
}

ctx.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to check Cyclops MCP server status",
"reason": err.Error(),
})
return
}

if module.Labels == nil {
ctx.JSON(http.StatusOK, MCPServerStatus{Installed: false})
return
}

_, ok := module.Labels[v1alpha1.MCPServerModuleLabel]
ctx.JSON(http.StatusOK, MCPServerStatus{Installed: ok})
}

func getTargetGeneration(generation string, module *v1alpha1.Module) (*v1alpha1.Module, bool) {
// no generation specified means current generation
if len(generation) == 0 {
Expand Down
3 changes: 3 additions & 0 deletions cyclops-ctrl/internal/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ func (h *Handler) Start() error {
h.router.GET("/modules/:name/helm-template", modulesController.HelmTemplate)
//h.router.POST("/modules/resources", modulesController.ModuleToResources)

h.router.POST("/modules/mcp/install", modulesController.InstallMCPServer)
h.router.GET("/modules/mcp/status", modulesController.MCPServerStatus)

h.router.GET("/resources/pods/:namespace/:name/:container/logs", modulesController.GetLogs)
h.router.GET("/resources/pods/:namespace/:name/:container/logs/stream", sse.HeadersMiddleware(), modulesController.GetLogsStream)
h.router.GET("/resources/pods/:namespace/:name/:container/logs/download", modulesController.DownloadLogs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func (r *ModuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return ctrl.Result{}, err
}

if module.Annotations[cyclopsv1alpha1.ModuleManagerAnnotation] == "mcp" {
if len(module.Labels) != 0 && module.Labels[cyclopsv1alpha1.ModuleManagerLabel] == "mcp" {
r.telemetryClient.MCPModuleReconciliation()
}

Expand Down
17 changes: 17 additions & 0 deletions cyclops-ctrl/internal/telemetry/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Client interface {
ReleaseMigration()
TemplateCreation()
TemplateEdit()
AddonInstall(addon string)
}

type logger interface {
Expand Down Expand Up @@ -129,6 +130,20 @@ func (c EnqueueClient) TemplateEdit() {
})
}

func (c EnqueueClient) AddonInstall(addon string) {
props := c.messageProps()
if props == nil {
props = map[string]interface{}{}
}
props["addon"] = addon

_ = c.client.Enqueue(posthog.Capture{
Event: "addon-install",
DistinctId: c.distinctID,
Properties: props,
})
}

func (c EnqueueClient) messageProps() map[string]interface{} {
props := map[string]interface{}{
"version": c.version,
Expand Down Expand Up @@ -163,4 +178,6 @@ func (c MockClient) TemplateCreation() {}

func (c MockClient) TemplateEdit() {}

func (c MockClient) AddonInstall(_ string) {}

// endregion
2 changes: 2 additions & 0 deletions cyclops-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@
"react-diff-viewer": "^3.1.1",
"react-dom": "^18.0.0",
"react-highlight-words": "^0.20.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^6.21.1",
"react-scripts": "^5.0.1",
"react-terminal": "^1.3.1",
"remark-gfm": "^4.0.1",
"runtime-env-cra": "^0.2.4",
"terser-webpack-plugin": "^5.3.10",
"ts-loader": "^9.5.1",
Expand Down
34 changes: 31 additions & 3 deletions cyclops-ui/src/components/layouts/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { Button, Menu, MenuProps } from "antd";
import {
AppstoreAddOutlined,
Expand All @@ -8,6 +8,8 @@ import {
GithubFilled,
ThunderboltFilled,
DiscordOutlined,
ApiOutlined,
RobotOutlined,
} from "@ant-design/icons";
import { useLocation } from "react-router";
import PathConstants from "../../routes/PathConstants";
Expand All @@ -17,7 +19,19 @@ import helmLogo from "../../static/img/helm_white.png";
import cyclopsLogo from "../../static/img/cyclops_logo.png";

const SideNav = () => {
const location = useLocation().pathname.split("/")[1];
const [openKeys, setOpenKeys] = useState<string[]>([]);
const location = useLocation(); // from react-router-dom
const [selectedKeys, setSelectedKeys] = useState<string>("");

useEffect(() => {
setSelectedKeys(location.pathname.split("/")[1]);

if (location.pathname.startsWith(PathConstants.ADDONS_MCP_SERVER)) {
setOpenKeys(["addons"]);
} else {
setOpenKeys([]);
}
}, [location.pathname]);

const sidebarItems: MenuProps["items"] = [
{
Expand All @@ -44,6 +58,18 @@ const SideNav = () => {
icon: <img alt="" style={{ height: "14px" }} src={helmLogo} />,
key: "helm",
},
{
label: "Addons",
icon: <ApiOutlined />,
key: "addons",
children: [
{
icon: <RobotOutlined />,
label: <a href={PathConstants.ADDONS_MCP_SERVER}>MCP server</a>,
key: "addons-mcp",
},
],
},
];

const tagChangelogLink = (tag: string) => {
Expand Down Expand Up @@ -73,8 +99,10 @@ const SideNav = () => {
<Menu
theme="dark"
mode="inline"
selectedKeys={[location]}
selectedKeys={[selectedKeys]}
items={sidebarItems}
openKeys={openKeys}
onOpenChange={(keys) => setOpenKeys(keys)}
/>
<Button
style={{ background: "transparent", margin: "auto 25px 12px 25px" }}
Expand Down
Loading