Skip to content

Commit 94747b3

Browse files
authored
Merge pull request #385 from headlamp-k8s/add-mcp-server
ai-assistant: Add ability to run mcp server from ai-assistant plugin
2 parents 97ee165 + 82f7896 commit 94747b3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+12169
-868
lines changed

ai-assistant/package-lock.json

Lines changed: 1170 additions & 386 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ai-assistant/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@
4141
"@langchain/core": "^1.1.20",
4242
"@langchain/deepseek": "1.0.8",
4343
"@langchain/google-genai": "^2.1.16",
44+
"@langchain/mcp-adapters": "^1.1.3",
4445
"@langchain/mistralai": "^1.0.4",
4546
"@langchain/ollama": "^1.2.2",
4647
"@langchain/openai": "^1.2.6",
48+
"@modelcontextprotocol/sdk": "^1.17.5",
4749
"@monaco-editor/react": "^4.5.2",
4850
"@types/prismjs": "^1.26.5",
4951
"@types/react-syntax-highlighter": "^15.5.13",

ai-assistant/src/ContentRenderer.tsx

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Link as RouterLink, useHistory } from 'react-router-dom';
66
import remarkGfm from 'remark-gfm';
77
import YAML from 'yaml';
88
import { LogsButton, YamlDisplay } from './components';
9+
import MCPFormattedMessage from './components/chat/MCPFormattedMessage';
910
import { getHeadlampLink } from './utils/promptLinkHelper';
1011
import { parseKubernetesYAML } from './utils/SampleYamlLibrary';
1112

@@ -87,6 +88,8 @@ const parseLogsButtonData = (content: string, logsButtonIndex: number): ParseRes
8788
interface ContentRendererProps {
8889
content: string;
8990
onYamlDetected?: (yaml: string, resourceType: string) => void;
91+
promptWidth?: string; // Add width prop
92+
onRetryTool?: (toolName: string, args: Record<string, any>) => void;
9093
}
9194

