Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Comment on lines +267 to +291
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 267 - 291, The README shows mismatched IDs between
the MCP annotation (_meta.additionalFields.olsUi.id set to 'my-mcp/my-tool') and
the plugin extension (the "ols.tool-ui" extension's properties.id set to
'my-obs/my-tool'); update one so both use the exact same tool UI id (e.g., make
both 'my-mcp/my-tool' or both 'my-obs/my-tool') so the mapping resolves; verify
the id in the annotation and the "ols.tool-ui" extension's properties.id match
exactly and keep the component.$codeRef (MyToolUI) unchanged.

```

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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.md

Repository: openshift/lightspeed-console

Length of output: 194


Add missing colon in TypeScript component declaration.

Line 306 is missing the : between MyToolUI and React.FC, making the example invalid TypeScript that won't compile when copied.

Proposed fix
-export const MyToolUI React.FC<{ tool: MyTool }> = ({ tool }) => {
+export const MyToolUI: React.FC<{ tool: MyTool }> = ({ tool }) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 299 - 307, The example TypeScript component
declaration is missing the colon between the component name and its type
annotation; update the declaration for MyToolUI so the identifier MyToolUI is
followed by a colon and the React.FC type annotation (i.e., change the
declaration with MyToolUI React.FC<{ tool: MyTool }> to use a colon between
MyToolUI and React.FC) so the example compiles as valid TypeScript.

}
```

## References

- [Console Plugin SDK README](https://github.com/openshift/console/tree/main/frontend/packages/console-dynamic-plugin-sdk)
Expand Down
50 changes: 50 additions & 0 deletions src/components/OlsToolUIs.tsx
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add null guard for toolsData to prevent runtime errors.

If entryIndex is invalid or the chat entry doesn't have tools yet, getIn returns undefined. Calling .map() on undefined will throw a runtime error.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
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) => {
const tool = value.toJS() as Tool;
const toolUIComponent = tool.olsToolUiID && toolUIMapping[tool.olsToolUiID];
return { tool, toolUIComponent };
})
.filter(({ tool, toolUIComponent }) => tool.status !== 'error' && !!toolUIComponent);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/OlsToolUIs.tsx` around lines 27 - 37, toolsData can be
undefined causing olsToolsWithUI to throw when calling .map(); guard by
converting undefined to an empty Immutable Map or early-return before mapping:
in the selector call/assignment for toolsData (where useSelector retrieves
s.plugins?.ols?.getIn(['chatHistory', entryIndex, 'tools'])), ensure toolsData
is set to an empty Immutable.Map() when falsy, or check toolsData before
creating olsToolsWithUI; then run .map()/.filter only on a guaranteed Immutable
collection so olsToolsWithUI, toolUIMapping, and downstream code using tool and
toolUIComponent are safe.


return (
<>
{olsToolsWithUI
.map(({ tool, toolUIComponent }, toolID) => (
<OlsToolUI key={`ols-tool-ui-${toolID}`} tool={tool} toolUIComponent={toolUIComponent} />
))
.valueSeq()}
</>
);
};

export default OlsToolUIs;
2 changes: 2 additions & 0 deletions src/components/Prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -623,13 +623,15 @@ const Prompt: React.FC<PromptProps> = ({ 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,
status,
...(uiResourceUri && { uiResourceUri }),
...(serverName && { serverName }),
...(structuredContent && { structuredContent }),
...(olsToolUiID && { olsToolUiID }),
}),
);
} else if (json.event === 'error') {
Expand Down
2 changes: 2 additions & 0 deletions src/components/ResponseTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -64,6 +65,7 @@ const ResponseTools: React.FC<ResponseToolsProps> = ({ entryIndex }) => {
.map((toolID) => (
<MCPApp entryIndex={entryIndex} key={`mcp-app-${toolID}`} toolID={toolID} />
))}
<OlsToolUIs entryIndex={entryIndex} />
<LabelGroup numLabels={4}>
{tools
.keySeq()
Expand Down
39 changes: 39 additions & 0 deletions src/hooks/useToolUIMapping.ts
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];
};
6 changes: 5 additions & 1 deletion src/types.ts
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';
Expand Down Expand Up @@ -27,15 +28,18 @@ export type ReferencedDoc = {
};

export type Tool = {
args: { [key: string]: Array<string> };
args: { [key: string]: string };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 -C3

Repository: openshift/lightspeed-console

Length of output: 54


🏁 Script executed:

cat -n src/types.ts | head -50

Repository: openshift/lightspeed-console

Length of output: 1392


🏁 Script executed:

rg -n "args" src/types.ts -B2 -A2

Repository: 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 -20

Repository: 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 -10

Repository: openshift/lightspeed-console

Length of output: 54


🏁 Script executed:

# Search for Tool type usage
rg "tool\.args|Tool\>" --type=ts -A2 -B2 | head -40

Repository: 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 -10

Repository: 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 -100

Repository: openshift/lightspeed-console

Length of output: 1818


🏁 Script executed:

rg "tool\.args" src/components/ -B3 -A3

Repository: openshift/lightspeed-console

Length of output: 54


🏁 Script executed:

rg "args" src/attachments.ts -B2 -A2

Repository: 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" --oneline

Repository: openshift/lightspeed-console

Length of output: 158


🏁 Script executed:

cat src/components/Prompt.tsx | grep -n "tool" -i -C3 | head -50

Repository: openshift/lightspeed-console

Length of output: 1415


🏁 Script executed:

cat src/components/AttachEventsModal.tsx | grep -n "tool\|args" -i -C2 | head -40

Repository: openshift/lightspeed-console

Length of output: 54


🏁 Script executed:

# Search for any access to Tool properties
rg "tool\[|tool\." src/ --type=ts --type=tsx -C2

Repository: openshift/lightspeed-console

Length of output: 99


🏁 Script executed:

rg "tool\[|tool\." src/ -C2 | head -80

Repository: 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.ts

Repository: 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 -20

Repository: openshift/lightspeed-console

Length of output: 54


🏁 Script executed:

rg "args.*Record|args.*unknown" src/ -B2 -A2

Repository: 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/ -C3

Repository: openshift/lightspeed-console

Length of output: 54


Fix args type to match actual usage patterns.

The args field type is defined as { [key: string]: string }, but the codebase expects it to hold values of any type. In ResponseToolModal.tsx, the code casts args as Record<string, unknown>, and in MCPApp.tsx, handleToolCall expects args: Record<string, unknown>. The type definition should be changed to Record<string, unknown> to reflect the actual API contract and prevent type safety issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types.ts` at line 31, The args field in the shared type is declared as "{
[key: string]: string }" but the codebase uses it as a generic map
(ResponseToolModal.tsx casts to Record<string, unknown> and MCPApp.tsx's
handleToolCall expects Record<string, unknown>). Update the type definition for
args to Record<string, unknown> in the declaration (the symbol to change is the
args field in the type in src/types.ts) so it matches the actual usage and
avoids unsafe casts.

