mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-20 22:27:24 -04:00
feat: complete the existing auth for mcp client (#9159)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 || '',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user