9295
// Table wrapper component with show more functionality - moved outside to preserve state
@@ -96,7 +99,14 @@ const TableWrapper: React.FC<{ children: React.ReactNode }> = React.memo(({ chil
9699

97100
// Extract table rows from children
98101
const tableElement = React.Children.only(children) as React.ReactElement;
99-
const tbody = React.Children.toArray(tableElement.props.children).find(
102+
const tableChildren = tableElement.props.children;
103+
104+
if (!tableChildren) {
105+
// No children found, return table as is
106+
return <Box sx={{ overflowX: 'auto', width: '100%', mb: 2 }}>{children}</Box>;
107+
}
108+
109+
const tbody = React.Children.toArray(tableChildren).find(
100110
(child: any) => child?.type === 'tbody' || child?.props?.component === 'tbody'
101111
);
102112

@@ -106,7 +116,8 @@ const TableWrapper: React.FC<{ children: React.ReactNode }> = React.memo(({ chil
106116
}
107117

108118
const tbodyElement = tbody as React.ReactElement;
109-
const rows = React.Children.toArray(tbodyElement.props.children);
119+
const tbodyChildren = tbodyElement.props.children;
120+
const rows = tbodyChildren ? React.Children.toArray(tbodyChildren) : [];
110121
const hasMoreRows = rows.length > maxRows;
111122
const visibleRows = showAll ? rows : rows.slice(0, maxRows);
112123

@@ -117,7 +128,7 @@ const TableWrapper: React.FC<{ children: React.ReactNode }> = React.memo(({ chil
117128

118129
// Clone the table with the limited tbody
119130
const limitedTable = React.cloneElement(tableElement, {
120-
children: React.Children.map(tableElement.props.children, (child: any) => {
131+
children: React.Children.map(tableChildren, (child: any) => {
121132
if (child?.type === 'tbody' || child?.props?.component === 'tbody') {
122133
return limitedTbody;
123134
}
@@ -276,7 +287,7 @@ markdownComponents.li.displayName = 'MarkdownLi';
276287
markdownComponents.blockquote.displayName = 'MarkdownBlockquote';
277288

278289
const ContentRenderer: React.FC<ContentRendererProps> = React.memo(
279-
({ content, onYamlDetected }) => {
290+
({ content, onYamlDetected, onRetryTool }) => {
280291
const history = useHistory();
281292
// Create code component that has access to onYamlDetected
282293
const CodeComponent = React.useMemo(() => {
@@ -531,7 +542,18 @@ const ContentRenderer: React.FC<ContentRendererProps> = React.memo(
531542
const processedContent = useMemo(() => {
532543
if (!content) return null;
533544

534-
// First, check if content is a JSON response with error or success keys
545+
// First, check if content is a formatted MCP output (pure JSON)
546+
try {
547+
const parsed = JSON.parse(content.trim());
548+
if (parsed.formatted && parsed.mcpOutput) {
549+
// This is a formatted MCP output, use our specialized component
550+
return <MCPFormattedMessage content={content} isAssistant onRetryTool={onRetryTool} />;
551+
}
552+
} catch (error) {
553+
// Not JSON or not formatted MCP output, continue with normal processing
554+
}
555+
556+
// Second, check if content is a JSON response with error or success keys
535557
const jsonParseResult = parseJsonContent(content.trim());
536558
if (jsonParseResult.success) {
537559
const parsedContent = jsonParseResult.data;
@@ -563,8 +585,8 @@ const ContentRenderer: React.FC<ContentRendererProps> = React.memo(
563585
<Box
564586
component="pre"
565587
sx={{
566-
backgroundColor: theme => theme.palette.grey[100],
567-
color: theme => theme.palette.grey[900],
588+
backgroundColor: (theme: any) => theme.palette.grey[100],
589+
color: (theme: any) => theme.palette.grey[900],
568590
padding: 2,
569591
borderRadius: 1,
570592
overflowX: 'auto',
@@ -640,7 +662,7 @@ const ContentRenderer: React.FC<ContentRendererProps> = React.memo(
640662
{content}
641663
</ReactMarkdown>
642664
);
643-
}, [content, onYamlDetected, processUnformattedYaml]);
665+
}, [content, onYamlDetected, onRetryTool, processUnformattedYaml]);
644666

645667
return (
646668
<Box sx={{ width: '100%', overflowWrap: 'break-word', wordWrap: 'break-word' }}>
@@ -652,7 +674,8 @@ const ContentRenderer: React.FC<ContentRendererProps> = React.memo(
652674
// Only re-render if content or onYamlDetected actually changed
653675
return (
654676
prevProps.content === nextProps.content &&
655-
prevProps.onYamlDetected === nextProps.onYamlDetected
677+
prevProps.onYamlDetected === nextProps.onYamlDetected &&
678+
prevProps.onRetryTool === nextProps.onRetryTool
656679
);
657680
}
658681
);

ai-assistant/src/ai/manager.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export type ToolCall = {
33
name: string;
44
description?: string;
55
arguments: Record<string, any>;
6-
type: 'regular';
6+
type: 'mcp' | 'regular';
77
};
88

99
export type AgentThinkingStep = {
@@ -29,6 +29,15 @@ export type Prompt = {
2929
agentThinkingSteps?: AgentThinkingStep[];
3030
/** Whether the agent run is complete (thinking block should collapse) */
3131
agentThinkingDone?: boolean;
32+
// Add support for inline tool confirmations
33+
toolConfirmation?: {
34+
tools: ToolCall[];
35+
onApprove: (approvedToolIds: string[]) => void;
36+
onDeny: () => void;
37+
loading?: boolean;
38+
//TODO: added this, because there was no userContext
39+
userContext?: any; // Additional context about the user or conversation for tool confirmation
40+
};
3241
};
3342

3443
export default abstract class AIManager {

0 commit comments

Comments
 (0)