Skip to content

Commit 1c1c7b4

Browse files
authored
fix: Add state and scope input UI for MCP OAuth parameter (#9507)
* add state and scope support * validate the form before sending, but don't block sending on validation errors for debug purpose * support public oAuth client without client secret
1 parent 94d0764 commit 1c1c7b4

File tree

6 files changed

+75
-45
lines changed

6 files changed

+75
-45
lines changed

packages/insomnia/src/main/mcp/oauth-client-provider.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,16 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
9292
}
9393

9494
if ('clientId' in this.mcpRequest.authentication && this.mcpRequest.authentication.clientId) {
95+
const { clientId, clientSecret, clientIdIssuedAt, clientSecretExpiresAt } = this.mcpRequest.authentication;
96+
97+
// https://github.com/modelcontextprotocol/typescript-sdk/blob/6b4d99f10b975d65392bb777cc8cb1151c20c972/packages/client/src/client/auth.ts#L223%20
98+
// Set client_secret to undefined if it's not set or empty string
99+
const parsedClientSecret = clientSecret && clientSecret.trim().length > 0 ? clientSecret : undefined;
95100
return {
96-
client_id: this.mcpRequest.authentication.clientId,
97-
client_secret: this.mcpRequest.authentication.clientSecret,
98-
client_id_issued_at: this.mcpRequest.authentication.clientIdIssuedAt,
99-
client_secret_expires_at: this.mcpRequest.authentication.clientSecretExpiresAt,
101+
client_id: clientId,
102+
client_secret: parsedClientSecret,
103+
client_id_issued_at: clientIdIssuedAt,
104+
client_secret_expires_at: clientSecretExpiresAt,
100105
};
101106
}
102107
return;
@@ -148,6 +153,13 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
148153
});
149154
await this.refreshMcpRequest();
150155
}
156+
// add state parameter to authorization url if present in authentication
157+
async state() {
158+
if ('state' in this.mcpRequest.authentication && this.mcpRequest.authentication.state) {
159+
return this.mcpRequest.authentication.state;
160+
}
161+
return '';
162+
}
151163
saveResourceMetadataUrl(url: URL | undefined) {
152164
this._resourceMetadataUrl = url;
153165
}

packages/insomnia/src/main/mcp/transport-streamable-http.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
auth,
33
type AuthResult,
4-
extractResourceMetadataUrl,
4+
extractWWWAuthenticateParams,
55
UnauthorizedError,
66
} from '@modelcontextprotocol/sdk/client/auth.js';
77
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
@@ -160,7 +160,12 @@ const wrappedFetch = async (
160160
}
161161
}
162162

