🔐 feat: Add API key authentication support for MCP servers (#10936)

* 🔐 feat: Add API key authentication support for MCP servers

* Chore: Copilot comments fixes

---------

Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
This commit is contained in:
Atef Bellaaj 2025-12-12 19:51:49 +01:00 committed by GitHub
parent abeaab6e17
commit e15d37b399
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 836 additions and 84 deletions

View file

@ -28,6 +28,7 @@ enum AuthorizationTypeEnum {
export interface AuthConfig {
auth_type?: AuthTypeEnum;
api_key?: string;
api_key_source?: 'admin' | 'user'; // Whether admin provides key for all or each user provides their own
api_key_authorization_type?: AuthorizationTypeEnum;
api_key_custom_header?: string;
oauth_client_id?: string;
@ -171,8 +172,6 @@ export default function MCPAuth({
{localize('com_ui_none')}
</label>
</div>
{/*
TODO Support API keys for auth
<div className="flex items-center gap-2">
<label htmlFor="auth-apikey" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item
@ -189,7 +188,7 @@ export default function MCPAuth({
</RadioGroup.Item>
{localize('com_ui_api_key')}
</label>
</div> */}
</div>
<div className="flex items-center gap-2">
<label htmlFor="auth-oauth" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item
@ -228,21 +227,84 @@ export default function MCPAuth({
const ApiKey = ({ inputClasses }: { inputClasses: string }) => {
const localize = useLocalize();
const { register, watch, setValue } = useFormContext();
const authorization_type = watch('api_key_authorization_type') || AuthorizationTypeEnum.Basic;
const api_key_source = watch('api_key_source') || 'admin';
const authorization_type = watch('api_key_authorization_type') || AuthorizationTypeEnum.Bearer;
return (
<>
<label className="mb-1 block text-sm font-medium">{localize('com_ui_api_key')}</label>
<input
placeholder="<HIDDEN>"
type="password"
autoComplete="new-password"
className={inputClasses}
{...register('api_key')}
/>
<label className="mb-1 block text-sm font-medium">{localize('com_ui_auth_type')}</label>
{/* API Key Source selection */}
<label className="mb-1 block text-sm font-medium">{localize('com_ui_api_key_source')}</label>
<RadioGroup.Root
defaultValue={AuthorizationTypeEnum.Basic}
defaultValue="admin"
onValueChange={(value) => setValue('api_key_source', value)}
value={api_key_source}
role="radiogroup"
aria-required="true"
dir="ltr"
className="mb-3 flex flex-col gap-2"
style={{ outline: 'none' }}
>
<div className="flex items-center gap-2">
<label htmlFor="source-admin" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item
type="button"
role="radio"
value="admin"
id="source-admin"
className={cn(
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
'border-border-heavy bg-surface-primary',
)}
>
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
</RadioGroup.Item>
{localize('com_ui_admin_provides_key')}
</label>
</div>
<div className="flex items-center gap-2">
<label htmlFor="source-user" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item
type="button"
role="radio"
value="user"
id="source-user"
className={cn(
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
'border-border-heavy bg-surface-primary',
)}
>
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
</RadioGroup.Item>
{localize('com_ui_user_provides_key')}
</label>
</div>
</RadioGroup.Root>
{/* API Key input - only show for admin-provided mode */}
{api_key_source === 'admin' && (
<>
<label className="mb-1 block text-sm font-medium">{localize('com_ui_api_key')}</label>
<input
placeholder="<HIDDEN>"
type="password"
autoComplete="new-password"
className={inputClasses}
{...register('api_key')}
/>
</>
)}
{/* User-provided mode info */}
{api_key_source === 'user' && (
<div className="mb-3 rounded-lg border border-border-medium bg-surface-secondary p-3">
<p className="text-sm text-text-secondary">{localize('com_ui_user_provides_key_note')}</p>
</div>
)}
{/* Header Format selection - shown for both modes */}
<label className="mb-1 block text-sm font-medium">{localize('com_ui_header_format')}</label>
<RadioGroup.Root
defaultValue={AuthorizationTypeEnum.Bearer}
onValueChange={(value) => setValue('api_key_authorization_type', value)}
value={authorization_type}
role="radiogroup"
@ -251,23 +313,6 @@ const ApiKey = ({ inputClasses }: { inputClasses: string }) => {
className="mb-2 flex gap-6 overflow-hidden rounded-lg"
style={{ outline: 'none' }}
>
<div className="flex items-center gap-2">
<label htmlFor="auth-basic" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item
type="button"
role="radio"
value={AuthorizationTypeEnum.Basic}
id="auth-basic"
className={cn(
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
'border-border-heavy bg-surface-primary',
)}
>
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
</RadioGroup.Item>
{localize('com_ui_basic')}
</label>
</div>
<div className="flex items-center gap-2">
<label htmlFor="auth-bearer" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item
@ -285,6 +330,23 @@ const ApiKey = ({ inputClasses }: { inputClasses: string }) => {
{localize('com_ui_bearer')}
</label>
</div>
<div className="flex items-center gap-2">
<label htmlFor="auth-basic" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item
type="button"
role="radio"
value={AuthorizationTypeEnum.Basic}
id="auth-basic"
className={cn(
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
'border-border-heavy bg-surface-primary',
)}
>
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
</RadioGroup.Item>
{localize('com_ui_basic')}
</label>
</div>
<div className="flex items-center gap-2">
<label htmlFor="auth-custom" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item

View file

@ -85,10 +85,13 @@ export default function MCPServerDialog({
let authType: AuthTypeEnum = AuthTypeEnum.None;
if (server.config.oauth) {
authType = AuthTypeEnum.OAuth;
} else if ('api_key' in server.config) {
} else if ('apiKey' in server.config && server.config.apiKey) {
authType = AuthTypeEnum.ServiceHttp;
}
// Extract apiKey config if present
const apiKeyConfig = 'apiKey' in server.config ? server.config.apiKey : undefined;
return {
title: server.config.title || '',
description: server.config.description || '',
@ -97,9 +100,12 @@ export default function MCPServerDialog({
icon: server.config.iconPath || '',
auth: {
auth_type: authType,
api_key: '',
api_key_authorization_type: AuthorizationTypeEnum.Basic,
api_key_custom_header: '',
api_key: '', // NEVER pre-fill secrets
api_key_source: (apiKeyConfig?.source as 'admin' | 'user') || 'admin',
api_key_authorization_type:
(apiKeyConfig?.authorization_type as AuthorizationTypeEnum) ||
AuthorizationTypeEnum.Bearer,
api_key_custom_header: apiKeyConfig?.custom_header || '',
oauth_client_id: server.config.oauth?.client_id || '',
oauth_client_secret: '', // NEVER pre-fill secrets
oauth_authorization_url: server.config.oauth?.authorization_url || '',
@ -119,7 +125,8 @@ export default function MCPServerDialog({
auth: {
auth_type: AuthTypeEnum.None,
api_key: '',
api_key_authorization_type: AuthorizationTypeEnum.Basic,
api_key_source: 'admin',
api_key_authorization_type: AuthorizationTypeEnum.Bearer,
api_key_custom_header: '',
oauth_client_id: '',
oauth_client_secret: '',
@ -251,6 +258,22 @@ export default function MCPServerDialog({
}
}
// Add API Key if auth type is service_http
if (formData.auth.auth_type === AuthTypeEnum.ServiceHttp) {
const source = formData.auth.api_key_source || 'admin';
const authorizationType = formData.auth.api_key_authorization_type || 'bearer';
config.apiKey = {
source,
authorization_type: authorizationType,
...(source === 'admin' && formData.auth.api_key && { key: formData.auth.api_key }),
...(authorizationType === 'custom' &&
formData.auth.api_key_custom_header && {
custom_header: formData.auth.api_key_custom_header,
}),
};
}
const params: MCPServerCreateParams = {
config,
};