diff --git a/frontend/src/components/ChatPanel.tsx b/frontend/src/components/ChatPanel.tsx
index 53d7a98..d44d597 100644
--- a/frontend/src/components/ChatPanel.tsx
+++ b/frontend/src/components/ChatPanel.tsx
@@ -3,6 +3,7 @@ import type { KeyboardEvent } from 'react'
import ReactMarkdown from 'react-markdown'
import { IconSend } from '@tabler/icons-react'
import { ToolCallCard } from './ToolCallCard'
+import { MCPAppRenderer } from './MCPAppRenderer'
import type { ChatMessage } from '../hooks/useChat'
interface ChatPanelProps {
@@ -61,9 +62,19 @@ export function ChatPanel({ messages, isStreaming, onSend, sessionId }: ChatPane
{message.role === 'assistant' ? (
{message.content}
- {message.toolCalls?.map(tc => (
-
- ))}
+ {message.toolCalls?.map(tc =>
+ tc._meta?.ui?.resourceUri ? (
+ onSend(content)}
+ />
+ ) : (
+
+ )
+ )}
) : (
{message.content}
diff --git a/frontend/src/test/ChatPanel.test.tsx b/frontend/src/test/ChatPanel.test.tsx
index 4b434a1..7f23bc1 100644
--- a/frontend/src/test/ChatPanel.test.tsx
+++ b/frontend/src/test/ChatPanel.test.tsx
@@ -14,6 +14,13 @@ vi.mock('react-markdown', () => ({
default: ({ children }: { children: string }) => {children}
,
}))
+// Mock MCPAppRenderer to avoid fetch/iframe in test environment
+vi.mock('../components/MCPAppRenderer', () => ({
+ MCPAppRenderer: ({ resourceUri }: { resourceUri: string }) => (
+
+ ),
+}))
+
describe('ChatPanel', () => {
const noop = () => undefined
@@ -90,6 +97,72 @@ describe('ChatPanel', () => {
expect(container.querySelector('.tool-call-card')).toBeTruthy()
})
+ it('renders MCPAppRenderer for tool calls with _meta.ui.resourceUri', () => {
+ const messages: ChatMessage[] = [
+ {
+ id: '1',
+ role: 'assistant',
+ content: 'Running an MCP app',
+ toolCalls: [
+ {
+ id: 'tc-mcp',
+ name: 'mcp_app',
+ args: {},
+ status: 'complete',
+ _meta: { ui: { resourceUri: 'app://chart' } },
+ structuredContent: { value: 42 },
+ },
+ ],
+ },
+ ]
+ render(
+
+ )
+ expect(screen.getByTestId('mcp-app-renderer')).toBeTruthy()
+ })
+
+ it('does not render ToolCallCard for tool calls with _meta.ui.resourceUri', () => {
+ const messages: ChatMessage[] = [
+ {
+ id: '1',
+ role: 'assistant',
+ content: 'Running an MCP app',
+ toolCalls: [
+ {
+ id: 'tc-mcp',
+ name: 'mcp_app',
+ args: {},
+ status: 'complete',
+ _meta: { ui: { resourceUri: 'app://chart' } },
+ structuredContent: { value: 42 },
+ },
+ ],
+ },
+ ]
+ const { container } = render(
+
+ )
+ expect(container.querySelector('.tool-call-card')).toBeNull()
+ })
+
+ it('renders ToolCallCard for tool calls without _meta.ui.resourceUri', () => {
+ const messages: ChatMessage[] = [
+ {
+ id: '1',
+ role: 'assistant',
+ content: 'Running a plain tool',
+ toolCalls: [
+ { id: 'tc-plain', name: 'bash', args: {}, status: 'complete' },
+ ],
+ },
+ ]
+ const { container } = render(
+
+ )
+ expect(container.querySelector('.tool-call-card')).toBeTruthy()
+ expect(screen.queryByTestId('mcp-app-renderer')).toBeNull()
+ })
+
it('shows Researching... when isStreaming and messages exist', () => {
const messages: ChatMessage[] = [
{ id: '1', role: 'user', content: 'Question' },