diff --git a/design/EP-2047-mcp-ui-plugin-registration.md b/design/EP-2047-mcp-ui-plugin-registration.md new file mode 100644 index 0000000000..d15191e939 --- /dev/null +++ b/design/EP-2047-mcp-ui-plugin-registration.md @@ -0,0 +1,126 @@ +# EP-2047: UI Plugins — register MCP server web UIs via RemoteMCPServer.spec.ui + +* Issue: [#2047](https://github.com/kagent-dev/kagent/issues/2047) + +## Background + +Some MCP servers ship their own web UI (e.g. a Kanban board, a dashboard). Today +there is no way to surface that UI inside the kagent console — users must open a +separate URL, losing the kagent theme, namespace context, and navigation chrome. + +This EP lets any MCP server that ships a web UI register itself as a first-class +**plugin** in the kagent UI sidebar, framed in an iframe, theme- and +namespace-aware — **without** the UI needing to know it is embedded and **without** +adding a new CRD. Registration is fully declarative: a single `RemoteMCPServer` +resource with a `spec.ui` block is the entire contract. + +This EP describes the architecture, the host↔plugin postMessage protocol, and the +reverse-proxy contract. + +## Motivation + +- Let MCP servers contribute UI surfaces to the kagent console with zero controller + changes and no new CRD. +- Keep the embedding declarative and namespace/theme-aware. +- Provide the foundation for shipped plugins such as `kanban-mcp` (EP-2048). + +### Goals + +- Add an optional `spec.ui` block to the `RemoteMCPServer` v1alpha2 CRD. +- Backend: list enabled plugins (`GET /api/plugins`) and reverse-proxy each + plugin's web root under `/_p/{pathPrefix}/`, with optional CSS injection. +- UI: a "Plugins" navigation section, a plugin frame page, and a host↔plugin + `postMessage` protocol (context, navigate, resize, badge, title, ready). + +### Non-Goals + +- A new CRD for plugins (reuse `RemoteMCPServer`). +- Shipping a specific plugin (the Kanban plugin is EP-2048). +- Cross-origin plugin hosting (plugins are proxied same-origin via `/_p/`). + +## Implementation Details + +### The contract: `RemoteMCPServer.spec.ui` (`RemoteMCPServerUI`) + +Defined in `go/api/v1alpha2/remotemcpserver_types.go`; the CRD +(`go/api/config/crd/bases/kagent.dev_remotemcpservers.yaml`, mirrored in +`helm/kagent-crds/templates/`) and `zz_generated.deepcopy.go` are generated from it. + +| Field | Type | Default | Validation | Purpose | +|-------|------|---------|------------|---------| +| `enabled` | bool | `false` | — | Opt-in: this server provides a web UI. | +| `pathPrefix` | string | `` | `maxLength=63`, `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$` | URL segment for `/_p/{pathPrefix}/`. | +| `displayName` | string | `` | — | Sidebar label. | +| `icon` | string | `puzzle` | — | `lucide-react` icon name. | +| `section` | enum | `RESOURCES` | `OVERVIEW\|AGENTS\|WORKFLOWS\|KNOWLEDGE\|EVALUATIONS\|RESOURCES\|ADMIN\|PLUGINS` | Sidebar section. | +| `defaultPath` | string | — | — | Initial sub-path at plugin root. | +| `injectCSS` | string | — | — | CSS injected into proxied HTML. | + +### Backend (`go/core/internal/httpserver`) + +- `GET /api/plugins` — `PluginsHandler.HandleListPlugins` lists `RemoteMCPServer`s + across watched namespaces where `spec.ui.enabled`, projects each to + `api.PluginResponse` (`{name, namespace, pathPrefix, displayName, icon, section, + defaultPath}`), applies the defaults, and sorts by `pathPrefix`. Authorized as a + `ToolServer` resource. +- `/_p/{pathPrefix}/*` — `PluginsHandler.HandleProxy` resolves `{pathPrefix}` to its + `RemoteMCPServer`, authorizes against the backing `ToolServer`, derives the target + from the **host** of `spec.url` (proxying to the web root `/`), resolves + `spec.headersFrom` via `RemoteMCPServer.ResolveHeaders`, injects + `spec.ui.injectCSS` into `text/html` responses, and returns `502` when the + upstream is unreachable. +- Wiring (added to the shared `handlers.go`/`server.go`/`httpapi/types.go`): the + `Plugins` handler, the `/api/plugins` route, and the `/_p/{pathPrefix}` proxy + prefix. Only the plugins-related hunks of these shared files are included in this + PR; the MCP-apps hunks belong to EP-2046. + +### UI (`ui/src`) + +- **Navigation** — `components/sidebars/AppSidebar.tsx` + `AppSidebarNav.tsx` render + a "Plugins" section driven by `useSidebarStatus()`, with live badge updates via the + `kagent:plugin-badge` event. Supporting nav pieces: `NamespaceSelector`, + `StatusIndicator`, `SidebarCollapseButton`, `MobileTopBar`, and the + `sidebar-status-context` / `namespace-context` providers. The root `layout.tsx` is + refactored to a Server Component delegating to a `providers.tsx` client boundary. +- **Plugin list** — `app/actions/plugins.ts` (`getPlugins()` → `GET /api/plugins`, + `checkPluginBackend()` health probe) and the BFF route `app/api/plugins/route.ts`. +- **Plugin frame** — `app/plugins/[name]/[[...path]]/page.tsx` renders + `