mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-23 18:29:21 -05:00
Add OAuth support for upstream MCP servers (#381)
Co-authored-by: samanhappy <samanhappy@gmail.com>
This commit is contained in:
@@ -1,188 +1,207 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Server } from '@/types'
|
||||
import { ChevronDown, ChevronRight, AlertCircle, Copy, Check } from 'lucide-react'
|
||||
import { StatusBadge } from '@/components/ui/Badge'
|
||||
import ToolCard from '@/components/ui/ToolCard'
|
||||
import PromptCard from '@/components/ui/PromptCard'
|
||||
import DeleteDialog from '@/components/ui/DeleteDialog'
|
||||
import { useToast } from '@/contexts/ToastContext'
|
||||
import { useSettingsData } from '@/hooks/useSettingsData'
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Server } from '@/types';
|
||||
import { ChevronDown, ChevronRight, AlertCircle, Copy, Check } from 'lucide-react';
|
||||
import { StatusBadge } from '@/components/ui/Badge';
|
||||
import ToolCard from '@/components/ui/ToolCard';
|
||||
import PromptCard from '@/components/ui/PromptCard';
|
||||
import DeleteDialog from '@/components/ui/DeleteDialog';
|
||||
import { useToast } from '@/contexts/ToastContext';
|
||||
import { useSettingsData } from '@/hooks/useSettingsData';
|
||||
|
||||
interface ServerCardProps {
|
||||
server: Server
|
||||
onRemove: (serverName: string) => void
|
||||
onEdit: (server: Server) => void
|
||||
onToggle?: (server: Server, enabled: boolean) => Promise<boolean>
|
||||
onRefresh?: () => void
|
||||
server: Server;
|
||||
onRemove: (serverName: string) => void;
|
||||
onEdit: (server: Server) => void;
|
||||
onToggle?: (server: Server, enabled: boolean) => Promise<boolean>;
|
||||
onRefresh?: () => void;
|
||||
}
|
||||
|
||||
const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { showToast } = useToast()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [isToggling, setIsToggling] = useState(false)
|
||||
const [showErrorPopover, setShowErrorPopover] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const errorPopoverRef = useRef<HTMLDivElement>(null)
|
||||
const { t } = useTranslation();
|
||||
const { showToast } = useToast();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [isToggling, setIsToggling] = useState(false);
|
||||
const [showErrorPopover, setShowErrorPopover] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const errorPopoverRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (errorPopoverRef.current && !errorPopoverRef.current.contains(event.target as Node)) {
|
||||
setShowErrorPopover(false)
|
||||
setShowErrorPopover(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { exportMCPSettings } = useSettingsData()
|
||||
const { exportMCPSettings } = useSettingsData();
|
||||
|
||||
const handleRemove = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setShowDeleteDialog(true)
|
||||
}
|
||||
e.stopPropagation();
|
||||
setShowDeleteDialog(true);
|
||||
};
|
||||
|
||||
const handleEdit = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onEdit(server)
|
||||
}
|
||||
e.stopPropagation();
|
||||
onEdit(server);
|
||||
};
|
||||
|
||||
const handleToggle = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (isToggling || !onToggle) return
|
||||
e.stopPropagation();
|
||||
if (isToggling || !onToggle) return;
|
||||
|
||||
setIsToggling(true)
|
||||
setIsToggling(true);
|
||||
try {
|
||||
await onToggle(server, !(server.enabled !== false))
|
||||
await onToggle(server, !(server.enabled !== false));
|
||||
} finally {
|
||||
setIsToggling(false)
|
||||
setIsToggling(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleErrorIconClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setShowErrorPopover(!showErrorPopover)
|
||||
}
|
||||
e.stopPropagation();
|
||||
setShowErrorPopover(!showErrorPopover);
|
||||
};
|
||||
|
||||
const copyToClipboard = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!server.error) return
|
||||
e.stopPropagation();
|
||||
if (!server.error) return;
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(server.error).then(() => {
|
||||
setCopied(true)
|
||||
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
setCopied(true);
|
||||
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success');
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
} else {
|
||||
// Fallback for HTTP or unsupported clipboard API
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = server.error
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = server.error;
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.position = 'fixed'
|
||||
textArea.style.left = '-9999px'
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-9999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
setCopied(true)
|
||||
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
document.execCommand('copy');
|
||||
setCopied(true);
|
||||
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success');
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
showToast(t('common.copyFailed') || 'Copy failed', 'error')
|
||||
console.error('Copy to clipboard failed:', err)
|
||||
showToast(t('common.copyFailed') || 'Copy failed', 'error');
|
||||
console.error('Copy to clipboard failed:', err);
|
||||
}
|
||||
document.body.removeChild(textArea)
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyServerConfig = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
e.stopPropagation();
|
||||
try {
|
||||
const result = await exportMCPSettings(server.name)
|
||||
const configJson = JSON.stringify(result.data, null, 2)
|
||||
const result = await exportMCPSettings(server.name);
|
||||
const configJson = JSON.stringify(result.data, null, 2);
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(configJson)
|
||||
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
|
||||
await navigator.clipboard.writeText(configJson);
|
||||
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success');
|
||||
} else {
|
||||
// Fallback for HTTP or unsupported clipboard API
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = configJson
|
||||
textArea.style.position = 'fixed'
|
||||
textArea.style.left = '-9999px'
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = configJson;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-9999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
|
||||
document.execCommand('copy');
|
||||
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success');
|
||||
} catch (err) {
|
||||
showToast(t('common.copyFailed') || 'Copy failed', 'error')
|
||||
console.error('Copy to clipboard failed:', err)
|
||||
showToast(t('common.copyFailed') || 'Copy failed', 'error');
|
||||
console.error('Copy to clipboard failed:', err);
|
||||
}
|
||||
document.body.removeChild(textArea)
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error copying server configuration:', error)
|
||||
showToast(t('common.copyFailed') || 'Copy failed', 'error')
|
||||
console.error('Error copying server configuration:', error);
|
||||
showToast(t('common.copyFailed') || 'Copy failed', 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
onRemove(server.name)
|
||||
setShowDeleteDialog(false)
|
||||
}
|
||||
onRemove(server.name);
|
||||
setShowDeleteDialog(false);
|
||||
};
|
||||
|
||||
const handleToolToggle = async (toolName: string, enabled: boolean) => {
|
||||
try {
|
||||
const { toggleTool } = await import('@/services/toolService')
|
||||
const result = await toggleTool(server.name, toolName, enabled)
|
||||
const { toggleTool } = await import('@/services/toolService');
|
||||
const result = await toggleTool(server.name, toolName, enabled);
|
||||
if (result.success) {
|
||||
showToast(
|
||||
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: toolName }),
|
||||
'success',
|
||||
)
|
||||
);
|
||||
// Trigger refresh to update the tool's state in the UI
|
||||
if (onRefresh) {
|
||||
onRefresh()
|
||||
onRefresh();
|
||||
}
|
||||
} else {
|
||||
showToast(result.error || t('tool.toggleFailed'), 'error')
|
||||
showToast(result.error || t('tool.toggleFailed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling tool:', error)
|
||||
showToast(t('tool.toggleFailed'), 'error')
|
||||
console.error('Error toggling tool:', error);
|
||||
showToast(t('tool.toggleFailed'), 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePromptToggle = async (promptName: string, enabled: boolean) => {
|
||||
try {
|
||||
const { togglePrompt } = await import('@/services/promptService')
|
||||
const result = await togglePrompt(server.name, promptName, enabled)
|
||||
const { togglePrompt } = await import('@/services/promptService');
|
||||
const result = await togglePrompt(server.name, promptName, enabled);
|
||||
if (result.success) {
|
||||
showToast(
|
||||
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: promptName }),
|
||||
'success',
|
||||
)
|
||||
);
|
||||
// Trigger refresh to update the prompt's state in the UI
|
||||
if (onRefresh) {
|
||||
onRefresh()
|
||||
onRefresh();
|
||||
}
|
||||
} else {
|
||||
showToast(result.error || t('tool.toggleFailed'), 'error')
|
||||
showToast(result.error || t('tool.toggleFailed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling prompt:', error)
|
||||
showToast(t('tool.toggleFailed'), 'error')
|
||||
console.error('Error toggling prompt:', error);
|
||||
showToast(t('tool.toggleFailed'), 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOAuthAuthorization = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
// Open the OAuth authorization URL in a new window
|
||||
if (server.oauth?.authorizationUrl) {
|
||||
const width = 600;
|
||||
const height = 700;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
|
||||
window.open(
|
||||
server.oauth.authorizationUrl,
|
||||
'OAuth Authorization',
|
||||
`width=${width},height=${height},left=${left},top=${top}`,
|
||||
);
|
||||
|
||||
showToast(t('status.oauthWindowOpened'), 'info');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -199,7 +218,7 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
||||
>
|
||||
{server.name}
|
||||
</h2>
|
||||
<StatusBadge status={server.status} />
|
||||
<StatusBadge status={server.status} onAuthClick={handleOAuthAuthorization} />
|
||||
|
||||
{/* Tool count display */}
|
||||
<div className="flex items-center px-2 py-1 bg-blue-50 text-blue-700 rounded-full text-sm btn-primary">
|
||||
@@ -269,8 +288,8 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowErrorPopover(false)
|
||||
e.stopPropagation();
|
||||
setShowErrorPopover(false);
|
||||
}}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
@@ -380,7 +399,7 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
||||
serverName={server.name}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerCard
|
||||
export default ServerCard;
|
||||
|
||||
@@ -42,6 +42,20 @@ const ServerForm = ({
|
||||
}));
|
||||
};
|
||||
|
||||
const getInitialOAuthConfig = (data: Server | null): ServerFormData['oauth'] => {
|
||||
const oauth = data?.config?.oauth;
|
||||
return {
|
||||
clientId: oauth?.clientId || '',
|
||||
clientSecret: oauth?.clientSecret || '',
|
||||
scopes: oauth?.scopes ? oauth.scopes.join(' ') : '',
|
||||
accessToken: oauth?.accessToken || '',
|
||||
refreshToken: oauth?.refreshToken || '',
|
||||
authorizationEndpoint: oauth?.authorizationEndpoint || '',
|
||||
tokenEndpoint: oauth?.tokenEndpoint || '',
|
||||
resource: oauth?.resource || '',
|
||||
};
|
||||
};
|
||||
|
||||
const [serverType, setServerType] = useState<'stdio' | 'sse' | 'streamable-http' | 'openapi'>(
|
||||
getInitialServerType(),
|
||||
);
|
||||
@@ -80,6 +94,7 @@ const ServerForm = ({
|
||||
initialData.config.options.maxTotalTimeout) ||
|
||||
undefined,
|
||||
},
|
||||
oauth: getInitialOAuthConfig(initialData),
|
||||
// OpenAPI configuration initialization
|
||||
openapi:
|
||||
initialData && initialData.config && initialData.config.openapi
|
||||
@@ -135,6 +150,7 @@ const ServerForm = ({
|
||||
);
|
||||
|
||||
const [isRequestOptionsExpanded, setIsRequestOptionsExpanded] = useState<boolean>(false);
|
||||
const [isOAuthSectionExpanded, setIsOAuthSectionExpanded] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const isEdit = !!initialData;
|
||||
|
||||
@@ -186,6 +202,19 @@ const ServerForm = ({
|
||||
setHeaderVars(newHeaderVars);
|
||||
};
|
||||
|
||||
const handleOAuthChange = <K extends keyof NonNullable<ServerFormData['oauth']>>(
|
||||
field: K,
|
||||
value: string,
|
||||
) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
oauth: {
|
||||
...(prev.oauth || {}),
|
||||
[field]: value,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
// Handle options changes
|
||||
const handleOptionsChange = (
|
||||
field: 'timeout' | 'resetTimeoutOnProgress' | 'maxTotalTimeout',
|
||||
@@ -232,6 +261,42 @@ const ServerForm = ({
|
||||
options.maxTotalTimeout = formData.options.maxTotalTimeout;
|
||||
}
|
||||
|
||||
const oauthConfig = (() => {
|
||||
if (!formData.oauth) return undefined;
|
||||
const {
|
||||
clientId,
|
||||
clientSecret,
|
||||
scopes,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
resource,
|
||||
} = formData.oauth;
|
||||
|
||||
const oauth: Record<string, unknown> = {};
|
||||
if (clientId && clientId.trim()) oauth.clientId = clientId.trim();
|
||||
if (clientSecret && clientSecret.trim()) oauth.clientSecret = clientSecret.trim();
|
||||
if (scopes && scopes.trim()) {
|
||||
const parsedScopes = scopes
|
||||
.split(/[\s,]+/)
|
||||
.map((scope) => scope.trim())
|
||||
.filter((scope) => scope.length > 0);
|
||||
if (parsedScopes.length > 0) {
|
||||
oauth.scopes = parsedScopes;
|
||||
}
|
||||
}
|
||||
if (accessToken && accessToken.trim()) oauth.accessToken = accessToken.trim();
|
||||
if (refreshToken && refreshToken.trim()) oauth.refreshToken = refreshToken.trim();
|
||||
if (authorizationEndpoint && authorizationEndpoint.trim()) {
|
||||
oauth.authorizationEndpoint = authorizationEndpoint.trim();
|
||||
}
|
||||
if (tokenEndpoint && tokenEndpoint.trim()) oauth.tokenEndpoint = tokenEndpoint.trim();
|
||||
if (resource && resource.trim()) oauth.resource = resource.trim();
|
||||
|
||||
return Object.keys(oauth).length > 0 ? oauth : undefined;
|
||||
})();
|
||||
|
||||
const payload = {
|
||||
name: formData.name,
|
||||
config: {
|
||||
@@ -304,6 +369,7 @@ const ServerForm = ({
|
||||
? {
|
||||
url: formData.url,
|
||||
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
||||
...(oauthConfig ? { oauth: oauthConfig } : {}),
|
||||
}
|
||||
: {
|
||||
command: formData.command,
|
||||
@@ -896,6 +962,132 @@ const ServerForm = ({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer bg-gray-50 hover:bg-gray-100 p-3 rounded border border-gray-200"
|
||||
onClick={() => setIsOAuthSectionExpanded(!isOAuthSectionExpanded)}
|
||||
>
|
||||
<label className="text-gray-700 text-sm font-bold">
|
||||
{t('server.oauth.sectionTitle')}
|
||||
</label>
|
||||
<span className="text-gray-500 text-sm">{isOAuthSectionExpanded ? '▼' : '▶'}</span>
|
||||
</div>
|
||||
|
||||
{isOAuthSectionExpanded && (
|
||||
<div className="border border-gray-200 rounded-b p-4 bg-gray-50 border-t-0">
|
||||
<p className="text-xs text-gray-500 mb-3">
|
||||
{t('server.oauth.sectionDescription')}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.oauth.clientId')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.oauth?.clientId || ''}
|
||||
onChange={(e) => handleOAuthChange('clientId', e.target.value)}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="client id"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.oauth.clientSecret')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.oauth?.clientSecret || ''}
|
||||
onChange={(e) => handleOAuthChange('clientSecret', e.target.value)}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="client secret"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
{/*
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.oauth.authorizationEndpoint')}
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={formData.oauth?.authorizationEndpoint || ''}
|
||||
onChange={(e) => handleOAuthChange('authorizationEndpoint', e.target.value)}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="https://auth.example.com/authorize"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.oauth.tokenEndpoint')}
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={formData.oauth?.tokenEndpoint || ''}
|
||||
onChange={(e) => handleOAuthChange('tokenEndpoint', e.target.value)}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="https://auth.example.com/token"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.oauth.scopes')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.oauth?.scopes || ''}
|
||||
onChange={(e) => handleOAuthChange('scopes', e.target.value)}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder={t('server.oauth.scopesPlaceholder')}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.oauth.resource')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.oauth?.resource || ''}
|
||||
onChange={(e) => handleOAuthChange('resource', e.target.value)}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="https://mcp.example.com/mcp"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.oauth.accessToken')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.oauth?.accessToken || ''}
|
||||
onChange={(e) => handleOAuthChange('accessToken', e.target.value)}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="access-token"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.oauth.refreshToken')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.oauth?.refreshToken || ''}
|
||||
onChange={(e) => handleOAuthChange('refreshToken', e.target.value)}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="refresh-token"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -13,24 +13,21 @@ type BadgeProps = {
|
||||
|
||||
const badgeVariants = {
|
||||
default: 'bg-blue-500 text-white hover:bg-blue-600',
|
||||
secondary: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600',
|
||||
outline: 'bg-transparent border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800',
|
||||
secondary:
|
||||
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600',
|
||||
outline:
|
||||
'bg-transparent border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800',
|
||||
destructive: 'bg-red-500 text-white hover:bg-red-600',
|
||||
};
|
||||
|
||||
export function Badge({
|
||||
children,
|
||||
variant = 'default',
|
||||
className,
|
||||
onClick
|
||||
}: BadgeProps) {
|
||||
export function Badge({ children, variant = 'default', className, onClick }: BadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors',
|
||||
badgeVariants[variant],
|
||||
onClick ? 'cursor-pointer' : '',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
@@ -40,27 +37,40 @@ export function Badge({
|
||||
}
|
||||
|
||||
// For backward compatibility with existing code
|
||||
export const StatusBadge = ({ status }: { status: 'connected' | 'disconnected' | 'connecting' }) => {
|
||||
export const StatusBadge = ({
|
||||
status,
|
||||
onAuthClick,
|
||||
}: {
|
||||
status: 'connected' | 'disconnected' | 'connecting' | 'oauth_required';
|
||||
onAuthClick?: (e: React.MouseEvent) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const colors = {
|
||||
connecting: 'status-badge-connecting',
|
||||
connected: 'status-badge-online',
|
||||
disconnected: 'status-badge-offline',
|
||||
oauth_required: 'status-badge-oauth-required',
|
||||
};
|
||||
|
||||
// Map status to translation keys
|
||||
const statusTranslations = {
|
||||
connected: 'status.online',
|
||||
disconnected: 'status.offline',
|
||||
connecting: 'status.connecting'
|
||||
connecting: 'status.connecting',
|
||||
oauth_required: 'status.oauthRequired',
|
||||
};
|
||||
|
||||
const isOAuthRequired = status === 'oauth_required';
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${colors[status]}`}
|
||||
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${colors[status]} ${isOAuthRequired && onAuthClick ? 'cursor-pointer hover:opacity-80' : ''}`}
|
||||
onClick={isOAuthRequired && onAuthClick ? (e) => onAuthClick(e) : undefined}
|
||||
title={isOAuthRequired ? t('status.clickToAuthorize') : undefined}
|
||||
>
|
||||
{isOAuthRequired && '🔐 '}
|
||||
{t(statusTranslations[status] || status)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -144,6 +144,18 @@ body {
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
}
|
||||
|
||||
.status-badge-oauth-required {
|
||||
background-color: white !important;
|
||||
color: rgba(156, 39, 176, 0.9) !important;
|
||||
border: 1px solid #ba68c8;
|
||||
}
|
||||
|
||||
.dark .status-badge-oauth-required {
|
||||
background-color: rgba(156, 39, 176, 0.15) !important;
|
||||
color: rgba(186, 104, 200, 0.9) !important;
|
||||
border: 1px solid rgba(156, 39, 176, 0.3);
|
||||
}
|
||||
|
||||
/* Enhanced status icons for dark theme */
|
||||
.dark .status-icon-blue {
|
||||
background-color: rgba(59, 130, 246, 0.15) !important;
|
||||
|
||||
@@ -12,14 +12,16 @@ const DashboardPage: React.FC = () => {
|
||||
total: servers.length,
|
||||
online: servers.filter((server: Server) => server.status === 'connected').length,
|
||||
offline: servers.filter((server: Server) => server.status === 'disconnected').length,
|
||||
connecting: servers.filter((server: Server) => server.status === 'connecting').length
|
||||
connecting: servers.filter((server: Server) => server.status === 'connecting').length,
|
||||
oauthRequired: servers.filter((server: Server) => server.status === 'oauth_required').length,
|
||||
};
|
||||
|
||||
// Map status to translation keys
|
||||
const statusTranslations: Record<string, string> = {
|
||||
connected: 'status.online',
|
||||
disconnected: 'status.offline',
|
||||
connecting: 'status.connecting'
|
||||
connecting: 'status.connecting',
|
||||
oauth_required: 'status.oauthRequired',
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -38,8 +40,17 @@ const DashboardPage: React.FC = () => {
|
||||
className="ml-4 text-gray-500 hover:text-gray-700 transition-colors duration-200"
|
||||
aria-label={t('app.closeButton')}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 011.414 0L10 8.586l4.293-4.293a1 1 111.414 1.414L11.414 10l4.293 4.293a1 1 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 01-1.414-1.414L8.586 10 4.293 5.707a1 1 010-1.414z" clipRule="evenodd" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 011.414 0L10 8.586l4.293-4.293a1 1 111.414 1.414L11.414 10l4.293 4.293a1 1 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 01-1.414-1.414L8.586 10 4.293 5.707a1 1 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -49,9 +60,25 @@ const DashboardPage: React.FC = () => {
|
||||
{isLoading && (
|
||||
<div className="bg-white shadow rounded-lg p-6 flex items-center justify-center loading-container">
|
||||
<div className="flex flex-col items-center">
|
||||
<svg className="animate-spin h-10 w-10 text-blue-500 mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
<svg
|
||||
className="animate-spin h-10 w-10 text-blue-500 mb-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<p className="text-gray-600">{t('app.loading')}</p>
|
||||
</div>
|
||||
@@ -64,12 +91,25 @@ const DashboardPage: React.FC = () => {
|
||||
<div className="bg-white rounded-lg shadow p-6 dashboard-card">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-blue-100 text-blue-800 icon-container status-icon-blue">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h2 className="text-xl font-semibold text-gray-700">{t('pages.dashboard.totalServers')}</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-700">
|
||||
{t('pages.dashboard.totalServers')}
|
||||
</h2>
|
||||
<p className="text-3xl font-bold text-gray-900">{serverStats.total}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,12 +119,25 @@ const DashboardPage: React.FC = () => {
|
||||
<div className="bg-white rounded-lg shadow p-6 dashboard-card">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-green-100 text-green-800 icon-container status-icon-green">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h2 className="text-xl font-semibold text-gray-700">{t('pages.dashboard.onlineServers')}</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-700">
|
||||
{t('pages.dashboard.onlineServers')}
|
||||
</h2>
|
||||
<p className="text-3xl font-bold text-gray-900">{serverStats.online}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,12 +147,25 @@ const DashboardPage: React.FC = () => {
|
||||
<div className="bg-white rounded-lg shadow p-6 dashboard-card">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-red-100 text-red-800 icon-container status-icon-red">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h2 className="text-xl font-semibold text-gray-700">{t('pages.dashboard.offlineServers')}</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-700">
|
||||
{t('pages.dashboard.offlineServers')}
|
||||
</h2>
|
||||
<p className="text-3xl font-bold text-gray-900">{serverStats.offline}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,16 +175,28 @@ const DashboardPage: React.FC = () => {
|
||||
<div className="bg-white rounded-lg shadow p-6 dashboard-card">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-yellow-100 text-yellow-800 icon-container status-icon-yellow">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h2 className="text-xl font-semibold text-gray-700">{t('pages.dashboard.connectingServers')}</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-700">
|
||||
{t('pages.dashboard.connectingServers')}
|
||||
</h2>
|
||||
<p className="text-3xl font-bold text-gray-900">{serverStats.connecting}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -126,24 +204,41 @@ const DashboardPage: React.FC = () => {
|
||||
{/* Recent activity list */}
|
||||
{servers.length > 0 && !isLoading && (
|
||||
<div className="mt-8">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">{t('pages.dashboard.recentServers')}</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">
|
||||
{t('pages.dashboard.recentServers')}
|
||||
</h2>
|
||||
<div className="bg-white shadow rounded-lg overflow-hidden table-container">
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
{t('server.name')}
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
{t('server.status')}
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
{t('server.tools')}
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
{t('server.prompts')}
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
{t('server.enabled')}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -155,12 +250,18 @@ const DashboardPage: React.FC = () => {
|
||||
{server.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<span className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${server.status === 'connected'
|
||||
? 'status-badge-online'
|
||||
: server.status === 'disconnected'
|
||||
? 'status-badge-offline'
|
||||
: 'status-badge-connecting'
|
||||
}`}>
|
||||
<span
|
||||
className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${
|
||||
server.status === 'connected'
|
||||
? 'status-badge-online'
|
||||
: server.status === 'disconnected'
|
||||
? 'status-badge-offline'
|
||||
: server.status === 'oauth_required'
|
||||
? 'status-badge-oauth-required'
|
||||
: 'status-badge-connecting'
|
||||
}`}
|
||||
>
|
||||
{server.status === 'oauth_required' && '🔐 '}
|
||||
{t(statusTranslations[server.status] || server.status)}
|
||||
</span>
|
||||
</td>
|
||||
@@ -188,4 +289,4 @@ const DashboardPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardPage;
|
||||
export default DashboardPage;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Server status types
|
||||
export type ServerStatus = 'connecting' | 'connected' | 'disconnected';
|
||||
export type ServerStatus = 'connecting' | 'connected' | 'disconnected' | 'oauth_required';
|
||||
|
||||
// Market server types
|
||||
export interface MarketServerRepository {
|
||||
@@ -121,6 +121,43 @@ export interface ServerConfig {
|
||||
resetTimeoutOnProgress?: boolean; // Reset timeout on progress notifications
|
||||
maxTotalTimeout?: number; // Maximum total timeout in milliseconds
|
||||
}; // MCP request options configuration
|
||||
// OAuth authentication for upstream MCP servers
|
||||
oauth?: {
|
||||
clientId?: string; // OAuth client ID
|
||||
clientSecret?: string; // OAuth client secret
|
||||
scopes?: string[]; // Required OAuth scopes
|
||||
accessToken?: string; // Pre-obtained access token (if available)
|
||||
refreshToken?: string; // Refresh token for renewing access
|
||||
dynamicRegistration?: {
|
||||
enabled?: boolean; // Enable/disable dynamic registration
|
||||
issuer?: string; // OAuth issuer URL for discovery
|
||||
registrationEndpoint?: string; // Direct registration endpoint URL
|
||||
metadata?: {
|
||||
client_name?: string;
|
||||
client_uri?: string;
|
||||
logo_uri?: string;
|
||||
scope?: string;
|
||||
redirect_uris?: string[];
|
||||
grant_types?: string[];
|
||||
response_types?: string[];
|
||||
token_endpoint_auth_method?: string;
|
||||
contacts?: string[];
|
||||
software_id?: string;
|
||||
software_version?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
initialAccessToken?: string;
|
||||
};
|
||||
resource?: string; // OAuth resource parameter (RFC8707)
|
||||
authorizationEndpoint?: string; // Authorization endpoint (authorization code flow)
|
||||
tokenEndpoint?: string; // Token endpoint for exchanging authorization codes for tokens
|
||||
pendingAuthorization?: {
|
||||
authorizationUrl?: string;
|
||||
state?: string;
|
||||
codeVerifier?: string;
|
||||
createdAt?: number;
|
||||
};
|
||||
};
|
||||
// OpenAPI specific configuration
|
||||
openapi?: {
|
||||
url?: string; // OpenAPI specification URL
|
||||
@@ -172,6 +209,10 @@ export interface Server {
|
||||
prompts?: Prompt[];
|
||||
config?: ServerConfig;
|
||||
enabled?: boolean;
|
||||
oauth?: {
|
||||
authorizationUrl?: string;
|
||||
state?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Group types
|
||||
@@ -209,6 +250,16 @@ export interface ServerFormData {
|
||||
resetTimeoutOnProgress?: boolean;
|
||||
maxTotalTimeout?: number;
|
||||
};
|
||||
oauth?: {
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
scopes?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
authorizationEndpoint?: string;
|
||||
tokenEndpoint?: string;
|
||||
resource?: string;
|
||||
};
|
||||
// OpenAPI specific fields
|
||||
openapi?: {
|
||||
url?: string;
|
||||
|
||||
Reference in New Issue
Block a user