feat: complete the existing auth for mcp client (#9159)

This commit is contained in:
Bingbing
2025-09-23 11:21:37 +08:00
committed by Kent Wang
parent 6b7924566b
commit f4bbb8e9d4
8 changed files with 109 additions and 38 deletions

View File

@@ -711,6 +711,7 @@ export async function getRenderContextAncestors(
models.request.type,
models.grpcRequest.type,
models.webSocketRequest.type,
models.mcpRequest.type,
models.requestGroup.type,
models.workspace.type,
models.project.type,

View File

@@ -37,8 +37,6 @@ import * as models from '~/models';
import { TRANSPORT_TYPES, type TransportType } from '~/models/mcp-request';
import type { McpResponse } from '~/models/mcp-response';
import type { RequestAuthentication, RequestHeader } from '~/models/request';
import { getBasicAuthHeader } from '~/network/basic-auth/get-header';
import { getBearerAuthHeader } from '~/network/bearer-auth/get-header';
import { invariant } from '~/utils/invariant';
import { ipcMainHandle, ipcMainOn } from '../ipc/electron';
@@ -383,21 +381,6 @@ const createStreamableHTTPTransport = (
throw new Error('MCP server url is required');
}
if (!options.authentication.disabled) {
if (options.authentication.type === 'basic') {
const { username, password, useISO88591 } = options.authentication;
const encoding = useISO88591 ? 'latin1' : 'utf8';
options.headers.push(getBasicAuthHeader(username, password, encoding));
}
if (options.authentication.type === 'apikey') {
const { key = '', value = '' } = options.authentication;
options.headers.push({ name: key, value: value });
}
if (options.authentication.type === 'bearer' && options.authentication.token) {
const { token, prefix } = options.authentication;
options.headers.push(getBearerAuthHeader(token, prefix));
}
}
const reduceArrayToLowerCaseKeyedDictionary = (acc: Record<string, string>, { name, value }: RequestHeader) => ({
...acc,
[name.toLowerCase() || '']: value || '',

View File

@@ -245,6 +245,43 @@ export const fetchRequestData = async (
};
};
export const fetchMcpRequestData = async (mcpRequestId: string) => {
const mcpRequest = await models.mcpRequest.getById(mcpRequestId);
invariant(mcpRequest, 'failed to find MCP request ' + mcpRequestId);
const workspace = await models.workspace.getById(mcpRequest.parentId);
invariant(workspace, 'failed to find workspace');
const workspaceId = workspace._id;
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspaceId);
const activeEnvironmentId = workspaceMeta.activeEnvironmentId;
const activeEnvironment = activeEnvironmentId && (await models.environment.getById(activeEnvironmentId));
const baseEnvironment = await models.environment.getOrCreateForParentId(workspaceId);
// no active environment in workspaceMeta, fallback to workspace root environment as active environment
const environment = activeEnvironment || baseEnvironment;
invariant(environment, 'failed to find environment ' + activeEnvironmentId);
const settings = await models.settings.get();
invariant(settings, 'failed to create settings');
const responseId = generateId('res');
const responsesDir = pathJoin(
process.env['INSOMNIA_DATA_PATH'] ||
(process.type === 'renderer' ? window : require('electron')).app.getPath('userData'),
'responses',
);
const timelinePath = pathJoin(responsesDir, responseId + '.timeline');
return {
environment,
settings,
clientCertificates: [] as ClientCertificate[],
caCert: undefined,
activeEnvironmentId,
timelinePath,
responseId,
};
};
export const tryToExecutePreRequestScript = async (
{
request,

View File

@@ -3,6 +3,8 @@ import querystring from 'node:querystring';
import { v4 as uuidv4 } from 'uuid';
import { isMcpRequestId } from '~/models/mcp-request';
import { version } from '../../../package.json';
import { getOauthRedirectUrl } from '../../common/constants';
import { database as db } from '../../common/database';
@@ -19,6 +21,7 @@ import { setDefaultProtocol } from '../../utils/url/protocol';
import { getAuthObjectOrNull, isAuthEnabled } from '../authentication';
import { getBasicAuthHeader } from '../basic-auth/get-header';
import {
fetchMcpRequestData,
fetchRequestData,
fetchRequestGroupData,
responseTransform,
@@ -252,16 +255,21 @@ async function getExistingAccessTokenAndRefreshIfExpired(
authentication: AuthTypeOAuth2,
forceRefresh: boolean,
): Promise<{ oAuth2Token: OAuth2Token | undefined; closestAuthId: string }> {
const activeRequest = await models.request.getById(requestId);
const requestGroups = (
await db.withAncestors<Request | RequestGroup>(activeRequest, [models.requestGroup.type])
).filter(isRequestGroup) as RequestGroup[];
const closestFolderAuth = [...requestGroups]
.reverse()
.find(({ authentication }) => getAuthObjectOrNull(authentication) && isAuthEnabled(authentication));
const isRequestAuthEnabled =
getAuthObjectOrNull(activeRequest?.authentication) && isAuthEnabled(activeRequest?.authentication);
const closestAuthId = isRequestAuthEnabled ? requestId : closestFolderAuth?._id || requestId;
let closestAuthId = requestId;
if (!isMcpRequestId(requestId)) {
const activeRequest = await models.request.getById(requestId);
const requestGroups = (
await db.withAncestors<Request | RequestGroup>(activeRequest, [models.requestGroup.type])
).filter(isRequestGroup) as RequestGroup[];
const closestFolderAuth = [...requestGroups]
.reverse()
.find(({ authentication }) => getAuthObjectOrNull(authentication) && isAuthEnabled(authentication));
const isRequestAuthEnabled =
getAuthObjectOrNull(activeRequest?.authentication) && isAuthEnabled(activeRequest?.authentication);
closestAuthId = isRequestAuthEnabled ? requestId : closestFolderAuth?._id || requestId;
}
const token = await models.oAuth2Token.getByParentId(closestAuthId);
if (!token) {
return { oAuth2Token: undefined, closestAuthId };
@@ -391,10 +399,12 @@ const sendAccessTokenRequest = async (
) => {
invariant(authentication.accessTokenUrl, 'Missing access token URL');
console.log(`[network] Sending with settings req=${requestOrGroupId}`);
// @TODO unpack oauth into regular timeline and remove oauth timeine dialog
// @TODO unpack oauth into regular timeline and remove oauth timeline dialog
const initializedData = isRequestGroupId(requestOrGroupId)
? await fetchRequestGroupData(requestOrGroupId)
: await fetchRequestData(requestOrGroupId);
: isMcpRequestId(requestOrGroupId)
? await fetchMcpRequestData(requestOrGroupId)
: await fetchRequestData(requestOrGroupId);
const { environment, settings, clientCertificates, caCert, activeEnvironmentId, timelinePath, responseId } =
initializedData;

View File

@@ -145,6 +145,7 @@ interface Props {
authTypes?: AuthTypes[];
disabled?: boolean;
hideOthers?: boolean;
hideInherit?: boolean;
}
export const AuthDropdown: FC<Props> = ({
@@ -152,6 +153,7 @@ export const AuthDropdown: FC<Props> = ({
authTypes = defaultTypes,
disabled = false,
hideOthers = false,
hideInherit = false,
}) => {
const { requestId, requestGroupId } = useParams() as {
organizationId: string;
@@ -257,10 +259,14 @@ export const AuthDropdown: FC<Props> = ({
name: 'Other',
icon: 'ellipsis-h',
items: [
{
id: 'inherit',
name: 'Inherit from parent',
},
...(hideInherit
? []
: [
{
id: 'inherit',
name: 'Inherit from parent',
} as const,
]),
{
id: 'none',
name: 'None',

View File

@@ -24,7 +24,8 @@ export const AuthWrapper: FC<{
disabled?: boolean;
authTypes?: AuthTypes[];
hideOthers?: boolean;
}> = ({ authentication, disabled = false, authTypes, hideOthers }) => {
hideInherit?: boolean;
}> = ({ authentication, disabled = false, authTypes, hideOthers, hideInherit }) => {
const type = getAuthObjectOrNull(authentication)?.type || '';
let authBody: ReactNode = null;
@@ -74,7 +75,12 @@ export const AuthWrapper: FC<{
return (
<>
<Toolbar className="flex h-[--line-height-sm] w-full flex-shrink-0 items-center border-b border-solid border-[--hl-md] px-2">
<AuthDropdown authentication={authentication} authTypes={authTypes} hideOthers={hideOthers} />
<AuthDropdown
authentication={authentication}
authTypes={authTypes}
hideOthers={hideOthers}
hideInherit={hideInherit}
/>
</Toolbar>
<div className="flex-1 overflow-y-auto">{authBody}</div>
</>

View File

@@ -24,7 +24,7 @@ import { Pane } from '../panes/pane';
import { McpUrlActionBar } from './mcp-url-bar';
import type { PrimitiveSubItem } from './types';
const supportedAuthTypes: AuthTypes[] = ['apikey', 'oauth2', 'bearer'];
const supportedAuthTypes: AuthTypes[] = ['basic', 'oauth2', 'bearer'];
const PaneReadOnlyBanner = () => {
return (
@@ -311,7 +311,7 @@ export const McpRequestPane: FC<Props> = ({ environment, readyState, selectedPri
authentication={activeRequest.authentication}
disabled={readyState}
authTypes={supportedAuthTypes}
hideOthers
hideInherit
/>
</TabPanel>
<TabPanel className="w-full flex-1 overflow-y-auto" id="headers">

View File

@@ -5,6 +5,11 @@ import { useParams } from 'react-router';
import { useLatest } from 'react-use';
import { type Project } from '~/models/project';
import type { AuthTypeOAuth2 } from '~/models/request';
import { _buildBearerHeader } from '~/network/authentication';
import { getBasicAuthHeader } from '~/network/basic-auth/get-header';
import { getBearerAuthHeader } from '~/network/bearer-auth/get-header';
import { getOAuth2Token } from '~/network/o-auth-2/get-token';
import {
type ConnectActionParams,
useRequestConnectActionFetcher,
@@ -90,10 +95,33 @@ export const McpUrlActionBar = ({
env: getDataFromKVPair(request.env).data,
},
});
const { authentication, headers } = rendered;
if (!authentication.disabled) {
if (authentication.type === 'basic') {
const { username, password, useISO88591 } = authentication;
const encoding = useISO88591 ? 'latin1' : 'utf8';
headers.push(getBasicAuthHeader(username, password, encoding));
} else if (authentication.type === 'bearer' && authentication.token) {
const { token, prefix } = authentication;
headers.push(getBearerAuthHeader(token, prefix));
} else if (authentication.type === 'oauth2') {
const oAuth2Token = await getOAuth2Token(request._id, authentication as AuthTypeOAuth2);
if (oAuth2Token) {
const token = oAuth2Token.accessToken;
const authHeader = _buildBearerHeader(token, authentication.tokenPrefix);
if (authHeader) {
headers.push(authHeader);
}
}
}
}
return {
url: rendered.url,
transportType: request.transportType,
headers: rendered.headers,
headers: headers,
authentication: rendered.authentication,
suppressUserAgent: rendered.suppressUserAgent,
cookieJar: rendered.workspaceCookieJar,