From de1e4a51094abb1c07b485252f6885d399d9a202 Mon Sep 17 00:00:00 2001 From: Ivan Necas Date: Mon, 2 Mar 2026 11:28:12 +0100 Subject: [PATCH 1/3] OLS-2722: olsToolUIs initial support Allows plugins to define 'ols.tool-ui' extensions to map the tools to react components to be rendered when the tool gets called. Sample extensions registration: ```json { "type": "ols.tool-ui", "properties": { "id": "my-obs/my-tool", "component": { "$codeRef": "MyToolUI" } } } ``` The ToolUI implemntation receives the tool details in it's argument: ```ts type MyTool = { name: 'my-tool'; args: object, // ... }; export const MyToolUI React.FC<{ tool: MyTool }> = ({ tool }) => { // component implementation } ``` --- src/components/OlsToolUIs.tsx | 48 ++++++++++++++++++++++++++++++++ src/components/Prompt.tsx | 2 ++ src/components/ResponseTools.tsx | 2 ++ src/hooks/useToolUIMapping.ts | 39 ++++++++++++++++++++++++++ src/types.ts | 6 +++- 5 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/components/OlsToolUIs.tsx create mode 100644 src/hooks/useToolUIMapping.ts diff --git a/src/components/OlsToolUIs.tsx b/src/components/OlsToolUIs.tsx new file mode 100644 index 00000000..c0079f6e --- /dev/null +++ b/src/components/OlsToolUIs.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { Map as ImmutableMap } from 'immutable'; +import { useSelector } from 'react-redux'; +import { State } from '../redux-reducers'; +import { useToolUIMapping } from '../hooks/useToolUIMapping'; +import type { OlsToolUIComponent, Tool } from '../types'; + +type OlsToolUIProps = { + tool: Tool; + toolUIComponent: OlsToolUIComponent; +}; + +export const OlsToolUI: React.FC = ({ tool, toolUIComponent: toolUIElement }) => { + const ToolComponent = toolUIElement; + return ; +}; + +type OlsUIToolsProps = { + entryIndex: number; +}; + +export const OlsToolUIs: React.FC = ({ entryIndex }) => { + const [toolUIMapping] = useToolUIMapping(); + + const toolsData: ImmutableMap> = useSelector((s: State) => + s.plugins?.ols?.getIn(['chatHistory', entryIndex, 'tools']), + ); + + const olsToolsWithUI = toolsData + .map((value) => { + const tool = value.toJS() as Tool; + const toolUIComponent = tool.olsToolUiID && toolUIMapping[tool.olsToolUiID]; + return { tool, toolUIComponent }; + }) + .filter(({ tool, toolUIComponent }) => tool.status !== 'error' && !!toolUIComponent); + + return ( + <> + {olsToolsWithUI + .map(({ tool, toolUIComponent }, toolID) => ( + + )) + .valueSeq()} + + ); +}; + +export default OlsToolUIs; diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index ae5c135f..7b964f68 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -623,6 +623,7 @@ const Prompt: React.FC = ({ scrollIntoView }) => { tool_meta: toolMeta, } = json.data; const uiResourceUri = toolMeta?.ui?.resourceUri as string | undefined; + const olsToolUiID = toolMeta?.olsUi?.id as string | undefined; dispatch( chatHistoryUpdateTool(chatEntryID, id, { content, @@ -630,6 +631,7 @@ const Prompt: React.FC = ({ scrollIntoView }) => { ...(uiResourceUri && { uiResourceUri }), ...(serverName && { serverName }), ...(structuredContent && { structuredContent }), + ...(olsToolUiID && { olsToolUiID }), }), ); } else if (json.event === 'error') { diff --git a/src/components/ResponseTools.tsx b/src/components/ResponseTools.tsx index 221e30da..8a9f6515 100644 --- a/src/components/ResponseTools.tsx +++ b/src/components/ResponseTools.tsx @@ -8,6 +8,7 @@ import { openToolSet } from '../redux-actions'; import { State } from '../redux-reducers'; import { Tool } from '../types'; import MCPApp from './MCPApp'; +import OlsToolUIs from './OlsToolUIs'; type ToolProps = { entryIndex: number; @@ -64,6 +65,7 @@ const ResponseTools: React.FC = ({ entryIndex }) => { .map((toolID) => ( ))} + {tools .keySeq() diff --git a/src/hooks/useToolUIMapping.ts b/src/hooks/useToolUIMapping.ts new file mode 100644 index 00000000..774e18d6 --- /dev/null +++ b/src/hooks/useToolUIMapping.ts @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { useResolvedExtensions } from '@openshift-console/dynamic-plugin-sdk'; +import type { + CodeRef, + Extension, + ExtensionDeclaration, +} from '@openshift-console/dynamic-plugin-sdk/lib/types'; +import type { OlsToolUIComponent } from '../types'; + +type ToolUIExtensionProperties = { + /** Id of the component (as refferrenced by the mcp tool) */ + id: string; + /** The component to be rendered when the MCP tool matches. */ + component: CodeRef; +}; + +type ToolUIExtension = ExtensionDeclaration<'ols.tool-ui', ToolUIExtensionProperties>; + +const isToolUIExtension = (e: Extension): e is ToolUIExtension => e.type === 'ols.tool-ui'; + +export const useToolUIExtensions = () => useResolvedExtensions(isToolUIExtension); + +export const useToolUIMapping = (): [Record, boolean] => { + const [extensions, resolved] = useToolUIExtensions(); + + const mapping = React.useMemo(() => { + const result: Record = {}; + extensions.forEach((extension) => { + const { id, component } = extension.properties as { + id: string; + component: OlsToolUIComponent; + }; + result[id] = component; + }); + return result; + }, [extensions]); + + return [mapping, resolved]; +}; diff --git a/src/types.ts b/src/types.ts index 000f7db2..40410ae3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +import * as React from 'react'; import { Map as ImmutableMap } from 'immutable'; import { ErrorType } from './error'; @@ -27,15 +28,18 @@ export type ReferencedDoc = { }; export type Tool = { - args: { [key: string]: Array }; + args: { [key: string]: string }; content: string; name: string; status: 'error' | 'success' | 'truncated'; uiResourceUri?: string; serverName?: string; structuredContent?: Record; + olsToolUiID: string; }; +export type OlsToolUIComponent = React.ComponentType<{ tool: Tool }>; + type ChatEntryUser = { attachments: { [key: string]: Attachment }; hidden?: boolean; From 6d94fd0dc5579f21ef49a39364e415d9e1bc947c Mon Sep 17 00:00:00 2001 From: Ivan Necas Date: Fri, 27 Mar 2026 16:47:58 +0100 Subject: [PATCH 2/3] OLS-2722: address review comments --- README.md | 50 +++++++++++++++++++++++++++++++++++ src/components/OlsToolUIs.tsx | 10 ++++--- src/hooks/useToolUIMapping.ts | 2 +- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6722b882..8baa3ac1 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,56 @@ import { useFlag } from '@openshift-console/dynamic-plugin-sdk'; const isLightspeedRunning = useFlag('LIGHTSPEED_CONSOLE'); ``` +## Providing tool visualization from an external plugin + +Other plugins can define a visualization for a specific MCP tool. + +In order to do so, they need to: + +1. annotate the particular MCP tool inside the MCP server: + +``` json +_meta: { + additionalFields: { + olsUi: { + id: 'my-mcp/my-tool', + }, + } +} +``` + +2. define an extension of type `ols.tool-ui` inside the plugin, connecting the tool (using the annotated id) +with the particular component: + +``` json +{ + "type": "ols.tool-ui", + "properties": { + "id": "my-obs/my-tool", + "component": { + "$codeRef": "MyToolUI" + } + } +} +``` + +This needs to follow the standard `openshift-console/dynamic-plugin-sdk` practices +of exporting the referenced component. + +3. Once the MCP tool gets called, OLS passes the tool details to the ToolUI component in the `tool` argument: + +``` typescript +type MyTool = { + name: 'my-tool'; + args: object, + // ... +}; + +export const MyToolUI React.FC<{ tool: MyTool }> = ({ tool }) => { + // component implementation +} +``` + ## References - [Console Plugin SDK README](https://github.com/openshift/console/tree/main/frontend/packages/console-dynamic-plugin-sdk) diff --git a/src/components/OlsToolUIs.tsx b/src/components/OlsToolUIs.tsx index c0079f6e..3c85bd9e 100644 --- a/src/components/OlsToolUIs.tsx +++ b/src/components/OlsToolUIs.tsx @@ -4,16 +4,18 @@ import { useSelector } from 'react-redux'; import { State } from '../redux-reducers'; import { useToolUIMapping } from '../hooks/useToolUIMapping'; import type { OlsToolUIComponent, Tool } from '../types'; +import ErrorBoundary from './ErrorBoundary'; type OlsToolUIProps = { tool: Tool; toolUIComponent: OlsToolUIComponent; }; -export const OlsToolUI: React.FC = ({ tool, toolUIComponent: toolUIElement }) => { - const ToolComponent = toolUIElement; - return ; -}; +export const OlsToolUI: React.FC = ({ tool, toolUIComponent: ToolComponent }) => ( + + + +); type OlsUIToolsProps = { entryIndex: number; diff --git a/src/hooks/useToolUIMapping.ts b/src/hooks/useToolUIMapping.ts index 774e18d6..209e9f46 100644 --- a/src/hooks/useToolUIMapping.ts +++ b/src/hooks/useToolUIMapping.ts @@ -8,7 +8,7 @@ import type { import type { OlsToolUIComponent } from '../types'; type ToolUIExtensionProperties = { - /** Id of the component (as refferrenced by the mcp tool) */ + /** ID of the component (as referenced by the MCP tool) */ id: string; /** The component to be rendered when the MCP tool matches. */ component: CodeRef; From 34d08ffee59830ccb9a2e3dec85245601f747f63 Mon Sep 17 00:00:00 2001 From: Ivan Necas Date: Mon, 30 Mar 2026 16:53:12 +0200 Subject: [PATCH 3/3] OLS-2722: address code-rabbit comments --- README.md | 12 ++++++------ src/components/OlsToolUIs.tsx | 4 ++++ src/types.ts | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8baa3ac1..b35ae3fd 100644 --- a/README.md +++ b/README.md @@ -267,10 +267,10 @@ In order to do so, they need to: 1. annotate the particular MCP tool inside the MCP server: ``` json -_meta: { - additionalFields: { - olsUi: { - id: 'my-mcp/my-tool', +"_meta": { + "additionalFields": { + "olsUi": { + "id": "my-mcp/my-tool", }, } } @@ -283,7 +283,7 @@ with the particular component: { "type": "ols.tool-ui", "properties": { - "id": "my-obs/my-tool", + "id": "my-mcp/my-tool", "component": { "$codeRef": "MyToolUI" } @@ -303,7 +303,7 @@ type MyTool = { // ... }; -export const MyToolUI React.FC<{ tool: MyTool }> = ({ tool }) => { +export const MyToolUI: React.FC<{ tool: MyTool }> = ({ tool }) => { // component implementation } ``` diff --git a/src/components/OlsToolUIs.tsx b/src/components/OlsToolUIs.tsx index 3c85bd9e..f59b46e6 100644 --- a/src/components/OlsToolUIs.tsx +++ b/src/components/OlsToolUIs.tsx @@ -28,6 +28,10 @@ export const OlsToolUIs: React.FC = ({ entryIndex }) => { s.plugins?.ols?.getIn(['chatHistory', entryIndex, 'tools']), ); + if (!toolsData) { + return null; + } + const olsToolsWithUI = toolsData .map((value) => { const tool = value.toJS() as Tool; diff --git a/src/types.ts b/src/types.ts index 40410ae3..77db045b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,14 +28,14 @@ export type ReferencedDoc = { }; export type Tool = { - args: { [key: string]: string }; + args: { [key: string]: unknown }; content: string; name: string; status: 'error' | 'success' | 'truncated'; uiResourceUri?: string; serverName?: string; structuredContent?: Record; - olsToolUiID: string; + olsToolUiID?: string; }; export type OlsToolUIComponent = React.ComponentType<{ tool: Tool }>;