-
Notifications
You must be signed in to change notification settings - Fork 27
OLS-2722: olsToolUIs initial support #1576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Comment on lines
+299
to
+307
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Expect 1 match before fix, 0 matches after fix
rg -n -C2 'export const MyToolUI React\.FC<\{ tool: MyTool \}>' README.mdRepository: openshift/lightspeed-console Length of output: 194 Add missing colon in TypeScript component declaration. Line 306 is missing the Proposed fix-export const MyToolUI React.FC<{ tool: MyTool }> = ({ tool }) => {
+export const MyToolUI: React.FC<{ tool: MyTool }> = ({ tool }) => {🤖 Prompt for AI Agents |
||
| } | ||
| ``` | ||
|
|
||
| ## References | ||
|
|
||
| - [Console Plugin SDK README](https://github.com/openshift/console/tree/main/frontend/packages/console-dynamic-plugin-sdk) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,50 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ErrorBoundary from './ErrorBoundary'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type OlsToolUIProps = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tool: Tool; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toolUIComponent: OlsToolUIComponent; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const OlsToolUI: React.FC<OlsToolUIProps> = ({ tool, toolUIComponent: ToolComponent }) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ErrorBoundary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ToolComponent tool={tool} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </ErrorBoundary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type OlsUIToolsProps = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entryIndex: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const OlsToolUIs: React.FC<OlsUIToolsProps> = ({ entryIndex }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [toolUIMapping] = useToolUIMapping(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const toolsData: ImmutableMap<string, ImmutableMap<string, unknown>> = 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add null guard for If Proposed fix const toolsData: ImmutableMap<string, ImmutableMap<string, unknown>> = useSelector((s: State) =>
s.plugins?.ols?.getIn(['chatHistory', entryIndex, 'tools']),
);
+ if (!toolsData) {
+ return null;
+ }
+
const olsToolsWithUI = toolsData
.map((value) => {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {olsToolsWithUI | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(({ tool, toolUIComponent }, toolID) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <OlsToolUI key={`ols-tool-ui-${toolID}`} tool={tool} toolUIComponent={toolUIComponent} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .valueSeq()} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default OlsToolUIs; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 referenced by the MCP tool) */ | ||
| id: string; | ||
| /** The component to be rendered when the MCP tool matches. */ | ||
| component: CodeRef<OlsToolUIComponent>; | ||
| }; | ||
|
|
||
| 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<string, OlsToolUIComponent>, boolean] => { | ||
| const [extensions, resolved] = useToolUIExtensions(); | ||
|
|
||
| const mapping = React.useMemo(() => { | ||
| const result: Record<string, OlsToolUIComponent> = {}; | ||
| extensions.forEach((extension) => { | ||
| const { id, component } = extension.properties as { | ||
| id: string; | ||
| component: OlsToolUIComponent; | ||
| }; | ||
| result[id] = component; | ||
| }); | ||
| return result; | ||
| }, [extensions]); | ||
|
|
||
| return [mapping, resolved]; | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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<string> }; | ||||||
| args: { [key: string]: string }; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for existing usages of tool.args to verify the expected structure
rg -n "tool\.args|args\[" --type=ts -C3Repository: openshift/lightspeed-console Length of output: 54 🏁 Script executed: cat -n src/types.ts | head -50Repository: openshift/lightspeed-console Length of output: 1392 🏁 Script executed: rg -n "args" src/types.ts -B2 -A2Repository: openshift/lightspeed-console Length of output: 178 🏁 Script executed: # Search more broadly for how args is used in the codebase
rg "\.args" --type=ts -i | head -20Repository: openshift/lightspeed-console Length of output: 529 🏁 Script executed: # Search for API response examples or test data that might show the actual structure
fd -e "test\|spec\|mock" -t f | head -10Repository: openshift/lightspeed-console Length of output: 54 🏁 Script executed: # Search for Tool type usage
rg "tool\.args|Tool\>" --type=ts -A2 -B2 | head -40Repository: openshift/lightspeed-console Length of output: 170 🏁 Script executed: # Look for streaming or API-related code
fd -e ts -e tsx | xargs rg "stream|api|tool_call" -l | head -10Repository: openshift/lightspeed-console Length of output: 319 🏁 Script executed: # Check git history to see what the type changed from
git log --oneline -p src/types.ts | head -100Repository: openshift/lightspeed-console Length of output: 1818 🏁 Script executed: rg "tool\.args" src/components/ -B3 -A3Repository: openshift/lightspeed-console Length of output: 54 🏁 Script executed: rg "args" src/attachments.ts -B2 -A2Repository: openshift/lightspeed-console Length of output: 54 🏁 Script executed: # Check if Tool type existed in a different location before
git log --all --full-history -S "type Tool" --onelineRepository: openshift/lightspeed-console Length of output: 158 🏁 Script executed: cat src/components/Prompt.tsx | grep -n "tool" -i -C3 | head -50Repository: openshift/lightspeed-console Length of output: 1415 🏁 Script executed: cat src/components/AttachEventsModal.tsx | grep -n "tool\|args" -i -C2 | head -40Repository: openshift/lightspeed-console Length of output: 54 🏁 Script executed: # Search for any access to Tool properties
rg "tool\[|tool\." src/ --type=ts --type=tsx -C2Repository: openshift/lightspeed-console Length of output: 99 🏁 Script executed: rg "tool\[|tool\." src/ -C2 | head -80Repository: openshift/lightspeed-console Length of output: 1975 🏁 Script executed: # Check what the review is referring to - search for Array<string> type references
rg "Array<string>" src/types.tsRepository: openshift/lightspeed-console Length of output: 54 🏁 Script executed: # Look at the full context of how Tool type is used in components
fd -e ts -e tsx -path "*/src/components/*" | xargs cat | grep -A5 -B5 "Tool"Repository: openshift/lightspeed-console Length of output: 240 🏁 Script executed: # Check if there's any documentation or API reference
fd -e md | xargs cat | grep -i "args\|tool" | head -20Repository: openshift/lightspeed-console Length of output: 54 🏁 Script executed: rg "args.*Record|args.*unknown" src/ -B2 -A2Repository: openshift/lightspeed-console Length of output: 811 🏁 Script executed: # Check all Tool property usages to see if args is accessed directly anywhere
rg "\.args\[|\.args\." src/ -C3Repository: openshift/lightspeed-console Length of output: 54 Fix The 🤖 Prompt for AI Agents |
||||||
| content: string; | ||||||
| name: string; | ||||||
| status: 'error' | 'success' | 'truncated'; | ||||||
| uiResourceUri?: string; | ||||||
| serverName?: string; | ||||||
| structuredContent?: Record<string, unknown>; | ||||||
| olsToolUiID: string; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The
Proposed fix- olsToolUiID: string;
+ olsToolUiID?: string;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| }; | ||||||
|
|
||||||
| export type OlsToolUIComponent = React.ComponentType<{ tool: Tool }>; | ||||||
|
|
||||||
| type ChatEntryUser = { | ||||||
| attachments: { [key: string]: Attachment }; | ||||||
| hidden?: boolean; | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the same tool UI id in both setup steps.
Line 273 (
my-mcp/my-tool) and Line 286 (my-obs/my-tool) differ, but the mapping requires an exact id match. With this mismatch, the Tool UI will not resolve.🤖 Prompt for AI Agents