Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
657e9c9
chore(frontend): bump @modelcontextprotocol/sdk to 1.29.0
malinskibeniamin Apr 18, 2026
d7c0cdb
feat(frontend): wire MCP transport.onerror to log transport-level fai…
malinskibeniamin Apr 19, 2026
1138c3f
feat(frontend): stream MCP tool calls with progress in AI agents
malinskibeniamin Apr 19, 2026
8db6752
feat(frontend): adopt MCP OAuthClientProvider wrapping console JWT
malinskibeniamin Apr 19, 2026
b3c303d
style(frontend): apply biome formatter to MCP SDK adoption files
malinskibeniamin Apr 19, 2026
4bac0cc
chore(frontend): clean up lint on MCP SDK adoption files
malinskibeniamin Apr 19, 2026
f601740
test(frontend): extend ConsoleJWTOAuthProvider test coverage
malinskibeniamin Apr 19, 2026
fe979f8
test(frontend): cover MCP transport wiring and stream mutation paths
malinskibeniamin Apr 19, 2026
4564057
test(frontend): cover MCP inspector tab streaming progress UI
malinskibeniamin Apr 19, 2026
332b3d7
fix(frontend): guard MCP streaming against hung mutations
malinskibeniamin Apr 19, 2026
f137752
fix(frontend): detect expired JWT in ConsoleJWTOAuthProvider.tokens()
malinskibeniamin Apr 19, 2026
2eb46a4
test(frontend): pin transport.onerror does not toast — mutation owns …
malinskibeniamin Apr 19, 2026
2522583
test(frontend): harden MCP critical path for concurrent/boundary cases
malinskibeniamin Apr 19, 2026
094f5f4
test(frontend): browser test + screenshot of MCP streaming inspector
malinskibeniamin Apr 19, 2026
598864c
chore(frontend): drop accidentally-committed failure screenshot
malinskibeniamin Apr 19, 2026
804a5da
ci(frontend): hoist browser test regex and drop formatter drift
malinskibeniamin Apr 19, 2026
f3fcfdf
fix(frontend): drop component-level toast to avoid double-fire with m…
malinskibeniamin Apr 19, 2026
d8847fa
fix(frontend): throw descriptive error when MCP stream yields empty e…
malinskibeniamin Apr 19, 2026
45452ed
fix(frontend): close MCP client in finally to avoid transport connect…
malinskibeniamin Apr 19, 2026
8984e2a
fix(frontend): apply streamTimeoutMs on capability-fallback callTool …
malinskibeniamin Apr 19, 2026
f1dd8cb
test(frontend): hoist regex and drop Error cast in new mcp test
malinskibeniamin Apr 19, 2026
f1723ac
fix(frontend): address review round 2 on MCP client lifecycle and hea…
malinskibeniamin Apr 19, 2026
e64f8ab
fix(frontend): address review round 3 on abort guard and state merging
malinskibeniamin Apr 19, 2026
6546212
test(frontend): polish mcp test fixtures and add callTool abort parity
malinskibeniamin Apr 19, 2026
4b88ebb
fix(frontend): load Tailwind in browser tests so screenshots render s…
malinskibeniamin Apr 20, 2026
4483d07
chore(frontend): remove docs/screenshots — pin PR screenshot via comm…
malinskibeniamin Apr 20, 2026
f35d15e
docs(frontend): temp screenshot bundle for PR description
malinskibeniamin Apr 20, 2026
74437ba
chore(frontend): revert temp screenshot bundle after pinning PR body
malinskibeniamin Apr 20, 2026
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
5 changes: 3 additions & 2 deletions frontend/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"@icons-pack/react-simple-icons": "^13.8.0",
"@milkdown/kit": "^7.18.0",
"@milkdown/react": "^7.18.0",
"@modelcontextprotocol/sdk": "^1.26.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@module-federation/runtime": "^2.3.2",
"@monaco-editor/react": "^4.7.0",
"@redpanda-data/ui": "^4.2.0",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* Copyright 2026 Redpanda Data, Inc.
*
* Use of this software is governed by the Business Source License
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
*
* As of the Change Date specified in that file, in accordance with
* the Business Source License, use of this software will be governed
* by the Apache License, Version 2.0
*/

import { create } from '@bufbuild/protobuf';
import type { MCPServer } from 'protogen/redpanda/api/dataplane/v1/mcp_pb';
import { MCPServer_State, MCPServerSchema } from 'protogen/redpanda/api/dataplane/v1/mcp_pb';
import { describe, expect, test, vi } from 'vitest';
import { page } from 'vitest/browser';
import { render } from 'vitest-browser-react';

import { mockConnectQuery, mockRouterForBrowserTest, ScreenshotFrame } from '../../../../__tests__/browser-test-utils';

const RUN_TOOL_REGEX = /run tool/i;