163-
const resourceMetadataUrl = extractResourceMetadataUrl(response);
163+
const { resourceMetadataUrl, scope: scopeFromWWWAuthenticate } = extractWWWAuthenticateParams(response);
164+
// Use scope from authenticate config if available, otherwise use scope from WWW-Authenticate header
165+
const scope =
166+
'scope' in authentication && authentication.scope && authentication.scope.trim().length > 0
167+
? authentication.scope
168+
: scopeFromWWWAuthenticate;
164169
if (resourceMetadataUrl) {
165170
authProvider.saveResourceMetadataUrl(resourceMetadataUrl);
166171
}
@@ -183,6 +188,7 @@ const wrappedFetch = async (
183188
const redirectPromise = new Promise<string>(res => (authPromiseResolve = res));
184189
const unsubscribe = authProvider.onRedirectEnd(async (authorizationCode: string) => {
185190
// Resolve the promise to continue the auth flow after user has completed authorization in default browser
191+
// Will be triggered when `redirectToAuthorization` completes in the oauth client provider
186192
authPromiseResolve(authorizationCode);
187193
});
188194

@@ -243,6 +249,7 @@ const wrappedFetch = async (
243249
serverUrl: url,
244250
resourceMetadataUrl,
245251
fetchFn: authFetchFn,
252+
scope,
246253
});
247254
// Wait for user to complete authorization in default browser and get authorization code
248255
if (authResult === 'REDIRECT') {
@@ -252,6 +259,7 @@ const wrappedFetch = async (
252259
authResult = await auth(authProvider, {
253260
serverUrl: url,
254261
resourceMetadataUrl,
262+
scope,
255263
authorizationCode,
256264
fetchFn: authFetchFn,
257265
});
@@ -268,6 +276,7 @@ const wrappedFetch = async (
268276
BrowserWindow.getAllWindows().forEach(window => {
269277
window.webContents.send('hide-oauth-authorization-modal');
270278
});
279+
// cleanup the oauth client provider listener
271280
unsubscribe();
272281
}
273282
return await wrappedFetch(url, init, context, authProvider, true);

packages/insomnia/src/ui/components/editors/auth/o-auth-2-auth.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ const getFieldsForGrantType = (authentication: Extract<RequestAuthentication, {
292292
advanced = [responseType, scope, state, tokenPrefix, audience];
293293
} else if (grantType === GRANT_TYPE_MCP_AUTH_FLOW) {
294294
basic = [clientId, clientSecret, readonlyRedirectUri];
295-
advanced = [];
295+
advanced = [state, scope];
296296
}
297297

298298
return {
@@ -320,6 +320,9 @@ export const OAuth2Auth = ({ showMcpAuthFlow, disabled }: { showMcpAuthFlow?: bo
320320
options={showMcpAuthFlow ? grantTypeOptionsWithMcpAuthFlow : grantTypeOptions}
321321
/>
322322
{basic}
323+
<AuthAccordion accordionKey="OAuth2AdvancedOptions" label="Advanced Options">
324+
{advanced}
325+
</AuthAccordion>
323326
</AuthTableBody>
324327
<div className="pad">
325328
<OAuth2Tokens hideRefresh />

packages/insomnia/src/ui/components/mcp/mcp-request-pane.tsx

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -133,34 +133,34 @@ export const McpRequestPane: FC<Props> = ({
133133
);
134134

135135
const handleSend = async () => {
136-
if (rjsfFormRef.current?.validate()) {
137-
try {
138-
if (selectedPrimitiveItem?.type === 'tools') {
139-
await window.main.mcp.primitive.callTool({
140-
name: selectedPrimitiveItem?.name || '',
141-
arguments: mcpParams[primitiveId],
142-
requestId: requestId,
143-
});
144-
} else if (selectedPrimitiveItem?.type === 'resources') {
145-
await window.main.mcp.primitive.readResource({
146-
requestId,
147-
uri: selectedPrimitiveItem?.uri || '',
148-
});
149-
} else if (selectedPrimitiveItem?.type === 'resourceTemplates') {
150-
await window.main.mcp.primitive.readResource({
151-
requestId,
152-
uri: fillUriTemplate(selectedPrimitiveItem.uriTemplate, mcpParams[primitiveId] || {}),
153-
});
154-
} else if (selectedPrimitiveItem?.type === 'prompts') {
155-
await window.main.mcp.primitive.getPrompt({
156-
requestId,
157-
name: selectedPrimitiveItem?.name || '',
158-
arguments: mcpParams[primitiveId],
159-
});
160-
}
161-
} catch (err) {
162-
console.warn('MCP primitive call error', err);
136+
// validate the form before sending, but don't block sending on validation errors for debug purpose
137+
rjsfFormRef.current?.validate();
138+
try {
139+
if (selectedPrimitiveItem?.type === 'tools') {
140+
await window.main.mcp.primitive.callTool({
141+
name: selectedPrimitiveItem?.name || '',
142+
arguments: mcpParams[primitiveId],
143+
requestId: requestId,
144+
});
145+
} else if (selectedPrimitiveItem?.type === 'resources') {
146+
await window.main.mcp.primitive.readResource({
147+
requestId,
148+
uri: selectedPrimitiveItem?.uri || '',
149+
});
150+
} else if (selectedPrimitiveItem?.type === 'resourceTemplates') {
151+
await window.main.mcp.primitive.readResource({
152+
requestId,
153+
uri: fillUriTemplate(selectedPrimitiveItem.uriTemplate, mcpParams[primitiveId] || {}),
154+
});
155+
} else if (selectedPrimitiveItem?.type === 'prompts') {
156+
await window.main.mcp.primitive.getPrompt({
157+
requestId,
158+
name: selectedPrimitiveItem?.name || '',
159+
arguments: mcpParams[primitiveId],
160+
});
163161
}
162+
} catch (err) {
163+
console.warn('MCP primitive call error', err);
164164
}
165165
};
166166

packages/insomnia/src/ui/components/viewers/response-error-viewer.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ interface Props {
1313
url: string;
1414
docsLink?: string;
1515
isMcpResponse?: boolean;
16+
showErrorDetails?: boolean;
1617
}
17-
export const ResponseErrorViewer: FC<Props> = memo(({ error, docsLink, isMcpResponse }) => {
18+
export const ResponseErrorViewer: FC<Props> = memo(({ error, docsLink, isMcpResponse, showErrorDetails = true }) => {
1819
const [isCertificatesModalOpen, setCertificatesModalOpen] = useState(false);
1920
let msg: React.ReactNode = null;
2021
const { settings } = useRootLoaderData()!;
@@ -52,15 +53,19 @@ export const ResponseErrorViewer: FC<Props> = memo(({ error, docsLink, isMcpResp
5253

5354
return (
5455
<div>
55-
<pre
56-
className="selectable pad force-pre-wrap"
57-
style={{
58-
fontSize: `${editorFontSize}px`,
59-
}}
60-
>
61-
{error}
62-
</pre>
63-
<hr />
56+
{showErrorDetails && (
57+
<>
58+
<pre
59+
className="selectable pad force-pre-wrap"
60+
style={{
61+
fontSize: `${editorFontSize}px`,
62+
}}
63+
>
64+
{error}
65+
</pre>
66+
<hr />
67+
</>
68+
)}
6469
<div className="pad text-center">
6570
<p className="faint pad-left pad-right">Here are some additional things that may help.</p>
6671
{msg}

packages/insomnia/src/ui/components/websockets/realtime-response-pane.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ const RealtimeActiveResponsePane: FC<RealtimeActiveResponsePaneProps & { readySt
314314
<TabPanel className="flex w-full flex-1 flex-col overflow-hidden" id="events">
315315
<PanelGroup direction="vertical" className="grid h-full w-full grid-rows-[repeat(auto-fit,minmax(0,1fr))]">
316316
{response.error && !isMCPAuthError ? (
317-
<ResponseErrorViewer url={response.url} error={response.error} />
317+
<ResponseErrorViewer url={response.url} error={response.error} isMcpResponse={isMcpResponse(response)} />
318318
) : (
319319
<>
320320
<Panel minSize={10} defaultSize={50} className="box-border flex w-full flex-1 flex-col overflow-hidden">
@@ -383,6 +383,7 @@ const RealtimeActiveResponsePane: FC<RealtimeActiveResponsePaneProps & { readySt
383383
url={response.url}
384384
error={response.error}
385385
docsLink={docsMcpAuthentication}
386+
showErrorDetails={false}
386387
isMcpResponse
387388
/>
388389
) : null}

0 commit comments

Comments
 (0)