mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-18 13:18:59 -04:00
fix: UX improvement for mcp client (#9229)
* fix call tool data issue * add api key auth
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
import type { GetPromptRequest, Notification, ReadResourceRequest } from '@modelcontextprotocol/sdk/types.js';
|
||||
import {
|
||||
type ClientRequest,
|
||||
CompatibilityCallToolResultSchema,
|
||||
ElicitResultSchema,
|
||||
EmptyResultSchema,
|
||||
InitializeRequestSchema,
|
||||
@@ -1018,7 +1019,7 @@ const callTool = async (options: CallToolOptions) => {
|
||||
const { requestId, name, parameters = {} } = options;
|
||||
const mcpClient = _getMcpClient(requestId);
|
||||
if (mcpClient) {
|
||||
const response = await mcpClient.callTool({ name, arguments: parameters });
|
||||
const response = await mcpClient.callTool({ name, arguments: parameters }, CompatibilityCallToolResultSchema);
|
||||
return response.content;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -12,11 +12,14 @@ export const options = [
|
||||
{ name: 'Cookie', value: COOKIE },
|
||||
];
|
||||
|
||||
export const ApiKeyAuth: FC<{ disabled?: boolean }> = ({ disabled = false }) => (
|
||||
export const ApiKeyAuth: FC<{ disabled?: boolean; addToHeaderOnly?: boolean }> = ({
|
||||
disabled = false,
|
||||
addToHeaderOnly = false,
|
||||
}) => (
|
||||
<AuthTableBody>
|
||||
<AuthToggleRow label="Enabled" property="disabled" invert disabled={disabled} />
|
||||
<AuthInputRow label="Key" property="key" disabled={disabled} />
|
||||
<AuthInputRow label="Value" property="value" mask disabled={disabled} />
|
||||
<AuthSelectRow label="Add to" property="addTo" options={options} disabled={disabled} />
|
||||
{!addToHeaderOnly && <AuthSelectRow label="Add to" property="addTo" options={options} disabled={disabled} />}
|
||||
</AuthTableBody>
|
||||
);
|
||||
|
||||
@@ -26,14 +26,15 @@ export const AuthWrapper: FC<{
|
||||
hideOthers?: boolean;
|
||||
hideInherit?: boolean;
|
||||
showMcpAuthFlow?: boolean;
|
||||
}> = ({ authentication, disabled = false, authTypes, hideOthers, hideInherit, showMcpAuthFlow }) => {
|
||||
addToHeaderOnly?: boolean;
|
||||
}> = ({ authentication, disabled = false, authTypes, hideOthers, hideInherit, showMcpAuthFlow, addToHeaderOnly }) => {
|
||||
const type = getAuthObjectOrNull(authentication)?.type || '';
|
||||
let authBody: ReactNode = null;
|
||||
|
||||
if (type === 'basic') {
|
||||
authBody = <BasicAuth disabled={disabled} />;
|
||||
} else if (type === 'apikey') {
|
||||
authBody = <ApiKeyAuth disabled={disabled} />;
|
||||
authBody = <ApiKeyAuth disabled={disabled} addToHeaderOnly={addToHeaderOnly} />;
|
||||
} else if (type === 'oauth2') {
|
||||
authBody = <OAuth2Auth showMcpAuthFlow={showMcpAuthFlow} disabled={disabled} />;
|
||||
} else if (type === 'hawk') {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { Button } from 'react-aria-components';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
@@ -11,9 +12,10 @@ import {
|
||||
PREVIEW_MODE_SOURCE,
|
||||
PREVIEW_MODES,
|
||||
} from '../../../common/constants';
|
||||
import { METHOD_CALL_TOOL } from '../../../common/mcp-utils';
|
||||
import type { McpEvent } from '../../../main/network/mcp';
|
||||
import { useRequestLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
|
||||
import { CodeEditor } from '../../components/.client/codemirror/code-editor';
|
||||
import { CodeEditor, type CodeEditorHandle } from '../../components/.client/codemirror/code-editor';
|
||||
import { showError } from '../../components/modals';
|
||||
import { useRequestMetaPatcher } from '../../hooks/use-request';
|
||||
import { Dropdown, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
|
||||
@@ -24,8 +26,10 @@ interface Props {
|
||||
|
||||
export const MessageEventView = ({ event }: Props) => {
|
||||
const { requestId } = useParams() as { requestId: string };
|
||||
const editorRef = useRef<CodeEditorHandle>(null);
|
||||
|
||||
const isErrorEvent = event.type === 'error';
|
||||
const isCallToolEvent = event.type === 'message' && event.method === METHOD_CALL_TOOL;
|
||||
const eventData = isErrorEvent ? event.error : 'data' in event ? event.data : '';
|
||||
const raw = JSON.stringify(eventData);
|
||||
|
||||
@@ -63,6 +67,27 @@ export const MessageEventView = ({ event }: Props) => {
|
||||
let pretty = raw;
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
// If call tool response, try to parse the `result.content` field if it's JSON string
|
||||
if (isCallToolEvent && 'result' in parsed) {
|
||||
const callToolResult = parsed.result;
|
||||
if ('content' in callToolResult) {
|
||||
const callToolParsedResult = CallToolResultSchema.safeParse(callToolResult);
|
||||
if (callToolParsedResult.success) {
|
||||
const callToolResultContents = callToolParsedResult.data.content;
|
||||
callToolResultContents.forEach((callToolResultContent, idx) => {
|
||||
if (callToolResultContent.type === 'text') {
|
||||
const callToolResultContentText = callToolResultContent.text;
|
||||
// Try to parse JSON text content
|
||||
try {
|
||||
const callToolResultContentTextParsed = JSON.parse(callToolResultContentText);
|
||||
callToolResultContent.text = callToolResultContentTextParsed;
|
||||
} catch (err) {}
|
||||
}
|
||||
parsed.result.content[idx] = callToolResultContent;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
pretty = JSON.stringify(parsed, null, '\t');
|
||||
} catch {
|
||||
// Can't parse as JSON.
|
||||
@@ -87,7 +112,10 @@ export const MessageEventView = ({ event }: Props) => {
|
||||
<ItemContent
|
||||
icon={previewMode === mode ? 'check' : 'empty'}
|
||||
label={getPreviewModeName(mode, true)}
|
||||
onClick={() => patchRequestMeta(requestId, { previewMode: mode })}
|
||||
onClick={() => {
|
||||
patchRequestMeta(requestId, { previewMode: mode });
|
||||
editorRef.current?.setValue(mode === PREVIEW_MODE_FRIENDLY ? pretty : raw);
|
||||
}}
|
||||
/>
|
||||
</DropdownItem>
|
||||
))}
|
||||
@@ -109,6 +137,7 @@ export const MessageEventView = ({ event }: Props) => {
|
||||
mode={previewMode === PREVIEW_MODE_RAW ? 'text/plain' : 'text/json'}
|
||||
defaultValue={previewMode === PREVIEW_MODE_FRIENDLY ? pretty : raw}
|
||||
uniquenessKey={event._id}
|
||||
ref={editorRef}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,12 @@ import { Button, Heading, Tab, TabList, TabPanel, Tabs, Toolbar } from 'react-ar
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
import { useLatest } from 'react-use';
|
||||
|
||||
import { docsBase } from '~/common/documentation';
|
||||
import { buildResourceJsonSchema, fillUriTemplate } from '~/common/mcp-utils';
|
||||
import { useWorkspaceLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId';
|
||||
import { Link } from '~/ui/components/base/link';
|
||||
import { EnvironmentKVEditor } from '~/ui/components/editors/environment-key-value-editor/key-value-editor';
|
||||
import { Icon } from '~/ui/components/icon';
|
||||
import { InsomniaRjsfForm, type InsomniaRjsfFormHandle } from '~/ui/components/rjsf';
|
||||
|
||||
import { type AuthTypes } from '../../../common/constants';
|
||||
@@ -26,7 +29,7 @@ import { McpRootsPanel } from './mcp-roots-panel';
|
||||
import { McpUrlActionBar } from './mcp-url-bar';
|
||||
import type { PrimitiveSubItem } from './types';
|
||||
|
||||
const supportedAuthTypes: AuthTypes[] = ['basic', 'oauth2', 'bearer'];
|
||||
const supportedAuthTypes: AuthTypes[] = ['basic', 'oauth2', 'bearer', 'apikey'];
|
||||
export type RequestPaneTabs = 'params' | 'auth' | 'headers' | 'roots';
|
||||
|
||||
const PaneReadOnlyBanner = () => {
|
||||
@@ -63,6 +66,7 @@ export const McpRequestPane: FC<Props> = ({
|
||||
}) => {
|
||||
const primitiveId = `${selectedPrimitiveItem?.type}_${selectedPrimitiveItem?.name}`;
|
||||
const { activeRequest, activeRequestMeta, requestPayload } = useRequestLoaderData()! as McpRequestLoaderData;
|
||||
const [isCalling, setIsCalling] = useState(false);
|
||||
const latestRequestPayloadRef = useLatest(requestPayload);
|
||||
|
||||
const { activeProject } = useWorkspaceLoaderData()!;
|
||||
@@ -125,30 +129,37 @@ export const McpRequestPane: FC<Props> = ({
|
||||
[primitiveId, selectedPrimitiveItem?.type],
|
||||
);
|
||||
|
||||
const handleSend = () => {
|
||||
const handleSend = async () => {
|
||||
rjsfFormRef.current?.validate();
|
||||
if (selectedPrimitiveItem?.type === 'tools') {
|
||||
window.main.mcp.primitive.callTool({
|
||||
name: selectedPrimitiveItem?.name || '',
|
||||
parameters: mcpParams[primitiveId],
|
||||
requestId: requestId,
|
||||
});
|
||||
} else if (selectedPrimitiveItem?.type === 'resources') {
|
||||
window.main.mcp.primitive.readResource({
|
||||
requestId,
|
||||
uri: selectedPrimitiveItem?.uri || '',
|
||||
});
|
||||
} else if (selectedPrimitiveItem?.type === 'resourceTemplates') {
|
||||
window.main.mcp.primitive.readResource({
|
||||
requestId,
|
||||
uri: fillUriTemplate(selectedPrimitiveItem.uriTemplate, mcpParams[primitiveId] || {}),
|
||||
});
|
||||
} else if (selectedPrimitiveItem?.type === 'prompts') {
|
||||
window.main.mcp.primitive.getPrompt({
|
||||
requestId,
|
||||
name: selectedPrimitiveItem?.name || '',
|
||||
parameters: mcpParams[primitiveId],
|
||||
});
|
||||
try {
|
||||
setIsCalling(true);
|
||||
if (selectedPrimitiveItem?.type === 'tools') {
|
||||
await window.main.mcp.primitive.callTool({
|
||||
name: selectedPrimitiveItem?.name || '',
|
||||
parameters: mcpParams[primitiveId],
|
||||
requestId: requestId,
|
||||
});
|
||||
} else if (selectedPrimitiveItem?.type === 'resources') {
|
||||
await window.main.mcp.primitive.readResource({
|
||||
requestId,
|
||||
uri: selectedPrimitiveItem?.uri || '',
|
||||
});
|
||||
} else if (selectedPrimitiveItem?.type === 'resourceTemplates') {
|
||||
await window.main.mcp.primitive.readResource({
|
||||
requestId,
|
||||
uri: fillUriTemplate(selectedPrimitiveItem.uriTemplate, mcpParams[primitiveId] || {}),
|
||||
});
|
||||
} else if (selectedPrimitiveItem?.type === 'prompts') {
|
||||
await window.main.mcp.primitive.getPrompt({
|
||||
requestId,
|
||||
name: selectedPrimitiveItem?.name || '',
|
||||
parameters: mcpParams[primitiveId],
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('MCP primitive call error', err);
|
||||
} finally {
|
||||
setIsCalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -272,9 +283,11 @@ export const McpRequestPane: FC<Props> = ({
|
||||
</TabList>
|
||||
<TabPanel className="flex h-full w-full flex-1 flex-col overflow-y-auto" id="params">
|
||||
{!readyState ? (
|
||||
<div className="flex h-full w-full flex-col items-center gap-3 pt-[5%] text-center">
|
||||
<div className="flex h-full w-full flex-col items-center p-5 text-center">
|
||||
{/* Hint when mcp server is not connected*/}
|
||||
<span className="text-md">Enter MCP server url to discover capabilities</span>
|
||||
<p className="notice info text-md no-margin-top w-full">
|
||||
Connect to an MCP server URL to reveal capabilities. <Link href={docsBase}>Learn More</Link>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<PanelGroup className="flex-1 overflow-hidden" direction={'vertical'}>
|
||||
@@ -289,11 +302,19 @@ export const McpRequestPane: FC<Props> = ({
|
||||
onClick={handleSend}
|
||||
className="rounded bg-[--color-surprise] px-[--padding-md] text-center text-[--color-font-surprise]"
|
||||
>
|
||||
{isCalling && <Icon className="mr-1 animate-spin" icon="spinner" />}
|
||||
{sendButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Toolbar>
|
||||
{!selectedPrimitiveItem && (
|
||||
<div className="flex h-full w-full flex-col items-center p-5 text-center">
|
||||
<p className="notice info text-md no-margin-top w-full">
|
||||
Select an MCP server primitive from the list to start.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{jsonSchema && (
|
||||
<div className="overflow-auto p-4">
|
||||
<p>{selectedPrimitiveItem?.name}</p>
|
||||
@@ -350,6 +371,7 @@ export const McpRequestPane: FC<Props> = ({
|
||||
authTypes={supportedAuthTypes}
|
||||
hideInherit
|
||||
showMcpAuthFlow
|
||||
addToHeaderOnly
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel className="w-full flex-1 overflow-y-auto" id="headers">
|
||||
|
||||
@@ -109,6 +109,9 @@ export const McpUrlActionBar = ({
|
||||
} else if (authentication.type === 'bearer' && authentication.token) {
|
||||
const { token, prefix } = authentication;
|
||||
headers.push(getBearerAuthHeader(token, prefix));
|
||||
} else if (authentication.type === 'apikey') {
|
||||
const { key, value } = authentication;
|
||||
headers.push({ name: key, value });
|
||||
} else if (authentication.type === 'oauth2') {
|
||||
const oAuth2Token = await getOAuth2Token(request._id, authentication as AuthTypeOAuth2);
|
||||
if (oAuth2Token) {
|
||||
@@ -213,38 +216,35 @@ export const McpUrlActionBar = ({
|
||||
}, []);
|
||||
|
||||
const isConnectingOrClosed = !readyState;
|
||||
const isDropdownDisabled = isOpen || isConnecting;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isOpen && (
|
||||
<div className="flex items-center">
|
||||
<Dropdown
|
||||
triggerButton={
|
||||
<RaButton className="pl-2" aria-label="Request Method">
|
||||
<span>{requestTransportTypeLabel}</span> <i className="fa fa-caret-down space-left" />
|
||||
</RaButton>
|
||||
}
|
||||
placement="bottom start"
|
||||
>
|
||||
<DropdownSection>
|
||||
{MCP_TRANSPORT_TYPES.map(transportType => (
|
||||
<DropdownItem key={transportType}>
|
||||
<ItemContent
|
||||
label={getTransportLabel(transportType)}
|
||||
onClick={() => patchRequest(request._id, { transportType })}
|
||||
/>
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownSection>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
{isOpen && (
|
||||
<span className="text-success flex items-center pl-[--padding-md]">
|
||||
<span className="mr-[--padding-sm] h-2.5 w-2.5 rounded-[50%] bg-[--color-success]" />
|
||||
CONNECTED
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Dropdown
|
||||
triggerButton={
|
||||
<RaButton
|
||||
className={`pl-2 ${isDropdownDisabled ? 'cursor-not-allowed opacity-30' : ''}`}
|
||||
aria-label="Request Method"
|
||||
>
|
||||
<span>{requestTransportTypeLabel}</span> <i className="fa fa-caret-down space-left" />
|
||||
</RaButton>
|
||||
}
|
||||
placement="bottom start"
|
||||
isDisabled={isDropdownDisabled}
|
||||
>
|
||||
<DropdownSection>
|
||||
{MCP_TRANSPORT_TYPES.map(transportType => (
|
||||
<DropdownItem key={transportType}>
|
||||
<ItemContent
|
||||
label={getTransportLabel(transportType)}
|
||||
onClick={() => patchRequest(request._id, { transportType })}
|
||||
/>
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownSection>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<form
|
||||
className="flex flex-1"
|
||||
aria-disabled={isOpen}
|
||||
@@ -273,7 +273,7 @@ export const McpUrlActionBar = ({
|
||||
disabled={connectRequestFetcher.state === 'submitting' || connectRequestFetcher.state === 'loading'}
|
||||
type="submit"
|
||||
>
|
||||
Discover
|
||||
Connect
|
||||
</button>
|
||||
) : (
|
||||
<DisconnectButton requestId={request._id} />
|
||||
|
||||
Reference in New Issue
Block a user