Skip to content

Commit 2a8c550

Browse files
authored
✅ install Cyclops mcp endpoint #831
install Cyclops mcp endpoint
2 parents e8c9179 + e377a8d commit 2a8c550

File tree

13 files changed

+1250
-8
lines changed

13 files changed

+1250
-8
lines changed

cyclops-ctrl/api/v1alpha1/module_types.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ const (
4848
GitOpsWritePathAnnotation = "cyclops-ui.com/write-path"
4949
GitOpsWriteRevisionAnnotation = "cyclops-ui.com/write-revision"
5050

51-
ModuleManagerAnnotation = "cyclops-ui.com/module-manager"
51+
ModuleManagerLabel = "cyclops-ui.com/module-manager"
52+
53+
AddonModuleLabel = "cyclops-ui.com/addon"
54+
MCPServerModuleLabel = "cyclops-ui.com/mcp-server"
5255
)
5356

5457
type TemplateRef struct {

cyclops-ctrl/internal/controller/modules.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import (
99
"strings"
1010
"time"
1111

12+
json "github.com/json-iterator/go"
13+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
14+
"k8s.io/apimachinery/pkg/api/errors"
15+
1216
"github.com/cyclops-ui/cyclops/cyclops-ctrl/pkg/template"
1317
"github.com/cyclops-ui/cyclops/cyclops-ctrl/pkg/template/render"
1418

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

1018+
func (m *Modules) InstallMCPServer(ctx *gin.Context) {
1019+
ctx.Header("Access-Control-Allow-Origin", "*")
1020+
1021+
mcpModuleValues := map[string]interface{}{
1022+
"replicas": 1,
1023+
"version": "latest",
1024+
}
1025+
1026+
m.telemetryClient.AddonInstall("mcp-server")
1027+
1028+
valBytes, err := json.Marshal(mcpModuleValues)
1029+
if err != nil {
1030+
ctx.JSON(http.StatusInternalServerError, gin.H{
1031+
"error": "Failed to create MCP server module values",
1032+
"reason": err.Error(),
1033+
})
1034+
}
1035+
1036+
mcpServerModule := v1alpha1.Module{
1037+
TypeMeta: metav1.TypeMeta{
1038+
Kind: "Module",
1039+
APIVersion: "cyclops-ui.com/v1alpha1",
1040+
},
1041+
ObjectMeta: metav1.ObjectMeta{
1042+
Name: "mcp-cyclops",
1043+
Labels: map[string]string{
1044+
v1alpha1.MCPServerModuleLabel: "true",
1045+
v1alpha1.AddonModuleLabel: "true",
1046+
},
1047+
},
1048+
Spec: v1alpha1.ModuleSpec{
1049+
TargetNamespace: "cyclops",
1050+
TemplateRef: v1alpha1.TemplateRef{
1051+
URL: "https://github.com/cyclops-ui/templates",
1052+
Path: "cyclops-mcp",
1053+
Version: "main",
1054+
SourceType: "git",
1055+
},
1056+
Values: apiextensionsv1.JSON{
1057+
Raw: valBytes,
1058+
},
1059+
},
1060+
History: make([]v1alpha1.HistoryEntry, 0),
1061+
}
1062+
1063+
if err := m.kubernetesClient.CreateModule(mcpServerModule); err != nil {
1064+
ctx.JSON(http.StatusInternalServerError, gin.H{
1065+
"error": "Failed to create Cyclops MCP server module",
1066+
"reason": err.Error(),
1067+
})
1068+
return
1069+
}
1070+
1071+
ctx.Status(http.StatusCreated)
1072+
}
1073+
1074+
func (m *Modules) MCPServerStatus(ctx *gin.Context) {
1075+
ctx.Header("Access-Control-Allow-Origin", "*")
1076+
1077+
type MCPServerStatus struct {
1078+
Installed bool `json:"installed"`
1079+
}
1080+
1081+
module, err := m.kubernetesClient.GetModule("mcp-cyclops")
1082+
if err != nil {
1083+
if errors.IsNotFound(err) {
1084+
ctx.JSON(http.StatusOK, MCPServerStatus{Installed: false})
1085+
return
1086+
}
1087+
1088+
ctx.JSON(http.StatusInternalServerError, gin.H{
1089+
"error": "Failed to check Cyclops MCP server status",
1090+
"reason": err.Error(),
1091+
})
1092+
return
1093+
}
1094+
1095+
if module.Labels == nil {
1096+
ctx.JSON(http.StatusOK, MCPServerStatus{Installed: false})
1097+
return
1098+
}
1099+
1100+
_, ok := module.Labels[v1alpha1.MCPServerModuleLabel]
1101+
ctx.JSON(http.StatusOK, MCPServerStatus{Installed: ok})
1102+
}
1103+
10141104
func getTargetGeneration(generation string, module *v1alpha1.Module) (*v1alpha1.Module, bool) {
10151105
// no generation specified means current generation
10161106
if len(generation) == 0 {

cyclops-ctrl/internal/handler/handler.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ func (h *Handler) Start() error {
103103
h.router.GET("/modules/:name/helm-template", modulesController.HelmTemplate)
104104
//h.router.POST("/modules/resources", modulesController.ModuleToResources)
105105

106+
h.router.POST("/modules/mcp/install", modulesController.InstallMCPServer)
107+
h.router.GET("/modules/mcp/status", modulesController.MCPServerStatus)
108+
106109
h.router.GET("/resources/pods/:namespace/:name/:container/logs", modulesController.GetLogs)
107110
h.router.GET("/resources/pods/:namespace/:name/:container/logs/stream", sse.HeadersMiddleware(), modulesController.GetLogsStream)
108111
h.router.GET("/resources/pods/:namespace/:name/:container/logs/download", modulesController.DownloadLogs)

cyclops-ctrl/internal/modulecontroller/module_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func (r *ModuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
136136
return ctrl.Result{}, err
137137
}
138138

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

cyclops-ctrl/internal/telemetry/client.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type Client interface {
1414
ReleaseMigration()
1515
TemplateCreation()
1616
TemplateEdit()
17+
AddonInstall(addon string)
1718
}
1819

1920
type logger interface {
@@ -129,6 +130,20 @@ func (c EnqueueClient) TemplateEdit() {
129130
})
130131
}
131132

133+
func (c EnqueueClient) AddonInstall(addon string) {
134+
props := c.messageProps()
135+
if props == nil {
136+
props = map[string]interface{}{}
137+
}
138+
props["addon"] = addon
139+
140+
_ = c.client.Enqueue(posthog.Capture{
141+
Event: "addon-install",
142+
DistinctId: c.distinctID,
143+
Properties: props,
144+
})
145+
}
146+
132147
func (c EnqueueClient) messageProps() map[string]interface{} {
133148
props := map[string]interface{}{
134149
"version": c.version,
@@ -163,4 +178,6 @@ func (c MockClient) TemplateCreation() {}
163178

164179
func (c MockClient) TemplateEdit() {}
165180

181+
func (c MockClient) AddonInstall(_ string) {}
182+
166183
// endregion

cyclops-ui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@
3434
"react-diff-viewer": "^3.1.1",
3535
"react-dom": "^18.0.0",
3636
"react-highlight-words": "^0.20.0",
37+
"react-markdown": "^10.1.0",
3738
"react-router-dom": "^6.21.1",
3839
"react-scripts": "^5.0.1",
3940
"react-terminal": "^1.3.1",
41+
"remark-gfm": "^4.0.1",
4042
"runtime-env-cra": "^0.2.4",
4143
"terser-webpack-plugin": "^5.3.10",
4244
"ts-loader": "^9.5.1",

cyclops-ui/src/components/layouts/Sidebar.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { useEffect, useState } from "react";
22
import { Button, Menu, MenuProps } from "antd";
33
import {
44
AppstoreAddOutlined,
@@ -8,6 +8,8 @@ import {
88
GithubFilled,
99
ThunderboltFilled,
1010
DiscordOutlined,
11+
ApiOutlined,
12+
RobotOutlined,
1113
} from "@ant-design/icons";
1214
import { useLocation } from "react-router";
1315
import PathConstants from "../../routes/PathConstants";
@@ -17,7 +19,19 @@ import helmLogo from "../../static/img/helm_white.png";
1719
import cyclopsLogo from "../../static/img/cyclops_logo.png";
1820

1921
const SideNav = () => {
20-
const location = useLocation().pathname.split("/")[1];
22+
const [openKeys, setOpenKeys] = useState<string[]>([]);
23+
const location = useLocation(); // from react-router-dom
24+
const [selectedKeys, setSelectedKeys] = useState<string>("");
25+
26+
useEffect(() => {
27+
setSelectedKeys(location.pathname.split("/")[1]);
28+
29+
if (location.pathname.startsWith(PathConstants.ADDONS_MCP_SERVER)) {
30+
setOpenKeys(["addons"]);
31+
} else {
32+
setOpenKeys([]);
33+
}
34+
}, [location.pathname]);
2135

2236
const sidebarItems: MenuProps["items"] = [
2337
{
@@ -44,6 +58,18 @@ const SideNav = () => {
4458
icon: <img alt="" style={{ height: "14px" }} src={helmLogo} />,
4559
key: "helm",
4660
},
61+
{
62+
label: "Addons",
63+
icon: <ApiOutlined />,
64+
key: "addons",
65+
children: [
66+
{
67+
icon: <RobotOutlined />,
68+
label: <a href={PathConstants.ADDONS_MCP_SERVER}>MCP server</a>,
69+
key: "addons-mcp",
70+
},
71+
],
72+
},
4773
];
4874

4975
const tagChangelogLink = (tag: string) => {
@@ -73,8 +99,10 @@ const SideNav = () => {
7399
<Menu
74100
theme="dark"
75101
mode="inline"
76-
selectedKeys={[location]}
102+
selectedKeys={[selectedKeys]}
77103
items={sidebarItems}
104+
openKeys={openKeys}
105+
onOpenChange={(keys) => setOpenKeys(keys)}
78106
/>
79107
<Button
80108
style={{ background: "transparent", margin: "auto 25px 12px 25px" }}

0 commit comments

Comments
 (0)