content: string;
name: string;
status: 'error' | 'success' | 'truncated';
uiResourceUri?: string;
serverName?: string;
structuredContent?: Record<string, unknown>;
olsToolUiID: string;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

olsToolUiID should be optional to match actual usage.

The olsToolUiID field is defined as required, but the implementation shows it's conditionally populated:

  • In Prompt.tsx (line 634), it's only included when toolMeta?.olsUi?.id is present
  • In OlsToolUIs.tsx (line 34), there's a guard tool.olsToolUiID && toolUIMapping[...] suggesting it can be falsy
  • Tool entries are created incrementally via tool_call and tool_result events, where olsToolUiID only arrives with tool_result
Proposed fix
-  olsToolUiID: string;
+  olsToolUiID?: string;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
olsToolUiID: string;
olsToolUiID?: string;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types.ts` at line 38, The olsToolUiID property in the type definition is
currently required but is treated as optional in usage (see olsToolUiID
references in Prompt.tsx and OlsToolUIs.tsx and the fact it may arrive only with
tool_result events), so update the type that declares olsToolUiID to make it
optional (e.g., change olsToolUiID: string to olsToolUiID?: string) so the
compiler reflects actual runtime behavior and avoids false positives in places
that guard against its absence; ensure you update the interface or type in
src/types.ts where olsToolUiID is declared and run type checks to verify callers
(Prompt.tsx, OlsToolUIs.tsx, any tool_call/tool_result handlers) still compile.

};

export type OlsToolUIComponent = React.ComponentType<{ tool: Tool }>;

type ChatEntryUser = {
attachments: { [key: string]: Attachment };
hidden?: boolean;
Expand Down