diff --git a/packages/insomnia/src/common/render.ts b/packages/insomnia/src/common/render.ts index e3f24ee9d7..d6af7135a7 100644 --- a/packages/insomnia/src/common/render.ts +++ b/packages/insomnia/src/common/render.ts @@ -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, diff --git a/packages/insomnia/src/main/network/mcp.ts b/packages/insomnia/src/main/network/mcp.ts index 8a49889a30..4ad0d16fb4 100644 --- a/packages/insomnia/src/main/network/mcp.ts +++ b/packages/insomnia/src/main/network/mcp.ts @@ -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, { name, value }: RequestHeader) => ({ ...acc, [name.toLowerCase() || '']: value || '', diff --git a/packages/insomnia/src/network/network.ts b/packages/insomnia/src/network/network.ts index ea0a1b6a09..8df66178c8 100644 --- a/packages/insomnia/src/network/network.ts +++ b/packages/insomnia/src/network/network.ts @@ -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, diff --git a/packages/insomnia/src/network/o-auth-2/get-token.ts b/packages/insomnia/src/network/o-auth-2/get-token.ts index 66dd55a357..10b7432a60 100644 --- a/packages/insomnia/src/network/o-auth-2/get-token.ts +++ b/packages/insomnia/src/network/o-auth-2/get-token.ts @@ -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(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(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; diff --git a/packages/insomnia/src/ui/components/dropdowns/auth-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/auth-dropdown.tsx index fc7b4c364d..9c844f9d0b 100644 --- a/packages/insomnia/src/ui/components/dropdowns/auth-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/auth-dropdown.tsx @@ -145,6 +145,7 @@ interface Props { authTypes?: AuthTypes[]; disabled?: boolean; hideOthers?: boolean; + hideInherit?: boolean; } export const AuthDropdown: FC = ({ @@ -152,6 +153,7 @@ export const AuthDropdown: FC = ({ authTypes = defaultTypes, disabled = false, hideOthers = false, + hideInherit = false, }) => { const { requestId, requestGroupId } = useParams() as { organizationId: string; @@ -257,10 +259,14 @@ export const AuthDropdown: FC = ({ 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', diff --git a/packages/insomnia/src/ui/components/editors/auth/auth-wrapper.tsx b/packages/insomnia/src/ui/components/editors/auth/auth-wrapper.tsx index f9bf2e3830..d569d1f5c8 100644 --- a/packages/insomnia/src/ui/components/editors/auth/auth-wrapper.tsx +++ b/packages/insomnia/src/ui/components/editors/auth/auth-wrapper.tsx @@ -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 ( <> - +
{authBody}
diff --git a/packages/insomnia/src/ui/components/mcp/mcp-request-pane.tsx b/packages/insomnia/src/ui/components/mcp/mcp-request-pane.tsx index b69d7bf7a5..a6b8868bf7 100644 --- a/packages/insomnia/src/ui/components/mcp/mcp-request-pane.tsx +++ b/packages/insomnia/src/ui/components/mcp/mcp-request-pane.tsx @@ -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 = ({ environment, readyState, selectedPri authentication={activeRequest.authentication} disabled={readyState} authTypes={supportedAuthTypes} - hideOthers + hideInherit /> diff --git a/packages/insomnia/src/ui/components/mcp/mcp-url-bar.tsx b/packages/insomnia/src/ui/components/mcp/mcp-url-bar.tsx index 6689aff274..ba0d230158 100644 --- a/packages/insomnia/src/ui/components/mcp/mcp-url-bar.tsx +++ b/packages/insomnia/src/ui/components/mcp/mcp-url-bar.tsx @@ -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,