Chore: Move MCP request page into debug router path (#9140)

* refactor mcp page router path
* fix lint issue
This commit is contained in:
Kent Wang
2025-09-16 11:22:27 +08:00
parent 81e85239a4
commit ef89ff4178
16 changed files with 572 additions and 659 deletions

View File

@@ -437,6 +437,7 @@ const openMcpClientConnection = async (options: OpenMcpClientConnectionOptions)
message: error.message || 'Something went wrong',
});
console.error(`Failed to create ${options.transportType} transport: ${error}`);
return;
}
mcpConnections.set(requestId, mcpClient as McpClient);
const serverCapabilities = mcpClient.getServerCapabilities();

View File

@@ -4,6 +4,7 @@ import { href } from 'react-router';
import type { ChangeBufferEvent } from '~/common/database';
import type { CookieJar } from '~/models/cookie-jar';
import * as requestOperations from '~/models/helpers/request-operations';
import { isMcpRequest, type TransportType } from '~/models/mcp-request';
import type { RequestAuthentication, RequestHeader } from '~/models/request';
import { isEventStreamRequest, isGraphqlSubscriptionRequest } from '~/models/request';
import { isRequestMeta } from '~/models/request-meta';
@@ -22,6 +23,7 @@ export interface ConnectActionParams {
authentication: RequestAuthentication;
cookieJar: CookieJar;
suppressUserAgent: boolean;
transportType?: TransportType;
query?: Record<string, string>;
}
@@ -91,6 +93,16 @@ export async function clientAction({ params, request }: Route.ClientActionArgs)
query: rendered.query || {},
});
}
if (isMcpRequest(req)) {
window.main.mcp.connect({
requestId,
workspaceId,
transportType: rendered.transportType || 'streamable-http',
url: rendered.url,
headers: rendered.headers,
authentication: rendered.authentication,
});
}
// HACK: even more elaborate hack to get the request to update
return new Promise(resolve => {
const unsubscribe = window.main.on('db.changes', async (_, changes: ChangeBufferEvent[]) => {

View File

@@ -7,6 +7,7 @@ import { type GrpcRequest, isGrpcRequestId } from '~/models/grpc-request';
import type { GrpcRequestMeta } from '~/models/grpc-request-meta';
import * as requestOperations from '~/models/helpers/request-operations';
import { isMcpRequest, type McpRequest } from '~/models/mcp-request';
import type { McpResponse } from '~/models/mcp-response';
import type { MockRoute } from '~/models/mock-route';
import type { MockServer } from '~/models/mock-server';
import { isGraphqlSubscriptionRequest } from '~/models/request';
@@ -46,6 +47,14 @@ export interface GrpcRequestLoaderData {
responses: [];
requestVersions: RequestVersion[];
}
export interface McpRequestLoaderData {
activeRequest: McpRequest;
activeRequestMeta: RequestMeta;
activeResponse: McpResponse;
responses: McpResponse[];
requestVersions: RequestVersion[];
}
export interface RequestLoaderData {
activeRequest: Request;
activeRequestMeta: RequestMeta;
@@ -155,6 +164,18 @@ export async function clientLoader({ params }: Route.ClientLoaderArgs) {
requestPayload: socketIOPayload,
} as SocketIORequestLoaderData;
}
if (isMcpRequest(activeRequest)) {
// TODO - add mcp request payload model
return {
activeRequest,
activeRequestMeta,
activeResponse,
responses,
requestVersions: await models.requestVersion.findByParentId(requestId),
} as McpRequestLoaderData;
}
return {
activeRequest,
activeRequestMeta,

View File

@@ -93,6 +93,7 @@ import { EnvironmentPicker } from '~/ui/components/environment-picker';
import { ErrorBoundary } from '~/ui/components/error-boundary';
import { Icon } from '~/ui/components/icon';
import { useDocBodyKeyboardShortcuts } from '~/ui/components/keydown-binder';
import { McpPane } from '~/ui/components/mcp/mcp-pane';
import { showModal } from '~/ui/components/modals';
import { AskModal } from '~/ui/components/modals/ask-modal';
import { CookiesModal } from '~/ui/components/modals/cookies-modal';
@@ -230,6 +231,15 @@ const RequestTiming = ({ requestId }: { requestId: string }) => {
) : null;
};
const DebugEntry = () => {
const { activeWorkspace } = useWorkspaceLoaderData()!;
if (activeWorkspace.scope === 'mcp') {
// MCP request under mcp workspace has different layout so we need to render a different component
return <McpPane />;
}
return <Debug />;
};
const Debug = () => {
const {
activeWorkspace,
@@ -1265,7 +1275,7 @@ const Debug = () => {
);
};
export default Debug;
export default DebugEntry;
const ScratchPadTutorialPanel = () => {
const [signUpTipDismissedState, setSignUpTipDismissedState] = useLocalStorage<{

View File

@@ -1,88 +0,0 @@
import { href } from 'react-router';
import type { ChangeBufferEvent } from '~/common/database';
import type { CookieJar } from '~/models/cookie-jar';
import * as requestOperations from '~/models/helpers/request-operations';
import { type TransportType } from '~/models/mcp-request';
import type { RequestAuthentication, RequestHeader } from '~/models/request';
import { isRequestMeta } from '~/models/request-meta';
import { invariant } from '~/utils/invariant';
import { createFetcherSubmitHook } from '~/utils/router';
import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId.connect';
export interface ConnectActionParams {
url: string;
headers: RequestHeader[];
authentication: RequestAuthentication;
cookieJar: CookieJar;
suppressUserAgent: boolean;
transportType?: TransportType;
query?: Record<string, string>;
}
export async function clientAction({ params, request }: Route.ClientActionArgs) {
const { requestId, workspaceId } = params;
const req = await requestOperations.getById(requestId);
invariant(req, 'Request not found');
invariant(workspaceId, 'Workspace ID is required');
const rendered = (await request.json()) as ConnectActionParams;
window.main.mcp.connect({
requestId,
workspaceId,
transportType: rendered.transportType || 'streamable-http',
url: rendered.url,
headers: rendered.headers,
authentication: rendered.authentication,
});
// HACK: even more elaborate hack to get the request to update
return new Promise(resolve => {
const unsubscribe = window.main.on('db.changes', async (_, changes: ChangeBufferEvent[]) => {
for (const change of changes) {
const [event, doc] = change;
if (isRequestMeta(doc) && doc.parentId === requestId && event === 'update') {
resolve(null);
unsubscribe();
return;
}
}
});
});
}
export const useRequestConnectActionFetcher = createFetcherSubmitHook(
submit =>
({
organizationId,
projectId,
workspaceId,
requestId,
connectParams,
}: {
organizationId: string;
projectId: string;
workspaceId: string;
requestId: string;
connectParams: ConnectActionParams;
}) => {
const url = href(
'/organization/:organizationId/project/:projectId/workspace/:workspaceId/mcp/request/:requestId/connect',
{
organizationId,
projectId,
workspaceId,
requestId,
},
);
return submit(JSON.stringify(connectParams), {
action: url,
method: 'POST',
encType: 'application/json',
});
},
clientAction,
);

View File

@@ -1,44 +0,0 @@
import { Outlet, useRouteLoaderData } from 'react-router';
import type { BaseModel } from '~/models';
import * as models from '~/models';
import * as requestOperations from '~/models/helpers/request-operations';
import type { McpRequest } from '~/models/mcp-request';
import { invariant } from '~/utils/invariant';
import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId';
export default Outlet;
export async function clientLoader({ params }: Route.ClientLoaderArgs) {
const { requestId, workspaceId } = params;
const activeRequest = (await requestOperations.getById(requestId)) as McpRequest;
const activeWorkspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
invariant(activeWorkspaceMeta, 'Active workspace meta not found');
const activeRequestMeta = await models.requestMeta.updateOrCreateByParentId(requestId, { lastActive: Date.now() });
invariant(activeRequestMeta, 'Request meta not found');
const { filterResponsesByEnv } = await models.settings.get();
const activeResponse = activeRequestMeta.activeResponseId
? await models.mcpResponse.getById(activeRequestMeta.activeResponseId)
: await models.mcpResponse.getLatestForRequestId(requestId, activeWorkspaceMeta.activeEnvironmentId);
const allResponses = await models.mcpResponse.findByParentId(requestId);
const filteredResponses = allResponses.filter(r => r.environmentId === activeWorkspaceMeta.activeEnvironmentId);
const responses = (filterResponsesByEnv ? filteredResponses : allResponses).sort((a: BaseModel, b: BaseModel) =>
a.created > b.created ? -1 : 1,
);
return {
activeRequest,
activeRequestMeta,
activeResponse,
responses,
requestVersions: await models.requestVersion.findByParentId(requestId),
};
}
export function useMcpRequestLoaderData() {
return useRouteLoaderData<typeof clientLoader>(
'routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId',
);
}

View File

@@ -1,509 +1,24 @@
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useEffect, useRef, useState } from 'react';
import {
Breadcrumb,
Breadcrumbs,
Button,
GridList,
GridListItem,
Input,
SearchField,
ToggleButton,
Tooltip,
TooltipTrigger,
} from 'react-aria-components';
import { type ImperativePanelGroupHandle, Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { NavLink, redirect, useParams } from 'react-router';
import { useLocalStorage } from 'react-use';
import { redirect } from 'react-router';
import { DEFAULT_SIDEBAR_SIZE } from '~/common/constants';
import {
getDefaultServerCapabilities,
type McpServerData,
METHOD_INITIALIZE,
METHOD_LIST_PROMPTS,
METHOD_LIST_RESOURCE_TEMPLATES,
METHOD_LIST_RESOURCES,
METHOD_LIST_TOOLS,
} from '~/common/mcp-utils';
import type { McpEvent, McpMessageEvent } from '~/main/network/mcp';
import * as models from '~/models';
import type { McpRequest, McpServerPrimitiveTypes } from '~/models/mcp-request';
import { useRootLoaderData } from '~/root';
import { useWorkspaceLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId';
import { useMcpRequestLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId';
import { McpActionsDropdown } from '~/ui/components/dropdowns/mcp-actions-dropdown';
import { WorkspaceDropdown } from '~/ui/components/dropdowns/workspace-dropdown';
import { EnvironmentPicker } from '~/ui/components/environment-picker';
import { ErrorBoundary } from '~/ui/components/error-boundary';
import { Icon } from '~/ui/components/icon';
import { McpRequestPane } from '~/ui/components/mcp/mcp-request-pane';
import {
type PrimitiveSubItem,
type PrimitiveTypeItem,
type PromptItem,
type ResourceItem,
type ResourceTemplateItem,
type ToolItem,
} from '~/ui/components/mcp/types';
import { WorkspaceEnvironmentsEditModal } from '~/ui/components/modals/workspace-environments-edit-modal';
import { OrganizationTabList } from '~/ui/components/tabs/tab-list';
import { McpRealtimeResponsePane } from '~/ui/components/websockets/realtime-response-pane';
import { INSOMNIA_TAB_HEIGHT } from '~/ui/constant';
import { useReadyState } from '~/ui/hooks/use-ready-state';
import { invariant } from '~/utils/invariant';
import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp';
export async function clientLoader({ params }: Route.ClientLoaderArgs) {
if (!params.requestId) {
const { projectId, workspaceId, organizationId } = params;
invariant(workspaceId, 'Workspace ID is required');
invariant(projectId, 'Project ID is required');
const activeWorkspace = await models.workspace.getById(workspaceId);
invariant(activeWorkspace, 'Workspace not found');
// Mcp collection only have one request
const activeRequest = await models.mcpRequest.getByParentId(workspaceId);
invariant(activeRequest, 'MCP Request not found');
if (activeRequest) {
return redirect(
`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mcp/request/${activeRequest._id}`,
);
}
const { projectId, workspaceId, organizationId } = params;
invariant(workspaceId, 'Workspace ID is required');
invariant(projectId, 'Project ID is required');
const activeWorkspace = await models.workspace.getById(workspaceId);
invariant(activeWorkspace, 'Workspace not found');
// Mcp collection only have one request
const activeRequest = await models.mcpRequest.getByParentId(workspaceId);
invariant(activeRequest, 'MCP Request not found');
// Redirect to the debug page of the only request in the MCP workspace
if (activeRequest) {
return redirect(
`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${activeRequest._id}`,
);
}
return null;
}
const McpPage = () => {
const { organizationId, projectId, workspaceId } = useParams() as {
organizationId: string;
projectId: string;
workspaceId: string;
};
const { activeRequest, activeResponse } = useMcpRequestLoaderData()!;
const sidebarPanelRef = useRef<ImperativePanelGroupHandle>(null);
const [isEnvironmentPickerOpen, setIsEnvironmentPickerOpen] = useState(false);
const [isEnvironmentModalOpen, setEnvironmentModalOpen] = useState(false);
const [allExpanded, setAllExpanded] = useState(true);
const [filter, setFilter] = useLocalStorage<string>(`${workspaceId}:mcp-list-filter`);
const { settings } = useRootLoaderData()!;
const [mcpServerData, setMcpServerData] = useState<McpServerData | null>(null);
const [collapsedPrimitives, setCollapsedPrimitives] = useState<McpServerPrimitiveTypes[]>([]);
const [selectedPrimitiveItem, setSelectedPrimitiveItem] = useState<PrimitiveSubItem | null>(null);
const getPrimitiveCollection = () => {
const collection: (PrimitiveTypeItem | PrimitiveSubItem)[] = [];
if (mcpServerData) {
const { primitives } = mcpServerData;
const { tools, resources, resourceTemplates, prompts } = primitives;
if (tools.length > 0) {
collection.push({
type: 'tools',
name: 'Tools',
collapsed: collapsedPrimitives.includes('tools'),
itemLevel: 0,
hide: false,
});
const hide = collapsedPrimitives.includes('tools');
collection.push(...(tools.map(t => ({ ...t, type: 'tools', itemLevel: 1, hide })) as ToolItem[]));
}
if (resources.length > 0 || resourceTemplates.length > 0) {
collection.push({
type: 'resources',
name: 'Resources',
collapsed: collapsedPrimitives.includes('resources'),
itemLevel: 0,
hide: false,
});
const hide = collapsedPrimitives.includes('resources');
collection.push(...(resources.map(r => ({ ...r, type: 'resources', itemLevel: 1, hide })) as ResourceItem[]));
collection.push(
...(resourceTemplates.map(rt => ({
...rt,
type: 'resources',
itemLevel: 1,
hide,
})) as ResourceTemplateItem[]),
);
}
if (prompts.length > 0) {
collection.push({
type: 'prompts',
name: 'Prompts',
collapsed: collapsedPrimitives.includes('prompts'),
itemLevel: 0,
hide: false,
});
const hide = collapsedPrimitives.includes('prompts');
collection.push(...(prompts.map(p => ({ ...p, type: 'prompts', itemLevel: 1, hide })) as PromptItem[]));
}
}
return collection;
};
const getServerCapabilities = () => {
const serverCapabilities = getDefaultServerCapabilities();
if (mcpServerData) {
const { tools, resources, prompts } = mcpServerData.serverCapabilities;
if (tools) {
serverCapabilities.tools.enabled = true;
serverCapabilities.tools.listChanged = !!tools.listChanged;
}
if (resources) {
serverCapabilities.resources.enabled = true;
serverCapabilities.resources.listChanged = !!resources.listChanged;
serverCapabilities.resources.subscribe = !!resources.subscribe;
}
if (prompts) {
serverCapabilities.prompts.enabled = true;
serverCapabilities.prompts.listChanged = !!prompts.listChanged;
}
}
return serverCapabilities;
};
// TODO Support filter
const visibleCollection = getPrimitiveCollection().filter(item => !item.hide);
const serverCapabilities = getServerCapabilities();
const allowSubscribeResources = serverCapabilities.resources.enabled && serverCapabilities.resources.subscribe;
const enableNotification =
serverCapabilities.tools.listChanged ||
serverCapabilities.resources.listChanged ||
serverCapabilities.prompts.listChanged;
// TODO Use these variables to enable notification
console.log(`enableNotification`, enableNotification);
console.log(`allowSubscribeResources`, allowSubscribeResources);
const requestId = activeRequest._id;
const { activeEnvironment } = useWorkspaceLoaderData()!;
const readyState = useReadyState({ requestId, protocol: 'mcp' });
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer<HTMLDivElement, Element>({
getScrollElement: () => parentRef.current,
count: visibleCollection.length,
estimateSize: useCallback(() => 32, []),
overscan: 20,
getItemKey: index => {
const item = visibleCollection[index];
return `${item.itemLevel}::${item.type}::${item.name}`;
},
});
function toggleSidebar() {
const layout = sidebarPanelRef.current?.getLayout();
if (!layout) {
return;
}
if (layout && layout[0] > 0) {
layout[0] = 0;
} else {
layout[0] = DEFAULT_SIDEBAR_SIZE;
}
sidebarPanelRef.current?.setLayout(layout);
}
useEffect(() => {
const unsubscribe = window.main.on('toggle-sidebar', toggleSidebar);
return unsubscribe;
}, []);
const [direction, setDirection] = useState<'horizontal' | 'vertical'>(
settings.forceVerticalLayout ? 'vertical' : 'horizontal',
);
useEffect(() => {
if (settings.forceVerticalLayout) {
setDirection('vertical');
return () => {};
}
// Listen on media query changes
const mediaQuery = window.matchMedia('(max-width: 880px)');
setDirection(mediaQuery.matches ? 'vertical' : 'horizontal');
const handleChange = (e: MediaQueryListEvent) => {
setDirection(e.matches ? 'vertical' : 'horizontal');
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, [settings.forceVerticalLayout, direction]);
useEffect(() => {
const updateServerData = async () => {
const findFirstMatchEventData = (mcpEvents: McpEvent[], method: string) => {
const firstMatchEvent = mcpEvents.find(
event => 'method' in event && event.method === method,
) as McpMessageEvent;
if (firstMatchEvent) {
return firstMatchEvent.data.result;
}
return undefined;
};
const activeResponseId = activeResponse?._id;
if (activeResponseId) {
const allEvents = await window.main.mcp.event.findMany({ responseId: activeResponseId });
const serverCapabilities =
findFirstMatchEventData(allEvents, METHOD_INITIALIZE)?.capabilities || getDefaultServerCapabilities();
const tools = findFirstMatchEventData(allEvents, METHOD_LIST_TOOLS)?.tools || [];
const resources = findFirstMatchEventData(allEvents, METHOD_LIST_RESOURCES)?.resources || [];
const resourceTemplates =
findFirstMatchEventData(allEvents, METHOD_LIST_RESOURCE_TEMPLATES)?.resourceTemplates || [];
const prompts = findFirstMatchEventData(allEvents, METHOD_LIST_PROMPTS)?.prompts || [];
const mcpServerData = {
serverCapabilities: serverCapabilities,
primitives: {
tools,
resources,
resourceTemplates,
prompts,
},
} as McpServerData;
setMcpServerData(mcpServerData);
}
};
if (readyState) {
// Get MCP server data when connection is ready
updateServerData();
}
}, [readyState, activeResponse?._id]);
return (
<PanelGroup
ref={sidebarPanelRef}
autoSaveId="insomnia-sidebar"
id="wrapper"
className="new-sidebar h-full w-full text-[--color-font]"
direction="horizontal"
>
<Panel id="sidebar" className="sidebar theme--sidebar" maxSize={40} minSize={10} collapsible>
<div className="flex flex-1 flex-col divide-y divide-solid divide-[--hl-md] overflow-hidden">
<div className="flex flex-col items-start divide-y divide-solid divide-[--hl-md]">
<div className={`flex w-full h-[${INSOMNIA_TAB_HEIGHT}px]`}>
<Breadcrumbs className="m-0 flex h-[--line-height-sm] w-full list-none items-center gap-2 px-[--padding-sm] font-bold">
<Breadcrumb className="flex h-full select-none items-center gap-2 text-[--color-font] outline-none data-[focused]:outline-none">
<NavLink
data-testid="project"
className="flex aspect-square h-7 flex-shrink-0 items-center justify-center gap-2 rounded-sm px-1 py-1 text-sm text-[--color-font] outline-none ring-1 ring-transparent transition-all hover:bg-[--hl-xs] focus:ring-inset focus:ring-[--hl-md] aria-pressed:bg-[--hl-sm] data-[focused]:outline-none"
to={`/organization/${organizationId}/project/${projectId}`}
>
<Icon className="text-xs" icon="chevron-left" />
</NavLink>
<span aria-hidden role="separator" className="h-4 text-[--hl-lg] outline outline-1" />
</Breadcrumb>
<Breadcrumb className="flex h-full select-none items-center gap-2 truncate text-[--color-font] outline-none data-[focused]:outline-none">
<WorkspaceDropdown />
</Breadcrumb>
</Breadcrumbs>
</div>
</div>
<div className="flex flex-col items-start gap-2 p-[--padding-sm]">
<div className="flex items-center justify-between gap-2">
<EnvironmentPicker
isOpen={isEnvironmentPickerOpen}
onOpenChange={setIsEnvironmentPickerOpen}
onOpenEnvironmentSettingsModal={() => setEnvironmentModalOpen(true)}
/>
</div>
</div>
<div className="flex flex-1 flex-col overflow-hidden">
<div className="flex justify-between gap-1 p-[--padding-sm]">
<SearchField
aria-label="Server Capability filter"
className="group relative flex-1"
value={filter ?? ''}
onChange={setFilter}
>
<Input
placeholder="Filter"
className="w-full rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] py-1 pl-2 pr-7 text-[--color-font] transition-colors focus:outline-none focus:ring-1 focus:ring-[--hl-md]"
/>
<div className="absolute right-0 top-0 flex h-full items-center px-2">
<Button className="flex aspect-square w-5 items-center justify-center rounded-sm text-sm text-[--color-font] ring-1 ring-transparent transition-all hover:bg-[--hl-xs] focus:ring-inset focus:ring-[--hl-md] aria-pressed:bg-[--hl-sm] group-data-[empty]:hidden">
<Icon icon="close" />
</Button>
</div>
</SearchField>
<TooltipTrigger>
<ToggleButton
aria-label="Expand All/Collapse all"
defaultSelected={allExpanded}
onChange={() => {
const newState = !allExpanded;
if (newState) {
setCollapsedPrimitives([]);
} else {
setCollapsedPrimitives(['tools', 'resources', 'prompts']);
}
setAllExpanded(newState);
}}
className="flex aspect-square h-full items-center justify-center rounded-sm text-sm text-[--color-font] ring-1 ring-transparent transition-all hover:bg-[--hl-xs] focus:ring-inset focus:ring-[--hl-md]"
>
{({ isSelected }) => (
<Icon
icon={isSelected ? 'down-left-and-up-right-to-center' : 'up-right-and-down-left-from-center'}
/>
)}
</ToggleButton>
<Tooltip
offset={8}
className="max-h-[85vh] max-w-xs select-none overflow-y-auto rounded-md border border-solid border-[--hl-sm] bg-[--color-bg] px-4 py-2 text-sm text-[--color-font] shadow-lg focus:outline-none"
>
<span>{allExpanded ? 'Collapse all' : 'Expand all'}</span>
</Tooltip>
</TooltipTrigger>
</div>
<div className="flex-1 overflow-y-auto" ref={parentRef}>
<GridList
id="sidebar-mcp-gridlist"
style={{ height: virtualizer.getTotalSize() }}
items={virtualizer.getVirtualItems()}
className="relative"
aria-label="Mcp Server Capabilities"
onAction={key => {
const id = key.toString();
if (id.startsWith('root_')) {
// Click on primitive type item
const primitiveType = id.split('root_')[1] as McpServerPrimitiveTypes;
setCollapsedPrimitives(prev => {
if (prev.includes(primitiveType)) {
return prev.filter(p => p !== primitiveType);
}
return [...prev, primitiveType];
});
} else {
// Click a specified primitive
const [type, name] = id.split('_');
const item = visibleCollection.find(i => i.itemLevel === 1 && i.type === type && i.name === name);
setSelectedPrimitiveItem(item as PrimitiveSubItem);
}
}}
>
{virtualItem => {
const item = visibleCollection[virtualItem.index];
return (
<CollectionGridListItem
activeRequest={activeRequest}
item={item}
collapsedPrimitives={collapsedPrimitives}
style={{
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
/>
);
}}
</GridList>
</div>
</div>
{isEnvironmentModalOpen && <WorkspaceEnvironmentsEditModal onClose={() => setEnvironmentModalOpen(false)} />}
</div>
</Panel>
<PanelResizeHandle className="h-full w-[1px] bg-[--hl-md]" />
<Panel className="flex flex-col">
<OrganizationTabList currentPage="mcp" />
<PanelGroup autoSaveId="insomnia-panels" id="insomnia-panels" direction={direction}>
<Panel id="mcp-request-pane" order={1} minSize={10} className="pane-one theme--pane">
<McpRequestPane
selectedPrimitiveItem={
selectedPrimitiveItem?.itemLevel === 1 ? (selectedPrimitiveItem as PrimitiveSubItem) : null
}
environment={activeEnvironment}
readyState={readyState}
/>
</Panel>
<Panel id="mcp-response-pane" order={2} minSize={10} className="pane-two theme--pane">
<ErrorBoundary showAlert>
<McpRealtimeResponsePane />
</ErrorBoundary>
</Panel>
</PanelGroup>
</Panel>
</PanelGroup>
);
};
const CollectionGridListItem = ({
activeRequest,
style,
item,
collapsedPrimitives,
}: {
activeRequest: McpRequest;
item: PrimitiveTypeItem | PrimitiveSubItem;
style: React.CSSProperties;
collapsedPrimitives: McpServerPrimitiveTypes[];
}) => {
const label = 'title' in item ? item.title : item.name;
const uniqueId = item.itemLevel === 0 ? `root_${item.type}` : `${item.type}_${item.name}`;
const itemLevel = item.itemLevel;
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
const triggerRef = useRef<HTMLDivElement>(null);
return (
<GridListItem
id={uniqueId}
className={`group absolute left-0 top-0 w-full select-none outline-none ${item.itemLevel === 0 ? 'data-[drop-target]:bg-[--hl-md]' : 'border-solid data-[drop-target]:border-b data-[drop-target]:border-[--color-surprise]'}`}
textValue={label}
data-testid={`test-${uniqueId}`}
style={style}
ref={triggerRef}
>
<div
onContextMenu={e => {
e.preventDefault();
setIsContextMenuOpen(true);
}}
className="relative flex h-[--line-height-xs] w-full select-none items-center gap-2 overflow-hidden pl-4 pr-2 text-[--hl] outline-none transition-colors group-hover:bg-[--hl-xs] group-focus:bg-[--hl-sm] data-[selected=true]:text-[--color-font]"
style={{
paddingLeft: `${itemLevel}em`,
}}
>
<div className="relative flex h-[--line-height-xs] w-full select-none items-center gap-2 overflow-hidden px-4 text-[--hl] outline-none transition-colors">
{itemLevel === 0 && (
<Icon
className="w-4 flex-shrink-0"
icon={collapsedPrimitives.includes(item.type) ? 'caret-right' : 'caret-down'}
/>
)}
{item.type === 'tools' && item.itemLevel === 1 && (
<span className="flex w-10 flex-shrink-0 items-center justify-center rounded-sm border border-solid border-[--hl-sm] bg-[rgba(var(--color-success-rgb),0.5)] text-[0.65rem] text-[--color-font-success]">
Tool
</span>
)}
{item.type === 'resources' && item.itemLevel === 1 && (
<span className="flex w-10 flex-shrink-0 items-center justify-center rounded-sm border border-solid border-[--hl-sm] bg-[rgba(var(--color-surprise-rgb),0.5)] text-[0.65rem] text-[--color-font-surprise]">
Res
</span>
)}
{item.type === 'prompts' && item.itemLevel === 1 && (
<span className="flex w-10 flex-shrink-0 items-center justify-center rounded-sm border border-solid border-[--hl-sm] bg-[rgba(var(--color-info-rgb),0.5)] text-[0.65rem] text-[--color-font-info]">
Prompt
</span>
)}
{label}
</div>
<McpActionsDropdown
item={item}
request={activeRequest}
isOpen={isContextMenuOpen}
onOpenChange={setIsContextMenuOpen}
triggerRef={triggerRef}
/>
</div>
</GridListItem>
);
};
export default McpPage;

View File

@@ -198,7 +198,7 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
});
return redirect(
href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/mcp/request/:requestId', {
href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/:requestId', {
organizationId,
projectId,
workspaceId: workspace._id,

View File

@@ -12,7 +12,7 @@ import {
PREVIEW_MODES,
} from '../../../common/constants';
import type { McpEvent } from '../../../main/network/mcp';
import { useMcpRequestLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId';
import { useRequestLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
import { CodeEditor } from '../../components/.client/codemirror/code-editor';
import { showError } from '../../components/modals';
import { useRequestMetaPatcher } from '../../hooks/use-request';
@@ -64,7 +64,7 @@ export const MessageEventView = ({ event }: Props) => {
} catch {
// Can't parse as JSON.
}
const { activeRequestMeta } = useMcpRequestLoaderData()!;
const { activeRequestMeta } = useRequestLoaderData()!;
const previewMode = ('previewMode' in activeRequestMeta && activeRequestMeta.previewMode) || PREVIEW_MODE_SOURCE;
return (
<div className="flex h-full flex-col">

View File

@@ -0,0 +1,489 @@
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useEffect, useRef, useState } from 'react';
import {
Breadcrumb,
Breadcrumbs,
Button,
GridList,
GridListItem,
Input,
SearchField,
ToggleButton,
Tooltip,
TooltipTrigger,
} from 'react-aria-components';
import { type ImperativePanelGroupHandle, Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { NavLink, useParams } from 'react-router';
import { useLocalStorage } from 'react-use';
import { DEFAULT_SIDEBAR_SIZE } from '~/common/constants';
import {
getDefaultServerCapabilities,
type McpServerData,
METHOD_INITIALIZE,
METHOD_LIST_PROMPTS,
METHOD_LIST_RESOURCE_TEMPLATES,
METHOD_LIST_RESOURCES,
METHOD_LIST_TOOLS,
} from '~/common/mcp-utils';
import type { McpEvent, McpMessageEvent } from '~/main/network/mcp';
import type { McpRequest, McpServerPrimitiveTypes } from '~/models/mcp-request';
import { useRootLoaderData } from '~/root';
import { useWorkspaceLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId';
import {
type McpRequestLoaderData,
useRequestLoaderData,
} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
import { McpActionsDropdown } from '~/ui/components/dropdowns/mcp-actions-dropdown';
import { WorkspaceDropdown } from '~/ui/components/dropdowns/workspace-dropdown';
import { EnvironmentPicker } from '~/ui/components/environment-picker';
import { ErrorBoundary } from '~/ui/components/error-boundary';
import { Icon } from '~/ui/components/icon';
import { McpRequestPane } from '~/ui/components/mcp/mcp-request-pane';
import {
type PrimitiveSubItem,
type PrimitiveTypeItem,
type PromptItem,
type ResourceItem,
type ResourceTemplateItem,
type ToolItem,
} from '~/ui/components/mcp/types';
import { WorkspaceEnvironmentsEditModal } from '~/ui/components/modals/workspace-environments-edit-modal';
import { OrganizationTabList } from '~/ui/components/tabs/tab-list';
import { RealtimeResponsePane } from '~/ui/components/websockets/realtime-response-pane';
import { INSOMNIA_TAB_HEIGHT } from '~/ui/constant';
import { useReadyState } from '~/ui/hooks/use-ready-state';
export const McpPane = () => {
const requestData = useRequestLoaderData()!;
const { activeResponse, activeRequest } = requestData as McpRequestLoaderData;
const { organizationId, projectId, workspaceId } = useParams() as {
organizationId: string;
projectId: string;
workspaceId: string;
};
const sidebarPanelRef = useRef<ImperativePanelGroupHandle>(null);
const [isEnvironmentPickerOpen, setIsEnvironmentPickerOpen] = useState(false);
const [isEnvironmentModalOpen, setEnvironmentModalOpen] = useState(false);
const [allExpanded, setAllExpanded] = useState(true);
const [filter, setFilter] = useLocalStorage<string>(`${workspaceId}:mcp-list-filter`);
const { settings } = useRootLoaderData()!;
const [mcpServerData, setMcpServerData] = useState<McpServerData | null>(null);
const [collapsedPrimitives, setCollapsedPrimitives] = useState<McpServerPrimitiveTypes[]>([]);
const [selectedPrimitiveItem, setSelectedPrimitiveItem] = useState<PrimitiveSubItem | null>(null);
const getPrimitiveCollection = () => {
const collection: (PrimitiveTypeItem | PrimitiveSubItem)[] = [];
if (mcpServerData) {
const { primitives } = mcpServerData;
const { tools, resources, resourceTemplates, prompts } = primitives;
if (tools.length > 0) {
collection.push({
type: 'tools',
name: 'Tools',
collapsed: collapsedPrimitives.includes('tools'),
itemLevel: 0,
hide: false,
});
const hide = collapsedPrimitives.includes('tools');
collection.push(...(tools.map(t => ({ ...t, type: 'tools', itemLevel: 1, hide })) as ToolItem[]));
}
if (resources.length > 0 || resourceTemplates.length > 0) {
collection.push({
type: 'resources',
name: 'Resources',
collapsed: collapsedPrimitives.includes('resources'),
itemLevel: 0,
hide: false,
});
const hide = collapsedPrimitives.includes('resources');
collection.push(...(resources.map(r => ({ ...r, type: 'resources', itemLevel: 1, hide })) as ResourceItem[]));
collection.push(
...(resourceTemplates.map(rt => ({
...rt,
type: 'resources',
itemLevel: 1,
hide,
})) as ResourceTemplateItem[]),
);
}
if (prompts.length > 0) {
collection.push({
type: 'prompts',
name: 'Prompts',
collapsed: collapsedPrimitives.includes('prompts'),
itemLevel: 0,
hide: false,
});
const hide = collapsedPrimitives.includes('prompts');
collection.push(...(prompts.map(p => ({ ...p, type: 'prompts', itemLevel: 1, hide })) as PromptItem[]));
}
}
return collection;
};
const getServerCapabilities = () => {
const serverCapabilities = getDefaultServerCapabilities();
if (mcpServerData) {
const { tools, resources, prompts } = mcpServerData.serverCapabilities;
if (tools) {
serverCapabilities.tools.enabled = true;
serverCapabilities.tools.listChanged = !!tools.listChanged;
}
if (resources) {
serverCapabilities.resources.enabled = true;
serverCapabilities.resources.listChanged = !!resources.listChanged;
serverCapabilities.resources.subscribe = !!resources.subscribe;
}
if (prompts) {
serverCapabilities.prompts.enabled = true;
serverCapabilities.prompts.listChanged = !!prompts.listChanged;
}
}
return serverCapabilities;
};
// TODO Support filter
const visibleCollection = getPrimitiveCollection().filter(item => !item.hide);
const serverCapabilities = getServerCapabilities();
const allowSubscribeResources = serverCapabilities.resources.enabled && serverCapabilities.resources.subscribe;
const enableNotification =
serverCapabilities.tools.listChanged ||
serverCapabilities.resources.listChanged ||
serverCapabilities.prompts.listChanged;
// TODO Use these variables to enable notification
console.log(`enableNotification`, enableNotification);
console.log(`allowSubscribeResources`, allowSubscribeResources);
const requestId = activeRequest._id;
const { activeEnvironment } = useWorkspaceLoaderData()!;
const readyState = useReadyState({ requestId, protocol: 'mcp' });
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer<HTMLDivElement, Element>({
getScrollElement: () => parentRef.current,
count: visibleCollection.length,
estimateSize: useCallback(() => 32, []),
overscan: 20,
getItemKey: index => {
const item = visibleCollection[index];
return `${item.itemLevel}::${item.type}::${item.name}`;
},
});
function toggleSidebar() {
const layout = sidebarPanelRef.current?.getLayout();
if (!layout) {
return;
}
if (layout && layout[0] > 0) {
layout[0] = 0;
} else {
layout[0] = DEFAULT_SIDEBAR_SIZE;
}
sidebarPanelRef.current?.setLayout(layout);
}
useEffect(() => {
const unsubscribe = window.main.on('toggle-sidebar', toggleSidebar);
return unsubscribe;
}, []);
const [direction, setDirection] = useState<'horizontal' | 'vertical'>(
settings.forceVerticalLayout ? 'vertical' : 'horizontal',
);
useEffect(() => {
if (settings.forceVerticalLayout) {
setDirection('vertical');
return () => {};
}
// Listen on media query changes
const mediaQuery = window.matchMedia('(max-width: 880px)');
setDirection(mediaQuery.matches ? 'vertical' : 'horizontal');
const handleChange = (e: MediaQueryListEvent) => {
setDirection(e.matches ? 'vertical' : 'horizontal');
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, [settings.forceVerticalLayout, direction]);
useEffect(() => {
const updateServerData = async () => {
const findFirstMatchEventData = (mcpEvents: McpEvent[], method: string) => {
const firstMatchEvent = mcpEvents.find(
event => 'method' in event && event.method === method,
) as McpMessageEvent;
if (firstMatchEvent) {
return firstMatchEvent.data.result;
}
return undefined;
};
const activeResponseId = activeResponse?._id;
if (activeResponseId) {
const allEvents = await window.main.mcp.event.findMany({ responseId: activeResponseId });
const serverCapabilities =
findFirstMatchEventData(allEvents, METHOD_INITIALIZE)?.capabilities || getDefaultServerCapabilities();
const tools = findFirstMatchEventData(allEvents, METHOD_LIST_TOOLS)?.tools || [];
const resources = findFirstMatchEventData(allEvents, METHOD_LIST_RESOURCES)?.resources || [];
const resourceTemplates =
findFirstMatchEventData(allEvents, METHOD_LIST_RESOURCE_TEMPLATES)?.resourceTemplates || [];
const prompts = findFirstMatchEventData(allEvents, METHOD_LIST_PROMPTS)?.prompts || [];
const mcpServerData = {
serverCapabilities: serverCapabilities,
primitives: {
tools,
resources,
resourceTemplates,
prompts,
},
} as McpServerData;
setMcpServerData(mcpServerData);
}
};
if (readyState) {
// Get MCP server data when connection is ready
updateServerData();
}
}, [readyState, activeResponse?._id]);
return (
<PanelGroup
ref={sidebarPanelRef}
autoSaveId="insomnia-sidebar"
id="wrapper"
className="new-sidebar h-full w-full text-[--color-font]"
direction="horizontal"
>
<Panel id="sidebar" className="sidebar theme--sidebar" maxSize={40} minSize={10} collapsible>
<div className="flex flex-1 flex-col divide-y divide-solid divide-[--hl-md] overflow-hidden">
<div className="flex flex-col items-start divide-y divide-solid divide-[--hl-md]">
<div className={`flex w-full h-[${INSOMNIA_TAB_HEIGHT}px]`}>
<Breadcrumbs className="m-0 flex h-[--line-height-sm] w-full list-none items-center gap-2 px-[--padding-sm] font-bold">
<Breadcrumb className="flex h-full select-none items-center gap-2 text-[--color-font] outline-none data-[focused]:outline-none">
<NavLink
data-testid="project"
className="flex aspect-square h-7 flex-shrink-0 items-center justify-center gap-2 rounded-sm px-1 py-1 text-sm text-[--color-font] outline-none ring-1 ring-transparent transition-all hover:bg-[--hl-xs] focus:ring-inset focus:ring-[--hl-md] aria-pressed:bg-[--hl-sm] data-[focused]:outline-none"
to={`/organization/${organizationId}/project/${projectId}`}
>
<Icon className="text-xs" icon="chevron-left" />
</NavLink>
<span aria-hidden role="separator" className="h-4 text-[--hl-lg] outline outline-1" />
</Breadcrumb>
<Breadcrumb className="flex h-full select-none items-center gap-2 truncate text-[--color-font] outline-none data-[focused]:outline-none">
<WorkspaceDropdown />
</Breadcrumb>
</Breadcrumbs>
</div>
</div>
<div className="flex flex-col items-start gap-2 p-[--padding-sm]">
<div className="flex items-center justify-between gap-2">
<EnvironmentPicker
isOpen={isEnvironmentPickerOpen}
onOpenChange={setIsEnvironmentPickerOpen}
onOpenEnvironmentSettingsModal={() => setEnvironmentModalOpen(true)}
/>
</div>
</div>
<div className="flex flex-1 flex-col overflow-hidden">
<div className="flex justify-between gap-1 p-[--padding-sm]">
<SearchField
aria-label="Server Capability filter"
className="group relative flex-1"
value={filter ?? ''}
onChange={setFilter}
>
<Input
placeholder="Filter"
className="w-full rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] py-1 pl-2 pr-7 text-[--color-font] transition-colors focus:outline-none focus:ring-1 focus:ring-[--hl-md]"
/>
<div className="absolute right-0 top-0 flex h-full items-center px-2">
<Button className="flex aspect-square w-5 items-center justify-center rounded-sm text-sm text-[--color-font] ring-1 ring-transparent transition-all hover:bg-[--hl-xs] focus:ring-inset focus:ring-[--hl-md] aria-pressed:bg-[--hl-sm] group-data-[empty]:hidden">
<Icon icon="close" />
</Button>
</div>
</SearchField>
<TooltipTrigger>
<ToggleButton
aria-label="Expand All/Collapse all"
defaultSelected={allExpanded}
onChange={() => {
const newState = !allExpanded;
if (newState) {
setCollapsedPrimitives([]);
} else {
setCollapsedPrimitives(['tools', 'resources', 'prompts']);
}
setAllExpanded(newState);
}}
className="flex aspect-square h-full items-center justify-center rounded-sm text-sm text-[--color-font] ring-1 ring-transparent transition-all hover:bg-[--hl-xs] focus:ring-inset focus:ring-[--hl-md]"
>
{({ isSelected }) => (
<Icon
icon={isSelected ? 'down-left-and-up-right-to-center' : 'up-right-and-down-left-from-center'}
/>
)}
</ToggleButton>
<Tooltip
offset={8}
className="max-h-[85vh] max-w-xs select-none overflow-y-auto rounded-md border border-solid border-[--hl-sm] bg-[--color-bg] px-4 py-2 text-sm text-[--color-font] shadow-lg focus:outline-none"
>
<span>{allExpanded ? 'Collapse all' : 'Expand all'}</span>
</Tooltip>
</TooltipTrigger>
</div>
<div className="flex-1 overflow-y-auto" ref={parentRef}>
<GridList
id="sidebar-mcp-gridlist"
style={{ height: virtualizer.getTotalSize() }}
items={virtualizer.getVirtualItems()}
className="relative"
aria-label="Mcp Server Capabilities"
onAction={key => {
const id = key.toString();
if (id.startsWith('root_')) {
// Click on primitive type item
const primitiveType = id.split('root_')[1] as McpServerPrimitiveTypes;
setCollapsedPrimitives(prev => {
if (prev.includes(primitiveType)) {
return prev.filter(p => p !== primitiveType);
}
return [...prev, primitiveType];
});
} else {
// Click a specified primitive
const [type, name] = id.split('_');
const item = visibleCollection.find(i => i.itemLevel === 1 && i.type === type && i.name === name);
setSelectedPrimitiveItem(item as PrimitiveSubItem);
}
}}
>
{virtualItem => {
const item = visibleCollection[virtualItem.index];
return (
<CollectionGridListItem
activeRequest={activeRequest}
item={item}
collapsedPrimitives={collapsedPrimitives}
style={{
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
/>
);
}}
</GridList>
</div>
</div>
{isEnvironmentModalOpen && <WorkspaceEnvironmentsEditModal onClose={() => setEnvironmentModalOpen(false)} />}
</div>
</Panel>
<PanelResizeHandle className="h-full w-[1px] bg-[--hl-md]" />
<Panel className="flex flex-col">
<OrganizationTabList currentPage="mcp" />
<PanelGroup autoSaveId="insomnia-panels" id="insomnia-panels" direction={direction}>
<Panel id="mcp-request-pane" order={1} minSize={10} className="pane-one theme--pane">
<McpRequestPane
selectedPrimitiveItem={
selectedPrimitiveItem?.itemLevel === 1 ? (selectedPrimitiveItem as PrimitiveSubItem) : null
}
environment={activeEnvironment}
readyState={readyState}
/>
</Panel>
<Panel id="mcp-response-pane" order={2} minSize={10} className="pane-two theme--pane">
<ErrorBoundary showAlert>
<RealtimeResponsePane />
</ErrorBoundary>
</Panel>
</PanelGroup>
</Panel>
</PanelGroup>
);
};
const CollectionGridListItem = ({
activeRequest,
style,
item,
collapsedPrimitives,
}: {
activeRequest: McpRequest;
item: PrimitiveTypeItem | PrimitiveSubItem;
style: React.CSSProperties;
collapsedPrimitives: McpServerPrimitiveTypes[];
}) => {
const label = 'title' in item ? item.title : item.name;
const uniqueId = item.itemLevel === 0 ? `root_${item.type}` : `${item.type}_${item.name}`;
const itemLevel = item.itemLevel;
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
const triggerRef = useRef<HTMLDivElement>(null);
return (
<GridListItem
id={uniqueId}
className={`group absolute left-0 top-0 w-full select-none outline-none ${item.itemLevel === 0 ? 'data-[drop-target]:bg-[--hl-md]' : 'border-solid data-[drop-target]:border-b data-[drop-target]:border-[--color-surprise]'}`}
textValue={label}
data-testid={`test-${uniqueId}`}
style={style}
ref={triggerRef}
>
<div
onContextMenu={e => {
e.preventDefault();
setIsContextMenuOpen(true);
}}
className="relative flex h-[--line-height-xs] w-full select-none items-center gap-2 overflow-hidden pl-4 pr-2 text-[--hl] outline-none transition-colors group-hover:bg-[--hl-xs] group-focus:bg-[--hl-sm] data-[selected=true]:text-[--color-font]"
style={{
paddingLeft: `${itemLevel}em`,
}}
>
<div className="relative flex h-[--line-height-xs] w-full select-none items-center gap-2 overflow-hidden px-4 text-[--hl] outline-none transition-colors">
{itemLevel === 0 && (
<Icon
className="w-4 flex-shrink-0"
icon={collapsedPrimitives.includes(item.type) ? 'caret-right' : 'caret-down'}
/>
)}
{item.type === 'tools' && item.itemLevel === 1 && (
<span className="flex w-10 flex-shrink-0 items-center justify-center rounded-sm border border-solid border-[--hl-sm] bg-[rgba(var(--color-success-rgb),0.5)] text-[0.65rem] text-[--color-font-success]">
Tool
</span>
)}
{item.type === 'resources' && item.itemLevel === 1 && (
<span className="flex w-10 flex-shrink-0 items-center justify-center rounded-sm border border-solid border-[--hl-sm] bg-[rgba(var(--color-surprise-rgb),0.5)] text-[0.65rem] text-[--color-font-surprise]">
Res
</span>
)}
{item.type === 'prompts' && item.itemLevel === 1 && (
<span className="flex w-10 flex-shrink-0 items-center justify-center rounded-sm border border-solid border-[--hl-sm] bg-[rgba(var(--color-info-rgb),0.5)] text-[0.65rem] text-[--color-font-info]">
Prompt
</span>
)}
{label}
</div>
<McpActionsDropdown
item={item}
request={activeRequest}
isOpen={isContextMenuOpen}
onOpenChange={setIsContextMenuOpen}
triggerRef={triggerRef}
/>
</div>
</GridListItem>
);
};
export default McpPane;

View File

@@ -10,7 +10,10 @@ import { InsomniaRjsfForm } from '~/ui/components/rjsf';
import { type AuthTypes } from '../../../common/constants';
import type { Environment } from '../../../models/environment';
import { getAuthObjectOrNull } from '../../../network/authentication';
import { useMcpRequestLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId';
import {
type McpRequestLoaderData,
useRequestLoaderData,
} from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
import { useRequestPatcher } from '../../hooks/use-request';
import { CodeEditor, type CodeEditorHandle } from '../.client/codemirror/code-editor';
import { AuthWrapper } from '../editors/auth/auth-wrapper';
@@ -51,7 +54,7 @@ interface Props {
}
export const McpRequestPane: FC<Props> = ({ environment, readyState, selectedPrimitiveItem }) => {
const { activeRequest } = useMcpRequestLoaderData()!;
const { activeRequest } = useRequestLoaderData()! as McpRequestLoaderData;
const [formData, setFormData] = useState({});
const paramEditorRef = useRef<CodeEditorHandle>(null);
const requestId = activeRequest._id;

View File

@@ -5,7 +5,7 @@ import { useParams } from 'react-router';
import {
type ConnectActionParams,
useRequestConnectActionFetcher,
} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId.connect';
} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.connect';
import { OneLineEditor, type OneLineEditorHandle } from '~/ui/components/.client/codemirror/one-line-editor';
import { Dropdown, DropdownItem, DropdownSection, ItemContent } from '~/ui/components/base/dropdown';

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { OverlayContainer } from 'react-aria';
import { useNavigate, useParams } from 'react-router';
import type { McpRequest } from '~/models/mcp-request';
import { useProjectListWorkspacesLoaderFetcher } from '~/routes/organization.$organizationId.project.$projectId.list-workspaces';
import { useRequestDuplicateActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.duplicate';
@@ -22,7 +23,7 @@ import { HelpTooltip } from '../help-tooltip';
import { Icon } from '../icon';
export interface RequestSettingsModalOptions {
request: Request | GrpcRequest | WebSocketRequest | SocketIORequest;
request: Request | GrpcRequest | WebSocketRequest | SocketIORequest | McpRequest;
}
export const RequestSettingsModal = ({ request, onHide }: ModalProps & RequestSettingsModalOptions) => {

View File

@@ -1,5 +1,7 @@
import React, { type FC, memo } from 'react';
import { isMcpRequest, type McpRequest } from '~/models/mcp-request';
import { CONTENT_TYPE_GRAPHQL, METHOD_DELETE, METHOD_OPTIONS } from '../../../common/constants';
import { type GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
import { isEventStreamRequest, isRequest, type Request } from '../../../models/request';
@@ -37,7 +39,9 @@ export function formatMethodName(method: string) {
return methodName;
}
export const getRequestMethodShortHand = (doc?: Request | WebSocketRequest | GrpcRequest | SocketIORequest) => {
export const getRequestMethodShortHand = (
doc?: Request | WebSocketRequest | GrpcRequest | SocketIORequest | McpRequest,
) => {
if (!doc) {
return '';
}
@@ -57,6 +61,10 @@ export const getRequestMethodShortHand = (doc?: Request | WebSocketRequest | Grp
return 'IO';
}
if (isMcpRequest(doc)) {
return 'MCP';
}
return '';
};

View File

@@ -16,7 +16,6 @@ import type { Response } from '../../../models/response';
import { isSocketIOResponse, type SocketIOResponse } from '../../../models/socket-io-response';
import { type WebSocketResponse } from '../../../models/websocket-response';
import { useRequestLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
import { useMcpRequestLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mcp.request.$requestId';
import { deserializeNDJSON } from '../../../utils/ndjson';
import { useReadyState } from '../../hooks/use-ready-state';
import { useRealtimeConnectionEvents } from '../../hooks/use-realtime-connection-events';
@@ -38,7 +37,7 @@ import { ResponseTimelineViewer } from '../viewers/response-timeline-viewer';
import { EventLogView } from './event-log-view';
import { EventView } from './event-view';
export const RealtimeResponsePane: FC<{ requestId: string }> = () => {
export const RealtimeResponsePane: FC<{ requestId?: string }> = () => {
const { activeResponse, responses, requestVersions } = useRequestLoaderData()!;
if (!activeResponse) {
@@ -54,22 +53,6 @@ export const RealtimeResponsePane: FC<{ requestId: string }> = () => {
);
};
export const McpRealtimeResponsePane = () => {
const { activeResponse, responses, requestVersions } = useMcpRequestLoaderData()!;
if (!activeResponse) {
return (
<Pane type="response">
<PaneHeader className="!justify-normal" />
<PlaceholderResponsePane />
</Pane>
);
}
return (
<RealtimeActiveResponsePane response={activeResponse} responses={responses} requestVersions={requestVersions} />
);
};
type ResponseType = WebSocketResponse | Response | SocketIOResponse | McpResponse;
type EventType = CurlEvent | WebSocketEvent | SocketIOEvent | McpEvent;
const RealtimeActiveResponsePane: FC<{

View File

@@ -1,6 +1,8 @@
import { useCallback, useEffect } from 'react';
import { matchPath, useLocation, useSearchParams } from 'react-router';
import type { McpRequest } from '~/models/mcp-request';
import type { GrpcRequest } from '../../models/grpc-request';
import type { MockRoute } from '../../models/mock-route';
import type { Project } from '../../models/project';
@@ -22,7 +24,7 @@ interface InsomniaTabProps {
workspaceId: string;
activeProject: Project;
activeWorkspace: Workspace;
activeRequest?: Request | GrpcRequest | WebSocketRequest | SocketIORequest;
activeRequest?: Request | GrpcRequest | WebSocketRequest | SocketIORequest | McpRequest;
activeRequestGroup?: RequestGroup;
activeMockRoute?: MockRoute;
unitTestSuite?: UnitTestSuite;