feat(MCP UI) plugins nav#2053
Open
dimetron wants to merge 4 commits into
Open
Conversation
…ver.spec.ui (kagent-dev#2047) Add an optional spec.ui block to the RemoteMCPServer v1alpha2 CRD so any MCP server shipping a web UI can be surfaced inside the kagent console: listed in a "Plugins" sidebar section and reverse-proxied same-origin under /_p/{pathPrefix}/ with theme/namespace propagation via a kagent:* postMessage protocol. Backend: GET /api/plugins registry + /_p/{pathPrefix} reverse proxy with CSS injection (plugins hunks of the shared server.go/handlers.go/httpapi types). UI: AppSidebar nav, plugin frame page, namespace/sidebar-status providers. Depends on kagent-dev#2045 only conceptually (SSO); the UserMenu sidebar variant ships here. Includes design/EP-2047-mcp-ui-plugin-registration.md and specs/mcp-ui-registration. Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
…gent-dev#2047) Add jest tests for getPlugins (success, backend error, 404 => empty list, unexpected failure), getBackendRoot (/api suffix stripping), SidebarStatusProvider (load ok, plugins-failed, retry re-fetch), and NamespaceProvider (default, update, out-of-provider guard). Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
…gent-dev#2047) - Validate event.origin on host<->plugin postMessage so foreign frames cannot drive navigation/resize/title/etc. - Restrict kagent:navigate to same-origin URLs (blocks javascript:/data: and external open-redirects) and use window.location.assign. - Post the kagent:context message to window.location.origin instead of "*". - Add tests covering same-origin vs foreign-origin message handling. - Remove specs/mcp-ui-registration/ design dump; EP-2047 is the design of record. Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds first-class “UI plugins” to the kagent console by letting RemoteMCPServer resources declaratively expose embedded web UIs via spec.ui, with backend registry + reverse-proxy support and new UI navigation/iframe framing.
Changes:
- Extends the
RemoteMCPServerv1alpha2 API/CRD with an optionalspec.uiblock and generated deepcopy/CRD manifests. - Implements backend plugin listing (
GET /api/plugins) and same-origin reverse-proxying under/_p/{pathPrefix}/…, including optional HTML CSS injection. - Updates the Next.js UI to render a dedicated sidebar experience (status, namespace selector, plugin nav) and a plugin iframe host page + supporting tests/docs.
Reviewed changes
Copilot reviewed 30 out of 31 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/src/lib/utils.ts | Adds getBackendRoot() helper for building non-/api root URLs (used for /_p/...). |
| ui/src/lib/sidebar-status-context.tsx | Adds client context for plugin list loading + status/retry. |
| ui/src/lib/namespace-context.tsx | Adds client namespace context used by sidebar + plugin messaging. |
| ui/src/lib/tests/sidebar-status-context.test.tsx | Unit tests for plugin list loading/retry status behavior. |
| ui/src/lib/tests/namespace-context.test.tsx | Unit tests for namespace context default + provider enforcement. |
| ui/src/lib/tests/getBackendRoot.test.ts | Unit tests for stripping /api suffix from backend URL. |
| ui/src/components/UserMenu.tsx | Adds a sidebar-specific rendering variant for user menu. |
| ui/src/components/sidebars/StatusIndicator.tsx | Adds sidebar footer status indicator for plugin load health + retry. |
| ui/src/components/sidebars/SidebarCollapseButton.tsx | Adds desktop-only sidebar collapse/expand toggle in footer. |
| ui/src/components/sidebars/NamespaceSelector.tsx | Adds namespace selector UI integrated into the new sidebar header. |
| ui/src/components/sidebars/AppSidebarNav.tsx | Implements sectioned sidebar navigation and merges in plugin links + badges. |
| ui/src/components/sidebars/AppSidebar.tsx | New sidebar shell (header/nav/footer) wiring status + namespace + user menu. |
| ui/src/components/MobileTopBar.tsx | Adds a mobile-only top bar with sidebar trigger to match sidebar sheet behavior. |
| ui/src/app/providers.tsx | Centralizes provider ordering in a single client boundary, including Sidebar/Namespace. |
| ui/src/app/plugins/page.tsx | Adds “Plugins status” internal page for viewing registry + backend health checks. |
| ui/src/app/plugins/[name]/[[...path]]/page.tsx | Adds iframe host page + host↔plugin postMessage protocol handling. |
| ui/src/app/plugins/[name]/[[...path]]/tests/page.test.tsx | Unit tests for same-origin filtering + resize/title message handling. |
| ui/src/app/layout.tsx | Refactors root layout to server component reading sidebar cookie + rendering sidebar/inset. |
| ui/src/app/api/plugins/route.ts | Adds Next.js BFF route proxying GET /api/plugins to the Go backend. |
| ui/src/app/actions/plugins.ts | Adds server actions for listing plugins and probing plugin backend health. |
| ui/src/app/actions/tests/plugins.test.ts | Unit tests for plugin list server action behavior. |
| helm/kagent-crds/templates/kagent.dev_remotemcpservers.yaml | Publishes CRD schema changes for spec.ui via Helm chart CRDs. |
| go/core/internal/httpserver/server.go | Wires new /api/plugins and /_p/{pathPrefix} routes into the HTTP server. |
| go/core/internal/httpserver/handlers/plugins.go | Implements plugin listing + reverse-proxy with optional CSS injection. |
| go/core/internal/httpserver/handlers/plugins_test.go | Unit tests for list filtering/defaulting + proxying/CSS injection/502 handling. |
| go/core/internal/httpserver/handlers/handlers.go | Registers the new Plugins handler in the handler bundle. |
| go/api/v1alpha2/zz_generated.deepcopy.go | Regenerates deepcopy support for new RemoteMCPServerUI type. |
| go/api/v1alpha2/remotemcpserver_types.go | Adds RemoteMCPServerUI to the API type and kubebuilder validations/defaults. |
| go/api/httpapi/types.go | Introduces PluginResponse type used by the UI registry endpoint. |
| go/api/config/crd/bases/kagent.dev_remotemcpservers.yaml | Updates generated CRD base manifest with spec.ui schema. |
| design/EP-2047-mcp-ui-plugin-registration.md | Adds design EP documenting the feature, contracts, and test plan. |
Files not reviewed (1)
- go/api/v1alpha2/zz_generated.deepcopy.go: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+47
to
+50
| export function getBackendRoot(): string { | ||
| const url = getBackendUrl(); | ||
| return url.replace(/\/api\/?$/, "") || url; | ||
| } |
Comment on lines
+80
to
+96
| if (isCollapsed) { | ||
| return ( | ||
| <TooltipProvider> | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| className="h-8 w-8 shrink-0" | ||
| aria-label={`Namespace: ${value || "none"}`} | ||
| > | ||
| {loading ? ( | ||
| <Loader2 className="h-4 w-4 animate-spin" /> | ||
| ) : ( | ||
| <Network className="h-4 w-4" /> | ||
| )} | ||
| </Button> |
Comment on lines
+134
to
+139
| // Add PLUGINS section for plugins that specify a section not in NAV_SECTIONS | ||
| const pluginsSection = plugins.filter((p) => !knownSections.includes(p.section)); | ||
| if (pluginsSection.length > 0) { | ||
| sections.push({ | ||
| label: "ADMIN", | ||
| items: pluginsSection.map((p) => ({ |
Comment on lines
+112
to
+117
| const pluginItems: NavItemWithBadge[] = plugins | ||
| .filter((p) => p.section === section.label) | ||
| .map((p) => ({ | ||
| label: p.displayName, | ||
| href: `/plugins/${p.pathPrefix}`, | ||
| icon: getIconByName(p.icon), |
Comment on lines
+8
to
+14
| export interface PluginItem { | ||
| name: string; | ||
| pathPrefix: string; | ||
| displayName: string; | ||
| icon: string; | ||
| section: string; | ||
| } |
Comment on lines
+12
to
+18
| export interface SidebarPluginNav { | ||
| name: string; | ||
| pathPrefix: string; | ||
| displayName: string; | ||
| icon: string; | ||
| section: string; | ||
| } |
Comment on lines
+27
to
+29
| const pathParams = useParams<{ path?: string[] }>(); | ||
| const subPath = pathParams.path ? "/" + pathParams.path.join("/") : "/"; | ||
| const iframeSrc = `/_p/${name}${subPath}`; |
Comment on lines
+135
to
+145
| Director: func(req *http.Request) { | ||
| req.URL.Scheme = target.Scheme | ||
| req.URL.Host = target.Host | ||
| req.Host = target.Host | ||
| req.URL.Path = remainder | ||
| // Request an uncompressed response so injectCSS can be spliced reliably. | ||
| req.Header.Set("Accept-Encoding", "identity") | ||
| for k, v := range headers { | ||
| req.Header.Set(k, v) | ||
| } | ||
| }, |
Comment on lines
+108
to
+114
| // PathPrefix is the URL path segment used for routing: /_p/{pathPrefix}/ | ||
| // Must be a valid URL path segment (lowercase alphanumeric + hyphens). | ||
| // Defaults to the RemoteMCPServer name if not specified. | ||
| // +optional | ||
| // +kubebuilder:validation:MaxLength=63 | ||
| // +kubebuilder:validation:Pattern=`^[a-z0-9][a-z0-9-]*[a-z0-9]$` | ||
| PathPrefix string `json:"pathPrefix,omitempty"` |
Comment on lines
+49
to
+56
| | Field | Type | Default | Validation | Purpose | | ||
| |-------|------|---------|------------|---------| | ||
| | `enabled` | bool | `false` | — | Opt-in: this server provides a web UI. | | ||
| | `pathPrefix` | string | `<name>` | `maxLength=63`, `^[a-z0-9][a-z0-9-]*[a-z0-9]$` | URL segment for `/_p/{pathPrefix}/`. | | ||
| | `displayName` | string | `<name>` | — | Sidebar label. | | ||
| | `icon` | string | `puzzle` | — | `lucide-react` icon name. | | ||
| | `section` | enum | `PLUGINS` | `OVERVIEW\|AGENTS\|RESOURCES\|ADMIN\|PLUGINS` | Sidebar section. | | ||
| | `defaultPath` | string | `/` | — | Initial sub-path at plugin root. | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
MCP UI plugins nav (#2047)
This pull request introduces a new, fully declarative mechanism for registering and surfacing MCP server web UIs as first-class plugins in the kagent UI. By extending the
RemoteMCPServerCRD with aspec.uiblock, MCP servers can now expose their own web interfaces directly within the kagent console, benefiting from theme, namespace, and navigation integration—all without requiring a new CRD or controller changes. The backend lists and proxies these plugins, and the UI displays them in a dedicated sidebar section, framing each plugin in an iframe and supporting a host↔plugin postMessage protocol.Key changes:
CRD and API Extensions
uiblock (RemoteMCPServerUI) to theRemoteMCPServerCRD (remotemcpserver_types.go, CRD YAML), allowing servers to declare UI plugin metadata such asenabled,pathPrefix,displayName,icon,section,defaultPath, andinjectCSS. This enables declarative plugin registration and configuration. [1] [2]RemoteMCPServerUIstruct to support Kubernetes CRD operations. [1] [2]PluginResponseAPI type to represent plugin metadata for the UI sidebar.Backend Implementation
PluginsHandlerwith two main endpoints:GET /api/plugins: Lists all enabled UI plugins across watched namespaces, projecting them toPluginResponseand applying defaults./_p/{pathPrefix}/*: Reverse-proxies requests to the plugin's web root, injecting custom CSS if specified and ensuring proper authorization and header resolution.Documentation
EP-2047-mcp-ui-plugin-registration.md) summarizing motivation, goals, implementation details, backend and UI contracts, and test plan for the UI plugin registration feature.