type StreamOptions = {
onProgress?: (p: { progress?: number; total?: number; statusMessage?: string; status?: string }) => void;
};

const mocks = vi.hoisted(() => ({
getMCPServer: vi.fn<() => { data: unknown; isLoading: boolean; error: Error | null }>().mockReturnValue({
data: undefined,
isLoading: false,
error: null,
}),
listTools: vi
.fn<
() => { data: unknown; isLoading: boolean; error: Error | null; isRefetchError: boolean; isRefetching: boolean }
>()
.mockReturnValue({
data: { tools: [] },
isLoading: false,
error: null,
isRefetchError: false,
isRefetching: false,
}),
listTopics: vi.fn<() => { data: unknown; refetch: () => void }>().mockReturnValue({
data: { topics: [] },
refetch: () => undefined,
}),
createTopic: vi.fn(),
lastCapturedOnProgress: undefined as undefined | ((p: unknown) => void),
streamState: {
data: undefined as unknown,
isPending: false as boolean,
error: null as Error | null,
},
}));

vi.mock('@tanstack/react-router', () => ({
...mockRouterForBrowserTest(),
getRouteApi: () => ({
useParams: () => ({ id: 'mcp-server-visual' }),
useRouteContext: ({ select }: { select: (ctx: Record<string, unknown>) => unknown }) =>
select({ gatewayUrl: 'http://localhost:8090' }),
}),
}));
vi.mock('@connectrpc/connect-query', () => mockConnectQuery());

vi.mock('config', () => ({
config: { jwt: 'test-jwt-token' },
isFeatureFlagEnabled: vi.fn(() => false),
isEmbedded: vi.fn(() => false),
addBearerTokenInterceptor: vi.fn((next) => async (request: unknown) => await next(request)),
}));

vi.mock('react-query/api/remote-mcp', () => ({
useGetMCPServerQuery: () => mocks.getMCPServer(),
useListMCPServerTools: () => mocks.listTools(),
useStreamMCPServerToolMutation: () => ({
data: mocks.streamState.data,
mutate: (params: StreamOptions) => {
mocks.lastCapturedOnProgress = params.onProgress as (p: unknown) => void;
mocks.streamState.isPending = true;
},
isPending: mocks.streamState.isPending,
error: mocks.streamState.error,
reset: () => {
mocks.streamState.isPending = false;
mocks.streamState.data = undefined;
mocks.streamState.error = null;
},
}),
}));

vi.mock('react-query/api/topic', () => ({
useLegacyListTopicsQuery: () => mocks.listTopics(),
useCreateTopicMutation: () => ({ mutateAsync: mocks.createTopic }),
}));

const testServer: MCPServer = create(MCPServerSchema, {
id: 'mcp-server-visual',
displayName: 'Visual Regression Server',
url: 'http://localhost:8090/mcp',
state: MCPServer_State.RUNNING,
tools: {
'process-events': { componentType: 2, configYaml: 'processor: identity' },
},
});

const { RemoteMCPInspectorTab } = await import('./remote-mcp-inspector-tab');

describe('RemoteMCPInspectorTab — browser visual regression', () => {
test('streaming inspector shows Progress bar and status line mid-call', async () => {
mocks.getMCPServer.mockReturnValue({
data: { mcpServer: testServer },
isLoading: false,
error: null,
});
mocks.listTools.mockReturnValue({
data: {
tools: [
{
name: 'process-events',
description: 'Transform a batch of events into structured output.',
inputSchema: {
type: 'object',
properties: {
batch_size: { type: 'integer', default: 10 },
},
required: ['batch_size'],
},
},
],
},
isLoading: false,
error: null,
isRefetchError: false,
isRefetching: false,
});
mocks.streamState.data = undefined;
mocks.streamState.isPending = false;
mocks.streamState.error = null;

render(
<ScreenshotFrame width={1280}>
<RemoteMCPInspectorTab />
</ScreenshotFrame>
);

await expect.element(page.getByRole('button', { name: RUN_TOOL_REGEX })).toBeVisible();

// Start a call and emit a progress update so the Progress bar renders.
await page.getByRole('button', { name: RUN_TOOL_REGEX }).click();

// Push a progress update through the captured onProgress callback so the
// UI moves into the streaming state (Progress bar + status line).
mocks.lastCapturedOnProgress?.({
taskId: 'task-42',
status: 'working',
statusMessage: 'Processing batch 6/10...',
progress: 6,
total: 10,
});

await expect.element(page.getByTestId('mcp-tool-progress-bar')).toBeVisible();
await expect.element(page.getByText('Processing batch 6/10...')).toBeVisible();

await expect(page.getByTestId('screenshot-frame')).toMatchScreenshot('mcp-streaming-inspector');
});
});
Loading
Loading