UNPKG

@mexl/backstage-plugin-mcp-frontend

Version:

Enhanced Backstage frontend plugin for MCP (Model Context Protocol) entity visualization with JSON/Text toggle, syntax highlighting, and VSCode integration

192 lines (189 loc) 7.63 kB
import { jsxs, jsx, Fragment } from 'react/jsx-runtime'; import { useState } from 'react'; import { makeStyles, Card, CardHeader, Button, CardContent, Typography, Box, List, ListItem, ListItemText, Chip } from '@material-ui/core'; import { Build, Storage, Chat } from '@material-ui/icons'; import { useEntity } from '@backstage/plugin-catalog-react'; const useStyles = makeStyles((theme) => ({ card: { height: "100%" }, section: { marginBottom: theme.spacing(2) }, sectionTitle: { display: "flex", alignItems: "center", gap: theme.spacing(1), marginBottom: theme.spacing(1) }, list: { paddingTop: 0 }, listItem: { paddingLeft: 0, paddingTop: theme.spacing(0.5), paddingBottom: theme.spacing(0.5) }, description: { color: theme.palette.text.secondary, fontSize: "0.875rem" }, jsonView: { backgroundColor: theme.palette.type === "dark" ? theme.palette.grey[900] : theme.palette.grey[50], border: `1px solid ${theme.palette.divider}`, padding: theme.spacing(2), borderRadius: theme.shape.borderRadius, fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace', fontSize: "0.875rem", overflowX: "auto", whiteSpace: "pre-wrap", lineHeight: 1.5 }, toggleButton: { borderRadius: 20, textTransform: "none", minWidth: 80, fontSize: "0.75rem", padding: theme.spacing(0.5, 1.5) }, jsonKey: { color: theme.palette.type === "dark" ? "#79C0FF" : "#0969DA", fontWeight: 600 }, jsonString: { color: theme.palette.type === "dark" ? "#A5D6FF" : "#0A3069" }, jsonBracket: { color: theme.palette.type === "dark" ? "#F85149" : "#CF222E", fontWeight: 600 } })); const formatJsonWithSyntaxHighlighting = (obj, classes) => { const jsonString = JSON.stringify(obj, null, 2); const highlighted = jsonString.replace(/"([^"]+)":/g, `<span class="${classes.jsonKey}">"$1":</span>`).replace(/: "([^"]+)"/g, `: <span class="${classes.jsonString}">"$1"</span>`).replace(/[{}]/g, `<span class="${classes.jsonBracket}">$&</span>`).replace(/[\[\]]/g, `<span class="${classes.jsonBracket}">$&</span>`); return highlighted; }; const renderCapabilityList = (items, icon, title, classes) => { if (!items || items.length === 0) { return null; } return /* @__PURE__ */ jsxs(Box, { className: classes.section, children: [ /* @__PURE__ */ jsxs(Typography, { variant: "h6", className: classes.sectionTitle, children: [ icon, title, " (", items.length, ")" ] }), /* @__PURE__ */ jsx(List, { className: classes.list, children: items.map((item, index) => { const name = typeof item === "string" ? item : item.name; const description = typeof item === "object" ? item.description : void 0; const enabled = typeof item === "object" ? item.enabled : void 0; const rateLimit = typeof item === "object" ? item.rateLimit : void 0; const permissions = typeof item === "object" ? item.permissions : void 0; const template = typeof item === "object" ? item.template : void 0; const parameters = typeof item === "object" ? item.parameters : void 0; return /* @__PURE__ */ jsx(ListItem, { className: classes.listItem, children: /* @__PURE__ */ jsx( ListItemText, { primary: /* @__PURE__ */ jsxs(Box, { display: "flex", alignItems: "center", style: { gap: 8 }, children: [ /* @__PURE__ */ jsx(Typography, { component: "span", children: name }), enabled === false && /* @__PURE__ */ jsx(Chip, { label: "Disabled", size: "small", color: "default" }), enabled === true && /* @__PURE__ */ jsx(Chip, { label: "Enabled", size: "small", color: "primary" }), permissions && /* @__PURE__ */ jsx(Box, { display: "flex", style: { gap: 4 }, children: permissions.map((perm, i) => /* @__PURE__ */ jsx( Chip, { label: perm, size: "small", variant: "outlined", color: perm === "write" || perm === "delete" ? "secondary" : "default" }, i )) }) ] }), secondary: /* @__PURE__ */ jsxs(Box, { children: [ description && /* @__PURE__ */ jsx(Typography, { className: classes.description, children: description }), rateLimit && /* @__PURE__ */ jsxs(Typography, { className: classes.description, children: [ "Rate limit: ", rateLimit.requestsPerMinute && `${rateLimit.requestsPerMinute}/min`, rateLimit.requestsPerHour && ` ${rateLimit.requestsPerHour}/hr` ] }), template && /* @__PURE__ */ jsxs(Typography, { className: classes.description, children: [ "Template: ", /* @__PURE__ */ jsx("code", { style: { fontSize: "0.75rem" }, children: template }) ] }), parameters && parameters.length > 0 && /* @__PURE__ */ jsxs(Box, { display: "flex", style: { gap: 4 }, mt: 0.5, children: [ /* @__PURE__ */ jsx(Typography, { variant: "caption", children: "Parameters: " }), parameters.map((param, i) => /* @__PURE__ */ jsx(Chip, { label: param, size: "small", variant: "outlined" }, i)) ] }) ] }) } ) }, index); }) }) ] }); }; const MCPCapabilitiesCard = () => { const { entity } = useEntity(); const classes = useStyles(); const [viewMode, setViewMode] = useState("text"); if (!entity || entity.kind !== "MCP") { return null; } const spec = entity.spec; const capabilities = spec.capabilities || {}; const { tools, resources, prompts } = capabilities; const hasCapabilities = tools && tools.length > 0 || resources && resources.length > 0 || prompts && prompts.length > 0; const toggleViewMode = () => { setViewMode(viewMode === "text" ? "json" : "text"); }; const renderJsonView = () => /* @__PURE__ */ jsx( Box, { className: classes.jsonView, dangerouslySetInnerHTML: { __html: formatJsonWithSyntaxHighlighting(capabilities, classes) } } ); return /* @__PURE__ */ jsxs(Card, { className: classes.card, children: [ /* @__PURE__ */ jsx( CardHeader, { title: "Capabilities", action: hasCapabilities && /* @__PURE__ */ jsx( Button, { variant: viewMode === "json" ? "contained" : "outlined", color: viewMode === "json" ? "primary" : "default", size: "small", onClick: toggleViewMode, className: classes.toggleButton, children: viewMode === "text" ? "JSON" : "Text" } ) } ), /* @__PURE__ */ jsx(CardContent, { children: !hasCapabilities ? /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "textSecondary", children: "No capabilities defined for this MCP server." }) : /* @__PURE__ */ jsx(Fragment, { children: viewMode === "json" ? renderJsonView() : /* @__PURE__ */ jsxs(Fragment, { children: [ renderCapabilityList( tools, /* @__PURE__ */ jsx(Build, { fontSize: "small" }), "Tools", classes ), renderCapabilityList( resources, /* @__PURE__ */ jsx(Storage, { fontSize: "small" }), "Resources", classes ), renderCapabilityList( prompts, /* @__PURE__ */ jsx(Chat, { fontSize: "small" }), "Prompts", classes ) ] }) }) }) ] }); }; export { MCPCapabilitiesCard }; //# sourceMappingURL=MCPCapabilitiesCard.esm.js.map